v0.7.7
What's new in v0.7.7
v0.7.7 fixes an N+1 SQL detection gap on non-Java stacks. The sanitizer-aware classifier and the SQL normalizer both assumed JDBC-style ? placeholders, so Go (pgx $1), Python (psycopg %s, SQLAlchemy :name), .NET (@param), and any stack that sends pre-parameterized SQL to the wire were silently classified as redundant_sql instead of n_plus_one_sql. The release also adds framework-aware fix recommendations for Go and Node.js/TypeScript (96 entries, was 70 in v0.7.6) and comprehensive per-language instrumentation documentation. Lab validation against 11 multistack services reached 103/110 findings (94%), up from 78/110 (71%) on v0.7.6. No config breaking change, no CLI surface change, no daemon wire protocol change, no report JSON schema change.
Sanitizer-aware detection extended to non-Java stacks
template_has_placeholder now recognizes five placeholder styles instead of one:
| Style | Stack | Example |
|---|---|---|
? |
JDBC (Java, Kotlin) | SELECT * FROM users WHERE id = ? |
$? |
PostgreSQL native (Go pgx, Rust sqlx) | SELECT * FROM users WHERE id = $1 |
%s |
Python DB-API (psycopg, MySQLdb) | SELECT * FROM users WHERE id = %s |
@param |
.NET (Npgsql, SqlClient) | SELECT * FROM users WHERE id = @userId |
:name |
Oracle, SQLAlchemy named | SELECT * FROM users WHERE id = :userId |
Guard rules prevent false positives on look-alikes: @@ROWCOUNT (SQL Server system variables) is excluded from the @param match, :: (PostgreSQL type casts) is excluded from :name, and the : guard requires an alphabetic character after the colon to avoid matching array slices like a[1:3].
SQL normalizer: PostgreSQL $N positional parameters
The SQL normalizer previously treated $1, $2, etc. as dollar-quoted string delimiters instead of positional parameters. This caused Go (pgx) and Python (asyncpg) traces to retain their original $1, $2 literals in the template, preventing the correlator from grouping identical queries that differ only in parameter indices. $N sequences are now normalized to $? with empty extracted params, consistent with how ? is handled for JDBC.
Strict mode: sequential-siblings signal for bare-driver stacks
sanitizer_aware_classification = "strict" was failing to reclassify n+1 SQL on bare-driver stacks (Vert.x reactive PG client, pgx, asyncpg, sqlx, Prisma queryRaw) because the strict path required an ORM scope marker that these stacks never emit. Strict now accepts a sequential-siblings signal as a substitute: if all spans in the group share one parent and chain end_us <= start_us after sort by start time, the group qualifies without an ORM marker, provided high timing variance is also present. The sequentiality check is evaluated in microseconds (start_ms * 1000 + duration_us), fixing an integer-division bug where the previous duration_us / 1000 truncated sub-millisecond durations to zero and let truly concurrent spans pass the gate. The same microsecond fix applies to the serialized-calls and pool-saturation detectors.
Strict mode: cache-warm ORM signal
Strict mode now reclassifies sanitized SQL groups that carry an ORM scope marker AND high occurrence count (>= 3 x n_plus_one_threshold, default 15) even when per-span timing variance is low. This covers the cache-warm trap observed on EF Core + Npgsql and Hibernate L2 cache, where a real n+1 lookup-by-PK produces tight per-span timings because rows stay in the database shared buffers. Legacy polling loops below the threshold (typical 5-10 calls per request) stay classified as redundant_sql to preserve precision. Auto mode is unchanged (orm || variance only, no high_occurrence).
Framework-aware fix recommendations for Go and Node.js/TypeScript
The suggested_fix system gains two new language ecosystems:
- Go:
go_gorm(scope substringgorm) andgo_genericfallback. CoversPreload/Joinsfor n+1,pgx.NamedArgs+ANY($1::int[])for redundant queries,errgroupfor fanout, connection pool tuning for saturation. - Node.js / TypeScript:
node_prisma(scope substringprisma) andnode_genericfallback. Covers Prismainclude/findMany+where: { id: { in } }for n+1,Promise.allfor fanout,pgBouncerextension for pool saturation.
Language detection uses scope prefixes (github.com/ for Go, @opentelemetry/instrumentation- / @prisma/ / @nestjs/ for JavaScript) and file extensions (.go, .js, .jsx, .ts, .tsx, .mjs, .mts, .cjs, .cts). Microsoft.EntityFrameworkCore detection uses exact match or . prefix boundary to avoid matching unrelated scopes that happen to start with the same string.
Total: 96 FIXES entries across 6 languages (Java, C#, Python, Rust, Go, JavaScript) and 19 framework tags. Was 70 entries in v0.7.6.
Per-span diagnostic timing stats
Findings now carry optional timing diagnostics on the pattern field: span_duration_us_p50 (median, microseconds), span_duration_us_p99 (P99), span_duration_cv_x1000 (coefficient of variation scaled by 1000, e.g. 523 = CV 0.523). Populated by the n+1 and slow detectors. Not used in any detection verdict or GreenOps scoring, exposed purely for downstream consumers (operator dashboards, lab validators) to profile cache-warm patterns without daemon-log access. Omitted from JSON when not populated (skip_serializing_if).
Operator-visible behavior change
Bare-driver workloads running under sanitizer_aware_classification = "strict" may see some groups previously reported as redundant_sql (Warning at >= 5 occurrences) now reported as n_plus_one_sql (Critical at >= 10 occurrences). CI gates wired on critical-only count may flag groups that were previously only at warning level. GreenOps scoring (IIS, avoidable_io_ops, io_waste_ratio, carbon attribution) is unchanged: both finding types contribute identically.
Recommended upgrade path for operators with quality_gate rules:
- Run
perf-sentinel analyzeonce on a representative trace sample with 0.7.7 and inspect the per-trace finding counts vs the 0.7.6 baseline. - If new
n_plus_one_sqlCritical findings appear on bare-driver services that were previously surfaced asredundant_sqlWarnings, either (a) raise the correspondingn_plus_one_sql_critical_maxthreshold to absorb the baseline shift, or (b) treat the upgrade as the expected moment to address those n+1 patterns at source. The findings are the same defects under a sharper label, not new defects. - The
serialized_callsandpool_saturationdetectors now correctly fire on sub-millisecond sequential / concurrent SQL bursts that were previously masked by an integer-division bug in the timing math. Same recommendation: re-baseline the corresponding*_critical_maxthresholds or fix the patterns before tightening the gate.
Documentation
docs/INSTRUMENTATION.mdand its FR mirror: four new per-language sections covering Go (otelhttp + otelpgx), Python Django (psycopg), Python FastAPI (SQLAlchemy + asyncpg), and Node.js (Nest.js + Prisma). New "SQL placeholder styles and detection" reference section with a five-row table mapping each placeholder style to its drivers and OTel SDK. R2DBC/WebFlux note added to the Java Agent section. MySQL/MariaDB drivers added to the placeholder table. Library versions from the lab: Java Agent v2.27, Quarkus OTel 3.33, .NET SDK 1.15, Go OTel 1.43, Python OTel 1.42, Node OTel 0.218, Rust tracing-opentelemetry 0.33.docs/CONFIGURATION.mdand its FR mirror: thesanitizer_aware_classificationtable row and introduction paragraph now list all five placeholder styles with their stacks.docs/design/04-DETECTION.mdand its FR mirror: sanitizer-aware section updated to documenttemplate_has_placeholder, the five placeholder styles, and the@@/::/:digitexclusion guards.docs/00-INDEX.mdanddocs/FR/00-INDEX-FR.md: new root-level documentation indexes grouping 21 documents into five sections (getting started, reference, features, operations, supply chain), with sub-directory pointers. The design indexes (design/00-INDEX.md,FR/design/00-INDEX-FR.md) now cross-reference the new root indexes instead of listing individual documents.
Tests
The diff adds tests across the new code paths:
sanitizer_aware.rs(28 tests): five-style placeholder recognition,@@exclusion,::exclusion,:digitexclusion, strict sequential-siblings, strict cache-warm ORM + high occurrence, auto mode precision guard (no high_occurrence), bare-driver branch coverage.suggestions.rs(91 tests): Go and Node.js framework detection, scope prefix matching (github.com/,@opentelemetry/instrumentation-,@prisma/,@nestjs/), filepath extension detection (.go,.js,.jsx,.ts,.tsx,.mjs,.mts,.cjs,.cts),Microsoft.EntityFrameworkCoreboundary guard, no-filepath fallback array, cardinality + anchor + URL allowlist guards, duplicate key detection.normalize/sql.rs(50 tests): PostgreSQL$Npositional parameter normalization,$?template output, empty params for$Nsequences.n_plus_one.rs(41 tests): per-span timing stats population, microsecond sequentiality evaluation, high-occurrence cache-warm signal.e2e.rs: lab-derived fixtures exercising Go pgx, Python asyncpg, .NET Npgsql, and Prisma queryRaw traces through the full pipeline.
Why this is a patch and not a minor
The detection and normalization fixes change the classification of some findings (from redundant_sql to n_plus_one_sql on bare-driver stacks), but no config key is added, removed, or renamed. The Go/Node.js suggestion entries are additive (new framework_tag values go_gorm, go_generic, node_prisma, node_generic on the suggested_fix JSON field). Per-span timing stats are additive optional fields on pattern, omitted when not populated. The CLI subcommand surface, daemon HTTP routes, OTLP wire protocol, and report JSON schema are byte-for-byte identical to v0.7.6. The co2.model enum, Prometheus metric names, and label sets are unchanged.
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.7.7" \
--expected-issuer "https://token.actions.githubusercontent.com"gh CLI 2.49 or newer required for gh attestation verify (unchanged from v0.7.2).
Full Changelog: v0.7.6...v0.7.7