Skip to content

Commit 916b84e

Browse files
committed
perf(database): make production query history opt-in
1 parent 8b46264 commit 916b84e

9 files changed

Lines changed: 69 additions & 25 deletions

File tree

bench/routing/README.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -90,29 +90,34 @@ papered over. `post-validate` has the same shape: Elysia uses its `t` schema,
9090
Hono a hand-written check behind its own `validator()` seam, and both are
9191
cheaper than a compiled rule set.
9292

93-
The fixture includes `query_logs` and its indexes, so stock Stacks query logging
94-
performs successful writes. Those writes are part of the database workload;
95-
the bare SQLite targets do not provide query logging. Accumulated logs are
96-
cleared before each repetition, outside warm-up and measurement, while the
97-
seeded read data stays unchanged. Results from the earlier fixture without
98-
`query_logs` measured failed logging writes and are not comparable.
93+
The fixture includes `query_logs` and its indexes, but persistent query history
94+
is disabled by default in production. This keeps the stock database scenario
95+
focused on the request and read path shared by every target. Request-scoped
96+
query tracking remains active for Stacks error diagnostics.
97+
98+
Set `DB_QUERY_LOGGING_ENABLED=true` to include durable query history in the
99+
workload. The bare SQLite targets do not provide an equivalent logger, so treat
100+
that run as an observability-cost profile rather than a like-for-like database
101+
comparison. Accumulated logs are cleared before each repetition, outside
102+
warm-up and measurement, while the seeded read data stays unchanged.
99103

100104
A former process-wide recursion guard could also skip legitimate queries while a
101105
log write was pending. Logging now suppresses only queries descended from its
102106
own write, with warm sequential and concurrent persistence checks. Database
103107
results from before that correction should be rerun because they may include
104108
less logging work.
105109

106-
Before measuring a Stacks database scenario, the parity probe clears old logs
107-
and waits for a successful log of its benchmark SELECT. Disabled or failing
108-
query logging aborts the run instead of producing a faster, incomplete workload.
109-
This check runs outside warm-up and measurement.
110+
When persistent logging is explicitly enabled, the parity probe clears old logs
111+
and waits for a successful log of its benchmark SELECT. Failing query logging
112+
then aborts the run instead of producing a faster, incomplete workload. This
113+
check runs outside warm-up and measurement.
110114

111-
With `oha` or `builtin`, each Stacks database load run also verifies that the successful
112-
persisted SELECT count matches the total warmup and measured request count. A
113-
count mismatch, request errors, or an empty measured load aborts the run. Counting
114-
happens after CPU sampling, outside the timed window. Raw warmup output and a
115-
`--persistence.json` sidecar record the request counts used by this check.
115+
With `oha` or `builtin` and persistent logging enabled, each Stacks database
116+
load run also verifies that the successful persisted SELECT count matches the
117+
total warmup and measured request count. A count mismatch, request errors, or
118+
an empty measured load aborts the run. Counting happens after CPU sampling,
119+
outside the timed window. Raw warmup output and a `--persistence.json` sidecar
120+
record the request counts used by this check.
116121

117122
Other drivers do not guarantee that their response counts include every in-flight
118123
request at the deadline. Their sidecars explicitly mark aggregate persistence
@@ -139,7 +144,7 @@ equivalent guarantees or it says plainly which profile produced the number.
139144

140145
The opt-in `stacks-wal-full` target measures a configured SQLite deployment:
141146
1000-page WAL checkpoints and `synchronous=FULL`, with the same security,
142-
validation, query logging, and cookie behavior as `stacks-warm`. It verifies
147+
validation, query-tracking, and cookie behavior as `stacks-warm`. It verifies
143148
both SQLite settings before listening. Run it explicitly:
144149

145150
```bash

bench/routing/report.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface RunMeta {
3737
warmupSeconds: number
3838
durationSeconds: number
3939
runs: number
40+
persistentQueryLogging: boolean
4041
machine: {
4142
platform: string
4243
release: string
@@ -81,6 +82,7 @@ export function renderReport(input: ReportInput): string {
8182
lines.push(`| Load generator | \`${meta.driver}\`${meta.publishable ? '' : ' (direction-only)'} |`)
8283
lines.push(`| Connections | ${meta.connections} |`)
8384
lines.push(`| Window | ${meta.warmupSeconds}s warm-up discarded, ${meta.durationSeconds}s measured, ${meta.runs} run(s), median reported |`)
85+
lines.push(`| Persistent query history | ${meta.persistentQueryLogging ? 'enabled (opt-in)' : 'disabled (production default)'} |`)
8486
lines.push(`| CPU | ${meta.machine.cpu} (${meta.machine.cores} cores) |`)
8587
lines.push(`| OS | ${meta.machine.platform} ${meta.machine.release} |`)
8688
lines.push(`| Bun | ${meta.machine.bun} |`)

