Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Logging

Logging

go.putnami.dev/logger provides structured logging with pluggable sinks, context-aware propagation, and output formats compatible with Google Cloud Logging.

Default logger

import "go.putnami.dev/logger"

log := logger.Default()
log.Info("server started")
log.Error("connection failed", err)

The default logger reads configuration from environment variables:

Env var Description
LOG_LEVEL Minimum log level: debug, info, warn, error (default: info)
K_SERVICE When set (Cloud Run), uses JSON sink automatically

Creating loggers

log := logger.New("my-service", logger.LevelInfo,
    logger.NewJSONSink(),
)

// With console output
log := logger.New("my-service", logger.LevelDebug,
    logger.NewConsoleSink(),
)

Log levels

type Level int

const (
    LevelDebug Level = iota
    LevelInfo
    LevelWarn
    LevelError
)

Parse from strings:

level := logger.ParseLevel("debug") // LevelDebug
level := logger.ParseLevel("info")  // LevelInfo
level := logger.ParseLevel("warn")  // LevelWarn
level := logger.ParseLevel("error") // LevelError

Logging methods

log.Debug("processing request")
log.Info("user created")
log.Warn("deprecated API called")
log.Error("query failed", err)

Structured context

Add persistent fields to a logger:

// Named child logger
requestLog := log.Named("http")
// Output: [my-service.http] ...

// Add key-value context
userLog := log.With("userId", "user-123", "requestId", "req-456")
userLog.Info("processing order")
// Output includes userId=user-123 requestId=req-456

Child loggers inherit the parent's sinks and level but add their own context fields.

Sinks

JSON sink

Structured JSON output compatible with Google Cloud Logging:

sink := logger.NewJSONSink()

Output format:

{"severity":"INFO","message":"server started","timestamp":"2024-01-15T10:30:00Z","logger":"my-service"}

Console sink

Human-readable text output for local development:

sink := logger.NewConsoleSink()

Output format:

2024-01-15 10:30:00 INFO  [my-service] server started

Buffer sink

Batches log entries and flushes periodically for better throughput:

inner := logger.NewJSONSink()
sink := logger.NewBufferSink(inner, 100, 5*time.Second)
// Flushes every 100 entries or every 5 seconds

Memory sink

In-memory sink for testing:

sink := logger.NewMemorySink()

log := logger.New("test", logger.LevelDebug, sink)
log.Info("test message")

// Inspect entries
fmt.Println(sink.Len())            // 1
fmt.Println(sink.Last().Message)   // "test message"

// All entries
for _, entry := range sink.Entries {
    fmt.Println(entry.Level, entry.Message)
}

// Clear
sink.Clear()

Context integration

Attach a logger to context.Context for propagation through the call chain:

// Attach to context
ctx := logger.WithLogger(ctx, log.With("requestId", requestID))

// Retrieve from context
func processRequest(ctx context.Context) {
    log := logger.FromContext(ctx)
    if log != nil {
        log.Info("processing")
    }

    // Or with fallback to default
    log = logger.FromContextOrDefault(ctx)
    log.Info("processing")
}

Patterns

Per-request logging

func requestLogger() fhttp.Middleware {
    return func(ctx *fhttp.Context, next func() *fhttp.Response) *fhttp.Response {
        log := logger.Default().With(
            "method", ctx.Method,
            "path", ctx.Path,
            "requestId", ctx.Header("X-Request-ID"),
        )

        // Attach to context for downstream use
        goCtx := logger.WithLogger(ctx.Context(), log)
        ctx = ctx.WithContext(goCtx)

        start := time.Now()
        resp := next()
        log.Info(fmt.Sprintf("completed in %dms", time.Since(start).Milliseconds()))
        return resp
    }
}

Service-level logging

type UserService struct {
    log  *logger.Logger
    repo *UserRepository
}

func NewUserService(repo *UserRepository) *UserService {
    return &UserService{
        log:  logger.Default().Named("UserService"),
        repo: repo,
    }
}

func (s *UserService) Create(ctx context.Context, input CreateInput) (*User, error) {
    s.log.Info("creating user")
    user, err := s.repo.Create(ctx, input)
    if err != nil {
        s.log.Error("failed to create user", err)
        return nil, err
    }
    s.log.Info("user created")
    return user, nil
}

Related guides

  • Telemetry — trace-correlated logging
  • Errors — structured error logging
  • HTTP & Middleware — request logging middleware

On this page

  • Logging
  • Default logger
  • Creating loggers
  • Log levels
  • Logging methods
  • Structured context
  • Sinks
  • JSON sink
  • Console sink
  • Buffer sink
  • Memory sink
  • Context integration
  • Patterns
  • Per-request logging
  • Service-level logging
  • Related guides