Self-Describing Binary Log Format (v3)
This page specifies the on-disk format of a binary log segment, version 3, in enough detail to implement a decoder without the Traffic Server source tree. A version 3 segment is self-describing: every field’s type is published in the segment header, so a generic reader can decode each entry by dispatching on a small, stable set of type codes — no embedded copy of the ATS symbol-to-type table is required.
Motivation
In version 2, a segment header carries the field symbols (fmt_fieldlist,
e.g. "chi,cqu,pssc") and a printf-style template (fmt_printf) but
not the field types. To decode an entry a reader had to already know the
type of each symbol, because the value encodings are only self-delimiting once
the type is known (IP is variable length, for example). That coupled every
out-of-tree parser to the exact ATS build that wrote the log.
Version 3 adds one thing: a per-segment field-type schema that lists the wire type of every field, in field order. Decoding then needs only the symbols (as keys) and the schema (for types).
Segment layout
A .blog file is a stream of segments, each a serialized LogBuffer:
LogBufferHeader (per segment)
cookie = 0xaceface
version = 3
format_type, byte_count, entry_count, timestamps, flags, signature
fmt_name_offset
fmt_fieldlist_offset -> "chi,cqu,pssc,..." (symbols, comma separated)
fmt_printf_offset -> "%<chi> %<cqu> ..."
src_hostname_offset, log_filename_offset
data_offset -> first entry
fmt_fieldtypes_offset -> field-type schema (NEW in v3)
[ LogEntryHeader | field0 field1 field2 ... ] x entry_count
LogEntryHeader: timestamp(8) timestamp_usec(4) entry_len(4)
fields: concatenated in fieldlist order, no per-field tags
All *_offset members are byte offsets from the start of the segment (the
address of the LogBufferHeader). fmt_fieldtypes_offset is appended
after data_offset so that the layout through data_offset is
byte-identical to version 2; a value of 0 means the schema is absent (e.g.
a text-format segment, or a version 2 segment).
Field-type schema
At fmt_fieldtypes_offset the segment stores:
uint16_t field_count; // == number of symbols in fmt_fieldlist
uint8_t type_code[field_count]; // one type code per field, in order
type_code[i] is the type of the i-th field, which corresponds to the i-th
symbol in fmt_fieldlist and the i-th value in each entry. The uint16_t
field_count prefix is written in host byte order, like the rest of
LogBufferHeader. The blob is padded along with the header to an 8-byte
boundary.
The schema carries no independent version of its own: the segment version
(3 here) governs this layout, so a future schema change rides the same
LOG_SEGMENT_VERSION bump rather than a second, separate counter.
Stable type codes
The type codes are the values of the in-tree LogField::Type enumeration,
serialized directly. They are part of the published format and are
append-only: codes are never renumbered or reused.
Code |
Name |
Wire encoding |
|---|---|---|
0 |
INVALID |
Reserved. Not emitted by a correct writer; a reader that meets it – or any code it does not recognize – cannot determine the field length and must stop decoding the entry. |
1 |
sINT |
A single |
2 |
dINT |
Two |
3 |
STRING |
NUL-terminated bytes, then padded to an 8-byte boundary. |
4 |
IP |
|
The code reflects how the value is framed on disk, i.e. how a reader walks
(or skips) it – not what the value means. (The sINT/dINT names are an
ATS-internal distinction; on the wire sINT is one 8-byte integer and
dINT is two consecutive ones.) How a consumer renders a value – mapping
a cache-result integer to TCP_HIT, or a dINT to 1.1 – is layered
on top by the consumer and is not part of the wire format.
Value encodings
- sINT
An
int64_toccupying exactly 8 bytes, in host byte order (as in version 2). Integer values are not endianness-normalized, so a.blogis not portable across hosts of differing endianness; cross-architecture portability is future work.- dINT
Two consecutive
sINTvalues: 16 bytes total, in host byte order. Used where one log field is stored as two integers, such as an HTTP version (major then minor). The reference decoder renders it as a JSON array, e.g.[1,1]; turning that into1.1is a consumer concern.- STRING
The string bytes followed by a single NUL, then zero padding up to the next 8-byte boundary. The on-wire length is therefore
align_up(strlen + 1, 8). An empty/absent string is written as"-".- IP
A
uint16_taddress family in host byte order, then:Family
Following bytes
AF_INET(IPv4)4-byte
in_addrAF_INET6(IPv6)16-byte
in6_addrAF_UNIXfixed-size path buffer
AF_UNSPEC/ otherno address bytes
The whole field is padded to the next 8-byte boundary. Because the length depends on the family byte inside the value, only a reader that knows the field is an
IP(from the schema) can compute its size — which is exactly why the schema is required to skip or decode unknown fields safely.
Decoding an entry
Given a segment, a generic decoder:
Reads
field_countand thetype_code[]array from the schema atfmt_fieldtypes_offset.Splits
fmt_fieldlistintofield_countcomma-separated symbols (LogFormat::parse_format_string()joins symbols with,; the reference decoder also tolerates spaces as separators).For each entry (located via
data_offsetand walked usingLogEntryHeader::entry_len), reads the fields left to right, usingtype_code[i]to pick the encoding above and advance the read cursor.
The reference implementation is log_entry_to_json()
(src/traffic_logcat/LogEntryJson.cc), which renders an entry as a JSON
object using only the symbols and the schema — it does not consult the global
field table. It is exposed by traffic_logcat’s -j/--json
option. For example, a three-field entry decodes to:
{"chi":"192.0.2.10","cqu":"GET /index.html","pssc":200}
Note
Some integer fields hold coded values (cache result, hierarchy, finish
status, etc.). The binary format stores the raw integer; mapping it to a
mnemonic such as TCP_HIT is a presentation concern left to the consumer.
Compatibility
New reader, old file (v3 reader, v2 file): supported. The readers shipped with Traffic Server accept the inclusive version range
[2, 3]and size the header read to the on-disk version, so a v2 segment (which has nofmt_fieldtypes_offset) still decodes. Its ASCII output is produced fromfmt_fieldlist+fmt_printfexactly as before.Old reader, new file (v2 reader, v3 file): a reader built before v3 support gates on the version and will skip v3 segments. v3 logs therefore require tooling from a release that understands v3. As an escape hatch, a binary log object can be pinned to the version 2 layout with
binary_log_version: 2inlogging.yaml, so a not-yet-upgraded downstream parser keeps working during a migration.The text/Squid/CLF ASCII output paths are unchanged: the schema is additive and ignored when rendering ASCII.
Note
v3 does not change integer endianness: field values, the integers in
LogBufferHeader / LogEntryHeader, and the IP family word are all
written in host byte order, as in v2. A .blog is therefore not portable
across hosts of differing endianness; cross-architecture portability is
future work.