@constructive-io/graphql-server 4.10.0 → 4.11.1

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.
Files changed (37) hide show
  1. package/README.md +28 -0
  2. package/diagnostics/debug-db-snapshot.d.ts +19 -0
  3. package/diagnostics/debug-db-snapshot.js +196 -0
  4. package/diagnostics/debug-memory-snapshot.d.ts +53 -0
  5. package/diagnostics/debug-memory-snapshot.js +77 -0
  6. package/diagnostics/debug-sampler.d.ts +5 -0
  7. package/diagnostics/debug-sampler.js +195 -0
  8. package/diagnostics/observability.d.ts +6 -0
  9. package/diagnostics/observability.js +62 -0
  10. package/esm/diagnostics/debug-db-snapshot.js +191 -0
  11. package/esm/diagnostics/debug-memory-snapshot.js +70 -0
  12. package/esm/diagnostics/debug-sampler.js +188 -0
  13. package/esm/diagnostics/observability.js +53 -0
  14. package/esm/middleware/graphile.js +8 -1
  15. package/esm/middleware/observability/debug-db.js +23 -0
  16. package/esm/middleware/observability/debug-memory.js +8 -0
  17. package/esm/middleware/observability/graphile-build-stats.js +165 -0
  18. package/esm/middleware/observability/guard.js +14 -0
  19. package/esm/middleware/observability/request-logger.js +42 -0
  20. package/esm/server.js +37 -25
  21. package/middleware/graphile.js +8 -1
  22. package/middleware/observability/debug-db.d.ts +3 -0
  23. package/middleware/observability/debug-db.js +27 -0
  24. package/middleware/observability/debug-memory.d.ts +2 -0
  25. package/middleware/observability/debug-memory.js +12 -0
  26. package/middleware/observability/graphile-build-stats.d.ts +45 -0
  27. package/middleware/observability/graphile-build-stats.js +171 -0
  28. package/middleware/observability/guard.d.ts +2 -0
  29. package/middleware/observability/guard.js +18 -0
  30. package/middleware/observability/request-logger.d.ts +6 -0
  31. package/middleware/observability/request-logger.js +46 -0
  32. package/package.json +35 -33
  33. package/server.d.ts +1 -0
  34. package/server.js +37 -25
  35. package/esm/middleware/debug-memory.js +0 -54
  36. package/middleware/debug-memory.d.ts +0 -15
  37. package/middleware/debug-memory.js +0 -58
