DEV Community

Mattia Pispisa
Mattia Pispisa

Posted on

Efficient logging in applications

Introduction

In this post, we'll explore an approach to logging messages of your applications. I'll also conclude with my own package solution for logging messages in dart/flutter applications.

But first of all, what does logging mean?

In the state of art logging is

the act of keeping a log of events that occur in a computer system, such as problems, errors or just information on current operations ...

So it's a crucial activity in computing for monitoring and understanding how a system operates.

But how can we do it?

We can answer this question by answering the following points:

  • "What should we log?";
  • "When should we log?";
  • "Where should we log?".

What should we log?

Even just from the previous explanation, we can find multiple information we should log:

  • Messages useful for debugging our application;
  • Error messages to alert us in case of abnormal behavior in our application;
  • Information about the normal use of the app. This approach is very effective for system verification, especially when providing support to customers where the application isn't showing errors but is still not functioning correctly due to behavioral bugs.

We have seen various purposes for which logging information is essential, which suggests the need for severity levels. These could be, in a simple way: debug, info, and error.

when should we log

We have seen various reasons for logging messages. However, writing them down every time might not always be useful; in fact, it could create noise that would make (for example) error analysis more difficult.
At first, by generalizing the use cases, we might assume that debug messages might only be logged during development (obviously, the other types of errors are also useful during development). Errors should always be logged but in production they take a greater importance. Info messages are useful for system checks or customer support. There will likely be many informational messages in the life of an application, so logging them all the time in production could be counterproductive (due to noise and storage costs). Therefore, it would be better to have flags to enable or disable them as needed (like for the customer support).

Where should we log

The final question is where. During development, it is very convenient to write any type of message to the debug console. However, in production or staging environments, it is very useful to have online systems to send messages, such as Sentry or Grafana, or, where that's not possible, a mechanism for saving them to the file system (or both and other options!).

Combining the "when" with the "where" gives us a matrix of scenarios. Obviously, this matrix doesn’t cover all the real-world cases of an actual application, but it can be a good initial approximation from which to develop a solution. It’s also useful as an intermediate step before analyzing my dart package proposal.

Development Production
Debug Console X (Enabled with a flag?)
Info Console X (Enabled with a flag?)
Error Console Online tools, File system, ...

However, only the where and when change; the logs themselves remain the same!

Let's dive into the analysis of my proposal.

It is important to differentiate between the system used to add a new message and the management of where and when these messages are handled (and any additional information).

logger-class-diagram-proposal

where the log method of the Logger class will be:

class Logger {
  void log(Severity severity, String message) {
    // iterate through each handler that will manage the log. 
    for (final handler in handlers) {
      handler.log(severity, message);
    }
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

This approach allows us to have a Logger instance that can be passed throughout the system, while we set up different handlers (implementation of Handler) in a single location, such as during the application's bootstrap phase.

For example, referring to the class diagram, we can add a ConsoleHandler only if we're in development mode, a SentryHandler only in production (and in its log method, we decide to log only if the severity is error) and or a FileSystemHandler that is activated only in production for applications that cannot communicate with external systems like Sentry.

class ConsoleHandler implements Handler {
  void log(Severity severity, String message) {
    developer.log(message)
  }
}

class SentryHandler implements Handler {
  void log(Severity severity, String message) {
    if (severity != Severity.error) {
      return 
    }
    // log message
  }
}

void bootstrap() {
  ...
  final logger = Logger();

  if (isDev()) {
    logger.add(ConsoleHandler());
  }

  if (isProd()) {
    logger.add(SentryHandler());
  }
}
Enter fullscreen mode Exit fullscreen mode

A system like this, which differentiates between loggers and handlers, makes the entire setup highly flexible. Another example, though perhaps less common but still effective, is having a handler within a UI component (AppPageViewHandler) to listen to messages and transform them into interactive widgets. This approach allows for filtering, categorization, and other functionalities.

In Dart, I developed the library en_logger. This library applies the concepts mentioned above, with an extended severity level adhering to syslog levels and many additional features such as: instances, attachments and prefix.

Here’s a practical example (included in the library's example):

void main(List<String> args) async {
  // a custom handler that under the hood use sentry
  final sentry = await SentryHandler.init();

  // default printer configured
  final printer = PrinterHandler()
    ..configure({Severity.notice: PrinterColor.green()});

  // an enLogger with a default prefix format
  final logger = EnLogger(
      defaultPrefixFormat: PrefixFormat(
    startFormat: '[',
    endFormat: ']',
    style: PrefixStyle.uppercaseSnakeCase,
  ))
    ..addHandlers([
      sentry,
      printer,
    ]);

  // debug log
  logger.debug('a debug message');

  // error with data
  logger.error(
    "error",
    data: [
      EnLoggerData(
        name: "response",
        content: jsonEncode("BE data"),
        description: "serialized BE response",
      ),
    ],
  );

  // logger instance with prefix
  final instLogger = logger.getConfiguredInstance(prefix: 'API Repository');
  instLogger.debug('a debug message'); // [API Repository] a debug message
  instLogger.error(
    'error',
    prefix: 'Custom prefix',
  ); // [Custom prefix] a debug message
}

class SentryHandler extends EnLoggerHandler {
  SentryHandler._();

  static Future<SentryHandler> init() async {
    await Sentry.init(
      (options) {
        options.dsn = 'https://example@sentry.io/example';
      },
    );
    return SentryHandler._();
  }

  @override
  void write(
    String message, {
    required Severity severity,
    String? prefix,
    StackTrace? stackTrace,
    List<EnLoggerData>? data,
  }) {
    // just a simple example
    // fine tune your implementation...
    if (severity.atLeastError) {
      Sentry.captureException(message, stackTrace: stackTrace);
      return;
    }
    Sentry.captureMessage(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Initially, we configure two handlers: PrinterHandler and SentryHandler. After setup, the only reference needed is the logger, which allows us to write new messages. This decouples message handling from the actual logging operations.

With this system, we can decouple our application from external systems by wrapping them within handlers. This way, the application is not tied to external packages and can easily swap them out over time if needed.

Conclusion

I hope this post has been helpful to you. Leave a feedback, and if you're interested, give en_logger a try!

P.S. How to pass the logger instance throughout the system? Check out the post about dependency injection.

Top comments (2)

Collapse
 
alt_exist profile image
Alternate Existance

good article, earned a follow from me :)

Collapse
 
mattia profile image
Mattia Pispisa

Thank you for the kind words 🙏.