Skip to content

Commit 8358d71

Browse files
committed
Self-describing binary log format (LogBuffer v3)
Publish each field's type in a per-segment schema so a generic reader can decode a .blog from the file alone, without an embedded ATS symbol-to-type table that must track the writer in lockstep. The per-field code is LogField::Type serialized directly (now an enum class : uint8_t with INVALID=0 reserved and sINT..IP = 1..4 as the frozen wire codes); a static_assert pins the values. This relies on each field's declared type matching its marshalled framing, which the parent commit ("Fix mismatched sINT/dINT log field types") establishes. Readers (LogBufferIterator, logcat, logstats, the ASCII output paths) accept both v2 and v3 segments, sizing the header read to the on-disk version, so a v3 build keeps decoding logs written by an older one. Integer values stay in host byte order, as in v2 (no endianness change). The public TSLogType enum is given the same values as LogField::Type so TSLogFieldRegister can static_cast between them; static_asserts in InkAPI.cc (the only TU that sees both) pin the alignment so a future reorder fails to compile. The writer version is per-LogObject: logging.yaml "binary_log_version: 2" pins a binary log to the pre-v3 layout (no schema, shorter header) so a not-yet-upgraded downstream parser keeps working during a migration; the default is v3. Decoding untrusted .blog input is bounded: LogBufferIterator validates data_offset and each entry against the segment, and the JSON decoder validates the schema offset alignment and cross-checks field_count against the symbol list.
1 parent 5b1a757 commit 8358d71

31 files changed

Lines changed: 1785 additions & 249 deletions

doc/admin-guide/files/logging.yaml.en.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,15 @@ filename string The name of the logfile relative to the defau
289289
format string a string with a valid named format specification.
290290
header string If present, emitted as the first line of each
291291
new log file.
292+
binary_log_version number For ``binary`` logs only: the on-disk segment
293+
format version, ``2`` or ``3`` (default
294+
``3``). Version 3 is self-describing (embeds a
295+
per-field type schema so a generic reader can
296+
decode the file without an ATS symbol table);
297+
use ``2`` to emit the legacy layout for
298+
downstream parsers that do not yet understand
299+
version 3. Ignored for ``ascii`` and
300+
``ascii_pipe``.
292301
rolling_enabled *see below* Determines the type of log rolling to use (or
293302
whether to disable rolling). Overrides
294303
:ts:cv:`proxy.config.log.rolling_enabled`.
@@ -389,3 +398,28 @@ matched the REFRESH_HIT filter we created.
389398
format: summaryfmt
390399
filters:
391400
- refreshhitfilter
401+
402+
The following is an example of a binary log. Binary logs are written in the
403+
self-describing version 3 format by default, so ``traffic_logcat`` and
404+
``traffic_logstats`` (and any reader built from the
405+
:ref:`v3 format specification <binary-log-v3-format>`) can decode the
406+
``minimal.blog`` file without an embedded copy of the field table:
407+
408+
.. code:: yaml
409+
410+
logs:
411+
- mode: binary
412+
filename: minimal
413+
format: minimalfmt
414+
415+
To keep emitting the older version 2 layout for a downstream parser that does
416+
not yet understand version 3, pin the object with ``binary_log_version``. This
417+
key only applies to ``binary`` logs and defaults to ``3``:
418+
419+
.. code:: yaml
420+
421+
logs:
422+
- mode: binary
423+
filename: minimal_legacy
424+
format: minimalfmt
425+
binary_log_version: 2

doc/appendices/command-line/traffic_logcat.en.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Description
3333
To analyze a binary log file using standard tools, you must first convert
3434
it to ASCII. :program:`traffic_logcat` does exactly that.
3535

36+
:program:`traffic_logcat` reads both version 2 and version 3 binary log
37+
segments. See :ref:`binary-log-v3-format` for the self-describing v3 format.
38+
3639
Options
3740
=======
3841

@@ -74,6 +77,12 @@ Attempts to transform the input to Squid format, if possible.
7477

7578
Attempt to transform the input to Netscape Extended-2 format, if possible.
7679

80+
.. option:: -j, --json
81+
82+
Emits each entry as a JSON object, decoded directly from the self-describing
83+
v3 field-type schema (see :ref:`binary-log-v3-format`). Requires version 3
84+
binary logs; version 2 segments lack the schema and are skipped with a note.
85+
7786
.. option:: -T, --debug_tags
7887

7988
.. option:: -w, --overwrite_output

doc/appendices/command-line/traffic_logstats.en.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ produce metrics for total and per origin requests. Currently, this utility
3535
only supports parsing and processing the Squid binary log format, or a custom
3636
format that is compatible with the initial log fields of the Squid format.
3737

