File indexing completed on 2024-05-19 05:49:16
0001 /* 0002 SPDX-FileCopyrightText: 2007 Nicolas Ternisien <nicolas.ternisien@gmail.com> 0003 SPDX-FileCopyrightText: 2015 Vyacheslav Matyushin 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "journaldNetworkAnalyzer.h" 0009 #include "journaldConfiguration.h" 0010 #include "ksystemlogConfig.h" 0011 #include "ksystemlog_debug.h" 0012 #include "logFile.h" 0013 #include "logViewModel.h" 0014 0015 #include <QJsonArray> 0016 #include <QJsonDocument> 0017 #include <QJsonObject> 0018 #include <QJsonParseError> 0019 #include <QRegularExpression> 0020 0021 #include <KLocalizedString> 0022 0023 JournaldNetworkAnalyzer::JournaldNetworkAnalyzer(LogMode *mode, const JournaldAnalyzerOptions &options) 0024 : JournaldAnalyzer(mode) 0025 { 0026 mAddress = options.address; 0027 0028 connect(&mNetworkManager, &QNetworkAccessManager::sslErrors, this, &JournaldNetworkAnalyzer::sslErrors); 0029 0030 auto *configuration = mode->logModeConfiguration<JournaldConfiguration *>(); 0031 0032 mBaseUrl = QStringLiteral("%1://%2:%3/").arg(mAddress.https ? QStringLiteral("https") : QStringLiteral("http")).arg(mAddress.address).arg(mAddress.port); 0033 0034 mEntriesUrlUpdating = mBaseUrl + QStringLiteral("entries"); 0035 mEntriesUrlFull = mEntriesUrlUpdating; 0036 0037 QString filterPrefix; 0038 if (configuration->displayCurrentBootOnly()) { 0039 mEntriesUrlUpdating.append(QStringLiteral("?boot&follow")); 0040 mEntriesUrlFull.append(QStringLiteral("?boot")); 0041 filterPrefix = QStringLiteral("&"); 0042 } else { 0043 mEntriesUrlUpdating.append(QStringLiteral("?follow")); 0044 filterPrefix = QStringLiteral("?"); 0045 } 0046 0047 if (!options.filter.isEmpty()) { 0048 mEntriesUrlUpdating.append(QStringLiteral("&") + options.filter); 0049 mEntriesUrlFull.append(filterPrefix + options.filter); 0050 } 0051 0052 mSyslogIdUrl = mBaseUrl + QStringLiteral("fields/SYSLOG_IDENTIFIER"); 0053 mSystemdUnitsUrl = mBaseUrl + QStringLiteral("fields/_SYSTEMD_UNIT"); 0054 0055 mFilterName = options.filter.section(QChar::fromLatin1('='), 1); 0056 0057 mReply = nullptr; 0058 } 0059 0060 void JournaldNetworkAnalyzer::watchLogFiles(bool enabled) 0061 { 0062 if (enabled) { 0063 sendRequest(RequestType::SyslogIds); 0064 } else { 0065 mCursor.clear(); 0066 updateStatus(QString()); 0067 if (mReply) { 0068 mReply->abort(); 0069 mReply->deleteLater(); 0070 mReply = nullptr; 0071 } 0072 } 0073 } 0074 0075 QStringList JournaldNetworkAnalyzer::units() const 0076 { 0077 return mSystemdUnits; 0078 } 0079 0080 QStringList JournaldNetworkAnalyzer::syslogIdentifiers() const 0081 { 0082 return mSyslogIdentifiers; 0083 } 0084 0085 void JournaldNetworkAnalyzer::httpFinished() 0086 { 0087 QByteArray data = mReply->readAll(); 0088 if (mCurrentRequest == RequestType::EntriesFull) { 0089 if (data.size()) { 0090 parseEntries(data, FullRead); 0091 updateStatus(i18n("Connected")); 0092 } 0093 if (!mCursor.isEmpty()) { 0094 sendRequest(RequestType::EntriesUpdate); 0095 } else { 0096 qCWarning(KSYSTEMLOG) << "Network journal analyzer failed to extract cursor string. " 0097 "Journal updates will be unavailable."; 0098 } 0099 } else { 0100 QString const identifiersString = QString::fromUtf8(data); 0101 QStringList const identifiersList = identifiersString.split(QChar::fromLatin1('\n'), Qt::SkipEmptyParts); 0102 switch (mCurrentRequest) { 0103 case RequestType::SyslogIds: 0104 mSyslogIdentifiers = identifiersList; 0105 mSyslogIdentifiers.sort(); 0106 sendRequest(RequestType::Units); 0107 break; 0108 case RequestType::Units: { 0109 mSystemdUnits = identifiersList; 0110 mSystemdUnits.sort(); 0111 auto *journalLogMode = dynamic_cast<JournaldLogMode *>(mLogMode); 0112 JournalFilters filters; 0113 filters.syslogIdentifiers = mSyslogIdentifiers; 0114 filters.systemdUnits = mSystemdUnits; 0115 journalLogMode->updateJournalFilters(mAddress, filters); 0116 // Regenerate the "Logs" submenu to include new syslog identifiers and systemd units. 0117 Q_EMIT mLogMode->menuChanged(); 0118 sendRequest(RequestType::EntriesFull); 0119 break; 0120 } 0121 default: 0122 break; 0123 } 0124 } 0125 } 0126 0127 void JournaldNetworkAnalyzer::httpReadyRead() 0128 { 0129 if (mCurrentRequest == RequestType::EntriesUpdate) { 0130 QByteArray data = mReply->readAll(); 0131 parseEntries(data, UpdatingRead); 0132 } 0133 } 0134 0135 void JournaldNetworkAnalyzer::httpError(QNetworkReply::NetworkError code) 0136 { 0137 if (mParsingPaused) { 0138 return; 0139 } 0140 0141 if (code == QNetworkReply::OperationCanceledError) { 0142 return; 0143 } 0144 0145 updateStatus(i18n("Connection error")); 0146 qCWarning(KSYSTEMLOG) << "Network journald connection error:" << code; 0147 } 0148 0149 void JournaldNetworkAnalyzer::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors) 0150 { 0151 Q_UNUSED(errors) 0152 reply->ignoreSslErrors(); 0153 } 0154 0155 void JournaldNetworkAnalyzer::parseEntries(QByteArray &data, Analyzer::ReadingMode readingMode) 0156 { 0157 if (mParsingPaused) { 0158 qCDebug(KSYSTEMLOG) << "Parsing is paused, discarding journald entries."; 0159 return; 0160 } 0161 0162 QList<QByteArray> items = data.split('{'); 0163 QList<JournalEntry> entries; 0164 for (int i = 0; i < items.size(); i++) { 0165 QByteArray &item = items[i]; 0166 if (item.isEmpty()) { 0167 continue; 0168 } 0169 item.prepend('{'); 0170 QJsonParseError jsonError{}; 0171 QJsonDocument const doc = QJsonDocument::fromJson(item, &jsonError); 0172 if (jsonError.error != 0) { 0173 continue; 0174 } 0175 QJsonObject object = doc.object(); 0176 0177 if ((readingMode == FullRead) && (i == items.size() - 1)) { 0178 mCursor = object[QStringLiteral("__CURSOR")].toString(); 0179 break; 0180 } 0181 0182 JournalEntry entry; 0183 auto timestampUsec = object[QStringLiteral("__REALTIME_TIMESTAMP")].toVariant().value<quint64>(); 0184 entry.date.setMSecsSinceEpoch(timestampUsec / 1000); 0185 entry.message = object[QStringLiteral("MESSAGE")].toString(); 0186 if (entry.message.isEmpty()) { 0187 // MESSAGE field contains a JSON array of bytes. 0188 QByteArray stringBytes; 0189 QJsonArray a = object[QStringLiteral("MESSAGE")].toArray(); 0190 for (int i = 0; i < a.size(); i++) { 0191 stringBytes.append(a[i].toVariant().value<char>()); 0192 } 0193 entry.message = QString::fromUtf8(stringBytes); 0194 } 0195 entry.message.remove(QRegularExpression(QLatin1String(ConsoleColorEscapeSequence))); 0196 entry.priority = object[QStringLiteral("PRIORITY")].toVariant().value<int>(); 0197 entry.bootID = object[QStringLiteral("_BOOT_ID")].toString(); 0198 QString unit = object[QStringLiteral("SYSLOG_IDENTIFIER")].toString(); 0199 if (unit.isEmpty()) { 0200 unit = object[QStringLiteral("_SYSTEMD_UNIT")].toString(); 0201 } 0202 entry.unit = unit; 0203 0204 entries << entry; 0205 } 0206 0207 if (entries.empty()) { 0208 qCDebug(KSYSTEMLOG) << "Received no entries."; 0209 } else { 0210 mInsertionLocking.lock(); 0211 mLogViewModel->startingMultipleInsertions(); 0212 0213 if (FullRead == readingMode) { 0214 Q_EMIT statusBarChanged(i18n("Reading journald entries...")); 0215 // Start displaying the loading bar. 0216 Q_EMIT readFileStarted(*mLogMode, LogFile(), 0, 1); 0217 } 0218 0219 // Add journald entries to the model. 0220 int const entriesInserted = updateModel(entries, readingMode); 0221 0222 mLogViewModel->endingMultipleInsertions(readingMode, entriesInserted); 0223 0224 if (FullRead == readingMode) { 0225 Q_EMIT statusBarChanged(i18n("Journald entries loaded successfully.")); 0226 0227 // Stop displaying the loading bar. 0228 Q_EMIT readEnded(); 0229 } 0230 0231 // Inform LogManager that new lines have been added. 0232 Q_EMIT logUpdated(entriesInserted); 0233 0234 mInsertionLocking.unlock(); 0235 } 0236 } 0237 0238 void JournaldNetworkAnalyzer::sendRequest(RequestType requestType) 0239 { 0240 if (mReply) { 0241 mReply->deleteLater(); 0242 } 0243 0244 mCurrentRequest = requestType; 0245 0246 QNetworkRequest request; 0247 QString url; 0248 0249 switch (requestType) { 0250 case RequestType::SyslogIds: 0251 url = mSyslogIdUrl; 0252 break; 0253 case RequestType::Units: 0254 url = mSystemdUnitsUrl; 0255 break; 0256 case RequestType::EntriesFull: { 0257 url = mEntriesUrlFull; 0258 int const entries = KSystemLogConfig::maxLines(); 0259 request.setRawHeader("Accept", "application/json"); 0260 request.setRawHeader("Range", QStringLiteral("entries=:-%1:%2").arg(entries - 1).arg(entries).toUtf8()); 0261 } break; 0262 case RequestType::EntriesUpdate: 0263 url = mEntriesUrlUpdating; 0264 request.setRawHeader("Accept", "application/json"); 0265 request.setRawHeader("Range", QStringLiteral("entries=%1").arg(mCursor).toUtf8()); 0266 default: 0267 break; 0268 } 0269 0270 request.setUrl(QUrl(url)); 0271 qCDebug(KSYSTEMLOG) << "Journal network analyzer requested" << url; 0272 mReply = mNetworkManager.get(request); 0273 connect(mReply, &QNetworkReply::finished, this, &JournaldNetworkAnalyzer::httpFinished); 0274 connect(mReply, &QNetworkReply::readyRead, this, &JournaldNetworkAnalyzer::httpReadyRead); 0275 connect(mReply, &QNetworkReply::errorOccurred, this, &JournaldNetworkAnalyzer::httpError); 0276 } 0277 0278 void JournaldNetworkAnalyzer::updateStatus(const QString &status) 0279 { 0280 QString newStatus = mBaseUrl; 0281 if (!mFilterName.isEmpty()) { 0282 newStatus += QLatin1String(" - ") + mFilterName; 0283 } 0284 if (!status.isEmpty()) { 0285 newStatus += QLatin1String(" - ") + status; 0286 } 0287 Q_EMIT statusChanged(newStatus); 0288 } 0289 0290 #include "moc_journaldNetworkAnalyzer.cpp"