github robintra/perf-sentinel v0.9.9

latest release: chart-v0.9.9
4 hours ago

What's new in v0.9.9

v0.9.9 is a bugfix release. slow_sql now fires for services instrumented by the PHP OpenTelemetry contrib packages (Symfony with Doctrine and PDO), which split each query across spans in a way that made the duration invisible to the detector. OTLP conversion previously dropped every duration-bearing span as missing_db_statement, so a Doctrine service produced SQL events that all lasted about zero milliseconds and slow_sql could never fire, while the duplicated statement spans raised spurious redundant_sql. This release re-joins each split query at conversion time into one event carrying both the statement and the real duration. Single-layer emitters are byte-for-byte unchanged, and no CLI flag, configuration key, output schema, daemon route, or wire shape changes.

Ingestion: stitching split Doctrine and PDO query spans

The PHP contrib packages emit, per query, a SELECT orders span that carries the statement in db.query.text and ends in about zero milliseconds, and a sibling Doctrine::execute span that carries the real duration but no statement, each layered over a pdo child that repeats the same shape. The database engine (db.system.name) appears only on the pdo layer, never on the Doctrine spans. The ingest I/O filter needed a statement to convert a span, so it kept the two ~0 ms statement spans and dropped both duration-bearing execute spans as missing_db_statement. Every SQL event a Doctrine service produced therefore lasted about zero milliseconds, slow_sql could not fire whatever the threshold, and the two surviving statement spans looked like a redundant pair.

A stitch pass at OTLP conversion now re-joins each query into a single event. A statement identifies a donor span on its own, from db.statement or db.query.text, so the Doctrine layer participates even though it carries no db.system. A statement-less span whose name marks it as a query execution adopts the statement of the nearest related donor, a sibling, an ancestor, or a descendant in the same trace, and a donor can be reused so a prepared statement executed many times yields one event per execution. A statement-less span with no db.system is stitched only when it has a statement-bearing sibling, so an ORM span that wraps its own SQL child, as Ruby ActiveRecord does, does not adopt a descendant's statement. The spans merged away are counted under a new merged_db_span reason on perf_sentinel_otlp_spans_filtered_total, which the daemon zero-retention warning treats as healthy rather than an instrumentation gap.

The stitch is scoped to one export request. A slow query whose fast prepare span and slow execute span land in different exporter batches, which the slowness itself makes likely, falls back to the previous missing_db_statement behavior, so slow_sql on split-layer emitters still fires but under-reports its occurrence count under load. docs/LIMITATIONS.md documents this and the operator mitigation, widening the collector or SDK batching window past the slowest query.

Operator-visible behavior change

A service instrumented with the PHP Symfony, Doctrine, and PDO contrib packages now produces slow_sql findings where it produced none, and the spurious redundant_sql from its duplicated statement spans is gone. On such fleets the SQL event count can also drop, because the duplicate statement events collapse into one, which lowers the interaction-intensity score and the waste ratio in the correct direction. Single-layer emitters, Laravel over PDO, Django over psycopg, and Rails over ActiveRecord, are byte-for-byte unchanged. The perf_sentinel_otlp_spans_filtered_total metric gains one bounded label value, merged_db_span. No CLI flag, configuration key, output schema, daemon HTTP route, or OTLP wire shape is added or changed.

Why this is a patch and not a minor

This is a single ingest bug fix that restores intended behavior for one instrumentation shape by re-joining spans the converter previously mis-split. It admits no new I/O kind and adds no detector, unlike a capability change, and single-layer emitters produce identical output on identical input. The one new surface is an additive label value on an existing Prometheus metric, which does not break a scraper. No CLI flag, configuration default, output format, daemon route, or wire shape is added or removed. The minimum supported Rust version is unchanged.

Validation

The simulation-lab release gate passed for v0.9.9 (lab commit c43c8c4, 2026-07-12). The symfony-svc scenario, whose Doctrine and PDO instrumentation is the exact split-span shape this fix targets, now produces the slow_sql finding it never could before and reaches 10 of 10 anti-patterns, with the php_doctrine remediation tag preserved on the doctrine scope. The single-layer stacks stayed put in the same run, Laravel, Django, and Rails at 10 of 10 with slow_sql passing, confirming the sibling-donor guard leaves them untouched. CI covers the build, the full test suite (cargo test --workspace), clippy at -D warnings, cargo fmt, and both the default and --no-default-features builds.

Verifying this release

# Binary integrity via SLSA Build L3 attestation
gh attestation verify perf-sentinel-linux-amd64 \
  --owner robintra --repo 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.9.9" \
  --expected-issuer "https://token.actions.githubusercontent.com"

gh CLI 2.49 or newer required for gh attestation verify.

Full Changelog: v0.9.8...v0.9.9

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.