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:
ispyisail
2026-08-03 14:25:42 +12:00
parent 834495387b
commit ff812f221a
6 changed files with 636 additions and 123 deletions
+4
View File
@@ -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
+91
View File
@@ -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;
}
+77
View File
@@ -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
+335
View File
@@ -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());
}
}
}
+114
View File
@@ -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
View File
@@ -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();