skip to content

Describe the OpenTelemetry log record as a data structure: what fields it carries, why it has two timestamps, and how its 1-24 severity numbering is meant to be used.

level: seniorimportance: should knowfreq 34%

answer

  1. Two timestamps: event time optional, observed time always present
  2. 1-24 severity in six bands of four; text preserved alongside
  3. Body is AnyValue (nested maps survive); attributes are the filter dimensions
  4. trace_id / span_id / trace_flags are fields, not conventions
  5. Resource -> InstrumentationScope -> LogRecord envelope, same as spans

basics

~20 s

It carries an event timestamp (which may be absent), an always-present observed timestamp, a numeric severity 1-24 in six bands of four plus the original severity text, an any-typed body, attributes, an event name, and trace id, span id and trace flags fields.

solid answer

~50 s

A record carries `time_unix_nano` — when the event happened, optional because some sources never recorded it — and `observed_time_unix_nano`, when the collection system saw the record, which is always populated and is the ordering fallback. Severity is two fields: `severity_number`, an integer 1-24 laid out as six bands of four (TRACE 1-4, DEBUG 5-8, INFO 9-12, WARN 13-16, ERROR 17-20, FATAL 21-24), and `severity_text`, the source library's original label preserved verbatim. `body` is an `AnyValue` — string, number, bool, bytes, array or nested key/value map — so a structured event is not flattened into a string. `attributes` are the typed dimensions you filter and group by, with `dropped_attributes_count` recording truncation. `event_name` names a record that represents a named event. `trace_id`, `span_id` and `trace_flags` are first-class fields, not conventions. Records nest inside the same Resource then InstrumentationScope envelope as spans.

code

text · 11 lines
text
TRACE 1-4   DEBUG 5-8   INFO 9-12   WARN 13-16   ERROR 17-20   FATAL 21-24

source library level      severity_number   severity_text
  "debug"                        5           "debug"
  "fine" (richer scheme)         6           "fine"
  "warning"                     13           "warning"
  "W"                           13           "W"

four slots per band absorb richer level schemes, so one numeric
predicate works across every source:
  severity_number >= 13  ==  "WARN or worse", whoever emitted it

go deeper

for a junior

Recall the field list and the two headline facts: two timestamps (one optional, one always there) and a numeric severity alongside the original text label.

for a middle

Explain why each design choice exists — optional event time versus mandatory observed time, banded numbers so heterogeneous libraries stay comparable, AnyValue body so structure is not flattened, attributes as the filterable dimensions.

for a senior

Reason about consequences in a running pipeline: ordering late-arriving records, writing severity predicates that hold fleet-wide, splitting body versus attributes so the backend indexes well, spotting dropped_attributes_count, and explaining why most records point at traces that were never exported.

for a principal

Frame the schema as the fleet-wide contract: banded severity and the resource/scope envelope are what let a single query language span teams and languages, and the record's independence from trace sampling is what makes it the durable signal that span events are not.

