Log Primer
Log Primer
Section titled “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))Start here when
Section titled “Start here when”- you are accepting or passing a
StructuredLoggeracross 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
Do not start here when
Section titled “Do not start here when”- 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”log/module.yamllog/logger.golog/fields.golog/glog.goreference/standard-service/internal/app/app.go
Concrete ownership examples
Section titled “Concrete ownership examples”Keep it in log when the work is about | Move out when the work becomes |
|---|---|
StructuredLogger interface — the contract all loggers satisfy | feature-specific logger adapters or app-domain logging semantics |
NewLogger() — canonical base construction returning StructuredLogger | CLI helpers, package-global singleton, or default logger registration |
| Field helpers: generic structured fields that any logger can accept | repo-wide test helpers or feature-specific field vocabularies |
| Implementing a new backend (e.g. JSON, noop) satisfying the interface | export-pipeline wiring, aggregation, or external shipping adapters |
Import
Section titled “Import”import "github.com/spcent/plumego/log"Construct a logger
Section titled “Construct a logger”// Text output to stderr — default for developmentlogger := log.NewLogger()
// JSON output — productionlogger := log.NewLogger(log.LoggerConfig{ Format: log.LoggerFormatJSON, Level: log.INFO,})
// Discard all output — useful in testslogger := log.NewLogger(log.LoggerConfig{Format: log.LoggerFormatDiscard})LoggerFormat options: LoggerFormatText, LoggerFormatJSON, LoggerFormatDiscard.
Log a message
Section titled “Log a message”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")Context-aware logging
Section titled “Context-aware logging”logger.InfoCtx(ctx, "request processed", log.Fields{ "user_id": userID, "duration_ms": duration.Milliseconds(),})Attach persistent fields
Section titled “Attach persistent fields”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")Inject as a dependency
Section titled “Inject as a dependency”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())Level constants
Section titled “Level constants”| Constant | Description |
|---|---|
log.DEBUG | Verbose — for development |
log.INFO | Normal operational messages |
log.WARNING | Non-fatal issues |
log.ERROR | Errors requiring attention |
log.FATAL | Calls os.Exit(1) after logging |
Why this primer exists
Section titled “Why this primer exists”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.