Structured JSON logging turns an event into named values instead of a sentence that every downstream tool must interpret. The useful part is not the curly braces. It is the agreement that the same key means the same thing across producers, transformations, and queries. Without that agreement, JSON simply gives inconsistent data a more attractive shape.
Consider two services that both emit latency. One uses seconds and the other milliseconds. Both documents are valid JSON, yet their combined chart is misleading. This guide proposes a contract-first approach: define meanings, constrain shapes, preserve types, and test realistic failures before deciding which fields deserve an index.
Choose an event, not a snapshot of memory
An event should describe something that happened: a request completed, an authorization check failed, or a background job was retried. Serializing an entire object graph describes whatever happened to be in memory. That can pull in library internals and fields unrelated to the investigation. It also makes your log structure change when a dependency changes.
Write a short description for each event name. For example, job.completed might mean the worker finished its business operation, not merely that it acknowledged a queue message. Include the outcome and a safe job reference. Make retries separate attempts with their own identifiers. This separation allows someone to count successful jobs without accidentally counting every attempt as a distinct business success.
Keep a small shared envelope
Use a compact envelope for information shared across event types. The sample design on this site includes schema_version, event_id, timestamp, event_name, severity, and service. Event-specific measurements belong in a controlled attributes object. This is a house format for examples, not a claim of universal compatibility.
The envelope helps a generic viewer sort and filter events without knowing every producer. The attributes preserve useful differences between a model request and a firewall decision. Avoid flattening all possible attributes into mandatory top-level fields. A token counter does not belong in every web access record, and an absent token counter must not be interpreted as zero model usage. The event schema reference shows the full illustrative contract.
Validate presence as well as type
A field appearing in a schema's property definitions does not necessarily make it mandatory. The JSON Schema object reference explains the distinction between properties, required, and additionalProperties. It also distinguishes an absent property from a property whose value is null. These details matter when producers and consumers are maintained by different teams.
Choose which fields are truly required. Requiring an unavailable user identifier can encourage producers to invent a value or leak a private one. For optional measurements, define whether omission means unknown, not applicable, or not collected. Configure validation according to the schema dialect and validator you actually deploy. For example, explicitly test timestamp validation rather than assuming every implementation enforces a format annotation in the same way.
Preserve numeric and boolean values
A duration should remain a number when it is measured numerically. An outcome flag should remain a boolean when it has two meaningful states. Turning everything into text makes comparisons and aggregation depend on later coercion. A string such as "100" also sorts differently from the number 100 in systems that perform lexical ordering.
Do not use a boolean when the process genuinely has more than two states. A workflow may be pending, completed, rejected, or cancelled. An explicit outcome string is clearer than a success field that becomes false for every state except completion. Define allowed values and reserve an explicit unknown state where necessary. These are modeling choices that should be settled before a dashboard embeds assumptions about them.
Make units and boundaries visible
Names such as bytes_written, duration_ms, and retry_count help readers interpret values without opening a separate document every time. The field dictionary should go further: describe the clock, measurement boundary, and whether the number is cumulative or local to one attempt. Two individually accurate durations can answer different questions.
For instance, a client-observed operation might include connection setup and retries, while a server handler duration excludes both. Do not subtract or compare them as though they describe identical work. Keep both under distinct names when both are useful. Add representative examples that include zero, missing, and unusually large values. A field definition becomes more reliable when its edge cases are part of the contract rather than discoveries during an incident.
Allow evolution without silent breakage
An additive optional field is usually easier to introduce than a changed meaning for an existing field. Keep existing names stable and introduce a new name when semantics change materially. A version marker can help route transformations, but it does not replace a migration plan. Consumers still need to know which versions they understand.
Maintain example records for old and new producers. Test both against the collection pipeline before deploying a schema change. Decide how unknown attributes are handled: rejected, preserved without indexing, or quarantined for review. Unlimited acceptance is flexible but can allow accidental secrets; strict rejection protects the contract but can interrupt delivery. Make the tradeoff explicit and monitor the resulting failures. Review the logging API guide for the broader boundary design.
Separate storage from indexing
A record can contain useful detail without requiring every field to become an indexed dimension. Unique request identifiers help retrieve a specific incident, while a service name or release label can support aggregation. Indexing decisions should follow the queries you intend to run and the capabilities of the chosen storage system.
Be especially cautious about dynamic property names. Encoding a customer identifier into a field name creates an ever-changing schema. Prefer a stable field whose value holds the approved reference, then apply access and retention controls appropriate to that value. Estimate the number of distinct values and review the workload with the storage owner. “Structured” should mean predictable shape, not an invitation to create a new column for every event.
Use serialization and safe rendering
Build a language-native object and serialize it with a JSON library. Hand-concatenating quotes, backslashes, or user-controlled strings can produce malformed records and ambiguous line boundaries. In a line-oriented stream, the transport convention should clearly define one encoded event per record. Test embedded newlines, non-ASCII text, and long messages through the complete path.
A valid stored document can still be unsafe when a viewer inserts its content as HTML. Treat the message as text when displaying it and keep any markup generated by the viewer separate from event values. A redacted secret should not reappear in an error tooltip, export filename, or debugging console. The log redaction guide covers those less obvious copies.
Build a meaningful fixture suite
Your fixtures should include more than a clean example that every producer already handles. Add a numeric field accidentally sent as a string, an unknown event name, a missing required service, and an unsupported schema version. Include a deliberately oversized attribute object. Decide the expected result for each record before running the tests.
Check the edge cases
Then verify downstream behavior. Does a rejected record increment a useful counter? Does an accepted record keep its original numeric precision? Does the reader distinguish missing values from zero? Can a migration preserve old records without changing their meaning? The goal is to make schema failures diagnosable. A validator that rejects bad data but leaves operators unable to locate the responsible producer is only half a solution.
Make the schema part of normal development
Store the field dictionary, schema, and fixtures alongside application changes. Review a new event as an interface change with a specific consumer and privacy boundary. Remove attributes that have no identified purpose rather than waiting for storage pressure to force a rushed cleanup.
Structured JSON logging works best when it reduces interpretation. A small stable envelope, explicit units, honest missing values, and tested migrations let people move from an event to a decision quickly. Begin with that discipline. More elaborate query tools and visualizations become much more useful when the records underneath them already agree on what happened.



