5#include <QtCore/QJsonArray>
6#include <QtCore/QJsonDocument>
7#include <QtStateMachine/QAbstractState>
8#include <QtStateMachine/QAbstractTransition>
9#include <QtStateMachine/QSignalTransition>
14 const char*
Red =
"\033[31m";
17 const char*
Blue =
"\033[34m";
19 const char*
Cyan =
"\033[36m";
22 const char*
Gray =
"\033[90m";
35 QString timeStr = QStringLiteral(
"[+%1]").arg(
elapsedMs / 1000.0, 7,
'f', 3);
39 result += timeStr +
" ";
44 if (indent &&
depth > 0) {
45 result += QString(
depth * 2,
' ');
54 eventStr = QStringLiteral(
"▶ Entered %1").arg(
state);
59 eventStr = QStringLiteral(
"◀ Exited %1").arg(
state);
75 eventStr = QStringLiteral(
"⏱ Timeout in %1").arg(
state);
80 eventStr = QStringLiteral(
"✖ Error in %1").arg(
state);
82 eventStr += QStringLiteral(
": %1").arg(
message);
88 eventStr = QStringLiteral(
"▷ Machine started: %1").arg(
machine);
93 eventStr = QStringLiteral(
"□ Machine stopped: %1").arg(
machine);
98 eventStr = QStringLiteral(
"↻ Retry in %1").arg(
state);
100 eventStr += QStringLiteral(
": %1").arg(
message);
106 eventStr = QStringLiteral(
"⚡ Signal: %1").arg(
message);
111 eventStr = QStringLiteral(
"◐ Progress: %1").arg(
message);
121 result += QStringLiteral(
"%1%2%3").arg(colorCode, eventStr,
Colors::Reset);
132 obj[
"timestamp"] = timestamp.toString(Qt::ISODateWithMs);
133 obj[
"elapsed_ms"] = elapsedMs;
134 obj[
"level"] =
static_cast<int>(level);
135 obj[
"event"] =
static_cast<int>(event);
136 obj[
"machine"] = machine;
138 if (!state.isEmpty()) {
139 obj[
"state"] = state;
141 if (!previousState.isEmpty()) {
142 obj[
"previous_state"] = previousState;
144 if (!message.isEmpty()) {
145 obj[
"message"] = message;
147 if (!transitionReason.isEmpty()) {
148 obj[
"transition_reason"] = transitionReason;
150 if (stateDurationMs > 0) {
151 obj[
"state_duration_ms"] = stateDurationMs;
153 if (!context.isEmpty()) {
154 obj[
"context"] = context;
157 obj[
"depth"] = depth;
168 : QObject(parent ? parent : machine)
180 if (_enabled == enabled) {
188 _connections.append(connect(_machine, &QStateMachine::started,
189 this, &StateMachineLogger::_onMachineStarted));
190 _connections.append(connect(_machine, &QStateMachine::stopped,
191 this, &StateMachineLogger::_onMachineStopped));
194 const auto states = _machine->findChildren<QAbstractState*>();
195 for (QAbstractState* state : states) {
196 _connectToState(state);
199 _machineTimer.start();
202 for (
const auto& conn : _connections) {
205 _connections.clear();
209void StateMachineLogger::_connectToState(QAbstractState* state)
211 auto entryConn = connect(state, &QAbstractState::entered,
212 this, &StateMachineLogger::_onStateEntered);
213 auto exitConn = connect(state, &QAbstractState::exited,
214 this, &StateMachineLogger::_onStateExited);
215 _connections.append(entryConn);
216 _connections.append(exitConn);
221 _stateLogLevels[stateName] = level;
226 _stateLogLevels.remove(stateName);
231 _excludedStates.insert(stateName);
236 _excludedStates.remove(stateName);
243 _logFile =
new QFile(filePath,
this);
244 if (!_logFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
250 _logStream =
new QTextStream(_logFile);
258 _logStream =
nullptr;
269 _crashLogMaxEntries = maxEntries;
275 _crashLogMaxEntries = 0;
282 lines << QStringLiteral(
"=== Crash Log: %1 ===").arg(_machine->objectName());
283 lines << QStringLiteral(
"Entries: %1").arg(_crashLog.size());
286 for (
const auto& entry : _crashLog) {
287 lines << entry.toString(
false,
true,
true);
290 return lines.join(
'\n');
295 if (!_enabled || level > _logLevel) {
300 entry.
timestamp = QDateTime::currentDateTime();
301 entry.
elapsedMs = _machineTimer.elapsed();
304 entry.
machine = _machine->objectName();
307 entry.
depth = _currentDepth;
314 if (!_enabled || !(_logFilter & event)) {
323 if (requiredLevel > _logLevel) {
328 entry.
timestamp = QDateTime::currentDateTime();
329 entry.
elapsedMs = _machineTimer.elapsed();
330 entry.
level = requiredLevel;
332 entry.
machine = _machine->objectName();
335 entry.
depth = _currentDepth;
340void StateMachineLogger::_log(
const LogEntry& entry)
343 _eventCounts[entry.event]++;
346 if (_crashLogMaxEntries > 0) {
347 _crashLog.append(entry);
348 while (_crashLog.size() > _crashLogMaxEntries) {
349 _crashLog.removeFirst();
354 QString formatted = entry.toString(_coloredOutput, _logTimings, _logIndent);
357 qCDebug(QGCStateMachineLog).noquote() << formatted;
361 *_logStream << entry.toString(
false, _logTimings, _logIndent) <<
"\n";
366 if (_customHandler) {
367 _customHandler(entry);
373 if (_stateLogLevels.contains(stateName)) {
374 return _stateLogLevels[stateName];
379void StateMachineLogger::_onMachineStarted()
385 _machineTimer.restart();
387 _previousState.clear();
388 _stateEntryTimes.clear();
391 entry.timestamp = QDateTime::currentDateTime();
395 entry.machine = _machine->objectName();
400void StateMachineLogger::_onMachineStopped()
407 entry.timestamp = QDateTime::currentDateTime();
408 entry.elapsedMs = _machineTimer.elapsed();
411 entry.machine = _machine->objectName();
416void StateMachineLogger::_onStateEntered()
418 auto* state = qobject_cast<QAbstractState*>(sender());
421 QString stateName = state->objectName();
424 if (_excludedStates.contains(stateName)) {
429 if (_effectiveLogLevel(stateName) <
Normal) {
435 _stateEntryTimes[stateName] = _machineTimer.elapsed();
436 _previousState = stateName;
440 qint64 now = _machineTimer.elapsed();
441 _stateEntryTimes[stateName] = now;
444 entry.timestamp = QDateTime::currentDateTime();
445 entry.elapsedMs = now;
448 entry.machine = _machine->objectName();
449 entry.state = stateName;
450 entry.previousState = _previousState;
451 entry.depth = _currentDepth;
454 if (_logTransitionReasons && !_previousState.isEmpty()) {
455 entry.transitionReason = _determineTransitionReason();
460 _previousState = stateName;
464void StateMachineLogger::_onStateExited()
466 auto* state = qobject_cast<QAbstractState*>(sender());
469 QString stateName = state->objectName();
472 if (_excludedStates.contains(stateName)) {
476 _currentDepth = qMax(0, _currentDepth - 1);
479 if (_effectiveLogLevel(stateName) <
Normal) {
487 qint64 now = _machineTimer.elapsed();
489 if (_stateEntryTimes.contains(stateName)) {
490 duration = now - _stateEntryTimes[stateName];
491 _stateEntryTimes.remove(stateName);
495 entry.timestamp = QDateTime::currentDateTime();
496 entry.elapsedMs = now;
499 entry.machine = _machine->objectName();
500 entry.state = stateName;
501 entry.stateDurationMs = duration;
502 entry.depth = _currentDepth;
507QString StateMachineLogger::_determineTransitionReason()
const
514QString StateMachineLogger::_colorize(
const QString& text,
const QString& colorCode)
const
516 if (!_coloredOutput) {
522QString StateMachineLogger::_eventColor(LogEvent event)
const
QGroundControl specific state machine with enhanced error handling.
void setEnabled(bool enabled)
void excludeState(const QString &stateName)
Exclude specific states from logging.
StateMachineLogger(QGCStateMachine *machine, QObject *parent=nullptr)
void enableCrashLog(int maxEntries)
Enable crash log buffer (circular buffer for post-mortem analysis)
void setStateLogLevel(const QString &stateName, LogLevel level)
Set log level override for a specific state.
void log(LogLevel level, const QString &message, const QJsonObject &context=QJsonObject())
Log a custom message.
void includeState(const QString &stateName)
void logEvent(LogEvent event, const QString &message, const QJsonObject &context=QJsonObject())
Log with specific event type.
LogLevel
Log verbosity levels.
@ Normal
Errors + state changes.
@ Verbose
Normal + signals/transitions.
QString dumpCrashLog() const
void clearStateLogLevel(const QString &stateName)
LogEvent
Types of log events.
bool setLogFile(const QString &filePath)
Log to a file (in addition to console)
~StateMachineLogger() override
QString toString(bool colored=false, bool showTiming=true, bool indent=true) const
QJsonObject toJson() const