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:
@@ -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
|
||||
Reference in New Issue
Block a user