Extracting log templates from source, binaries, and Helm charts

How 10x reads log formats out of source code, jars and Helm charts.

Share
The fixed wording of a log line was written into the code long before the line printed. Guessing that wording back at ingest is the usual approach, and frequency alone merges unrelated lines that happen to look alike. A compiler reads that wording where it already sits, in repos, compiled binaries and Helm charts, and links what it finds into one library. The runtime holds that library before the first line arrives.

A log line is a template plus some data. Guessing the template at ingest is one way to recover it. The other is to go and read it where it already sits: the repos, the open-source libraries, the compiled binary, the Helm chart, the jar in Artifactory.

We built a compiler that pulls the template out of all of those and links them into one library, the way a C toolchain links .o files into a .so. The runtime holds that answer before the first line shows up.

Here is the line that makes the case:

2025-09-29 09:01:23.456 INFO o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''

Most of this repeats across every Spring Boot restart. Structural: the class name TomcatWebServer, the format string Tomcat started on port(s): X (http) with context path Y, the INFO severity. Data: the 8080.

A vocabulary is the set of fixed strings the code can print, every class name, format-string literal and severity label. Each token gets checked against it.

Frequency alone breaks on cases this plain, because the same 8080 is a TCP port in one service and a row count in another:

Server listening on port 8080
Flushed 8080 rows to the segment store

A frequency guesser decides both 8080s are the same field and merges two unrelated shapes. Whatever supplies the vocabulary has to hold still while traffic does not, which leaves the code.

Log lines tinted token by token: structural tokens from the source (class names, format strings, INFO) in blue, runtime data (timestamps, the 8080s) in amber, showing the same value 8080, a port in one shape and a row count in another, never collides.

The compiler stages

Same shape as a C build.

  • Pull fetches source files, binary artifacts and Helm chart contents from their stores, caching by content hash so unchanged inputs are never reprocessed.
  • Scan runs each input through the scanner for its format and writes per-file .10x.json symbol units, the .o of this build.
  • Link merges the symbol units into one library, the way ld combines .o files into a .so.
  • Push commits the library to a target GitHub repo. The runtime pulls from there at startup.

At runtime the vocabulary lets the engine blank the data out of each line, leaving one template per shape. The hash is taken over that template. Identical shapes collapse to one for dedup, and cost tracking keys on the pattern, the message words the engine selects from that template, so the format variants of one statement count together.

Relink gives back a stable library: the linker dedups the symbol units and writes them in a canonical order, sorted by unit name and then by unit hash, so the same inputs produce the same symbol payload. Each shape's hash then holds across deploys that pin the same library and engine version, for the lines that library covers, and dashboards and savings numbers survive a redeploy (more on the runtime story).

Polyglot on two axes

A stack has variety on two axes: many source systems, many formats in each.

The pull module carries an adapter per source: github, docker, helm, artifactory, plus a go.mod resolver that expands Go dependencies into GitHub pulls.

Inside any one source the content is heterogeneous. Source code is routed by language to a scanner that knows its grammar: a dedicated parser for Java, Python and Scala, one shared ANTLR walker for C and C++, C#, Go, JavaScript, TypeScript and Rust, and quoted-string extraction alone for Ruby, Kotlin, PHP, Swift, Lua, Groovy and bash. Text configs carry structural strings as keys and labels.

The executable scanner walks a binary and extracts printable strings the way the Unix strings command does. Most log lines from a Go service live in the binary rather than the source, so for a Go service that one scanner carries most of the vocabulary, and it doubles as the extension point for any source that emits candidate strings.

What ships precompiled

Spring Boot is the same Spring Boot everywhere it runs. Compiling it once per install would repeat the same work per customer for the same result.

So we ran the pipeline over public code and bundled the output. The project list is one YAML file in the open pipeline definitions, with Tomcat, Netty, Elasticsearch, Django, Gin, Envoy, Prometheus, Redis and Postgres among them. The shipped library carries a manifest naming every repository and image the build read, each pinned to a commit or a digest, so what went into it is checkable rather than asserted. The compile step then runs only for code the default does not cover.

Most tooling instead pushes the vocabulary problem onto the operator. Splunk sourcetype field extractions, Datadog log processing pipelines, Elasticsearch ingest grok rules, Cribl and Vector and Logstash transforms: prebuilt for the common integrations, hand-written for everything the application itself emits. Reading the vocabulary out of the code that prints the logs works, but only if the compiler can read every source and every format in the environment. That is a lot of scanners to own, which is presumably why the hand-written rule stayed the default.

The compile pipeline modules are open source at github.com/log-10x/modules/tree/main/pipelines/compile. Clone it and run it against any repo or image. The ANTLR scanner covers language support. Hidden log classes covers what attaches to templates.