Skip to content

Log Primer

Open this page after Stable Roots when the change still belongs to the default service path and the real question has narrowed to how the service writes structured log output: what interface a component accepts, how a base logger is constructed, and which field helpers apply.

log owns the StructuredLogger interface, canonical base logger construction with NewLogger, and logging field helpers. Only stdlib is allowed as an import — log has no knowledge of app bootstrap, routing, or feature behavior.

logger := log.NewLogger()
logger.Info("server starting", log.Field("port", 8080))
logger.Error("db ping failed", log.Field("err", err))
  • you are accepting or passing a StructuredLogger across a module boundary
  • you are calling NewLogger() to construct the base logger for an application
  • you are adding a structured field to a log event using the field helper API
  • you are implementing a new logging backend that satisfies StructuredLogger
  • the change is about export pipelines, aggregation, or shipping logs to an external system — that belongs in x/observability
  • the change adds CLI bootstrap helpers, default singleton construction, or package-global state
  • the work is about request-level access logging, field redaction policy, or log sampling — those are middleware concerns
  • the change is feature-specific logging semantics or repo-wide testing helpers

First files to read in the current repository

Section titled “First files to read in the current repository”
  1. log/module.yaml
  2. log/logger.go
  3. log/fields.go
  4. log/glog.go
  5. reference/standard-service/internal/app/app.go
Keep it in log when the work is aboutMove out when the work becomes
StructuredLogger interface — the contract all loggers satisfyfeature-specific logger adapters or app-domain logging semantics
NewLogger() — canonical base construction returning StructuredLoggerCLI helpers, package-global singleton, or default logger registration
Field helpers: generic structured fields that any logger can acceptrepo-wide test helpers or feature-specific field vocabularies
Implementing a new backend (e.g. JSON, noop) satisfying the interfaceexport-pipeline wiring, aggregation, or external shipping adapters
import "github.com/spcent/plumego/log"
// Text output to stderr — default for development
logger := log.NewLogger()
// JSON output — production
logger := log.NewLogger(log.LoggerConfig{
Format: log.LoggerFormatJSON,
Level: log.INFO,
})
// Discard all output — useful in tests
logger := log.NewLogger(log.LoggerConfig{Format: log.LoggerFormatDiscard})

LoggerFormat options: LoggerFormatText, LoggerFormatJSON, LoggerFormatDiscard.

logger.Info("server started", log.Fields{"addr": ":8080"})
logger.Warn("config missing, using default", log.Fields{"key": "TIMEOUT"})
logger.Error("database unavailable", log.Fields{"err": err.Error()})

Fields are optional — omit them when there is nothing structured to attach:

logger.Info("shutdown complete")
logger.InfoCtx(ctx, "request processed", log.Fields{
"user_id": userID,
"duration_ms": duration.Milliseconds(),
})

Use WithFields to create a child logger that carries fields on every subsequent call:

reqLogger := logger.WithFields(log.Fields{
"request_id": contract.RequestIDFromContext(ctx),
"method": r.Method,
"path": r.URL.Path,
})
reqLogger.Info("handler started")
reqLogger.Error("handler failed", log.Fields{"err": err.Error()})

With is shorthand for a single field:

svcLogger := logger.With("service", "user-api")

Accept log.StructuredLogger at module boundaries — never a concrete type:

type UserRepository interface {
Find(ctx context.Context, id string) (*User, error)
}
type UserService struct {
repo UserRepository
logger log.StructuredLogger
}
func NewUserService(repo UserRepository, logger log.StructuredLogger) *UserService {
return &UserService{repo: repo, logger: logger}
}

In app.New, get the logger from the core app instance:

app := core.New(cfg.Core, core.AppDependencies{Logger: log.NewLogger()})
svc := NewUserService(db, app.Logger())
ConstantDescription
log.DEBUGVerbose — for development
log.INFONormal operational messages
log.WARNINGNon-fatal issues
log.ERRORErrors requiring attention
log.FATALCalls os.Exit(1) after logging

Logging is the one observability tool that nearly every component touches. If the interface leaks app-bootstrap assumptions or pulls in feature packages, the coupling spreads everywhere. log keeps its import list to stdlib specifically to allow any stable root or extension to accept a StructuredLogger without creating cycles. Never log secrets — this is the single hardest rule to audit after the fact.