DEV Community

Cover image for Go's slog: Modern Structured Logging Made Easy
Leapcell
Leapcell

Posted on

Go's slog: Modern Structured Logging Made Easy

Cover

Preface

Go version 1.21.0 introduced a new package, log/slog, which provides structured logging functionality. Compared to traditional logging, structured logging is more popular because it offers better readability and significant advantages in processing, analysis, and searching.

The slog Package

The slog package provides structured logs, where each log entry contains a message, severity level, and various other attributes, all represented as key-value pairs.

The main features of the slog package are as follows:

  • Structured logging
  • Log severity levels
  • Custom log handlers
  • Log grouping

First Experience

package main

import (
    "context"
    "log/slog"
)

func main() {
    slog.Info("slog msg", "greeting", "hello slog")
    // Carrying context
    slog.InfoContext(context.Background(), "slog msg with context", "greeting", "hello slog")
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we directly output an info-level log by calling the package function slog.Info. Internally, this function uses a default Logger instance to perform the logging operation. In addition, you can use slog.InfoContext to output logs with an associated context.

Besides Info() and InfoContext(), there are also functions like Debug(), Warn(), and Error() for logging at different levels.

Running the above program will produce the following output:

2025/06/18 21:08:08 INFO slog msg greeting="hello slog"
2025/06/18 21:08:08 INFO slog msg with context greeting="hello slog"
Enter fullscreen mode Exit fullscreen mode

Creating a Logger

By default, when using slog package functions to output logs, the format is just plain text. If you want to output in JSON or key=value format, you need to create a Logger instance using slog.New(). When using this function, you must pass in an implementation of slog.Handler. The slog package provides two implementations: TextHandler and JsonHandler.

TextHandler

TextHandler is a log handler that writes log records as a series of key-value pairs to an io.Writer. Each key-value pair is represented in the form key=value, separated by spaces.

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    textLogger.InfoContext(context.Background(), "TextHandler", "Name", "Leapcell")
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we create a log handler using slog.NewTextHandler. The first parameter, os.Stdout, indicates that logs will be output to the console. The handler is then passed as a parameter to slog.New to create a Logger instance, which is used to perform logging operations.

The output of the program is as follows:

time=2025-06-18T21:09:03.912+00:00 level=INFO msg=TextHandler Name=Leapcell
Enter fullscreen mode Exit fullscreen mode

JsonHandler

JsonHandler is a log handler that writes log records in JSON format to an io.Writer.

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell")
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we use slog.NewJsonHandler to create a JSON log handler. The first parameter, os.Stdout, indicates output to the console. The handler is passed to slog.New to create a Logger instance, which is then used for logging operations.

The program output is as follows:

{
  "time": "2025-06-18T21:09:22.614686104+00:00",
  "level": "INFO",
  "msg": "JsonHandler",
  "name": "Leapcell"
}
Enter fullscreen mode Exit fullscreen mode

Global Logger Instance

slog has a default Logger instance. If you want to obtain the default Logger, you can refer to the following code:

logger := slog.Default()
Enter fullscreen mode Exit fullscreen mode

In previous examples, we always used a specifically created Logger instance to output logs. However, if you don’t want to log through a specific Logger instance every time but instead want to operate globally, you can use the slog.SetDefault function to set and replace the default Logger instance. This makes logging more convenient and flexible.

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(jsonLogger)
    slog.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell") //{"time":"2025-06-18T21:11:22.41760604+00:00","level":"INFO","msg":"JsonHandler","name":"Leapcell"}
}
Enter fullscreen mode Exit fullscreen mode

Grouping

Grouping refers to grouping related attributes (key-value pairs) in a log record. Here’s an example:

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).WithGroup("information")
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890))

    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)).WithGroup("information")
    textLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890))
}
Enter fullscreen mode Exit fullscreen mode

The result of running this program is as follows:

{"time":"2025-06-18T21:12:23.124255258+00:00","level":"INFO","msg":"json-log","information":{"name":"Leapcell","phone":1234567890}}
time=2025-06-18T21:12:23.127+00:00 level=INFO msg=json-log information.name=Leapcell information.phone=1234567890
Enter fullscreen mode Exit fullscreen mode

According to the output, if you group a Logger instance with a JsonHandler, the group name becomes a key, and the value is a JSON object composed of all key-value pairs.

If you group a Logger with a TextHandler, the group name is combined with the keys of all key-value pairs, and ultimately displayed as groupName.key=value.

Efficient Logging with LogAttrs

