1 /*=========================================================================
2
3 Library: CTK
4
5 Copyright (c) Kitware Inc.
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0.txt
12
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS,
15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 See the License for the specific language governing permissions and
17 limitations under the License.
18
19 =========================================================================*/
20
21 // Qt includes
22 #include <QCoreApplication>
23 #include <QDateTime>
24 #include <QDebug>
25 #include <QFile>
26 #include <QMetaEnum>
27 #include <QMetaType>
28 #include <QMutexLocker>
29 #include <QPointer>
30 #include <QStandardItem>
31 #include <QThread>
32
33 // CTK includes
34 #include "ctkErrorLogContext.h"
35 #include "ctkErrorLogModel.h"
36 #include "ctkErrorLogAbstractMessageHandler.h"
37 #include "ctkFileLogger.h"
38
39
40 // --------------------------------------------------------------------------
41 // ctkErrorLogModelPrivate
42
43 // --------------------------------------------------------------------------
44 class ctkErrorLogModelPrivate
45 {
46 Q_DECLARE_PUBLIC(ctkErrorLogModel);
47 protected:
48 ctkErrorLogModel* const q_ptr;
49 public:
50 ctkErrorLogModelPrivate(ctkErrorLogModel& object);
51 ~ctkErrorLogModelPrivate();
52
53 void init();
54
55 void setMessageHandlerConnection(ctkErrorLogAbstractMessageHandler * msgHandler, bool asynchronous);
56
57 QStandardItemModel StandardItemModel;
58
59 QHash<QString, ctkErrorLogAbstractMessageHandler*> RegisteredHandlers;
60
61 ctkErrorLogLevel::LogLevels CurrentLogLevelFilter;
62
63 bool LogEntryGrouping;
64 bool AsynchronousLogging;
65 bool AddingEntry;
66
67 ctkErrorLogLevel ErrorLogLevel;
68
69 ctkErrorLogTerminalOutput StdErrTerminalOutput;
70 ctkErrorLogTerminalOutput StdOutTerminalOutput;
71
72 ctkFileLogger FileLogger;
73 QString FileLoggingPattern;
74 };
75
76 // --------------------------------------------------------------------------
77 // ctkErrorLogModelPrivate methods
78
79 // --------------------------------------------------------------------------
ctkErrorLogModelPrivate(ctkErrorLogModel & object)80 ctkErrorLogModelPrivate::ctkErrorLogModelPrivate(ctkErrorLogModel& object)
81 : q_ptr(&object)
82 {
83 qRegisterMetaType<ctkErrorLogContext>("ctkErrorLogContext");
84 this->StandardItemModel.setColumnCount(ctkErrorLogModel::MaxColumn);
85 this->LogEntryGrouping = false;
86 this->AsynchronousLogging = true;
87 this->AddingEntry = false;
88 this->FileLogger.setEnabled(false);
89 this->FileLoggingPattern = "[%{level}][%{origin}] %{timestamp} [%{category}] (%{file}:%{line}) - %{msg}";
90 }
91
92 // --------------------------------------------------------------------------
~ctkErrorLogModelPrivate()93 ctkErrorLogModelPrivate::~ctkErrorLogModelPrivate()
94 {
95 foreach(const QString& handlerName, this->RegisteredHandlers.keys())
96 {
97 ctkErrorLogAbstractMessageHandler * msgHandler =
98 this->RegisteredHandlers.value(handlerName);
99 Q_ASSERT(msgHandler);
100 msgHandler->setEnabled(false);
101 delete msgHandler;
102 }
103 }
104
105 // --------------------------------------------------------------------------
init()106 void ctkErrorLogModelPrivate::init()
107 {
108 Q_Q(ctkErrorLogModel);
109 q->setDynamicSortFilter(true);
110 //
111 // WARNING - Using a QSortFilterProxyModel slows down the insertion of rows by a factor 10
112 //
113 q->setSourceModel(&this->StandardItemModel);
114 q->setFilterKeyColumn(ctkErrorLogModel::LogLevelColumn);
115 }
116
117 // --------------------------------------------------------------------------
setMessageHandlerConnection(ctkErrorLogAbstractMessageHandler * msgHandler,bool asynchronous)118 void ctkErrorLogModelPrivate::setMessageHandlerConnection(
119 ctkErrorLogAbstractMessageHandler * msgHandler, bool asynchronous)
120 {
121 Q_Q(ctkErrorLogModel);
122
123 msgHandler->disconnect();
124
125 QObject::connect(msgHandler,
126 SIGNAL(messageHandled(QDateTime,QString,ctkErrorLogLevel::LogLevel,QString,ctkErrorLogContext,QString)),
127 q, SLOT(addEntry(QDateTime,QString,ctkErrorLogLevel::LogLevel,QString,ctkErrorLogContext,QString)),
128 asynchronous ? Qt::QueuedConnection : Qt::BlockingQueuedConnection);
129 }
130
131 // --------------------------------------------------------------------------
132 // ctkErrorLogModel methods
133
134 //------------------------------------------------------------------------------
ctkErrorLogModel(QObject * parentObject)135 ctkErrorLogModel::ctkErrorLogModel(QObject * parentObject)
136 : Superclass(parentObject)
137 , d_ptr(new ctkErrorLogModelPrivate(*this))
138 {
139 Q_D(ctkErrorLogModel);
140
141 d->init();
142 }
143
144 //------------------------------------------------------------------------------
~ctkErrorLogModel()145 ctkErrorLogModel::~ctkErrorLogModel()
146 {
147 }
148
149 //------------------------------------------------------------------------------
registerMsgHandler(ctkErrorLogAbstractMessageHandler * msgHandler)150 bool ctkErrorLogModel::registerMsgHandler(ctkErrorLogAbstractMessageHandler * msgHandler)
151 {
152 Q_D(ctkErrorLogModel);
153 if (!msgHandler)
154 {
155 return false;
156 }
157 if (d->RegisteredHandlers.keys().contains(msgHandler->handlerName()))
158 {
159 return false;
160 }
161
162 d->setMessageHandlerConnection(msgHandler, d->AsynchronousLogging);
163
164 msgHandler->setTerminalOutput(ctkErrorLogTerminalOutput::StandardError, &d->StdErrTerminalOutput);
165 msgHandler->setTerminalOutput(ctkErrorLogTerminalOutput::StandardOutput, &d->StdOutTerminalOutput);
166
167 d->RegisteredHandlers.insert(msgHandler->handlerName(), msgHandler);
168 return true;
169 }
170
171 //------------------------------------------------------------------------------
msgHandlerNames() const172 QStringList ctkErrorLogModel::msgHandlerNames()const
173 {
174 Q_D(const ctkErrorLogModel);
175 return d->RegisteredHandlers.keys();
176 }
177
178 //------------------------------------------------------------------------------
msgHandlerEnabled(const QString & handlerName) const179 bool ctkErrorLogModel::msgHandlerEnabled(const QString& handlerName) const
180 {
181 Q_D(const ctkErrorLogModel);
182 if (!d->RegisteredHandlers.keys().contains(handlerName))
183 {
184 return false;
185 }
186 return d->RegisteredHandlers.value(handlerName)->enabled();
187 }
188
189 //------------------------------------------------------------------------------
setMsgHandlerEnabled(const QString & handlerName,bool enabled)190 void ctkErrorLogModel::setMsgHandlerEnabled(const QString& handlerName, bool enabled)
191 {
192 Q_D(ctkErrorLogModel);
193 if (!d->RegisteredHandlers.keys().contains(handlerName))
194 {
195 // qCritical() << "Failed to enable/disable message handler " << handlerName
196 // << "- Handler not registered !";
197 return;
198 }
199 d->RegisteredHandlers.value(handlerName)->setEnabled(enabled);
200 }
201
202 //------------------------------------------------------------------------------
msgHandlerEnabled() const203 QStringList ctkErrorLogModel::msgHandlerEnabled() const
204 {
205 Q_D(const ctkErrorLogModel);
206 QStringList msgHandlers;
207 foreach(const QString& handlerName, d->RegisteredHandlers.keys())
208 {
209 if (d->RegisteredHandlers.value(handlerName)->enabled())
210 {
211 msgHandlers << handlerName;
212 }
213 }
214 return msgHandlers;
215 }
216
217 //------------------------------------------------------------------------------
setMsgHandlerEnabled(const QStringList & handlerNames)218 void ctkErrorLogModel::setMsgHandlerEnabled(const QStringList& handlerNames)
219 {
220 foreach(const QString& handlerName, handlerNames)
221 {
222 this->setMsgHandlerEnabled(handlerName, true);
223 }
224 }
225
226 //------------------------------------------------------------------------------
enableAllMsgHandler()227 void ctkErrorLogModel::enableAllMsgHandler()
228 {
229 this->setAllMsgHandlerEnabled(true);
230 }
231
232 //------------------------------------------------------------------------------
disableAllMsgHandler()233 void ctkErrorLogModel::disableAllMsgHandler()
234 {
235 this->setAllMsgHandlerEnabled(false);
236 }
237
238 //------------------------------------------------------------------------------
setAllMsgHandlerEnabled(bool enabled)239 void ctkErrorLogModel::setAllMsgHandlerEnabled(bool enabled)
240 {
241 Q_D(ctkErrorLogModel);
242 foreach(const QString& msgHandlerName, d->RegisteredHandlers.keys())
243 {
244 this->setMsgHandlerEnabled(msgHandlerName, enabled);
245 }
246 }
247
248 //------------------------------------------------------------------------------
terminalOutputs() const249 ctkErrorLogTerminalOutput::TerminalOutputs ctkErrorLogModel::terminalOutputs()const
250 {
251 Q_D(const ctkErrorLogModel);
252 ctkErrorLogTerminalOutput::TerminalOutputs currentTerminalOutputs;
253 currentTerminalOutputs |= d->StdErrTerminalOutput.enabled() ? ctkErrorLogTerminalOutput::StandardError : ctkErrorLogTerminalOutput::None;
254 currentTerminalOutputs |= d->StdOutTerminalOutput.enabled() ? ctkErrorLogTerminalOutput::StandardOutput : ctkErrorLogTerminalOutput::None;
255 return currentTerminalOutputs;
256 }
257
258 //------------------------------------------------------------------------------
setTerminalOutputs(const ctkErrorLogTerminalOutput::TerminalOutputs & terminalOutput)259 void ctkErrorLogModel::setTerminalOutputs(
260 const ctkErrorLogTerminalOutput::TerminalOutputs& terminalOutput)
261 {
262 Q_D(ctkErrorLogModel);
263 d->StdErrTerminalOutput.setEnabled(terminalOutput & ctkErrorLogTerminalOutput::StandardOutput);
264 d->StdOutTerminalOutput.setEnabled(terminalOutput & ctkErrorLogTerminalOutput::StandardError);
265 }
266
267 //------------------------------------------------------------------------------
addEntry(const QDateTime & currentDateTime,const QString & threadId,ctkErrorLogLevel::LogLevel logLevel,const QString & origin,const ctkErrorLogContext & context,const QString & text)268 void ctkErrorLogModel::addEntry(const QDateTime& currentDateTime, const QString& threadId,
269 ctkErrorLogLevel::LogLevel logLevel,
270 const QString& origin, const ctkErrorLogContext &context, const QString &text)
271 {
272 Q_D(ctkErrorLogModel);
273
274 if (d->AddingEntry)
275 {
276 return;
277 }
278
279 d->AddingEntry = true;
280
281 QString timeFormat("dd.MM.yyyy hh:mm:ss");
282
283 bool groupEntry = false;
284 if (d->LogEntryGrouping)
285 {
286 int lastRowIndex = d->StandardItemModel.rowCount() - 1;
287
288 QString lastRowThreadId = this->logEntryData(lastRowIndex, Self::ThreadIdColumn).toString();
289 bool threadIdMatched = threadId == lastRowThreadId;
290
291 QString lastRowLogLevel = this->logEntryData(lastRowIndex, Self::LogLevelColumn).toString();
292 bool logLevelMatched = d->ErrorLogLevel(logLevel) == lastRowLogLevel;
293
294 QString lastRowOrigin = this->logEntryData(lastRowIndex, Self::OriginColumn).toString();
295 bool originMatched = origin == lastRowOrigin;
296
297 QDateTime lastRowDateTime =
298 QDateTime::fromString(this->logEntryData(lastRowIndex, Self::TimeColumn).toString(), timeFormat);
299 int groupingIntervalInMsecs = 1000;
300 bool withinGroupingInterval = lastRowDateTime.time().msecsTo(currentDateTime.time()) <= groupingIntervalInMsecs;
301
302 groupEntry = threadIdMatched && logLevelMatched && originMatched && withinGroupingInterval;
303 }
304
305 if (!groupEntry)
306 {
307 QList<QStandardItem*> itemList;
308
309 // Time item
310 QStandardItem * timeItem = new QStandardItem(currentDateTime.toString(timeFormat));
311 timeItem->setEditable(false);
312 itemList << timeItem;
313
314 // ThreadId item
315 QStandardItem * threadIdItem = new QStandardItem(threadId);
316 threadIdItem->setEditable(false);
317 itemList << threadIdItem;
318
319 // LogLevel item
320 QStandardItem * logLevelItem = new QStandardItem(d->ErrorLogLevel(logLevel));
321 logLevelItem->setEditable(false);
322 itemList << logLevelItem;
323
324 // Origin item
325 QStandardItem * originItem = new QStandardItem(origin);
326 originItem->setEditable(false);
327 itemList << originItem;
328
329 // Description item
330 QStandardItem * descriptionItem = new QStandardItem();
331 QString descriptionText(text);
332 descriptionItem->setData(descriptionText.left(160).append((descriptionText.size() > 160) ? "..." : ""), Qt::DisplayRole);
333 descriptionItem->setData(descriptionText, ctkErrorLogModel::DescriptionTextRole);
334 descriptionItem->setEditable(false);
335 itemList << descriptionItem;
336
337 d->StandardItemModel.invisibleRootItem()->appendRow(itemList);
338 }
339 else
340 {
341 // Retrieve description associated with last row
342 QModelIndex lastRowDescriptionIndex =
343 d->StandardItemModel.index(d->StandardItemModel.rowCount() - 1, ctkErrorLogModel::DescriptionColumn);
344
345 QStringList updatedDescription;
346 updatedDescription << lastRowDescriptionIndex.data(ctkErrorLogModel::DescriptionTextRole).toString();
347 updatedDescription << text;
348
349 d->StandardItemModel.setData(lastRowDescriptionIndex, updatedDescription.join("\n"),
350 ctkErrorLogModel::DescriptionTextRole);
351
352 // Append '...' to displayText if needed
353 QString displayText = lastRowDescriptionIndex.data().toString();
354 if (!displayText.endsWith("..."))
355 {
356 d->StandardItemModel.setData(lastRowDescriptionIndex, displayText.append("..."), Qt::DisplayRole);
357 }
358 }
359
360 d->AddingEntry = false;
361
362 QString fileLogText = d->FileLoggingPattern;
363 fileLogText.replace("%{level}", d->ErrorLogLevel(logLevel).toUpper());
364 fileLogText.replace("%{timestamp}", currentDateTime.toString(timeFormat));
365 fileLogText.replace("%{origin}", origin);
366 fileLogText.replace("%{pid}", QString("%1").arg(QCoreApplication::applicationPid()));
367 fileLogText.replace("%{threadid}", threadId);
368 fileLogText.replace("%{function}", context.Function);
369 fileLogText.replace("%{line}", QString("%1").arg(context.Line));
370 fileLogText.replace("%{file}", context.File);
371 fileLogText.replace("%{category}", context.Category);
372 fileLogText.replace("%{msg}", context.Message);
373 d->FileLogger.logMessage(fileLogText.trimmed());
374
375 emit this->entryAdded(logLevel);
376 }
377
378 //------------------------------------------------------------------------------
clear()379 void ctkErrorLogModel::clear()
380 {
381 Q_D(ctkErrorLogModel);
382 d->StandardItemModel.invisibleRootItem()->removeRows(0, d->StandardItemModel.rowCount());
383 }
384
385 //------------------------------------------------------------------------------
filterEntry(const ctkErrorLogLevel::LogLevels & logLevel,bool disableFilter)386 void ctkErrorLogModel::filterEntry(const ctkErrorLogLevel::LogLevels& logLevel,
387 bool disableFilter)
388 {
389 Q_D(ctkErrorLogModel);
390
391 QStringList patterns;
392 if (!this->filterRegExp().pattern().isEmpty())
393 {
394 patterns << this->filterRegExp().pattern().split("|");
395 }
396 patterns.removeAll(d->ErrorLogLevel(ctkErrorLogLevel::None));
397
398 // foreach(QString s, patterns)
399 // {
400 // std::cout << "pattern:" << qPrintable(s) << std::endl;
401 // }
402
403 QMetaEnum logLevelEnum = d->ErrorLogLevel.metaObject()->enumerator(0);
404 Q_ASSERT(QString("LogLevel").compare(logLevelEnum.name()) == 0);
405
406 // Loop over enum values and append associated name to 'patterns' if
407 // it has been specified within 'logLevel'
408 for (int i = 1; i < logLevelEnum.keyCount(); ++i)
409 {
410 int aLogLevel = logLevelEnum.value(i);
411 if (logLevel & aLogLevel)
412 {
413 QString logLevelAsString = d->ErrorLogLevel(static_cast<ctkErrorLogLevel::LogLevel>(aLogLevel));
414 if (!disableFilter)
415 {
416 patterns << logLevelAsString;
417 d->CurrentLogLevelFilter |= static_cast<ctkErrorLogLevel::LogLevels>(aLogLevel);
418 }
419 else
420 {
421 patterns.removeAll(logLevelAsString);
422 d->CurrentLogLevelFilter ^= static_cast<ctkErrorLogLevel::LogLevels>(aLogLevel);
423 }
424 }
425 }
426
427 if (patterns.isEmpty())
428 {
429 // If there are no patterns, let's filter with the None level so that
430 // all entries are filtered out.
431 patterns << d->ErrorLogLevel(ctkErrorLogLevel::None);
432 }
433
434 bool filterChanged = true;
435 QStringList currentPatterns = this->filterRegExp().pattern().split("|");
436 if (currentPatterns.count() == patterns.count())
437 {
438 foreach(const QString& p, patterns)
439 {
440 currentPatterns.removeAll(p);
441 }
442 filterChanged = currentPatterns.count() > 0;
443 }
444
445 this->setFilterRegExp(patterns.join("|"));
446
447 if (filterChanged)
448 {
449 emit this->logLevelFilterChanged();
450 }
451 }
452
453 //------------------------------------------------------------------------------
logLevelFilter() const454 ctkErrorLogLevel::LogLevels ctkErrorLogModel::logLevelFilter()const
455 {
456 Q_D(const ctkErrorLogModel);
457 QMetaEnum logLevelEnum = d->ErrorLogLevel.metaObject()->enumerator(0);
458 Q_ASSERT(QString("LogLevel").compare(logLevelEnum.name()) == 0);
459
460 ctkErrorLogLevel::LogLevels filter = ctkErrorLogLevel::Unknown;
461 foreach(const QString& filterAsString, this->filterRegExp().pattern().split("|"))
462 {
463 filter |= static_cast<ctkErrorLogLevel::LogLevels>(logLevelEnum.keyToValue(filterAsString.toLatin1()));
464 }
465 return filter;
466 }
467
468 //------------------------------------------------------------------------------
logEntryGrouping() const469 bool ctkErrorLogModel::logEntryGrouping()const
470 {
471 Q_D(const ctkErrorLogModel);
472 return d->LogEntryGrouping;
473 }
474
475 //------------------------------------------------------------------------------
setLogEntryGrouping(bool value)476 void ctkErrorLogModel::setLogEntryGrouping(bool value)
477 {
478 Q_D(ctkErrorLogModel);
479 d->LogEntryGrouping = value;
480 }
481
482 //------------------------------------------------------------------------------
asynchronousLogging() const483 bool ctkErrorLogModel::asynchronousLogging()const
484 {
485 Q_D(const ctkErrorLogModel);
486 return d->AsynchronousLogging;
487 }
488
489 //------------------------------------------------------------------------------
setAsynchronousLogging(bool value)490 void ctkErrorLogModel::setAsynchronousLogging(bool value)
491 {
492 Q_D(ctkErrorLogModel);
493 if (d->AsynchronousLogging == value)
494 {
495 return;
496 }
497
498 foreach(const QString& handlerName, d->RegisteredHandlers.keys())
499 {
500 d->setMessageHandlerConnection(
501 d->RegisteredHandlers.value(handlerName), value);
502 }
503
504 d->AsynchronousLogging = value;
505 }
506
507 // --------------------------------------------------------------------------
filePath() const508 QString ctkErrorLogModel::filePath()const
509 {
510 Q_D(const ctkErrorLogModel);
511 return d->FileLogger.filePath();
512 }
513
514 // --------------------------------------------------------------------------
setFilePath(const QString & filePath)515 void ctkErrorLogModel::setFilePath(const QString& filePath)
516 {
517 Q_D(ctkErrorLogModel);
518 return d->FileLogger.setFilePath(filePath);
519 }
520
521 // --------------------------------------------------------------------------
numberOfFilesToKeep() const522 int ctkErrorLogModel::numberOfFilesToKeep()const
523 {
524 Q_D(const ctkErrorLogModel);
525 return d->FileLogger.numberOfFilesToKeep();
526 }
527
528 // --------------------------------------------------------------------------
setNumberOfFilesToKeep(int value)529 void ctkErrorLogModel::setNumberOfFilesToKeep(int value)
530 {
531 Q_D(ctkErrorLogModel);
532 return d->FileLogger.setNumberOfFilesToKeep(value);
533 }
534
535 // --------------------------------------------------------------------------
fileLoggingEnabled() const536 bool ctkErrorLogModel::fileLoggingEnabled()const
537 {
538 Q_D(const ctkErrorLogModel);
539 return d->FileLogger.enabled();
540 }
541
542 // --------------------------------------------------------------------------
setFileLoggingEnabled(bool value)543 void ctkErrorLogModel::setFileLoggingEnabled(bool value)
544 {
545 Q_D(ctkErrorLogModel);
546 d->FileLogger.setEnabled(value);
547 }
548
549 // --------------------------------------------------------------------------
fileLoggingPattern() const550 QString ctkErrorLogModel::fileLoggingPattern()const
551 {
552 Q_D(const ctkErrorLogModel);
553 return d->FileLoggingPattern;
554 }
555
556 // --------------------------------------------------------------------------
setFileLoggingPattern(const QString & value)557 void ctkErrorLogModel::setFileLoggingPattern(const QString& value)
558 {
559 Q_D(ctkErrorLogModel);
560 d->FileLoggingPattern = value;
561 }
562
563 // --------------------------------------------------------------------------
logEntryData(int row,int column,int role) const564 QVariant ctkErrorLogModel::logEntryData(int row, int column, int role) const
565 {
566 Q_D(const ctkErrorLogModel);
567 if (column < 0 || column > Self::MaxColumn
568 || row < 0 || row > this->logEntryCount())
569 {
570 return QVariant();
571 }
572 QModelIndex rowDescriptionIndex = d->StandardItemModel.index(row, column);
573 return rowDescriptionIndex.data(role);
574 }
575
576 // --------------------------------------------------------------------------
logEntryDescription(int row) const577 QString ctkErrorLogModel::logEntryDescription(int row) const
578 {
579 return this->logEntryData(row, Self::DescriptionColumn, Self::DescriptionTextRole).toString();
580 }
581
582 // --------------------------------------------------------------------------
logEntryCount() const583 int ctkErrorLogModel::logEntryCount()const
584 {
585 Q_D(const ctkErrorLogModel);
586 return d->StandardItemModel.rowCount();
587 }
588