@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.
- package/README.md +28 -20
- package/dist/src/ai/mcp/index.d.ts +9 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/authentication/capabilities.d.ts +18 -0
- package/dist/src/authentication/capabilities.d.ts.map +1 -0
- package/dist/src/authentication/capabilities.test.d.ts +2 -0
- package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/secrets.d.ts +9 -0
- package/dist/src/authentication/secrets.d.ts.map +1 -0
- package/dist/src/authentication/secrets.test.d.ts +2 -0
- package/dist/src/authentication/secrets.test.d.ts.map +1 -0
- package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +25 -0
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/index.d.ts +40 -2
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +30 -1
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts +2 -0
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +12 -4
- package/dist/src/console/session.d.ts.map +1 -1
- package/dist/src/cron/index.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +20 -6
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +4 -6
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +2 -3
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
- package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
- package/dist/src/databases/core/query-builder.d.ts +5 -0
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +5 -0
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +5 -0
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/audit.d.ts +27 -0
- package/dist/src/logging/audit.d.ts.map +1 -0
- package/dist/src/logging/audit.test.d.ts +2 -0
- package/dist/src/logging/audit.test.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.d.ts +29 -0
- package/dist/src/logging/slowQuery.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.test.d.ts +2 -0
- package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
- package/dist/src/observability/health.d.ts +23 -0
- package/dist/src/observability/health.d.ts.map +1 -0
- package/dist/src/observability/health.test.d.ts +2 -0
- package/dist/src/observability/health.test.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.d.ts +7 -0
- package/dist/src/observability/httpMetrics.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.test.d.ts +2 -0
- package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.d.ts +11 -0
- package/dist/src/observability/httpTracing.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.test.d.ts +2 -0
- package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.d.ts +81 -0
- package/dist/src/observability/metrics.d.ts.map +1 -0
- package/dist/src/observability/metrics.perf.test.d.ts +2 -0
- package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.test.d.ts +2 -0
- package/dist/src/observability/metrics.test.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.d.ts +19 -0
- package/dist/src/observability/metricsRoute.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.test.d.ts +2 -0
- package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.d.ts +92 -0
- package/dist/src/observability/tracing.d.ts.map +1 -0
- package/dist/src/observability/tracing.perf.test.d.ts +2 -0
- package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.test.d.ts +2 -0
- package/dist/src/observability/tracing.test.d.ts.map +1 -0
- package/dist/src/queues/kafka.d.ts +6 -0
- package/dist/src/queues/kafka.d.ts.map +1 -1
- package/dist/src/queues/kafka.test.d.ts +2 -0
- package/dist/src/queues/kafka.test.d.ts.map +1 -0
- package/dist/src/queues/rabbitmq.d.ts.map +1 -1
- package/dist/src/remoteREST/proxy.d.ts.map +1 -1
- package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
- package/dist/src/singletons/cache/redisClient.d.ts +2 -2
- package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
- package/dist/src/singletons/databases.d.ts +1 -0
- package/dist/src/singletons/databases.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +37 -4
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queues.d.ts +1 -0
- package/dist/src/singletons/queues.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts +1 -0
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
- package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
- package/dist/src/types/env.d.ts +113 -12
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/zod/configuration.d.ts +25 -0
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +85 -0
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/utils/rateLimit.d.ts.map +1 -1
- package/dist/src/utils/redis.d.ts +17 -0
- package/dist/src/utils/redis.d.ts.map +1 -0
- package/dist/src/utils/redis.test.d.ts +2 -0
- package/dist/src/utils/redis.test.d.ts.map +1 -0
- package/dist/src/utils/responses.d.ts +6 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +2 -0
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/playgrounds/console/index.html +9 -9
- package/playgrounds/graphiql/index.html +549 -686
- package/playgrounds/scalar/index.html +1 -1
- package/src/ai/mcp/index.ts +19 -14
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
- package/src/analyzeQuery/depthLimit.ts +15 -5
- package/src/authentication/capabilities.ts +56 -0
- package/src/authentication/index.ts +3 -3
- package/src/authentication/jwt.ts +30 -25
- package/src/authentication/paseto.ts +36 -26
- package/src/authentication/secrets.ts +30 -0
- package/src/authentication/tokenRepository.ts +2 -3
- package/src/config/types/db.ts +16 -2
- package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
- package/src/configuration/gql/gqlAuthOperations.ts +26 -0
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +241 -148
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
- package/src/configuration/rest/generateOpenAPI.ts +61 -11
- package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
- package/src/console/api.ts +64 -24
- package/src/console/session.ts +30 -26
- package/src/cron/index.ts +59 -36
- package/src/databases/common.ts +109 -10
- package/src/databases/core/executor.ts +97 -20
- package/src/databases/core/function-mapping.ts +1 -2
- package/src/databases/core/procedure-arguments.ts +25 -0
- package/src/databases/engines/mssql/connection.ts +0 -3
- package/src/databases/engines/mssql/query/index.ts +4 -1
- package/src/databases/engines/mysql/connection.ts +10 -7
- package/src/databases/engines/mysql/query/index.ts +5 -2
- package/src/databases/engines/postgresql/connection.ts +10 -7
- package/src/databases/engines/postgresql/query/index.ts +5 -2
- package/src/databases/high-level-operations.ts +44 -2
- package/src/index.ts +200 -76
- package/src/logging/audit.ts +86 -0
- package/src/logging/slowQuery.ts +59 -0
- package/src/observability/health.ts +67 -0
- package/src/observability/httpMetrics.ts +37 -0
- package/src/observability/httpTracing.ts +48 -0
- package/src/observability/metrics.ts +243 -0
- package/src/observability/metricsRoute.ts +33 -0
- package/src/observability/tracing.ts +421 -0
- package/src/queues/kafka.ts +45 -2
- package/src/queues/rabbitmq.ts +37 -1
- package/src/remoteREST/proxy.ts +17 -0
- package/src/remoteSchemas/proxy.ts +17 -0
- package/src/singletons/cache/redisClient.ts +4 -5
- package/src/singletons/databases.ts +5 -0
- package/src/singletons/queues.ts +9 -3
- package/src/subscriptions/strategies/database.ts +2 -0
- package/src/subscriptions/utils/polling.ts +80 -21
- package/src/types/env.ts +53 -8
- package/src/utils/rateLimit.ts +6 -1
- package/src/utils/redis.ts +71 -0
- package/src/utils/responses.ts +12 -0
- package/src/utils/sessionVariables.ts +2 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
import type { Logger } from "pino";
|
|
4
|
+
|
|
5
|
+
import { version } from "../../package.json";
|
|
6
|
+
import { logger } from "../logging";
|
|
7
|
+
|
|
8
|
+
export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer";
|
|
9
|
+
|
|
10
|
+
export type SpanStatus = "unset" | "ok" | "error";
|
|
11
|
+
|
|
12
|
+
export type AttributeValue = string | number | boolean;
|
|
13
|
+
|
|
14
|
+
export type Attributes = Record<string, AttributeValue | undefined>;
|
|
15
|
+
|
|
16
|
+
/** What rides in the async context: three scalars, never the span itself. */
|
|
17
|
+
export type SpanContext = { traceId: string; spanId: string; sampled: boolean };
|
|
18
|
+
|
|
19
|
+
export type SpanOptions = {
|
|
20
|
+
kind?: SpanKind;
|
|
21
|
+
attributes?: Attributes;
|
|
22
|
+
/** Overrides the active context — the inbound `traceparent`, or a deliberate root. */
|
|
23
|
+
parent?: SpanContext | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type Span = {
|
|
27
|
+
readonly context: SpanContext;
|
|
28
|
+
setAttribute(key: string, value: AttributeValue | undefined): void;
|
|
29
|
+
setStatus(status: SpanStatus, message?: string): void;
|
|
30
|
+
recordError(error: unknown): void;
|
|
31
|
+
end(): void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type FinishedSpan = {
|
|
35
|
+
traceId: string;
|
|
36
|
+
spanId: string;
|
|
37
|
+
parentSpanId?: string;
|
|
38
|
+
name: string;
|
|
39
|
+
kind: SpanKind;
|
|
40
|
+
startTimeUnixNano: string;
|
|
41
|
+
endTimeUnixNano: string;
|
|
42
|
+
attributes: Attributes;
|
|
43
|
+
status: SpanStatus;
|
|
44
|
+
statusMessage?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type Tracer = {
|
|
48
|
+
startSpan(name: string, options?: SpanOptions): Span;
|
|
49
|
+
/** Runs `fn` with `span` active, ends it, and marks it errored if `fn` throws. */
|
|
50
|
+
withSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
51
|
+
active(): SpanContext | undefined;
|
|
52
|
+
flush(): Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type TracerOptions = {
|
|
56
|
+
endpoint: string;
|
|
57
|
+
/** `k=v,k2=v2`, sent on every export — an API key for a hosted collector. */
|
|
58
|
+
headers?: string;
|
|
59
|
+
serviceName: string;
|
|
60
|
+
serviceVersion: string;
|
|
61
|
+
sampleRatio?: number;
|
|
62
|
+
maxBatch?: number;
|
|
63
|
+
maxQueue?: number;
|
|
64
|
+
/** `0` starts no timer, which is how a test drives the exporter by hand. */
|
|
65
|
+
exportIntervalMs?: number;
|
|
66
|
+
/** Test seam: where an export failure and a queue overflow are reported. */
|
|
67
|
+
log?: Pick<Logger, "warn">;
|
|
68
|
+
/** Test seam: the transport. Production POSTs with `fetch`. */
|
|
69
|
+
send?: (
|
|
70
|
+
url: string,
|
|
71
|
+
init: { method: string; headers: Record<string, string>; body: string },
|
|
72
|
+
) => Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const MAX_BATCH = 512;
|
|
76
|
+
const MAX_QUEUE = 2048;
|
|
77
|
+
const EXPORT_INTERVAL_MS = 5000;
|
|
78
|
+
|
|
79
|
+
const KIND_CODES: Record<SpanKind, number> = {
|
|
80
|
+
internal: 1,
|
|
81
|
+
server: 2,
|
|
82
|
+
client: 3,
|
|
83
|
+
producer: 4,
|
|
84
|
+
consumer: 5,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const STATUS_CODES: Record<SpanStatus, number> = { unset: 0, ok: 1, error: 2 };
|
|
88
|
+
|
|
89
|
+
const ZERO_TRACE_ID = "0".repeat(32);
|
|
90
|
+
const ZERO_SPAN_ID = "0".repeat(16);
|
|
91
|
+
|
|
92
|
+
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
93
|
+
|
|
94
|
+
// A span is minted per statement, so the id path is on the request's critical
|
|
95
|
+
// path: a lookup table beats formatting each byte.
|
|
96
|
+
const HEX = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, "0"));
|
|
97
|
+
|
|
98
|
+
const hex = (bytes: number) => {
|
|
99
|
+
const buffer = crypto.getRandomValues(new Uint8Array(bytes));
|
|
100
|
+
let id = "";
|
|
101
|
+
for (let index = 0; index < bytes; index++) id += HEX[buffer[index]!];
|
|
102
|
+
return id;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// An all-zero id is invalid per the W3C spec and a collector rejects the span,
|
|
106
|
+
// so the (vanishing) draw is re-rolled rather than exported.
|
|
107
|
+
const newTraceId = () => {
|
|
108
|
+
let id = hex(16);
|
|
109
|
+
while (id === ZERO_TRACE_ID) id = hex(16);
|
|
110
|
+
return id;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const newSpanId = () => {
|
|
114
|
+
let id = hex(8);
|
|
115
|
+
while (id === ZERO_SPAN_ID) id = hex(8);
|
|
116
|
+
return id;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const parseTraceparent = (header: string | null | undefined): SpanContext | undefined => {
|
|
120
|
+
if (!header) return undefined;
|
|
121
|
+
const match = TRACEPARENT.exec(header);
|
|
122
|
+
if (!match) return undefined;
|
|
123
|
+
const [, traceId, spanId, flags] = match as unknown as [string, string, string, string];
|
|
124
|
+
if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return undefined;
|
|
125
|
+
return { traceId, spanId, sampled: (Number.parseInt(flags, 16) & 1) === 1 };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const formatTraceparent = (context: SpanContext): string =>
|
|
129
|
+
`00-${context.traceId}-${context.spanId}-${context.sampled ? "01" : "00"}`;
|
|
130
|
+
|
|
131
|
+
const parseHeaders = (headers: string): Record<string, string> => {
|
|
132
|
+
const parsed: Record<string, string> = {};
|
|
133
|
+
for (const entry of headers.split(",")) {
|
|
134
|
+
const index = entry.indexOf("=");
|
|
135
|
+
if (index <= 0) continue;
|
|
136
|
+
const key = entry.slice(0, index).trim();
|
|
137
|
+
if (key) parsed[key] = entry.slice(index + 1).trim();
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type AnyValue =
|
|
143
|
+
| { stringValue: string }
|
|
144
|
+
| { boolValue: boolean }
|
|
145
|
+
| { intValue: string }
|
|
146
|
+
| { doubleValue: number };
|
|
147
|
+
|
|
148
|
+
const renderAttributes = (attributes: Attributes) =>
|
|
149
|
+
Object.entries(attributes).flatMap<{ key: string; value: AnyValue }>(([key, value]) => {
|
|
150
|
+
if (value === undefined) return [];
|
|
151
|
+
if (typeof value === "string") return [{ key, value: { stringValue: value } }];
|
|
152
|
+
if (typeof value === "boolean") return [{ key, value: { boolValue: value } }];
|
|
153
|
+
// OTLP/JSON carries an int64 as a string; a fractional number is a double.
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
key,
|
|
157
|
+
value: Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value },
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const renderSpan = (span: FinishedSpan) => ({
|
|
163
|
+
traceId: span.traceId,
|
|
164
|
+
spanId: span.spanId,
|
|
165
|
+
...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
|
|
166
|
+
name: span.name,
|
|
167
|
+
kind: KIND_CODES[span.kind],
|
|
168
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
169
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
170
|
+
attributes: renderAttributes(span.attributes),
|
|
171
|
+
status: {
|
|
172
|
+
code: STATUS_CODES[span.status],
|
|
173
|
+
...(span.statusMessage ? { message: span.statusMessage } : {}),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const renderPayload = (spans: FinishedSpan[], serviceName: string, serviceVersion: string) => ({
|
|
178
|
+
resourceSpans: [
|
|
179
|
+
{
|
|
180
|
+
resource: {
|
|
181
|
+
attributes: [
|
|
182
|
+
{ key: "service.name", value: { stringValue: serviceName } },
|
|
183
|
+
{ key: "service.version", value: { stringValue: serviceVersion } },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
scopeSpans: [{ scope: { name: "graphoria" }, spans: spans.map(renderSpan) }],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const defaultSend: NonNullable<TracerOptions["send"]> = async (url, init) => {
|
|
192
|
+
const response = await fetch(url, init);
|
|
193
|
+
if (!response.ok) throw new Error(`collector answered HTTP ${response.status}`);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One async context for every tracer in the process. It holds three scalars
|
|
200
|
+
* rather than the span, so a retained context cannot pin a finished span's
|
|
201
|
+
* attributes in memory.
|
|
202
|
+
*/
|
|
203
|
+
const store = new AsyncLocalStorage<SpanContext>();
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A tracer with no dependencies: W3C ids, OTLP/HTTP JSON, and a bounded export
|
|
207
|
+
* queue. Nothing here reads process state — everything it needs is closed over,
|
|
208
|
+
* so it is unit-testable without booting a server.
|
|
209
|
+
*/
|
|
210
|
+
export const createTracer = ({
|
|
211
|
+
endpoint,
|
|
212
|
+
headers = "",
|
|
213
|
+
serviceName,
|
|
214
|
+
serviceVersion,
|
|
215
|
+
sampleRatio = 1,
|
|
216
|
+
maxBatch = MAX_BATCH,
|
|
217
|
+
maxQueue = MAX_QUEUE,
|
|
218
|
+
exportIntervalMs = EXPORT_INTERVAL_MS,
|
|
219
|
+
send = defaultSend,
|
|
220
|
+
log = logger("tracing"),
|
|
221
|
+
}: TracerOptions): Tracer => {
|
|
222
|
+
const url = `${endpoint.replace(/\/+$/, "")}/v1/traces`;
|
|
223
|
+
const exportHeaders = { "content-type": "application/json", ...parseHeaders(headers) };
|
|
224
|
+
|
|
225
|
+
let queue: FinishedSpan[] = [];
|
|
226
|
+
let dropped = 0;
|
|
227
|
+
|
|
228
|
+
const post = async (batch: FinishedSpan[]) => {
|
|
229
|
+
try {
|
|
230
|
+
await send(url, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: exportHeaders,
|
|
233
|
+
body: JSON.stringify(renderPayload(batch, serviceName, serviceVersion)),
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Dropped, never retried: an unbounded retry buffer is the failure mode
|
|
237
|
+
// that turns an observability feature into the outage.
|
|
238
|
+
log.warn({ err: error, spans: batch.length }, "span export failed");
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const flush = async () => {
|
|
243
|
+
if (queue.length === 0) return;
|
|
244
|
+
const batch = queue;
|
|
245
|
+
queue = [];
|
|
246
|
+
if (dropped > 0) {
|
|
247
|
+
log.warn({ dropped }, "span queue full, oldest spans dropped");
|
|
248
|
+
dropped = 0;
|
|
249
|
+
}
|
|
250
|
+
await post(batch);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (exportIntervalMs > 0) {
|
|
254
|
+
// Unref'd: there is no graceful-shutdown hook to clear it, and a live timer
|
|
255
|
+
// would keep the process alive after the server closes.
|
|
256
|
+
setInterval(() => void flush(), exportIntervalMs).unref();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const enqueue = (span: FinishedSpan) => {
|
|
260
|
+
queue.push(span);
|
|
261
|
+
if (queue.length > maxQueue) {
|
|
262
|
+
dropped += queue.length - maxQueue;
|
|
263
|
+
queue = queue.slice(queue.length - maxQueue);
|
|
264
|
+
}
|
|
265
|
+
if (queue.length >= maxBatch) void flush();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const shouldSample = (parent: SpanContext | undefined) => {
|
|
269
|
+
if (parent) return parent.sampled;
|
|
270
|
+
if (sampleRatio >= 1) return true;
|
|
271
|
+
if (sampleRatio <= 0) return false;
|
|
272
|
+
return Math.random() < sampleRatio;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const startSpan = (name: string, options: SpanOptions = {}): Span => {
|
|
276
|
+
const parent = options.parent === undefined ? store.getStore() : (options.parent ?? undefined);
|
|
277
|
+
const sampled = shouldSample(parent);
|
|
278
|
+
const context: SpanContext = {
|
|
279
|
+
traceId: parent?.traceId ?? newTraceId(),
|
|
280
|
+
spanId: newSpanId(),
|
|
281
|
+
sampled,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Wall clock for the start, monotonic for the length, so a span's duration
|
|
285
|
+
// does not drift with the system clock.
|
|
286
|
+
const startedAtNs = BigInt(Date.now()) * 1_000_000n;
|
|
287
|
+
const startedAt = Bun.nanoseconds();
|
|
288
|
+
const attributes: Attributes = { ...options.attributes };
|
|
289
|
+
let status: SpanStatus = "unset";
|
|
290
|
+
let statusMessage: string | undefined;
|
|
291
|
+
let ended = false;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
context,
|
|
295
|
+
setAttribute: (key, value) => {
|
|
296
|
+
attributes[key] = value;
|
|
297
|
+
},
|
|
298
|
+
setStatus: (next, message) => {
|
|
299
|
+
status = next;
|
|
300
|
+
statusMessage = message;
|
|
301
|
+
},
|
|
302
|
+
recordError: (error) => {
|
|
303
|
+
status = "error";
|
|
304
|
+
statusMessage = errorMessage(error);
|
|
305
|
+
},
|
|
306
|
+
end: () => {
|
|
307
|
+
if (ended) return;
|
|
308
|
+
ended = true;
|
|
309
|
+
if (!sampled) return;
|
|
310
|
+
enqueue({
|
|
311
|
+
traceId: context.traceId,
|
|
312
|
+
spanId: context.spanId,
|
|
313
|
+
...(parent ? { parentSpanId: parent.spanId } : {}),
|
|
314
|
+
name,
|
|
315
|
+
kind: options.kind ?? "internal",
|
|
316
|
+
startTimeUnixNano: String(startedAtNs),
|
|
317
|
+
endTimeUnixNano: String(startedAtNs + BigInt(Bun.nanoseconds() - startedAt)),
|
|
318
|
+
attributes,
|
|
319
|
+
status,
|
|
320
|
+
...(statusMessage ? { statusMessage } : {}),
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
startSpan,
|
|
328
|
+
withSpan: async (name, options, fn) => {
|
|
329
|
+
const span = startSpan(name, options);
|
|
330
|
+
try {
|
|
331
|
+
return await store.run(span.context, () => fn(span));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
span.recordError(error);
|
|
334
|
+
throw error;
|
|
335
|
+
} finally {
|
|
336
|
+
span.end();
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
active: () => store.getStore(),
|
|
340
|
+
flush,
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Off until boot turns it on, so a handler or an executor used outside a booted
|
|
346
|
+
* server records nothing and never enters the async store.
|
|
347
|
+
*/
|
|
348
|
+
let enabled = false;
|
|
349
|
+
|
|
350
|
+
let settings: Omit<TracerOptions, "serviceVersion"> = {
|
|
351
|
+
endpoint: "http://localhost:4318",
|
|
352
|
+
headers: "",
|
|
353
|
+
serviceName: "graphoria",
|
|
354
|
+
sampleRatio: 1,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
let override: Tracer | null = null;
|
|
358
|
+
let instance: Tracer | null = null;
|
|
359
|
+
|
|
360
|
+
export const configureTracing = (next: {
|
|
361
|
+
enabled: boolean;
|
|
362
|
+
endpoint: string;
|
|
363
|
+
headers: string;
|
|
364
|
+
serviceName: string;
|
|
365
|
+
sampleRatio: number;
|
|
366
|
+
}): void => {
|
|
367
|
+
enabled = next.enabled;
|
|
368
|
+
settings = {
|
|
369
|
+
endpoint: next.endpoint,
|
|
370
|
+
headers: next.headers,
|
|
371
|
+
serviceName: next.serviceName,
|
|
372
|
+
sampleRatio: next.sampleRatio,
|
|
373
|
+
};
|
|
374
|
+
instance = null;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** Test seam: pass `null` to drop the process tracer and its queued spans. */
|
|
378
|
+
export const setTracer = (tracer: Tracer | null): void => {
|
|
379
|
+
override = tracer;
|
|
380
|
+
instance = null;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const tracer = (): Tracer => {
|
|
384
|
+
if (override) return override;
|
|
385
|
+
if (!instance) instance = createTracer({ ...settings, serviceVersion: version });
|
|
386
|
+
return instance;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const isTracingEnabled = (): boolean => enabled;
|
|
390
|
+
|
|
391
|
+
export const startSpan = (name: string, options?: SpanOptions): Span | undefined => {
|
|
392
|
+
if (!enabled) return undefined;
|
|
393
|
+
return tracer().startSpan(name, options);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export const withSpan = async <T>(
|
|
397
|
+
name: string,
|
|
398
|
+
options: SpanOptions,
|
|
399
|
+
fn: (span: Span | undefined) => T | Promise<T>,
|
|
400
|
+
): Promise<T> => {
|
|
401
|
+
if (!enabled) return fn(undefined);
|
|
402
|
+
return tracer().withSpan(name, options, fn);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Runs `fn` with `span` active, leaving its lifetime to the caller. A span that
|
|
407
|
+
* is never ended is never exported, which is how a websocket upgrade — a
|
|
408
|
+
* request that is never answered — produces no span at all.
|
|
409
|
+
*/
|
|
410
|
+
export const withActiveSpan = <T>(span: Span | undefined, fn: () => T): T =>
|
|
411
|
+
span ? store.run(span.context, fn) : fn();
|
|
412
|
+
|
|
413
|
+
export const activeSpanContext = (): SpanContext | undefined => {
|
|
414
|
+
if (!enabled) return undefined;
|
|
415
|
+
return tracer().active();
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export const flushSpans = (): Promise<void> => {
|
|
419
|
+
if (!enabled) return Promise.resolve();
|
|
420
|
+
return tracer().flush();
|
|
421
|
+
};
|
package/src/queues/kafka.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { KafkaConfig } from "../types/zod/queue";
|
|
|
6
6
|
|
|
7
7
|
import { queryEventEmitter } from "../configuration/gql/handleGraphQLSubscriptionFactory";
|
|
8
8
|
import { logger } from "../logging";
|
|
9
|
+
import { incMetric } from "../observability/metrics";
|
|
10
|
+
import { startSpan } from "../observability/tracing";
|
|
9
11
|
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// Reconnection Configuration
|
|
@@ -58,9 +60,19 @@ export const startConsumer = async (
|
|
|
58
60
|
|
|
59
61
|
await consumer.run({
|
|
60
62
|
eachMessage: async (payload) => {
|
|
63
|
+
const record = (outcome: "success" | "error") =>
|
|
64
|
+
incMetric("graphoria_queue_messages_consumed_total", {
|
|
65
|
+
broker: "kafka",
|
|
66
|
+
queue: connectionName,
|
|
67
|
+
consumer: name,
|
|
68
|
+
outcome,
|
|
69
|
+
});
|
|
70
|
+
|
|
61
71
|
try {
|
|
62
72
|
sendMessage(connectionName, name, payload);
|
|
73
|
+
record("success");
|
|
63
74
|
} catch (error) {
|
|
75
|
+
record("error");
|
|
64
76
|
log.error({ err: error }, "message processing failed");
|
|
65
77
|
// Note: Kafka doesn't have individual message nack like RabbitMQ
|
|
66
78
|
// Error handling would be done through retry mechanisms or dead letter topics
|
|
@@ -107,7 +119,15 @@ process.on("warning", (warning) => {
|
|
|
107
119
|
);
|
|
108
120
|
});
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
export type KafkaDependencies = {
|
|
123
|
+
/** Test seam: the manager builds its client through this. */
|
|
124
|
+
createKafka?: (config: ConstructorParameters<typeof Kafka>[0]) => Kafka;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const createKafkaConnectionManager = (
|
|
128
|
+
queueConfig: KafkaConfig,
|
|
129
|
+
{ createKafka = (config) => new Kafka(config) }: KafkaDependencies = {},
|
|
130
|
+
): KafkaConnectionManager => {
|
|
111
131
|
if (queueConfig.type !== "kafka") {
|
|
112
132
|
throw new Error("Invalid queue type for Kafka connection manager");
|
|
113
133
|
}
|
|
@@ -158,7 +178,7 @@ const createKafkaConnectionManager = (queueConfig: KafkaConfig): KafkaConnection
|
|
|
158
178
|
const connConfig = getConnectionConfig();
|
|
159
179
|
|
|
160
180
|
// Create Kafka client with built-in retry
|
|
161
|
-
const kafka =
|
|
181
|
+
const kafka = createKafka({
|
|
162
182
|
clientId: connConfig.clientId ?? `datagraph-${queueConfig.name}`,
|
|
163
183
|
brokers: connConfig.brokers,
|
|
164
184
|
ssl: connConfig.ssl,
|
|
@@ -205,8 +225,29 @@ const createKafkaConnectionManager = (queueConfig: KafkaConfig): KafkaConnection
|
|
|
205
225
|
name: p.name,
|
|
206
226
|
topic: exchange.name,
|
|
207
227
|
send: async (message: string | object, key?: string) => {
|
|
228
|
+
const publisher = `${queueConfig.name}_${p.name}`;
|
|
229
|
+
// No message body on the span — it is caller data.
|
|
230
|
+
const span = startSpan("queue.publish", {
|
|
231
|
+
kind: "producer",
|
|
232
|
+
attributes: {
|
|
233
|
+
"messaging.system": "kafka",
|
|
234
|
+
"messaging.destination.name": exchange.name,
|
|
235
|
+
"graphoria.queue.publisher": publisher,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const record = (outcome: "success" | "error") => {
|
|
239
|
+
incMetric("graphoria_queue_messages_published_total", {
|
|
240
|
+
broker: "kafka",
|
|
241
|
+
publisher,
|
|
242
|
+
outcome,
|
|
243
|
+
});
|
|
244
|
+
if (outcome === "error") span?.setStatus("error");
|
|
245
|
+
span?.end();
|
|
246
|
+
};
|
|
247
|
+
|
|
208
248
|
if (!state.producer) {
|
|
209
249
|
log.error("cannot send: producer not available");
|
|
250
|
+
record("error");
|
|
210
251
|
return false;
|
|
211
252
|
}
|
|
212
253
|
try {
|
|
@@ -220,9 +261,11 @@ const createKafkaConnectionManager = (queueConfig: KafkaConfig): KafkaConnection
|
|
|
220
261
|
},
|
|
221
262
|
],
|
|
222
263
|
});
|
|
264
|
+
record("success");
|
|
223
265
|
return true;
|
|
224
266
|
} catch (error) {
|
|
225
267
|
log.error({ err: error, topic: exchange.name }, "failed to send message");
|
|
268
|
+
record("error");
|
|
226
269
|
return false;
|
|
227
270
|
}
|
|
228
271
|
},
|
package/src/queues/rabbitmq.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { RabbitMQConfig } from "../types/zod/queue";
|
|
|
7
7
|
import { queryEventEmitter } from "../configuration/gql/handleGraphQLSubscriptionFactory";
|
|
8
8
|
import { InvalidationHelper } from "../singletons/cache/registry";
|
|
9
9
|
import { logger } from "../logging";
|
|
10
|
+
import { incMetric } from "../observability/metrics";
|
|
11
|
+
import { startSpan } from "../observability/tracing";
|
|
10
12
|
|
|
11
13
|
// ============================================================================
|
|
12
14
|
// Reconnection Configuration
|
|
@@ -41,6 +43,14 @@ export const startConsumer = async (
|
|
|
41
43
|
) => {
|
|
42
44
|
const log = logger("rabbitmq").child({ queue: connectionName, consumer: name });
|
|
43
45
|
|
|
46
|
+
const record = (outcome: "success" | "error") =>
|
|
47
|
+
incMetric("graphoria_queue_messages_consumed_total", {
|
|
48
|
+
broker: "rabbitmq",
|
|
49
|
+
queue: connectionName,
|
|
50
|
+
consumer: name,
|
|
51
|
+
outcome,
|
|
52
|
+
});
|
|
53
|
+
|
|
44
54
|
channel.consume(queueName, async (msg) => {
|
|
45
55
|
if (msg !== null) {
|
|
46
56
|
try {
|
|
@@ -58,10 +68,12 @@ export const startConsumer = async (
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
channel.ack(msg);
|
|
71
|
+
record("success");
|
|
61
72
|
} catch (error) {
|
|
62
73
|
log.error({ err: error }, "message processing failed");
|
|
63
74
|
|
|
64
75
|
channel.nack(msg);
|
|
76
|
+
record("error");
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
});
|
|
@@ -162,16 +174,40 @@ export const createRabbitMQConnectionManager = (
|
|
|
162
174
|
routingKey: p.routingKey,
|
|
163
175
|
exchangeName: exchange.name,
|
|
164
176
|
send: (message: string | object) => {
|
|
177
|
+
const publisher = `${queueConfig.name}_${p.name}`;
|
|
178
|
+
// No message body on the span — it is caller data.
|
|
179
|
+
const span = startSpan("queue.publish", {
|
|
180
|
+
kind: "producer",
|
|
181
|
+
attributes: {
|
|
182
|
+
"messaging.system": "rabbitmq",
|
|
183
|
+
"messaging.destination.name": exchange.name,
|
|
184
|
+
"graphoria.queue.publisher": publisher,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const record = (outcome: "success" | "error") => {
|
|
188
|
+
incMetric("graphoria_queue_messages_published_total", {
|
|
189
|
+
broker: "rabbitmq",
|
|
190
|
+
publisher,
|
|
191
|
+
outcome,
|
|
192
|
+
});
|
|
193
|
+
if (outcome === "error") span?.setStatus("error");
|
|
194
|
+
span?.end();
|
|
195
|
+
};
|
|
196
|
+
|
|
165
197
|
if (!state.channel) {
|
|
166
198
|
log.error("cannot send: channel not available");
|
|
199
|
+
record("error");
|
|
167
200
|
return false;
|
|
168
201
|
}
|
|
169
|
-
|
|
202
|
+
|
|
203
|
+
const sent = state.channel.publish(
|
|
170
204
|
exchange.name,
|
|
171
205
|
p.routingKey,
|
|
172
206
|
Buffer.from(typeof message === "string" ? message : JSON.stringify(message)),
|
|
173
207
|
p.options,
|
|
174
208
|
);
|
|
209
|
+
record(sent ? "success" : "error");
|
|
210
|
+
return sent;
|
|
175
211
|
},
|
|
176
212
|
};
|
|
177
213
|
}
|
package/src/remoteREST/proxy.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RemoteRESTResolved, RemoteRESTRoute } from "./types";
|
|
2
2
|
|
|
3
|
+
import { formatTraceparent, startSpan } from "../observability/tracing";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Build headers for the remote request by merging:
|
|
5
7
|
* 1. Static headers from configuration
|
|
@@ -62,6 +64,16 @@ export const proxyRemoteRESTRequest = async (
|
|
|
62
64
|
const controller = new AbortController();
|
|
63
65
|
const timeoutId = setTimeout(() => controller.abort(), resolved.config.timeout ?? 10000);
|
|
64
66
|
|
|
67
|
+
// The host only: a remote URL's path and query string carry caller data.
|
|
68
|
+
const span = startSpan("graphoria.remote_rest", {
|
|
69
|
+
kind: "client",
|
|
70
|
+
attributes: {
|
|
71
|
+
"http.request.method": route.method.toUpperCase(),
|
|
72
|
+
"server.address": new URL(targetUrl).host,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (span) headers["traceparent"] = formatTraceparent(span.context);
|
|
76
|
+
|
|
65
77
|
try {
|
|
66
78
|
const fetchInit: RequestInit = {
|
|
67
79
|
method: route.method.toUpperCase(),
|
|
@@ -77,6 +89,7 @@ export const proxyRemoteRESTRequest = async (
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
const response = await fetch(targetUrl, fetchInit);
|
|
92
|
+
span?.setAttribute("http.response.status_code", response.status);
|
|
80
93
|
|
|
81
94
|
// Return the response with the same status and body
|
|
82
95
|
return new Response(response.body, {
|
|
@@ -86,7 +99,11 @@ export const proxyRemoteRESTRequest = async (
|
|
|
86
99
|
"content-type": response.headers.get("content-type") ?? "application/json",
|
|
87
100
|
},
|
|
88
101
|
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
span?.recordError(error);
|
|
104
|
+
throw error;
|
|
89
105
|
} finally {
|
|
106
|
+
span?.end();
|
|
90
107
|
clearTimeout(timeoutId);
|
|
91
108
|
}
|
|
92
109
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SelectionAnalysis } from "../analyzeQuery/types";
|
|
2
2
|
import type { RemoteSchemaResolved } from "./types";
|
|
3
3
|
|
|
4
|
+
import { formatTraceparent, startSpan } from "../observability/tracing";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Build the sub-query string for a remote field by stripping the prefix from
|
|
6
8
|
* the field name and type references.
|
|
@@ -110,6 +112,16 @@ export const proxyRemoteField = async (
|
|
|
110
112
|
const controller = new AbortController();
|
|
111
113
|
const timeoutId = setTimeout(() => controller.abort(), remoteSchema.config.timeout ?? 10000);
|
|
112
114
|
|
|
115
|
+
// The host only: a remote URL's path and query string carry caller data.
|
|
116
|
+
const span = startSpan("graphoria.remote_schema", {
|
|
117
|
+
kind: "client",
|
|
118
|
+
attributes: {
|
|
119
|
+
"http.request.method": "POST",
|
|
120
|
+
"server.address": new URL(remoteSchema.config.url).host,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (span) headers["traceparent"] = formatTraceparent(span.context);
|
|
124
|
+
|
|
113
125
|
try {
|
|
114
126
|
const response = await fetch(remoteSchema.config.url, {
|
|
115
127
|
method: "POST",
|
|
@@ -120,6 +132,7 @@ export const proxyRemoteField = async (
|
|
|
120
132
|
}),
|
|
121
133
|
signal: controller.signal,
|
|
122
134
|
});
|
|
135
|
+
span?.setAttribute("http.response.status_code", response.status);
|
|
123
136
|
|
|
124
137
|
if (!response.ok) {
|
|
125
138
|
throw new Error(
|
|
@@ -140,7 +153,11 @@ export const proxyRemoteField = async (
|
|
|
140
153
|
|
|
141
154
|
// Extract the result for the original field name
|
|
142
155
|
return json.data?.[originalFieldName] ?? null;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
span?.recordError(error);
|
|
158
|
+
throw error;
|
|
143
159
|
} finally {
|
|
160
|
+
span?.end();
|
|
144
161
|
clearTimeout(timeoutId);
|
|
145
162
|
}
|
|
146
163
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { RedisClient as ValkeyClient } from "bun";
|
|
2
|
-
|
|
3
1
|
import { env } from "../env";
|
|
2
|
+
import { createRedisClient } from "../../utils/redis";
|
|
4
3
|
|
|
5
|
-
let client:
|
|
4
|
+
let client: ReturnType<typeof createRedisClient> | null = null;
|
|
6
5
|
|
|
7
|
-
export const getCacheRedisClient = ():
|
|
6
|
+
export const getCacheRedisClient = (): ReturnType<typeof createRedisClient> => {
|
|
8
7
|
if (!client) {
|
|
9
|
-
client =
|
|
8
|
+
client = createRedisClient(env.cache.redisUrl);
|
|
10
9
|
}
|
|
11
10
|
return client;
|
|
12
11
|
};
|