pipework 0.282.0 → 0.283.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 +4 -0
- package/dist/core/config/load.d.ts +3 -1
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +1 -0
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +3 -1
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +3 -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 +1 -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/pool.js +4 -4
- package/dist/data/db/pool.js.map +1 -1
- package/dist/data/db/query-watch.d.ts +8 -0
- package/dist/data/db/query-watch.d.ts.map +1 -1
- package/dist/data/db/query-watch.js +70 -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 +6 -6
- package/dist/test/plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/core/config/load.ts +4 -1
- package/src/core/config/schema.ts +4 -1
- package/src/core/config/statement-observer.ts +61 -0
- package/src/core/surface/types.ts +1 -0
- package/src/data/db/instrumentation.ts +15 -14
- package/src/data/db/pool.ts +4 -4
- package/src/data/db/query-watch.ts +72 -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 +6 -6
package/src/data/db/pool.ts
CHANGED
|
@@ -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 })
|
|
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
|
|
|
@@ -1,7 +1,9 @@
|
|
|
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'
|
|
4
5
|
import type { StatementWatch } from '../query/postgres-js/session.js'
|
|
6
|
+
import type { StatementSettledError, StatementSettledObserver } from '../../core/config/statement-observer.js'
|
|
5
7
|
|
|
6
8
|
const LABEL_SQL_CHARS = 120
|
|
7
9
|
const SAMPLE_TIMEOUT_MS = 2_000
|
|
@@ -23,13 +25,42 @@ export interface StatementWatchOptions {
|
|
|
23
25
|
readonly slowQueryMs: number
|
|
24
26
|
/** Undefined: no timer is ever created. */
|
|
25
27
|
readonly inFlightQueryMs: number | undefined
|
|
28
|
+
/** `observability.onStatementSettled`; undefined: nothing extra is tracked or fired. */
|
|
29
|
+
readonly onSettled?: StatementSettledObserver | undefined
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface StatementWatchHandle {
|
|
29
33
|
readonly watch: StatementWatch
|
|
34
|
+
/**
|
|
35
|
+
* Pass to `createDebugFn` for the same pool: the driver's debug hook is the only place that
|
|
36
|
+
* learns a statement's connection. Undefined when no `onSettled` hook is configured.
|
|
37
|
+
*/
|
|
38
|
+
readonly noteConnection: ((connectionId: number, parameters: readonly unknown[]) => void) | undefined
|
|
30
39
|
close(): Promise<void>
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
/** Rows a settled statement returned, or affected for a write: the driver's command-tag count, else the row count. */
|
|
43
|
+
function rowsOf(result: unknown): number {
|
|
44
|
+
if (typeof result !== 'object' || result === null) return 0
|
|
45
|
+
const count = (result as { count?: unknown }).count
|
|
46
|
+
if (typeof count === 'number') return count
|
|
47
|
+
return Array.isArray(result) ? result.length : 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Name, message and code only: the driver's error object carries the statement text and bind parameters. */
|
|
51
|
+
function settledError(raw: unknown): StatementSettledError {
|
|
52
|
+
// Drizzle wraps a driver failure in an error whose message is `Failed query: <sql>\nparams: <values>`: report the cause.
|
|
53
|
+
let err = raw
|
|
54
|
+
while (err instanceof DrizzleQueryError && err.cause !== undefined) err = err.cause
|
|
55
|
+
if (typeof err !== 'object' || err === null) return { name: 'Error', message: String(err) }
|
|
56
|
+
const { name, message, code } = err as { name?: unknown; message?: unknown; code?: unknown }
|
|
57
|
+
return {
|
|
58
|
+
name: typeof name === 'string' ? name : 'Error',
|
|
59
|
+
message: typeof message === 'string' ? message : '',
|
|
60
|
+
...(typeof code === 'string' ? { code } : {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
function labelFor(sqlText: string): string {
|
|
34
65
|
return currentQueryLabel() ?? sqlText.replace(/\s+/g, ' ').trim().slice(0, LABEL_SQL_CHARS)
|
|
35
66
|
}
|
|
@@ -41,7 +72,10 @@ function labelFor(sqlText: string): string {
|
|
|
41
72
|
* dedicated connection, never the statement's own and never the app pool.
|
|
42
73
|
*/
|
|
43
74
|
export function createStatementWatch(options: StatementWatchOptions): StatementWatchHandle {
|
|
44
|
-
const { database, url, slowQueryMs, inFlightQueryMs } = options
|
|
75
|
+
const { database, url, slowQueryMs, inFlightQueryMs, onSettled } = options
|
|
76
|
+
// Keyed on the parameter array the driver builds for a statement: the same array reaches the
|
|
77
|
+
// debug hook (with the connection ordinal) and sits on the pending query the session hands us.
|
|
78
|
+
const connections = onSettled ? new WeakMap<object, number>() : undefined
|
|
45
79
|
let sampler: postgres.Sql | undefined
|
|
46
80
|
let sampling = false
|
|
47
81
|
let closed = false
|
|
@@ -72,8 +106,24 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
72
106
|
return Promise.race([run, timeout]).catch(() => UNKNOWN_BACKEND).finally(() => { clearTimeout(timer) })
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
|
|
109
|
+
function fireSettled(event: Parameters<StatementSettledObserver>[0]): void {
|
|
110
|
+
if (!onSettled) return
|
|
111
|
+
const failed = (err: unknown): void => {
|
|
112
|
+
getLogger().warn({ err, database }, 'statement:settled-hook-failed')
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const out = onSettled(event)
|
|
116
|
+
if (out !== undefined && typeof (out as Promise<void>).then === 'function') void Promise.resolve(out).catch(failed)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
failed(err)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const watch: StatementWatch = async <T>(sqlText: string, run: (seen: (pending: object) => void) => Promise<T>): Promise<T> => {
|
|
76
123
|
const startedMs = Date.now()
|
|
124
|
+
let pendingQuery: object | undefined
|
|
125
|
+
let outcome: { result: unknown } | { error: unknown } | undefined
|
|
126
|
+
const seen = (pending: object): void => { pendingQuery = pending }
|
|
77
127
|
let interval: NodeJS.Timeout | undefined
|
|
78
128
|
let settled = false
|
|
79
129
|
if (inFlightQueryMs !== undefined) {
|
|
@@ -93,7 +143,12 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
93
143
|
interval.unref()
|
|
94
144
|
}
|
|
95
145
|
try {
|
|
96
|
-
|
|
146
|
+
const result = await run(seen)
|
|
147
|
+
if (onSettled) outcome = { result }
|
|
148
|
+
return result
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (onSettled) outcome = { error: err }
|
|
151
|
+
throw err
|
|
97
152
|
} finally {
|
|
98
153
|
settled = true
|
|
99
154
|
if (interval !== undefined) clearInterval(interval)
|
|
@@ -101,11 +156,25 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
101
156
|
if (slowQueryMs > 0 && durationMs > slowQueryMs) {
|
|
102
157
|
getLogger().warn({ label: labelFor(sqlText), durationMs, database }, 'query:slow')
|
|
103
158
|
}
|
|
159
|
+
if (onSettled && outcome !== undefined) {
|
|
160
|
+
const parameters = pendingQuery === undefined ? undefined : (pendingQuery as { parameters?: unknown }).parameters
|
|
161
|
+
const connectionId = typeof parameters === 'object' && parameters !== null ? connections?.get(parameters) ?? null : null
|
|
162
|
+
fireSettled({
|
|
163
|
+
label: currentQueryLabel() ?? null,
|
|
164
|
+
durationMs,
|
|
165
|
+
rows: 'result' in outcome ? rowsOf(outcome.result) : 0,
|
|
166
|
+
connectionId,
|
|
167
|
+
...('error' in outcome ? { error: settledError(outcome.error) } : {}),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
104
170
|
}
|
|
105
171
|
}
|
|
106
172
|
|
|
107
173
|
return {
|
|
108
174
|
watch,
|
|
175
|
+
noteConnection: connections
|
|
176
|
+
? (connectionId, parameters) => { connections.set(parameters, connectionId) }
|
|
177
|
+
: undefined,
|
|
109
178
|
async close() {
|
|
110
179
|
closed = true
|
|
111
180
|
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 ' +
|
|
@@ -16,10 +16,19 @@ import { type Assume, mapResultRow } from '../utils.js';
|
|
|
16
16
|
import { propagateObserved, throughErrorBoundary } from './driver-error-boundary.js';
|
|
17
17
|
|
|
18
18
|
/** Wraps one statement's execution; installed by the pool to time it and report it while it runs. */
|
|
19
|
-
export type StatementWatch = <T>(query: string, run: () => Promise<T>) => Promise<T>;
|
|
19
|
+
export type StatementWatch = <T>(query: string, run: (seen: (pending: object) => void) => Promise<T>) => Promise<T>;
|
|
20
20
|
|
|
21
|
-
function watched<T>(watch: StatementWatch | undefined, query: string, run: () => Promise<T>): Promise<T> {
|
|
22
|
-
return watch === undefined ? run() : watch(query, run);
|
|
21
|
+
function watched<T>(watch: StatementWatch | undefined, query: string, run: (seen: (pending: object) => void) => Promise<T>): Promise<T> {
|
|
22
|
+
return watch === undefined ? run(noteSeen) : watch(query, run);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function noteSeen(): void {}
|
|
26
|
+
|
|
27
|
+
/** Starts the statement and tells the watch which pending query it is, so it can learn the connection it runs on. */
|
|
28
|
+
function startUnsafe(client: Sql, query: string, params: unknown[], seen: (pending: object) => void) {
|
|
29
|
+
const pending = client.unsafe(query, params as any[]);
|
|
30
|
+
seen(pending);
|
|
31
|
+
return pending;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export class PostgresJsPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
|
|
@@ -58,8 +67,8 @@ export class PostgresJsPreparedQuery<T extends PreparedQueryConfig> extends PgPr
|
|
|
58
67
|
const { fields, queryString: query, client, joinsNotNullableMap, customResultMapper, watch } = this;
|
|
59
68
|
if (!fields && !customResultMapper) {
|
|
60
69
|
return tracer.startActiveSpan('drizzle.driver.execute', () => {
|
|
61
|
-
return throughErrorBoundary(client, () => watched(watch, query, () => this.queryWithCache(query, params, async () => {
|
|
62
|
-
return await client
|
|
70
|
+
return throughErrorBoundary(client, () => watched(watch, query, (seen) => this.queryWithCache(query, params, async () => {
|
|
71
|
+
return await startUnsafe(client, query, params, seen);
|
|
63
72
|
})));
|
|
64
73
|
});
|
|
65
74
|
}
|
|
@@ -69,8 +78,8 @@ export class PostgresJsPreparedQuery<T extends PreparedQueryConfig> extends PgPr
|
|
|
69
78
|
'drizzle.query.text': query,
|
|
70
79
|
'drizzle.query.params': JSON.stringify(params),
|
|
71
80
|
});
|
|
72
|
-
return throughErrorBoundary(client, () => watched(watch, query, () => this.queryWithCache(query, params, async () => {
|
|
73
|
-
return await client
|
|
81
|
+
return throughErrorBoundary(client, () => watched(watch, query, (seen) => this.queryWithCache(query, params, async () => {
|
|
82
|
+
return await startUnsafe(client, query, params, seen).values();
|
|
74
83
|
})));
|
|
75
84
|
});
|
|
76
85
|
|
|
@@ -95,8 +104,8 @@ export class PostgresJsPreparedQuery<T extends PreparedQueryConfig> extends PgPr
|
|
|
95
104
|
'drizzle.query.text': this.queryString,
|
|
96
105
|
'drizzle.query.params': JSON.stringify(params),
|
|
97
106
|
});
|
|
98
|
-
return throughErrorBoundary(this.client, () => watched(this.watch, this.queryString, () => this.queryWithCache(this.queryString, params, async () => {
|
|
99
|
-
return this.client
|
|
107
|
+
return throughErrorBoundary(this.client, () => watched(this.watch, this.queryString, (seen) => this.queryWithCache(this.queryString, params, async () => {
|
|
108
|
+
return startUnsafe(this.client, this.queryString, params, seen);
|
|
100
109
|
})));
|
|
101
110
|
});
|
|
102
111
|
});
|
|
@@ -165,14 +174,14 @@ export class PostgresJsSession<
|
|
|
165
174
|
|
|
166
175
|
query(query: string, params: unknown[]): Promise<RowList<Row[]>> {
|
|
167
176
|
this.logger.logQuery(query, params);
|
|
168
|
-
return throughErrorBoundary(this.client, () => watched(this.options.watch, query, () => this.client
|
|
177
|
+
return throughErrorBoundary(this.client, () => watched(this.options.watch, query, (seen) => startUnsafe(this.client, query, params, seen).values()));
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
queryObjects<T extends Row>(
|
|
172
181
|
query: string,
|
|
173
182
|
params: unknown[],
|
|
174
183
|
): Promise<RowList<T[]>> {
|
|
175
|
-
return throughErrorBoundary(this.client, () => watched(this.options.watch, query, () => this.client
|
|
184
|
+
return throughErrorBoundary(this.client, () => watched(this.options.watch, query, (seen) => startUnsafe(this.client, query, params, seen) as unknown as Promise<RowList<T[]>>));
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
override transaction<T>(
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { TimeoutOptions } from './data/db/timeouts.js'
|
|
|
21
21
|
// sanctioned door to a raw connection; keeps the WRAPPED_PACKAGES postgres ban airtight.
|
|
22
22
|
export { tap } from './data/db/pool.js'
|
|
23
23
|
export type { ManagedConnection, ConnectOptions } from './data/db/pool.js'
|
|
24
|
-
export type { StatementObserver, StatementEvent } from './core/config/statement-observer.js'
|
|
24
|
+
export type { StatementObserver, StatementEvent, StatementSettledObserver, StatementSettledEvent, StatementSettledError } from './core/config/statement-observer.js'
|
|
25
25
|
|
|
26
26
|
export type { DefinedTable, ExternalTable, DomainDefinition, DefinitionMethods, FieldDescriptor, FieldModifiers, FieldMeta, DefineOptions,
|
|
27
27
|
FactoryBuilder, FactoryOptions, TraitDefinition, FieldColumn, ProjectedColumns,
|
package/src/test/plugin.ts
CHANGED
|
@@ -127,10 +127,10 @@ async function createIsolatedConnection(
|
|
|
127
127
|
observability: ResolvedObservability,
|
|
128
128
|
withSystem: boolean,
|
|
129
129
|
): Promise<PostgresJsDatabase> {
|
|
130
|
-
const
|
|
130
|
+
const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled })
|
|
131
|
+
const debug = createDebugFn(observability, watch.noteConnection)
|
|
131
132
|
const client = postgres(testDb.url, { max: 1, onnotice: () => {}, ...(debug ? { debug } : {}) })
|
|
132
133
|
if (isObserverOnly(observability)) markObservedConnection(client)
|
|
133
|
-
const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
|
|
134
134
|
const db = drizzle(client, { statementWatch: watch.watch })
|
|
135
135
|
|
|
136
136
|
await db.execute(sql.raw('BEGIN'))
|
|
@@ -177,12 +177,12 @@ export function createSetupConnection(
|
|
|
177
177
|
testDb: TestDatabase,
|
|
178
178
|
observability?: ResolvedObservability,
|
|
179
179
|
): { drizzle: PostgresJsDatabase; close: () => Promise<void> } {
|
|
180
|
-
const debug = observability ? createDebugFn(observability) : undefined
|
|
181
|
-
const client = postgres(testDb.url, { max: 1, onnotice: () => {}, ...(debug ? { debug } : {}) })
|
|
182
|
-
if (observability && isObserverOnly(observability)) markObservedConnection(client)
|
|
183
180
|
const watch = observability
|
|
184
|
-
? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
|
|
181
|
+
? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled })
|
|
185
182
|
: undefined
|
|
183
|
+
const debug = observability ? createDebugFn(observability, watch?.noteConnection) : undefined
|
|
184
|
+
const client = postgres(testDb.url, { max: 1, onnotice: () => {}, ...(debug ? { debug } : {}) })
|
|
185
|
+
if (observability && isObserverOnly(observability)) markObservedConnection(client)
|
|
186
186
|
const db = splitLargeInserts(drizzle(client, watch ? { statementWatch: watch.watch } : {}))
|
|
187
187
|
return { drizzle: db, close: async () => { await watch?.close(); await client.end() } }
|
|
188
188
|
}
|