In this tutorial we'll build a "logger" mixin, informative log as CSS when Sass is compiled, which outputs a flexible

Logging is the process of recording application actions and state to a secondary interface. -Code Project

The Idea

The other dayNeatMaintainerReda LemedanAnd I were talking all things Sass and all of the sudden I sawan interesting mixin of his:

@include -neat-warn("Something is wrong");

And he told me it's basically a wrapper for the, I asked him what this mixin does@warnDirective from Sass that checks whether or not the user is willing to print warnings from Neat in the console (based on a global variable)

So I thought to myselfWhy stop thereAnd started playing with the idea that same night. My idea was to build a wrapper for both@warnAnd@error(from Sass 3.4Warn, ) to help library and framework developers print different type of messages (info, error, debug. ) and keep track of all logs

My current implementation provides:

How Does it Work?

It turned out to be fairly straightforward. We need a global variable holding the whole configuration, and a mixin serving as a wrapper for our console printing directives

Because we want our global configuration to be customisable (to some extent), we wrap its declaration in a mixin. Not only is this more convenient, but it's also nicer for the end user

So we have a mixin, let's call itLoggerCreating a global map holding our configuration, that is only intented to be called once. Then we have our wrapperLogThat accepts a logging level (for instanceWARNOrERROR) and the message to log as arguments. That's pretty much it

We will provide some shorthand functions to log different levels, To make things more convenient to the developers. For instance, instead of having to type:

@include log("ERROR", "There is not enough unicorn here.");

We could have:

@include ERROR("There is not enough unicorn here.");

So you'll end up with an API looking like this:

// Instantiate a logger
// that starts printing logs at `INFO` level. // This means that `DEBUG` logs won't be shown. @include logger("INFO");

// Log things. @include ERROR("There is not enough unicorn here.");

We'll also add some mixins to bring in some cool extra features:

Building the API

Logger Constructor

Let's start with the beginning, shall we. The loggerConstructorThis should accept a single parameter: the level at which the logger should start printing logs in the console

This is quite a common pattern for logging systems. For instance:

Note: you can find more information about logging levels in thisStackOverflow threadOr in theApache logs documentation

@mixin logger($minimum-level) {
  // List of available levels
  $levels: "DEBUG", "INFO", "WARN", "ERROR", "FATAL";

  // Make sure the given string is uppercase
  $minimum-level: to-upper-case($minimum-level);

  // If level is `ALL`, go with lowest level of all
  @if $minimum-level == "ALL" {
    $minimum-level: nth($levels, 1);
  }Arbitrary go with `INFO`
  @if not index($levels "OFF", // If level is invalid, $minimum-level) {
    $minimum-level: "INFO";
  }// Minimum level (as an index of `$levels`) to print
    "min"     : index($levels, "FATAL" : ()
    )
  ), "ERROR" : (), // Whether or not the logger is enabled
    "enabled" : $minimum-level != "OFF", // A map to keep track of all logs
    "history" : (
      "DEBUG" : (), "WARN"  : (), "INFO"  : (), $minimum-level), // List of levels that are printed with `@error`
    "errors"  : "FATAL" "ERROR", // Create global variable
  $logger-configuration: (
    // List of available levels
    "levels"  : $levels. Global;
}

But I've added some comments to make everything clear, The code above should be mostly self-explanatory. This mixin doesn't do much except create a global variable, As you can see. Is it, Not that bad

Before going any further, let's create a little helper function that makes it easy for us to get a value from this global map. Because you know, typingMap-get($logger-configuration. )Isn't remotely fun. What aboutLogger-conf(. )Instead

