mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 18:14: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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user