bench/routing/run.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { createFixture, resetFixtureLogs } from './fixture'
2525
import { measureLoad } from './measurement'
2626
import { verifyLoadPersistence } from './persistence'
2727
import { renderReport } from './report'
28-
import { assertParity, boot, FIXTURE, headersFor, PORT, REPO_ROOT, stop } from './runtime'
28+
import { assertParity, benchmarkQueryLoggingEnabled, boot, FIXTURE, headersFor, PORT, REPO_ROOT, stop } from './runtime'
2929
import { SCENARIOS } from './scenarios'
3030
import { readSourceState } from './source'
3131
import { DEFAULT_TARGETS, TARGETS } from './targets'
@@ -149,6 +149,7 @@ async function main(): Promise<void> {
149149
warmupSeconds: opts.warmupSeconds,
150150
durationSeconds: opts.durationSeconds,
151151
runs: opts.runs,
152+
persistentQueryLogging: benchmarkQueryLoggingEnabled(),
152153
machine: {
153154
platform: platform(),
154155
release: release(),
@@ -199,7 +200,7 @@ async function main(): Promise<void> {
199200
writeFileSync(join(rawDir, `${target.id}--${scenario.id}--run${run}.txt`), result.raw)
200201
if (warmupResult)
201202
writeFileSync(join(rawDir, `${target.id}--${scenario.id}--run${run}--warmup.txt`), warmupResult.raw)
202-
if (scenario.requiresDb && target.server === 'stacks.ts') {
203+
if (benchmarkQueryLoggingEnabled() && scenario.requiresDb && target.server === 'stacks.ts') {
203204
// The fixture was cleared before warmup. Count both load windows,
204205
// after CPU sampling, so verification cannot inflate measured cost.
205206
const persistence = await verifyLoadPersistence(FIXTURE, driver, result, warmupResult)

bench/routing/runtime-version.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ describe('benchmark runtime requirements', () => {
3636
meta: {
3737
startedAt: '2026-09-05T00:00:00Z', driver: 'oha', publishable: true,
3838
connections: 50, warmupSeconds: 1, durationSeconds: 3, runs: 1,
39+
persistentQueryLogging: false,
3940
machine: { platform: 'darwin', release: 'test', cpu: 'test', cores: 1, bun: '1.3.14' },
4041
runtimeRequirement: { range: '1.4.1', matches: false },
4142
},
4243
scenarios: [], targets: [], measurements: [],
4344
})
4445
expect(report).toContain('| Bun | 1.3.14 |')
4546
expect(report).toContain('| Project Bun requirement | 1.4.1 (runtime mismatch) |')
47+
expect(report).toContain('| Persistent query history | disabled (production default) |')
4648
expect(report).toContain('Runtime mismatch: Bun 1.3.14 does not satisfy package.json engines.bun (1.4.1).')
4749
})
4850
})

bench/routing/runtime.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'bun:test'
22
import { join } from 'node:path'
3-
import { BENCH_ROOT, serverCommand, serverEnvironment } from './runtime'
3+
import { BENCH_ROOT, benchmarkQueryLoggingEnabled, serverCommand, serverEnvironment } from './runtime'
44
import { DEFAULT_TARGETS, targetById } from './targets'
55

66
describe('benchmark server isolation', () => {
@@ -26,6 +26,24 @@ describe('benchmark server isolation', () => {
2626
expect(env.BENCH_SCENARIO).toBe('static-json')
2727
})
2828

29+
it('treats persistent query logging as an explicit benchmark profile', () => {
30+
const previous = process.env.DB_QUERY_LOGGING_ENABLED
31+
try {
32+
delete process.env.DB_QUERY_LOGGING_ENABLED
33+
expect(benchmarkQueryLoggingEnabled()).toBe(false)
34+
process.env.DB_QUERY_LOGGING_ENABLED = 'true'
35+
expect(benchmarkQueryLoggingEnabled()).toBe(true)
36+
process.env.DB_QUERY_LOGGING_ENABLED = '1'
37+
expect(benchmarkQueryLoggingEnabled()).toBe(true)
38+
process.env.DB_QUERY_LOGGING_ENABLED = 'false'
39+
expect(benchmarkQueryLoggingEnabled()).toBe(false)
40+
}
41+
finally {
42+
if (previous === undefined) delete process.env.DB_QUERY_LOGGING_ENABLED
43+
else process.env.DB_QUERY_LOGGING_ENABLED = previous
44+
}
45+
})
46+
2947
it('keeps tuned settings opt-in and prevents shell settings from changing stock profiles', () => {
3048
const keys = ['BENCH_MODE', 'BENCH_SQLITE_PROFILE', 'STACKS_SECURITY_HEADERS_DISABLE', 'BENCH_SCENARIO', 'DB_CONNECTION'] as const
3149
const previous = keys.map(key => process.env[key])

bench/routing/runtime.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export const TMP = join(BENCH_ROOT, '.tmp')
1414
export const FIXTURE = join(TMP, 'bench.sqlite')
1515
export const PORT = Number(process.env.BENCH_PORT ?? 39400)
1616

17+
/** Persistent query history is opt-in for the production benchmark. */
18+
export function benchmarkQueryLoggingEnabled(): boolean {
19+
const value = process.env.DB_QUERY_LOGGING_ENABLED?.toLowerCase()
20+
return value === 'true' || value === '1'
21+
}
22+
1723
export interface BootedServer {
1824
proc: ReturnType<typeof Bun.spawn>
1925
pid: number
@@ -129,7 +135,7 @@ export function headersFor(target: Target, scenario: Scenario): Record<string, s
129135

130136
/** Require byte-identical successful responses before measuring a target. */
131137
export async function assertParity(target: Target, scenario: Scenario): Promise<void> {
132-
const requiresQueryLog = target.server === 'stacks.ts' && scenario.requiresDb
138+
const requiresQueryLog = benchmarkQueryLoggingEnabled() && target.server === 'stacks.ts' && scenario.requiresDb
133139
if (requiresQueryLog)
134140
resetFixtureLogs(FIXTURE)
135141

config/database.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,12 @@ export default {
184184
*/
185185
queryLogging: {
186186
/**
187-
* Enable query logging to database
187+
* Persist query diagnostics to the database. Development keeps the
188+
* dashboard useful out of the box; production makes the write-heavy
189+
* history an explicit opt-in. Request-scoped error diagnostics remain
190+
* available in either mode.
188191
*/
189-
enabled: env.DB_QUERY_LOGGING_ENABLED ?? true,
192+
enabled: env.DB_QUERY_LOGGING_ENABLED ?? !['production', 'prod'].includes(env.APP_ENV || ''),
190193

191194
/**
192195
* Capture caller stacks for successful fast queries too. Slow and failed

docs/guide/query-monitoring.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export default {
2424
// ... other database configs
2525

2626
queryLogging: {
27-
// Enable or disable query logging
28-
enabled: true,
27+
// Persistent history defaults on outside production and off in production
28+
enabled: process.env.DB_QUERY_LOGGING_ENABLED === 'true',
2929

3030
// Also capture caller stacks for successful fast queries
3131
captureAllTraces: false,
@@ -69,6 +69,12 @@ DB_QUERY_LOGGING_EXPLAIN_PLAN=true
6969
DB_QUERY_LOGGING_SUGGESTIONS=true
7070
```
7171

72+
Persistent query history is disabled by default in production because every
73+
application query otherwise creates an additional database write. Set
74+
`DB_QUERY_LOGGING_ENABLED=true` when durable history is worth that cost.
75+
Request-scoped query tracking for error diagnostics remains available when
76+
persistence is disabled.
77+
7278
## Dashboard Interface
7379

7480
The query monitoring dashboard provides several views:

storage/framework/defaults/ai/skills/stacks-database/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ Entity-centric API for single-table design:
258258
migrations: 'migrations',
259259
migrationLocks: 'migration_locks',
260260
queryLogging: {
261-
enabled: true,
261+
// Defaults on outside production and off in production.
262+
enabled: env.DB_QUERY_LOGGING_ENABLED ?? !['production', 'prod'].includes(env.APP_ENV || ''),
262263
captureAllTraces: false, // slow and failed queries always keep traces
263264
slowThreshold: 100, // ms
264265
retention: 7, // days

0 commit comments

Comments
 (0)