38+
Both version 2 and version 3 binary log segments are supported. See
39+
:ref:`binary-log-v3-format` for the self-describing v3 format.
40+
3841
Output can either be a human readable text file, or a JSON format. Parsing can
3942
be done incrementally, and :program:`traffic_logstats` supports restarting
4043
where it left off previously (state is stored in an external file). This is
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
.. Licensed to the Apache Software Foundation (ASF) under one
2+
or more contributor license agreements. See the NOTICE file
3+
distributed with this work for additional information
4+
regarding copyright ownership. The ASF licenses this file
5+
to you under the Apache License, Version 2.0 (the
6+
"License"); you may not use this file except in compliance
7+
with the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing,
12+
software distributed under the License is distributed on an
13+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
KIND, either express or implied. See the License for the
15+
specific language governing permissions and limitations
16+
under the License.
17+
18+
.. include:: ../../common.defs
19+
20+
.. _binary-log-v3-format:
21+
22+
Self-Describing Binary Log Format (v3)
23+
**************************************
24+
25+
This page specifies the on-disk format of a binary log segment, version 3, in
26+
enough detail to implement a decoder *without* the Traffic Server source tree.
27+
A version 3 segment is **self-describing**: every field's type is published in
28+
the segment header, so a generic reader can decode each entry by dispatching on
29+
a small, stable set of type codes — no embedded copy of the ATS symbol-to-type
30+
table is required.
31+
32+
Motivation
33+
==========
34+
35+
In version 2, a segment header carries the field *symbols* (``fmt_fieldlist``,
36+
e.g. ``"chi cqu pssc"``) and a printf-style *template* (``fmt_printf``) but
37+
**not** the field types. To decode an entry a reader had to already know the
38+
type of each symbol, because the value encodings are only self-delimiting once
39+
the type is known (``IP`` is variable length, for example). That coupled every
40+
out-of-tree parser to the exact ATS build that wrote the log.
41+
42+
Version 3 adds one thing: a per-segment **field-type schema** that lists the
43+
wire type of every field, in field order. Decoding then needs only the symbols
44+
(as keys) and the schema (for types).
45+
46+
Segment layout
47+
==============
48+
49+
A ``.blog`` file is a stream of segments, each a serialized ``LogBuffer``:
50+
51+
::
52+
53+
LogBufferHeader (per segment)
54+
cookie = 0xaceface
55+
version = 3
56+
format_type, byte_count, entry_count, timestamps, flags, signature
57+
fmt_name_offset
58+
fmt_fieldlist_offset -> "chi cqu pssc ..." (symbols, space separated)
59+
fmt_printf_offset -> "%<chi> %<cqu> ..."
60+
src_hostname_offset, log_filename_offset
61+
data_offset -> first entry
62+
fmt_fieldtypes_offset -> field-type schema (NEW in v3)
63+
[ LogEntryHeader | field0 field1 field2 ... ] x entry_count
64+
LogEntryHeader: timestamp(8) timestamp_usec(4) entry_len(4)
65+
fields: concatenated in fieldlist order, no per-field tags
66+
67+
All ``*_offset`` members are byte offsets from the start of the segment (the
68+
address of the ``LogBufferHeader``). ``fmt_fieldtypes_offset`` is appended
69+
**after** ``data_offset`` so that the layout through ``data_offset`` is
70+
byte-identical to version 2; a value of ``0`` means the schema is absent (e.g.
71+
a text-format segment, or a version 2 segment).
72+
73+
Field-type schema
74+
=================
75+
76+
At ``fmt_fieldtypes_offset`` the segment stores:
77+
78+
::
79+
80+
uint16_t field_count; // == number of symbols in fmt_fieldlist
81+
uint8_t type_code[field_count]; // one type code per field, in order
82+
83+
``type_code[i]`` is the type of the i-th field, which corresponds to the i-th
84+
symbol in ``fmt_fieldlist`` and the i-th value in each entry. The ``uint16_t``
85+
``field_count`` prefix is written in **host byte order**, like the rest of
86+
``LogBufferHeader``. The blob is padded along with the header to an 8-byte
87+
boundary.
88+
89+
The schema carries no independent version of its own: the segment ``version``
90+
(``3`` here) governs this layout, so a future schema change rides the same
91+
``LOG_SEGMENT_VERSION`` bump rather than a second, separate counter.
92+
93+
Stable type codes
94+
=================
95+
96+
The type codes are the values of the in-tree ``LogField::Type`` enumeration,
97+
serialized directly. They are part of the published format and are
98+
**append-only**: codes are never renumbered or reused.
99+
100+
==== ========= ===========================================================
101+
Code Name Wire encoding
102+
==== ========= ===========================================================
103+
0 INVALID Reserved. Not emitted by a correct writer; a reader that
104+
meets it -- or any code it does not recognize -- cannot
105+
determine the field length and must stop decoding the entry.
106+
1 sINT A single ``int64_t``, fixed 8 bytes, **host byte order**.
107+
2 dINT Two ``int64_t`` (16 bytes), host byte order. Used for
108+
values stored as two integers, e.g. HTTP version
109+
major/minor.
110+
3 STRING NUL-terminated bytes, then padded to an 8-byte boundary.
111+
4 IP ``uint16_t`` address family followed by a family-sized
112+
address, then padded to an 8-byte boundary (see below).
113+
==== ========= ===========================================================
114+
115+
The code reflects how the value is *framed* on disk, i.e. how a reader walks
116+
(or skips) it -- not what the value means. (The ``sINT``/``dINT`` names are an
117+
ATS-internal distinction; on the wire ``sINT`` is one 8-byte integer and
118+
``dINT`` is two consecutive ones.) How a consumer *renders* a value -- mapping
119+
a cache-result integer to ``TCP_HIT``, or a ``dINT`` to ``1.1`` -- is layered
120+
on top by the consumer and is not part of the wire format.
121+
122+
Value encodings
123+
===============
124+
125+
sINT
126+
An ``int64_t`` occupying exactly 8 bytes, in **host byte order** (as in
127+
version 2). Integer values are not endianness-normalized, so a ``.blog`` is
128+
not portable across hosts of differing endianness; cross-architecture
129+
portability is future work.
130+
131+
dINT
132+
Two consecutive ``sINT`` values: 16 bytes total, in host byte order. Used
133+
where one log field is stored as two integers, such as an HTTP version
134+
(major then minor). The reference decoder renders it as a JSON array, e.g.
135+
``[1,1]``; turning that into ``1.1`` is a consumer concern.
136+
137+
STRING
138+
The string bytes followed by a single NUL, then zero padding up to the next
139+
8-byte boundary. The on-wire length is therefore
140+
``align_up(strlen + 1, 8)``. An empty/absent string is written as ``"-"``.
141+
142+
IP
143+
A ``uint16_t`` address family in host byte order, then:
144+
145+
.. list-table::
146+
:header-rows: 1
147+
:widths: 30 70
148+
149+
* - Family
150+
- Following bytes
151+
* - ``AF_INET`` (IPv4)
152+
- 4-byte ``in_addr``
153+
* - ``AF_INET6`` (IPv6)
154+
- 16-byte ``in6_addr``
155+
* - ``AF_UNIX``
156+
- fixed-size path buffer
157+
* - ``AF_UNSPEC`` / other
158+
- no address bytes
159+
160+
The whole field is padded to the next 8-byte boundary. Because the length
161+
depends on the family byte *inside* the value, only a reader that knows the
162+
field is an ``IP`` (from the schema) can compute its size — which is exactly
163+
why the schema is required to skip or decode unknown fields safely.
164+
165+
Decoding an entry
166+
=================
167+
168+
Given a segment, a generic decoder:
169+
170+
#. Reads ``field_count`` and the ``type_code[]`` array from the schema at
171+
``fmt_fieldtypes_offset``.
172+
#. Splits ``fmt_fieldlist`` into ``field_count`` whitespace-separated symbols.
173+
#. For each entry (located via ``data_offset`` and walked using
174+
``LogEntryHeader::entry_len``), reads the fields left to right, using
175+
``type_code[i]`` to pick the encoding above and advance the read cursor.
176+
177+
The reference implementation is ``log_entry_to_json()``
178+
(``src/traffic_logcat/LogEntryJson.cc``), which renders an entry as a JSON
179+
object using only the symbols and the schema — it does not consult the global
180+
field table. It is exposed by :program:`traffic_logcat`'s ``-j``/``--json``
181+
option. For example, a three-field entry decodes to:
182+
183+
::
184+
185+
{"chi":"192.0.2.10","cqu":"GET /index.html","pssc":200}
186+
187+
.. note::
188+
189+
Some integer fields hold coded values (cache result, hierarchy, finish
190+
status, etc.). The binary format stores the raw integer; mapping it to a
191+
mnemonic such as ``TCP_HIT`` is a presentation concern left to the consumer.
192+
193+
Compatibility
194+
=============
195+
196+
* **New reader, old file (v3 reader, v2 file):** supported. The readers shipped
197+
with Traffic Server accept the inclusive version range
198+
``[2, 3]`` and size the header read to the on-disk version, so a v2 segment
199+
(which has no ``fmt_fieldtypes_offset``) still decodes. Its ASCII output is
200+
produced from ``fmt_fieldlist`` + ``fmt_printf`` exactly as before.
201+
* **Old reader, new file (v2 reader, v3 file):** a reader built before v3
202+
support gates on the version and will skip v3 segments. v3 logs therefore
203+
require tooling from a release that understands v3. As an escape hatch, a
204+
binary log object can be pinned to the version 2 layout with
205+
``binary_log_version: 2`` in :file:`logging.yaml`, so a not-yet-upgraded
206+
downstream parser keeps working during a migration.
207+
* The text/Squid/CLF ASCII output paths are unchanged: the schema is additive
208+
and ignored when rendering ASCII.
209+
210+
.. note::
211+
212+
v3 does not change integer endianness: field values, the integers in
213+
``LogBufferHeader`` / ``LogEntryHeader``, and the ``IP`` family word are all
214+
written in host byte order, as in v2. A ``.blog`` is therefore not portable
215+
across hosts of differing endianness; cross-architecture portability is
216+
future work.

doc/developer-guide/logging-architecture/index.en.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ Logging Architecture
2626
:maxdepth: 2
2727

2828
architecture.en
29+
binary-log-v3-format.en

0 commit comments

Comments
 (0)