The Punk::Observe demo
Three Punk applications on localhost: a shop, the card processor it calls, and the receiver that watches both.
./bin/demo
That starts everything, drives real traffic through the shop, breaks the card processor in the middle, and fixes it again. Ctrl-C stops it.
./bin/demo --seconds 60 how long the traffic runs
./bin/demo --no-traffic just start the servers
./bin/traffic --seconds 120 drive an already-running demo
What is actually happening
bin/traffic --HTTP--> shop --HTTP--> cards
| |
+----- OTLP ------+
|
v
observe
Nothing in this demo fabricates telemetry. bin/traffic makes real HTTP
requests with Fetch; the shop makes a real HTTP call to the card processor;
and every span the receiver sees was produced by the instrumentation because
the two applications each contain these two lines:
otel service_name => 'shop';
plugin 'OpenTelemetry';
That is the whole instrumentation. No manual spans on the request path, no timing code, no logger threaded through the controllers.
The receiver
observe/lib/Demo/Observe.pm is the part this demo exists to show:
plugin 'Observe' => {
prefix => '/observe',
guard => \&_demo_guard, # not optional
store => 'var/store',
ingest => { prefix => '/v1' },
limits => { series => 100_000, attributes => [ ... ] },
};
Comment out guard and the application refuses to boot. That is
deliberate: an unguarded mount is every log line the shop has ever written,
served to anybody who finds the prefix.
The demo's guard lets everybody in and says so in a comment. A real one is whatever already protects your admin pages.
The incident
A third of the way through the run, bin/traffic calls:
PUT /incident?on=1
on the card processor, which then takes about three seconds to answer and fails roughly half the time. Two thirds of the way through it is switched back. You can drive that yourself:
curl -X PUT '127.0.0.1:5002/incident?on=1'
curl -X PUT '127.0.0.1:5002/incident?on=0'
The traffic driver prints a character per request - . for a 2xx, 4 for a
4xx, ! for a 5xx - so the incident is visible as it happens.
What to look at
curl -s 127.0.0.1:5001/stats
All three signals arrive - a run of thirty seconds produces roughly
5 POST /v1/logs 116 POST /v1/metrics 29 POST /v1/traces
/stats is the raw view; /observe is the rendered one. Everything on
both came out of real OTLP. During a run it shows both service names, the routes by pattern (/product/:id, never
/product/7 - one series, not one per product), and the slowest spans:
"slowest": [
{ "service": "cards", "name": "POST /authorize", "ms": 3262 },
{ "service": "cards", "name": "POST /authorize", "ms": 2897 },
...
{ "service": "cards", "name": "POST /authorize", "ms": 59 }
]
Three seconds against a healthy fifty-nine milliseconds, on one service, is
the incident - and it is on cards rather than on shop, which is the
question a service map exists to answer.
The durable copy is a write-ahead log per worker:
ls var/store/default/wal/ one log per worker
ls var/store/summary/ one summary per worker
The receiver runs a real prefork pool (--workers 4), and both directories
show it. Each worker appends to its own log with no lock; that is the storage
design, and running a single worker to make the demo tidy would have hidden
it.
The summaries are the same shape applied to the demo's own counters. A
counter in one worker's memory is a SHARD of the traffic, so refreshing
/stats would show a different quarter each time and look exactly like data
being lost. Each worker publishes its summary once per batch and the read
side merges every file it finds - so the page is stable whichever worker
answers it.
Logs from each process are in var/.
Pointing something else at it
The receiver speaks OTLP, so anything that exports OTLP works:
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:5001 \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
your-app
The endpoint is the base. An exporter appends /v1/traces,
/v1/metrics and /v1/logs itself; those paths are fixed by the
specification. Putting /v1 in the endpoint too gives /v1/v1/traces and a
404 that looks like a broken receiver.
http/json works as well, and is easier to read when something is wrong.
The UI
http://127.0.0.1:5001/observe
The screens are served by the plugin from the distribution's own templates
and stylesheet, behind the guard. /observe/traces shows the slowest spans
as a waterfall - during an incident that is POST /checkout at three
seconds, against fifty milliseconds either side of it.
The numbers come from the store, through the query engine - the same path a
question typed into /observe/explore takes. The application supplies
nothing, which is the only honest arrangement: a stats callback that made
the figures up would be showing the application's opinion of its telemetry
rather than the telemetry.
The charts are drawn in the browser from a figure the server computes, and the library that draws them is served from the mount rather than a CDN. The waterfall, the flamegraph and the service map are still laid out on the server and are correct with scripting switched off.
/observe/alerts has two rules, declared in observe/lib/Demo/Observe.pm:
error rate spans | where status = 2 | bucket(30s) count by service
slow checkout spans | where service = "shop" | bucket(30s) p95
Both are evaluated over the run by the real evaluator, so their states are
what actually happened rather than what this file says. The first is grouped
by service, which is what makes cards and shop carry their own state -
one recovering cannot resolve the other. The timeline above the table is
drawn from the transitions, so a rule that flapped looks nothing like one
that broke once.
What this demo does not show yet
A database for the configuration. Alert rules and dashboards belong in
one - sqitch/ ships the schema - and this demo has none. The rules on the
alerts screen are declared in observe/lib/Demo/Observe.pm instead.
What they are not is faked: each one is a real query, evaluated over the real spans by the real evaluator, so the states on that screen are what this run actually did. Break the card processor and watch one go pending, then firing, then back to ok.
Exponential histograms and summaries. Explicit histograms decode into
cumulative _bucket series labelled by le, plus _sum and _count, so a
percentile merges exactly from the buckets. An exponential histogram's
buckets are base-2 rather than explicit and have to be computed from a scale
first; a summary carries pre-computed quantiles that cannot be merged across
points at all. Both are skipped rather than half-decoded.
The pieces
shop/ a Punk app, instrumented. `punk new Demo::Shop`
cards/ the dependency that breaks. `punk new Demo::Cards`
observe/ mounts Punk::Plugin::Observe. `punk new Demo::Observe`
bin/demo starts all three and drives them
bin/traffic mock user traffic, on its own or driven by bin/demo
Each application was generated with punk new and then edited, so the
diff from a fresh skeleton is the interesting part.