@constructive-io/graphql-server 4.10.0 → 4.11.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.
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 +31 -29
  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,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRequestLogger = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const logger_1 = require("@pgpmjs/logger");
6
+ const log = new logger_1.Logger('server');
7
+ const SAFE_REQUEST_ID = /^[a-zA-Z0-9\-_]{1,128}$/;
8
+ const createRequestLogger = ({ observabilityEnabled }) => {
9
+ return (req, res, next) => {
10
+ const headerRequestId = req.header('x-request-id');
11
+ const reqId = (headerRequestId && SAFE_REQUEST_ID.test(headerRequestId))
12
+ ? headerRequestId
13
+ : (0, crypto_1.randomUUID)();
14
+ const start = process.hrtime.bigint();
15
+ let finished = false;
16
+ req.requestId = reqId;
17
+ const host = req.hostname || req.headers.host || 'unknown';
18
+ const ip = req.clientIp ?? req.ip ?? 'unknown';
19
+ log.debug(`[${reqId}] -> ${req.method} ${req.originalUrl} host=${host} ip=${ip}`);
20
+ res.on('finish', () => {
21
+ finished = true;
22
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
23
+ const apiInfo = req.api
24
+ ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
25
+ : 'api=unresolved';
26
+ const authInfo = req.token ? 'auth=token' : 'auth=anon';
27
+ const svcInfo = req.svc_key ? `svc=${req.svc_key}` : 'svc=unset';
28
+ log.debug(`[${reqId}] <- ${res.statusCode} ${req.method} ${req.originalUrl} (${durationMs.toFixed(1)} ms) ${apiInfo} ${svcInfo} ${authInfo}`);
29
+ });
30
+ if (observabilityEnabled) {
31
+ res.on('close', () => {
32
+ if (finished) {
33
+ return;
34
+ }
35
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
36
+ const apiInfo = req.api
37
+ ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
38
+ : 'api=unresolved';
39
+ log.warn(`[${reqId}] connection closed before response completed ` +
40
+ `${req.method} ${req.originalUrl} (${durationMs.toFixed(1)} ms) ${apiInfo}`);
41
+ });
42
+ }
43
+ next();
44
+ };
45
+ };
46
+ exports.createRequestLogger = createRequestLogger;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -26,6 +26,8 @@
26
26
  "build:dev": "makage build --dev",
27
27
  "dev": "ts-node src/run.ts",
28
28
  "dev:watch": "nodemon --watch src --ext ts --exec ts-node src/run.ts",
29
+ "debug:memory:analyze": "node scripts/analyze-debug-logs.mjs",
30
+ "debug:heap:capture": "node scripts/capture-heap-snapshot.mjs",
29
31
  "lint": "eslint . --fix",
30
32
  "test": "jest --passWithNoTests",
31
33
  "test:watch": "jest --watch",
@@ -39,55 +41,55 @@
39
41
  "backend"
40
42
  ],
