1
0
forked from BRT/arc
arc/views/log/FileLogWindow.cpp

306 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "FileLogWindow.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QMetaObject>
#include <QRegularExpression>
#include <QSettings>
#include <QProcess>
#include <QPixmap>
#include <QVBoxLayout>
#include "dump/TaskDumpRecorder.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "log/LogDetailWindow.h"
#include "net/NetworkService.h"
static constexpr int COL_ID = 0;
static constexpr int COL_TIME = 1;
static constexpr int COL_LEVEL = 2;
static constexpr int COL_SOURCE = 3;
static constexpr int COL_MESSAGE = 4;
FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("Логи");
resize(1200, 650);
m_table = new QTableWidget(this);
m_table->setColumnCount(5);
m_table->setHorizontalHeaderLabels({"#", "Время", "Уровень", "Источник", "Сообщение"});
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->verticalHeader()->hide();
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->setColumnWidth(COL_ID, 45);
m_table->setColumnWidth(COL_TIME, 145);
m_table->setColumnWidth(COL_LEVEL, 80);
m_table->setColumnWidth(COL_SOURCE, 250);
// Панель скриншота
m_screenshotLabel = new QLabel(this);
m_screenshotLabel->setAlignment(Qt::AlignCenter);
m_screenshotLabel->setMinimumWidth(300);
m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;");
m_screenshotLabel->setText("Выберите запись\nдля просмотра скриншота");
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(m_table);
splitter->addWidget(m_screenshotLabel);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
m_showAllCheck = new QCheckBox("Показать все (включая DEBUG/INFO)", this);
m_prevBtn = new QPushButton("", this);
m_nextBtn = new QPushButton("", this);
m_pageLabel = new QLabel(this);
m_pageLabel->setAlignment(Qt::AlignCenter);
m_pageLabel->setMinimumWidth(140);
auto *refreshBtn = new QPushButton("Обновить", this);
m_sendBtn = new QPushButton("Отправить", this);
m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;");
auto *paginationLayout = new QHBoxLayout;
paginationLayout->addWidget(m_prevBtn);
paginationLayout->addWidget(m_pageLabel);
paginationLayout->addWidget(m_nextBtn);
paginationLayout->addStretch();
paginationLayout->addWidget(m_showAllCheck);
paginationLayout->addWidget(refreshBtn);
paginationLayout->addWidget(m_sendBtn);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(splitter, 1);
mainLayout->addLayout(paginationLayout);
connect(m_prevBtn, &QPushButton::clicked, this, [this]() {
if (m_page > 0) { --m_page; loadPage(); }
});
connect(m_nextBtn, &QPushButton::clicked, this, [this]() {
if (m_page < m_totalPages - 1) { ++m_page; loadPage(); }
});
connect(refreshBtn, &QPushButton::clicked, this, [this]() {
m_page = 0;
loadPage();
});
connect(m_showAllCheck, &QCheckBox::toggled, this, [this]() {
m_page = 0;
loadPage();
});
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
showScreenshot(row);
});
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
if (row >= 0 && row < m_entries.size()) {
auto *detail = new LogDetailWindow(m_entries[row], this);
detail->setAttribute(Qt::WA_DeleteOnClose);
detail->show();
}
});
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogsToBackend);
loadPage();
}
QStringList FileLogWindow::currentLevelFilter() const {
if (m_showAllCheck->isChecked())
return {};
return {"WARNING", "CRITICAL", "FATAL"};
}
void FileLogWindow::loadPage() {
const QStringList levels = currentLevelFilter();
const int total = GeneralLogDAO::getTotalCount(levels);
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
m_page = qBound(0, m_page, m_totalPages - 1);
m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels);
m_table->setRowCount(static_cast<int>(m_entries.size()));
for (int i = 0; i < m_entries.size(); ++i) {
const auto &e = m_entries[i];
m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id)));
m_table->setItem(i, COL_TIME, new QTableWidgetItem(
e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss")));
m_table->setItem(i, COL_LEVEL, new QTableWidgetItem(e.level));
m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source));
m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message));
}
updatePagination();
}
void FileLogWindow::showScreenshot(int row) {
if (row < 0 || row >= m_entries.size()) return;
const QByteArray &data = m_entries[row].screenshot;
if (data.isEmpty()) {
m_screenshotLabel->setText("Нет скриншота");
return;
}
QPixmap pixmap;
pixmap.loadFromData(data);
if (pixmap.isNull()) {
m_screenshotLabel->setText("Ошибка загрузки");
return;
}
m_screenshotLabel->setPixmap(pixmap.scaled(
m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void FileLogWindow::updatePagination() {
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
m_prevBtn->setEnabled(m_page > 0);
m_nextBtn->setEnabled(m_page < m_totalPages - 1);
}
void FileLogWindow::sendLogsToBackend() {
qDebug() << "[sendLogsToBackend] step 1: checking log file";
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
return;
}
// Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath,
// с разворотом '~' в домашнюю директорию и приведением к абсолютному пути
// относительно applicationDirPath, чтобы не зависеть от текущего CWD.
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);
}
if (dbPath.isEmpty() || !QFile::exists(dbPath)) {
QMessageBox::warning(this, "Ошибка",
"Файл БД не найден: " + (dbPath.isEmpty() ? QStringLiteral("<пусто>") : dbPath));
return;
}
NetworkService *svc = TaskDumpRecorder::networkService();
if (!svc) {
QMessageBox::warning(this, "Ошибка",
"NetworkService не инициализирован — отправка на бэкенд недоступна.");
return;
}
m_sendBtn->setEnabled(false);
m_sendBtn->setText("Подготовка...");
qDebug() << "[sendLogsToBackend] step 2: creating tmp dir";
const QString tmpDir = QDir::tempPath() + "/arc_logs_export";
QDir(tmpDir).removeRecursively();
QDir().mkpath(tmpDir);
// 1. application.log целиком
QFile::copy(logPath, tmpDir + "/application.log");
// 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL
// снимок будет рассогласованным.
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);
}
}
qDebug() << "[sendLogsToBackend] step 3: creating archive";
const QString archivePath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(archivePath);
#ifdef Q_OS_WIN
QProcess archiver;
archiver.setWorkingDirectory(tmpDir);
archiver.start("powershell", QStringList()
<< "-NoProfile" << "-Command"
<< QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath));
if (!archiver.waitForFinished(30000)) {
qWarning() << "[sendLogsToBackend] powershell archiver timeout/error:" << archiver.errorString();
}
qDebug() << "[sendLogsToBackend] archiver exit code:" << archiver.exitCode()
<< "stdout:" << archiver.readAllStandardOutput()
<< "stderr:" << archiver.readAllStandardError();
#else
QProcess archiver;
archiver.setWorkingDirectory(tmpDir);
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
archiver.waitForFinished(30000);
#endif
QDir(tmpDir).removeRecursively();
if (!QFile::exists(archivePath)) {
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
QFile zipFile(archivePath);
if (!zipFile.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
QFile::remove(archivePath);
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
const QByteArray archiveBytes = zipFile.readAll();
zipFile.close();
QFile::remove(archivePath);
if (archiveBytes.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Архив пуст — отправлять нечего");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
const QString apiToken = SettingsDAO::get("api_key");
const QDateTime now = QDateTime::currentDateTime();
const QString timestamp = now.toString(Qt::ISODate);
QString header = QStringLiteral("[LOGS]");
if (!apiToken.isEmpty()) header += QStringLiteral(" [%1]").arg(apiToken.toHtmlEscaped());
const QString text = QStringLiteral("<b>%1</b>\n%2").arg(header, timestamp.toHtmlEscaped());
QString filenameSafeToken = apiToken;
filenameSafeToken.replace(QRegularExpression(R"([^\w\-.])"), "_");
const QString filenameStamp = now.toString("yyyy-MM-dd_HH-mm-ss");
const QString archiveFilename = filenameSafeToken.isEmpty()
? QStringLiteral("LOGS_%1.zip").arg(filenameStamp)
: QStringLiteral("LOGS_%1_%2.zip").arg(filenameSafeToken, filenameStamp);
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
<< "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
Qt::QueuedConnection,
Q_ARG(QString, text),
Q_ARG(QByteArray, archiveBytes),
Q_ARG(QByteArray, QByteArray()),
Q_ARG(QString, archiveFilename));
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
if (!dispatched) {
QMessageBox::warning(this, "Ошибка",
"Не удалось поставить отправку в очередь NetworkService.");
return;
}
QMessageBox::information(this, "Готово",
"Логи поставлены в очередь на отправку на бэкенд.");
}