Logging plays a critical role in Kubernetes environments where distributed systems generate vast amounts of operational data. Traditional logging approaches using string formatting (like fmt.Sprintf) create logs that are difficult to parse, analyze, and integrate with modern observability stacks. With Go 1.22's standard library support for structured logging via the log/slog package, developers can now implement more effective logging strategies with minimal external dependencies.
This guide shows you how to replace string-based logging with structured JSON logging in Go 1.22 to create Kubernetes-native applications with improved observability.
Why String Formatting Fails in Kubernetes Environments
String-formatted logs have significant limitations in cloud-native environments:
- Hard to parse: Log aggregation tools struggle with inconsistent text formats
- Difficult to search: Finding specific information requires complex regex patterns
- Limited metadata: Important context often gets buried in text
- Poor integration: Modern observability systems prefer structured data
Take this traditional logging approach:
func processRequest(r *http.Request) {
userID := getUserID(r)
startTime := time.Now()
// Process the request...
duration := time.Since(startTime)
// String-formatted log with sprintf
log.Printf("Request processed for user %s with path %s taking %v with status %d",
userID, r.URL.Path, duration, http.StatusOK)
}
This approach creates logs that are difficult to analyze programmatically and requires complex regex patterns for extraction.
Structured JSON Logging with slog in Go 1.22
Go 1.22 introduces the log/slog package for structured logging in the standard library. This package provides a consistent way to log data as key-value pairs, making logs machine-readable while maintaining human readability.
Setting Up slog JSON Logging
First, configure a JSON logger:
package main
import (
"log/slog"
"os"
)
func main() {
// Create a JSON handler that writes to stdout
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
// Include source file and line in logs
AddSource: true,
// Set minimum log level
Level: slog.LevelInfo,
})
// Create a new logger with the JSON handler
logger := slog.New(handler)
// Set as default logger
slog.SetDefault(logger)
// Your application code...
}
Converting sprintf to Structured Logging
Let's transform our previous example to use structured logging:
func processRequest(r *http.Request) {
userID := getUserID(r)
startTime := time.Now()
// Process the request...
duration := time.Since(startTime)
// Structured JSON log with slog
slog.Info("Request processed",
"user_id", userID,
"path", r.URL.Path,
"duration_ms", duration.Milliseconds(),
"status", http.StatusOK)
}
This produces a structured JSON log like:
{
"time": "2025-05-06T10:45:12.928Z",
"level": "INFO",
"msg": "Request processed",
"user_id": "user-123",
"path": "/api/v1/resources",
"duration_ms": 213,
"status": 200
}
Benefits for Kubernetes Environments
Structured JSON logging provides significant advantages:
- Log aggregation: Seamless integration with systems like Elasticsearch, Loki, and Splunk
- Filtering and searching: Query logs by specific fields without complex regex
- Consistent metadata: Standard format for timestamps, log levels, and context
- Improved analytics: Better insights from application logs
- Reduced storage costs: More efficient compression of structured data
Adding Kubernetes Context
In Kubernetes environments, adding pod and container context enhances logs:
import (
"log/slog"
"os"
)
func setupLogger() *slog.Logger {
// Create JSON handler
handler := slog.NewJSONHandler(os.Stdout, nil)
// Create base logger with Kubernetes context
hostname, _ := os.Hostname()
logger := slog.New(handler).With(
"pod", hostname,
"namespace", os.Getenv("POD_NAMESPACE"),
"container", os.Getenv("CONTAINER_NAME"),
"service", "user-service", // Your service name
)
return logger
}
Advanced Techniques
Contextual Logging
Pass a logger through context for request-scoped logging:
func middlewareWithLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create request-specific logger with request ID
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
logger := slog.Default().With(
"request_id", requestID,
"method", r.Method,
"path", r.URL.Path,
"client_ip", r.RemoteAddr,
)
// Add logger to request context
ctx := context.WithValue(r.Context(), loggerKey, logger)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Get logger from context in handlers
func getLogger(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
Structured Error Logging
Log errors with additional context:
func processItem(id string) error {
// Processing logic...
if err := someOperation(); err != nil {
slog.Error("Failed to process item",
"item_id", id,
"error", err,
"retry_count", retryCount)
return fmt.Errorf("processing item %s: %w", id, err)
}
return nil
}
Grouping Related Attributes
Organize complex logs using groups:
slog.Info("User updated profile",
"user", slog.Group("",
"id", user.ID,
"email", user.Email,
),
"changes", slog.Group("",
"name", slog.Group("",
"old", oldName,
"new", newName,
),
"email", slog.Group("",
"old", oldEmail,
"new", newEmail,
),
),
)
This creates clear hierarchy in the JSON output:
{
"time": "2025-05-06T14:28:32.192Z",
"level": "INFO",
"msg": "User updated profile",
"user": {
"id": "user-456",
"email": "user@example.com"
},
"changes": {
"name": {
"old": "John Doe",
"new": "John Smith"
},
"email": {
"old": "john.doe@example.com",
"new": "john.smith@example.com"
}
}
}
Performance Considerations
The slog package is designed for high performance with minimal allocations. Key performance tips:
Avoid unnecessary string formatting: Let slog handle serialization
Use LogValuer for expensive operations:
type expensiveValue struct { data string } func (e expensiveValue) LogValue() slog.Value { return slog.StringValue(computeExpensiveString(e.data)) } // Log will only compute the expensive value if this level is enabled slog.Debug("Debug information", "value", expensiveValue{data: "input"})Reuse loggers with predefined attributes: Create loggers with common fields once
Integration with Kubernetes Logging Infrastructure
Kubernetes best practices for structured logging:
- Output to stdout/stderr: Kubernetes captures container output
- Use JSON format: Compatible with most log aggregators
- Include standard fields: Timestamp, log level, and message
- Add Kubernetes metadata: Pod, namespace, and container info
Example Kubernetes deployment with environment variables for logging context:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-service
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-go-service:1.0.0
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONTAINER_NAME
value: app
- name: LOG_LEVEL
value: info
Before and After Comparison
Before: String-Formatted Logging
func handleAPIRequest(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
userID := r.Header.Get("X-User-ID")
log.Printf("Starting API request for user %s on %s", userID, r.URL.Path)
// Process request...
result, err := processData(r)
if err != nil {
log.Printf("Error processing request for user %s: %v", userID, err)
http.Error(w, "Processing failed", http.StatusInternalServerError)
return
}
duration := time.Since(startTime)
log.Printf("Request completed for user %s in %v with %d results",
userID, duration, len(result))
// Send response...
}
After: Structured JSON Logging
func handleAPIRequest(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
userID := r.Header.Get("X-User-ID")
logger := slog.With(
"user_id", userID,
"path", r.URL.Path,
"method", r.Method,
)
logger.Info("Starting API request")
// Process request...
result, err := processData(r)
if err != nil {
logger.Error("Error processing request",
"error", err,
"duration_ms", time.Since(startTime).Milliseconds())
http.Error(w, "Processing failed", http.StatusInternalServerError)
return
}
logger.Info("Request completed",
"duration_ms", time.Since(startTime).Milliseconds(),
"result_count", len(result))
// Send response...
}
Conclusion
Replacing fmt.Sprintf with structured JSON logging using Go 1.22's slog package significantly improves application observability in Kubernetes environments. This approach creates logs that are machine-readable, searchable, and integrate seamlessly with modern cloud-native monitoring systems.
By adopting structured logging, you gain:
- Improved log searchability and analysis
- Better integration with Kubernetes ecosystem
- Consistent context across all log entries
- Enhanced debugging capabilities
As Kubernetes environments grow in complexity, proper observability becomes essential. Structured logging forms the foundation of effective monitoring and troubleshooting strategies, helping teams maintain reliable, production-grade services.