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:
- Shared library wrapper: Create an internal logging library that pre-populates standard fields (
service_name,environment,version) automatically. Developers calllog.info("event_name", ...)and the wrapper handles the boilerplate. This is the most effective approach. - 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).
- 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, andhttp.url— notmethod,status, andurl.
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.