@graphoria/server 0.3.0 → 0.5.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 (203) hide show
  1. package/README.md +28 -20
  2. package/dist/src/ai/mcp/index.d.ts +9 -2
  3. package/dist/src/ai/mcp/index.d.ts.map +1 -1
  4. package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
  5. package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
  6. package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
  7. package/dist/src/authentication/capabilities.d.ts +18 -0
  8. package/dist/src/authentication/capabilities.d.ts.map +1 -0
  9. package/dist/src/authentication/capabilities.test.d.ts +2 -0
  10. package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
  11. package/dist/src/authentication/jwt.d.ts.map +1 -1
  12. package/dist/src/authentication/paseto.d.ts.map +1 -1
  13. package/dist/src/authentication/secrets.d.ts +9 -0
  14. package/dist/src/authentication/secrets.d.ts.map +1 -0
  15. package/dist/src/authentication/secrets.test.d.ts +2 -0
  16. package/dist/src/authentication/secrets.test.d.ts.map +1 -0
  17. package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
  18. package/dist/src/config/types/db.d.ts +25 -0
  19. package/dist/src/config/types/db.d.ts.map +1 -1
  20. package/dist/src/configuration/getSchemas/index.d.ts +40 -2
  21. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  22. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -1
  23. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  24. package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
  25. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
  26. package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
  27. package/dist/src/configuration/index.d.ts +30 -1
  28. package/dist/src/configuration/index.d.ts.map +1 -1
  29. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  30. package/dist/src/configuration/rest/generateOpenAPI.test.d.ts +2 -0
  31. package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
  32. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  33. package/dist/src/console/api.d.ts.map +1 -1
  34. package/dist/src/console/session.d.ts +12 -4
  35. package/dist/src/console/session.d.ts.map +1 -1
  36. package/dist/src/cron/index.d.ts.map +1 -1
  37. package/dist/src/databases/common.d.ts +20 -6
  38. package/dist/src/databases/common.d.ts.map +1 -1
  39. package/dist/src/databases/core/executor.d.ts +4 -6
  40. package/dist/src/databases/core/executor.d.ts.map +1 -1
  41. package/dist/src/databases/core/function-mapping.d.ts +2 -3
  42. package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
  43. package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
  44. package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
  45. package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
  46. package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
  47. package/dist/src/databases/core/query-builder.d.ts +5 -0
  48. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  49. package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
  50. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  51. package/dist/src/databases/engines/mssql/getStructure.d.ts +5 -0
  52. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  53. package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
  54. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  55. package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
  56. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  57. package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
  58. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  59. package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
  60. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  61. package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
  62. package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
  63. package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
  64. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  65. package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
  66. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  67. package/dist/src/databases/high-level-operations.d.ts +5 -0
  68. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  69. package/dist/src/databases/metadata/structure.d.ts +5 -0
  70. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  71. package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
  72. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  73. package/dist/src/index.d.ts +4 -0
  74. package/dist/src/index.d.ts.map +1 -1
  75. package/dist/src/logging/audit.d.ts +27 -0
  76. package/dist/src/logging/audit.d.ts.map +1 -0
  77. package/dist/src/logging/audit.test.d.ts +2 -0
  78. package/dist/src/logging/audit.test.d.ts.map +1 -0
  79. package/dist/src/logging/slowQuery.d.ts +29 -0
  80. package/dist/src/logging/slowQuery.d.ts.map +1 -0
  81. package/dist/src/logging/slowQuery.test.d.ts +2 -0
  82. package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
  83. package/dist/src/observability/health.d.ts +23 -0
  84. package/dist/src/observability/health.d.ts.map +1 -0
  85. package/dist/src/observability/health.test.d.ts +2 -0
  86. package/dist/src/observability/health.test.d.ts.map +1 -0
  87. package/dist/src/observability/httpMetrics.d.ts +7 -0
  88. package/dist/src/observability/httpMetrics.d.ts.map +1 -0
  89. package/dist/src/observability/httpMetrics.test.d.ts +2 -0
  90. package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
  91. package/dist/src/observability/httpTracing.d.ts +11 -0
  92. package/dist/src/observability/httpTracing.d.ts.map +1 -0
  93. package/dist/src/observability/httpTracing.test.d.ts +2 -0
  94. package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
  95. package/dist/src/observability/metrics.d.ts +81 -0
  96. package/dist/src/observability/metrics.d.ts.map +1 -0
  97. package/dist/src/observability/metrics.perf.test.d.ts +2 -0
  98. package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
  99. package/dist/src/observability/metrics.test.d.ts +2 -0
  100. package/dist/src/observability/metrics.test.d.ts.map +1 -0
  101. package/dist/src/observability/metricsRoute.d.ts +19 -0
  102. package/dist/src/observability/metricsRoute.d.ts.map +1 -0
  103. package/dist/src/observability/metricsRoute.test.d.ts +2 -0
  104. package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
  105. package/dist/src/observability/tracing.d.ts +92 -0
  106. package/dist/src/observability/tracing.d.ts.map +1 -0
  107. package/dist/src/observability/tracing.perf.test.d.ts +2 -0
  108. package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
  109. package/dist/src/observability/tracing.test.d.ts +2 -0
  110. package/dist/src/observability/tracing.test.d.ts.map +1 -0
  111. package/dist/src/queues/kafka.d.ts +6 -0
  112. package/dist/src/queues/kafka.d.ts.map +1 -1
  113. package/dist/src/queues/kafka.test.d.ts +2 -0
  114. package/dist/src/queues/kafka.test.d.ts.map +1 -0
  115. package/dist/src/queues/rabbitmq.d.ts.map +1 -1
  116. package/dist/src/remoteREST/proxy.d.ts.map +1 -1
  117. package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
  118. package/dist/src/singletons/cache/redisClient.d.ts +2 -2
  119. package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
  120. package/dist/src/singletons/databases.d.ts +1 -0
  121. package/dist/src/singletons/databases.d.ts.map +1 -1
  122. package/dist/src/singletons/env.d.ts +37 -4
  123. package/dist/src/singletons/env.d.ts.map +1 -1
  124. package/dist/src/singletons/queues.d.ts +1 -0
  125. package/dist/src/singletons/queues.d.ts.map +1 -1
  126. package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
  127. package/dist/src/subscriptions/utils/polling.d.ts +1 -0
  128. package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
  129. package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
  130. package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
  131. package/dist/src/types/env.d.ts +113 -12
  132. package/dist/src/types/env.d.ts.map +1 -1
  133. package/dist/src/types/zod/configuration.d.ts +25 -0
  134. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  135. package/dist/src/types/zod/db.d.ts +85 -0
  136. package/dist/src/types/zod/db.d.ts.map +1 -1
  137. package/dist/src/utils/rateLimit.d.ts.map +1 -1
  138. package/dist/src/utils/redis.d.ts +17 -0
  139. package/dist/src/utils/redis.d.ts.map +1 -0
  140. package/dist/src/utils/redis.test.d.ts +2 -0
  141. package/dist/src/utils/redis.test.d.ts.map +1 -0
  142. package/dist/src/utils/responses.d.ts +6 -0
  143. package/dist/src/utils/responses.d.ts.map +1 -1
  144. package/dist/src/utils/sessionVariables.d.ts +2 -0
  145. package/dist/src/utils/sessionVariables.d.ts.map +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/package.json +8 -5
  148. package/playgrounds/console/index.html +9 -9
  149. package/playgrounds/graphiql/index.html +549 -686
  150. package/playgrounds/scalar/index.html +1 -1
  151. package/src/ai/mcp/index.ts +19 -14
  152. package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
  153. package/src/analyzeQuery/depthLimit.ts +15 -5
  154. package/src/authentication/capabilities.ts +56 -0
  155. package/src/authentication/index.ts +3 -3
  156. package/src/authentication/jwt.ts +30 -25
  157. package/src/authentication/paseto.ts +36 -26
  158. package/src/authentication/secrets.ts +30 -0
  159. package/src/authentication/tokenRepository.ts +2 -3
  160. package/src/config/types/db.ts +16 -2
  161. package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
  162. package/src/configuration/gql/gqlAuthOperations.ts +26 -0
  163. package/src/configuration/gql/handleGraphQLRequestFactory.ts +241 -148
  164. package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
  165. package/src/configuration/rest/generateOpenAPI.ts +61 -11
  166. package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
  167. package/src/console/api.ts +64 -24
  168. package/src/console/session.ts +30 -26
  169. package/src/cron/index.ts +59 -36
  170. package/src/databases/common.ts +109 -10
  171. package/src/databases/core/executor.ts +97 -20
  172. package/src/databases/core/function-mapping.ts +1 -2
  173. package/src/databases/core/procedure-arguments.ts +25 -0
  174. package/src/databases/engines/mssql/connection.ts +0 -3
  175. package/src/databases/engines/mssql/query/index.ts +4 -1
  176. package/src/databases/engines/mysql/connection.ts +10 -7
  177. package/src/databases/engines/mysql/query/index.ts +5 -2
  178. package/src/databases/engines/postgresql/connection.ts +10 -7
  179. package/src/databases/engines/postgresql/query/index.ts +5 -2
  180. package/src/databases/high-level-operations.ts +44 -2
  181. package/src/index.ts +200 -76
  182. package/src/logging/audit.ts +86 -0
  183. package/src/logging/slowQuery.ts +59 -0
  184. package/src/observability/health.ts +67 -0
  185. package/src/observability/httpMetrics.ts +37 -0
  186. package/src/observability/httpTracing.ts +48 -0
  187. package/src/observability/metrics.ts +243 -0
  188. package/src/observability/metricsRoute.ts +33 -0
  189. package/src/observability/tracing.ts +421 -0
  190. package/src/queues/kafka.ts +45 -2
  191. package/src/queues/rabbitmq.ts +37 -1
  192. package/src/remoteREST/proxy.ts +17 -0
  193. package/src/remoteSchemas/proxy.ts +17 -0
  194. package/src/singletons/cache/redisClient.ts +4 -5
  195. package/src/singletons/databases.ts +5 -0
  196. package/src/singletons/queues.ts +9 -3
  197. package/src/subscriptions/strategies/database.ts +2 -0
  198. package/src/subscriptions/utils/polling.ts +80 -21
  199. package/src/types/env.ts +53 -8
  200. package/src/utils/rateLimit.ts +6 -1
  201. package/src/utils/redis.ts +71 -0
  202. package/src/utils/responses.ts +12 -0
  203. package/src/utils/sessionVariables.ts +2 -0