If you need to log frequently, compared to the previous examples, using the slog.LogAttrs function together with the slog.Attr type is more efficient, because it reduces the process of type parsing.

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.LogAttrs(context.Background(), slog.LevelInfo, "Efficient log output", slog.String("Name", "Leapcell"), slog.Int("Contact", 12345678901))
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we use the LogAttrs method to output a log entry. The method’s signature is:
func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)

Based on the signature, the first parameter is a context.Context, the second parameter is a Level (the log severity level defined in the slog package), and the third parameter is an Attr key-value pair.

When using other methods like Info to output logs, the key-value pairs are internally converted to the Attr type. By using the LogAttrs method, you can directly specify the Attr type, reducing the conversion process, and thus making logging more efficient.

With: Setting Common Attributes

If every log needs to contain the same key-value pair, you can consider setting a common attribute.

package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger := jsonLogger.With("systemID", "s1")
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k1", "v1"))
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k2", "v2"))
}
Enter fullscreen mode Exit fullscreen mode

You can use the With method to add one or more fixed attributes and return a new Logger instance. Any logs output by this new instance will include the added fixed attributes, thus avoiding the need to add the same key-value pairs to every log statement.

The output of this program is as follows:

{"time":"2025-06-18T21:19:51.338328238+00:00","level":"INFO","msg":"json-log","systemID":"s1","k1":"v1"}
{"time":"2025-06-18T21:19:51.338604943+00:00","level":"INFO","msg":"json-log","systemID":"s1","k2":"v2"}
Enter fullscreen mode Exit fullscreen mode

HandlerOptions: Configuration Options for Log Handlers

Careful readers may have noticed that in previous examples, whether using NewJSONHandler or NewTextHandler, the second parameter was set to nil, which means the default configuration is used.

This parameter is of type *HandlerOptions. With it, you can configure whether to display the source code location of log statements, the minimum log output level, and how to rewrite key-value pair attributes.

package main

import (
    "context"
    "log/slog"
    "os"
    "time"
)

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        AddSource: true,
        Level: slog.LevelError,
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            if a.Key == slog.TimeKey {
                if t, ok := a.Value.Any().(time.Time); ok {
                    a.Value = slog.StringValue(t.Format(time.DateTime))
                }
            }
            return a
        },
    }))
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"))
    jsonLogger.ErrorContext(context.Background(), "json-log", slog.String("name", "Leapcell"))
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create a Logger instance with a JsonHandler. When creating the JsonHandler, the following configurations are specified via the HandlerOptions parameter:

  • Output the source code (Source information) of the log statement
  • Set the minimum log level to Error
  • Rewrite the format of the attribute with key "time" to "2006-01-02 15:04:05"

The output of this program is as follows:

{
  "time": "2025-06-18 21:21:31",
  "level": "ERROR",
  "source": { "function": "main.main", "file": "D:/goproject/src/gocode/play/main.go", "line": 24 },
  "msg": "json-log",
  "name": "Leapcell"
}
Enter fullscreen mode Exit fullscreen mode

The output matches expectations: logs of level INFO are not output, the Source information is included, and the value of the "time" key has been rewritten.

Customizing the Value in Key-Value Pairs

In a previous example, we used the HandlerOptions configuration to modify the value in a key-value pair. Besides this method, the slog package also supports another way to change the value.

package main

import (
    "context"
    "log/slog"
)

type Password string

func (Password) LogValue() slog.Value {
    return slog.StringValue("REDACTED_PASSWORD")
}

func main() {
    slog.LogAttrs(context.Background(), slog.LevelInfo, "Sensitive Data", slog.Any("password", Password("1234567890")))
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we implement the slog.LogValuer interface (by adding the LogValue() slog.Value method to a type), which allows us to override the value of a key-value pair. When logging, the value will be replaced by the return value of the LogValue method.

The output of this program is as follows:

2025/06/18 21:37:11 INFO Sensitive Data password=REDACTED_PASSWORD
Enter fullscreen mode Exit fullscreen mode

As expected, the value of password has been changed.

Summary

This article provides a detailed introduction to the slog package in Go, including basic usage, creating Logger instances, efficient logging, and customizing log information.

After reading this article, you should have a deeper understanding of the slog package and be able to use it more effectively to manage and record logs.


We are Leapcell, your top choice for hosting Go projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (1)

Collapse
 
dotallio profile image
Dotallio

Really love how slog's grouping and attr features tidy up log management, especially at scale. Are you already replacing logrus/zap in any production projects?