Stage 7 · Master
Observability
Structured Logs and Error Taxonomies
This lesson is closest to shipped code: libs/platform/logging already emits JSON slog records tagged with a service attribute, and libs/platform/errors already defines the wire envelope that future handlers will use for machine-readable failure classes.
Implemented Logging Baseline
Meridian already standardizes process-wide structured logging. libs/platform/logging.New builds a slog.Logger backed by slog.NewJSONHandler and appends a fixed service attribute to every record. gateway, identity-service, community-service, and billing-service all call this constructor in main.go. The result is one consistent JSON format across the whole workspace before any business handler exists.
Why the service Field Matters
In a microservice system, log lines rarely live in one terminal. Once the four Meridian binaries are aggregated into a shared sink, the service attribute becomes the first partitioning key for triage. Without it, an error from billing-service and a startup message from gateway would be indistinguishable except by message text. With it, cross-service queries become straightforward: filter on service=billing-service, then refine by route, tenant_id, code, or trace_id once those fields are added at handler boundaries.
| Logging style | What exists in the line | Operational result |
|---|---|---|
| Message-only logging | Free-form prose and maybe one ID | Difficult to query once multiple services interleave |
| Structured JSON with service tag | Stable fields plus human-readable message | Chosen. Works for terminals, aggregators, and alert pipelines alike. |
API Error Envelope
The error side of the Platform Contract is also already implemented. libs/platform/errors.APIError defines the public failure shape as { code, message, trace_id, details? }. The code field is the machine-readable classification slot; message is human-readable; trace_id is reserved for cross-request correlation once tracing exists; details is for structured context such as field-level validation failures. No handler constructs APIError yet because the repo still exposes only /healthz, but the type is real and compiling today.
Planned Error Taxonomy
The next implementation step is not a giant hierarchy of custom error structs. It is a small shared code catalog that maps predictable failure classes into APIError.Code and into log severity at the HTTP boundary. Candidate codes include invalid_argument, unauthenticated, forbidden, not_found, conflict, dependency_unavailable, and internal. The list should stay small enough that dashboards and alert routing can use it directly. If a code does not influence handling, it does not belong in the taxonomy.
Structured logging is implemented now; taxonomy enforcement is not. The existing package gives the wire shape, but the route-to-status mapping and the stable code catalog still belong to the phase where real handlers are introduced.
apps/gateway/internal/middleware/request_logger.gocreateResponsibility: Emit one boundary log per request with route, duration, status, service, and future trace_id fields.
Why now: The boundary has the full context needed for one coherent log decision.
Connects to: Builds on the shared slog logger created in main.go.
apps/identity-service/internal/http/errors.gocreateResponsibility: Translate application errors into APIError values and HTTP status codes.
Why now: The service boundary should attach stable codes once instead of logging the same failure in every inner layer.
Connects to: Returns the libs/platform/errors.APIError shape and the libs/platform/response envelope contract.
The Decision
Meridian standardizes observability in two layers: structured JSON logs are already implemented through libs/platform/logging, and machine-readable failure codes already have a concrete wire envelope through libs/platform/errors. The implementation phase should build on that base with a deliberately small error taxonomy applied once at service boundaries.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.