Go (golang) Slog Package
Overview
In Go (golang) release 1.21, the slog package will be added to the standard library. It includes many useful features such as structured logging as well as level logging. In this article, we will talk about the history of logging in Go, the challenges faced, and how the new slog
package will help address those challenges.
Target Audience
This article is aimed at developers that have minimum experience with Go.
In this article, we'll cover the following topics:
- Introduce the Slog package that is released in Go 1.21
- Cover the major features being introduced with this logging package
- See variations of how to use the logger in the
slog
package
History of Logging in Go
When Go was first released in 2012 to the public, it shipped with the very simple log package. While it was useful in a limited fashion, it lacked most of what was needed for enterprise applications.
I think many of us in the Go community expected a better logger to appear, and some did, but as of 2017, the standard library had still not included any more robust options for logging. As a result, many community members got together with the idea of solving this problem. As you can see by the document that was started and shared, the effort failed. Mostly, it simply failed because of a lack of agreement on fundamentally what a logger should even do. Should it do level logging? Should it be structured?
As a result, many third party packages evolved over time that addressed those issues in various capacities, but the standard library still only had the basic functionality included.
Thankfully, the wait is over, and Go has officially shipped the slog package!
The Basics
Unlike the basic log
package, there are no Print
, Printf
, or Println
functions. This is due to the fact that all messages need to be of a specific type, or level
. The current levels are Info
, Warn
, Error
, and Debug
. Each of them have a corresponding function in the library and can be called as follows:
package main
import "log/slog"
func main() {
slog.Info("hello gophers")
slog.Warn("be warned!")
slog.Error("this is broken")
slog.Debug("show some debugging output")
}
$ go run .
2023/09/06 10:02:10 INFO hello gophers
2023/09/06 10:02:10 WARN be warned!
2023/09/06 10:02:10 ERROR this is broken
--------------------------------------------------------------------------------
Go Version: go1.21.0
Notice that the Debug
statement did not print? That is because the default level of the logger is Info
. We will see later in this article how to enable the DEBUG
statements to be logged to the output.
Creating a Logger
While you can use the basic logging methods directly, it's more likely you'll want to have more control over how your logger works. As such, you will need to create a logger to work with.
Here we create a logger, and we tell it that we want to use JSON
as our output:
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello gophers")
logger.Warn("be warned!")
logger.Error("this is broken")
}
$ go run .
{"time":"2023-09-06T10:02:10.898219-05:00","level":"INFO","msg":"hello gophers"}
{"time":"2023-09-06T10:02:10.898343-05:00","level":"WARN","msg":"be warned!"}
{"time":"2023-09-06T10:02:10.898345-05:00","level":"ERROR","msg":"this is broken"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Default Logger
By default, tThe slog
package uses a default logger. You may want to actually create your own logger with specific options to use, and then set the default logger to use those settings everywhere.
The first step is to create your logger, then set it to the default logger. This will allow all other packages in your program to use these log settings.
package main
import (
"log"
"log/slog"
"os"
"training/store"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello gophers")
logger.Warn("be warned!")
logger.Error("this is broken")
// Set the logger for the application
slog.SetDefault(logger)
s := store.New()
// we can now use the standard logger again as it uses the options we set above
slog.Info("new store created", "store-id", s.ID)
// the standard logger now uses the new logger as well
log.Println("I'm log.println, look at me!")
}
Now that we have set the default logger, we can use the standard slog
package to log from any package that we create and import, and it will use the settings we just applied. Note that it also updates the standard logger from the log
package, which means that all future log.Print
, log.Println
, and log.Printf
statements use the new logger that was applied.
package store
import (
"log/slog"
"math/rand"
)
type Store struct {
ID int
}
func New() *Store {
s := &Store{
ID: rand.Intn(100),
}
slog.Info("creating store", "store-id", s.ID)
return s
}
$ go run .
{"time":"2023-09-06T10:02:10.635254-05:00","level":"INFO","msg":"hello gophers"}
{"time":"2023-09-06T10:02:10.635564-05:00","level":"WARN","msg":"be warned!"}
{"time":"2023-09-06T10:02:10.635567-05:00","level":"ERROR","msg":"this is broken"}
{"time":"2023-09-06T10:02:10.635591-05:00","level":"INFO","msg":"creating store","store-id":74}
{"time":"2023-09-06T10:02:10.63561-05:00","level":"INFO","msg":"new store created","store-id":74}
{"time":"2023-09-06T10:02:10.635634-05:00","level":"INFO","msg":"I'm log.println, look at me!"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Notice that that even though we are just using the slog.Info
function in the store
package, it still uses the logger we set from the main
function in the main
package. This feature allows us to set the logger once for our application, and any future calls from either the log
or slog
package will have properly formatted output.
Levels
As stated before, there are four levels of logging, Info
, Warn
, Error
, and Debug
. When creating your logger, you can set the level at which you want information to be logged.
To change the logging level, we can set it with the
package main
import (
"log/slog"
"os"
)
func main() {
// create a logging level variable
// the level is Info by default
var loggingLevel = new(slog.LevelVar)
// Pass the loggingLevel to the new logger being created so we can change it later at any time
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: loggingLevel}))
// set the global logger
slog.SetDefault(logger)
// set the level to debug
loggingLevel.Set(slog.LevelDebug)
logger.Info("hello gophers")
logger.Warn("be warned!")
logger.Error("this is broken")
logger.Debug("be warned!")
}
$ go run .
{"time":"2023-09-06T10:02:10.20828-05:00","level":"INFO","msg":"hello gophers"}
{"time":"2023-09-06T10:02:10.208381-05:00","level":"WARN","msg":"be warned!"}
{"time":"2023-09-06T10:02:10.208383-05:00","level":"ERROR","msg":"this is broken"}
{"time":"2023-09-06T10:02:10.208384-05:00","level":"DEBUG","msg":"be warned!"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
It is important to note that depending on the level, some messages may not be logged out. For instance, Debug
messages are ONLY logged out if the logger level is set to slog.LevelDebug
.
Grouping Values
The logger also allows you to group
information for logging. For example, for an http request, you may want to log information specific to that request all in the same group.
Here is an example of using the Group
feature of the logger with a JSON
handler:
package main
import (
"log"
"log/slog"
"net/http"
"os"
)
func main() {
// create, configure, and set the global logger.
var loggingLevel = new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: loggingLevel}))
slog.SetDefault(logger)
loggingLevel.Set(slog.LevelDebug)
log.Println("program started")
r, err := http.Get("https://www.gopherguides.com")
if err != nil {
slog.Error("error retrieving site", "err", err)
}
slog.Info("success", slog.Group("request", "method", r.Request.Method, "url", r.Request.URL.String()))
}
$ go run .
{"time":"2023-09-06T10:02:11.221197-05:00","level":"INFO","msg":"program started"}
{"time":"2023-09-06T10:02:11.541371-05:00","level":"INFO","msg":"success","request":{"method":"GET","url":"https://www.gopherguides.com"}}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Notice that the output for request
is now a new message containing the method
and the url
values.
Improving Log Performance
Logging can be an expensive operation in your application. Much of that expense is due to the allocations created to hold the values before logging the output. That expense can be avoided however, if you use the convenience methods for setting key/value pairs when logging. These methods are slog.Int
, slog.String,
slog.Booland
slog.Any`.
package main
import (
"log"
"log/slog"
"net/http"
"os"
)
func main() {
// create, configure, and set the global logger.
var loggingLevel = new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: loggingLevel}))
slog.SetDefault(logger)
loggingLevel.Set(slog.LevelDebug)
log.Println("program started")
r, err := http.Get("https://www.gopherguides.com")
if err != nil {
slog.Error("error retrieving site", slog.String("err", err.Error()))
}
slog.Info("success",
slog.Group(
"request",
slog.String("method", r.Request.Method),
slog.String("url", r.Request.URL.String()),
))
}
$ go run .
{"time":"2023-09-06T10:02:11.104102-05:00","level":"INFO","msg":"program started"}
{"time":"2023-09-06T10:02:11.549409-05:00","level":"INFO","msg":"success","request":{"method":"GET","url":"https://www.gopherguides.com"}}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Notice that the output didn't change, but using the Attr
helpers will avoid allocations when logging which will improve the performance of your application.
Customizing Log Behavior
There may be times that you want to control how values are output by the logger. One of those times might be if you want to sanitize a value, perhaps a password. You can do this by implementing the slog.LogValuer
interface.
Let's look at the following program. Currently it will output all the values for the logged data struct, even the sensitive ones such as password.
package main
import (
"log"
"log/slog"
"os"
)
type User struct {
ID int
Username string
Password string
}
func main() {
// create, configure, and set the global logger.
var loggingLevel = new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: loggingLevel}))
slog.SetDefault(logger)
loggingLevel.Set(slog.LevelDebug)
log.Println("program started")
u := User{ID: 10, Username: "admin", Password: "abc123"}
log.Println(u)
}
$ go run .
{"time":"2023-09-06T10:02:10.303863-05:00","level":"INFO","msg":"program started"}
{"time":"2023-09-06T10:02:10.304024-05:00","level":"INFO","msg":"{10 admin abc123}"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Currently, sensitive data can be leaked out to the log. If we implement the slog.LogValuer
interface, we can prevent our sensitive data from leaking out to the logs.
package main
import (
"log"
"log/slog"
"os"
)
type User struct {
ID int
Username string
Password string
}
func (u User) LogValue() slog.Value {
return slog.GroupValue(
slog.Int("ID", u.ID),
slog.String("username", u.Username),
slog.String("password", "TOKEN_REDACTED"),
)
}
func main() {
// create, configure, and set the global logger.
var loggingLevel = new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: loggingLevel}))
slog.SetDefault(logger)
loggingLevel.Set(slog.LevelDebug)
log.Println("program started")
u := User{ID: 10, Username: "admin", Password: "abc123"}
// the log package does NOT detect the `LogValuer` interface
log.Println(u)
// You must use the slog package to detect the `Log.Valuer`
slog.Info("user", "user", u)
}
$ go run .
{"time":"2023-09-06T10:02:10.534894-05:00","level":"INFO","msg":"program started"}
{"time":"2023-09-06T10:02:10.535191-05:00","level":"INFO","msg":"{10 admin abc123}"}
{"time":"2023-09-06T10:02:10.535247-05:00","level":"INFO","msg":"user","user":{"ID":10,"username":"admin","password":"TOKEN_REDACTED"}}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Note that while the standard log
package will work well in most cases with the new slog
package, it does NOT detect the slog.LogValuer
interface and therefor should be avoided if you need that functinality.
Adding Source File Information
Many times, the hardest part of finding a bug is determining which file and which line the log message originated from. In the slog
package, this is simplified by setting the AddSource
option when creating the handler options.
package main
import (
"log/slog"
"os"
"training/store"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))
// Set the logger for the application
slog.SetDefault(logger)
slog.Info("hello gophers")
slog.Warn("be warned!")
slog.Error("this is broken")
_ = store.New()
}
$ go run .
{"time":"2023-09-06T10:02:11.007066-05:00","level":"INFO","source":{"function":"main.main","file":"./main.go","line":14},"msg":"hello gophers"}
{"time":"2023-09-06T10:02:11.007223-05:00","level":"WARN","source":{"function":"main.main","file":"./main.go","line":15},"msg":"be warned!"}
{"time":"2023-09-06T10:02:11.007227-05:00","level":"ERROR","source":{"function":"main.main","file":"./main.go","line":16},"msg":"this is broken"}
{"time":"2023-09-06T10:02:11.007231-05:00","level":"INFO","source":{"function":"training/store.New","file":"./store/store.go","line":8},"msg":"starting store"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Notice that you get the fully qualified path. This is likely not what you want, so we can also use the ReplaceAttr
option as well to create the desired output:
package main
import (
"log/slog"
"os"
"path/filepath"
"training/store"
)
func main() {
replacer := func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
source.File = filepath.Base(source.File)
}
return a
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replacer}))
// Set the logger for the application
slog.SetDefault(logger)
slog.Info("hello gophers")
slog.Warn("be warned!")
slog.Error("this is broken")
_ = store.New()
}
$ go run .
{"time":"2023-09-06T10:02:10.720634-05:00","level":"INFO","source":{"function":"main.main","file":"main.go","line":24},"msg":"hello gophers"}
{"time":"2023-09-06T10:02:10.720926-05:00","level":"WARN","source":{"function":"main.main","file":"main.go","line":25},"msg":"be warned!"}
{"time":"2023-09-06T10:02:10.720936-05:00","level":"ERROR","source":{"function":"main.main","file":"main.go","line":26},"msg":"this is broken"}
{"time":"2023-09-06T10:02:10.720942-05:00","level":"INFO","source":{"function":"training/store.New","file":"store.go","line":8},"msg":"starting store"}
--------------------------------------------------------------------------------
Go Version: go1.21.0
Summary
The slog
package has added many great convinience features to the standard library that bring it up to date with most modern logging philosphies. Making use of these features will allow you to create enterprise applications in the future.
More Articles

The Slices Package
Overview
In release 1.21, the slices package will be officially added to the standard library. It includes many useful functions for sorting, managing, and searching slices. In this article, we will cover the more commonly used functions included in the Slices package.

Masters Style Go Courses
Overview
After training for over four years, and recently enrolling in my own Masters degree program, I've married both of my passions and I'm excited to announce our new Masters Style Go Courses!

Table Driven Testing In Parallel
Overview
Table driven testing is not a unique concept to the Go programming language. However, there are some great features that make table driven testing in Go faster and more reusable. This article will cover how to get the most out of table driven testing with Go (golang).