{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/structured-logging-with-slog/",
  "description": "Master Go 1.21's log/slog package for structured logging with levels, JSON output, and attribute grouping. No third-party libraries needed.",
  "path": "/go/structured-logging-with-slog/",
  "publishedAt": "2023-08-10T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "TIL",
    "Logging"
  ],
  "textContent": "Before the release of version 1.21, you couldn't set levels for your log messages in Go\nwithout either using third-party libraries or writing your own boilerplates. Coming from\nPython, I've always found this odd, considering that this capability has been in the Python\nstandard library forever. However, it seems like the new log/slog subpackage in Go allows\nyou to do that and a whole lot more.\n\nApart from being able to add levels to log messages, slog also allows you to emit\nJSON-structured log messages and group them by certain attributes. The ability to do all\nthis in-house is quite neat and I wanted to take it for a spin. The [official documentation]\non this is on the terser side but still comprehensive. So, here, instead of repeating the\nsame information, I wanted to write something for me that mainly highlights the most common\ncases.\n\nKickoff\n\nHere's how you'd add levels to your log messages:\n\nRunning this will print the following output.\n\nNotice how the concomitant local time and level are prepended to each log message. Also,\nobserve that the DEBUG message is missing there. That's because the default log handler\nwill only print messages if the log level is INFO or higher. We'll see how we can set\ncustom log levels shortly. But before that here's a quick overview of how the different\ncomponents of slog work together.\n\nMachineries\n\nThe slog package lets you create Logger instances. These instances have methods like\nInfo() and Error() that you can call to log stuff. When you call one of these methods,\nit creates a Record from the data you passed in and sends it to a Handler. The Handler\nfigures out what to actually do with the log - like print it somewhere or send it over the\nnetwork. You can write your own or use one of the predefined TextHandler or JSONHandler\nto format your log output.\n\nThere's a default Logger you can use right away with functions like Info() and Error()\nat the top level. Underneath, the Info() function calls the Logger.Info() method. This\nmeans you don't need to create a Logger instance by hand just to start logging. You've\nalready seen how we can use these top-level functions to send different levels of logs to\nthe stdout.\n\nEach log entry has an associated severity level which is represented by an integer. The more\nsevere the log level is, the higher the value of the integer will be. The default logger\nonly emits LevelInfo or higher levels of log messages. Predefined levels have the\nfollowing values:\n\nUsing custom log handlers\n\nYou can use predefined custom handlers to change the format of your log output. The\nfollowing snippet creates a new Logger instance from a TextHandler instance and then\nuses that to print log messages to the stdout:\n\nRunning this prints:\n\nThe NewTextHandler function has two arguments: the first one takes in a type that\nimplements the io.Writer interface and the second one accepts a HandlerOptions struct.\nThe HandlerOptions struct can be used to customize the output format. We can pass nil\nfor this value if we don't need to change the handler's default output format.\n\nWe're passing os.Stdout as the first argument to direct the log messages to stdout and\nnil as the second argument. The NewTextHandler returns a slog.TextHandler struct\npointer which is passed to slog.New to get a new Logger instance. Then we set this newly\ncreated Logger as the default one via the slog.SetDefault() function. Finally, the\nupdated logger is used to print an info and a warning message. Notice how the TextHandler\noutput records are constituted as key-value attribute pairs.\n\nPrinting log messages in JSON format\n\nSimilar to NewTextHandler, NewJSONHandler can be used to create a JSONHandler, which\nprints the log records as JSON objects:\n\nThis prints:\n\nChanging log levels\n\nYou've already seen that the default logger only prints log messages of level Info and up.\nWe'll need to define a custom log handler to change the default log level. Here's an example\nthat enables printing Debug messages:\n\nIt'll print:\n\nFirst, we create an instance of slog.LevelVar with the new allocator. Next, we create a\nTextHandler instance and the programLevel to the slog.HandlerOptions struct pointer.\nThen we create a new Logger instance as before and set that as the default logger. In the\nlast step, the programLevel is updated so that it signals the handler to allow emitting\nDebug messages.\n\nDefining custom log levels\n\nApart from Debug, Info, Warn, and Error, you can define your own custom log levels.\nHere's an example of doing that with the default Logger instance:\n\nThis will return:\n\nObserve that you'll have to use Logger.Log() to pass your custom log level. Another\nexample with a custom log handler:\n\nThis prints:\n\nAdding or removing log attributes\n\nLog attributes are just key-value pairs. The following example appends a new key and a value\nto the log message:\n\nTo remove attributes from log records, you'll need to configure your custom handler and\ncreate a logger instance from that:\n\nRunning this will print the following. The time key no longer exists on the second log\nrecord:\n\nThe main focus here is the ReplaceAttr function which is used to transform or remove\nattributes before they are processed by a handler. It accepts two arguments: a slice of\ngroup names and an Attr struct. The group name allows attributes to be qualified into\ndifferent scopes, which we won't use right now. The Attr contains the Key and Value of\nthe attribute that's being logged.\n\nIn this case, ReplaceAttr checks if the attribute key is time and if so, returns an\nempty Attr struct, effectively signaling the handler not to include that attribute. If the\nkey is not time, it returns the original Attr unchanged.\n\nAdding sticky attributes\n\nSometimes you want to have a few common attributes that should persist across multiple log\ncalls. This can be done via Logger.With() method:\n\nIt prints:\n\nThe Logger.With() method accepts key-value pairs of attributes. This saves you from\npassing the same attributes over and over again to make them persist across multiple log\ncalls.\n\nGrouping log attributes\n\nYou can group the log attributes for better organization. Adding a group makes the attribute\nkeys of a log record qualified by the group name. What _qualify_ means here can vary\ndepending on whether you're using a TextHandler or a JSONHandler. Here's an example that\ndemonstrates both:\n\nThis prints:\n\nHere, in the case of the text logger, the log attribute key is qualified by the group name\nas group_a.key_a. On the other hand, the JSON logger emits the log record in a way where\nthe group name group_a is used as the key of a nested object containing the\n{\"key_a\": \"value_a\"} log attributes.\n\nMaking log groups sticky\n\nAkin to attributes, you can also make attribute group sticky with the Logger.WithGroup()\nmethod:\n\nThis returns:\n\nDirecting logs to different sinks\n\nThe predefined TextHandler and JSONHandler takes in a type that implements the\nio.Writer interface as the first argument. We can leverage this aspect to change the\ndestination of a structured logger. The following example shows how you can direct the\nstructured log stream to both stdout and a file:\n\nThe TeeWriter struct associates stdout and a file handle. It implements a custom Write\nmethod to write to both streams, enabling _teeing_ of output. In main(), a TeeWriter\ninstance is created with stdout and a file. A pointer to TeeWriter is then passed to the\nTextHandler. Next, the TextHandler is used to create a new Logger, so when the\nLogger logs, the messages go through the TextHandler's TeeWriter and are written to\nboth the console and a file via the custom Write method.\n\nLeveraging Attrs and Values for performance\n\nWhen using a logger, you can pass in key-value pairs called Attrs instead of separate keys\nand values. For example:\n\nThis is the same as:\n\nThere are helper functions like Int(), String(), and Bool() to create Attrs for common\ntypes. You can also use Any() to make an Attr for any type.\n\nThe real benefit is that Attrs are more efficient than separate keys and values. So for max\nspeed, we can use the LogAttrs() instead of Log().\n\nFor example:\n\nThis avoids extra allocations while giving the same result as:\n\n\n\n\n\n[official documentation]:\n    https://pkg.go.dev/log/slog",
  "title": "Go structured logging with slog"
}