Skip to Content
Course content

158: Structured Logging with log/slog

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, you've probably been using the standard log package. It's great for "quick and dirty" output, but the moment you move your code to a production environment with thousands of lines of logs, fmt.Printf-style logging becomes a liability. If you want to find every failed payment for a specific user ID across ten different microservices, you can't just grep through a wall of unstructured text efficiently.

That's where log/slog comes in. It brings structured logging to the standard library, allowing us to treat logs as data (usually JSON) rather than just strings. Let's build a small payment processing logger to see how this actually changes your workflow.

Starting with basic attributes

I want to track when a payment is processed. Instead of building a long string, I'll use slog.Info and pass key-value pairs. This allows a log aggregator (like ELK or Datadog) to index these fields individually.

package main

import (
	"log/slog"
	"os"
)

func main() {
	orderID := "ORD-12345"
	amount := 99.99

	slog.Info("Processing payment", 
		"order_id", orderID, 
		"amount", amount,
	)
}

Notice how I'm not using fmt.Sprintf. I'm providing a message, and then a sequence of keys and values. By default, this prints as text, but the structure is already there.

The mistake: Falling back into old habits

It's easy to slip back into the "string formatting" mindset. I actually did this in a project last week, and it's exactly what you want to avoid. Look at this:

// DON'T DO THIS
slog.Info(fmt.Sprintf("Payment %s failed for user %d", orderID, userID))

I realized a few minutes later that I'd just defeated the entire purpose of slog. By putting the variables inside the message string, I've made the "message" unique for every single log entry. Now, my log aggregator can't group all "Payment failed" events together because every message is slightly different. I have to fix it by moving those variables into attributes:

// DO THIS instead
slog.Error("Payment failed", 
	"order_id", orderID, 
	"user_id", userID,
)

Switching to JSON for the machines

While the default text output is readable for us humans during local development, servers prefer JSON. It's much faster for machines to parse. To switch, we need to create a Handler and a new Logger instance.

func main() {
	// Create a JSON handler that writes to standard out
	handler := slog.NewJSONHandler(os.Stdout, nil)
	logger := slog.New(handler)

	// Set this as the global logger so we don't have to pass it around
	slog.SetDefault(logger)

	slog.Info("System boot complete", "version", "1.0.4")
}

Now, instead of INFO System boot complete version=1.0.4, you get a clean JSON object. This is the gold standard for modern cloud applications.

Reducing repetition with logger groups

If I'm writing a whole module for "Payment Processing," I don't want to manually add "module", "payments" to every single log call. I can create a "pre-configured" logger that carries those attributes automatically using With().

func processPayment(id string) {
	// Create a logger that always includes the module and the specific order ID
	paymentLog := slog.Default().With(
		"module", "payments",
		"order_id", id,
	)

	paymentLog.Info("Validating card")
	// ... some logic ...
	paymentLog.Info("Charging account")
}

By using With(), I've created a child logger. Every time I call paymentLog.Info, it automatically attaches the module and order ID to the output. It keeps the actual log calls clean and focused only on what's happening in that specific moment.




📋 Practical Task

Build a Structured User Session Tracker

Create a small program that simulates a user session. Your task is to implement the following:

  • Configure log/slog to output in JSON format to the console.
  • Create a function called trackEvent(userID string, eventName string, details map[string]any).
  • Inside that function, use a logger With() to attach the userID as a persistent attribute.
  • Log the eventName as the main message, and iterate through the details map to add them as structured attributes to the log.
  • Test it by simulating a "login" event with ip_address and browser details, and a "purchase" event with item_id and price details.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.