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

Last change on this file was 11179, checked in by stuerze, 6 hours ago

structuered logging is added and bug is fixed in SP3 file writing

File size: 7.4 KB
Line 
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"
23
24namespace {
25
26QMutex stationMutex;
27QSet<QByteArray> stations;
28
29// text as UTF-8 (BNC's messages are Latin-1)
30QByteArray utf8(const QByteArray& latin1) {
31 return QString::fromLatin1(latin1).toUtf8();
32}
33
34QByteArray jsonString(const QByteArray& latin1) {
35 QByteArray in = utf8(latin1);
36 QByteArray out = "\"";
37 for (char c : in) {
38 unsigned char u = static_cast<unsigned char>(c);
39 switch (c) {
40 case '"': out += "\\\""; break;
41 case '\\': out += "\\\\"; break;
42 case '\n': out += "\\n"; break;
43 case '\r': out += "\\r"; break;
44 case '\t': out += "\\t"; break;
45 default:
46 if (u < 0x20) {
47 out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
48 }
49 else {
50 out += c;
51 }
52 }
53 }
54 return out + "\"";
55}
56
57QByteArray logfmtValue(const QByteArray& latin1) {
58 QByteArray in = utf8(latin1);
59 bool quote = in.isEmpty();
60 for (char c : in) {
61 unsigned char u = static_cast<unsigned char>(c);
62 if (u <= 0x20 || c == '=' || c == '"' || c == '\\') {
63 quote = true;
64 break;
65 }
66 }
67 if (!quote) {
68 return in;
69 }
70 QByteArray out = "\"";
71 for (char c : in) {
72 unsigned char u = static_cast<unsigned char>(c);
73 switch (c) {
74 case '"': out += "\\\""; break;
75 case '\\': out += "\\\\"; break;
76 case '\n': out += "\\n"; break;
77 case '\r': out += "\\r"; break;
78 case '\t': out += "\\t"; break;
79 default:
80 if (u < 0x20) {
81 out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
82 }
83 else {
84 out += c;
85 }
86 }
87 }
88 return out + "\"";
89}
90
91bool containsWord(const QByteArray& lower, const char* word) {
92 return lower.contains(word);
93}
94
95}
96
97// Envelope of a plain text message
98////////////////////////////////////////////////////////////////////////////
99t_logRecord t_logRecord::fromMessage(const QByteArray& msg) {
100 t_logRecord rec(info, "bnc", "message", "", msg);
101
102 // station: first token of the text, if it is a known station name
103 int end = 0;
104 while (end < msg.size() && msg[end] != ':' && msg[end] != ' ' && msg[end] != '(') {
105 ++end;
106 }
107 if (end > 0) {
108 QByteArray token = msg.left(end);
109 QMutexLocker locker(&stationMutex);
110 if (stations.contains(token)) {
111 rec._station = token;
112 }
113 }
114
115 // level from keywords
116 QByteArray lower = msg.toLower();
117 static const char* errorWords[] = {"error", "fail", "cannot", "can't", "could not",
118 "unable", "exception", 0};
119 static const char* warningWords[] = {"warning", "outage", "corrupt", "timeout",
120 "wrong", "invalid", "ignored", "refused",
121 "unauthorized", "not found", "missing",
122 "unknown", 0};
123 for (int ii = 0; errorWords[ii]; ii++) {
124 if (containsWord(lower, errorWords[ii])) {
125 rec._level = error;
126 return rec;
127 }
128 }
129 for (int ii = 0; warningWords[ii]; ii++) {
130 if (containsWord(lower, warningWords[ii])) {
131 rec._level = warning;
132 return rec;
133 }
134 }
135 return rec;
136}
137
138//
139////////////////////////////////////////////////////////////////////////////
140t_logRecord& t_logRecord::add(const char* key, double value, int precision) {
141 t_field ff;
142 ff._key = key;
143 ff._isString = false;
144 if (std::isfinite(value)) {
145 ff._value = QByteArray::number(value, 'f', precision); // C locale
146 }
147 else {
148 ff._value = "null";
149 }
150 _fields.append(ff);
151 return *this;
152}
153
154//
155////////////////////////////////////////////////////////////////////////////
156t_logRecord& t_logRecord::add(const char* key, int value) {
157 t_field ff;
158 ff._key = key;
159 ff._value = QByteArray::number(value);
160 ff._isString = false;
161 _fields.append(ff);
162 return *this;
163}
164
165//
166////////////////////////////////////////////////////////////////////////////
167t_logRecord& t_logRecord::add(const char* key, bool value) {
168 t_field ff;
169 ff._key = key;
170 ff._value = value ? "true" : "false";
171 ff._isString = false;
172 _fields.append(ff);
173 return *this;
174}
175
176//
177////////////////////////////////////////////////////////////////////////////
178t_logRecord& t_logRecord::add(const char* key, const QByteArray& value) {
179 t_field ff;
180 ff._key = key;
181 ff._value = value;
182 ff._isString = true;
183 _fields.append(ff);
184 return *this;
185}
186
187//
188////////////////////////////////////////////////////////////////////////////
189t_logRecord& t_logRecord::addTime(const char* key, const QDateTime& utc) {
190 QDateTime tt = utc.toUTC();
191 return add(key, tt.toString("yyyy-MM-ddThh:mm:ss.zzzZ").toLatin1());
192}
193
194//
195////////////////////////////////////////////////////////////////////////////
196const char* t_logRecord::levelString(e_level level) {
197 switch (level) {
198 case warning: return "warning";
199 case error: return "error";
200 default: return "info";
201 }
202}
203
204//
205////////////////////////////////////////////////////////////////////////////
206t_logRecord::e_format t_logRecord::formatFromString(const QString& str) {
207 return (str.trimmed().toLower() == "logfmt") ? logfmt : json;
208}
209
210//
211////////////////////////////////////////////////////////////////////////////
212void t_logRecord::registerStation(const QByteArray& station) {
213 if (station.isEmpty()) {
214 return;
215 }
216 QMutexLocker locker(&stationMutex);
217 stations.insert(station);
218}
219
220// One line of the structured logfile
221////////////////////////////////////////////////////////////////////////////
222QByteArray t_logRecord::encode(e_format format, const QDateTime& gpst) const {
223
224 // UTC = GPS time - leap seconds
225 QDate dd = gpst.date();
226 QDateTime utc = gpst.addSecs(-gnumleap(dd.year(), dd.month(), dd.day()));
227
228 QByteArray msg = _msg;
229 while (msg.startsWith('\n')) {
230 msg.remove(0, 1);
231 }
232
233 QVector<t_field> all;
234 auto put = [&all](const char* key, const QByteArray& value, bool isString) {
235 t_field ff;
236 ff._key = key; ff._value = value; ff._isString = isString;
237 all.append(ff);
238 };
239 put("time", utc.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1() + "Z", true);
240 put("gpst", gpst.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1(), true);
241 put("level", levelString(_level), true);
242 put("component", _component, true);
243 put("event", _event, true);
244 if (!_station.isEmpty()) {
245 put("station", _station, true);
246 }
247 all += _fields;
248 put("msg", msg, true);
249 put("v", QByteArray::number(version), false);
250
251 QByteArray line;
252 if (format == logfmt) {
253 for (int ii = 0; ii < all.size(); ii++) {
254 if (ii) {
255 line += ' ';
256 }
257 line += all[ii]._key + '=';
258 line += all[ii]._isString ? logfmtValue(all[ii]._value) : all[ii]._value;
259 }
260 }
261 else {
262 line = "{";
263 for (int ii = 0; ii < all.size(); ii++) {
264 if (ii) {
265 line += ',';
266 }
267 line += '"' + all[ii]._key + "\":";
268 line += all[ii]._isString ? jsonString(all[ii]._value) : all[ii]._value;
269 }
270 line += '}';
271 }
272 return line;
273}
Note: See TracBrowser for help on using the repository browser.