@@ -0,0 +1,191 @@
1
+ import { buildConnectionString, getPgPool } from 'pg-cache';
2
+ import { getPgEnvOptions } from 'pg-env';
3
+ import { Pool } from 'pg';
4
+ const ACTIVE_ACTIVITY_SQL = `
5
+ select
6
+ pid,
7
+ usename,
8
+ application_name,
9
+ state,
10
+ wait_event_type,
11
+ wait_event,
12
+ age(now(), xact_start) as xact_age,
13
+ age(now(), query_start) as query_age,
14
+ left(query, 500) as query
15
+ from pg_stat_activity
16
+ where datname = current_database()
17
+ and pid <> pg_backend_pid()
18
+ and (
19
+ xact_start is not null
20
+ or wait_event_type is not null
21
+ or state <> 'idle'
22
+ )
23
+ order by xact_start asc nulls last, query_start asc nulls last
24
+ limit 50
25
+ `;
26
+ const BLOCKED_ACTIVITY_SQL = `
27
+ with blocked as (
28
+ select
29
+ a.pid as blocked_pid,
30
+ a.usename as blocked_user,
31
+ a.application_name as blocked_application,
32
+ a.state as blocked_state,
33
+ a.wait_event_type,
34
+ a.wait_event,
35
+ age(now(), a.query_start) as blocked_for,
36
+ left(a.query, 500) as blocked_query,
37
+ pg_blocking_pids(a.pid) as blocker_pids
38
+ from pg_stat_activity a
39
+ where a.datname = current_database()
40
+ and cardinality(pg_blocking_pids(a.pid)) > 0
41
+ )
42
+ select
43
+ b.blocked_pid,
44
+ b.blocked_user,
45
+ b.blocked_application,
46
+ b.blocked_state,
47
+ b.wait_event_type,
48
+ b.wait_event,
49
+ b.blocked_for,
50
+ b.blocked_query,
51
+ blocker.pid as blocker_pid,
52
+ blocker.usename as blocker_user,
53
+ blocker.application_name as blocker_application,
54
+ blocker.state as blocker_state,
55
+ left(blocker.query, 500) as blocker_query
56
+ from blocked b
57
+ left join lateral unnest(b.blocker_pids) blocker_pid on true
58
+ left join pg_stat_activity blocker on blocker.pid = blocker_pid
59
+ order by b.blocked_for desc
60
+ `;
61
+ const LOCK_SUMMARY_SQL = `
62
+ select
63
+ locktype,
64
+ mode,
65
+ granted,
66
+ count(*)::int as count
67
+ from pg_locks
68
+ group by locktype, mode, granted
69
+ order by granted asc, count desc, locktype asc, mode asc
70
+ `;
71
+ const DATABASE_STATS_SQL = `
72
+ select
73
+ numbackends,
74
+ xact_commit,
75
+ xact_rollback,
76
+ blks_read,
77
+ blks_hit,
78
+ tup_returned,
79
+ tup_fetched,
80
+ tup_inserted,
81
+ tup_updated,
82
+ tup_deleted,
83
+ temp_files,
84
+ temp_bytes,
85
+ deadlocks,
86
+ checksum_failures,
87
+ stats_reset
88
+ from pg_stat_database
89
+ where datname = current_database()
90
+ `;
91
+ const SETTINGS_SQL = `
92
+ select
93
+ name,
94
+ setting,
95
+ unit
96
+ from pg_settings
97
+ where name = any(array[
98
+ 'max_connections',
99
+ 'shared_buffers',
100
+ 'work_mem',
101
+ 'maintenance_work_mem',
102
+ 'effective_cache_size',
103
+ 'statement_timeout',
104
+ 'lock_timeout',
105
+ 'idle_in_transaction_session_timeout'
106
+ ])
107
+ order by name asc
108
+ `;
109
+ const NOTIFY_QUEUE_SQL = `
110
+ select pg_notification_queue_usage() as queue_usage
111
+ `;
112
+ const DIAGNOSTICS_STATEMENT_TIMEOUT_MS = 3_000;
113
+ const DIAGNOSTICS_LOCK_TIMEOUT_MS = 500;
114
+ const diagnosticsPools = new Map();
115
+ const buildDiagnosticsConnectionString = (opts) => {
116
+ const pgConfig = getPgEnvOptions(opts.pg);
117
+ return buildConnectionString(pgConfig.user, pgConfig.password, pgConfig.host, pgConfig.port, pgConfig.database);
118
+ };
119
+ const getDiagnosticsPool = (opts) => {
120
+ const connectionString = buildDiagnosticsConnectionString(opts);
121
+ const existing = diagnosticsPools.get(connectionString);
122
+ if (existing) {
123
+ return existing;
124
+ }
125
+ const pool = new Pool({
126
+ connectionString,
127
+ max: 1,
128
+ idleTimeoutMillis: 10_000,
129
+ connectionTimeoutMillis: 1_500,
130
+ allowExitOnIdle: true,
131
+ application_name: 'constructive-debug-snapshot',
132
+ });
133
+ diagnosticsPools.set(connectionString, pool);
134
+ return pool;
135
+ };
136
+ const withDiagnosticsClient = async (opts, fn) => {
137
+ const diagnosticsPool = getDiagnosticsPool(opts);
138
+ const client = await diagnosticsPool.connect();
139
+ try {
140
+ await client.query('BEGIN');
141
+ await client.query(`SET LOCAL statement_timeout = '${DIAGNOSTICS_STATEMENT_TIMEOUT_MS}ms'`);
142
+ await client.query(`SET LOCAL lock_timeout = '${DIAGNOSTICS_LOCK_TIMEOUT_MS}ms'`);
143
+ const result = await fn(client);
144
+ await client.query('COMMIT');
145
+ return result;
146
+ }
147
+ catch (error) {
148
+ try {
149
+ await client.query('ROLLBACK');
150
+ }
151
+ catch {
152
+ // Best-effort rollback for diagnostics-only transactions.
153
+ }
154
+ throw error;
155
+ }
156
+ finally {
157
+ client.release();
158
+ }
159
+ };
160
+ export const closeDebugDatabasePools = async () => {
161
+ const pools = [...diagnosticsPools.values()];
162
+ diagnosticsPools.clear();
163
+ await Promise.allSettled(pools.map((pool) => pool.end()));
164
+ };
165
+ export const getDebugDatabaseSnapshot = async (opts) => {
166
+ const appPool = getPgPool(opts.pg);
167
+ const { activity, blocked, lockSummary, databaseStats, settings, notifyQueue, } = await withDiagnosticsClient(opts, async (client) => ({
168
+ activity: await client.query(ACTIVE_ACTIVITY_SQL),
169
+ blocked: await client.query(BLOCKED_ACTIVITY_SQL),
170
+ lockSummary: await client.query(LOCK_SUMMARY_SQL),
171
+ databaseStats: await client.query(DATABASE_STATS_SQL),
172
+ settings: await client.query(SETTINGS_SQL),
173
+ notifyQueue: await client.query(NOTIFY_QUEUE_SQL),
174
+ }));
175
+ return {
176
+ database: opts.pg?.database ?? null,
177
+ pool: {
178
+ max: appPool.options?.max ?? null,
179
+ totalCount: appPool.totalCount,
180
+ idleCount: appPool.idleCount,
181
+ waitingCount: appPool.waitingCount,
182
+ },
183
+ activeActivity: activity.rows,
184
+ blockedActivity: blocked.rows,
185
+ lockSummary: lockSummary.rows,
186
+ databaseStats: databaseStats.rows[0] ?? null,
187
+ settings: settings.rows,
188
+ notificationQueueUsage: notifyQueue.rows[0]?.queue_usage ?? null,
189
+ timestamp: new Date().toISOString(),
190
+ };
191
+ };
@@ -0,0 +1,70 @@
1
+ import os from 'node:os';
2
+ import v8 from 'node:v8';
3
+ import { svcCache, SVC_CACHE_TTL_MS } from '@pgpmjs/server-utils';
4
+ import { getCacheStats } from 'graphile-cache';
5
+ import { getInFlightCount, getInFlightKeys } from '../middleware/graphile';
6
+ import { getGraphileBuildStats } from '../middleware/observability/graphile-build-stats';
7
+ const toMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
8
+ export const getDebugMemorySnapshot = () => {
9
+ const mem = process.memoryUsage();
10
+ const heapSpaces = v8.getHeapSpaceStatistics().map((space) => ({
11
+ spaceName: space.space_name,
12
+ spaceSizeBytes: space.space_size,
13
+ spaceUsedBytes: space.space_used_size,
14
+ spaceAvailableBytes: space.space_available_size,
15
+ physicalSpaceSizeBytes: space.physical_space_size,
16
+ }));
17
+ return {
18
+ pid: process.pid,
19
+ nodeEnv: process.env.NODE_ENV,
20
+ memory: {
21
+ heapUsedBytes: mem.heapUsed,
22
+ heapTotalBytes: mem.heapTotal,
23
+ rssBytes: mem.rss,
24
+ externalBytes: mem.external,
25
+ arrayBuffersBytes: mem.arrayBuffers,
26
+ heapUsed: toMB(mem.heapUsed),
27
+ heapTotal: toMB(mem.heapTotal),
28
+ rss: toMB(mem.rss),
29
+ external: toMB(mem.external),
30
+ arrayBuffers: toMB(mem.arrayBuffers),
31
+ },
32
+ cpuUsageMicros: process.cpuUsage(),
33
+ resourceUsage: process.resourceUsage(),
34
+ system: {
35
+ loadAverage: os.loadavg(),
36
+ freeMemoryBytes: os.freemem(),
37
+ totalMemoryBytes: os.totalmem(),
38
+ uptimeSeconds: os.uptime(),
39
+ },
40
+ v8: {
41
+ heapStatistics: v8.getHeapStatistics(),
42
+ heapSpaces,
43
+ },
44
+ graphileCache: getCacheStats(),
45
+ svcCache: {
46
+ size: svcCache.size,
47
+ max: svcCache.max,
48
+ ttlMs: SVC_CACHE_TTL_MS,
49
+ // Note: with updateAgeOnGet: true, this is "time since last access" not "time since creation"
50
+ oldestKeyAgeMs: (() => {
51
+ let minRemaining = Infinity;
52
+ for (const key of svcCache.keys()) {
53
+ const remaining = svcCache.getRemainingTTL(key);
54
+ if (remaining < minRemaining) {
55
+ minRemaining = remaining;
56
+ }
57
+ }
58
+ return Number.isFinite(minRemaining) ? SVC_CACHE_TTL_MS - minRemaining : null;
59
+ })(),
60
+ keys: [...svcCache.keys()].slice(0, 200),
61
+ },
62
+ inFlight: {
63
+ count: getInFlightCount(),
64
+ keys: getInFlightKeys(),
65
+ },
66
+ graphileBuilds: getGraphileBuildStats(),
67
+ uptimeMinutes: process.uptime() / 60,
68
+ timestamp: new Date().toISOString(),
69
+ };
70
+ };
@@ -0,0 +1,188 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { Logger } from '@pgpmjs/logger';
4
+ import { getDebugDatabaseSnapshot } from './debug-db-snapshot';
5
+ import { getDebugMemorySnapshot } from './debug-memory-snapshot';
6
+ import { isGraphqlDebugSamplerEnabled } from './observability';
7
+ const log = new Logger('debug-sampler');
8
+ const MAX_TOTAL_BYTES = 1024 * 1024 * 1024; // 1 GB
9
+ const getSamplerIntervalMs = () => {
10
+ const raw = process.env.GRAPHQL_DEBUG_SAMPLER_INTERVAL_MS;
11
+ const parsed = raw ? Number.parseInt(raw, 10) : 10_000;
12
+ return Number.isFinite(parsed) && parsed >= 1_000 ? parsed : 10_000;
13
+ };
14
+ const getSamplerRootDir = () => {
15
+ if (process.env.GRAPHQL_DEBUG_SAMPLER_DIR) {
16
+ return path.resolve(process.env.GRAPHQL_DEBUG_SAMPLER_DIR);
17
+ }
18
+ return path.resolve(__dirname, '../..', 'logs');
19
+ };
20
+ const createSessionLogDir = () => {
21
+ const rootDir = getSamplerRootDir();
22
+ const sessionName = `run-${new Date().toISOString().replace(/[:.]/g, '-')}-pid${process.pid}`;
23
+ return process.env.GRAPHQL_DEBUG_SAMPLER_DIR
24
+ ? rootDir
25
+ : path.join(rootDir, sessionName);
26
+ };
27
+ const appendJsonLine = async (filePath, payload) => {
28
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
29
+ await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
30
+ };
31
+ const getDirSize = async (dirPath) => {
32
+ let total = 0;
33
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const fullPath = path.join(dirPath, entry.name);
36
+ if (entry.isDirectory()) {
37
+ total += await getDirSize(fullPath);
38
+ }
39
+ else {
40
+ const stat = await fs.stat(fullPath);
41
+ total += stat.size;
42
+ }
43
+ }
44
+ return total;
45
+ };
46
+ const enforceMaxSize = async (rootDir, currentSessionDir) => {
47
+ try {
48
+ const totalSize = await getDirSize(rootDir);
49
+ if (totalSize <= MAX_TOTAL_BYTES) {
50
+ return;
51
+ }
52
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
53
+ const sessionDirs = await Promise.all(entries
54
+ .filter((e) => e.isDirectory())
55
+ .map(async (e) => {
56
+ const fullPath = path.join(rootDir, e.name);
57
+ const stat = await fs.stat(fullPath);
58
+ return { path: fullPath, mtimeMs: stat.mtimeMs };
59
+ }));
60
+ sessionDirs.sort((a, b) => a.mtimeMs - b.mtimeMs);
61
+ for (const dir of sessionDirs) {
62
+ if (dir.path === currentSessionDir) {
63
+ continue;
64
+ }
65
+ log.info(`Rolling cleanup: removing old session dir ${dir.path}`);
66
+ await fs.rm(dir.path, { recursive: true, force: true });
67
+ const newSize = await getDirSize(rootDir);
68
+ if (newSize <= MAX_TOTAL_BYTES) {
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ catch (error) {
74
+ log.error('Failed to enforce max log size', error);
75
+ }
76
+ };
77
+ export const startDebugSampler = (opts) => {
78
+ if (!isGraphqlDebugSamplerEnabled(opts.server?.host)) {
79
+ return null;
80
+ }
81
+ const intervalMs = getSamplerIntervalMs();
82
+ const logDir = createSessionLogDir();
83
+ const memoryLogPath = path.join(logDir, 'debug-memory.ndjson');
84
+ const dbLogPath = path.join(logDir, 'debug-db.ndjson');
85
+ const errorLogPath = path.join(logDir, 'debug-sampler-errors.ndjson');
86
+ let timer = null;
87
+ let stopped = false;
88
+ let inFlight = null;
89
+ let writeFailureLogged = false;
90
+ const runBackgroundWrite = (promise, scope) => {
91
+ promise.catch((error) => {
92
+ // Avoid recursive attempts to write additional error files when the
93
+ // underlying storage path is broken or unavailable.
94
+ if (!writeFailureLogged) {
95
+ writeFailureLogged = true;
96
+ log.error(`Debug sampler background write failed (${scope})`, error);
97
+ }
98
+ });
99
+ };
100
+ const recordError = async (scope, error) => {
101
+ const payload = {
102
+ scope,
103
+ timestamp: new Date().toISOString(),
104
+ pid: process.pid,
105
+ error: error instanceof Error ? {
106
+ name: error.name,
107
+ message: error.message,
108
+ stack: error.stack,
109
+ } : {
110
+ message: String(error),
111
+ },
112
+ };
113
+ await appendJsonLine(errorLogPath, payload);
114
+ };
115
+ const sampleOnce = async () => {
116
+ if (stopped) {
117
+ return;
118
+ }
119
+ try {
120
+ await appendJsonLine(memoryLogPath, getDebugMemorySnapshot());
121
+ }
122
+ catch (error) {
123
+ log.error('Failed to capture debug memory snapshot', error);
124
+ await recordError('memory', error);
125
+ }
126
+ try {
127
+ await appendJsonLine(dbLogPath, await getDebugDatabaseSnapshot(opts));
128
+ }
129
+ catch (error) {
130
+ log.error('Failed to capture debug DB snapshot', error);
131
+ await recordError('db', error);
132
+ }
133
+ await enforceMaxSize(getSamplerRootDir(), logDir);
134
+ };
135
+ const tick = () => {
136
+ if (stopped) {
137
+ return;
138
+ }
139
+ if (inFlight) {
140
+ runBackgroundWrite(recordError('sampler', new Error('Skipped debug sample because previous sample is still running')), 'record-skip');
141
+ return;
142
+ }
143
+ inFlight = sampleOnce()
144
+ .catch(async (error) => {
145
+ log.error('Debug sampler tick failed', error);
146
+ await recordError('sampler', error);
147
+ })
148
+ .finally(() => {
149
+ inFlight = null;
150
+ });
151
+ };
152
+ const lifecyclePayload = {
153
+ event: 'sampler_started',
154
+ intervalMs,
155
+ logDir,
156
+ pid: process.pid,
157
+ timestamp: new Date().toISOString(),
158
+ };
159
+ runBackgroundWrite(appendJsonLine(memoryLogPath, lifecyclePayload), 'lifecycle-memory-start');
160
+ runBackgroundWrite(appendJsonLine(dbLogPath, lifecyclePayload), 'lifecycle-db-start');
161
+ log.info(`Debug sampler writing snapshots every ${intervalMs}ms to ${logDir}`);
162
+ tick();
163
+ timer = setInterval(tick, intervalMs);
164
+ timer.unref();
165
+ return {
166
+ async stop() {
167
+ stopped = true;
168
+ if (timer) {
169
+ clearInterval(timer);
170
+ timer = null;
171
+ }
172
+ if (inFlight) {
173
+ await inFlight;
174
+ }
175
+ const payload = {
176
+ event: 'sampler_stopped',
177
+ intervalMs,
178
+ logDir,
179
+ pid: process.pid,
180
+ timestamp: new Date().toISOString(),
181
+ };
182
+ await Promise.allSettled([
183
+ appendJsonLine(memoryLogPath, payload),
184
+ appendJsonLine(dbLogPath, payload),
185
+ ]);
186
+ },
187
+ };
188
+ };
@@ -0,0 +1,53 @@
1
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
2
+ const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1']);
3
+ const parseBooleanEnv = (value, fallback) => {
4
+ if (value == null) {
5
+ return fallback;
6
+ }
7
+ const normalized = value.trim().toLowerCase();
8
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
9
+ return true;
10
+ if (['0', 'false', 'no', 'off'].includes(normalized))
11
+ return false;
12
+ return fallback;
13
+ };
14
+ const normalizeHost = (value) => {
15
+ if (!value) {
16
+ return null;
17
+ }
18
+ const trimmed = value.trim().toLowerCase();
19
+ if (!trimmed) {
20
+ return null;
21
+ }
22
+ if (trimmed.startsWith('[')) {
23
+ const closingIndex = trimmed.indexOf(']');
24
+ return closingIndex === -1 ? trimmed : trimmed.slice(0, closingIndex + 1);
25
+ }
26
+ const colonCount = trimmed.split(':').length - 1;
27
+ if (colonCount === 1) {
28
+ return trimmed.split(':')[0] || null;
29
+ }
30
+ return trimmed;
31
+ };
32
+ const normalizeAddress = (value) => {
33
+ const normalized = normalizeHost(value);
34
+ if (!normalized) {
35
+ return null;
36
+ }
37
+ return normalized.startsWith('::ffff:') ? normalized.slice(7) : normalized;
38
+ };
39
+ export const isDevelopmentObservabilityMode = () => process.env.NODE_ENV === 'development';
40
+ export const isLoopbackHost = (value) => {
41
+ const normalized = normalizeHost(value);
42
+ return normalized != null && LOOPBACK_HOSTS.has(normalized);
43
+ };
44
+ export const isLoopbackAddress = (value) => {
45
+ const normalized = normalizeAddress(value);
46
+ return normalized != null && LOOPBACK_ADDRESSES.has(normalized);
47
+ };
48
+ export const isGraphqlObservabilityRequested = () => parseBooleanEnv(process.env.GRAPHQL_OBSERVABILITY_ENABLED, false);
49
+ export const isGraphqlObservabilityEnabled = (serverHost) => isDevelopmentObservabilityMode() &&
50
+ isGraphqlObservabilityRequested() &&
51
+ isLoopbackHost(serverHost);
52
+ export const isGraphqlDebugSamplerEnabled = (serverHost) => isGraphqlObservabilityEnabled(serverHost) &&
53
+ parseBooleanEnv(process.env.GRAPHQL_DEBUG_SAMPLER_ENABLED, true);
@@ -6,7 +6,9 @@ import { ConstructivePreset, makePgService } from 'graphile-settings';
6
6
  import { buildConnectionString } from 'pg-cache';
7
7
  import { getPgEnvOptions } from 'pg-env';
8
8
  import './types'; // for Request type
9
+ import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
9
10
  import { HandlerCreationError } from '../errors/api-errors';
11
+ import { observeGraphileBuild } from './observability/graphile-build-stats';
10
12
  const maskErrorLog = new Logger('graphile:maskError');
11
13
  const SAFE_ERROR_CODES = new Set([
12
14
  // GraphQL standard
@@ -170,6 +172,7 @@ const buildPreset = (connectionString, schemas, anonRole, roleName) => ({
170
172
  },
171
173
  });
172
174
  export const graphile = (opts) => {
175
+ const observabilityEnabled = isGraphqlObservabilityEnabled(opts.server?.host);
173
176
  return async (req, res, next) => {
174
177
  const label = reqLabel(req);
175
178
  try {
@@ -233,7 +236,11 @@ export const graphile = (opts) => {
233
236
  const connectionString = buildConnectionString(pgConfig.user, pgConfig.password, pgConfig.host, pgConfig.port, pgConfig.database);
234
237
  // Create promise and store in in-flight map BEFORE try block
235
238
  const preset = buildPreset(connectionString, schema || [], anonRole, roleName);
236
- const creationPromise = createGraphileInstance({ preset, cacheKey: key });
239
+ const creationPromise = observeGraphileBuild({
240
+ cacheKey: key,
241
+ serviceKey: key,
242
+ databaseId: api.databaseId ?? null,
243
+ }, () => createGraphileInstance({ preset, cacheKey: key }), { enabled: observabilityEnabled });
237
244
  creating.set(key, creationPromise);
238
245
  try {
239
246
  const instance = await creationPromise;
@@ -0,0 +1,23 @@
1
+ import { Logger } from '@pgpmjs/logger';
2
+ import { getDebugDatabaseSnapshot } from '../../diagnostics/debug-db-snapshot';
3
+ const log = new Logger('debug-db');
4
+ export const createDebugDatabaseMiddleware = (opts) => {
5
+ return async (_req, res) => {
6
+ try {
7
+ const response = await getDebugDatabaseSnapshot(opts);
8
+ log.debug('Database debug snapshot', {
9
+ activeActivity: response.activeActivity.length,
10
+ blockedActivity: response.blockedActivity.length,
11
+ lockSummary: response.lockSummary.length,
12
+ });
13
+ res.json(response);
14
+ }
15
+ catch (error) {
16
+ log.error('Failed to fetch debug DB snapshot', error);
17
+ res.status(500).json({
18
+ error: 'Failed to fetch database debug snapshot',
19
+ message: error instanceof Error ? error.message : String(error),
20
+ });
21
+ }
22
+ };
23
+ };
@@ -0,0 +1,8 @@
1
+ import { Logger } from '@pgpmjs/logger';
2
+ import { getDebugMemorySnapshot } from '../../diagnostics/debug-memory-snapshot';
3
+ const log = new Logger('debug-memory');
4
+ export const debugMemory = (_req, res) => {
5
+ const response = getDebugMemorySnapshot();
6
+ log.debug('Memory snapshot:', response);
7
+ res.json(response);
8
+ };