mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-04-29 14:14:34 +02:00
feat(logs): parse log4j xml events in logs
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
parent
15a981b3f8
commit
47295da390
@ -175,6 +175,8 @@ set(LAUNCH_SOURCES
|
||||
launch/LogModel.h
|
||||
launch/TaskStepWrapper.cpp
|
||||
launch/TaskStepWrapper.h
|
||||
logs/LogParser.cpp
|
||||
logs/LogParser.h
|
||||
)
|
||||
|
||||
# Old update system
|
||||
|
@ -4,6 +4,8 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName)
|
||||
{
|
||||
if (levelName == "Launcher")
|
||||
return MessageLevel::Launcher;
|
||||
else if (levelName == "Trace")
|
||||
return MessageLevel::Trace;
|
||||
else if (levelName == "Debug")
|
||||
return MessageLevel::Debug;
|
||||
else if (levelName == "Info")
|
||||
|
@ -12,6 +12,7 @@ enum Enum {
|
||||
StdOut, /**< Undetermined stderr messages */
|
||||
StdErr, /**< Undetermined stdout messages */
|
||||
Launcher, /**< Launcher Messages */
|
||||
Trace, /**< Trace Messages */
|
||||
Debug, /**< Debug Messages */
|
||||
Info, /**< Info Messages */
|
||||
Message, /**< Standard Messages */
|
||||
|
@ -37,11 +37,14 @@
|
||||
|
||||
#include "launch/LaunchTask.h"
|
||||
#include <assert.h>
|
||||
#include <qlogging.h>
|
||||
#include <QAnyStringView>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <variant>
|
||||
#include "MessageLevel.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
@ -213,6 +216,52 @@ shared_qobject_ptr<LogModel> LaunchTask::getLogModel()
|
||||
return m_logModel;
|
||||
}
|
||||
|
||||
bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level)
|
||||
{
|
||||
LogParser* parser;
|
||||
switch (level) {
|
||||
case MessageLevel::StdErr:
|
||||
parser = &m_stderrParser;
|
||||
break;
|
||||
case MessageLevel::StdOut:
|
||||
parser = &m_stdoutParser;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
parser->appendLine(line);
|
||||
auto items = parser->parseAvailable();
|
||||
if (auto err = parser->getError(); err.has_value()) {
|
||||
auto& model = *getLogModel();
|
||||
model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage));
|
||||
return false;
|
||||
} else {
|
||||
if (items.has_value()) {
|
||||
auto& model = *getLogModel();
|
||||
for (auto const& item : items.value()) {
|
||||
if (std::holds_alternative<LogParser::LogEntry>(item)) {
|
||||
auto entry = std::get<LogParser::LogEntry>(item);
|
||||
auto msg = QString("[%1] [%2/%3] [%4]: %5")
|
||||
.arg(entry.timestamp.toString("HH:mm:ss"))
|
||||
.arg(entry.thread)
|
||||
.arg(entry.levelText)
|
||||
.arg(entry.logger)
|
||||
.arg(entry.message);
|
||||
msg = censorPrivateInfo(msg);
|
||||
model.append(entry.level, msg);
|
||||
} else if (std::holds_alternative<LogParser::PlainText>(item)) {
|
||||
auto msg = std::get<LogParser::PlainText>(item).message;
|
||||
level = m_instance->guessLevel(msg, level);
|
||||
msg = censorPrivateInfo(msg);
|
||||
model.append(level, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel)
|
||||
{
|
||||
for (auto& line : lines) {
|
||||
@ -222,6 +271,10 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default
|
||||
|
||||
void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
|
||||
{
|
||||
if (parseXmlLogs(line, level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the launcher part set a log level, use it
|
||||
auto innerLevel = MessageLevel::fromLine(line);
|
||||
if (innerLevel != MessageLevel::Unknown) {
|
||||
@ -229,7 +282,7 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
|
||||
}
|
||||
|
||||
// If the level is still undetermined, guess level
|
||||
if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) {
|
||||
if (level == MessageLevel::Unknown) {
|
||||
level = m_instance->guessLevel(line, level);
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@
|
||||
#include "LaunchStep.h"
|
||||
#include "LogModel.h"
|
||||
#include "MessageLevel.h"
|
||||
#include "logs/LogParser.h"
|
||||
|
||||
class LaunchTask : public Task {
|
||||
Q_OBJECT
|
||||
@ -114,6 +115,9 @@ class LaunchTask : public Task {
|
||||
private: /*methods */
|
||||
void finalizeSteps(bool successful, const QString& error);
|
||||
|
||||
protected:
|
||||
bool parseXmlLogs(QString const& line, MessageLevel::Enum level);
|
||||
|
||||
protected: /* data */
|
||||
MinecraftInstancePtr m_instance;
|
||||
shared_qobject_ptr<LogModel> m_logModel;
|
||||
@ -122,4 +126,6 @@ class LaunchTask : public Task {
|
||||
int currentStep = -1;
|
||||
State state = NotStarted;
|
||||
qint64 m_pid = -1;
|
||||
LogParser m_stdoutParser;
|
||||
LogParser m_stderrParser;
|
||||
};
|
||||
|
322
launcher/logs/LogParser.cpp
Normal file
322
launcher/logs/LogParser.cpp
Normal file
@ -0,0 +1,322 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "LogParser.h"
|
||||
|
||||
void LogParser::appendLine(QAnyStringView data)
|
||||
{
|
||||
if (!m_partialData.isEmpty()) {
|
||||
m_buffer = QString(m_partialData);
|
||||
m_buffer.append("\n");
|
||||
m_buffer.append(data.toString());
|
||||
m_partialData.clear();
|
||||
} else {
|
||||
m_buffer.append(data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<LogParser::Error> LogParser::getError()
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
MessageLevel::Enum LogParser::parseLogLevel(const QString& level)
|
||||
{
|
||||
auto test = level.trimmed().toUpper();
|
||||
if (test == "TRACE") {
|
||||
return MessageLevel::Trace;
|
||||
} else if (test == "DEBUG") {
|
||||
return MessageLevel::Debug;
|
||||
} else if (test == "INFO") {
|
||||
return MessageLevel::Info;
|
||||
} else if (test == "WARN") {
|
||||
return MessageLevel::Warning;
|
||||
} else if (test == "ERROR") {
|
||||
return MessageLevel::Error;
|
||||
} else if (test == "FATAL") {
|
||||
return MessageLevel::Fatal;
|
||||
} else {
|
||||
return MessageLevel::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<LogParser::LogEntry> LogParser::parseAttributes()
|
||||
{
|
||||
LogParser::LogEntry entry{
|
||||
"",
|
||||
MessageLevel::Info,
|
||||
};
|
||||
auto attributes = m_parser.attributes();
|
||||
|
||||
for (const auto& attr : attributes) {
|
||||
auto name = attr.name();
|
||||
auto value = attr.value();
|
||||
if (name == "logger") {
|
||||
entry.logger = value.trimmed().toString();
|
||||
} else if (name == "timestamp") {
|
||||
if (value.trimmed().isEmpty()) {
|
||||
m_parser.raiseError("log4j:Event Missing required attribute: timestamp");
|
||||
return {};
|
||||
}
|
||||
entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong());
|
||||
} else if (name == "level") {
|
||||
entry.levelText = value.trimmed().toString();
|
||||
entry.level = parseLogLevel(entry.levelText);
|
||||
} else if (name == "thread") {
|
||||
entry.thread = value.trimmed().toString();
|
||||
}
|
||||
}
|
||||
if (entry.logger.isEmpty()) {
|
||||
m_parser.raiseError("log4j:Event Missing required attribute: logger");
|
||||
return {};
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void LogParser::setError()
|
||||
{
|
||||
m_error = {
|
||||
m_parser.errorString(),
|
||||
m_parser.error(),
|
||||
};
|
||||
}
|
||||
|
||||
void LogParser::clearError()
|
||||
{
|
||||
m_error = {}; // clear previous error
|
||||
}
|
||||
|
||||
bool isPotentialLog4JStart(QStringView buffer)
|
||||
{
|
||||
static QString target = QStringLiteral("<log4j:event");
|
||||
if (buffer.isEmpty() || buffer[0] != '<') {
|
||||
return false;
|
||||
}
|
||||
auto bufLower = buffer.toString().toLower();
|
||||
return target.startsWith(bufLower) || bufLower.startsWith(target);
|
||||
}
|
||||
|
||||
std::optional<LogParser::ParsedItem> LogParser::parseNext()
|
||||
{
|
||||
clearError();
|
||||
|
||||
if (m_buffer.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (m_buffer.trimmed().isEmpty()) {
|
||||
m_buffer.clear();
|
||||
return {};
|
||||
}
|
||||
|
||||
// check if we have a full xml log4j event
|
||||
bool isCompleteLog4j = false;
|
||||
m_parser.clear();
|
||||
m_parser.setNamespaceProcessing(false);
|
||||
m_parser.addData(m_buffer);
|
||||
if (m_parser.readNextStartElement()) {
|
||||
if (m_parser.qualifiedName() == "log4j:Event") {
|
||||
int depth = 1;
|
||||
bool eod = false;
|
||||
while (depth > 0 && !eod) {
|
||||
auto tok = m_parser.readNext();
|
||||
switch (tok) {
|
||||
case QXmlStreamReader::TokenType::StartElement: {
|
||||
depth += 1;
|
||||
} break;
|
||||
case QXmlStreamReader::TokenType::EndElement: {
|
||||
depth -= 1;
|
||||
} break;
|
||||
case QXmlStreamReader::TokenType::EndDocument: {
|
||||
eod = true; // break outer while loop
|
||||
} break;
|
||||
default: {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
if (m_parser.hasError()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isCompleteLog4j = depth == 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCompleteLog4j) {
|
||||
return parseLog4J();
|
||||
} else {
|
||||
if (isPotentialLog4JStart(m_buffer)) {
|
||||
m_partialData = QString(m_buffer);
|
||||
return LogParser::Partial{ QString(m_buffer) };
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
auto bufView = QStringView(m_buffer);
|
||||
while (start < bufView.length()) {
|
||||
if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) {
|
||||
auto slicestart = start + pos;
|
||||
auto slice = bufView.right(bufView.length() - slicestart);
|
||||
if (isPotentialLog4JStart(slice)) {
|
||||
if (slicestart > 0) {
|
||||
auto text = m_buffer.left(slicestart);
|
||||
m_buffer = m_buffer.right(m_buffer.length() - slicestart);
|
||||
if (!text.trimmed().isEmpty()) {
|
||||
return LogParser::PlainText{ text };
|
||||
}
|
||||
}
|
||||
m_partialData = QString(m_buffer);
|
||||
return LogParser::Partial{ QString(m_buffer) };
|
||||
}
|
||||
start = slicestart + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no log4j found, all plain text
|
||||
auto text = QString(m_buffer);
|
||||
m_buffer.clear();
|
||||
if (text.trimmed().isEmpty()) {
|
||||
return {};
|
||||
} else {
|
||||
return LogParser::PlainText{ text };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QList<LogParser::ParsedItem>> LogParser::parseAvailable()
|
||||
{
|
||||
QList<LogParser::ParsedItem> items;
|
||||
bool doNext = true;
|
||||
while (doNext) {
|
||||
auto item_ = parseNext();
|
||||
if (m_error.has_value()) {
|
||||
return {};
|
||||
}
|
||||
if (item_.has_value()) {
|
||||
auto item = item_.value();
|
||||
if (std::holds_alternative<LogParser::Partial>(item)) {
|
||||
break;
|
||||
} else {
|
||||
items.push_back(item);
|
||||
}
|
||||
} else {
|
||||
doNext = false;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
std::optional<LogParser::ParsedItem> LogParser::parseLog4J()
|
||||
{
|
||||
m_parser.clear();
|
||||
m_parser.setNamespaceProcessing(false);
|
||||
m_parser.addData(m_buffer);
|
||||
|
||||
m_parser.readNextStartElement();
|
||||
if (m_parser.qualifiedName() == "log4j:Event") {
|
||||
auto entry_ = parseAttributes();
|
||||
if (!entry_.has_value()) {
|
||||
setError();
|
||||
return {};
|
||||
}
|
||||
auto entry = entry_.value();
|
||||
|
||||
bool foundMessage = false;
|
||||
int depth = 1;
|
||||
|
||||
while (!m_parser.atEnd()) {
|
||||
auto tok = m_parser.readNext();
|
||||
switch (tok) {
|
||||
case QXmlStreamReader::TokenType::StartElement: {
|
||||
depth += 1;
|
||||
if (m_parser.qualifiedName() == "log4j:Message") {
|
||||
QString message;
|
||||
bool messageComplete = false;
|
||||
|
||||
while (!messageComplete) {
|
||||
auto tok = m_parser.readNext();
|
||||
|
||||
switch (tok) {
|
||||
case QXmlStreamReader::TokenType::Characters: {
|
||||
message.append(m_parser.text());
|
||||
} break;
|
||||
case QXmlStreamReader::TokenType::EndElement: {
|
||||
if (m_parser.qualifiedName() == "log4j:Message") {
|
||||
messageComplete = true;
|
||||
}
|
||||
} break;
|
||||
case QXmlStreamReader::TokenType::EndDocument: {
|
||||
return {}; // parse fail
|
||||
} break;
|
||||
default: {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
|
||||
if (m_parser.hasError()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
entry.message = message;
|
||||
foundMessage = true;
|
||||
depth -= 1;
|
||||
}
|
||||
break;
|
||||
case QXmlStreamReader::TokenType::EndElement: {
|
||||
depth -= 1;
|
||||
if (depth == 0 && m_parser.qualifiedName() == "log4j:Event") {
|
||||
if (foundMessage) {
|
||||
auto consumed = m_parser.characterOffset();
|
||||
if (consumed > 0 && consumed <= m_buffer.length()) {
|
||||
m_buffer = m_buffer.right(m_buffer.length() - consumed);
|
||||
|
||||
if (!m_buffer.isEmpty() && m_buffer.trimmed().isEmpty()) {
|
||||
// only whitespace, dump it
|
||||
m_buffer.clear();
|
||||
}
|
||||
}
|
||||
clearError();
|
||||
return entry;
|
||||
}
|
||||
m_parser.raiseError("log4j:Event Missing required attribute: message");
|
||||
setError();
|
||||
return {};
|
||||
}
|
||||
} break;
|
||||
case QXmlStreamReader::TokenType::EndDocument: {
|
||||
return {};
|
||||
} break;
|
||||
default: {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_parser.hasError()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("unreachable: already verified this was a complete log4j:Event");
|
||||
}
|
74
launcher/logs/LogParser.h
Normal file
74
launcher/logs/LogParser.h
Normal file
@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QAnyStringView>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringView>
|
||||
#include <QXmlStreamReader>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include "MessageLevel.h"
|
||||
|
||||
class LogParser {
|
||||
public:
|
||||
struct LogEntry {
|
||||
QString logger;
|
||||
MessageLevel::Enum level;
|
||||
QString levelText;
|
||||
QDateTime timestamp;
|
||||
QString thread;
|
||||
QString message;
|
||||
};
|
||||
struct Partial {
|
||||
QString data;
|
||||
};
|
||||
struct PlainText {
|
||||
QString message;
|
||||
};
|
||||
struct Error {
|
||||
QString errMessage;
|
||||
QXmlStreamReader::Error error;
|
||||
};
|
||||
|
||||
using ParsedItem = std::variant<LogEntry, PlainText, Partial>;
|
||||
|
||||
public:
|
||||
LogParser() = default;
|
||||
|
||||
void appendLine(QAnyStringView data);
|
||||
std::optional<ParsedItem> parseNext();
|
||||
std::optional<QList<ParsedItem>> parseAvailable();
|
||||
std::optional<Error> getError();
|
||||
|
||||
protected:
|
||||
MessageLevel::Enum parseLogLevel(const QString& level);
|
||||
std::optional<LogEntry> parseAttributes();
|
||||
void setError();
|
||||
void clearError();
|
||||
|
||||
std::optional<ParsedItem> parseLog4J();
|
||||
|
||||
private:
|
||||
QString m_buffer;
|
||||
QString m_partialData;
|
||||
QXmlStreamReader m_parser;
|
||||
std::optional<Error> m_error;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user