mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
Merge pull request #646 from ispyisail/feature-diagnostic-logging
Rework diagnostic logging: fix file writer, add rotation and a ring buffer (discussion #644, steps 1-3)
This commit is contained in:
@@ -116,6 +116,10 @@ set(QET_RES_FILES
|
||||
set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/cli_export.cpp
|
||||
${QET_DIR}/sources/cli_export.h
|
||||
${QET_DIR}/sources/logging/logring.cpp
|
||||
${QET_DIR}/sources/logging/logring.h
|
||||
${QET_DIR}/sources/logging/qetlogger.cpp
|
||||
${QET_DIR}/sources/logging/qetlogger.h
|
||||
${QET_DIR}/sources/pdf_links.cpp
|
||||
${QET_DIR}/sources/pdf_links.h
|
||||
${QET_DIR}/sources/import/edz/edzarchive.cpp
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "logring.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
/**
|
||||
@brief LogRing::LogRing
|
||||
Preallocates all kCapacityEntries slots up front -- the only
|
||||
allocation this class ever does.
|
||||
*/
|
||||
LogRing::LogRing()
|
||||
{
|
||||
m_entries.resize(kCapacityEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LogRing::append
|
||||
@param line one already-formatted log line (no further formatting
|
||||
is done here). Truncated to kEntryBytes - 1 bytes if longer, with a
|
||||
trailing marker, so the stored entry is always a complete,
|
||||
independently-readable line.
|
||||
*/
|
||||
void LogRing::append(const QByteArray &line)
|
||||
{
|
||||
static const char kMarker[] = "...[ring-truncated]\n";
|
||||
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
|
||||
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
Entry &slot = m_entries[static_cast<size_t>(m_next_index)];
|
||||
|
||||
if (line.size() < kEntryBytes) {
|
||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
|
||||
slot.length = line.size();
|
||||
} else {
|
||||
const int keep = kEntryBytes - marker_len;
|
||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
|
||||
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
|
||||
slot.length = kEntryBytes;
|
||||
}
|
||||
|
||||
m_next_index = (m_next_index + 1) % kCapacityEntries;
|
||||
if (m_count < kCapacityEntries) {
|
||||
++m_count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LogRing::snapshot
|
||||
@return the entries currently held, oldest first. Safe to call from
|
||||
normal (non-signal) code only.
|
||||
*/
|
||||
QVector<QByteArray> LogRing::snapshot() const
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
QVector<QByteArray> result;
|
||||
result.reserve(m_count);
|
||||
|
||||
const int start = (m_count < kCapacityEntries) ? 0 : m_next_index;
|
||||
for (int i = 0; i < m_count; ++i) {
|
||||
const Entry &slot = m_entries[static_cast<size_t>((start + i) % kCapacityEntries)];
|
||||
result.append(QByteArray(slot.data, slot.length));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogRing::clear()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_next_index = 0;
|
||||
m_count = 0;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef LOGRING_H
|
||||
#define LOGRING_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QMutex>
|
||||
#include <QVector>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@brief The LogRing class
|
||||
Fixed-capacity, always-on in-memory ring of the most recent log
|
||||
lines. Discussion #644 (step 3): the ring exists as forward-compatible
|
||||
infrastructure for a future crash-flush (step 4, not implemented
|
||||
here) as well as an on-demand "what just happened" snapshot, so its
|
||||
entries are stored pre-formatted as plain bytes in storage
|
||||
preallocated once at construction -- append() never allocates.
|
||||
|
||||
Entries are fixed-size slots rather than a byte-packed ring: with
|
||||
kCapacityEntries * kEntryBytes chosen to land exactly on the 2 MiB
|
||||
budget, this keeps wraparound trivial (whole-slot overwrite, so a
|
||||
slot is always either fully the old entry or fully the new one --
|
||||
no torn entries) at the cost of truncating any single line to
|
||||
kEntryBytes, independently of the logger's own (larger) per-message
|
||||
truncation.
|
||||
|
||||
Thread-safe via a plain QMutex. This is *not* the lock-free design
|
||||
discussion #644 specifies for a signal-handler crash path (step 4)
|
||||
-- no signal handler is installed by this code, so nothing calls
|
||||
into the ring from inside a signal context.
|
||||
*/
|
||||
class LogRing
|
||||
{
|
||||
public:
|
||||
static constexpr int kCapacityEntries = 4096;
|
||||
static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total
|
||||
|
||||
LogRing();
|
||||
|
||||
/// Append one already-formatted, already-truncated log line.
|
||||
/// Bytes beyond kEntryBytes - 1 are dropped with a truncation marker.
|
||||
void append(const QByteArray &line);
|
||||
|
||||
/// Snapshot of the entries currently held, oldest first.
|
||||
QVector<QByteArray> snapshot() const;
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
char data[kEntryBytes] = {};
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
mutable QMutex m_mutex;
|
||||
std::vector<Entry> m_entries; // preallocated once, capacity fixed
|
||||
int m_next_index = 0;
|
||||
int m_count = 0;
|
||||
};
|
||||
|
||||
#endif // LOGRING_H
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetlogger.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
@brief legacyStderrOutput
|
||||
The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of
|
||||
every other function in this file -- including sanitize()/
|
||||
formatLine(), which are exactly the new code a problem might be in
|
||||
-- so this path stays usable even if the rest of the rework
|
||||
misbehaves. No ring, no file, no rotation, no mutex.
|
||||
*/
|
||||
void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
const QByteArray local_msg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
const char *level = "Unknown";
|
||||
switch (type) {
|
||||
case QtDebugMsg: level = "Debug"; break;
|
||||
case QtInfoMsg: level = "Info"; break;
|
||||
case QtWarningMsg: level = "Warning"; break;
|
||||
case QtCriticalMsg: level = "Critical"; break;
|
||||
case QtFatalMsg: level = "Fatal"; break;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: %s (%s:%u, %s)\n",
|
||||
level, local_msg.constData(), file, context.line, function);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ReentrancyGuard
|
||||
Sets the referenced flag on construction, clears it on destruction
|
||||
(including via early return / exception unwinding). Used as the
|
||||
per-thread guard against the logger recursing into itself.
|
||||
*/
|
||||
struct ReentrancyGuard
|
||||
{
|
||||
bool &flag;
|
||||
explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;}
|
||||
~ReentrancyGuard() {flag = false;}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@brief QetLogger::instance
|
||||
Function-local static: guaranteed constructed exactly once, in a
|
||||
thread-safe way, on first use -- but the *meaningful* initialisation
|
||||
(log path resolution, opening the file) happens in init(), called
|
||||
explicitly from main() at a defined point, not implicitly on
|
||||
whichever thread happens to log first.
|
||||
*/
|
||||
QetLogger &QetLogger::instance()
|
||||
{
|
||||
static QetLogger logger;
|
||||
return logger;
|
||||
}
|
||||
|
||||
void QetLogger::init()
|
||||
{
|
||||
m_disabled = (qgetenv("QET_LOG_DISABLE") == "1");
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_log_dir = QETApp::dataDir();
|
||||
m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
|
||||
|
||||
QMutexLocker locker(&m_file_mutex);
|
||||
m_file_output_ok = ensureFileOpenLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::ensureFileOpenLocked
|
||||
Caller must hold m_file_mutex. Opens the current session's log file
|
||||
if not already open. Refuses to follow a pre-existing symlink at
|
||||
that path, and creates the file owner-read/write only.
|
||||
*/
|
||||
bool QetLogger::ensureFileOpenLocked()
|
||||
{
|
||||
if (m_file.isOpen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QDir().mkpath(m_log_dir);
|
||||
|
||||
const QString path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
|
||||
|
||||
const QFileInfo info(path);
|
||||
if (info.exists() && info.isSymLink()) {
|
||||
// Filesystem hardening: refuse a pre-planted symlink rather than
|
||||
// silently appending to whatever it points at.
|
||||
return false;
|
||||
}
|
||||
|
||||
m_file.setFileName(path);
|
||||
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) {
|
||||
return false;
|
||||
}
|
||||
m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
m_bytes_written_current_file = m_file.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString QetLogger::rotatedPath(int index) const
|
||||
{
|
||||
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::rotateLocked
|
||||
Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the
|
||||
previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log,
|
||||
then opens a fresh, empty current file.
|
||||
*/
|
||||
void QetLogger::rotateLocked()
|
||||
{
|
||||
m_file.close();
|
||||
|
||||
const QString base_path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
|
||||
|
||||
for (int i = kRotationKeep; i >= 1; --i) {
|
||||
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
|
||||
const QString to = rotatedPath(i);
|
||||
|
||||
if (QFile::exists(to)) {
|
||||
QFile::remove(to);
|
||||
}
|
||||
if (QFile::exists(from)) {
|
||||
QFile::rename(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
m_bytes_written_current_file = 0;
|
||||
m_file_output_ok = ensureFileOpenLocked();
|
||||
}
|
||||
|
||||
void QetLogger::writeToFile(const QByteArray &line, QtMsgType type)
|
||||
{
|
||||
QMutexLocker locker(&m_file_mutex);
|
||||
|
||||
if (!m_file_output_ok) {
|
||||
// Write-failure policy: once file output has failed, stop
|
||||
// attempting it rather than spin-retrying every message. The
|
||||
// ring keeps running regardless.
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 written = m_file.write(line);
|
||||
if (written != line.size()) {
|
||||
m_file_output_ok = false;
|
||||
m_file.close();
|
||||
return;
|
||||
}
|
||||
m_bytes_written_current_file += written;
|
||||
|
||||
if (type >= QtWarningMsg) {
|
||||
m_file.flush();
|
||||
}
|
||||
|
||||
if (m_bytes_written_current_file >= kMaxFileBytes) {
|
||||
rotateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::sanitize
|
||||
Escapes newlines, carriage returns and other control characters.
|
||||
Much of what QET logs is externally controlled (file paths, element
|
||||
names, font strings read out of a .qet file); left unescaped, a
|
||||
crafted string containing '\n' can forge additional log lines.
|
||||
Operates on already-UTF-8-encoded bytes: this is safe because UTF-8
|
||||
continuation bytes are always >= 0x80, so any byte < 0x20 found here
|
||||
is a genuine ASCII control character, never part of a multi-byte
|
||||
sequence.
|
||||
*/
|
||||
QByteArray QetLogger::sanitize(const QByteArray &input)
|
||||
{
|
||||
QByteArray out;
|
||||
out.reserve(input.size());
|
||||
|
||||
for (unsigned char c : input) {
|
||||
if (c == '\n') {
|
||||
out += "\\n";
|
||||
} else if (c == '\r') {
|
||||
out += "\\r";
|
||||
} else if (c == '\t') {
|
||||
out += static_cast<char>(c);
|
||||
} else if (c < 0x20 || c == 0x7F) {
|
||||
out += "\\x";
|
||||
out += QByteArray::number(c, 16).rightJustified(2, '0');
|
||||
} else {
|
||||
out += static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::truncateMessage
|
||||
Caps a single message at max_bytes, appending a marker stating how
|
||||
many bytes were dropped, so one pathological caller (e.g. dumping an
|
||||
entire XML document to qDebug()) can't consume an unbounded amount
|
||||
of the ring's or file's byte budget.
|
||||
*/
|
||||
QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes)
|
||||
{
|
||||
if (input.size() <= max_bytes) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const int dropped = input.size() - max_bytes;
|
||||
QByteArray out = input.left(max_bytes);
|
||||
out += " ...[truncated ";
|
||||
out += QByteArray::number(dropped);
|
||||
out += " bytes]";
|
||||
return out;
|
||||
}
|
||||
|
||||
QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg)
|
||||
{
|
||||
// Includes the date (not just the time) so that a session crossing
|
||||
// midnight -- now kept in a single file -- doesn't read as ambiguous.
|
||||
const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8();
|
||||
|
||||
const char *level = "Unknown";
|
||||
switch (type) {
|
||||
case QtDebugMsg: level = "Debug"; break;
|
||||
case QtInfoMsg: level = "Info"; break;
|
||||
case QtWarningMsg: level = "Warning"; break;
|
||||
case QtCriticalMsg: level = "Critical"; break;
|
||||
case QtFatalMsg: level = "Fatal"; break;
|
||||
}
|
||||
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
QByteArray line = timestamp;
|
||||
line += ' ';
|
||||
line += level;
|
||||
line += ": ";
|
||||
line += sanitized_msg;
|
||||
|
||||
if (type == QtInfoMsg) {
|
||||
line += " \n";
|
||||
} else {
|
||||
line += " (";
|
||||
line += file;
|
||||
line += ":";
|
||||
line += QByteArray::number(context.line ? context.line : 0);
|
||||
line += ", ";
|
||||
line += function;
|
||||
line += ")\n";
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
if (m_disabled) {
|
||||
legacyStderrOutput(type, context, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
static thread_local bool in_handler = false;
|
||||
if (in_handler) {
|
||||
// The logger itself triggered a message (e.g. from inside a Qt
|
||||
// call it made) -- drop it rather than recurse.
|
||||
return;
|
||||
}
|
||||
ReentrancyGuard guard(in_handler);
|
||||
|
||||
const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes);
|
||||
const QByteArray line = formatLine(type, context, sanitized);
|
||||
|
||||
fwrite(line.constData(), 1, static_cast<size_t>(line.size()), stderr);
|
||||
|
||||
m_ring.append(line);
|
||||
writeToFile(line, type);
|
||||
}
|
||||
|
||||
void QetLogger::pruneOldLogFiles(int days)
|
||||
{
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDate today = QDate::currentDate();
|
||||
const QStringList filters = {
|
||||
QStringLiteral("????????.log"), // base files, e.g. 20260803.log
|
||||
QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log
|
||||
};
|
||||
|
||||
const QDir dir(m_log_dir);
|
||||
const auto entries = dir.entryInfoList(filters, QDir::Files);
|
||||
for (const QFileInfo &file_info : entries) {
|
||||
if (!file_info.isFile()) {
|
||||
continue;
|
||||
}
|
||||
// lastModified(), not lastRead(): reading the log (opening it to
|
||||
// attach to a bug report, a backup job, an indexer) must not
|
||||
// reset the retention clock and keep it alive indefinitely.
|
||||
if (file_info.lastModified().date().daysTo(today) > days) {
|
||||
QFile::remove(file_info.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETLOGGER_H
|
||||
#define QETLOGGER_H
|
||||
|
||||
#include "logring.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
/**
|
||||
@brief The QetLogger class
|
||||
Rework of QET's diagnostic logging (discussion #644, steps 1-3):
|
||||
|
||||
- Step 1: one file handle held open for the session under a mutex
|
||||
instead of opening/closing per message; the log path (including
|
||||
the date-stamped filename) is resolved exactly once, at init(),
|
||||
instead of being recomputed on every message -- a session that
|
||||
crosses midnight now stays in one file; retention now uses
|
||||
lastModified() instead of lastRead(); stderr and file output both
|
||||
use UTF-8 explicitly (previously stderr used the local 8-bit
|
||||
codec and the file's encoding silently differed between Qt5 and
|
||||
Qt6).
|
||||
- Step 2: the previously-unbounded daily file is now size-capped
|
||||
and rotated (kMaxFileBytes per file, kRotationKeep old files kept
|
||||
beyond the current one); each message is truncated to
|
||||
kMaxMessageBytes and control characters are escaped before being
|
||||
written, so one pathological caller can't blow the size budget or
|
||||
forge log lines; the log file is refused if it already exists as
|
||||
a symlink and is created owner-read/write only.
|
||||
- Step 3: every formatted line is also appended to an in-memory
|
||||
LogRing (see logring.h) -- always on, fixed capacity, allocation-
|
||||
free on the hot path.
|
||||
|
||||
Deliberately NOT included in this step (see discussion #644): no
|
||||
signal handler / crash-flush (step 4), no diagnostics export UI
|
||||
(step 5), no log categories, no session header, no repeat collapsing
|
||||
or rate limiting. Those are independent, separately-scoped follow-ups.
|
||||
|
||||
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
|
||||
init() time, this class does nothing beyond a minimal, independent
|
||||
stderr passthrough -- no ring, no file, no rotation -- so a problem
|
||||
in this rework can be worked around without a rebuild.
|
||||
*/
|
||||
class QetLogger
|
||||
{
|
||||
public:
|
||||
static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file
|
||||
static constexpr int kRotationKeep = 4; // .1.log .. .4.log
|
||||
static constexpr int kMaxMessageBytes = 4096; // per-message truncation
|
||||
|
||||
static QetLogger &instance();
|
||||
|
||||
/// Must be called exactly once, from main(), before
|
||||
/// qInstallMessageHandler(). Resolves the log directory and the
|
||||
/// session's log filename, and opens the file.
|
||||
void init();
|
||||
|
||||
/// The function installed via qInstallMessageHandler() forwards here.
|
||||
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||
|
||||
/// Replaces the old delete_old_log_files(): same call shape, fixed
|
||||
/// to use lastModified() (not lastRead()) and to also match rotated
|
||||
/// file names.
|
||||
void pruneOldLogFiles(int days);
|
||||
|
||||
/// Snapshot of the in-memory ring, oldest first. For future use
|
||||
/// (e.g. a diagnostics export action) -- not wired to any UI here.
|
||||
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
|
||||
|
||||
private:
|
||||
QetLogger() = default;
|
||||
QetLogger(const QetLogger &) = delete;
|
||||
|
||||
bool ensureFileOpenLocked();
|
||||
void rotateLocked();
|
||||
void writeToFile(const QByteArray &line, QtMsgType type);
|
||||
QString rotatedPath(int index) const;
|
||||
|
||||
static QByteArray sanitize(const QByteArray &input);
|
||||
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
|
||||
static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg);
|
||||
|
||||
bool m_disabled = false;
|
||||
|
||||
QString m_log_dir;
|
||||
QString m_base_name; // e.g. "20260803", resolved once in init()
|
||||
|
||||
QMutex m_file_mutex;
|
||||
QFile m_file;
|
||||
qint64 m_bytes_written_current_file = 0;
|
||||
bool m_file_output_ok = false;
|
||||
|
||||
LogRing m_ring;
|
||||
};
|
||||
|
||||
#endif // QETLOGGER_H
|
||||
+15
-123
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "machine_info.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
@@ -62,131 +63,16 @@ class EarlyFileOpenCatcher : public QObject
|
||||
#endif
|
||||
|
||||
/**
|
||||
@brief myMessageOutput
|
||||
for debugging
|
||||
@param type : the messages that can be sent to a message handler
|
||||
@param context : were? wat?
|
||||
@param msg : Message
|
||||
@brief qetLogMessageHandler
|
||||
Installed via qInstallMessageHandler(); forwards to QetLogger, which
|
||||
holds all the actual formatting/ring/rotation state. See
|
||||
logging/qetlogger.h for the rationale (discussion #644).
|
||||
*/
|
||||
void myMessageOutput(QtMsgType type,
|
||||
void qetLogMessageHandler(QtMsgType type,
|
||||
const QMessageLogContext &context,
|
||||
const QString &msg)
|
||||
{
|
||||
|
||||
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
QByteArray dbs =txt.toLocal8Bit();
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
fprintf(stderr,
|
||||
"%s Debug: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Debug: ";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
fprintf(stderr,
|
||||
"%s Info: %s \n",
|
||||
dbs.constData(),
|
||||
localMsg.constData());
|
||||
txt+=" Info: ";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
fprintf(stderr,
|
||||
"%s Warning: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file, context.line,
|
||||
function);
|
||||
txt+=" Warning: ";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Critical: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Critical: ";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Fatal: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Fatal: ";
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr,
|
||||
"%s Unknown: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Unknown: ";
|
||||
}
|
||||
txt+= msg;
|
||||
if(type==QtInfoMsg){
|
||||
txt+=" \n";
|
||||
} else {
|
||||
txt+= " (";
|
||||
txt+= context.file ? context.file : "";
|
||||
txt+= ":";
|
||||
txt+=QString::number(context.line ? context.line :0);
|
||||
txt+= ", ";
|
||||
txt+= context.function ? context.function : "";
|
||||
txt+=")\n";
|
||||
}
|
||||
QFile outFile(QETApp::dataDir()
|
||||
+"/"
|
||||
+QDate::currentDate().toString("yyyyMMdd")
|
||||
+".log");
|
||||
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
|
||||
{
|
||||
QTextStream ts(&outFile);
|
||||
ts << txt;
|
||||
}
|
||||
outFile.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief delete_old_log_files
|
||||
delete old log files
|
||||
@param days : max days old
|
||||
*/
|
||||
void delete_old_log_files(int days)
|
||||
{
|
||||
const QDate today = QDate::currentDate();
|
||||
const QString path = QETApp::dataDir() % "/";
|
||||
|
||||
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
|
||||
filter = filter.arg("[0123456789]"); // valid characters
|
||||
|
||||
Q_FOREACH (auto fileInfo,
|
||||
QDir(path).entryInfoList(
|
||||
QStringList(filter),
|
||||
QDir::Files))
|
||||
{
|
||||
if (fileInfo.lastRead().date().daysTo(today) > days)
|
||||
{
|
||||
QString filepath = fileInfo.absoluteFilePath();
|
||||
QDir deletefile;
|
||||
deletefile.setPath(filepath);
|
||||
deletefile.remove(filepath);
|
||||
qDebug() << "File " % filepath % " is deleted!";
|
||||
}
|
||||
}
|
||||
QetLogger::instance().handleMessage(type, context, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,13 +139,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the logger's state (log directory, session filename, open
|
||||
// file handle) explicitly here, immediately before installing the
|
||||
// handler -- not implicitly on whichever thread happens to log
|
||||
// first. See QetLogger::init().
|
||||
//
|
||||
// Install the log-file message handler BEFORE the application starts:
|
||||
// QETApp's constructor does the whole startup (collections, editor,
|
||||
// opening the projects given on the command line), so installing the
|
||||
// handler afterwards - as was done in the startup worker below - meant
|
||||
// exactly the interesting lines (collection and project load timers)
|
||||
// went to stderr, which is invisible in a Windows GUI session.
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
QetLogger::instance().init();
|
||||
qInstallMessageHandler(qetLogMessageHandler);
|
||||
|
||||
SingleApplication app(argc, argv, true);
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -308,7 +200,7 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
{
|
||||
qInfo("Start-up");
|
||||
// delete old log files of max 7 days old.
|
||||
delete_old_log_files(7);
|
||||
QetLogger::instance().pruneOldLogFiles(7);
|
||||
MachineInfo::instance()->send_info_to_debug();
|
||||
});
|
||||
return app.exec();
|
||||
|
||||
Reference in New Issue
Block a user