github robintra/perf-sentinel v0.7.7

latest releases: chart-v0.9.11, v0.9.11, chart-v0.9.10...
one month ago

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 substring gorm) and go_generic fallback. Covers Preload / Joins for n+1, pgx.NamedArgs + ANY($1::int[]) for redundant queries, errgroup for fanout, connection pool tuning for saturation.
  • Node.js / TypeScript: node_prisma (scope substring prisma) and node_generic fallback. Covers Prisma include / findMany + where: { id: { in } } for n+1, Promise.all for fanout, pgBouncer extension 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:

  1. Run perf-sentinel analyze once on a representative trace sample with 0.7.7 and inspect the per-trace finding counts vs the 0.7.6 baseline.
  2. If new n_plus_one_sql Critical findings appear on bare-driver services that were previously surfaced as redundant_sql Warnings, either (a) raise the corresponding n_plus_one_sql_critical_max threshold 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.
  3. The serialized_calls and pool_saturation detectors 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_max thresholds or fix the patterns before tightening the gate.

Documentation

  • docs/INSTRUMENTATION.md and 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.md and its FR mirror: the sanitizer_aware_classification table row and introduction paragraph now list all five placeholder styles with their stacks.
  • docs/design/04-DETECTION.md and its FR mirror: sanitizer-aware section updated to document template_has_placeholder, the five placeholder styles, and the @@/::/:digit exclusion guards.
  • docs/00-INDEX.md and docs/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, :digit exclusion, 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.EntityFrameworkCore boundary guard, no-filepath fallback array, cardinality + anchor + URL allowlist guards, duplicate key detection.
  • normalize/sql.rs (50 tests): PostgreSQL $N positional parameter normalization, $? template output, empty params for $N sequences.
  • 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.

Helm chart

charts/perf-sentinel 0.2.42 to 0.2.43, appVersion 0.7.6 to 0.7.7.

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

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.