Traceloom
Structured application traces without a heavy framework.
A small library that records what actually happened during one logical process: every event tied to one trace_id, written as JSONL you can read with grep. No agent, no collector, no platform.
# one request, one trace_id, three events
{"timestamp":"2026-07-10T10:41:20.112345Z",
"trace_id":"9f1a8e7c2d4b4a9f93e2b2b1454f0c0a",
"event":"request_start","sequence":1,
"elapsed_ms":0.121,
"data":{"method":"POST","path":"/orders"}}
{"timestamp":"2026-07-10T10:41:20.117381Z",
"trace_id":"9f1a8e7c2d4b4a9f93e2b2b1454f0c0a",
"event":"auth_success","sequence":2,
"elapsed_ms":5.157,
"data":{"user_id":42}}
{"timestamp":"2026-07-10T10:41:20.289842Z",
"trace_id":"9f1a8e7c2d4b4a9f93e2b2b1454f0c0a",
"event":"request_end","sequence":3,
"elapsed_ms":177.618,
"data":{"status":201}}
Why it exists
Logs show isolated messages. A trace shows a story.
Traditional application logs give you lines. Each one is true, and none of them tells you what the request actually did. Traceloom connects related events into a readable execution timeline for one logical process: a request, a job, a webhook, a script.
The niche is narrow on purpose. A small PHP API, a Node worker or a Go job does not need a distributed tracing platform: agents, collectors and dashboards solve problems it does not have. What it does need is the answer to one question: what happened inside the request that broke at 10:41? Traceloom does exactly that, and nothing else.
Reconstructing that answer from ordinary logs means grepping for a user ID, guessing which lines belong together and hoping nothing interleaved. Traceloom gives those lines a shared identity instead.
Boundaries
What Traceloom is not
It is not a replacement for Monolog or Pino: use a general-purpose logger when you need levels, transports and integrations.
It is not a replacement for OpenTelemetry: use that when you need industry-standard distributed tracing.
It is not an observability platform: use one when you need aggregation, dashboards, alerting or cross-service querying.
Core concept
One trace_id, one timeline
The whole library is built on a single idea, and everything else is a consequence of it.
You start a trace at the beginning of a logical process. Every event recorded through it carries the same trace_id, so the events of one request are findable as a group instead of scattered through a file shared with everything else running at that moment.
Each event also carries a sequence number and elapsed_ms measured from the start of the trace on a monotonic clock. Order survives even when timestamps collide; duration survives an NTP correction. A gap in sequence is deliberate too: it is how a reader learns the timeline lost an event.
The output is JSONL: one JSON object per line, appended to a file. No daemon to run, no port to open, no schema registry. You can grep it, pipe it through jq, or use the bundled eventtrace CLI to print a trace as a timeline.
Language versions
Three implementations. All released.
PHP came first and is the most mature. JavaScript and Go have both reached v0.1.0 and write the same event format; they are published, installable and usable today.
composer require golovanov/traceloom
What's specific to PHP
The original implementation and the most mature at v0.4.0. Its sanitizer is the reference for the others: string, record, array, node, depth and key size limits, with a digest appended to truncated keys so two long keys can never collapse into one. Writes are synchronous; concurrent processes coordinate with flock. Ships the eventtrace CLI via vendor/bin.
use Golovanov\Traceloom\Tracer;
$tracer = Tracer::fromDirectory(__DIR__ . '/logs');
$trace = $tracer->start();
$trace->event('request_start', [
'method' => 'POST',
'path' => '/orders',
]);
$trace->event('auth_success', [
'user_id' => 42,
]);
$trace->event('request_end', [
'status' => 201,
]);
npm install @rgolovanov/traceloom
What's specific to JavaScript
TypeScript, dependency-free, shipping both ESM and CommonJS builds with types. Node.js 20 or newer. The API is async: events are queued and appended in batches, bounded by maxQueuedEvents with a configurable overflow policy, so a producer that outruns the disk loses events loudly rather than silently. Cross-process writers coordinate through an O_CREAT | O_EXCL lock file.
import { Tracer } from '@rgolovanov/traceloom';
const tracer = Tracer.fromDirectory('./logs');
const trace = tracer.start();
await trace.event('request_start', {
method: 'POST',
path: '/orders',
});
await trace.event('auth_success', { user_id: 42 });
await trace.event('request_end', { status: 201 });
await tracer.close();
go get github.com/golovanov-dev/traceloom-go
What's specific to Go
No external dependencies, Go 1.22 or newer. A thread-safe API configured with functional options. Interprocess coordination uses kernel file locks on every supported platform (syscall.Flock on Linux, macOS and the BSDs, LockFileEx on Windows), so a crashed writer cannot strand a lock. Shards are crash-consistent: a write that dies mid-record cannot corrupt the record after it. Event is synchronous; EventContext honours cancellation.
package main
import (
"log"
traceloom "github.com/golovanov-dev/traceloom-go"
)
func main() {
tracer, err := traceloom.New("./logs")
if err != nil {
log.Fatal(err)
}
defer tracer.Close()
trace, err := tracer.Start("")
if err != nil {
log.Fatal(err)
}
if err := trace.Event("request_start", traceloom.Data{
"method": "POST",
"path": "/orders",
}); err != nil {
log.Fatal(err)
}
_ = trace.Event("auth_success", traceloom.Data{"user_id": 42})
_ = trace.Event("request_end", traceloom.Data{"status": 201})
}
One format, three languages
A polyglot system, one readable timeline
The three implementations are not three products that happen to share a name. They write the same JSONL schema and apply the same sanitizer rules, so a PHP API, a Node worker and a Go job produce timelines a human reads the same way.
{
"timestamp": "2026-07-10T10:41:20.112345Z",
"trace_id": "9f1a8e7c2d4b4a9f93e2b2b1454f0c0a",
"event": "request_start",
"sequence": 1,
"elapsed_ms": 0.121,
"data": { "method": "POST", "path": "/orders" }
}
An incoming trace ID is not taken at face value by default: all three implementations record it as parent_trace_id and start a trace with its own ID, so a request from outside cannot write into your timeline. Behind a gateway that sets the header itself, you enable trust with one setting. This is data hygiene, nothing more.
One eventtrace show <trace-id> command works on the files of any implementation and prints the same timeline, whichever language wrote them.
Write coordination is simple: PHP and Go can append to one directory, and a Node writer keeps a directory of its own. All three read each other's files.
Design principles
The rules the library is held to
Every one of these has said no to a feature at some point. That is what makes them principles rather than adjectives.
01
Low dependency footprint
The Go and JavaScript packages have zero runtime dependencies; PHP pulls in nothing beyond the language. Adding tracing should not reshape your dependency tree.
02
Predictable API
Start a trace, record events, close. Three concepts. The same three in every language, adjusted only where the language demands it.
03
Structured context
Events carry typed payloads, not interpolated strings. A machine can filter them; a human can still read them.
04
Framework independence
No PSR-7, Laravel, Symfony or Express adapter in the core. The library does not care what routed the request.
05
Practical diagnostics
The output is a file you can grep on a server at 2am, not a format that requires a running platform to mean anything.
06
Safe production behaviour
Fail-safe by default: a tracing failure never breaks the host application. Secrets are masked, payloads bounded, and losses counted rather than swallowed. One limit to know: masking matches key names, not values, so a secret stored under an innocuous key is not detected.
07
Easy integration
One line to start, one line per event. Files are created with restrictive modes, rotated by size and expired by retention without a cron job.
My role
What I own on this project
Product definition, architecture, cross-implementation contract decisions, AI-agent coordination, technical review and release decisions.
I defined the product, set the shared JSONL contract across the PHP, Node.js and Go implementations, and directed AI-assisted implementation, while retaining responsibility for architecture, reviews and releases.
Keeping one event format aligned across three languages is the actual engineering work here. The JavaScript and Go implementations found security gaps in the sanitizer that the PHP version then had to close, which is why PHP is at v0.4.0 rather than v0.1.0.
Current status
Current state of the project
All three packages are published and installable. PHP is the furthest along; JavaScript and Go have just reached their first release. Each implementation ships unit, integration, CLI and multi-process concurrency tests, with CI on GitHub Actions.
Roadmap
What shipped, and what I'm not promising
Released means published and installable. Everything below that line is an intention.
-
The PHP core
Tracer and Trace, trace-ID generation and continuation, JSONL writer with date-based files and size-based shard rotation, flock coordination, sensitive-key masking, fail-safe error handling and the eventtrace show CLI.
-
Two more languages, one format
The Node.js package (ESM + CJS + types, batched async writes, bounded queue with an overflow policy) and the Go module (kernel file locks on Unix and Windows, context-aware events, reflection-based payload normalization), both writing the PHP wire format.
-
The contract closes the gaps
Building the JS and Go implementations exposed real holes in the PHP sanitizer. v0.3.0 aligned masking, marker escaping and defaults across all three; v0.4.0 added maxKeyBytes: an unbounded key was the last way a single hostile field could degrade an entire event.
-
Toward a stable v1.0
Stabilizing the API across the three implementations so the shared contract can be frozen at v1.0. No new features and no dates promised: the work is letting the interfaces settle and hold.
-
Beyond the MVP core
A richer CLI (tail, event search) and framework adapters for PSR-7, Laravel, Symfony or Express are deliberately outside the core today. If they land, they land as separate packages: the core stays small.
Install it, trace one endpoint, grep the file.
That is the whole evaluation. If the timeline it prints does not tell you something your logs weren't telling you, Traceloom is not the right tool for your system, and I would rather you found that out in ten minutes.