pipework 0.282.0 → 0.284.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/core/config/load.d.ts +11 -1
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +5 -0
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +5 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +11 -1
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +7 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/statement-observer.d.ts +58 -0
- package/dist/core/config/statement-observer.d.ts.map +1 -1
- package/dist/core/surface/types.d.ts +5 -0
- package/dist/core/surface/types.d.ts.map +1 -1
- package/dist/data/db/instrumentation.d.ts +6 -4
- package/dist/data/db/instrumentation.d.ts.map +1 -1
- package/dist/data/db/instrumentation.js +12 -14
- package/dist/data/db/instrumentation.js.map +1 -1
- package/dist/data/db/namespace.d.ts +1 -1
- package/dist/data/db/namespace.d.ts.map +1 -1
- package/dist/data/db/namespace.js +1 -1
- package/dist/data/db/namespace.js.map +1 -1
- package/dist/data/db/pool.js +5 -5
- package/dist/data/db/pool.js.map +1 -1
- package/dist/data/db/query-size.d.ts +18 -0
- package/dist/data/db/query-size.d.ts.map +1 -0
- package/dist/data/db/query-size.js +68 -0
- package/dist/data/db/query-size.js.map +1 -0
- package/dist/data/db/query-watch.d.ts +24 -0
- package/dist/data/db/query-watch.d.ts.map +1 -1
- package/dist/data/db/query-watch.js +101 -2
- package/dist/data/db/query-watch.js.map +1 -1
- package/dist/data/db/raw-client-warning.js +1 -1
- package/dist/data/db/raw-client-warning.js.map +1 -1
- package/dist/data/query/postgres-js/session.d.ts +1 -1
- package/dist/data/query/postgres-js/session.d.ts.map +1 -1
- package/dist/data/query/postgres-js/session.js +16 -9
- package/dist/data/query/postgres-js/session.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/test/plugin.js +7 -7
- package/dist/test/plugin.js.map +1 -1
- package/package.json +6 -2
- package/src/core/config/load.ts +16 -1
- package/src/core/config/schema.ts +8 -1
- package/src/core/config/statement-observer.ts +61 -0
- package/src/core/surface/types.ts +5 -0
- package/src/data/db/instrumentation.ts +15 -14
- package/src/data/db/namespace.ts +1 -1
- package/src/data/db/pool.ts +5 -5
- package/src/data/db/query-size.ts +63 -0
- package/src/data/db/query-watch.ts +115 -3
- package/src/data/db/raw-client-warning.ts +1 -1
- package/src/data/query/postgres-js/session.ts +20 -11
- package/src/index.ts +1 -1
- package/src/test/plugin.ts +7 -7
package/dist/test/plugin.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createTestContext as createCtx } from '../request/context/create.js';
|
|
|
6
6
|
import { runInContext } from '../request/context/store.js';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { createDebugFn, isObserverOnly } from '../data/db/instrumentation.js';
|
|
9
|
-
import { createStatementWatch } from '../data/db/query-watch.js';
|
|
9
|
+
import { createStatementWatch, largeQueryBudgets } from '../data/db/query-watch.js';
|
|
10
10
|
import { markObservedConnection } from '../data/query/postgres-js/driver-error-boundary.js';
|
|
11
11
|
import { collapseTransactionsToSavepoints } from './savepoints.js';
|
|
12
12
|
import { splitLargeInserts } from '../data/db/insert-split.js';
|
|
@@ -80,11 +80,11 @@ export async function endTestIsolation(instance, ctx) {
|
|
|
80
80
|
instance.isolation.markEnd();
|
|
81
81
|
}
|
|
82
82
|
async function createIsolatedConnection(testDb, connections, name, observability, withSystem) {
|
|
83
|
-
const
|
|
83
|
+
const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) });
|
|
84
|
+
const debug = createDebugFn(observability, watch.noteConnection);
|
|
84
85
|
const client = postgres(testDb.url, { max: 1, onnotice: () => { }, ...(debug ? { debug } : {}) });
|
|
85
86
|
if (isObserverOnly(observability))
|
|
86
87
|
markObservedConnection(client);
|
|
87
|
-
const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs });
|
|
88
88
|
const db = drizzle(client, { statementWatch: watch.watch });
|
|
89
89
|
await db.execute(sql.raw('BEGIN'));
|
|
90
90
|
const join = collapseTransactionsToSavepoints(db, client);
|
|
@@ -119,13 +119,13 @@ export async function propagateTestSessionVars(instance, testConnections, tenant
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
export function createSetupConnection(testDb, observability) {
|
|
122
|
-
const
|
|
122
|
+
const watch = observability
|
|
123
|
+
? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) })
|
|
124
|
+
: undefined;
|
|
125
|
+
const debug = observability ? createDebugFn(observability, watch?.noteConnection) : undefined;
|
|
123
126
|
const client = postgres(testDb.url, { max: 1, onnotice: () => { }, ...(debug ? { debug } : {}) });
|
|
124
127
|
if (observability && isObserverOnly(observability))
|
|
125
128
|
markObservedConnection(client);
|
|
126
|
-
const watch = observability
|
|
127
|
-
? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
|
|
128
|
-
: undefined;
|
|
129
129
|
const db = splitLargeInserts(drizzle(client, watch ? { statementWatch: watch.watch } : {}));
|
|
130
130
|
return { drizzle: db, close: async () => { await watch?.close(); await client.end(); } };
|
|
131
131
|
}
|
package/dist/test/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,OAAO,EAA2B,MAAM,oCAAoC,CAAA;AACrF,OAAO,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAA;AAE5C,OAAO,EAAE,eAAe,EAAqB,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAE,iBAAiB,IAAI,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pipework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.284.0",
|
|
4
4
|
"description": "TypeScript framework for multi-tenant SaaS applications. PostgreSQL-only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -203,6 +203,10 @@
|
|
|
203
203
|
"test:stress": "vitest run --config vitest.stress.config.ts",
|
|
204
204
|
"test:packages": "pnpm -r --filter='./packages/*' test",
|
|
205
205
|
"test:all": "pnpm test:packages && vitest run && vitest run --config vitest.isolation.config.ts && vitest run --config vitest.property.config.ts && vitest run --config vitest.stress.config.ts",
|
|
206
|
-
"mutate": "stryker run"
|
|
206
|
+
"mutate": "stryker run",
|
|
207
|
+
"rust:fmt": "cargo fmt --all --check",
|
|
208
|
+
"rust:clippy": "cargo clippy --workspace --all-targets -- -D warnings && cargo clippy -p pipework-query --all-targets --no-default-features --features all-types,audit,hashable-value,serde,thread-safe,postgres-driver -- -D warnings",
|
|
209
|
+
"rust:test": "cargo test --workspace && cargo test -p pipework-query --features all-types,hashable-value,serde",
|
|
210
|
+
"rust:check": "pnpm rust:fmt && pnpm rust:clippy && pnpm rust:test"
|
|
207
211
|
}
|
|
208
212
|
}
|
package/src/core/config/load.ts
CHANGED
|
@@ -4,16 +4,26 @@ import { resolveEnvironment, resolveEnvironmentStrict, resolveDbUrl, resolveTest
|
|
|
4
4
|
import { ConfigError } from './errors.js'
|
|
5
5
|
import { resolveEnvVars } from './env-resolve.js'
|
|
6
6
|
import { consumeConfigBaseDir } from './base-dir.js'
|
|
7
|
-
import type { StatementObserver } from './statement-observer.js'
|
|
7
|
+
import type { StatementObserver, StatementSettledObserver } from './statement-observer.js'
|
|
8
8
|
|
|
9
9
|
export interface ResolvedObservability {
|
|
10
10
|
readonly slowQueryMs: number
|
|
11
11
|
/** Interval at which a still-running statement is reported; undefined = off. */
|
|
12
12
|
readonly inFlightQueryMs?: number | undefined
|
|
13
|
+
/** Estimated bind-parameter bytes above which a statement logs `query:large`; 0 = off. */
|
|
14
|
+
readonly largeQueryParamBytes: number
|
|
15
|
+
/** SQL text length (characters) above which a statement logs `query:large`; 0 = off. */
|
|
16
|
+
readonly largeQuerySqlChars: number
|
|
17
|
+
/** UNION/INTERSECT/EXCEPT arms (a text count) above which a statement logs `query:large`; 0 = off. */
|
|
18
|
+
readonly largeQueryArms: number
|
|
19
|
+
/** Rows returned (or affected) above which a statement logs `query:large`; 0 = off. */
|
|
20
|
+
readonly largeQueryRows: number
|
|
13
21
|
readonly logAllQueries: boolean
|
|
14
22
|
readonly poolMetricsInterval: number
|
|
15
23
|
/** Caller-supplied per-statement observer; undefined when not configured. */
|
|
16
24
|
readonly onStatement: StatementObserver | undefined
|
|
25
|
+
/** Caller-supplied per-statement completion hook; undefined when not configured. */
|
|
26
|
+
readonly onStatementSettled?: StatementSettledObserver | undefined
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export interface ResolvedTrace {
|
|
@@ -244,9 +254,14 @@ function buildResolved(config: Blueprint, environment: Environment, baseDir: str
|
|
|
244
254
|
observability: {
|
|
245
255
|
slowQueryMs: config.observability?.slowQueryMs ?? 500,
|
|
246
256
|
inFlightQueryMs: config.observability?.inFlightQueryMs,
|
|
257
|
+
largeQueryParamBytes: config.observability?.largeQueryParamBytes ?? 8_388_608,
|
|
258
|
+
largeQuerySqlChars: config.observability?.largeQuerySqlChars ?? 131_072,
|
|
259
|
+
largeQueryArms: config.observability?.largeQueryArms ?? 50,
|
|
260
|
+
largeQueryRows: config.observability?.largeQueryRows ?? 100_000,
|
|
247
261
|
logAllQueries: config.observability?.logAllQueries ?? false,
|
|
248
262
|
poolMetricsInterval: config.observability?.poolMetricsInterval ?? 0,
|
|
249
263
|
onStatement: config.observability?.onStatement,
|
|
264
|
+
onStatementSettled: config.observability?.onStatementSettled,
|
|
250
265
|
},
|
|
251
266
|
trace: {
|
|
252
267
|
hotRetentionDays: config.trace?.hotRetentionDays ?? 7,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod/v4'
|
|
2
|
-
import type { StatementObserver } from './statement-observer.js'
|
|
2
|
+
import type { StatementObserver, StatementSettledObserver } from './statement-observer.js'
|
|
3
3
|
|
|
4
4
|
const SETTING_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/
|
|
5
5
|
|
|
@@ -171,6 +171,10 @@ const fastPathSchema = z.object({
|
|
|
171
171
|
|
|
172
172
|
const observabilitySchema = z.object({
|
|
173
173
|
slowQueryMs: z.number().int().positive().optional().describe('Warn when a query exceeds this duration in ms'),
|
|
174
|
+
largeQueryParamBytes: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s bind parameters total more than this many bytes, estimated (default 8388608; 0 = off)'),
|
|
175
|
+
largeQuerySqlChars: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s SQL text is longer than this many characters (default 131072; 0 = off)'),
|
|
176
|
+
largeQueryArms: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s SQL has more than this many UNION/INTERSECT/EXCEPT arms, counted in the text (default 50; 0 = off)'),
|
|
177
|
+
largeQueryRows: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement returns (or affects) more than this many rows (default 100000; 0 = off)'),
|
|
174
178
|
inFlightQueryMs: z.number().int().positive().optional().describe('While a statement is still running, log what it is waiting on every this many ms (unset = off)'),
|
|
175
179
|
logAllQueries: z.boolean().optional().describe('Log every query at debug level'),
|
|
176
180
|
poolMetricsInterval: z.number().int().nonnegative().optional().describe('Seconds between pool metric summaries (0 = disabled)'),
|
|
@@ -181,6 +185,9 @@ const observabilitySchema = z.object({
|
|
|
181
185
|
onStatement: z.custom<StatementObserver>(v => typeof v === 'function', { message: 'Expected a function' })
|
|
182
186
|
.optional()
|
|
183
187
|
.describe('Per-statement observer — fires once per statement written to the wire, including driver-issued BEGIN/SAVEPOINT/COMMIT/ROLLBACK'),
|
|
188
|
+
onStatementSettled: z.custom<StatementSettledObserver>(v => typeof v === 'function', { message: 'Expected a function' })
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('Per-statement completion hook — fires once per statement when it settles, with label, durationMs, rows, connectionId and (on failure) a sanitized error'),
|
|
184
191
|
})
|
|
185
192
|
|
|
186
193
|
const envVarDefSchema = z.discriminatedUnion('type', [
|
|
@@ -46,3 +46,64 @@ export interface StatementEvent {
|
|
|
46
46
|
* database.
|
|
47
47
|
*/
|
|
48
48
|
export type StatementObserver = (event: StatementEvent) => void
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A failed statement's error, reduced to what is safe to hand to a hook:
|
|
52
|
+
* the driver's own error objects carry the statement text and bind parameters
|
|
53
|
+
* (and, on an observed connection, make them enumerable), so the hook never
|
|
54
|
+
* sees the error object itself.
|
|
55
|
+
*/
|
|
56
|
+
export interface StatementSettledError {
|
|
57
|
+
readonly name: string
|
|
58
|
+
readonly message: string
|
|
59
|
+
/** The PostgreSQL SQLSTATE (or the driver's error code) when the error carries one. */
|
|
60
|
+
readonly code?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One statement as it settles — the completion counterpart to `StatementEvent`.
|
|
65
|
+
* Fields are only ever added to this payload, never reshaped.
|
|
66
|
+
*/
|
|
67
|
+
export interface StatementSettledEvent {
|
|
68
|
+
/**
|
|
69
|
+
* The `pipe.labelled` label in effect when the statement settled; null when
|
|
70
|
+
* none. This is the label alone: unlike `query:slow`, it never falls back to
|
|
71
|
+
* the statement text.
|
|
72
|
+
*/
|
|
73
|
+
readonly label: string | null
|
|
74
|
+
/**
|
|
75
|
+
* Wall-clock milliseconds the statement took, measured by the same timer as
|
|
76
|
+
* `query:slow` (so for one statement the two agree exactly).
|
|
77
|
+
*/
|
|
78
|
+
readonly durationMs: number
|
|
79
|
+
/**
|
|
80
|
+
* Rows the statement returned, or, for a write with no `RETURNING`, rows it
|
|
81
|
+
* affected: the command tag's count. 0 for a statement with no count
|
|
82
|
+
* (DDL, `SET`, ...) and for a failed statement.
|
|
83
|
+
*/
|
|
84
|
+
readonly rows: number
|
|
85
|
+
/**
|
|
86
|
+
* The same ordinal `StatementEvent.connectionId` carries for this statement;
|
|
87
|
+
* null when the statement never reached a connection (it failed before being
|
|
88
|
+
* written).
|
|
89
|
+
*/
|
|
90
|
+
readonly connectionId: number | null
|
|
91
|
+
/** Present exactly when the statement failed. Never carries parameters. */
|
|
92
|
+
readonly error?: StatementSettledError
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Caller-supplied completion hook. Fires once per statement that settles, on
|
|
97
|
+
* every statement pipework runs through its own query path: `pipe()`, every
|
|
98
|
+
* `pipe.*` write, `connection.drizzle`, transactions and savepoints, and the
|
|
99
|
+
* test harness's connections. It does not fire for the driver's own
|
|
100
|
+
* transaction control (`BEGIN`/`SAVEPOINT`/`COMMIT`/`ROLLBACK`), nor for the
|
|
101
|
+
* raw `ManagedConnection.client` door, nor for the migration runner's own
|
|
102
|
+
* connection; `onStatement` is the hook that sees wire truth.
|
|
103
|
+
*
|
|
104
|
+
* Pure observation, fired after the statement has settled and its result is on
|
|
105
|
+
* its way back: a hook that throws, or returns a promise that rejects, is
|
|
106
|
+
* logged (`statement:settled-hook-failed`) and never affects the statement.
|
|
107
|
+
* A returned promise is not awaited.
|
|
108
|
+
*/
|
|
109
|
+
export type StatementSettledObserver = (event: StatementSettledEvent) => void | Promise<void>
|
|
@@ -70,9 +70,14 @@ export interface SurfaceContext {
|
|
|
70
70
|
readonly observability: {
|
|
71
71
|
readonly slowQueryMs: number
|
|
72
72
|
readonly inFlightQueryMs?: number | undefined
|
|
73
|
+
readonly largeQueryParamBytes: number
|
|
74
|
+
readonly largeQuerySqlChars: number
|
|
75
|
+
readonly largeQueryArms: number
|
|
76
|
+
readonly largeQueryRows: number
|
|
73
77
|
readonly logAllQueries: boolean
|
|
74
78
|
readonly poolMetricsInterval: number
|
|
75
79
|
readonly onStatement: import('../config/statement-observer.js').StatementObserver | undefined
|
|
80
|
+
readonly onStatementSettled?: import('../config/statement-observer.js').StatementSettledObserver | undefined
|
|
76
81
|
}
|
|
77
82
|
readonly raw: unknown
|
|
78
83
|
}
|
|
@@ -5,6 +5,9 @@ import type { ResolvedObservability } from '../../core/config/load.js'
|
|
|
5
5
|
const DEADLOCK_CODE = '40P01'
|
|
6
6
|
const QUERY_TRUNCATE_LENGTH = 500
|
|
7
7
|
|
|
8
|
+
/** Records which connection a statement's parameter array was written on; see `createStatementWatch`. */
|
|
9
|
+
export type ConnectionNote = (connectionId: number, parameters: readonly unknown[]) => void
|
|
10
|
+
|
|
8
11
|
/** postgres.js's `debug` hook signature. */
|
|
9
12
|
type DebugFn = (connection: number, query: string, parameters: unknown[]) => void
|
|
10
13
|
|
|
@@ -15,30 +18,28 @@ type DebugFn = (connection: number, query: string, parameters: unknown[]) => voi
|
|
|
15
18
|
* are today, that mode having already opted into verbose errors.
|
|
16
19
|
*/
|
|
17
20
|
export function isObserverOnly(config: ResolvedObservability): boolean {
|
|
18
|
-
return config.onStatement !== undefined && !config.logAllQueries
|
|
21
|
+
return (config.onStatement !== undefined || config.onStatementSettled !== undefined) && !config.logAllQueries
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* The driver's `debug` hook for a connection, or undefined when nothing is
|
|
23
|
-
* observing. Query logging
|
|
24
|
-
*
|
|
25
|
-
* fire for every statement.
|
|
26
|
+
* observing. Query logging, the caller-supplied `onStatement` observer and the
|
|
27
|
+
* connection note behind `onStatementSettled` are independent: any one alone
|
|
28
|
+
* installs the hook, and with several configured all fire for every statement.
|
|
26
29
|
*/
|
|
27
|
-
export function createDebugFn(config: ResolvedObservability): DebugFn | undefined {
|
|
30
|
+
export function createDebugFn(config: ResolvedObservability, noteConnection?: ConnectionNote): DebugFn | undefined {
|
|
28
31
|
const logQuery = config.logAllQueries ? createQueryLogFn() : undefined
|
|
29
32
|
const observer = config.onStatement
|
|
33
|
+
// The completion hook needs the connection a statement ran on, which only this hook is told.
|
|
34
|
+
const note = config.onStatementSettled !== undefined ? noteConnection : undefined
|
|
30
35
|
|
|
31
|
-
if (!logQuery && !observer) return undefined
|
|
32
|
-
if (!observer) return logQuery
|
|
33
|
-
if (!logQuery) {
|
|
34
|
-
return (connection, query, parameters) => {
|
|
35
|
-
observer({ connectionId: connection, query, parameters })
|
|
36
|
-
}
|
|
37
|
-
}
|
|
36
|
+
if (!logQuery && !observer && !note) return undefined
|
|
37
|
+
if (!observer && !note) return logQuery
|
|
38
38
|
|
|
39
39
|
return (connection, query, parameters) => {
|
|
40
|
-
logQuery(connection, query, parameters)
|
|
41
|
-
|
|
40
|
+
logQuery?.(connection, query, parameters)
|
|
41
|
+
note?.(connection, parameters)
|
|
42
|
+
observer?.({ connectionId: connection, query, parameters })
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
package/src/data/db/namespace.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const pipe = Object.assign(getDb, {
|
|
|
38
38
|
/** Query operators and aggregates — eq, gt, lt, and, or, count, sum, etc. */
|
|
39
39
|
filter,
|
|
40
40
|
|
|
41
|
-
/** Names every statement `fn` starts, in `query:slow` and `query:in-flight` log lines: `pipe.labelled('servable-footprints', async () => await ...)`. A query builder is lazy: `fn` must await it, not return it, or it runs outside the scope. */
|
|
41
|
+
/** Names every statement `fn` starts, in `query:slow`, `query:large` and `query:in-flight` log lines: `pipe.labelled('servable-footprints', async () => await ...)`. A query builder is lazy: `fn` must await it, not return it, or it runs outside the scope. */
|
|
42
42
|
labelled,
|
|
43
43
|
|
|
44
44
|
/** Raw statement tag — for a statement pipework does not model. Interpolated values are parameters; `sql.raw` is not. Runs on the caller's connection, so inside a request it is the request transaction. Writes still go through pipe.update / insert / delete, which is what audit hooks. */
|
package/src/data/db/pool.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createDebugFn, isObserverOnly } from './instrumentation.js'
|
|
|
6
6
|
import { markObservedConnection } from '../query/postgres-js/driver-error-boundary.js'
|
|
7
7
|
import { warnRawClientIsUnsanitized } from './raw-client-warning.js'
|
|
8
8
|
import { splitLargeInserts } from './insert-split.js'
|
|
9
|
-
import { createStatementWatch } from './query-watch.js'
|
|
9
|
+
import { createStatementWatch, largeQueryBudgets } from './query-watch.js'
|
|
10
10
|
|
|
11
11
|
/** Which database role a pooled connection authenticates as. `owner` runs migrations/bootstrap and owns tables (bypasses RLS); `app` is the non-owner, non-BYPASSRLS runtime role that RLS policies enforce against; `system` is the platform role (`rls.platformRole`, via `systemUrl`), which reaches a tenant table only through a declared door. */
|
|
12
12
|
export type ConnectionRole = 'owner' | 'app' | 'system'
|
|
@@ -166,7 +166,10 @@ export function createManagedConnection(
|
|
|
166
166
|
observability?: ResolvedObservability,
|
|
167
167
|
applicationName?: string,
|
|
168
168
|
): ManagedConnection {
|
|
169
|
-
const
|
|
169
|
+
const statementWatch = observability
|
|
170
|
+
? createStatementWatch({ database: config.name, url: config.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) })
|
|
171
|
+
: undefined
|
|
172
|
+
const baseDebug = observability ? createDebugFn(observability, statementWatch?.noteConnection) : undefined
|
|
170
173
|
const slot: ProbeSlot | undefined = baseDebug ? { current: null } : undefined
|
|
171
174
|
const debug = baseDebug && slot
|
|
172
175
|
? ((connection, query, parameters) => {
|
|
@@ -187,9 +190,6 @@ export function createManagedConnection(
|
|
|
187
190
|
const observerOnly = observability !== undefined && isObserverOnly(observability)
|
|
188
191
|
if (observerOnly) markObservedConnection(client)
|
|
189
192
|
|
|
190
|
-
const statementWatch = observability
|
|
191
|
-
? createStatementWatch({ database: config.name, url: config.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
|
|
192
|
-
: undefined
|
|
193
193
|
const db = splitLargeInserts(drizzle(client, statementWatch ? { statementWatch: statementWatch.watch } : {}))
|
|
194
194
|
const canceller = createCanceller(config.url)
|
|
195
195
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Size measurements behind `query:large`. Each runs only when its budget is enabled (> 0), after the
|
|
2
|
+
// statement has settled, so a pool with every budget at 0 never calls into this file.
|
|
3
|
+
|
|
4
|
+
/** Bytes assumed for a number, bigint, boolean, Date or other fixed-width scalar parameter. */
|
|
5
|
+
const SCALAR_BYTES = 8
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimated bytes of the bind parameters of one statement. Strings count their UTF-8 length
|
|
9
|
+
* (no copy is made), Buffers/typed arrays their byte length, arrays the sum of their elements,
|
|
10
|
+
* numbers/booleans/dates a fixed 8, null/undefined 0. A plain object is a JSON value the driver
|
|
11
|
+
* will serialise: it is measured by `JSON.stringify` length, which serialises it once more than
|
|
12
|
+
* the driver does; ORM json columns arrive already stringified (counted as strings, free), so
|
|
13
|
+
* only a raw object parameter pays that. An unserialisable object counts as one scalar.
|
|
14
|
+
*/
|
|
15
|
+
export function parameterBytes(parameters: unknown): number {
|
|
16
|
+
if (!Array.isArray(parameters)) return 0
|
|
17
|
+
let total = 0
|
|
18
|
+
for (const value of parameters) total += valueBytes(value)
|
|
19
|
+
return total
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function valueBytes(value: unknown): number {
|
|
23
|
+
if (value === null || value === undefined) return 0
|
|
24
|
+
switch (typeof value) {
|
|
25
|
+
case 'string': return Buffer.byteLength(value)
|
|
26
|
+
case 'object': break
|
|
27
|
+
default: return SCALAR_BYTES
|
|
28
|
+
}
|
|
29
|
+
if (value instanceof Date) return SCALAR_BYTES
|
|
30
|
+
if (ArrayBuffer.isView(value)) return value.byteLength
|
|
31
|
+
if (value instanceof ArrayBuffer) return value.byteLength
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
let total = 0
|
|
34
|
+
for (const item of value) total += valueBytes(item)
|
|
35
|
+
return total
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.stringify(value) as string | undefined
|
|
39
|
+
return json === undefined ? 0 : json.length
|
|
40
|
+
} catch {
|
|
41
|
+
return SCALAR_BYTES
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Single left-to-right scan: a quoted literal or quoted identifier is consumed whole, so a keyword
|
|
46
|
+
// inside one is never counted; what is left are bare UNION / INTERSECT / EXCEPT words.
|
|
47
|
+
const SET_OPERATION = /'[^']*(?:''[^']*)*'|"[^"]*(?:""[^"]*)*"|\b(?:union|intersect|except)\b/gi
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Arms of the set operations in a statement's SQL text: set-operator keywords + 1, 0 when there
|
|
51
|
+
* are none. This is a text count at every nesting depth (a union inside a subquery or CTE costs the
|
|
52
|
+
* planner the same as one at the top), not a parse. Pipework sends values as bind parameters, so
|
|
53
|
+
* no keyword can appear inside a string value; a literal written with `sql.raw` is skipped by
|
|
54
|
+
* the scan. Comments and dollar-quoted bodies are not understood and can over-count.
|
|
55
|
+
*/
|
|
56
|
+
export function setOperationArms(sqlText: string): number {
|
|
57
|
+
let operators = 0
|
|
58
|
+
for (const match of sqlText.matchAll(SET_OPERATION)) {
|
|
59
|
+
const first = match[0][0]
|
|
60
|
+
if (first !== "'" && first !== '"') operators++
|
|
61
|
+
}
|
|
62
|
+
return operators === 0 ? 0 : operators + 1
|
|
63
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import postgres from 'postgres'
|
|
2
2
|
import { getLogger } from '../../core/logging/logger.js'
|
|
3
3
|
import { currentQueryLabel } from './query-label.js'
|
|
4
|
+
import { DrizzleQueryError } from '../query/errors.js'
|
|
5
|
+
import { parameterBytes, setOperationArms } from './query-size.js'
|
|
4
6
|
import type { StatementWatch } from '../query/postgres-js/session.js'
|
|
7
|
+
import type { StatementSettledError, StatementSettledObserver } from '../../core/config/statement-observer.js'
|
|
5
8
|
|
|
6
9
|
const LABEL_SQL_CHARS = 120
|
|
7
10
|
const SAMPLE_TIMEOUT_MS = 2_000
|
|
@@ -16,6 +19,24 @@ interface BackendSample {
|
|
|
16
19
|
|
|
17
20
|
const UNKNOWN_BACKEND: BackendSample = { pid: null, state: null, waitEventType: null, waitEvent: null }
|
|
18
21
|
|
|
22
|
+
/** `observability.largeQuery*` budgets; 0 disables a budget and nothing is measured for it. */
|
|
23
|
+
export interface LargeQueryBudgets {
|
|
24
|
+
readonly parameterBytes: number
|
|
25
|
+
readonly sqlChars: number
|
|
26
|
+
readonly arms: number
|
|
27
|
+
readonly rows: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The budgets of a resolved `observability` section. */
|
|
31
|
+
export function largeQueryBudgets(o: {
|
|
32
|
+
readonly largeQueryParamBytes: number
|
|
33
|
+
readonly largeQuerySqlChars: number
|
|
34
|
+
readonly largeQueryArms: number
|
|
35
|
+
readonly largeQueryRows: number
|
|
36
|
+
}): LargeQueryBudgets {
|
|
37
|
+
return { parameterBytes: o.largeQueryParamBytes, sqlChars: o.largeQuerySqlChars, arms: o.largeQueryArms, rows: o.largeQueryRows }
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/** What a pool's watch needs: where it logs, how slow is slow, and how to reach the server for a wait-event sample. */
|
|
20
41
|
export interface StatementWatchOptions {
|
|
21
42
|
readonly database: string
|
|
@@ -23,13 +44,44 @@ export interface StatementWatchOptions {
|
|
|
23
44
|
readonly slowQueryMs: number
|
|
24
45
|
/** Undefined: no timer is ever created. */
|
|
25
46
|
readonly inFlightQueryMs: number | undefined
|
|
47
|
+
/** `observability.onStatementSettled`; undefined: nothing extra is tracked or fired. */
|
|
48
|
+
readonly onSettled?: StatementSettledObserver | undefined
|
|
49
|
+
/** Undefined: no `query:large` check. */
|
|
50
|
+
readonly budgets?: LargeQueryBudgets | undefined
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
export interface StatementWatchHandle {
|
|
29
54
|
readonly watch: StatementWatch
|
|
55
|
+
/**
|
|
56
|
+
* Pass to `createDebugFn` for the same pool: the driver's debug hook is the only place that
|
|
57
|
+
* learns a statement's connection. Undefined when no `onSettled` hook is configured.
|
|
58
|
+
*/
|
|
59
|
+
readonly noteConnection: ((connectionId: number, parameters: readonly unknown[]) => void) | undefined
|
|
30
60
|
close(): Promise<void>
|
|
31
61
|
}
|
|
32
62
|
|
|
63
|
+
/** Rows a settled statement returned, or affected for a write: the driver's command-tag count, else the row count. */
|
|
64
|
+
function rowsOf(result: unknown): number {
|
|
65
|
+
if (typeof result !== 'object' || result === null) return 0
|
|
66
|
+
const count = (result as { count?: unknown }).count
|
|
67
|
+
if (typeof count === 'number') return count
|
|
68
|
+
return Array.isArray(result) ? result.length : 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Name, message and code only: the driver's error object carries the statement text and bind parameters. */
|
|
72
|
+
function settledError(raw: unknown): StatementSettledError {
|
|
73
|
+
// Drizzle wraps a driver failure in an error whose message is `Failed query: <sql>\nparams: <values>`: report the cause.
|
|
74
|
+
let err = raw
|
|
75
|
+
while (err instanceof DrizzleQueryError && err.cause !== undefined) err = err.cause
|
|
76
|
+
if (typeof err !== 'object' || err === null) return { name: 'Error', message: String(err) }
|
|
77
|
+
const { name, message, code } = err as { name?: unknown; message?: unknown; code?: unknown }
|
|
78
|
+
return {
|
|
79
|
+
name: typeof name === 'string' ? name : 'Error',
|
|
80
|
+
message: typeof message === 'string' ? message : '',
|
|
81
|
+
...(typeof code === 'string' ? { code } : {}),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
33
85
|
function labelFor(sqlText: string): string {
|
|
34
86
|
return currentQueryLabel() ?? sqlText.replace(/\s+/g, ' ').trim().slice(0, LABEL_SQL_CHARS)
|
|
35
87
|
}
|
|
@@ -41,7 +93,12 @@ function labelFor(sqlText: string): string {
|
|
|
41
93
|
* dedicated connection, never the statement's own and never the app pool.
|
|
42
94
|
*/
|
|
43
95
|
export function createStatementWatch(options: StatementWatchOptions): StatementWatchHandle {
|
|
44
|
-
const { database, url, slowQueryMs, inFlightQueryMs } = options
|
|
96
|
+
const { database, url, slowQueryMs, inFlightQueryMs, onSettled, budgets } = options
|
|
97
|
+
const watchesSize = budgets !== undefined && (budgets.parameterBytes > 0 || budgets.sqlChars > 0 || budgets.arms > 0 || budgets.rows > 0)
|
|
98
|
+
const needsOutcome = onSettled !== undefined || (budgets?.rows ?? 0) > 0
|
|
99
|
+
// Keyed on the parameter array the driver builds for a statement: the same array reaches the
|
|
100
|
+
// debug hook (with the connection ordinal) and sits on the pending query the session hands us.
|
|
101
|
+
const connections = onSettled ? new WeakMap<object, number>() : undefined
|
|
45
102
|
let sampler: postgres.Sql | undefined
|
|
46
103
|
let sampling = false
|
|
47
104
|
let closed = false
|
|
@@ -72,8 +129,43 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
72
129
|
return Promise.race([run, timeout]).catch(() => UNKNOWN_BACKEND).finally(() => { clearTimeout(timer) })
|
|
73
130
|
}
|
|
74
131
|
|
|
75
|
-
|
|
132
|
+
function fireSettled(event: Parameters<StatementSettledObserver>[0]): void {
|
|
133
|
+
if (!onSettled) return
|
|
134
|
+
const failed = (err: unknown): void => {
|
|
135
|
+
getLogger().warn({ err, database }, 'statement:settled-hook-failed')
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const out = onSettled(event)
|
|
139
|
+
if (out !== undefined && typeof (out as Promise<void>).then === 'function') void Promise.resolve(out).catch(failed)
|
|
140
|
+
} catch (err) {
|
|
141
|
+
failed(err)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One `query:large` per statement over any enabled budget, carrying every enabled measurement. Never throws. */
|
|
146
|
+
function warnIfLarge(sqlText: string, parameters: unknown, outcome: { result: unknown } | { error: unknown } | undefined, b: LargeQueryBudgets): void {
|
|
147
|
+
try {
|
|
148
|
+
const measured: Record<string, number> = {}
|
|
149
|
+
const over: string[] = []
|
|
150
|
+
const check = (name: string, budget: number, measure: () => number): void => {
|
|
151
|
+
if (budget <= 0) return
|
|
152
|
+
const value = measure()
|
|
153
|
+
measured[name] = value
|
|
154
|
+
if (value > budget) over.push(name)
|
|
155
|
+
}
|
|
156
|
+
check('parameterBytes', b.parameterBytes, () => parameterBytes(parameters))
|
|
157
|
+
check('sqlChars', b.sqlChars, () => sqlText.length)
|
|
158
|
+
check('arms', b.arms, () => setOperationArms(sqlText))
|
|
159
|
+
check('rows', b.rows, () => (outcome !== undefined && 'result' in outcome ? rowsOf(outcome.result) : 0))
|
|
160
|
+
if (over.length > 0) getLogger().warn({ label: labelFor(sqlText), database, over, ...measured }, 'query:large')
|
|
161
|
+
} catch { /* a measurement must never affect the statement */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const watch: StatementWatch = async <T>(sqlText: string, run: (seen: (pending: object) => void) => Promise<T>): Promise<T> => {
|
|
76
165
|
const startedMs = Date.now()
|
|
166
|
+
let pendingQuery: object | undefined
|
|
167
|
+
let outcome: { result: unknown } | { error: unknown } | undefined
|
|
168
|
+
const seen = (pending: object): void => { pendingQuery = pending }
|
|
77
169
|
let interval: NodeJS.Timeout | undefined
|
|
78
170
|
let settled = false
|
|
79
171
|
if (inFlightQueryMs !== undefined) {
|
|
@@ -93,7 +185,12 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
93
185
|
interval.unref()
|
|
94
186
|
}
|
|
95
187
|
try {
|
|
96
|
-
|
|
188
|
+
const result = await run(seen)
|
|
189
|
+
if (needsOutcome) outcome = { result }
|
|
190
|
+
return result
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (needsOutcome) outcome = { error: err }
|
|
193
|
+
throw err
|
|
97
194
|
} finally {
|
|
98
195
|
settled = true
|
|
99
196
|
if (interval !== undefined) clearInterval(interval)
|
|
@@ -101,11 +198,26 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
101
198
|
if (slowQueryMs > 0 && durationMs > slowQueryMs) {
|
|
102
199
|
getLogger().warn({ label: labelFor(sqlText), durationMs, database }, 'query:slow')
|
|
103
200
|
}
|
|
201
|
+
const parameters = pendingQuery === undefined ? undefined : (pendingQuery as { parameters?: unknown }).parameters
|
|
202
|
+
if (watchesSize) warnIfLarge(sqlText, parameters, outcome, budgets)
|
|
203
|
+
if (onSettled && outcome !== undefined) {
|
|
204
|
+
const connectionId = typeof parameters === 'object' && parameters !== null ? connections?.get(parameters) ?? null : null
|
|
205
|
+
fireSettled({
|
|
206
|
+
label: currentQueryLabel() ?? null,
|
|
207
|
+
durationMs,
|
|
208
|
+
rows: 'result' in outcome ? rowsOf(outcome.result) : 0,
|
|
209
|
+
connectionId,
|
|
210
|
+
...('error' in outcome ? { error: settledError(outcome.error) } : {}),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
104
213
|
}
|
|
105
214
|
}
|
|
106
215
|
|
|
107
216
|
return {
|
|
108
217
|
watch,
|
|
218
|
+
noteConnection: connections
|
|
219
|
+
? (connectionId, parameters) => { connections.set(parameters, connectionId) }
|
|
220
|
+
: undefined,
|
|
109
221
|
async close() {
|
|
110
222
|
closed = true
|
|
111
223
|
const client = sampler
|
|
@@ -23,7 +23,7 @@ export function warnRawClientIsUnsanitized(database: string): void {
|
|
|
23
23
|
|
|
24
24
|
getBaseLogger().warn(
|
|
25
25
|
{ database },
|
|
26
|
-
'[pipework] raw .client read on a connection with observability.onStatement configured. ' +
|
|
26
|
+
'[pipework] raw .client read on a connection with observability.onStatement or onStatementSettled configured. ' +
|
|
27
27
|
'Queries issued through .client bypass pipework\'s driver error boundary, so their errors ' +
|
|
28
28
|
'serialize bind parameters (postgres.js marks query/parameters/args enumerable whenever its ' +
|
|
29
29
|
'debug hook is installed). Use connection.drizzle — or pipe() inside a managed context — to ' +
|