Free — no signup required

Schema Discipline: The "Semantic" Part

3 min read

Why JSON Alone Is Not Enough

Outputting JSON is a necessary first step, but it is not sufficient. Consider two microservices in the same platform:

  • Auth service logs: {"user_id": "u_123", "latency": 45}
  • Payment service logs: {"uid": "u_123", "response_time_ms": 45}

Both are JSON. Both contain the same conceptual data. But because the field names differ (user_id vs uid, latency vs response_time_ms), you cannot write a single query that joins or correlates them. You cannot build a dashboard that shows end-to-end latency across both services. You cannot alert on latency > 100 because one service calls it latency and the other calls it response_time_ms.

This is the "semantic" problem that gives semantic logging its name. The meaning of a field must be consistent across your entire system, not just within a single service.

Establishing a Field Schema

A field schema is a company-wide (or team-wide) agreement on field names, types, and units. It is typically documented in a shared wiki or enforced via a shared logging library wrapper.

Naming conventions:

Rule Bad Good
Use snake_case userId, UserID, user-id user_id
Include units in numeric field names duration, size, latency duration_ms, size_bytes, latency_ms
Use consistent entity names uid, user, account_id user_id (always)
Boolean fields use is_ prefix failed, cached is_failed, is_cached

Standard fields every log event should carry:

{
  "timestamp": "2024-01-15T10:23:45.123Z",
  "level": "error",
  "service_name": "payment-service",
  "version": "2.3.1",
  "environment": "production",
  "trace_id": "abc123def456",
  "event": "payment_failed",
  "user_id": "u_123",
  "amount_cents": 5000,
  "currency": "USD",
  "reason": "insufficient_funds"
}

The fields timestamp, level, service_name, trace_id, and event are universal — they appear on every log line from every service. The remaining fields are event-specific. This structure lets you query across all services (service_name = "payment-service") or correlate a single request across multiple services (trace_id = "abc123def456").

Enforcing the Schema

Schema discipline breaks down without enforcement. Three practical approaches:

  1. Shared library wrapper: Create an internal logging library that pre-populates standard fields (service_name, environment, version) automatically. Developers call log.info("event_name", ...) and the wrapper handles the boilerplate. This is the most effective approach.
  2. Linting in CI: Write a linter that scans log calls for banned patterns (string interpolation in the message, non-snake_case keys, numeric fields without units).
  3. OpenTelemetry Semantic Conventions: The OpenTelemetry project publishes a standardized set of field names for common domains (HTTP, databases, messaging). Adopting these conventions means your logs are compatible with the broader observability ecosystem out of the box. For example, HTTP logs should use http.method, http.status_code, and http.url — not method, status, and url.
Interview Tip

A common SRE interview question is: "How would you standardize logging across 50 microservices owned by different teams?" The answer has two parts: (1) a shared internal logging library that enforces standard fields automatically, so developers don't have to think about it; and (2) OpenTelemetry Semantic Conventions as the field naming standard, so you're not inventing your own schema from scratch. Mention that enforcement via a shared library is far more reliable than documentation alone.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free