## A log is a typed record, not a line In OpenTelemetry a log is a record with a fixed schema, carried over OTLP in the same envelope shape as the other signals. Knowing the schema field by field is what tells you what survives a pipeline, what a backend can index, and what a query can compare across services written by different teams in different languages. ## The two timestamps - **`time_unix_nano`** (spec name: Timestamp) — when the event actually occurred. It is **optional**, because plenty of sources genuinely do not record one: a bare line of text, a device that emits no clock reading, a subsystem whose format has no time field. - **`observed_time_unix_nano`** (ObservedTimestamp) — when the collection system first saw the record. It is **always present**; when event time is missing, this is what a consumer falls back to for ordering. They answer different questions and can differ by a long way. A file tailed hours after a partition, or a batch flushed after a queue drains, produces records whose event time orders the story and whose observed time explains delivery. Keeping both means a late-arriving record can still be placed correctly in the narrative while you can still see that it arrived late. ## Severity: a number and the original text Severity is two fields on purpose. `severity_number` is an integer 1-24, arranged as **six bands of four**: TRACE 1-4, DEBUG 5-8, INFO 9-12, WARN 13-16, ERROR 17-20, FATAL 21-24 (0/unset means unspecified). The banding is the whole point. Source libraries disagree about how many gradations they offer and what they call them — one has a level between DEBUG and INFO, another has two flavours of warning, another lets you define your own. A single flat enum would force those into lossy collisions. Four slots per band give a mapper room to place a richer scheme inside the right band while preserving relative order within it. The consequence is the thing to say in an interview: because the *number* is banded the same way everywhere, `severity_number >= 13` means "WARN or worse" for **every** source in a heterogeneous fleet, without the query having to know which library emitted the record or what string it used. Filtering on `severity_text` cannot do that — the text is free-form and differs per library ("warn", "warning", "WARNING", "W"). `severity_text` exists so the mapping is not destructive: the original label is preserved verbatim for display and for anyone who needs the source's own vocabulary back. ## Body versus attributes `body` is an **`AnyValue`**: a string, a number, a boolean, a byte array, an array, or a nested key/value map. That typing is deliberate — a structured event can be carried as structure, with nested maps and arrays intact, instead of being serialised into a string that a backend then has to substring-search or re-parse. `attributes` are typed key/values governed by the semantic conventions, and they are what you filter, group and aggregate on. The practical split: **body = the payload of the event; attributes = the dimensions of the event**. Two habits cause pain. Formatting everything into a string body pushes backends into substring matching, which is slow and imprecise. Duplicating every attribute inside the body doubles storage and buys nothing. A small set of well-named attributes plus a structured body indexes well and reads well. When attribute count or value length limits truncate, `dropped_attributes_count` records how many were lost, so a consumer can tell a sparse record from a truncated one rather than silently believing what it received is complete. ## `event_name` Newer revisions of the record carry an `event_name` field, used when the record represents a *named* event rather than free-form text — the name identifies the event's shape, and the body/attributes carry its payload. Earlier tooling expressed the same idea as an `event.name` attribute, so both shapes appear in real pipelines. ## The envelope Records do not travel alone. The structure is **Resource -> InstrumentationScope -> LogRecord**, the same nesting spans use. The resource carries the identity of the emitting entity (service name, instance, host, deployment attributes) and is attached once for many records rather than repeated per record; the instrumentation scope records which library or module emitted them. So a log inherits producer identity for free, and "all logs from this service instance" is a resource-level filter rather than a string convention someone had to remember to include. ## Correlation fields, and one caveat `trace_id`, `span_id` and `trace_flags` are **fields of the record**, not attributes bolted on by convention. That is what lets a backend offer symmetric navigation: from a span to the records emitted during it, and from a record back to its trace. The caveat worth knowing: `trace_flags` carries the sampled bit of the trace that was active when the record was created. If traces are sampled at a low rate, most records will point at traces that were **never exported** — the ids are correct, but the trace they name does not exist in the backend. That is not a defect in the record; it means "jump to trace" fails most of the time unless you keep an unsampled slice or bias sampling toward interesting requests. ## Log record versus span event Both are timestamped, attributed points in time with trace correlation, so the schemas look similar. The difference that matters is lifecycle and indexing. A span event is *part of* a span: it is sampled away with the span, retained with it, and reachable only through it. A log record is its own signal with its own retention, its own volume controls and its own index, searchable across all requests without going through a trace at all. That difference — plus the record's independence from trace sampling — is why event semantics have been converging onto the log-record schema.

  • Why does a record need both an event timestamp and an observed timestamp?
    They answer different questions and can be far apart. The event timestamp says when the thing happened and is what you order the narrative by, but it is optional because many sources never record one. The observed timestamp says when the collection system saw the record, is always populated, and doubles as the ordering fallback and as the evidence that a record arrived late — a batch flushed after a partition keeps its true event times while the observed times show the delay.
  • If severity_text is preserved, why bother with severity_number at all?
    Because the text is free-form and per-library: "warn", "warning", "WARNING", "W" all mean the same thing and none of them compare. The number is banded identically for every source, so `severity_number >= 13` means "WARN or worse" fleet-wide regardless of which library emitted the record. The text is kept so the mapping is non-destructive and the source's own vocabulary can still be displayed.
  • A record has a trace id and span id, but the backend cannot show the trace. Is the record wrong?
    Usually not. The record's trace_flags carry the sampled bit of the trace that was active at emission, and if traces are sampled at a low rate most records will reference traces that were never exported. The ids are accurate; the trace simply is not in the backend. Fixes are sampling-side — keep an unsampled slice, or bias sampling toward the requests you will want to pivot into — not record-side.
  • When would you put something in the body versus in attributes?
    Body is the payload of the event and can be an AnyValue, so a structured event keeps its nested maps and arrays instead of being flattened to a string. Attributes are the dimensions you filter, group and aggregate on, and they are governed by semantic conventions. Formatting everything into a string body forces substring search; duplicating attributes into the body doubles storage for no gain.

saying these in an interview costs you the question

  • Saying you filter on severity_text — it is free-form per library and does not compare across sources; the banded severity_number is what makes 'WARN or worse' fleet-wide queries meaningful.
  • Claiming every record has an event timestamp. `time_unix_nano` is optional; `observed_time_unix_nano` is the one that is always present.
  • Assuming the body must be a string. It is an AnyValue, so nested maps and arrays survive un-stringified.
  • Treating trace_id and span_id as attributes attached by convention — they are first-class fields of the record.
  • Reading the record's sampled flag as a statement about the log. It is the sampled bit of the trace that was active, which is why most records can point at traces the backend never received.

context