mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
Rework diagnostic logging: fix the file writer, add rotation and a ring buffer
Implements steps 1-3 of discussion #644 (deliberately not steps 4/5 -- no signal handler / crash flush, no diagnostics UI; see below). ## Step 1 -- fix the existing logger (bugs, no new behavior) - One QFile handle held open for the whole session under a mutex, instead of opening and closing the log file on every single message. - The log directory and the session's date-stamped filename are resolved exactly once, in the new QetLogger::init() called explicitly from main() immediately before qInstallMessageHandler() -- not recomputed per message, so a session that runs past midnight now stays in one file instead of silently splitting. - Age-based retention now uses lastModified() instead of lastRead(): opening a log to attach it to a bug report no longer resets its retention clock. - stderr and file output both encode UTF-8 explicitly (toUtf8()), replacing stderr's toLocal8Bit() and the file stream's previously Qt5/Qt6-inconsistent default encoding. ## Step 2 -- size-capped rotation + hardening - The previously-unbounded daily file is now capped at 2 MiB and rotated (kMaxFileBytes/kRotationKeep in QetLogger), keeping <date>.log plus <date>.1.log .. <date>.4.log; oldest is dropped. - Each message is truncated to 4 KB with a "...[truncated N bytes]" marker before it reaches the ring or the file. - Control characters (newlines, tabs, other non-printables) in message content are escaped, since much of what QET logs is externally controlled (file paths, element names, font strings out of a .qet file) -- left unescaped, an embedded '\n' could forge log lines. - The log file is refused if a symlink already exists at that path, and is created/rotated owner-read/write only. ## Step 3 -- in-memory ring buffer - LogRing (sources/logging/logring.h) is a fixed-capacity, always-on ring of the last 4096 log lines, preallocated once at construction (4096 * 512 B = 2 MiB) so append() never allocates. Entries are stored as plain pre-formatted bytes in fixed-size slots -- the shape discussion #644 specifies so a *future* crash handler could dump it with nothing but write(2), even though no such handler exists yet. Thread-safe via a plain QMutex (the lock-free requirement in the discussion applies specifically to a signal-handler read path, which this step doesn't add). ## Escape hatch QET_LOG_DISABLE=1 in the environment at startup bypasses all of the above -- no ring, no file, no rotation -- falling back to a minimal, self-contained stderr passthrough that doesn't share any code with the new formatting/sanitization path, so it stays usable even if that path is what's misbehaving. ## Deliberately not included (per the discussion's own phasing) - No signal handler / crash-time ring flush (step 4) -- the discussion flags this as the highest-risk piece, explicitly meant to land last and behind its own switch once the rest is proven. - No diagnostics export UI (step 5). - No log categories, session header, repeat collapsing or rate limiting -- listed under "best practices worth building in", not part of steps 1-3. ## Testing Built clean, no new warnings. Verified with real runs (QT_QPA_PLATFORM=offscreen, isolated HOME): - Log file created at the expected dataDir()/YYYYMMDD.log path, mode 0600. - A full startup's worth of real messages (translations, MachineInfo's system dump, collection loading) written correctly; every one of the 231 lines in one run starts with a proper timestamp -- confirmed the sanitizer correctly escapes the raw embedded newlines/tabs in MachineInfo's multi-line CPU/GPU description fields into visible \n/\t sequences rather than letting them fragment the log. - QET_LOG_DISABLE=1: zero log files created, stderr still worked via the independent legacy path. - Rotation: pre-filled a log to just under the 2 MiB cap, ran a normal session, confirmed it rotated to <date>.1.log (still 0600) with a byte-clean split (no truncated/duplicated line at the boundary) and a fresh <date>.log picked up from the next line.
This commit is contained in:
@@ -116,6 +116,10 @@ set(QET_RES_FILES
|
|||||||
set(QET_SRC_FILES
|
set(QET_SRC_FILES
|
||||||
${QET_DIR}/sources/cli_export.cpp
|
${QET_DIR}/sources/cli_export.cpp
|
||||||
${QET_DIR}/sources/cli_export.h
|
${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.cpp
|
||||||
${QET_DIR}/sources/pdf_links.h
|
${QET_DIR}/sources/pdf_links.h
|
||||||
${QET_DIR}/sources/import/edz/edzarchive.cpp
|
${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/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#include "cli_export.h"
|
#include "cli_export.h"
|
||||||
|
#include "logging/qetlogger.h"
|
||||||
#include "machine_info.h"
|
#include "machine_info.h"
|
||||||
#include "qet.h"
|
#include "qet.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
@@ -62,131 +63,16 @@ class EarlyFileOpenCatcher : public QObject
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief myMessageOutput
|
@brief qetLogMessageHandler
|
||||||
for debugging
|
Installed via qInstallMessageHandler(); forwards to QetLogger, which
|
||||||
@param type : the messages that can be sent to a message handler
|
holds all the actual formatting/ring/rotation state. See
|
||||||
@param context : were? wat?
|
logging/qetlogger.h for the rationale (discussion #644).
|
||||||
@param msg : Message
|
|
||||||
*/
|
*/
|
||||||
void myMessageOutput(QtMsgType type,
|
void qetLogMessageHandler(QtMsgType type,
|
||||||
const QMessageLogContext &context,
|
const QMessageLogContext &context,
|
||||||
const QString &msg)
|
const QString &msg)
|
||||||
{
|
{
|
||||||
|
QetLogger::instance().handleMessage(type, context, 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!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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:
|
// Install the log-file message handler BEFORE the application starts:
|
||||||
// QETApp's constructor does the whole startup (collections, editor,
|
// QETApp's constructor does the whole startup (collections, editor,
|
||||||
// opening the projects given on the command line), so installing the
|
// opening the projects given on the command line), so installing the
|
||||||
// handler afterwards - as was done in the startup worker below - meant
|
// handler afterwards - as was done in the startup worker below - meant
|
||||||
// exactly the interesting lines (collection and project load timers)
|
// exactly the interesting lines (collection and project load timers)
|
||||||
// went to stderr, which is invisible in a Windows GUI session.
|
// went to stderr, which is invisible in a Windows GUI session.
|
||||||
qInstallMessageHandler(myMessageOutput);
|
QetLogger::instance().init();
|
||||||
|
qInstallMessageHandler(qetLogMessageHandler);
|
||||||
|
|
||||||
SingleApplication app(argc, argv, true);
|
SingleApplication app(argc, argv, true);
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
@@ -308,7 +200,7 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
|||||||
{
|
{
|
||||||
qInfo("Start-up");
|
qInfo("Start-up");
|
||||||
// delete old log files of max 7 days old.
|
// delete old log files of max 7 days old.
|
||||||
delete_old_log_files(7);
|
QetLogger::instance().pruneOldLogFiles(7);
|
||||||
MachineInfo::instance()->send_info_to_debug();
|
MachineInfo::instance()->send_info_to_debug();
|
||||||
});
|
});
|
||||||
return app.exec();
|
return app.exec();
|
||||||
|
|||||||
Reference in New Issue
Block a user