Skip to Content
Course content

68: Structured Logging in Go

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

I've spent far too many late nights staring at a terminal, running a complex grep command against a 10GB log file, trying to piece together why a specific user's payment failed. In those moments, you realize that the way you write your logs isn't just a detail—it's the difference between finding a bug in five minutes or spending five hours guessing.

The Grep Nightmare

When we first start out, the instinct is to use fmt.Printf or the basic log package to describe what's happening. It feels natural. You just write a sentence that describes the error. In a payment processing system, it usually looks something like this:

log.Printf("Payment failed for user %s, transaction %s: %v", userID, txID, err)

On the surface, this is perfectly readable for a human. But logs aren't just for humans; they're for machines. When your app is running on twenty different pods in a Kubernetes cluster, you aren't reading a text file; you're querying a system like Elasticsearch, Datadog, or CloudWatch. If you want to find every failed transaction for a specific txID, you have to write a regular expression to parse a string that might change the moment a teammate decides to rewrite "Payment failed" as "Transaction failed." It's fragile, slow, and honestly, a bit miserable.

Turning Logs into Data

This is why we move to structured logging. Since Go 1.21, we have the slog package in the standard library, which treats logs as a collection of key-value pairs rather than a single formatted string. Instead of building a sentence, you're building a record.

Here is how I would rewrite that same payment failure using slog:

import "log/slog"

slog.Error("payment failed", 
    "user_id", userID, 
    "tx_id", txID, 
    "error", err,
)

Now, the log isn't just a line of text; it's a structured object. If you configure slog to output JSON (which you should do in production), it looks like this:

{"time":"2023-10-27T10:00:00Z", "level":"ERROR", "msg":"payment failed", "user_id":"user_123", "tx_id":"tx_987", "error":"insufficient funds"}

This is a game-changer. I can now run a query in my logging dashboard for json.tx_id == "tx_987" and get an instant result. No regex, no guessing, and no fragility.

The Cost of Consistency

You might think, "Why bother with all those extra arguments? Just typing %s is faster." And you're right—the naive way is faster to write. Structured logging requires you to be intentional about your keys. You have to decide: is it userID, user_id, or uID? If half the team uses one and half uses the other, you're back to the Grep Nightmare.

The trade-off is a bit of extra verbosity in your code for a massive increase in observability. I usually recommend creating a "logger" that is pre-populated with context. If you're inside a function that handles a specific request, you can create a sub-logger that attaches the request_id to every single call automatically:

logger := slog.Default().With("request_id", reqID)
// Now every log call using 'logger' includes the request_id automatically
logger.Info("processing payment") 
logger.Error("database timeout", "retry_count", 3)

By shifting the focus from "telling a story" to "recording data," you stop guessing what happened and start knowing. It's a mental shift from writing a diary to maintaining a database of your application's behavior.




📋 Practical Task

Refactoring the Order Processing Pipeline to Structured Logs

You have been handed a legacy OrderProcessor struct that uses fmt.Printf and the standard log package to track orders. This is making it impossible for the SRE team to track order failures in production.

Your Goal: Refactor the provided code to use the log/slog package.

  • Replace all log.Printf and fmt.Printf calls with slog.Info or slog.Error.
  • Ensure that key identifiers (like OrderID, CustomerID, and Amount) are passed as structured key-value pairs, not embedded in the message string.
  • Implement a WithOrderContext method that returns a *slog.Logger pre-populated with the order_id, so that subsequent logs in the processing pipeline don't have to repeat the ID manually.
package main

import (
    "fmt"
    "log"
)

type Order struct {
    ID         string
    CustomerID string
    Amount     float64
}

func ProcessOrder(o Order) {
    // TODO: Replace this with structured logging
    log.Printf("Starting process for order %s (Customer: %s)", o.ID, o.CustomerID)
    
    if o.Amount <= 0 {
        // TODO: Replace this with structured logging
        fmt.Printf("Error: Order %s has invalid amount: %f\n", o.ID, o.Amount)
        return
    }
    
    log.Printf("Order %s processed successfully", o.ID)
}

func main() {
    order := Order{ID: "ORD-123", CustomerID: "CUST-456", Amount: 99.99}
    ProcessOrder(order)
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.