DEV Community

Kittipat.po
Kittipat.po

Posted on

Structured Logging in Go: Context-Aware, JSON-Ready, and Production-Proven

Every developer has been there:

You’re on-call, an alert fires at 2 AM, and you’re staring at a wall of logs that look like this:

Blog Image

The Evolution of Logging 🧭

Logging in software has evolved a lot — but not every project has kept up:

1. Print Statements (the Stone Age): Just fmt.Println(“something happened”). Quick and dirty, but useless at scale.
2. Plain Logs with Timestamps (basic survival): Go’s log.Println gives you a timestamp, but everything else is left to you. Debugging across services? Forget it.
3. Leveled Logging (finding your voice): Tools like INFO, WARN, and ERROR levels made logs less noisy. You could finally filter out debug spam in production. But still no structure.
4. Structured Logging (the turning point): Instead of dumping plain text, logs became key–value pairs in JSON. Now you can query logs like data:
{ “severity”: “error”, “user_id”: 12345, “message”: “login failed” }
5. Context-Aware & Observability-Ready (where we are now): Modern systems aren’t just about logs — they’re about traces, spans, request IDs, and correlating everything in your observability stack. If your logger doesn’t integrate with context.Context, you’re already missing half the picture.

The Problem With Go’s Default Logging

Go’s built-in log package is fine for “hello world” projects, but once you’re in microservices land:

  • It doesn’t support structured fields.
  • It doesn’t know about context.Context.
  • It won’t give you trace IDs, caller info, or stack traces.

That gap inspired me to build something better. 🚀

Introducing the Logger Package 🎉

That’s where my Logger package comes in.

It’s a structured, context-aware logging solution for Go, built on top of Logrus, designed for production environments.

Think of it as logging upgraded for the cloud-native era:

  • JSON logs out of the box.
  • Automatic context propagation (trace_id, span_id).
  • Caller + stack trace included when you need it.
  • Customizable formatters and outputs.
  • Clean API that feels natural in Go.

How It Works ⚙️

The Logger package is designed to feel simple but powerful. Here’s how you can bring it into your project.

1. Install

go get github.com/kittipat1413/go-common/framework/logger
Enter fullscreen mode Exit fullscreen mode

2. Create a Logger
You can start with the default production-ready logger:

log := logger.NewDefaultLogger()
log.Info(context.Background(), "Application started", nil)
Enter fullscreen mode Exit fullscreen mode

Or configure it yourself using the Config struct:

logConfig := logger.Config{
    Level: logger.INFO,
    Formatter: &logger.StructuredJSONFormatter{
        TimestampFormat: time.RFC3339,
        // PrettyPrint
        // - true  = pretty-printed JSON (indented, easier for humans to read)
        // - false = compact JSON (single line, no indentation)
        PrettyPrint:     false,
    },
    Environment: "production",
    ServiceName: "order-service",
}

log, _ := logger.NewLogger(logConfig)
Enter fullscreen mode Exit fullscreen mode

3. Structured JSON Formatter
By default, logs are emitted in structured JSON:

Structured JSON Log

4. Customize Keys and Metadata
Don’t like the default field names in your logs?

You can redefine field names using a FieldKeyFormatter:

formatter := &logger.StructuredJSONFormatter{
    TimestampFormat: time.RFC3339,
    FieldKeyFormatter: func(key string) string {
        switch key {
        case logger.DefaultEnvironmentKey:
            return "env"
        case logger.DefaultServiceNameKey:
            return "service"
        default:
            return key
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

Now your logs use env and service instead of the defaults.

5. Context-Aware Logging
Pass in a context.Context with tracing info, and the logger automatically enriches logs with trace_id and span_id.

ctx, span := tracer.Start(context.Background(), "checkout")
defer span.End()

log.Info(ctx, "Processing checkout request", logger.Fields{
    "user_id": 42,
})
Enter fullscreen mode Exit fullscreen mode

Log output:

Tracing Log

6. Error Logging with Stack Traces
When logging errors, the logger automatically includes stack traces for quick debugging:

err := errors.New("database connection failed")
log.Error(ctx, "Failed to query user", err, logger.Fields{
    "query": "SELECT * FROM users",
})
Enter fullscreen mode Exit fullscreen mode

Log output:

Error Log

Final Thoughts ✨

This logger has already helped me a lot in my own projects — from cleaning up noisy logs to making debugging at 2 AM way less painful. That’s why I decided to share it with the community.

If you give it a try and it makes your life easier too, that’s a win.

And if you have suggestions, ideas, or improvements, I’d love to hear them. Open an issue or drop a comment — feedback is always welcome.

👉 Explore the examples.
👉 Or check out the documentation on pkg.go.dev.

Top comments (0)