| [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 |
|
|---|
| 25 | namespace {
|
|---|
| 26 |
|
|---|
| [11185] | 27 | QMutex streamMutex;
|
|---|
| 28 | QSet<QByteArray> streams;
|
|---|
| [11179] | 29 |
|
|---|
| 30 | // text as UTF-8 (BNC's messages are Latin-1)
|
|---|
| 31 | QByteArray utf8(const QByteArray& latin1) {
|
|---|
| 32 | return QString::fromLatin1(latin1).toUtf8();
|
|---|
| 33 | }
|
|---|
| 34 |
|
|---|
| 35 | QByteArray 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 |
|
|---|
| 58 | QByteArray 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 |
|
|---|
| 92 | bool 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] | 100 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 141 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 150 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 158 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 174 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 185 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 196 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 207 | t_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] | 214 | t_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] | 226 | const 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 237 | t_logRecord::e_format t_logRecord::formatFromString(const QString& str) {
|
|---|
| 238 | return (str.trimmed().toLower() == "logfmt") ? logfmt : json;
|
|---|
| 239 | }
|
|---|
| 240 |
|
|---|
| 241 | //
|
|---|
| 242 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| [11185] | 243 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 253 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 263 | QByteArray 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 | }
|
|---|