41
43
  "dependencies": {
42
- "@constructive-io/graphql-env": "^3.4.2",
43
- "@constructive-io/graphql-types": "^3.3.2",
44
- "@constructive-io/s3-utils": "^2.9.2",
45
- "@constructive-io/upload-names": "^2.9.2",
44
+ "@constructive-io/graphql-env": "^3.4.3",
45
+ "@constructive-io/graphql-types": "^3.3.3",
46
+ "@constructive-io/s3-utils": "^2.9.3",
47
+ "@constructive-io/upload-names": "^2.9.3",
46
48
  "@constructive-io/url-domains": "^2.9.2",
47
- "@graphile-contrib/pg-many-to-many": "2.0.0-rc.1",
48
- "@graphile/simplify-inflection": "8.0.0-rc.3",
49
+ "@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
50
+ "@graphile/simplify-inflection": "8.0.0-rc.5",
49
51
  "@pgpmjs/env": "^2.15.2",
50
52
  "@pgpmjs/logger": "^2.4.2",
51
- "@pgpmjs/server-utils": "^3.4.2",
53
+ "@pgpmjs/server-utils": "^3.4.3",
52
54
  "@pgpmjs/types": "^2.19.2",
53
55
  "@pgsql/quotes": "^17.1.0",
54
56
  "cors": "^2.8.6",
55
57
  "deepmerge": "^4.3.1",
56
58
  "express": "^5.2.1",
57
- "gql-ast": "^3.3.2",
58
- "grafast": "1.0.0-rc.7",
59
- "grafserv": "1.0.0-rc.6",
60
- "graphile-build": "5.0.0-rc.4",
61
- "graphile-build-pg": "5.0.0-rc.5",
62
- "graphile-cache": "^3.3.2",
63
- "graphile-config": "1.0.0-rc.5",
64
- "graphile-settings": "^4.9.0",
65
- "graphile-utils": "5.0.0-rc.5",
66
- "graphql": "^16.13.0",
59
+ "gql-ast": "^3.3.3",
60
+ "grafast": "1.0.0-rc.9",
61
+ "grafserv": "1.0.0-rc.7",
62
+ "graphile-build": "5.0.0-rc.6",
63
+ "graphile-build-pg": "5.0.0-rc.8",
64
+ "graphile-cache": "^3.3.3",
65
+ "graphile-config": "1.0.0-rc.6",
66
+ "graphile-settings": "^4.9.1",
67
+ "graphile-utils": "5.0.0-rc.8",
68
+ "graphql": "16.13.0",
67
69
  "graphql-upload": "^13.0.0",
68
- "lru-cache": "^11.2.6",
69
- "multer": "^2.1.0",
70
- "pg": "^8.19.0",
71
- "pg-cache": "^3.3.2",
70
+ "lru-cache": "^11.2.7",
71
+ "multer": "^2.1.1",
72
+ "pg": "^8.20.0",
73
+ "pg-cache": "^3.3.3",
72
74
  "pg-env": "^1.7.2",
73
- "pg-query-context": "^2.8.2",
74
- "pg-sql2": "5.0.0-rc.4",
75
- "postgraphile": "5.0.0-rc.7",
75
+ "pg-query-context": "^2.8.3",
76
+ "pg-sql2": "5.0.0-rc.5",
77
+ "postgraphile": "5.0.0-rc.10",
76
78
  "postgraphile-plugin-connection-filter": "3.0.0-rc.1",
77
79
  "request-ip": "^3.3.0"
78
80
  },
79
81
  "devDependencies": {
80
- "@aws-sdk/client-s3": "^3.1001.0",
82
+ "@aws-sdk/client-s3": "^3.1009.0",
81
83
  "@types/cors": "^2.8.17",
82
84
  "@types/express": "^5.0.6",
83
85
  "@types/graphql-upload": "^8.0.12",
84
- "@types/multer": "^2.0.0",
86
+ "@types/multer": "^2.1.0",
85
87
  "@types/pg": "^8.18.0",
86
88
  "@types/request-ip": "^0.0.41",
87
- "graphile-test": "4.5.2",
89
+ "graphile-test": "4.5.3",
88
90
  "makage": "^0.1.10",
89
91
  "nodemon": "^3.1.14",
90
92
  "ts-node": "^10.9.2"
91
93
  },
92
- "gitHead": "180ba9eed8414667a0d6dcef99ca4bb988c73bbd"
94
+ "gitHead": "21fd7c2c30663548cf15aa448c1935ab56e5497d"
93
95
  }
