/*
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 .
*/
#ifndef QETLOGGER_H
#define QETLOGGER_H
#include "logring.h"
#include
#include
#include
#include
/**
@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.
- Step 4: installCrashHandler() wires the ring up to CrashHandler
(see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or,
on Windows, an unhandled structured exception) flushes the ring to
a fixed crash-dump file before the process dies.
- Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/
clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles())
notice and offer an unretrieved crash dump from the *previous* run;
buildDiagnosticsReport() is the equivalent for a manual "save a
report right now" action on the *current*, still-running session.
Both go through redact() before ever reaching the user, since both
are destined for a public bug tracker.
Deliberately NOT included: log categories, a full session header
beyond what the crash dump/report already carry, repeat collapsing,
rate limiting. Those are listed in discussion #644 under "best
practices worth building in", not part of the numbered steps.
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 constexpr int kMaxPendingCrashDumps = 10; // newest kept, rest pruned
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();
/// Step 4: installs the crash handler (see crashhandler.h). Must
/// be called after init() (the ring and the dump path must exist
/// first) and, like init(), only once.
void installCrashHandler();
/// 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.
QVector ringSnapshot() const {return m_ring.snapshot();}
// --- Step 5: getting the data back out -------------------------
/// True if a previous run's crash handler left an unretrieved
/// dump behind.
bool hasPendingCrashDump() const;
/// Dumps left by previous runs, newest first, capped at
/// kMaxPendingCrashDumps. This run's own dump path is never
/// included. Take this list once and pass the same list to
/// pendingCrashDumpContents() and clearPendingCrashDump(): that
/// is what makes "only what was offered gets deleted" true,
/// rather than re-reading the directory at each step and
/// deleting a dump that arrived in between unseen.
QStringList pendingCrashDumpFiles() const;
/// Raw contents of `files`, newest first, concatenated and
/// redacted. Deletes nothing -- pass the same list to
/// clearPendingCrashDump() once it has been offered.
QByteArray pendingCrashDumpContents(const QStringList &files) const;
/// Deletes exactly `files`, nothing else. Call after the user
/// has been offered them (whether they chose to save them or
/// not) so they are never offered a second time.
void clearPendingCrashDump(const QStringList &files);
/// Builds a redacted diagnostics bundle from the *current* session
/// (header + this session's log file so far) for the manual
/// "Save report" action -- as opposed to pendingCrashDumpContents(),
/// which is about a *previous*, already-terminated session.
QByteArray buildDiagnosticsReport() const;
/// Replaces occurrences of the user's home directory with "~",
/// and an AppImage's per-run /tmp/.mount_XXXXXX prefix with
/// "" -- the latter because backtrace_symbols_fd()
/// writes absolute module paths into the dump. Applied to both
/// the crash dump and buildDiagnosticsReport() before they are
/// ever shown to the user, since both are destined for a public
/// bug tracker.
static QByteArray redact(const QByteArray &input);
private:
QetLogger() = default;
QetLogger(const QetLogger &) = delete;
bool ensureFileOpenLocked();
void rotateLocked();
void writeToFile(const QByteArray &line, QtMsgType type);
QString rotatedPath(int index) const;
/// Pure path getter: creates nothing. Callers that are about
/// to write there call ensureCrashDumpDir() instead.
QString crashDumpDir() const;
QString ensureCrashDumpDir() const;
QString buildCrashDumpPath() const;
/// Moves a crash_dump.log left by a pre-#905 version into
/// crashes/, so upgrading does not strand it unoffered.
void migrateLegacyCrashDump() const;
/// Keeps the newest kMaxPendingCrashDumps dumps and deletes
/// the rest, so a crash loop cannot fill the log directory
/// before anyone gets the chance to see a dialog.
void pruneCrashDumps() const;
QString currentLogFilePath() 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()
/// This run's own dump path, fixed at installCrashHandler():
/// the handler writes here, and it is excluded when collecting
/// dumps left by previous runs.
QString m_crash_dump_path;
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