/* ------------------------------------------------------------------------- * BKG NTRIP Client * ------------------------------------------------------------------------- * * Class: t_logRecord * * Purpose: Record of the structured logfile (JSON Lines or logfmt) * * Created: 04-Nov-2026 * * Changes: * * -----------------------------------------------------------------------*/ #include #include #include #include #include "bnclog.h" #include "bncutils.h" #include "bnctime.h" namespace { QMutex streamMutex; QSet streams; // text as UTF-8 (BNC's messages are Latin-1) QByteArray utf8(const QByteArray& latin1) { return QString::fromLatin1(latin1).toUtf8(); } QByteArray jsonString(const QByteArray& latin1) { QByteArray in = utf8(latin1); QByteArray out = "\""; for (char c : in) { unsigned char u = static_cast(c); switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (u < 0x20) { out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0'); } else { out += c; } } } return out + "\""; } QByteArray logfmtValue(const QByteArray& latin1) { QByteArray in = utf8(latin1); bool quote = in.isEmpty(); for (char c : in) { unsigned char u = static_cast(c); if (u <= 0x20 || c == '=' || c == '"' || c == '\\') { quote = true; break; } } if (!quote) { return in; } QByteArray out = "\""; for (char c : in) { unsigned char u = static_cast(c); switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (u < 0x20) { out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0'); } else { out += c; } } } return out + "\""; } bool containsWord(const QByteArray& lower, const char* word) { return lower.contains(word); } } // Envelope of a plain text message //////////////////////////////////////////////////////////////////////////// t_logRecord t_logRecord::fromMessage(const QByteArray& msg, const QByteArray& component) { t_logRecord rec(info, component, "message", "", msg); // stream: first token of the text, if it is a known stream or PPP station int end = 0; while (end < msg.size() && msg[end] != ':' && msg[end] != ' ' && msg[end] != '(') { ++end; } if (end > 0) { QByteArray token = msg.left(end); QMutexLocker locker(&streamMutex); if (streams.contains(token)) { rec._stream = token; } } // level from keywords QByteArray lower = msg.toLower(); static const char* errorWords[] = {"error", "fail", "cannot", "can't", "could not", "unable", "exception", 0}; static const char* warningWords[] = {"warning", "outage", "corrupt", "timeout", "wrong", "invalid", "ignored", "refused", "unauthorized", "not found", "missing", "unknown", 0}; for (int ii = 0; errorWords[ii]; ii++) { if (containsWord(lower, errorWords[ii])) { rec._level = error; return rec; } } for (int ii = 0; warningWords[ii]; ii++) { if (containsWord(lower, warningWords[ii])) { rec._level = warning; return rec; } } return rec; } // Plain text message with an explicit level //////////////////////////////////////////////////////////////////////////// t_logRecord t_logRecord::message(e_level level, const QByteArray& msg, const QByteArray& component) { t_logRecord rec = fromMessage(msg, component); rec._level = level; return rec; } // Message of a BNC_DEBUG_* build //////////////////////////////////////////////////////////////////////////// t_logRecord t_logRecord::debugMessage(const QByteArray& component, const QByteArray& msg) { t_logRecord rec = fromMessage(msg, component); rec._level = debug; return rec; } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::add(const char* key, double value, int precision) { t_field ff; ff._key = key; ff._isString = false; if (std::isfinite(value)) { ff._value = QByteArray::number(value, 'f', precision); // C locale } else { ff._value = "null"; } _fields.append(ff); return *this; } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::add(const char* key, int value) { t_field ff; ff._key = key; ff._value = QByteArray::number(value); ff._isString = false; _fields.append(ff); return *this; } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::add(const char* key, bool value) { t_field ff; ff._key = key; ff._value = value ? "true" : "false"; ff._isString = false; _fields.append(ff); return *this; } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::add(const char* key, const QByteArray& value) { t_field ff; ff._key = key; ff._value = value; ff._isString = true; _fields.append(ff); return *this; } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::addTime(const char* key, const QDateTime& utc) { QDateTime tt = utc.toUTC(); return add(key, tt.toString("yyyy-MM-ddThh:mm:ss.zzzZ").toLatin1()); } // //////////////////////////////////////////////////////////////////////////// t_logRecord& t_logRecord::setEpoch(const bncTime& epoch) { if (epoch.valid()) { _epoch = QByteArray((epoch.datestr() + 'T' + epoch.timestr(3)).c_str()); } else { _epoch.clear(); } return *this; } // //////////////////////////////////////////////////////////////////////////// const char* t_logRecord::levelString(e_level level) { switch (level) { case debug: return "debug"; case warning: return "warning"; case error: return "error"; default: return "info"; } } // //////////////////////////////////////////////////////////////////////////// t_logRecord::e_format t_logRecord::formatFromString(const QString& str) { return (str.trimmed().toLower() == "logfmt") ? logfmt : json; } // //////////////////////////////////////////////////////////////////////////// t_logRecord::e_level t_logRecord::levelFromString(const QString& str) { QString ss = str.trimmed().toLower(); if (ss == "info") return info; if (ss == "warning") return warning; if (ss == "error") return error; return debug; } // //////////////////////////////////////////////////////////////////////////// void t_logRecord::registerStream(const QByteArray& stream) { if (stream.isEmpty()) { return; } QMutexLocker locker(&streamMutex); streams.insert(stream); } // One line of the structured logfile //////////////////////////////////////////////////////////////////////////// QByteArray t_logRecord::encode(e_format format, const QDateTime& gpst) const { // UTC = GPS time - leap seconds QDate dd = gpst.date(); QDateTime utc = gpst.addSecs(-gnumleap(dd.year(), dd.month(), dd.day())); QByteArray msg = _msgStructured.isEmpty() ? _msg : _msgStructured; while (msg.startsWith('\n')) { msg.remove(0, 1); } // the mountpoint is a key of its own if (!_stream.isEmpty() && msg.startsWith(_stream + ": ")) { msg.remove(0, _stream.size() + 2); } // no trailing blanks (e.g. RTCM scan: "Receiver Serial Number: 1831551 ") while (msg.endsWith(' ')) { msg.chop(1); } QVector all; auto put = [&all](const char* key, const QByteArray& value, bool isString) { t_field ff; ff._key = key; ff._value = value; ff._isString = isString; all.append(ff); }; put("time", utc.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1() + "Z", true); if (!_epoch.isEmpty()) { put("epoch", _epoch, true); } put("level", levelString(_level), true); put("component", _component, true); put("event", _event, true); if (!_stream.isEmpty()) { put("mp", _stream, true); } all += _fields; put("msg", msg, true); put("v", QByteArray::number(version), false); QByteArray line; if (format == logfmt) { for (int ii = 0; ii < all.size(); ii++) { if (ii) { line += ' '; } line += all[ii]._key + '='; line += all[ii]._isString ? logfmtValue(all[ii]._value) : all[ii]._value; } } else { line = "{"; for (int ii = 0; ii < all.size(); ii++) { if (ii) { line += ','; } line += '"' + all[ii]._key + "\":"; line += all[ii]._isString ? jsonString(all[ii]._value) : all[ii]._value; } line += '}'; } return line; }