v0.5.8
What's new in v0.5.8
v0.5.7 introduced the sanitizer-aware reclassification heuristic with three modes (Auto, Always, Never) and an OR rule across the two signals (ORM scope marker, timing variance). On Spring Data and EF Core stacks, the ORM scope is present on practically every JDBC/ADO span, so under Auto the OR rule fires on every sanitized SQL group whose ORM marker is in the chain. That was the right default for catching N+1 patterns hidden by the agent sanitizer, but it also meant operators lost the ability to surface legitimate redundant_sql findings on truly repeated identical queries (cache-warming loops, polling repositories, unmemoized findById(sameId) in legacy code) since those would silently flip to n_plus_one_sql with the wrong remediation.
v0.5.8 adds a fourth Strict mode that requires both signals to fire conjointly (AND): the ORM scope must be present and the per-span timing variance must clear the empirical CV > 0.5 threshold. Default stays Auto, so production users on Spring Data, EF Core and similar stacks see no behavior change on upgrade. Operators who want to recover redundant_sql precision on cached identical queries opt in with [detection] sanitizer_aware_classification = "strict" in .perf-sentinel.toml.
The trade-off is explicit: Auto favors recall (catches all ORM-induced N+1), Strict favors precision (preserves redundant_sql findings on cached identical queries). Findings reclassified under Strict carry the same classification_method = "sanitizer_heuristic" JSON marker as findings reclassified under Auto or Always, so downstream JSON consumers and inspect TUI rendering behave identically. GreenOps aggregates (green_summary.avoidable_io_ops, IIS, waste ratio, top_offenders ranking) are preserved across modes because both n_plus_one_sql and redundant_sql weight identically into the avoidable-I/O bucket and the score::compute dedup key collapses any double-counting risk.
Added
[detection] sanitizer_aware_classification = "strict"(4th value, opt-in). Reclassifies a sanitized SQL group asn_plus_one_sqlonly when the ORM scope marker AND the timing variance signal fire conjointly. Recommended on environments where actionableredundant_sqlfindings are valuable signal that should not be silently absorbed inton_plus_one_sql. Seedocs/CONFIGURATION.mdanddocs/design/04-DETECTION.mdfor the full recall-vs-precision dial across the four modes.SanitizerAwareMode::Strictenum variant andclassify_sanitized_sql_group_strictpublic function incrates/sentinel-core/src/detect/sanitizer_aware.rs. The new function takes the same(spans, scopes)arguments asclassify_sanitized_sql_groupand returnsLikelyNPlusOneonly whenhas_orm_scope && timing_variance_suggests_n_plus_one. Short-circuits on the cheap ORM-scope check, so on non-ORM stacksStrictis strictly cheaper thanAuto(the variance computation is skipped entirely).
Changed
- BREAKING (
perf-sentinel-core, pre-1.0 so minor-bump allowed):classify_sanitized_sql_group_indexed(the hot-path entry called fromdetect_n_plus_one) now takes amode: SanitizerAwareModeparameter and dispatches between the OR-logic (classify_sanitized_sql_group) and the AND-logic (classify_sanitized_sql_group_strict) based on the mode. External consumers calling this directly must pass the mode explicitly. Alwaysmode now short-circuits indetect_n_plus_one::classify_groupbefore the verdict computation. Pure cleanup, no observable behavior change: the verdict was already ignored by the emit gate underAlways, so runninghas_orm_scopeandtiming_variance_suggests_n_plus_onewas wasted allocation. On traces with many sanitized SQL groups underAlways, this trims aVec<&NormalizedEvent>allocation and acollect_scopesVec<String>clone per group.classify_sanitized_sql_group_indexeddispatch is now exhaustive (no_wildcard):Strictmatches one arm,Auto | Always | Nevermatches the other. A future fifth variant onSanitizerAwareModewill fail to compile here rather than silently fall through to the OR logic.classify_sanitized_sql_group(the OR-logic public entry) is now a single booleanORinstead of two sequentialifreturns. Behavior unchanged.
Behavior
- Default behavior unchanged:
Autostill emits on either signal. Strictdoes NOT changegreen_summaryaggregates: bothn_plus_one_sqlandredundant_sqlweight identically intoavoidable_io_ops, and thescore::computededup key is(trace_id, template, source_endpoint). Only the per-findingtypelabel and thesuggestiontext differ between modes.- Under
Strict, the timing-variance threshold becomes load-bearing: it is the only gate that lets a sanitized group reachLikelyNPlusOneonce the ORM scope check has passed. Real ORM-induced N+1 against a fully warm row cache (e.g. 100 lookups by primary key with all rows inshared_buffers) can cluster within ±10% (CV ~ 0.1) and stay silent underStrict. The 0.5 threshold is preserved across modes pending empirical validation in the simulation lab. If lab traffic shows the threshold to be too restrictive underStrict, the right follow-up is exposing a[detection] sanitizer_aware_min_cvknob rather than picking a new global default.
Install
Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.8/perf-sentinel-linux-amd64
chmod +x perf-sentinel-linux-amd64
sudo mv perf-sentinel-linux-amd64 /usr/local/bin/perf-sentinelLinux binaries are statically linked against musl and run on any distribution (Alpine, Debian, RHEL, Ubuntu any version) regardless of glibc version, and inside FROM scratch images.
From crates.io:
cargo install perf-sentinelDocker:
docker run --rm -p 4317:4317 -p 4318:4318 \
ghcr.io/robintra/perf-sentinel:0.5.8 watch --listen-address 0.0.0.0Also available on Docker Hub: robintrassard/perf-sentinel:0.5.8.
Helm (chart 0.2.11 ships 0.5.8 as its appVersion default):
helm install perf-sentinel oci://ghcr.io/robintra/charts/perf-sentinel \
--version 0.2.11 \
--namespace observability --create-namespaceVerify the binary against SHA256SUMS.txt:
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.8/SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missingFull diff: v0.5.7...v0.5.8