What's new in v0.11.2
v0.11.2 is about which endpoint a finding belongs to. Four changes to how a span's endpoint is resolved, on OTLP, Jaeger and Zipkin alike, plus one span kind that stopped counting as an outbound call. Together they move the endpoint on findings that were previously attributed to an inner route, to unknown, or to a service that never made the call, which means acknowledgment signatures and persisted baselines move with them. One consequence deserves reading before you upgrade a pipeline that gates on the waste ratio, and it has its own paragraph below. No CLI flag, no configuration key and no wire format changed.
The outermost route wins, on every format
Attribution walked to the nearest inbound route in the parent chain. On a layered stack the nearest route is the controller two frames above the DAO, or a framework's internal name, and on a hop that crossed into another service it was whatever that service happened to expose. Findings landed on an inner route, or on unknown when the walk found nothing at all.
The walk now keeps going to the outermost inbound route in the contiguous same-service chain, and stops before a caller-service boundary rather than borrowing the caller's endpoint. Anonymous resources stay local, so a span with no service.name cannot inherit a route across an emitter it has no relation to. Every format enforces the same eight-hop bound, which was previously the OTLP path's alone.
The daemon gained the other half of the problem. A trace does not arrive in one export request, and the parent that carries the route can land in a later one. Route and intermediate-parent context is now retained across split exports, keyed by trace and span id, under the existing trace LRU and TTL. Batch commands still see one input at a time and are unaffected.
A framework route name is not a path
Symfony emits http.route=app_fault_nplusonesql next to url.path=/api/fault/n-plus-one-sql. The symbolic name won, and findings were filed under a route no reader recognizes and no URL matches.
A route holding no / now yields to a usable url.path on the same span. The substitution needs the span to be the inbound side, SERVER for the span carrying the event and any non-CLIENT kind for an ancestor in the chain, so an instrumentation that never sets a span kind keeps the route name rather than picking up a path that may describe something else. A route holding a / anywhere stays authoritative, which is what protects a Django template such as api/orders/{id} from being replaced by the instantiated path beside it.
That Django template gained something too: a route that omits its leading slash now gets one, so api/orders/{id} reports as /api/orders/{id} and stops splitting from the same endpoint seen through a full URL. This is the one change that moves findings which were already attributed to the right route.
A SERVER span is not an outbound call
Legacy semantic conventions put http.url on the inbound handler span as well as on the outbound client span. perf-sentinel admitted both, so every instrumented hop was counted twice: once as the caller's real call, once as the callee reporting a call to itself. That is the same double-counting that made RPC spans CLIENT-only back in 0.9.8, and the HTTP branch had never been given the equivalent gate.
A SERVER span no longer produces an outbound HTTP call, on OTLP, on Jaeger via the span.kind tag and on Zipkin via the kind field. SQL is classified first, so a SERVER span carrying db.statement is still analyzed, and a span that sets no kind at all stays eligible for HTTP. A rejected SERVER span is not discarded either: it still supplies the inbound endpoint that the findings under it are attributed to, and it counts as not_io rather than as a missing-URL instrumentation gap, since inbound work is not a stripped outbound call.
Upgrade impact
Re-capture your acknowledgments and persisted report baselines. source_endpoint is part of a finding's signature, and three of the four changes above move it. A persisted baseline shows each moved identity once as resolved and once as new, with no application change behind it. A baseline that is a trace corpus fed to diff --before stays stable, since both sides are re-analyzed by the current binary.
Check your CI thresholds before you gate on this release. The phantom SERVER calls counted as I/O operations. Dropping them shrinks the waste-ratio denominator while no avoidable operation leaves with them, so io_waste_ratio rises on identical traffic and can cross into a worse band. In a trace of eight instrumented hops over one six-query N+1, the ratio moves from 5/14 to 5/6, high to critical, and analyze --ci fails on unchanged code with unchanged thresholds. Re-baseline [thresholds] io_waste_ratio_max against a 0.11.2 run of your own corpus first.
On a legacy-semconv fleet, some HTTP findings disappear rather than move. excessive_fanout, chatty_service, redundant_http, n_plus_one_http and slow_http raised on inbound handler spans are gone. They surface as resolved in a diff with no fix behind them, and a fresh batch analysis reports their acknowledgments as unmatched_acknowledgment. Read that warning before acting on it: when the endpoint still emitted I/O, and a surviving SQL child on the same handler is enough for that, the message says the problem looks fixed and the entry can be removed, which is the wrong conclusion here.
The daemon holds more per-trace state at the same settings. max_events_per_trace now caps three per-trace collections independently, the event ring, the retained inbound endpoint contexts and the ancestry index, so an unchanged configuration carries a larger memory envelope than v0.11.1 did. Worth a look at the daemon's memory limit before upgrading a pod that already runs close to its ceiling.
The minimum supported Rust version stays 1.97.1.
Validation
The full Rust suite passes (3152 tests), clippy is clean at -D warnings across all targets, cargo fmt --check passes, and the default and --no-default-features builds both pass.
The simulation-lab release-gate records a PASS for v0.11.2, validated against the exact upstream candidate through the RabbitMQ multistack rollout: every completed stack passed its historical 10 findings plus both messaging findings, and the combined 12-finding runner passed representative Spring and Quarkus gates before remaining exhaustive coverage moved to blocking CI.
Verifying this release
# Binary integrity via SLSA Build L3 attestation
gh attestation verify perf-sentinel-linux-amd64 \
--repo robintra/perf-sentinel
# A periodic disclosure produced by this binary
perf-sentinel verify-hash --report perf-sentinel-report.json \
--expected-identity "https://github.com/robintra/perf-sentinel/.github/workflows/release.yml@refs/tags/v0.11.2" \
--expected-issuer "https://token.actions.githubusercontent.com" \
--verify-binary ./perf-sentinel-linux-amd64gh CLI 2.49 or newer required for gh attestation verify.
Full Changelog: v0.11.1...v0.11.2