Skip to content

Commit fd52a3c

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 fd52a3c

File tree

4 files changed

+161
-14
lines changed

4 files changed

+161
-14
lines changed

LiteCore/Logging/LogDecoder.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ namespace litecore {
3131
unsigned microsecs;
3232
};
3333

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

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

LiteCore/Logging/MultiLogDecoder.hh

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

1313
#pragma once
1414
#include "LogDecoder.hh"
15+
#include "TextLogDecoder.hh"
1516
#include <algorithm>
1617
#include <climits>
1718
#include <fstream>
@@ -27,22 +28,20 @@ namespace litecore {
2728
class MultiLogDecoder : public LogIterator {
2829
public:
2930
MultiLogDecoder() {
30-
_startTime = {UINT_MAX, 0};
31-
for ( unsigned i = 0; i <= kMaxLevel; i++ ) _startTimeByLevel[i] = {UINT_MAX, 0};
31+
_startTime = kMaxTimestamp;
32+
for ( unsigned i = 0; i <= kMaxLevel; i++ ) _startTimeByLevel[i] = kMaxTimestamp;
3233
}
3334

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

4240
auto startTime = log->startTime();
4341
_startTime = std::min(_startTime, startTime);
44-
auto level = log->level();
45-
if ( level >= 0 && level <= kMaxLevel )
42+
if ( !log->next() ) return;
43+
_logs.push(log);
44+
if ( auto level = log->level(); level >= 0 && level <= kMaxLevel )
4645
_startTimeByLevel[level] = std::min(_startTimeByLevel[level], startTime);
4746
}
4847

@@ -52,19 +51,28 @@ namespace litecore {
5251
if ( !in ) return false;
5352
in.exceptions(std::ifstream::badbit);
5453
_inputs.push_back(std::move(in));
55-
LogDecoder decoder(_inputs.back());
54+
std::unique_ptr<LogIterator> decoder;
55+
if ( TextLogDecoder::looksTextual(_inputs.back()) )
56+
decoder = std::make_unique<TextLogDecoder>(_inputs.back());
57+
else
58+
decoder = std::make_unique<LogDecoder>(_inputs.back());
5659
_decoders.push_back(std::move(decoder));
57-
add(&_decoders.back());
60+
add(_decoders.back().get());
5861
return true;
5962
}
6063

6164
/// Time when the earliest log began
6265
[[nodiscard]] Timestamp startTime() const override { return _startTime; }
6366

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

@@ -116,10 +124,10 @@ namespace litecore {
116124
std::priority_queue<LogIterator*, std::vector<LogIterator*>, logcmp> _logs;
117125
LogIterator* _current{nullptr};
118126
Timestamp _startTime{};
119-
Timestamp _startTimeByLevel[kMaxLevel + 1]{};
127+
std::array<Timestamp, kMaxLevel + 1> _startTimeByLevel{};
120128

121-
std::deque<LogDecoder> _decoders;
122-
std::deque<std::ifstream> _inputs;
129+
std::vector<std::unique_ptr<LogIterator>> _decoders;
130+
std::deque<std::ifstream> _inputs;
123131
};
124132

125133

LiteCore/Logging/TextLogDecoder.hh

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