1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qloggingregistry_p.h"
5
6#include <QtCore/qfile.h>
7#include <QtCore/qlibraryinfo.h>
8#include <QtCore/private/qlocking_p.h>
9#include <QtCore/qstandardpaths.h>
10#include <QtCore/qstringtokenizer.h>
11#include <QtCore/qtextstream.h>
12#include <QtCore/qdir.h>
13#include <QtCore/qcoreapplication.h>
14
15#if QT_CONFIG(settings)
16#include <QtCore/qsettings.h>
17#include <QtCore/private/qsettings_p.h>
18#endif
19
20// We can't use the default macros because this would lead to recursion.
21// Instead let's define our own one that unconditionally logs...
22#define debugMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").debug
23#define warnMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").warning
24
25QT_BEGIN_NAMESPACE
26
27using namespace Qt::StringLiterals;
28
29Q_GLOBAL_STATIC(QLoggingRegistry, qtLoggingRegistry)
30
31/*!
32 \internal
33 Constructs a logging rule with default values.
34*/
35QLoggingRule::QLoggingRule() :
36 enabled(false)
37{
38}
39
40/*!
41 \internal
42 Constructs a logging rule.
43*/
44QLoggingRule::QLoggingRule(QStringView pattern, bool enabled) :
45 messageType(-1),
46 enabled(enabled)
47{
48 parse(pattern);
49}
50
51/*!
52 \internal
53 Return value 1 means filter passed, 0 means filter doesn't influence this
54 category, -1 means category doesn't pass this filter.
55 */
56int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const
57{
58 // check message type
59 if (messageType > -1 && messageType != msgType)
60 return 0;
61
62 if (flags == FullText) {
63 // full match
64 if (category == cat)
65 return (enabled ? 1 : -1);
66 else
67 return 0;
68 }
69
70 const qsizetype idx = cat.indexOf(s: category);
71 if (idx >= 0) {
72 if (flags == MidFilter) {
73 // matches somewhere
74 return (enabled ? 1 : -1);
75 } else if (flags == LeftFilter) {
76 // matches left
77 if (idx == 0)
78 return (enabled ? 1 : -1);
79 } else if (flags == RightFilter) {
80 // matches right
81 if (idx == (cat.size() - category.size()))
82 return (enabled ? 1 : -1);
83 }
84 }
85 return 0;
86}
87
88/*!
89 \internal
90 Parses \a pattern.
91 Allowed is f.ex.:
92 qt.core.io.debug FullText, QtDebugMsg
93 qt.core.* LeftFilter, all types
94 *.io.warning RightFilter, QtWarningMsg
95 *.core.* MidFilter
96 */
97void QLoggingRule::parse(QStringView pattern)
98{
99 QStringView p;
100
101 // strip trailing ".messagetype"
102 if (pattern.endsWith(s: ".debug"_L1)) {
103 p = pattern.chopped(n: 6); // strlen(".debug")
104 messageType = QtDebugMsg;
105 } else if (pattern.endsWith(s: ".info"_L1)) {
106 p = pattern.chopped(n: 5); // strlen(".info")
107 messageType = QtInfoMsg;
108 } else if (pattern.endsWith(s: ".warning"_L1)) {
109 p = pattern.chopped(n: 8); // strlen(".warning")
110 messageType = QtWarningMsg;
111 } else if (pattern.endsWith(s: ".critical"_L1)) {
112 p = pattern.chopped(n: 9); // strlen(".critical")
113 messageType = QtCriticalMsg;
114 } else {
115 p = pattern;
116 }
117
118 const QChar asterisk = u'*';
119 if (!p.contains(c: asterisk)) {
120 flags = FullText;
121 } else {
122 if (p.endsWith(c: asterisk)) {
123 flags |= LeftFilter;
124 p = p.chopped(n: 1);
125 }
126 if (p.startsWith(c: asterisk)) {
127 flags |= RightFilter;
128 p = p.mid(pos: 1);
129 }
130 if (p.contains(c: asterisk)) // '*' only supported at start/end
131 flags = PatternFlags();
132 }
133
134 category = p.toString();
135}
136
137/*!
138 \class QLoggingSettingsParser
139 \since 5.3
140 \internal
141
142 Parses a .ini file with the following format:
143
144 [rules]
145 rule1=[true|false]
146 rule2=[true|false]
147 ...
148
149 [rules] is the default section, and therefore optional.
150*/
151
152/*!
153 \internal
154 Parses configuration from \a content.
155*/
156void QLoggingSettingsParser::setContent(QStringView content)
157{
158 _rules.clear();
159 for (auto line : qTokenize(h&: content, n: u'\n'))
160 parseNextLine(line);
161}
162
163/*!
164 \internal
165 Parses configuration from \a stream.
166*/
167void QLoggingSettingsParser::setContent(QTextStream &stream)
168{
169 _rules.clear();
170 QString line;
171 while (stream.readLineInto(line: &line))
172 parseNextLine(line: qToStringViewIgnoringNull(s: line));
173}
174
175/*!
176 \internal
177 Parses one line of the configuration file
178*/
179
180void QLoggingSettingsParser::parseNextLine(QStringView line)
181{
182 // Remove whitespace at start and end of line:
183 line = line.trimmed();
184
185 // comment
186 if (line.startsWith(c: u';'))
187 return;
188
189 if (line.startsWith(c: u'[') && line.endsWith(c: u']')) {
190 // new section
191 auto sectionName = line.mid(pos: 1).chopped(n: 1).trimmed();
192 m_inRulesSection = sectionName.compare(s: "rules"_L1, cs: Qt::CaseInsensitive) == 0;
193 return;
194 }
195
196 if (m_inRulesSection) {
197 const qsizetype equalPos = line.indexOf(c: u'=');
198 if (equalPos != -1) {
199 if (line.lastIndexOf(c: u'=') == equalPos) {
200 const auto key = line.left(n: equalPos).trimmed();
201#if QT_CONFIG(settings)
202 QString tmp;
203 QSettingsPrivate::iniUnescapedKey(key: key.toUtf8(), result&: tmp);
204 QStringView pattern = qToStringViewIgnoringNull(s: tmp);
205#else
206 QStringView pattern = key;
207#endif
208 const auto valueStr = line.mid(pos: equalPos + 1).trimmed();
209 int value = -1;
210 if (valueStr == "true"_L1)
211 value = 1;
212 else if (valueStr == "false"_L1)
213 value = 0;
214 QLoggingRule rule(pattern, (value == 1));
215 if (rule.flags != 0 && (value != -1))
216 _rules.append(t: std::move(rule));
217 else
218 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
219 } else {
220 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
221 }
222 }
223 }
224}
225
226/*!
227 \internal
228 QLoggingRegistry constructor
229 */
230QLoggingRegistry::QLoggingRegistry()
231 : categoryFilter(defaultCategoryFilter)
232{
233#if defined(Q_OS_ANDROID)
234 // Unless QCoreApplication has been constructed we can't be sure that
235 // we are on Qt's main thread. If we did allow logging here, we would
236 // potentially set Qt's main thread to Android's thread 0, which would
237 // confuse Qt later when running main().
238 if (!qApp)
239 return;
240#endif
241
242 initializeRules(); // Init on first use
243}
244
245static bool qtLoggingDebug()
246{
247 static const bool debugEnv = qEnvironmentVariableIsSet(varName: "QT_LOGGING_DEBUG");
248 return debugEnv;
249}
250
251static QList<QLoggingRule> loadRulesFromFile(const QString &filePath)
252{
253 QFile file(filePath);
254 if (file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
255 if (qtLoggingDebug())
256 debugMsg(msg: "Loading \"%s\" ...",
257 QDir::toNativeSeparators(pathName: file.fileName()).toUtf8().constData());
258 QTextStream stream(&file);
259 QLoggingSettingsParser parser;
260 parser.setContent(stream);
261 return parser.rules();
262 }
263 return QList<QLoggingRule>();
264}
265
266/*!
267 \internal
268 Initializes the rules database by loading
269 $QT_LOGGING_CONF, $QT_LOGGING_RULES, and .config/QtProject/qtlogging.ini.
270 */
271void QLoggingRegistry::initializeRules()
272{
273 QList<QLoggingRule> er, qr, cr;
274 // get rules from environment
275 const QByteArray rulesFilePath = qgetenv(varName: "QT_LOGGING_CONF");
276 if (!rulesFilePath.isEmpty())
277 er = loadRulesFromFile(filePath: QFile::decodeName(localFileName: rulesFilePath));
278
279 const QByteArray rulesSrc = qgetenv(varName: "QT_LOGGING_RULES").replace(before: ';', after: '\n');
280 if (!rulesSrc.isEmpty()) {
281 QTextStream stream(rulesSrc);
282 QLoggingSettingsParser parser;
283 parser.setImplicitRulesSection(true);
284 parser.setContent(stream);
285 er += parser.rules();
286 }
287
288 const QString configFileName = QStringLiteral("qtlogging.ini");
289
290#if !defined(QT_BOOTSTRAPPED)
291 // get rules from Qt data configuration path
292 const QString qtConfigPath
293 = QDir(QLibraryInfo::path(p: QLibraryInfo::DataPath)).absoluteFilePath(fileName: configFileName);
294 qr = loadRulesFromFile(filePath: qtConfigPath);
295#endif
296
297 // get rules from user's/system configuration
298 const QString envPath = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation,
299 fileName: QString::fromLatin1(ba: "QtProject/") + configFileName);
300 if (!envPath.isEmpty())
301 cr = loadRulesFromFile(filePath: envPath);
302
303 const QMutexLocker locker(&registryMutex);
304
305 ruleSets[EnvironmentRules] = std::move(er);
306 ruleSets[QtConfigRules] = std::move(qr);
307 ruleSets[ConfigRules] = std::move(cr);
308
309 if (!ruleSets[EnvironmentRules].isEmpty() || !ruleSets[QtConfigRules].isEmpty() || !ruleSets[ConfigRules].isEmpty())
310 updateRules();
311}
312
313/*!
314 \internal
315 Registers a category object.
316
317 This method might be called concurrently for the same category object.
318*/
319void QLoggingRegistry::registerCategory(QLoggingCategory *cat, QtMsgType enableForLevel)
320{
321 const auto locker = qt_scoped_lock(mutex&: registryMutex);
322
323 const auto oldSize = categories.size();
324 auto &e = categories[cat];
325 if (categories.size() != oldSize) {
326 // new entry
327 e = enableForLevel;
328 (*categoryFilter)(cat);
329 }
330}
331
332/*!
333 \internal
334 Unregisters a category object.
335*/
336void QLoggingRegistry::unregisterCategory(QLoggingCategory *cat)
337{
338 const auto locker = qt_scoped_lock(mutex&: registryMutex);
339 categories.remove(key: cat);
340}
341
342/*!
343 \since 6.3
344 \internal
345
346 Registers the environment variable \a environment as the control variable
347 for enabling debugging by default for category \a categoryName. The
348 category name must start with "qt."
349*/
350void QLoggingRegistry::registerEnvironmentOverrideForCategory(QByteArrayView categoryName,
351 QByteArrayView environment)
352{
353 qtCategoryEnvironmentOverrides.insert(key: categoryName, value: environment);
354}
355
356/*!
357 \internal
358 Installs logging rules as specified in \a content.
359 */
360void QLoggingRegistry::setApiRules(const QString &content)
361{
362 QLoggingSettingsParser parser;
363 parser.setImplicitRulesSection(true);
364 parser.setContent(content);
365
366 if (qtLoggingDebug())
367 debugMsg(msg: "Loading logging rules set by QLoggingCategory::setFilterRules ...");
368
369 const QMutexLocker locker(&registryMutex);
370
371 ruleSets[ApiRules] = parser.rules();
372
373 updateRules();
374}
375
376/*!
377 \internal
378 Activates a new set of logging rules for the default filter.
379
380 (The caller must lock registryMutex to make sure the API is thread safe.)
381*/
382void QLoggingRegistry::updateRules()
383{
384 for (auto it = categories.keyBegin(), end = categories.keyEnd(); it != end; ++it)
385 (*categoryFilter)(*it);
386}
387
388/*!
389 \internal
390 Installs a custom filter rule.
391*/
392QLoggingCategory::CategoryFilter
393QLoggingRegistry::installFilter(QLoggingCategory::CategoryFilter filter)
394{
395 const auto locker = qt_scoped_lock(mutex&: registryMutex);
396
397 if (!filter)
398 filter = defaultCategoryFilter;
399
400 QLoggingCategory::CategoryFilter old = categoryFilter;
401 categoryFilter = filter;
402
403 updateRules();
404
405 return old;
406}
407
408QLoggingRegistry *QLoggingRegistry::instance()
409{
410 return qtLoggingRegistry();
411}
412
413/*!
414 \internal
415 Updates category settings according to rules.
416
417 As a category filter, it is run with registryMutex held.
418*/
419void QLoggingRegistry::defaultCategoryFilter(QLoggingCategory *cat)
420{
421 const QLoggingRegistry *reg = QLoggingRegistry::instance();
422 Q_ASSERT(reg->categories.contains(cat));
423 QtMsgType enableForLevel = reg->categories.value(key: cat);
424
425 // NB: note that the numeric values of the Qt*Msg constants are
426 // not in severity order.
427 bool debug = (enableForLevel == QtDebugMsg);
428 bool info = debug || (enableForLevel == QtInfoMsg);
429 bool warning = info || (enableForLevel == QtWarningMsg);
430 bool critical = warning || (enableForLevel == QtCriticalMsg);
431
432 // hard-wired implementation of
433 // qt.*.debug=false
434 // qt.debug=false
435 if (const char *categoryName = cat->categoryName()) {
436 // == "qt" or startsWith("qt.")
437 if (strcmp(s1: categoryName, s2: "qt") == 0) {
438 debug = false;
439 } else if (strncmp(s1: categoryName, s2: "qt.", n: 3) == 0) {
440 // may be overridden
441 auto it = reg->qtCategoryEnvironmentOverrides.find(key: categoryName);
442 if (it == reg->qtCategoryEnvironmentOverrides.end())
443 debug = false;
444 else
445 debug = qEnvironmentVariableIntValue(varName: it.value().data());
446 }
447 }
448
449 const auto categoryName = QLatin1StringView(cat->categoryName());
450
451 for (const auto &ruleSet : reg->ruleSets) {
452 for (const auto &rule : ruleSet) {
453 int filterpass = rule.pass(cat: categoryName, msgType: QtDebugMsg);
454 if (filterpass != 0)
455 debug = (filterpass > 0);
456 filterpass = rule.pass(cat: categoryName, msgType: QtInfoMsg);
457 if (filterpass != 0)
458 info = (filterpass > 0);
459 filterpass = rule.pass(cat: categoryName, msgType: QtWarningMsg);
460 if (filterpass != 0)
461 warning = (filterpass > 0);
462 filterpass = rule.pass(cat: categoryName, msgType: QtCriticalMsg);
463 if (filterpass != 0)
464 critical = (filterpass > 0);
465 }
466 }
467
468 cat->setEnabled(type: QtDebugMsg, enable: debug);
469 cat->setEnabled(type: QtInfoMsg, enable: info);
470 cat->setEnabled(type: QtWarningMsg, enable: warning);
471 cat->setEnabled(type: QtCriticalMsg, enable: critical);
472}
473
474
475QT_END_NAMESPACE
476

source code of qtbase/src/corelib/io/qloggingregistry.cpp