source: ntrip/trunk/BNC/src/bnclog.cpp

Last change on this file was 11190, checked in by stuerze, 25 hours ago

Structured logging improved

File size: 9.0 KB
RevLine 
[11179]1/* -------------------------------------------------------------------------
2 * BKG NTRIP Client
3 * -------------------------------------------------------------------------
4 *
5 * Class: t_logRecord
6 *
7 * Purpose: Record of the structured logfile (JSON Lines or logfmt)
8 *
9 * Created: 04-Nov-2026
10 *
11 * Changes:
12 *
13 * -----------------------------------------------------------------------*/
14
15#include <cmath>
16
17#include <QMutex>
18#include <QSet>
19#include <QString>
20
21#include "bnclog.h"
22#include "bncutils.h"
[11187]23#include "bnctime.h"
[11179]24
25namespace {
26
[11185]27QMutex streamMutex;
28QSet<QByteArray> streams;
[11179]29
30// text as UTF-8 (BNC's messages are Latin-1)
31QByteArray utf8(const QByteArray& latin1) {
32 return QString::fromLatin1(latin1).toUtf8();
33}
34
35QByteArray jsonString(const QByteArray& latin1) {
36 QByteArray in = utf8(latin1);
37 QByteArray out = "\"";
38 for (char c : in) {
39 unsigned char u = static_cast<unsigned char>(c);
40 switch (c) {
41 case '"': out += "\\\""; break;
42 case '\\': out += "\\\\"; break;
43 case '\n': out += "\\n"; break;
44 case '\r': out += "\\r"; break;
45 case '\t': out += "\\t"; break;
46 default:
47 if (u < 0x20) {
48 out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
49 }
50 else {
51 out += c;
52 }
53 }
54 }
55 return out + "\"";
56}
57
58QByteArray logfmtValue(const QByteArray& latin1) {
59 QByteArray in = utf8(latin1);
60 bool quote = in.isEmpty();
61 for (char c : in) {
62 unsigned char u = static_cast<unsigned char>(c);
63 if (u <= 0x20 || c == '=' || c == '"' || c == '\\') {
64 quote = true;
65 break;
66 }
67 }
68 if (!quote) {
69 return in;
70 }
71 QByteArray out = "\"";
72 for (char c : in) {
73 unsigned char u = static_cast<unsigned char>(c);
74 switch (c) {
75 case '"': out += "\\\""; break;
76 case '\\': out += "\\\\"; break;
77 case '\n': out += "\\n"; break;
78 case '\r': out += "\\r"; break;
79 case '\t': out += "\\t"; break;
80 default:
81 if (u < 0x20) {
82 out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
83 }
84 else {
85 out += c;
86 }
87 }
88 }
89 return out + "\"";
90}
91
92bool containsWord(const QByteArray& lower, const char* word) {
93 return lower.contains(word);
94}
95
96}
97
98// Envelope of a plain text message
99////////////////////////////////////////////////////////////////////////////
[11185]100t_logRecord t_logRecord::fromMessage(const QByteArray& msg, const QByteArray& component) {
101 t_logRecord rec(info, component, "message", "", msg);
[11179]102
[11185]103 // stream: first token of the text, if it is a known stream or PPP station
[11179]104 int end = 0;
105 while (end < msg.size() && msg[end] != ':' && msg[end] != ' ' && msg[end] != '(') {
106 ++end;
107 }
108 if (end > 0) {
109 QByteArray token = msg.left(end);
[11185]110 QMutexLocker locker(&streamMutex);
111 if (streams.contains(token)) {
112 rec._stream = token;
[11179]113 }
114 }
115
116 // level from keywords
117 QByteArray lower = msg.toLower();
118 static const char* errorWords[] = {"error", "fail", "cannot", "can't", "could not",
119 "unable", "exception", 0};
120 static const char* warningWords[] = {"warning", "outage", "corrupt", "timeout",
121 "wrong", "invalid", "ignored", "refused",
122 "unauthorized", "not found", "missing",
123 "unknown", 0};
124 for (int ii = 0; errorWords[ii]; ii++) {
125 if (containsWord(lower, errorWords[ii])) {
126 rec._level = error;
127 return rec;
128 }
129 }
130 for (int ii = 0; warningWords[ii]; ii++) {
131 if (containsWord(lower, warningWords[ii])) {
132 rec._level = warning;
133 return rec;
134 }
135 }
136 return rec;
137}
138
[11185]139// Plain text message with an explicit level
140////////////////////////////////////////////////////////////////////////////
141t_logRecord t_logRecord::message(e_level level, const QByteArray& msg,
142 const QByteArray& component) {
143 t_logRecord rec = fromMessage(msg, component);
144 rec._level = level;
145 return rec;
146}
147
148// Message of a BNC_DEBUG_* build
149////////////////////////////////////////////////////////////////////////////
150t_logRecord t_logRecord::debugMessage(const QByteArray& component, const QByteArray& msg) {
151 t_logRecord rec = fromMessage(msg, component);
152 rec._level = debug;
153 return rec;
154}
155
[11179]156//
157////////////////////////////////////////////////////////////////////////////
158t_logRecord& t_logRecord::add(const char* key, double value, int precision) {
159 t_field ff;
160 ff._key = key;
161 ff._isString = false;
162 if (std::isfinite(value)) {
163 ff._value = QByteArray::number(value, 'f', precision); // C locale
164 }
165 else {
166 ff._value = "null";
167 }
168 _fields.append(ff);
169 return *this;
170}
171
172//
173////////////////////////////////////////////////////////////////////////////
174t_logRecord& t_logRecord::add(const char* key, int value) {
175 t_field ff;
176 ff._key = key;
177 ff._value = QByteArray::number(value);
178 ff._isString = false;
179 _fields.append(ff);
180 return *this;
181}
182
183//
184////////////////////////////////////////////////////////////////////////////
185t_logRecord& t_logRecord::add(const char* key, bool value) {
186 t_field ff;
187 ff._key = key;
188 ff._value = value ? "true" : "false";
189 ff._isString = false;
190 _fields.append(ff);
191 return *this;
192}
193
194//
195////////////////////////////////////////////////////////////////////////////
196t_logRecord& t_logRecord::add(const char* key, const QByteArray& value) {
197 t_field ff;
198 ff._key = key;
199 ff._value = value;
200 ff._isString = true;
201 _fields.append(ff);
202 return *this;
203}
204
205//
206////////////////////////////////////////////////////////////////////////////
207t_logRecord& t_logRecord::addTime(const char* key, const QDateTime& utc) {
208 QDateTime tt = utc.toUTC();
209 return add(key, tt.toString("yyyy-MM-ddThh:mm:ss.zzzZ").toLatin1());
210}
211
212//
213////////////////////////////////////////////////////////////////////////////
[11187]214t_logRecord& t_logRecord::setEpoch(const bncTime& epoch) {
215 if (epoch.valid()) {
216 _epoch = QByteArray((epoch.datestr() + 'T' + epoch.timestr(3)).c_str());
217 }
218 else {
219 _epoch.clear();
220 }
221 return *this;
222}
223
224//
225////////////////////////////////////////////////////////////////////////////
[11179]226const char* t_logRecord::levelString(e_level level) {
227 switch (level) {
[11185]228 case debug: return "debug";
[11179]229 case warning: return "warning";
230 case error: return "error";
231 default: return "info";
232 }
233}
234
235//
236////////////////////////////////////////////////////////////////////////////
237t_logRecord::e_format t_logRecord::formatFromString(const QString& str) {
238 return (str.trimmed().toLower() == "logfmt") ? logfmt : json;
239}
240
241//
242////////////////////////////////////////////////////////////////////////////
[11185]243t_logRecord::e_level t_logRecord::levelFromString(const QString& str) {
244 QString ss = str.trimmed().toLower();
245 if (ss == "info") return info;
246 if (ss == "warning") return warning;
247 if (ss == "error") return error;
248 return debug;
249}
250
251//
252////////////////////////////////////////////////////////////////////////////
253void t_logRecord::registerStream(const QByteArray& stream) {
254 if (stream.isEmpty()) {
[11179]255 return;
256 }
[11185]257 QMutexLocker locker(&streamMutex);
258 streams.insert(stream);
[11179]259}
260
261// One line of the structured logfile
262////////////////////////////////////////////////////////////////////////////
263QByteArray t_logRecord::encode(e_format format, const QDateTime& gpst) const {
264
265 // UTC = GPS time - leap seconds
266 QDate dd = gpst.date();
267 QDateTime utc = gpst.addSecs(-gnumleap(dd.year(), dd.month(), dd.day()));
268
[11187]269 QByteArray msg = _msgStructured.isEmpty() ? _msg : _msgStructured;
[11179]270 while (msg.startsWith('\n')) {
271 msg.remove(0, 1);
272 }
[11189]273 // the mountpoint is a key of its own
274 if (!_stream.isEmpty() && msg.startsWith(_stream + ": ")) {
275 msg.remove(0, _stream.size() + 2);
276 }
[11190]277 // no trailing blanks (e.g. RTCM scan: "Receiver Serial Number: 1831551 ")
278 while (msg.endsWith(' ')) {
279 msg.chop(1);
280 }
[11179]281
282 QVector<t_field> all;
283 auto put = [&all](const char* key, const QByteArray& value, bool isString) {
284 t_field ff;
285 ff._key = key; ff._value = value; ff._isString = isString;
286 all.append(ff);
287 };
288 put("time", utc.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1() + "Z", true);
[11187]289 if (!_epoch.isEmpty()) {
290 put("epoch", _epoch, true);
291 }
[11179]292 put("level", levelString(_level), true);
293 put("component", _component, true);
294 put("event", _event, true);
[11185]295 if (!_stream.isEmpty()) {
[11187]296 put("mp", _stream, true);
[11179]297 }
298 all += _fields;
299 put("msg", msg, true);
300 put("v", QByteArray::number(version), false);
301
302 QByteArray line;
303 if (format == logfmt) {
304 for (int ii = 0; ii < all.size(); ii++) {
305 if (ii) {
306 line += ' ';
307 }
308 line += all[ii]._key + '=';
309 line += all[ii]._isString ? logfmtValue(all[ii]._value) : all[ii]._value;
310 }
311 }
312 else {
313 line = "{";
314 for (int ii = 0; ii < all.size(); ii++) {
315 if (ii) {
316 line += ',';
317 }
318 line += '"' + all[ii]._key + "\":";
319 line += all[ii]._isString ? jsonString(all[ii]._value) : all[ii]._value;
320 }
321 line += '}';
322 }
323 return line;
324}
Note: See TracBrowser for help on using the repository browser.