1 /*
2 * \copyright Copyright (c) 2014-2021 Governikus GmbH & Co. KG, Germany
3 */
4
5 #include "LogHandler.h"
6
7 #include "SingletonHelper.h"
8
9 #include <QCoreApplication>
10 #include <QDir>
11 #include <QScopeGuard>
12 #include <QStringBuilder>
13
14 using namespace governikus;
15
16 defineSingleton(LogHandler)
17
Q_DECLARE_LOGGING_CATEGORY(fileprovider)18 Q_DECLARE_LOGGING_CATEGORY(fileprovider)
19 Q_DECLARE_LOGGING_CATEGORY(securestorage)
20 Q_DECLARE_LOGGING_CATEGORY(configuration)
21
22 #define LOGCAT(name) QString::fromLatin1(name().categoryName())
23
24
25 #if !defined(Q_OS_ANDROID) && !defined(QT_USE_JOURNALD)
26 #define ENABLE_MESSAGE_PATTERN
27 #endif
28
29
30 LogHandler::LogHandler()
31 : mEventHandler()
32 , mEnvPattern(!qEnvironmentVariableIsEmpty("QT_MESSAGE_PATTERN"))
33 , mFunctionFilenameSize(74)
34 , mBacklogPosition(0)
35 , mCriticalLog(false)
36 , mCriticalLogWindow(10)
37 , mCriticalLogIgnore({LOGCAT(fileprovider), LOGCAT(securestorage), LOGCAT(configuration)})
38 , mMessagePattern(QStringLiteral("%{category} %{time yyyy.MM.dd hh:mm:ss.zzz} %{threadid} %{if-debug} %{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif} %{function}(%{file}:%{line}) %{message}"))
39 , mDefaultMessagePattern(QStringLiteral("%{if-category}%{category}: %{endif}%{message}")) // as defined in qlogging.cpp
40 , mLogFile()
41 , mHandler(nullptr)
42 , mUseHandler(true)
43 , mFilePrefix("/src/")
44 , mMutex()
45 {
46 }
47
48
~LogHandler()49 LogHandler::~LogHandler()
50 {
51 reset();
52 }
53
54
getLogFileTemplate()55 QString LogHandler::getLogFileTemplate()
56 {
57 // if you change value you need to adjust getOtherLogfiles()
58 return QDir::tempPath() % QLatin1Char('/') % QCoreApplication::applicationName() % QStringLiteral(".XXXXXX.log");
59 }
60
61
reset()62 void LogHandler::reset()
63 {
64 const QMutexLocker mutexLocker(&mMutex);
65 if (isInstalled())
66 {
67 qInstallMessageHandler(nullptr);
68 mHandler = nullptr;
69 }
70 }
71
72
init()73 void LogHandler::init()
74 {
75 const QMutexLocker mutexLocker(&mMutex);
76
77 QStringList rules;
78
79 // Enable this warning again when our minimum Qt version is 5.14
80 rules << QStringLiteral("qt.qml.connections.warning=false");
81
82 #ifndef QT_NO_DEBUG
83 rules << QStringLiteral("qt.qml.binding.removal.info=true");
84 #endif
85
86 QLoggingCategory::setFilterRules(rules.join(QLatin1Char('\n')));
87
88 if (mEventHandler.isNull())
89 {
90 mEventHandler = new LogEventHandler();
91 QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, mEventHandler.data(), &QObject::deleteLater);
92 QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, mEventHandler.data(), [this] {
93 mEventHandler.clear(); // clear immediately, otherwise logging in &QObject::destroyed is dangerous!
94 });
95 }
96
97 if (mLogFile.isNull())
98 {
99 mLogFile = new QTemporaryFile(getLogFileTemplate());
100 QObject::connect(QCoreApplication::instance(), &QCoreApplication::destroyed, mLogFile.data(), [this] {
101 delete this->mLogFile.data();
102 });
103
104 if (useLogfile())
105 {
106 mLogFile->open();
107 }
108
109 // Avoid deadlock with subsequent logging of this call.
110 QMetaObject::invokeMethod(mLogFile.data(), [this] {removeOldLogfiles();}, Qt::QueuedConnection);
111 }
112
113 if (!isInstalled())
114 {
115 mHandler = qInstallMessageHandler(&LogHandler::messageHandler);
116 }
117 }
118
119
getEventHandler() const120 const LogEventHandler* LogHandler::getEventHandler() const
121 {
122 return mEventHandler;
123 }
124
125
isInstalled() const126 bool LogHandler::isInstalled() const
127 {
128 return mHandler;
129 }
130
131
setAutoRemove(bool pRemove)132 bool LogHandler::setAutoRemove(bool pRemove)
133 {
134 if (mLogFile)
135 {
136 mLogFile->setAutoRemove(pRemove);
137 return true;
138 }
139
140 return false;
141 }
142
143
logToFile(const QString & pOutput)144 void LogHandler::logToFile(const QString& pOutput)
145 {
146 if (mLogFile && mLogFile->isOpen() && mLogFile->isWritable())
147 {
148 mLogFile->write(pOutput.toUtf8());
149 mLogFile->flush();
150 }
151 }
152
153
readLogFile(qint64 pStart,qint64 pLength)154 QByteArray LogHandler::readLogFile(qint64 pStart, qint64 pLength)
155 {
156 if (mLogFile && mLogFile->isOpen() && mLogFile->isReadable())
157 {
158 const auto currentPos = mLogFile->pos();
159 const auto resetPosition = qScopeGuard([this, currentPos] {
160 mLogFile->seek(currentPos);
161 });
162
163 mLogFile->seek(pStart);
164 return pLength > 0 ? mLogFile->read(pLength) : mLogFile->readAll();
165 }
166
167 if (useLogfile())
168 {
169 //: LABEL ALL_PLATFORMS
170 return QObject::tr("An error occurred in log handling: %1").arg(mLogFile->errorString()).toUtf8();
171 }
172
173 return QByteArray();
174 }
175
176
getBacklog(bool pAll)177 QByteArray LogHandler::getBacklog(bool pAll)
178 {
179 const QMutexLocker mutexLocker(&mMutex);
180 return readLogFile(pAll ? 0 : mBacklogPosition);
181 }
182
183
getCriticalLogWindow()184 QByteArray LogHandler::getCriticalLogWindow()
185 {
186 const QMutexLocker mutexLocker(&mMutex);
187
188 if (mCriticalLog)
189 {
190 const auto first = qAsConst(mCriticalLogWindow).first();
191 const auto last = qAsConst(mCriticalLogWindow).last();
192 return readLogFile(first.mPosition, last.mPosition - first.mPosition + last.mLength);
193 }
194
195 return QByteArray();
196 }
197
198
hasCriticalLog() const199 bool LogHandler::hasCriticalLog() const
200 {
201 return mCriticalLog;
202 }
203
204
getCriticalLogCapacity() const205 int LogHandler::getCriticalLogCapacity() const
206 {
207 return mCriticalLogWindow.capacity();
208 }
209
210
setCriticalLogCapacity(int pSize)211 void LogHandler::setCriticalLogCapacity(int pSize)
212 {
213 const QMutexLocker mutexLocker(&mMutex);
214 mCriticalLogWindow.setCapacity(pSize);
215 }
216
217
getFileDate(const QFileInfo & pInfo)218 QDateTime LogHandler::getFileDate(const QFileInfo& pInfo)
219 {
220 if (const auto& dateTime = pInfo.birthTime(); dateTime.isValid())
221 {
222 return dateTime;
223 }
224
225 return pInfo.metadataChangeTime();
226 }
227
228
getCurrentLogfileDate() const229 QDateTime LogHandler::getCurrentLogfileDate() const
230 {
231 return useLogfile() ? getFileDate(QFileInfo(*mLogFile)) : QDateTime();
232 }
233
234
resetBacklog()235 void LogHandler::resetBacklog()
236 {
237 const QMutexLocker mutexLocker(&mMutex);
238
239 if (useLogfile())
240 {
241 mBacklogPosition = mLogFile->pos();
242 mCriticalLog = false;
243 mCriticalLogWindow.clear();
244 }
245 }
246
247
copyMessageLogContext(const QMessageLogContext & pSource,QMessageLogContext & pDestination,const QByteArray & pFilename,const QByteArray & pFunction,const QByteArray & pCategory) const248 void LogHandler::copyMessageLogContext(const QMessageLogContext& pSource,
249 QMessageLogContext& pDestination,
250 const QByteArray& pFilename,
251 const QByteArray& pFunction,
252 const QByteArray& pCategory) const
253 {
254 pDestination.file = pFilename.isNull() ? pSource.file : pFilename.constData();
255 pDestination.function = pFunction.isNull() ? pSource.function : pFunction.constData();
256 pDestination.category = pCategory.isNull() ? pSource.category : pCategory.constData();
257
258 pDestination.line = pSource.line;
259 pDestination.version = pSource.version;
260 }
261
262
formatFilename(const char * const pFilename) const263 QByteArray LogHandler::formatFilename(const char* const pFilename) const
264 {
265 QByteArray filename(pFilename);
266
267 // Normalize the file name
268 filename.replace(QByteArrayLiteral("\\"), "/");
269
270 // Remove useless path
271 return filename.mid(filename.lastIndexOf(mFilePrefix) + mFilePrefix.size());
272 }
273
274
formatFunction(const char * const pFunction,const QByteArray & pFilename,int pLine) const275 QByteArray LogHandler::formatFunction(const char* const pFunction, const QByteArray& pFilename, int pLine) const
276 {
277 QByteArray function(pFunction);
278
279 // Remove namespace governikus::
280 function.replace(QByteArrayLiteral("governikus::"), "");
281
282 // Remove the parameter list
283 const auto start = function.indexOf('(');
284 const auto end = function.indexOf(')');
285 function = function.left(start) + function.mid(end + 1);
286
287 if (function.endsWith(" const"))
288 {
289 function = function.left(function.size() - 6);
290 }
291
292 // Remove the return type (if any)
293 if (function.indexOf(' ') != -1)
294 {
295 function = function.mid(function.lastIndexOf(' ') + 1);
296 }
297
298 // Trim function name
299 const auto size = mFunctionFilenameSize - 3 - pFilename.size() - QString::number(pLine).size();
300
301 if (size >= function.size())
302 {
303 return function;
304 }
305
306 if (size < 3)
307 {
308 return QByteArray();
309 }
310
311 if (size < 10)
312 {
313 return QByteArrayLiteral("...");
314 }
315
316 return QByteArrayLiteral("...") + function.right(size - 3);
317 }
318
319
formatCategory(const QByteArray & pCategory) const320 QByteArray LogHandler::formatCategory(const QByteArray& pCategory) const
321 {
322 const int MAX_CATEGORY_LENGTH = 10;
323 if (pCategory.length() > MAX_CATEGORY_LENGTH)
324 {
325 return pCategory.left(MAX_CATEGORY_LENGTH - 3) + QByteArrayLiteral("...");
326 }
327
328 return pCategory + QByteArray(MAX_CATEGORY_LENGTH - pCategory.size(), ' ');
329 }
330
331
getPaddedLogMsg(const QMessageLogContext & pContext,const QString & pMsg) const332 QString LogHandler::getPaddedLogMsg(const QMessageLogContext& pContext, const QString& pMsg) const
333 {
334 const auto paddingSize = (pContext.function == nullptr && pContext.file == nullptr && pContext.line == 0) ?
335 mFunctionFilenameSize - 18 : // padding for nullptr == "unknown(unknown:0)"
336 mFunctionFilenameSize - 3 - static_cast<int>(qstrlen(pContext.function)) - static_cast<int>(qstrlen(pContext.file)) - QString::number(pContext.line).size();
337
338 QString padding;
339 padding.reserve(paddingSize + pMsg.size() + 3);
340 padding.fill(QLatin1Char(' '), paddingSize);
341 padding += QStringLiteral(": ");
342 padding += pMsg;
343 return padding;
344 }
345
346
handleMessage(QtMsgType pType,const QMessageLogContext & pContext,const QString & pMsg)347 void LogHandler::handleMessage(QtMsgType pType, const QMessageLogContext& pContext, const QString& pMsg)
348 {
349 const QMutexLocker mutexLocker(&mMutex);
350
351 const QByteArray& filename = formatFilename(pContext.file);
352 const QByteArray& function = formatFunction(pContext.function, filename, pContext.line);
353 const QByteArray& category = formatCategory(pContext.category);
354
355 QMessageLogContext ctx;
356 copyMessageLogContext(pContext, ctx, filename, function, category);
357
358 const QString& message = mEnvPattern ? pMsg : getPaddedLogMsg(ctx, pMsg);
359
360 qSetMessagePattern(mMessagePattern);
361
362 #ifdef Q_OS_WIN
363 const QLatin1String lineBreak("\r\n");
364 #else
365 const QLatin1Char lineBreak('\n');
366 #endif
367
368 const QString logMsg = qFormatLogMessage(pType, ctx, message) + lineBreak;
369 handleLogWindow(pType, pContext.category, logMsg);
370 logToFile(logMsg);
371
372 if (Q_LIKELY(mUseHandler))
373 {
374 #ifdef ENABLE_MESSAGE_PATTERN
375 mHandler(pType, ctx, message);
376 #else
377 qSetMessagePattern(mDefaultMessagePattern);
378 mHandler(pType, ctx, pMsg);
379 #endif
380 }
381
382 if (mEventHandler)
383 {
384 Q_EMIT mEventHandler->fireRawLog(pMsg, QString::fromLatin1(pContext.category));
385 Q_EMIT mEventHandler->fireLog(logMsg);
386 }
387 }
388
389
handleLogWindow(QtMsgType pType,const char * pCategory,const QString & pMsg)390 void LogHandler::handleLogWindow(QtMsgType pType, const char* pCategory, const QString& pMsg)
391 {
392 if (!useLogfile())
393 {
394 return;
395 }
396
397 if (mCriticalLog && mCriticalLogWindow.isFull())
398 {
399 return;
400 }
401 else if (pType == QtCriticalMsg && !mCriticalLogIgnore.contains(QLatin1String(pCategory)))
402 {
403 mCriticalLog = true;
404 }
405
406 mCriticalLogWindow.append({mLogFile->pos(), pMsg.size()});
407 }
408
409
copy(const QString & pDest)410 bool LogHandler::copy(const QString& pDest)
411 {
412 const QMutexLocker mutexLocker(&mMutex);
413
414 if (useLogfile())
415 {
416 return copyOther(mLogFile->fileName(), pDest);
417 }
418
419 return false;
420 }
421
422
copyOther(const QString & pSource,const QString & pDest) const423 bool LogHandler::copyOther(const QString& pSource, const QString& pDest) const
424 {
425 if (pDest.trimmed().isEmpty())
426 {
427 return false;
428 }
429
430 Q_ASSERT(mLogFile);
431 if (pSource != mLogFile->fileName() && !getOtherLogfiles().contains(QFileInfo(pSource)))
432 {
433 return false;
434 }
435
436 if (QFile::exists(pDest) && !QFile::remove(pDest))
437 {
438 return false;
439 }
440
441 return QFile::copy(pSource, pDest);
442 }
443
444
getOtherLogfiles() const445 QFileInfoList LogHandler::getOtherLogfiles() const
446 {
447 QDir tmpPath = QDir::temp();
448 tmpPath.setSorting(QDir::Time);
449 tmpPath.setFilter(QDir::Files);
450 tmpPath.setNameFilters(QStringList({QCoreApplication::applicationName() + QStringLiteral(".*.log")}));
451
452 QFileInfoList list = tmpPath.entryInfoList();
453
454 if (useLogfile())
455 {
456 list.removeAll(QFileInfo(*mLogFile));
457 }
458
459 return list;
460 }
461
462
removeOldLogfiles()463 void LogHandler::removeOldLogfiles()
464 {
465 const auto& threshold = QDateTime::currentDateTime().addDays(-14);
466 const QFileInfoList& logfileInfos = getOtherLogfiles();
467 for (const QFileInfo& entry : logfileInfos)
468 {
469 if (entry.fileTime(QFileDevice::FileModificationTime) < threshold)
470 {
471 const auto result = QFile::remove(entry.absoluteFilePath());
472 qDebug() << "Auto-remove old logfile:" << entry.absoluteFilePath() << '|' << result;
473 }
474 }
475 }
476
477
removeOtherLogfiles()478 bool LogHandler::removeOtherLogfiles()
479 {
480 const auto otherLogFiles = getOtherLogfiles();
481 for (const auto& entry : otherLogFiles)
482 {
483 const auto result = QFile::remove(entry.absoluteFilePath());
484 qDebug() << "Remove old logfile:" << entry.absoluteFilePath() << '|' << result;
485 }
486
487 return !otherLogFiles.isEmpty();
488 }
489
490
setLogfile(bool pEnable)491 void LogHandler::setLogfile(bool pEnable)
492 {
493 const QMutexLocker mutexLocker(&mMutex);
494 Q_ASSERT(mLogFile);
495
496 if (pEnable)
497 {
498 if (!mLogFile->isOpen())
499 {
500 mLogFile->setFileTemplate(getLogFileTemplate());
501 mLogFile->open();
502 }
503 }
504 else
505 {
506 if (mLogFile->isOpen())
507 {
508 mLogFile->close();
509 mLogFile->remove();
510 mBacklogPosition = 0;
511 mCriticalLog = false;
512 mCriticalLogWindow.clear();
513 }
514 mLogFile->setFileTemplate(QString());
515 }
516 }
517
518
useLogfile() const519 bool LogHandler::useLogfile() const
520 {
521 return mLogFile && !mLogFile->fileTemplate().isNull();
522 }
523
524
setUseHandler(bool pEnable)525 void LogHandler::setUseHandler(bool pEnable)
526 {
527 mUseHandler = pEnable;
528 }
529
530
useHandler() const531 bool LogHandler::useHandler() const
532 {
533 return mUseHandler;
534 }
535
536
messageHandler(QtMsgType pType,const QMessageLogContext & pContext,const QString & pMsg)537 void LogHandler::messageHandler(QtMsgType pType, const QMessageLogContext& pContext, const QString& pMsg)
538 {
539 getInstance().handleMessage(pType, pContext, pMsg);
540 }
541
542
543 static_assert(!std::is_base_of<QObject, LogHandler>::value,
544 "LogHandler cannot be a QObject because it is a Singleton that should not be destroyed at all!");
545