Files
qelectrotech-source-mirror/sources/logging/logring.cpp
T
ispyisail ff812f221a 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.
2026-08-03 14:25:42 +12:00

92 lines
2.5 KiB
C++

/*
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;
}