Skip to content

Commit 7a46221

Browse files
committed
Support for decoding textual log files
This is for use by the cbl-log and cblite tools. - New class TextLogDecoder parses LiteCore text logs the same way as LogDecode does for binary logs. - MultiLogDecoder recognizes when a log file is text, and opens it using a TextLogDecoder. - Fixed a few minor bugs in MultiLogDecoder. - Added a few more ANSI escape codes to Tool.hh
1 parent 742f556 commit 7a46221

File tree

4 files changed

+171
-14
lines changed

4 files changed

+171
-14
lines changed

LiteCore/Logging/LogDecoder.hh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <map>
1717
#include <optional>
1818
#include <cstdint>
19+
#include <limits>
1920
#include <string>
2021
#include <vector>
2122

@@ -31,6 +32,9 @@ namespace litecore {
3132
unsigned microsecs;
3233
};
3334

35+
static constexpr Timestamp kMinTimestamp{0, 0};
36+
static constexpr Timestamp kMaxTimestamp{std::numeric_limits<time_t>::max(), 999999};
37+
3438
virtual ~LogIterator() = default;
3539

3640
/** Decodes the entire log and writes it to the output stream, with timestamps.

LiteCore/Logging/MultiLogDecoder.hh

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212

1313
#pragma once
1414
#include "LogDecoder.hh"
15+
#include "TextLogDecoder.hh"
1516
#include <algorithm>
17+
#include <array>
1618
#include <climits>
1719
#include <fstream>
1820
#include <queue>
@@ -27,22 +29,20 @@ namespace litecore {
2729
class MultiLogDecoder : public LogIterator {
2830
public:
2931
MultiLogDecoder() {
30-
_startTime = {UINT_MAX, 0};
31-
for ( unsigned i = 0; i <= kMaxLevel; i++ ) _startTimeByLevel[i] = {UINT_MAX, 0};
32+
_startTime = kMaxTimestamp;
33+
for ( unsigned i = 0; i <= kMaxLevel; i++ ) _startTimeByLevel[i] = kMaxTimestamp;
3234
}
3335

3436
/// Adds a log iterator. Must be called before calling \ref next().
3537
/// The iterator is assumed to be at its start, so its \ref next() will be called first.
3638
void add(LogIterator* log) {
3739
assert(!_current);
38-
if ( !log->next() ) return;
39-
40-
_logs.push(log);
4140

4241
auto startTime = log->startTime();
4342
_startTime = std::min(_startTime, startTime);
44-
auto level = log->level();
45-
if ( level >= 0 && level <= kMaxLevel )
43+
if ( !log->next() ) return;
44+
_logs.push(log);
45+
if ( auto level = log->level(); level >= 0 && level <= kMaxLevel )
4646
_startTimeByLevel[level] = std::min(_startTimeByLevel[level], startTime);
4747
}
4848

@@ -52,19 +52,28 @@ namespace litecore {
5252
if ( !in ) return false;
5353
in.exceptions(std::ifstream::badbit);
5454
_inputs.push_back(std::move(in));
55-
LogDecoder decoder(_inputs.back());
55+
std::unique_ptr<LogIterator> decoder;
56+
if ( TextLogDecoder::looksTextual(_inputs.back()) )
57+
decoder = std::make_unique<TextLogDecoder>(_inputs.back());
58+
else
59+
decoder = std::make_unique<LogDecoder>(_inputs.back());
5660
_decoders.push_back(std::move(decoder));
57-
add(&_decoders.back());
61+
add(_decoders.back().get());
5862
return true;
5963
}
6064

6165
/// Time when the earliest log began
6266
[[nodiscard]] Timestamp startTime() const override { return _startTime; }
6367

68+
/// Time that earliest logs at `level` begin, or kMaxTimestamp if none.
69+
[[nodiscard]] Timestamp startTimeOfLevel(unsigned level) const { return _startTimeByLevel[level]; }
70+
6471
/// First time when logs of all levels are available
6572
[[nodiscard]] Timestamp fullStartTime() const {
66-
Timestamp fullStartTime = {0, 0};
67-
for ( unsigned i = 0; i <= kMaxLevel; i++ ) fullStartTime = std::max(fullStartTime, _startTimeByLevel[i]);
73+
Timestamp fullStartTime = kMinTimestamp;
74+
for ( auto& ts : _startTimeByLevel ) {
75+
if ( fullStartTime < ts && ts != kMaxTimestamp ) fullStartTime = ts;
76+
}
6877
return fullStartTime;
6978
}
7079

@@ -116,10 +125,10 @@ namespace litecore {
116125
std::priority_queue<LogIterator*, std::vector<LogIterator*>, logcmp> _logs;
117126
LogIterator* _current{nullptr};
118127
Timestamp _startTime{};
119-
Timestamp _startTimeByLevel[kMaxLevel + 1]{};
128+
std::array<Timestamp, kMaxLevel + 1> _startTimeByLevel{};
120129

121-
std::deque<LogDecoder> _decoders;
122-
std::deque<std::ifstream> _inputs;
130+
std::vector<std::unique_ptr<LogIterator>> _decoders;
131+
std::deque<std::ifstream> _inputs;
123132
};
124133

125134

LiteCore/Logging/TextLogDecoder.hh

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// TextLogDecoder.hh
3+
//
4+
// Copyright 2025-Present Couchbase, Inc.
5+
//
6+
// Use of this software is governed by the Business Source License included
7+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
8+
// in that file, in accordance with the Business Source License, use of this
9+
// software will be governed by the Apache License, Version 2.0, included in
10+
// the file licenses/APL2.txt.
11+
//
12+
13+
#pragma once
14+
#include "LogDecoder.hh"
15+
#include "ParseDate.hh"
16+
#include <istream>
17+
#include <regex>
18+
#include <stdexcept>
19+
20+
namespace litecore {
21+
22+
/** Parses LiteCore-generated textual log files. */
23+
class TextLogDecoder : public LogIterator {
24+
public:
25+
/** Returns true if the stream `in` appears to contain textual log data. */
26+
static bool looksTextual(std::istream& in) {
27+
auto pos = in.tellg();
28+
char chars[27] = {};
29+
bool result = true;
30+
in.read((char*)chars, sizeof(chars));
31+
in.seekg(pos);
32+
return looksLikeLogLine(std::string_view(chars, std::size(chars)));
33+
}
34+
35+
/** Initializes decoder with a stream written by LiteCore's textual log encoder. */
36+
explicit TextLogDecoder(std::istream& in) : _in(in) {
37+
_in.exceptions(std::istream::badbit);
38+
if ( next() && next() ) // Read header line to get the initial timestamp
39+
_startTime = _curTimestamp;
40+
}
41+
42+
bool next() override {
43+
if ( _line.empty() ) {
44+
// Read next line if there's not one in the buffer:
45+
if ( !_in || _in.peek() < 0 ) return false;
46+
std::getline(_in, _line);
47+
if ( _line.empty() ) return false;
48+
}
49+
50+
// Example: 2025-12-09T06:47:55.507699Z WS Verbose Obj=/JRepl@1175308903/…/ Received 58-byte message
51+
52+
std::string_view rest(_line);
53+
auto nextColumn = [&] {
54+
auto next = rest.find(' ');
55+
std::string_view column = rest.substr(0, next);
56+
rest = rest.substr(next + 1);
57+
return column;
58+
};
59+
60+
auto timestamp = nextColumn();
61+
auto micros = std::stoul(std::string(timestamp.substr(timestamp.size() - 7, 6)));
62+
auto millis = fleece::ParseISO8601Date(timestamp);
63+
if ( millis == kInvalidDate || millis < 0x19000000 || micros > 999999 )
64+
throw std::runtime_error("Could not parse timestamp in log line: " + _line);
65+
_curTimestamp = {millis / 1000, unsigned(micros)};
66+
67+
_curDomain = nextColumn();
68+
69+
auto levelStr = nextColumn();
70+
if ( auto i = std::ranges::find(kLevelNames, levelStr); i != std::end(kLevelNames) )
71+
_curLevel = i - std::begin(kLevelNames);
72+
else
73+
_curLevel = 0;
74+
75+
_curObject.clear();
76+
_curObjectID = 0;
77+
if ( rest.starts_with("Obj=/") ) {
78+
std::string_view obj = nextColumn();
79+
if ( auto size = obj.size(); size >= 13 && obj.ends_with('/') ) {
80+
if ( auto pos = obj.rfind('#'); pos != std::string::npos ) {
81+
_curObject = obj.substr(5, size - 6); // trim 'Obj=/' and '/ '
82+
_curObjectID = std::stoul(std::string(obj.substr(pos + 1, size - 2 - pos)));
83+
}
84+
}
85+
}
86+
87+
_curMessage = rest;
88+
89+
// Add any following non-log-format lines to the message:
90+
_line.clear();
91+
while ( _in && _in.peek() >= 0 ) {
92+
std::getline(_in, _line);
93+
if ( _line.empty() ) break;
94+
if ( looksLikeLogLine(_line) ) break;
95+
_curMessage += '\n';
96+
_curMessage += _line;
97+
_line.clear();
98+
}
99+
100+
return true;
101+
}
102+
103+
Timestamp startTime() const override { return _startTime; }
104+
105+
Timestamp timestamp() const override { return _curTimestamp; }
106+
107+
int8_t level() const override { return _curLevel; }
108+
109+
const std::string& domain() const override { return _curDomain; }
110+
111+
uint64_t objectID() const override { return _curObjectID; }
112+
113+
const std::string* objectDescription() const override { return &_curObject; }
114+
115+
void decodeMessageTo(std::ostream& out) override { out << _curMessage; }
116+
117+
private:
118+
static bool looksLikeLogLine(std::string_view line) {
119+
if ( line.size() < 27 ) return false;
120+
for ( uint8_t c : line.substr(0, 27) ) {
121+
if ( !isdigit(c) && c != '-' && c != ':' && c != '.' && c != 'Z' && c != 'T' ) return false;
122+
}
123+
return true;
124+
}
125+
126+
static constexpr std::string_view kLevelNames[] = {"Debug", "Verbose", "Info", "WARNING", "ERROR"};
127+
128+
std::istream& _in;
129+
Timestamp _startTime{};
130+
std::string _line;
131+
132+
Timestamp _curTimestamp;
133+
int8_t _curLevel;
134+
std::string _curDomain;
135+
std::string _curObject;
136+
uint64_t _curObjectID;
137+
std::string _curMessage;
138+
};
139+
140+
} // namespace litecore

tool_support/Tool.hh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,12 @@ class Tool {
182182

183183
std::string ansiItalic() { return ansi("3"); }
184184

185+
std::string ansiNoItalic() { return ansi("23"); }
186+
185187
std::string ansiUnderline() { return ansi("4"); }
186188

189+
std::string ansiNoUnderline() { return ansi("24"); }
190+
187191
std::string ansiRed() { return ansi("31"); }
188192

189193
std::string ansiReset() { return ansi("0"); }

0 commit comments

Comments
 (0)