What's new in v0.5.7
OpenTelemetry agents collapse SQL literals to ? by default to keep PII out of trace attributes. Until now, that put perf-sentinel in a frustrating spot on every ORM stack: the standard distinct_params >= threshold rule could not see distinct values, so an ORM-induced N+1 would land in redundant_sql with the wrong remediation ("cache or deduplicate") instead of n_plus_one_sql ("batch fetch / @EntityGraph"). v0.5.7 closes the gap with a two-signal heuristic, gated by a TOML toggle, that recovers the correct classification on every sanitized SQL group without forcing operators to disable PII protection.
The heuristic activates only when every span in a (event_type, template) group has empty params and a ? placeholder in its template (the on-wire signature of an OTel-sanitized N+1). It then evaluates two independent signals: a case-insensitive, word-bounded match of the spans' instrumentation_scopes against a list of ORM markers (Spring Data, Hibernate, JPA, Micronaut Data, JDBI, R2DBC, EF Core, SQLAlchemy, Django, ActiveRecord, GORM, sqlx, Sequelize, Prisma, TypeORM, Mongoose, SeaORM, Diesel), and as a fallback the coefficient of variation of duration_us (CV > 0.5 indicates distinct row lookups, threshold empirical). The word-boundary check prevents short markers like jpa and sqlx from false-positive matching unrelated substrings (mysqlxapackage, myappjpastats). Findings reclassified by the heuristic carry a new classification_method = "sanitizer_heuristic" so consumers can spot where it fires.
After upgrading, a Spring Boot + JPA app instrumented with the default OTel Java agent (sanitizer ON, the production-realistic posture) produces N+1 findings classified as n_plus_one_sql instead of redundant_sql, with the framework-aware suggested_fix pointing at JOIN FETCH / @EntityGraph. The same applies to every ORM stack listed above. Operators who want to keep the pre-0.5.7 behavior can set [detection] sanitizer_aware_classification = "never".
Added
[detection] sanitizer_aware_classification = "auto" | "always" | "never"(default"auto"). New TOML toggle controlling the sanitizer-aware reclassification heuristic."auto"requires either the ORM scope signal or the timing variance signal,"always"reclassifies any sanitized group with>= n_plus_one_min_occurrencesspans (most aggressive),"never"disables the heuristic entirely. Unknown values warn (with the offending value sanitized: control characters replaced, length capped at 32 bytes) and fall back to"auto". Seedocs/CONFIGURATION.mdanddocs/design/04-DETECTION.md.classification_methodfield onFindingwith valuesdirect(omitted from JSON whenNone) andsanitizer_heuristic. Findings produced by the standarddistinct_params >= thresholdrule omit the field, findings reclassified by the heuristic are stampedsanitizer_heuristic. The newClassificationMethodenum is parallel toConfidence, not nested in it, so the runtime-context signal (CI vs production daemon) and the classification-method signal stay orthogonal in the API.- New
sanitizer_awaremodule incrates/sentinel-core/src/detect/exposingSanitizerAwareMode,SanitizerVerdict,looks_sanitized,has_orm_scope(allocation-free word-bounded ASCII match),collect_scopes,timing_variance_suggests_n_plus_one,classify_sanitized_sql_group. All functions are pure, with no allocation on the gate (looks_sanitized_indexed) so the heavy verdict only runs on groups that already pass the fast sanitized-shape check.
Fixed
- Sanitized N+1 SQL groups now classify as
n_plus_one_sqlinstead ofredundant_sql. Affects every ORM stack with a real OTel agent (Java JPA / Hibernate / Micronaut Data, .NET EF Core, Python SQLAlchemy / Django, Ruby ActiveRecord, Go database/sql with GORM or sqlx, Node.js Prisma / TypeORM / Sequelize, Rust Diesel / SeaORM, ...) running with the default sanitizer ON. Operators get the correct remediation (batch fetch,@EntityGraph, eager loading) instead of "cache result or deduplicate". detect_redundantnow skips templates already flagged as N+1. Takes the slice of N+1 findings already produced for the trace as a new parameter, so the same template is never double-reported as bothn_plus_one_sqlandredundant_sql(whether the N+1 came from the standard rule or from the heuristic).
Changed
- BREAKING (
perf-sentinel-core, pre-1.0 so minor-bump allowed):detect::detect_n_plus_onegains amode: SanitizerAwareModeparameter anddetect::detect_redundantgains ann_plus_one_findings: &[Finding]parameter.DetectConfigandConfiggain asanitizer_aware_classificationfield.PerTraceFindingArgsgains aclassification_methodfield. External consumers calling these directly must update accordingly. - Behavior change: users with the OpenTelemetry SQL sanitizer ON (the default posture) will see findings move from
redundant_sqlton_plus_one_sqlon upgrade, withclassification_method = sanitizer_heuristic. This is not a regression: the new classification carries the correct remediation. Set[detection] sanitizer_aware_classification = "never"to keep the pre-0.5.7 behavior. - Helm chart 0.2.10 bumps
appVersionand the default daemon image tag to0.5.7. Chart templates are unchanged.
Install
Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.7/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.7 watch --listen-address 0.0.0.0Also available on Docker Hub: robintrassard/perf-sentinel:0.5.7.
Helm (chart 0.2.10 ships 0.5.7 as its appVersion default):
helm install perf-sentinel oci://ghcr.io/robintra/charts/perf-sentinel \
--version 0.2.10 \
--namespace observability --create-namespaceVerify the binary against SHA256SUMS.txt:
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.7/SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missingFull diff: v0.5.6...v0.5.7