File indexing completed on 2024-05-12 05:26:00

0001 #include "log.h"
0002 
0003 #include <QString>
0004 #include <QDir>
0005 #include <QIODevice>
0006 #include <QCoreApplication>
0007 #include <QSettings>
0008 #include <QSharedPointer>
0009 #include <QMutex>
0010 #include <QMutexLocker>
0011 #include <QDateTime>
0012 #include <iostream>
0013 #ifdef Q_OS_WIN
0014 #include <windows.h>
0015 #include <io.h>
0016 #include <process.h>
0017 #else
0018 #include <unistd.h>
0019 #endif
0020 #include <atomic>
0021 #include <definitions.h>
0022 #include <QThreadStorage>
0023 #include <QStringBuilder>
0024 
0025 using namespace Sink::Log;
0026 
0027 const char *getComponentName() { return nullptr; }
0028 
0029 static QThreadStorage<QSharedPointer<QSettings>> sSettings;
0030 static QSettings &config()
0031 {
0032     if (!sSettings.hasLocalData()) {
0033         sSettings.setLocalData(QSharedPointer<QSettings>::create(Sink::configLocation() + "/log.ini", QSettings::IniFormat));
0034     }
0035     return *sSettings.localData();
0036 }
0037 
0038 Q_GLOBAL_STATIC(QByteArray, sPrimaryComponent);
0039 
0040 void Sink::Log::setPrimaryComponent(const QString &component)
0041 {
0042     if (!sPrimaryComponent.isDestroyed()) {
0043         *sPrimaryComponent = component.toUtf8();
0044     }
0045 }
0046 
0047 class DebugStream : public QIODevice
0048 {
0049 public:
0050     QString m_location;
0051     DebugStream() : QIODevice()
0052     {
0053         open(WriteOnly);
0054     }
0055     ~DebugStream() override;
0056 
0057     bool isSequential() const override
0058     {
0059         return true;
0060     }
0061     qint64 readData(char *, qint64) override
0062     {
0063         return 0; /* eof */
0064     }
0065     qint64 readLineData(char *, qint64) override
0066     {
0067         return 0; /* eof */
0068     }
0069     qint64 writeData(const char *data, qint64 len) override
0070     {
0071 #ifdef Q_OS_WIN
0072         const auto string = QString::fromUtf8(data, len);
0073         OutputDebugStringW(reinterpret_cast<const wchar_t*>(string.utf16()));
0074 #else
0075         std::cout << data << std::endl;
0076 #endif
0077         return len;
0078     }
0079 
0080 private:
0081     Q_DISABLE_COPY(DebugStream)
0082 };
0083 
0084 // Virtual method anchor
0085 DebugStream::~DebugStream()
0086 {
0087 }
0088 
0089 class NullStream : public QIODevice
0090 {
0091 public:
0092     NullStream() : QIODevice()
0093     {
0094         open(WriteOnly);
0095     }
0096     ~NullStream() override;
0097 
0098     bool isSequential() const override
0099     {
0100         return true;
0101     }
0102     qint64 readData(char *, qint64) override
0103     {
0104         return 0; /* eof */
0105     }
0106     qint64 readLineData(char *, qint64) override
0107     {
0108         return 0; /* eof */
0109     }
0110     qint64 writeData(const char *data, qint64 len) override
0111     {
0112         return len;
0113     }
0114 
0115 private:
0116     Q_DISABLE_COPY(NullStream)
0117 };
0118 
0119 // Virtual method anchor
0120 NullStream::~NullStream()
0121 {
0122 }
0123 
0124 /*
0125  * ANSI color codes:
0126  * 0: reset colors/style
0127  * 1: bold
0128  * 4: underline
0129  * 30 - 37: black, red, green, yellow, blue, magenta, cyan, and white text
0130  * 40 - 47: black, red, green, yellow, blue, magenta, cyan, and white background
0131  */
0132 enum ANSI_Colors
0133 {
0134     DoNothing = -1,
0135     Reset = 0,
0136     Bold = 1,
0137     Underline = 4,
0138     Black = 30,
0139     Red = 31,
0140     Green = 32,
0141     Yellow = 33,
0142     Blue = 34
0143 };
0144 
0145 static QString colorCommand(int colorCode)
0146 {
0147     return QString("\x1b[%1m").arg(colorCode);
0148 }
0149 
0150 static QString colorCommand(QList<int> colorCodes)
0151 {
0152     colorCodes.removeAll(ANSI_Colors::DoNothing);
0153     if (colorCodes.isEmpty()) {
0154         return QString();
0155     }
0156     QString string("\x1b[");
0157     for (int code : colorCodes) {
0158         string += QString("%1;").arg(code);
0159     }
0160     string.chop(1);
0161     string += "m";
0162     return string;
0163 }
0164 
0165 QByteArray Sink::Log::debugLevelName(DebugLevel debugLevel)
0166 {
0167     switch (debugLevel) {
0168         case DebugLevel::Trace:
0169             return "Trace";
0170         case DebugLevel::Log:
0171             return "Log";
0172         case DebugLevel::Warning:
0173             return "Warning";
0174         case DebugLevel::Error:
0175             return "Error";
0176         default:
0177             break;
0178     }
0179     Q_ASSERT(false);
0180     return QByteArray();
0181 }
0182 
0183 DebugLevel Sink::Log::debugLevelFromName(const QByteArray &name)
0184 {
0185     const QByteArray lowercaseName = name.toLower();
0186     if (lowercaseName == "trace")
0187         return DebugLevel::Trace;
0188     if (lowercaseName == "log")
0189         return DebugLevel::Log;
0190     if (lowercaseName == "warning")
0191         return DebugLevel::Warning;
0192     if (lowercaseName == "error")
0193         return DebugLevel::Error;
0194     return DebugLevel::Log;
0195 }
0196 
0197 void Sink::Log::setDebugOutputLevel(DebugLevel debugLevel)
0198 {
0199     config().setValue("level", debugLevel);
0200 }
0201 
0202 Sink::Log::DebugLevel Sink::Log::debugOutputLevel()
0203 {
0204     return static_cast<Sink::Log::DebugLevel>(config().value("level", Sink::Log::Log).toInt());
0205 }
0206 
0207 void Sink::Log::setDebugOutputFilter(FilterType type, const QByteArrayList &filter)
0208 {
0209     switch (type) {
0210         case ApplicationName:
0211             config().setValue("applicationfilter", QVariant::fromValue(filter));
0212             break;
0213         case Area:
0214             config().setValue("areafilter", QVariant::fromValue(filter));
0215             break;
0216     }
0217 }
0218 
0219 QByteArrayList Sink::Log::debugOutputFilter(FilterType type)
0220 {
0221     switch (type) {
0222         case ApplicationName:
0223             return config().value("applicationfilter").value<QByteArrayList>();
0224         case Area:
0225             return config().value("areafilter").value<QByteArrayList>();
0226         default:
0227             return QByteArrayList();
0228     }
0229 }
0230 
0231 void Sink::Log::setDebugOutputFields(const QByteArrayList &output)
0232 {
0233     config().setValue("outputfields", QVariant::fromValue(output));
0234 }
0235 
0236 QByteArrayList Sink::Log::debugOutputFields()
0237 {
0238     return config().value("outputfields").value<QByteArrayList>();
0239 }
0240 
0241 static QByteArray getProgramName()
0242 {
0243     if (QCoreApplication::instance()) {
0244         return QCoreApplication::instance()->applicationName().toLocal8Bit();
0245     } else {
0246         return "<unknown program name>";
0247     }
0248 }
0249 
0250 static QSharedPointer<QSettings> debugAreasConfig()
0251 {
0252     return QSharedPointer<QSettings>::create(Sink::dataLocation() + "/debugAreas.ini", QSettings::IniFormat);
0253 }
0254 
0255 class DebugAreaCollector {
0256 public:
0257     DebugAreaCollector()
0258     {
0259         //This call can potentially print a log message (if we fail to remove the qsettings lockfile), which would result in a deadlock if we locked over all of it.
0260         const auto areas = debugAreasConfig()->value("areas").value<QString>().split(';').toSet();
0261         {
0262             QMutexLocker locker(&mutex);
0263             mDebugAreas = areas;
0264         }
0265     }
0266 
0267     ~DebugAreaCollector()
0268     {
0269         //This call can potentially print a log message (if we fail to remove the qsettings lockfile), which would result in a deadlock if we locked over all of it.
0270         const auto areas = debugAreasConfig()->value("areas").value<QString>().split(';').toSet();
0271         {
0272             QMutexLocker locker(&mutex);
0273             mDebugAreas += areas;
0274         }
0275         debugAreasConfig()->setValue("areas", QVariant::fromValue(mDebugAreas.toList().join(';')));
0276     }
0277 
0278     void add(const QString &area)
0279     {
0280         QMutexLocker locker(&mutex);
0281         mDebugAreas << area;
0282     }
0283 
0284     QSet<QString> debugAreas()
0285     {
0286         QMutexLocker locker(&mutex);
0287         return mDebugAreas;
0288     }
0289 
0290     QMutex mutex;
0291     QSet<QString> mDebugAreas;
0292 };
0293 
0294 Q_GLOBAL_STATIC(DebugAreaCollector, sDebugAreaCollector);
0295 
0296 QSet<QString> Sink::Log::debugAreas()
0297 {
0298     if (!sDebugAreaCollector.isDestroyed()) {
0299         return sDebugAreaCollector->debugAreas();
0300     }
0301     return {};
0302 }
0303 
0304 static void collectDebugArea(const QString &debugArea)
0305 {
0306     if (!sDebugAreaCollector.isDestroyed()) {
0307         sDebugAreaCollector->add(debugArea);
0308     }
0309 }
0310 
0311 static bool containsItemStartingWith(const QByteArray &pattern, const QByteArrayList &list)
0312 {
0313     for (const auto &item : list) {
0314         int start = 0;
0315         int end = item.size();
0316         if (item.startsWith('*')) {
0317             start++;
0318         }
0319         if (item.endsWith('*')) {
0320             end--;
0321         }
0322         if (pattern.contains(item.mid(start, end - start))) {
0323             return true;
0324         }
0325     }
0326     return false;
0327 }
0328 
0329 static bool caseInsensitiveContains(const QByteArray &pattern, const QByteArrayList &list)
0330 {
0331     for (const auto &item : list) {
0332         if (item.toLower() == pattern) {
0333             return true;
0334         }
0335     }
0336     return false;
0337 }
0338 
0339 static QByteArray getFileName(const char *file)
0340 {
0341     static char sep = QDir::separator().toLatin1();
0342     auto filename = QByteArray(file).split(sep).last();
0343     return filename.split('.').first();
0344 }
0345 
0346 static QString assembleDebugArea(const char *debugArea, const char *debugComponent, const char *file)
0347 {
0348     if (!sPrimaryComponent.isDestroyed() && sPrimaryComponent->isEmpty()) {
0349         *sPrimaryComponent = getProgramName();
0350     }
0351     if (!sPrimaryComponent.isDestroyed()) {
0352         //Using stringbuilder for fewer allocations
0353         return QLatin1String{*sPrimaryComponent} % QLatin1String{"."} %
0354             (debugComponent ? (QLatin1String{debugComponent} + QLatin1String{"."}) : QLatin1String{""}) %
0355             (debugArea ? QLatin1String{debugArea} : QLatin1String{getFileName(file)});
0356     } else {
0357         return {};
0358     }
0359 }
0360 
0361 static bool isFiltered(DebugLevel debugLevel, const QByteArray &fullDebugArea)
0362 {
0363     if (debugLevel < debugOutputLevel()) {
0364         return true;
0365     }
0366     const auto areas = debugOutputFilter(Sink::Log::Area);
0367     if ((debugLevel <= Sink::Log::Trace) && !areas.isEmpty()) {
0368         if (!containsItemStartingWith(fullDebugArea, areas)) {
0369             return true;
0370         }
0371     }
0372     return false;
0373 }
0374 
0375 bool Sink::Log::isFiltered(DebugLevel debugLevel, const char *debugArea, const char *debugComponent, const char *file)
0376 {
0377     //Avoid assembleDebugArea if we can, because it's fairly expensive.
0378     if (debugLevel < debugOutputLevel()) {
0379         return true;
0380     }
0381     return isFiltered(debugLevel, assembleDebugArea(debugArea, debugComponent, file).toLatin1());
0382 }
0383 
0384 Q_GLOBAL_STATIC(NullStream, sNullStream);
0385 Q_GLOBAL_STATIC(DebugStream, sDebugStream);
0386 
0387 QDebug Sink::Log::debugStream(DebugLevel debugLevel, int line, const char *file, const char *function, const char *debugArea, const char *debugComponent)
0388 {
0389     const auto fullDebugArea = assembleDebugArea(debugArea, debugComponent, file);
0390     collectDebugArea(fullDebugArea);
0391 
0392     if (isFiltered(debugLevel, fullDebugArea.toLatin1())) {
0393         if (!sNullStream.isDestroyed()) {
0394             return QDebug(sNullStream);
0395         }
0396         return QDebug{QtDebugMsg};
0397     }
0398 
0399     QString prefix;
0400     int prefixColorCode = ANSI_Colors::DoNothing;
0401     switch (debugLevel) {
0402         case DebugLevel::Trace:
0403             prefix = "Trace:  ";
0404             break;
0405         case DebugLevel::Log:
0406             prefix = "Log:    ";
0407             prefixColorCode = ANSI_Colors::Green;
0408             break;
0409         case DebugLevel::Warning:
0410             prefix = "Warning:";
0411             prefixColorCode = ANSI_Colors::Red;
0412             break;
0413         case DebugLevel::Error:
0414             prefix = "Error:  ";
0415             prefixColorCode = ANSI_Colors::Red;
0416             break;
0417     }
0418 
0419     auto debugOutput = debugOutputFields();
0420 
0421     bool showTime = debugOutput.isEmpty() ? false : caseInsensitiveContains("time", debugOutput);
0422     bool showLocation = debugOutput.isEmpty() ? false : caseInsensitiveContains("location", debugOutput);
0423     bool showFunction = debugOutput.isEmpty() ? false : caseInsensitiveContains("function", debugOutput);
0424     bool showProgram = debugOutput.isEmpty() ? false : caseInsensitiveContains("application", debugOutput);
0425 #ifdef Q_OS_WIN
0426     bool useColor = false;
0427 #else
0428     bool useColor = true;
0429 #endif
0430     bool multiline = false;
0431 
0432     const QString resetColor = colorCommand(ANSI_Colors::Reset);
0433     QString output;
0434     if (showTime) {
0435         output = QString("%1 ").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs));
0436     }
0437     if (useColor) {
0438         output += colorCommand(QList<int>() << ANSI_Colors::Bold << prefixColorCode);
0439     }
0440     output += prefix;
0441     if (useColor) {
0442         output += resetColor;
0443     }
0444     if (showProgram) {
0445         int width = 10;
0446         output += QString(" %1(%2)").arg(QString::fromLatin1(getProgramName()).leftJustified(width, ' ', true)).arg(unsigned(getpid())).rightJustified(width + 8, ' ');
0447     }
0448     if (useColor) {
0449         output += colorCommand(QList<int>() << ANSI_Colors::Bold << prefixColorCode);
0450     }
0451     static std::atomic<int> maxDebugAreaSize{25};
0452     maxDebugAreaSize = qMax(fullDebugArea.size(), maxDebugAreaSize.load());
0453     output += QString(" %1 ").arg(fullDebugArea.leftJustified(maxDebugAreaSize, ' ', false));
0454     if (useColor) {
0455         output += resetColor;
0456     }
0457     if (showFunction) {
0458         output += QString(" %3").arg(fullDebugArea.leftJustified(25, ' ', true));
0459     }
0460     if (showLocation) {
0461         const auto filename = QString::fromLatin1(file).split('/').last();
0462         output += QString(" %1:%2").arg(filename.right(25)).arg(QString::number(line).leftJustified(4, ' ')).leftJustified(30, ' ', true);
0463     }
0464     if (multiline) {
0465         output += "\n  ";
0466     }
0467     output += ":";
0468 
0469     if (sDebugStream.isDestroyed()) {
0470         return QDebug{QtDebugMsg};
0471     }
0472     QDebug debug(sDebugStream);
0473     debug.noquote().nospace() << output;
0474     return debug.space().quote();
0475 }