diff --git a/src/core/logging.cpp b/src/core/logging.cpp index a5ed80f5..7a8426b8 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include #include "logging_p.hpp" +#include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" namespace qs::log { @@ -82,6 +85,16 @@ void LogMessage::formatMessage( if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } +bool CategoryFilter::shouldDisplay(QtMsgType type) const { + switch (type) { + case QtDebugMsg: return this->debug; + case QtInfoMsg: return this->info; + case QtWarningMsg: return this->warn; + case QtCriticalMsg: return this->critical; + default: return true; + } +} + LogManager::LogManager(): stdoutStream(stdout) {} void LogManager::messageHandler( @@ -98,14 +111,7 @@ void LogManager::messageHandler( const auto* key = static_cast(context.category); if (self->sparseFilters.contains(key)) { - auto filter = self->sparseFilters.value(key); - switch (type) { - case QtDebugMsg: display = filter.debug; break; - case QtInfoMsg: display = filter.info; break; - case QtWarningMsg: display = filter.warn; break; - case QtCriticalMsg: display = filter.critical; break; - default: break; - } + display = self->sparseFilters.value(key).shouldDisplay(type); } if (display) { @@ -520,7 +526,8 @@ start: slot->time = this->lastMessageTime; } } else { - auto category = this->categories.value(next - EncodedLogOpcode::BeginCategories); + auto categoryId = next - EncodedLogOpcode::BeginCategories; + auto category = this->categories.value(categoryId); quint8 field = 0; if (!this->reader.readU8(&field)) return false; @@ -555,6 +562,7 @@ start: if (!this->readString(&body)) return false; *slot = LogMessage(msgType, QLatin1StringView(category), body, this->lastMessageTime); + slot->readCategoryId = categoryId; } this->recentMessages.emplace(*slot); @@ -633,7 +641,17 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device) { +bool readEncodedLogs(QIODevice* device, const QString& rulespec) { + using namespace qt_logging_registry; + + QList rules; + + { + QLoggingSettingsParser parser; + parser.setContent(rulespec); + rules = parser.rules(); + } + auto reader = EncodedLogReader(); reader.setDevice(device); @@ -655,11 +673,36 @@ bool readEncodedLogs(QIODevice* device) { auto color = LogManager::instance()->colorLogs; + auto filters = QHash(); + LogMessage message; auto stream = QTextStream(stdout); while (reader.read(&message)) { - LogMessage::formatMessage(stream, message, color, true); - stream << '\n'; + CategoryFilter filter; + if (filters.contains(message.readCategoryId)) { + filter = filters.value(message.readCategoryId); + } else { + for (const auto& rule: rules) { + auto filterpass = rule.pass(message.category, QtDebugMsg); + if (filterpass != 0) filter.debug = filterpass > 0; + + filterpass = rule.pass(message.category, QtInfoMsg); + if (filterpass != 0) filter.info = filterpass > 0; + + filterpass = rule.pass(message.category, QtWarningMsg); + if (filterpass != 0) filter.warn = filterpass > 0; + + filterpass = rule.pass(message.category, QtCriticalMsg); + if (filterpass != 0) filter.critical = filterpass > 0; + } + + filters.insert(message.readCategoryId, filter); + } + + if (filter.shouldDisplay(message.type)) { + LogMessage::formatMessage(stream, message, color, true); + stream << '\n'; + } } stream << Qt::flush; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 69188391..5462144f 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -33,6 +33,7 @@ struct LogMessage { QDateTime time; QLatin1StringView category; QByteArray body; + quint16 readCategoryId = 0; static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); }; @@ -63,6 +64,8 @@ struct CategoryFilter { , warn(category->isWarningEnabled()) , critical(category->isCriticalEnabled()) {} + [[nodiscard]] bool shouldDisplay(QtMsgType type) const; + bool debug = true; bool info = true; bool warn = true; @@ -95,7 +98,7 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device); +bool readEncodedLogs(QIODevice* device, const QString& rulespec); } // namespace qs::log diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp new file mode 100644 index 00000000..05393f02 --- /dev/null +++ b/src/core/logging_qtprivate.cpp @@ -0,0 +1,159 @@ +// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. + +// Was unable to properly link the functions when directly using the headers (which we depend +// on anyway), so below is a slightly stripped down copy. Making the originals link would +// be preferable. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::log { +Q_DECLARE_LOGGING_CATEGORY(logLogging); + +namespace qt_logging_registry { + +class QLoggingRule { +public: + QLoggingRule(); + QLoggingRule(QStringView pattern, bool enabled); + [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; + + enum PatternFlag { + FullText = 0x1, + LeftFilter = 0x2, + RightFilter = 0x4, + MidFilter = LeftFilter | RightFilter + }; + Q_DECLARE_FLAGS(PatternFlags, PatternFlag) + + QString category; + int messageType; + PatternFlags flags; + bool enabled; + +private: + void parse(QStringView pattern); +}; + +class QLoggingSettingsParser { +public: + void setContent(QStringView content); + + [[nodiscard]] QList rules() const { return this->mRules; } + +private: + void parseNextLine(QStringView line); + +private: + QList mRules; +}; + +void QLoggingSettingsParser::setContent(QStringView content) { + this->mRules.clear(); + for (auto line: qTokenize(content, u';')) this->parseNextLine(line); +} + +void QLoggingSettingsParser::parseNextLine(QStringView line) { + // Remove whitespace at start and end of line: + line = line.trimmed(); + + const qsizetype equalPos = line.indexOf(u'='); + if (equalPos != -1) { + if (line.lastIndexOf(u'=') == equalPos) { + const auto key = line.left(equalPos).trimmed(); + const QStringView pattern = key; + const auto valueStr = line.mid(equalPos + 1).trimmed(); + int value = -1; + if (valueStr == QString("true")) value = 1; + else if (valueStr == QString("false")) value = 0; + QLoggingRule rule(pattern, (value == 1)); + if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule)); + else + qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); + } else { + qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); + } + } +} + +QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) { + this->parse(pattern); +} + +void QLoggingRule::parse(QStringView pattern) { + QStringView p; + + // strip trailing ".messagetype" + if (pattern.endsWith(QString(".debug"))) { + p = pattern.chopped(6); // strlen(".debug") + this->messageType = QtDebugMsg; + } else if (pattern.endsWith(QString(".info"))) { + p = pattern.chopped(5); // strlen(".info") + this->messageType = QtInfoMsg; + } else if (pattern.endsWith(QString(".warning"))) { + p = pattern.chopped(8); // strlen(".warning") + this->messageType = QtWarningMsg; + } else if (pattern.endsWith(QString(".critical"))) { + p = pattern.chopped(9); // strlen(".critical") + this->messageType = QtCriticalMsg; + } else { + p = pattern; + } + + const QChar asterisk = u'*'; + if (!p.contains(asterisk)) { + this->flags = FullText; + } else { + if (p.endsWith(asterisk)) { + this->flags |= LeftFilter; + p = p.chopped(1); + } + if (p.startsWith(asterisk)) { + this->flags |= RightFilter; + p = p.mid(1); + } + if (p.contains(asterisk)) // '*' only supported at start/end + this->flags = PatternFlags(); + } + + this->category = p.toString(); +} + +int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const { + // check message type + if (this->messageType > -1 && this->messageType != msgType) return 0; + + if (this->flags == FullText) { + // full match + if (this->category == cat) return (this->enabled ? 1 : -1); + else return 0; + } + + const qsizetype idx = cat.indexOf(this->category); + if (idx >= 0) { + if (this->flags == MidFilter) { + // matches somewhere + return (this->enabled ? 1 : -1); + } else if (this->flags == LeftFilter) { + // matches left + if (idx == 0) return (this->enabled ? 1 : -1); + } else if (this->flags == RightFilter) { + // matches right + if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1); + } + } + return 0; +} + +} // namespace qt_logging_registry + +} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 86f4ec8c..23eb2124 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -140,9 +140,17 @@ int qs_main(int argc, char** argv) { ); /// --- - QStringOption logpath; + QStringOption logPath; + QStringOption logFilter; auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); - readLog->add_option("path", logpath, "Path to the log file to read")->required(); + readLog->add_option("path", logPath, "Path to the log file to read")->required(); + + readLog->add_option( + "-f,--filter", + logFilter, + "Logging categories to display. (same syntax as QT_LOGGING_RULES)" + ); + readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); CLI11_PARSE(app, argc, argv); @@ -153,15 +161,15 @@ int qs_main(int argc, char** argv) { LogManager::init(!noColor, sparseLogsOnly); if (*readLog) { - auto file = QFile(*logpath); + auto file = QFile(*logPath); if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << *logpath; + qCritical() << "Failed to open log for reading:" << *logPath; return -1; } else { - qInfo() << "Reading log" << *logpath; + qInfo() << "Reading log" << *logPath; } - return qs::log::readEncodedLogs(&file) ? 0 : -1; + return qs::log::readEncodedLogs(&file, *logFilter) ? 0 : -1; } else { // NOLINTBEGIN