Logging

The logging subsystem in the Control Engine is logically split into two major parts:

  • Logging Sinks provide capabilities to write logging messages to one output channel/medium (e.g. writing to a terminal / writing to a set of log files)

  • Sinked Loggers manage a set of sinks and are used to distribute messages to the managed sinks. They are also responsible for message filtering based on their severity.

@startuml
package semodia::controlengine::native::osal::task {
    class BasicTaskLoopTask {
        # {abstract} iterate()
}
}
package semodia::controlengine::native::osal::logging {
 enum LogLevel {
        none,
        debug,
        info,
        warning,
        warn,
        error,
        fatal,
        max
    }

    interface ILogger {
        + log(logMessage)
        + log(LogLevel, logMessage)
        + debug(logMessage)
        + info(logMessage)
        + warning(logMessage)
        + error(logMessage)
        + fatal(logMessage)
    }

    abstract class AbstractFilteringMessageLogger implements ILogger {
        + log(logMessage)
        + log(LogLevel, logMessage)
        + debug(logMessage)
        + info(logMessage)
        + warning(logMessage)
        + error(logMessage)
        + fatal(logMessage)

        + setMinimumLogLevel(LogLevel)
        + setLock(ILockable)

        # {abstract} displayMessage(logMessage, LogLevel)
    }
    AbstractFilteringMessageLogger "*" --> "1" LogLevel : minLogLevel

    interface ILoggingSink extends BasicTaskLoopTask {
        + writeMessage(LogMessage)
        + flush()
        + setLogMessageFormatter(LogMessageFormatter)
    }

    interface ILoggingSinkManager extends BasicTaskLoopTask {
        + addSink(ILoggingSink, sinkName, minimumSeverity)
        + removeSink(sinkName)
        + setMinimumLogLevel(sinkName, LogLevel)
    }

    abstract class SinkedLogger extends AbstractFilteringMessageLogger implements ILoggingSinkManager {
        # displayMessage(logMessage, LogLevel)

        + addSink(ILoggingSink, sinkName, minimumSeverity)
        + removeSink(sinkName)
        + setMinimumLogLevel(sinkName, LogLevel)

        # iterate()
    }
    SinkedLogger "*" --> "*" ILoggingSink : sinks
    note left of SinkedLogger::displayMessage
        for sink : sinks
            if (minimumSeverity(sink) <= logMessage.getSeverity())
                sink.writeMessage(LogMessage)
    end note

    abstract class AbstractLoggingSink implements ILoggingSink {
        + setLock(ILockable)
        + setLogMessageFormatter(LogMessageFormatter)

        + writeMessage(LogMessage)
        + flush()

        # {abstract} doWriteMessage(string)
        # {abstract} doFlush()
    }
    AbstractLoggingSink "*" --> "1" LogMessageFormatter : formatter
    note left of AbstractLoggingSink::writeMessage(LogMessage)
        lock.lock()
        string message = formatter.format(LogMessage)
        doWriteMessage(message)
        lock.unlock()
    end note
    note left of AbstractLoggingSink::flush()
        lock.lock()
        doFlush()
        lock.unlock()
    end note

    class RotatingFileSink extends AbstractLoggingSink {
        + AbstractRotatingFileSink(maxFileSize, maxFileCount, filePrefix, fileExtension)
        # doWriteMessage(string)
        # doFlush()

        # iterate()
    }

    class TerminalOutputSink extends AbstractLoggingSink {
        # writeImpl(string)
        # doFlush()

        # iterate()
    }

    class UartSink extends AbstractLoggingSink {
        # doWriteMessage(string)
        # doFlush()

        # iterate()
    }

    class LogMessage{
        + std::string getPayload()
        + LogLevel getSeverity()
    }
    LogMessage "*" --> "1" LogLevel : severity

    class LogMessageFormatter {
        - std::string formatString

        + LogMessageFormatter(FormatString)
        + string formatLogMessage(LogMessage)
    }
}
@enduml

Logging Sinks

Logging Sinks are responsible for the actual writing of log messages to a medium / output channel. Logging Sink implementations implement the ILoggingSink interface:

  • writeMessage(message) - Order a logging message to be written

  • flush() - Logging sinks may internally use some kind of buffering mechanisms, force the buffers to be flushed to the media.

  • iterate() - If I/O is slow and you want to make use of asynchronous I/O, it is implemented here. See OSAL Task Framework for details.

Shared logic (locking for multithreading, formatting log messages) is implemented in an abstract super class AbstractLoggingSink. If you would like to create a new Logging Sink, simply extend AbstractLoggingSink and add your logic for writing, flushing and async I/O operations.

Currently available Logging Sinks

  • TerminalOutputSink - Writes log messages to std::cout

  • RotatingFileLogger - Writes logs to a rotating set of files - See the Doxygen documentation of the class for details.

SinkedLogger

The SinkedLogger class implements the ILoggingSink interface.

It manages multiple Logging Sinks. Messages sent to a SinkedLogger will be distributed to all registered logging sinks. Filtering logging messages is achieved in two distinct levels

  • Sinked Logger instances have their own minimum severity

  • The Sinked Logger has a minimum severity associated for each Logging Sink it manages

Following this, messages are written (i.e. not filtered) if their severity is equal or higher than both the minimum severity of the logger they are sent to, and the logging sink. This way we can easily filter messages based on the output channel, e.g. Display all log messages on a Terminal but only write critical log messages to a log file.

LogMessageFormatters

LogMessageFormatter instances are used to create writable string representations out of LogMessage objects.

LogLevels (Severities)

See the doxygen documentation for available severities.

Note

Severity::none

Setting the minimum severity of a Logging Sink or a Sinked Logger to LogLevel::none will cause filtering to be disabled.

Messages with a severity of LogLevel::None will not be filtered (i.e. always written).