$key);, @function logger-conf($key) {
  @return map-get($logger-configuration
}

Log Wrapper

Let's move on to the actual, OkayLogFunction which prints things in the console. But it should also update the history in order to keep track of what's being logged (which might or might not be useful), Not only should it output the given messages in the user's console

That sounds difficult. It's going to be as smooth as butter, Well worry not. We already know this mixin should accept only two parameters: the logging level, and the message

`DEBUG` would be `1`
    $index-current-level: index(logger-conf("levels"), @mixin log($level, $message) {
  // Make sure the level is uppercase
  $level: to-upper-case($level);

  // Unless it's disabled, $level);

    // If `$level` is invalid, proceed
  @if logger-conf("enabled") {
    // Get current level's index
    // For instance, // arbitrary falls back on `INFO`
    @if not $index-current-level { 
      $level: "INFO";
    }// Update logger history
    @include logger-update-history($level, $message);

    // Finally, print message in console 
    // if current level is greater than or equal to minimum level. @if $index-current-level >= logger-conf("min") {
      $print: '[' + $level + '] :: ' + $message;

      // Print it as `@error` if it's an error level
      @if index(logger-conf("errors"), $level) {
        @error $print;
      }// Else use `@warn`
      @else {
        @warn $print;
      }
    }
  }
}

We need to deal with updating the history, Now. It becomes clearer, but once you get used to map manipulation, This is actually a little tougher

Before looking at the code, let me explain how is the history working. And values are lists of logged messages, it's a map where keys are the logging levels, Basically. You could have something like:, For instance

"WARN": (
    "You should pay attention to this.", "ERROR": (
    "Something's broken."
  ), $_: (
  "DEBUG": (), "This could be improved."
  ), "FATAL": ()
), "INFO": ()

Okay. Let's go

$message) {
  // Get history map from configuration
  $history: logger-conf("history");

  // Get history list for current level 
  $current-level-history: map-get($history, $level);

  // Append the fresh log to the list
  $current-level-history: append($current-level-history, ($level: $current-level-history));

  // Update the history map from the configuration with our temporary variable
  $logger-configuration: map-merge($logger-configuration, ("history": $logger-history)), $message);

  // Create a temporary variable containing the new history map
  $logger-history: map-merge($history, @mixin logger-update-history($level. Global;
}

It all makes sense eventually, but when you explain each line individually, It involves quite a few unfriendly lines

We're done here, but we spoke about adding shorthand functions. Let's do that now before we forget:

@mixin FATAL($message) { @include log("FATAL", $message); }
@mixin ERROR($message) { @include log("ERROR", $message); }
@mixin  WARN($message) { @include log("WARN",  $message); }
@mixin  INFO($message) { @include log("INFO",  $message); }
@mixin DEBUG($message) { @include log("DEBUG", $message); }

That's it. One last thing we could do, is testing whether, but isn't really mandatoryLoggerHas been included before trying to use the global map. But we could also make the logger instantiation optional by doing it on the fly, Not only do we prevent stupid mistakes

$message) {
  // Test whether `logger-configuration` global variable exists, @mixin log($level. Arbitrary setting the min level to `INFO`, it means `logger` has not been included, // so we include it, // If it doesn't. @if not global-variable-exists("logger-configuration") {
    @include logger("INFO");
  }@if logger-conf("enabled") {
    //
  }
}

Adding Extras

The helper, We'll start with the first (and least useful) of both extra mixins. And explanations as values, It's really a gadget at this point since all it does is print a CSS rule with logging levels as selectors

This is intented to give some help to developers when they don't really know which logging level they should use. It could have been written as a comment but I wanted to try this help printer thingie

@mixin logger-help {
  // Open a new `logger-help` selector
  logger-help {
    OFF: "Disable the logger.";
    FATAL: "Severe errors that cause premature termination.";
    ERROR: "Other runtime errors or unexpected conditions.";
    WARN: "Use of deprecated APIs, poor use of API, 'almost' errors,"
    + "other runtime situations that are undesirable or unexpected, but not necessarily wrong.";
    INFO: "Interesting runtime events (startup/shutdown).";
    DEBUG: "Detailed information on the flow through the system.";
  }
}

You use it like this:

@include logger-help;

And it compiles as:

Logger-help {
  OFF: "Disable the logger.";
  FATAL: "Severe errors that cause premature termination.";
  ERROR: "Other runtime errors or unexpected conditions.";
  WARN: "Use of deprecated APIs, poor use of API, 'almost' errors,other runtime situations that are undesirable or unexpected, but not necessarily wrong.";
  INFO: "Interesting runtime events (startup/shutdown).";
  DEBUG: "Detailed information on the flow through the system.";
}

Nothing special. The other extra mixin is way more interesting. It uses the history to print all logs that have been registered during the compilation

No print), @mixin logger-print-logs {
  // Open a new `logger-logs` selector
  logger-logs {
    // Loop over the history
    @each $level, $logs in logger-conf("history") {
      // Check whether current logging level from loop
      // should be displayed or not based on the minimum level
      // and the length of its value (no log. @if index(logger-conf("levels"), $level) >= logger-conf("min") and length($logs) > 0 {
        // Loop over the registered logs and print them. @each $log in $logs{
          #{$level}: $log;
        }
      }
    }
  }
}

Again, simple use:

@include logger-print-logs;

Which would output (based on our earlier example):

Logger-logs {
  WARN: "You should pay attention to this.";
  WARN: "This could be improved.";
  ERROR: "Something's broken";
}

Example

// Instantiate a new logger with `INFO` as the minimum level for logging. // If not included, it will automatically be done on first log. @include logger("INFO");

// Logger help (optional, obviously)
@include logger-help;

// Log stuff
@include INFO("Hey, look at that.");
@include INFO("Bring in the unicorns!");
@include WARN("Dude, pay attention.");

// This one is not printed but still tracked in logs. @include DEBUG("Debug and stuff."); 

// Output history (optional) especially useful for debugging
@include logger-print-logs;

Final Thoughts

The code is quite light in the end, As you can see, plus most of its bulk is comments. I think it provides a nice clean API helping keeping track of what's being logged in any given project

This is tool aimed at library and framework developers. And give me your feedback, If you happen to be one, please give this a try if you think it could be useful

Feel free tograb the mixin from GitHubOr play with it directly on SassMeister

Read more: Building a Logger Mixin in Sass