| 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 |
|
|---|
| 24 | namespace {
|
|---|
| 25 |
|
|---|
| 26 | QMutex stationMutex;
|
|---|
| 27 | QSet<QByteArray> stations;
|
|---|
| 28 |
|
|---|
| 29 | // text as UTF-8 (BNC's messages are Latin-1)
|
|---|
| 30 | QByteArray utf8(const QByteArray& latin1) {
|
|---|
| 31 | return QString::fromLatin1(latin1).toUtf8();
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | QByteArray 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 |
|
|---|
| 57 | QByteArray 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 |
|
|---|
| 91 | bool containsWord(const QByteArray& lower, const char* word) {
|
|---|
| 92 | return lower.contains(word);
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | // Envelope of a plain text message
|
|---|
| 98 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 99 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 140 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 156 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 167 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 178 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 189 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 196 | const 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 206 | t_logRecord::e_format t_logRecord::formatFromString(const QString& str) {
|
|---|
| 207 | return (str.trimmed().toLower() == "logfmt") ? logfmt : json;
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | //
|
|---|
| 211 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 212 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 222 | QByteArray 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 | }
|
|---|