Add LogArchiveSender for automated log and database archiving with support for background processing and dispatching through NetworkService.
This commit is contained in:
parent
07871231a3
commit
4ab46b8b49
137
services/LogArchiveSender.cpp
Normal file
137
services/LogArchiveSender.cpp
Normal file
@ -0,0 +1,137 @@
|
||||
#include "LogArchiveSender.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
|
||||
#include "dao/SettingsDAO.h"
|
||||
#include "net/NetworkService.h"
|
||||
|
||||
namespace {
|
||||
|
||||
QString resolveDbPath() {
|
||||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||
QString dbPath = settings.value("db/dbPath").toString();
|
||||
if (dbPath.startsWith("~"))
|
||||
dbPath.replace(0, 1, QDir::homePath());
|
||||
if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative())
|
||||
dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath);
|
||||
return dbPath;
|
||||
}
|
||||
|
||||
// Запускается в worker-потоке. Возвращает байты zip или пустой массив.
|
||||
QByteArray buildArchive() {
|
||||
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
|
||||
const QString dbPath = resolveDbPath();
|
||||
|
||||
const bool hasLog = QFile::exists(logPath);
|
||||
const bool hasDb = !dbPath.isEmpty() && QFile::exists(dbPath);
|
||||
if (!hasLog && !hasDb) {
|
||||
qWarning() << "[LogArchiveSender] neither log nor db exists — nothing to archive";
|
||||
return {};
|
||||
}
|
||||
|
||||
const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") +
|
||||
QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
QDir().mkpath(tmpDir);
|
||||
|
||||
if (hasLog) {
|
||||
QFile::copy(logPath, tmpDir + "/application.log");
|
||||
}
|
||||
if (hasDb) {
|
||||
const QString dbBaseName = QFileInfo(dbPath).fileName();
|
||||
QFile::copy(dbPath, tmpDir + "/" + dbBaseName);
|
||||
for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) {
|
||||
const QString sidecar = dbPath + suffix;
|
||||
if (QFile::exists(sidecar))
|
||||
QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix);
|
||||
}
|
||||
}
|
||||
|
||||
const QString archivePath = tmpDir + ".zip";
|
||||
QFile::remove(archivePath);
|
||||
QProcess archiver;
|
||||
archiver.setWorkingDirectory(tmpDir);
|
||||
#ifdef Q_OS_WIN
|
||||
archiver.start("powershell", QStringList()
|
||||
<< "-NoProfile" << "-Command"
|
||||
<< QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath));
|
||||
#else
|
||||
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
|
||||
#endif
|
||||
archiver.waitForFinished(30000);
|
||||
|
||||
QDir(tmpDir).removeRecursively();
|
||||
|
||||
if (!QFile::exists(archivePath)) {
|
||||
qWarning() << "[LogArchiveSender] archive not created at" << archivePath;
|
||||
return {};
|
||||
}
|
||||
QFile zip(archivePath);
|
||||
if (!zip.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "[LogArchiveSender] cannot open archive" << archivePath;
|
||||
QFile::remove(archivePath);
|
||||
return {};
|
||||
}
|
||||
const QByteArray bytes = zip.readAll();
|
||||
zip.close();
|
||||
QFile::remove(archivePath);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LogArchiveSender::dispatch(const QString &label, int eventId) {
|
||||
const QString apiToken = SettingsDAO::get("api_key");
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
const QString timestamp = now.toString(Qt::ISODate);
|
||||
|
||||
QStringList head{QStringLiteral("[LOGS]")};
|
||||
if (!apiToken.isEmpty()) head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped());
|
||||
if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped());
|
||||
const QString text = QStringLiteral("<b>%1</b>\n%2")
|
||||
.arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped());
|
||||
|
||||
QString tokenSafe = apiToken;
|
||||
tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_");
|
||||
const QString stamp = now.toString("yyyy-MM-dd_HH-mm-ss");
|
||||
QStringList parts{QStringLiteral("LOGS")};
|
||||
if (!tokenSafe.isEmpty()) parts << tokenSafe;
|
||||
parts << stamp;
|
||||
if (!label.isEmpty()) parts << label;
|
||||
if (eventId > 0) parts << QStringLiteral("ev%1").arg(eventId);
|
||||
const QString archiveFilename = parts.join(QStringLiteral("_")) + QStringLiteral(".zip");
|
||||
|
||||
auto *worker = QThread::create([text, archiveFilename]() {
|
||||
const QByteArray archive = buildArchive();
|
||||
if (archive.isEmpty()) {
|
||||
qWarning() << "[LogArchiveSender] empty archive, skipping dispatch";
|
||||
return;
|
||||
}
|
||||
|
||||
auto *thread = new QThread;
|
||||
auto *svc = new NetworkService;
|
||||
svc->moveToThread(thread);
|
||||
QObject::connect(thread, &QThread::started, svc,
|
||||
[svc, text, archive, archiveFilename]() {
|
||||
svc->postMonitoringDump(text, archive, QByteArray(), archiveFilename);
|
||||
emit svc->finished();
|
||||
});
|
||||
QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit);
|
||||
QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater);
|
||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
thread->start();
|
||||
});
|
||||
QObject::connect(worker, &QThread::finished, worker, &QObject::deleteLater);
|
||||
worker->start();
|
||||
return true;
|
||||
}
|
||||
16
services/LogArchiveSender.h
Normal file
16
services/LogArchiveSender.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
// Headless-варианты сборки и отправки в Telegram-relay архива из application.log
|
||||
// + БД (+ WAL/SHM). Используется EventHandler-ом для авто-отправки при ошибках задач.
|
||||
// FileLogWindow по-прежнему имеет свою UI-обвязку для ручной отправки.
|
||||
namespace LogArchiveSender {
|
||||
// Собирает архив и постит через свежий NetworkService::postMonitoringDump
|
||||
// в собственных background-потоках (worker для zip + thread для HTTP).
|
||||
// `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла.
|
||||
// `eventId` (если > 0) тоже уходит в имя файла для трассировки.
|
||||
// Возвращает true если задача поставлена в очередь; реальный результат отправки
|
||||
// не отслеживается (fire-and-forget, как у sendMonitoringLog).
|
||||
bool dispatch(const QString &label, int eventId = -1);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user