@@ -0,0 +1,37 @@
1
+ import { incMetric, isMetricsEnabled, observeMetric } from "./metrics";
2
+
3
+ /**
4
+ * Wraps a route handler so the request is counted and timed. The route label is
5
+ * the name of the mounted route, never the URL: a path carries identifiers, and
6
+ * one series per identifier is a cardinality leak.
7
+ */
8
+ export const withHttpMetrics =
9
+ <Req extends Request, Args extends unknown[]>(
10
+ route: string,
11
+ handler: (req: Req, ...args: Args) => Response | undefined | Promise<Response | undefined>,
12
+ ) =>
13
+ async (req: Req, ...args: Args): Promise<Response | undefined> => {
14
+ if (!isMetricsEnabled()) return handler(req, ...args);
15
+
16
+ const startTime = Bun.nanoseconds();
17
+ const method = req.method;
18
+ const record = (status: string) => {
19
+ incMetric("graphoria_http_requests_total", { route, method, status });
20
+ observeMetric(
21
+ "graphoria_http_request_duration_seconds",
22
+ { route, method },
23
+ (Bun.nanoseconds() - startTime) / 1e9,
24
+ );
25
+ };
26
+
27
+ try {
28
+ const response = await handler(req, ...args);
29
+ // A websocket upgrade answers no request, so it is not counted as one.
30
+ if (response) record(String(response.status));
31
+ return response;
32
+ } catch (error) {
33
+ // What Bun answers when a route handler throws.
34
+ record("500");
35
+ throw error;
36
+ }
37
+ };
@@ -0,0 +1,48 @@
1
+ import { isTracingEnabled, parseTraceparent, startSpan, withActiveSpan } from "./tracing";
2
+
3
+ /**
4
+ * Wraps a route handler so the request roots a trace. The span is named for the
5
+ * mounted route, never the URL: a path carries identifiers and a query string
6
+ * carries caller data, and a span name is exported verbatim.
7
+ *
8
+ * An inbound `traceparent` is continued, so a gateway's span and this one join
9
+ * up. `parent: null` is deliberate — a request is a root, never a child of
10
+ * whatever context the server happened to be in.
11
+ */
12
+ export const withHttpTracing =
13
+ <Req extends Request, Args extends unknown[]>(
14
+ route: string,
15
+ handler: (req: Req, ...args: Args) => Response | undefined | Promise<Response | undefined>,
16
+ ) =>
17
+ async (req: Req, ...args: Args): Promise<Response | undefined> => {
18
+ if (!isTracingEnabled()) return handler(req, ...args);
19
+
20
+ const url = new URL(req.url);
21
+ const span = startSpan(`${req.method} ${route}`, {
22
+ kind: "server",
23
+ parent: parseTraceparent(req.headers.get("traceparent")) ?? null,
24
+ attributes: {
25
+ "http.request.method": req.method,
26
+ "http.route": route,
27
+ "url.scheme": url.protocol.replace(":", ""),
28
+ "server.address": url.host,
29
+ },
30
+ });
31
+
32
+ return withActiveSpan(span, async () => {
33
+ try {
34
+ const response = await handler(req, ...args);
35
+ // A websocket upgrade answers no request, so it gets no span: leaving
36
+ // this one unended is what keeps it out of the export.
37
+ if (response) {
38
+ span?.setAttribute("http.response.status_code", response.status);
39
+ span?.end();
40
+ }
41
+ return response;
42
+ } catch (error) {
43
+ span?.recordError(error);
44
+ span?.end();
45
+ throw error;
46
+ }
47
+ });
48
+ };
@@ -0,0 +1,243 @@
1
+ type MetricDefinition = {
2
+ type: "counter" | "histogram";
3
+ help: string;
4
+ /** Declared sorted, so a rendered series lists its labels in a stable order. */
5
+ labelNames: readonly string[];
6
+ };
7
+
8
+ const METRICS = {
9
+ graphoria_graphql_operations_total: {
10
+ type: "counter",
11
+ help: "GraphQL operations executed, by outcome.",
12
+ labelNames: ["operation", "outcome", "role", "type"],
13
+ },
14
+ graphoria_graphql_operation_duration_seconds: {
15
+ type: "histogram",
16
+ help: "Time spent handling a GraphQL operation, in seconds.",
17
+ labelNames: ["operation", "role", "type"],
18
+ },
19
+ graphoria_http_requests_total: {
20
+ type: "counter",
21
+ help: "HTTP requests answered by the GraphQL and REST routes, by status.",
22
+ labelNames: ["method", "route", "status"],
23
+ },
24
+ graphoria_http_request_duration_seconds: {
25
+ type: "histogram",
26
+ help: "Time spent answering an HTTP request, in seconds.",
27
+ labelNames: ["method", "route"],
28
+ },
29
+ graphoria_cron_runs_total: {
30
+ type: "counter",
31
+ help: "Cron ticks that ran to completion or threw, by job.",
32
+ labelNames: ["job", "outcome"],
33
+ },
34
+ graphoria_cron_run_duration_seconds: {
35
+ type: "histogram",
36
+ help: "Time spent in a cron tick, in seconds.",
37
+ labelNames: ["job"],
38
+ },
39
+ graphoria_queue_messages_published_total: {
40
+ type: "counter",
41
+ help: "Messages handed to a broker, by publisher and outcome.",
42
+ labelNames: ["broker", "outcome", "publisher"],
43
+ },
44
+ graphoria_queue_messages_consumed_total: {
45
+ type: "counter",
46
+ help: "Messages taken from a broker, by consumer and outcome.",
47
+ labelNames: ["broker", "consumer", "outcome", "queue"],
48
+ },
49
+ graphoria_rate_limit_rejections_total: {
50
+ type: "counter",
51
+ help: "Requests refused by the rate limiter, by the role the ceiling was read from.",
52
+ labelNames: ["role"],
53
+ },
54
+ graphoria_graphql_rejections_total: {
55
+ type: "counter",
56
+ help: "GraphQL operations rejected before execution.",
57
+ labelNames: ["reason"],
58
+ },
59
+ } as const satisfies Record<string, MetricDefinition>;
60
+
61
+ export type MetricName = keyof typeof METRICS;
62
+
63
+ export type Labels = Record<string, string>;
64
+
65
+ export type Registry = {
66
+ inc(metric: MetricName, labels: Labels, value?: number): void;
67
+ observe(metric: MetricName, labels: Labels, seconds: number): void;
68
+ render(): string;
69
+ };
70
+
71
+ export type RegistryOptions = {
72
+ /** Distinct values the caller-supplied `operation` label may take, per metric. */
73
+ maxOperationLabels?: number;
74
+ };
75
+
76
+ const BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] as const;
77
+
78
+ const SEPARATOR = "\u0000";
79
+
80
+ const OVERFLOW_LABEL = "other";
81
+
82
+ const DEFAULT_MAX_OPERATION_LABELS = 200;
83
+
84
+ type HistogramSeries = { counts: number[]; sum: number; count: number };
85
+
86
+ const escapeLabelValue = (value: string) =>
87
+ value.replaceAll("\\", "\\\\").replaceAll("\n", "\\n").replaceAll('"', '\\"');
88
+
89
+ /**
90
+ * A metric store with no dependencies. The `operation` label is the only one a
91
+ * caller controls, so it is the only one capped: past the cap every new name
92
+ * collapses into `other` rather than minting a series the scrape has to carry
93
+ * forever.
94
+ */
95
+ export const createRegistry = ({
96
+ maxOperationLabels = DEFAULT_MAX_OPERATION_LABELS,
97
+ }: RegistryOptions = {}): Registry => {
98
+ const counters = new Map<MetricName, Map<string, number>>();
99
+ const histograms = new Map<MetricName, Map<string, HistogramSeries>>();
100
+ const knownOperations = new Map<MetricName, Set<string>>();
101
+
102
+ const boundOperation = (metric: MetricName, operation: string) => {
103
+ let known = knownOperations.get(metric);
104
+ if (!known) {
105
+ known = new Set();
106
+ knownOperations.set(metric, known);
107
+ }
108
+ if (known.has(operation)) return operation;
109
+ if (known.size >= maxOperationLabels) return OVERFLOW_LABEL;
110
+ known.add(operation);
111
+ return operation;
112
+ };
113
+
114
+ const keyOf = (metric: MetricName, labels: Labels) =>
115
+ METRICS[metric].labelNames
116
+ .map((name) => {
117
+ const value = labels[name] ?? "";
118
+ return name === "operation" ? boundOperation(metric, value) : value;
119
+ })
120
+ .join(SEPARATOR);
121
+
122
+ const renderLabels = (metric: MetricName, key: string, extra?: string) => {
123
+ const values = key.split(SEPARATOR);
124
+ const pairs = METRICS[metric].labelNames.map(
125
+ (name, index) => `${name}="${escapeLabelValue(values[index] ?? "")}"`,
126
+ );
127
+ if (extra) pairs.push(extra);
128
+ return pairs.length > 0 ? `{${pairs.join(",")}}` : "";
129
+ };
130
+
131
+ const seriesOf = <T>(store: Map<MetricName, Map<string, T>>, metric: MetricName) => {
132
+ let series = store.get(metric);
133
+ if (!series) {
134
+ series = new Map();
135
+ store.set(metric, series);
136
+ }
137
+ return series;
138
+ };
139
+
140
+ const renderHeader = (lines: string[], metric: MetricName) => {
141
+ const definition = METRICS[metric];
142
+ lines.push(`# HELP ${metric} ${definition.help}`, `# TYPE ${metric} ${definition.type}`);
143
+ };
144
+
145
+ return {
146
+ inc: (metric, labels, value = 1) => {
147
+ const series = seriesOf(counters, metric);
148
+ const key = keyOf(metric, labels);
149
+ series.set(key, (series.get(key) ?? 0) + value);
150
+ },
151
+
152
+ observe: (metric, labels, seconds) => {
153
+ const series = seriesOf(histograms, metric);
154
+ const key = keyOf(metric, labels);
155
+ let entry = series.get(key);
156
+ if (!entry) {
157
+ entry = { counts: new Array(BUCKETS.length).fill(0), sum: 0, count: 0 };
158
+ series.set(key, entry);
159
+ }
160
+ entry.sum += seconds;
161
+ entry.count += 1;
162
+ for (let index = 0; index < BUCKETS.length; index++) {
163
+ if (seconds <= BUCKETS[index]!) entry.counts[index]! += 1;
164
+ }
165
+ },
166
+
167
+ render: () => {
168
+ const lines: string[] = [];
169
+
170
+ for (const [metric, series] of counters) {
171
+ if (series.size === 0) continue;
172
+ renderHeader(lines, metric);
173
+ for (const [key, value] of series) {
174
+ lines.push(`${metric}${renderLabels(metric, key)} ${value}`);
175
+ }
176
+ }
177
+
178
+ for (const [metric, series] of histograms) {
179
+ if (series.size === 0) continue;
180
+ renderHeader(lines, metric);
181
+ for (const [key, entry] of series) {
182
+ for (let index = 0; index < BUCKETS.length; index++) {
183
+ lines.push(
184
+ `${metric}_bucket${renderLabels(metric, key, `le="${BUCKETS[index]}"`)} ${entry.counts[index]}`,
185
+ );
186
+ }
187
+ lines.push(
188
+ `${metric}_bucket${renderLabels(metric, key, 'le="+Inf"')} ${entry.count}`,
189
+ `${metric}_sum${renderLabels(metric, key)} ${entry.sum}`,
190
+ `${metric}_count${renderLabels(metric, key)} ${entry.count}`,
191
+ );
192
+ }
193
+ }
194
+
195
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
196
+ },
197
+ };
198
+ };
199
+
200
+ /**
201
+ * Off until boot turns it on, so an executor or a handler used outside a booted
202
+ * server records nothing. Every call site goes through the helpers below, whose
203
+ * first act is this check — a disabled process pays one branch and allocates
204
+ * nothing.
205
+ */
206
+ let enabled = false;
207
+
208
+ let options: RegistryOptions = {};
209
+
210
+ let override: Registry | null = null;
211
+ let instance: Registry | null = null;
212
+
213
+ export const configureMetrics = (settings: { enabled: boolean } & RegistryOptions): void => {
214
+ enabled = settings.enabled;
215
+ options = { maxOperationLabels: settings.maxOperationLabels };
216
+ instance = null;
217
+ };
218
+
219
+ /** Test seam: pass `null` to drop the process registry and its recorded series. */
220
+ export const setMetricsRegistry = (registry: Registry | null): void => {
221
+ override = registry;
222
+ instance = null;
223
+ };
224
+
225
+ const registry = (): Registry => {
226
+ if (override) return override;
227
+ if (!instance) instance = createRegistry(options);
228
+ return instance;
229
+ };
230
+
231
+ export const isMetricsEnabled = (): boolean => enabled;
232
+
233
+ export const incMetric = (metric: MetricName, labels: Labels, value = 1): void => {
234
+ if (!enabled) return;
235
+ registry().inc(metric, labels, value);
236
+ };
237
+
238
+ export const observeMetric = (metric: MetricName, labels: Labels, seconds: number): void => {
239
+ if (!enabled) return;
240
+ registry().observe(metric, labels, seconds);
241
+ };
242
+
243
+ export const renderMetrics = (): string => registry().render();
@@ -0,0 +1,33 @@
1
+ import type { CapabilityGrant } from "../authentication/capabilities";
2
+
3
+ import { S401 } from "../utils/responses";
4
+
5
+ export type MetricsRouteOptions = {
6
+ path: string;
7
+ /** The header a scoped credential rides in — the admin-secret header. */
8
+ secretHeader: string;
9
+ authorize: (candidate: string | null) => CapabilityGrant | null;
10
+ render: () => string;
11
+ };
12
+
13
+ const CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
14
+
15
+ /**
16
+ * A scrape is gated because the exposition names operations, roles and
17
+ * databases. It is deliberately not rate limited: a scrape interval is fixed
18
+ * and counting it against a caller ceiling would drop samples.
19
+ */
20
+ export const createMetricsRoute = ({
21
+ path,
22
+ secretHeader,
23
+ authorize,
24
+ render,
25
+ }: MetricsRouteOptions) => ({
26
+ [path]: {
27
+ GET: (req: Request) => {
28
+ if (!authorize(req.headers.get(secretHeader))) return new S401({ error: "Unauthorized" });
29
+
30
+ return new Response(render(), { headers: { "content-type": CONTENT_TYPE } });
31
+ },
32
+ },
33
+ });