package/server.d.ts CHANGED
@@ -32,6 +32,7 @@ declare class Server {
32
32
  private shuttingDown;
33
33
  private closed;
34
34
  private httpServer;
35
+ private debugSampler;
35
36
  constructor(opts: ConstructiveOptions);
36
37
  listen(): HttpServer;
37
38
  flush(databaseId: string): Promise<void>;
package/server.js CHANGED
@@ -8,12 +8,13 @@ const graphql_env_1 = require("@constructive-io/graphql-env");
8
8
  const logger_1 = require("@pgpmjs/logger");
9
9
  const server_utils_1 = require("@pgpmjs/server-utils");
10
10
  const url_domains_1 = require("@constructive-io/url-domains");
11
- const crypto_1 = require("crypto");
12
11
  const express_1 = __importDefault(require("express"));
13
12
  const graphql_upload_1 = __importDefault(require("graphql-upload"));
14
13
  const graphile_cache_1 = require("graphile-cache");
15
14
  const pg_cache_1 = require("pg-cache");
16
15
  const request_ip_1 = __importDefault(require("request-ip"));
16
+ const debug_db_snapshot_1 = require("./diagnostics/debug-db-snapshot");
17
+ const observability_1 = require("./diagnostics/observability");
17
18
  const api_1 = require("./middleware/api");
18
19
  const auth_1 = require("./middleware/auth");
19
20
  const cors_1 = require("./middleware/cors");
@@ -22,8 +23,12 @@ const favicon_1 = require("./middleware/favicon");
22
23
  const flush_1 = require("./middleware/flush");
23
24
  const graphile_1 = require("./middleware/graphile");
24
25
  const multipart_bridge_1 = require("./middleware/multipart-bridge");
26
+ const debug_db_1 = require("./middleware/observability/debug-db");
27
+ const debug_memory_1 = require("./middleware/observability/debug-memory");
28
+ const guard_1 = require("./middleware/observability/guard");
29
+ const request_logger_1 = require("./middleware/observability/request-logger");
25
30
  const upload_1 = require("./middleware/upload");
26
- const debug_memory_1 = require("./middleware/debug-memory");
31
+ const debug_sampler_1 = require("./diagnostics/debug-sampler");
27
32
  const log = new logger_1.Logger('server');
28
33
  /**
29
34
  * Creates and starts a GraphQL server instance
@@ -61,35 +66,17 @@ class Server {
61
66
  shuttingDown = false;
62
67
  closed = false;
63
68
  httpServer = null;
69
+ debugSampler = null;
64
70
  constructor(opts) {
65
71
  this.opts = (0, graphql_env_1.getEnvOptions)(opts);
66
72
  const effectiveOpts = this.opts;
73
+ const observabilityRequested = (0, observability_1.isGraphqlObservabilityRequested)();
74
+ const observabilityEnabled = (0, observability_1.isGraphqlObservabilityEnabled)(effectiveOpts.server?.host);
67
75
  const app = (0, express_1.default)();
68
76
  const api = (0, api_1.createApiMiddleware)(effectiveOpts);
69
77
  const authenticate = (0, auth_1.createAuthenticateMiddleware)(effectiveOpts);
70
78
  const uploadAuthenticate = (0, upload_1.createUploadAuthenticateMiddleware)(effectiveOpts);
71
- const SAFE_REQUEST_ID = /^[a-zA-Z0-9\-_]{1,128}$/;
72
- const requestLogger = (req, res, next) => {
73
- const headerRequestId = req.header('x-request-id');
74
- const reqId = (headerRequestId && SAFE_REQUEST_ID.test(headerRequestId))
75
- ? headerRequestId
76
- : (0, crypto_1.randomUUID)();
77
- const start = process.hrtime.bigint();
78
- req.requestId = reqId;
79
- const host = req.hostname || req.headers.host || 'unknown';
80
- const ip = req.clientIp || req.ip;
81
- log.debug(`[${reqId}] -> ${req.method} ${req.originalUrl} host=${host} ip=${ip}`);
82
- res.on('finish', () => {
83
- const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
84
- const apiInfo = req.api
85
- ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
86
- : 'api=unresolved';
87
- const authInfo = req.token ? 'auth=token' : 'auth=anon';
88
- const svcInfo = req.svc_key ? `svc=${req.svc_key}` : 'svc=unset';
89
- log.debug(`[${reqId}] <- ${res.statusCode} ${req.method} ${req.originalUrl} (${durationMs.toFixed(1)} ms) ${apiInfo} ${svcInfo} ${authInfo}`);
90
- });
91
- next();
92
- };
79
+ const requestLogger = (0, request_logger_1.createRequestLogger)({ observabilityEnabled });
93
80
  // Log startup configuration (non-sensitive values only)
94
81
  const apiOpts = effectiveOpts.api || {};
95
82
  log.info('[server] Starting with config:', {
@@ -104,9 +91,28 @@ class Server {
104
91
  exposedSchemas: apiOpts.exposedSchemas?.join(',') || 'none',
105
92
  anonRole: apiOpts.anonRole,
106
93
  roleName: apiOpts.roleName,
94
+ observabilityEnabled,
107
95
  });
96
+ if (observabilityRequested && !observabilityEnabled) {
97
+ const reasons = [];
98
+ if (!(0, observability_1.isDevelopmentObservabilityMode)()) {
99
+ reasons.push('NODE_ENV must be development');
100
+ }
101
+ if (!(0, observability_1.isLoopbackHost)(effectiveOpts.server?.host)) {
102
+ reasons.push('server host must be localhost, 127.0.0.1, or ::1');
103
+ }
104
+ log.warn(`GRAPHQL_OBSERVABILITY_ENABLED was requested but observability remains disabled${reasons.length > 0 ? `: ${reasons.join('; ')}` : ''}`);
105
+ }
108
106
  (0, server_utils_1.healthz)(app);
109
- app.get('/debug/memory', debug_memory_1.debugMemory);
107
+ if (observabilityEnabled) {
108
+ app.get('/debug/memory', guard_1.localObservabilityOnly, debug_memory_1.debugMemory);
109
+ app.get('/debug/db', guard_1.localObservabilityOnly, (0, debug_db_1.createDebugDatabaseMiddleware)(effectiveOpts));
110
+ }
111
+ else {
112
+ app.use('/debug', (_req, res) => {
113
+ res.status(404).send('Not found');
114
+ });
115
+ }
110
116
  app.use(favicon_1.favicon);
111
117
  (0, server_utils_1.trustProxy)(app, effectiveOpts.server.trustProxy);
112
118
  // Warn if a global CORS override is set in production
@@ -139,6 +145,7 @@ class Server {
139
145
  app.use(error_handler_1.notFoundHandler); // Catches unmatched routes (404)
140
146
  app.use(error_handler_1.errorHandler); // Catches all thrown errors
141
147
  this.app = app;
148
+ this.debugSampler = observabilityEnabled ? (0, debug_sampler_1.startDebugSampler)(effectiveOpts) : null;
142
149
  }
143
150
  listen() {
144
151
  const { server } = this.opts;
@@ -228,9 +235,14 @@ class Server {
228
235
  this.closed = true;
229
236
  this.shuttingDown = true;
230
237
  await this.removeEventListener();
238
+ if (this.debugSampler) {
239
+ await this.debugSampler.stop();
240
+ this.debugSampler = null;
241
+ }
231
242
  if (this.httpServer?.listening) {
232
243
  await new Promise((resolve) => this.httpServer.close(() => resolve()));
233
244
  }
245
+ await (0, debug_db_snapshot_1.closeDebugDatabasePools)();
234
246
  if (closeCaches) {
235
247
  await Server.closeCaches({ closePools: true });
236
248
  }
@@ -1,54 +0,0 @@
1
- import { getNodeEnv } from '@pgpmjs/env';
2
- import { Logger } from '@pgpmjs/logger';
3
- import { svcCache } from '@pgpmjs/server-utils';
4
- import { getCacheStats } from 'graphile-cache';
5
- import { getInFlightCount, getInFlightKeys } from './graphile';
6
- const log = new Logger('debug-memory');
7
- const toMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
8
- /**
9
- * Development-only debug endpoint for monitoring memory usage and cache state.
10
- *
11
- * Mounts GET /debug/memory which returns:
12
- * - Node.js process memory (heap, RSS, external, array buffers)
13
- * - Graphile cache stats (size, max, TTL, keys with ages)
14
- * - Service cache size
15
- * - In-flight handler creation count
16
- * - Process uptime
17
- *
18
- * This endpoint is only available when NODE_ENV=development.
19
- * In production, it returns 404.
20
- */
21
- export const debugMemory = (_req, res) => {
22
- if (getNodeEnv() !== 'development') {
23
- res.status(404).send('Not found');
24
- return;
25
- }
26
- const mem = process.memoryUsage();
27
- const cacheStats = getCacheStats();
28
- const response = {
29
- memory: {
30
- heapUsed: toMB(mem.heapUsed),
31
- heapTotal: toMB(mem.heapTotal),
32
- rss: toMB(mem.rss),
33
- external: toMB(mem.external),
34
- arrayBuffers: toMB(mem.arrayBuffers),
35
- },
36
- graphileCache: {
37
- size: cacheStats.size,
38
- max: cacheStats.max,
39
- ttl: `${(cacheStats.ttl / 1000 / 60).toFixed(0)} min`,
40
- keys: cacheStats.keys,
41
- },
42
- svcCache: {
43
- size: svcCache.size,
44
- },
45
- inFlight: {
46
- count: getInFlightCount(),
47
- keys: getInFlightKeys(),
48
- },
49
- uptime: `${(process.uptime() / 60).toFixed(1)} min`,
50
- timestamp: new Date().toISOString(),
51
- };
52
- log.debug('Memory snapshot:', response);
53
- res.json(response);
54
- };
@@ -1,15 +0,0 @@
1
- import type { RequestHandler } from 'express';
2
- /**
3
- * Development-only debug endpoint for monitoring memory usage and cache state.
4
- *
5
- * Mounts GET /debug/memory which returns:
6
- * - Node.js process memory (heap, RSS, external, array buffers)
7
- * - Graphile cache stats (size, max, TTL, keys with ages)
8
- * - Service cache size
9
- * - In-flight handler creation count
10
- * - Process uptime
11
- *
12
- * This endpoint is only available when NODE_ENV=development.
13
- * In production, it returns 404.
14
- */
15
- export declare const debugMemory: RequestHandler;
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.debugMemory = void 0;
4
- const env_1 = require("@pgpmjs/env");
5
- const logger_1 = require("@pgpmjs/logger");
6
- const server_utils_1 = require("@pgpmjs/server-utils");
7
- const graphile_cache_1 = require("graphile-cache");
8
- const graphile_1 = require("./graphile");
9
- const log = new logger_1.Logger('debug-memory');
10
- const toMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
11
- /**
12
- * Development-only debug endpoint for monitoring memory usage and cache state.
13
- *
14
- * Mounts GET /debug/memory which returns:
15
- * - Node.js process memory (heap, RSS, external, array buffers)
16
- * - Graphile cache stats (size, max, TTL, keys with ages)
17
- * - Service cache size
18
- * - In-flight handler creation count
19
- * - Process uptime
20
- *
21
- * This endpoint is only available when NODE_ENV=development.
22
- * In production, it returns 404.
23
- */
24
- const debugMemory = (_req, res) => {
25
- if ((0, env_1.getNodeEnv)() !== 'development') {
26
- res.status(404).send('Not found');
27
- return;
28
- }
29
- const mem = process.memoryUsage();
30
- const cacheStats = (0, graphile_cache_1.getCacheStats)();
31
- const response = {
32
- memory: {
33
- heapUsed: toMB(mem.heapUsed),
34
- heapTotal: toMB(mem.heapTotal),
35
- rss: toMB(mem.rss),
36
- external: toMB(mem.external),
37
- arrayBuffers: toMB(mem.arrayBuffers),
38
- },
39
- graphileCache: {
40
- size: cacheStats.size,
41
- max: cacheStats.max,
42
- ttl: `${(cacheStats.ttl / 1000 / 60).toFixed(0)} min`,
43
- keys: cacheStats.keys,
44
- },
45
- svcCache: {
46
- size: server_utils_1.svcCache.size,
47
- },
48
- inFlight: {
49
- count: (0, graphile_1.getInFlightCount)(),
50
- keys: (0, graphile_1.getInFlightKeys)(),
51
- },
52
- uptime: `${(process.uptime() / 60).toFixed(1)} min`,
53
- timestamp: new Date().toISOString(),
54
- };
55
- log.debug('Memory snapshot:', response);
56
- res.json(response);
57
- };
58
- exports.debugMemory = debugMemory;