@bleedingdev/modern-js-server-runtime-extensions 0.0.0-trusted-publisher-bootstrap
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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/cjs/contractGateAutopilot.js +162 -0
- package/dist/cjs/contractGateSnapshotStore.js +253 -0
- package/dist/cjs/env.js +58 -0
- package/dist/cjs/index.js +162 -0
- package/dist/cjs/mfCache.js +106 -0
- package/dist/cjs/moduleFederationCss.js +285 -0
- package/dist/cjs/runtimeFallbackSignal.js +311 -0
- package/dist/cjs/telemetry.js +373 -0
- package/dist/cjs/telemetryCore.js +819 -0
- package/dist/esm/contractGateAutopilot.mjs +124 -0
- package/dist/esm/contractGateSnapshotStore.mjs +190 -0
- package/dist/esm/env.mjs +17 -0
- package/dist/esm/index.mjs +6 -0
- package/dist/esm/mfCache.mjs +55 -0
- package/dist/esm/moduleFederationCss.mjs +225 -0
- package/dist/esm/runtimeFallbackSignal.mjs +222 -0
- package/dist/esm/telemetry.mjs +275 -0
- package/dist/esm/telemetryCore.mjs +759 -0
- package/dist/esm-node/contractGateAutopilot.mjs +125 -0
- package/dist/esm-node/contractGateSnapshotStore.mjs +192 -0
- package/dist/esm-node/env.mjs +18 -0
- package/dist/esm-node/index.mjs +7 -0
- package/dist/esm-node/mfCache.mjs +56 -0
- package/dist/esm-node/moduleFederationCss.mjs +226 -0
- package/dist/esm-node/runtimeFallbackSignal.mjs +223 -0
- package/dist/esm-node/telemetry.mjs +276 -0
- package/dist/esm-node/telemetryCore.mjs +760 -0
- package/dist/types/contractGateAutopilot.d.ts +35 -0
- package/dist/types/contractGateSnapshotStore.d.ts +57 -0
- package/dist/types/env.d.ts +40 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mfCache.d.ts +27 -0
- package/dist/types/moduleFederationCss.d.ts +87 -0
- package/dist/types/runtimeFallbackSignal.d.ts +94 -0
- package/dist/types/telemetry.d.ts +12 -0
- package/dist/types/telemetryCore.d.ts +257 -0
- package/package.json +69 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +7 -0
- package/src/contractGateAutopilot.ts +247 -0
- package/src/contractGateSnapshotStore.ts +420 -0
- package/src/env.ts +63 -0
- package/src/index.ts +84 -0
- package/src/mfCache.ts +119 -0
- package/src/moduleFederationCss.ts +473 -0
- package/src/runtimeFallbackSignal.ts +584 -0
- package/src/telemetry.ts +554 -0
- package/src/telemetryCore.ts +1332 -0
- package/tests/contractGateAutopilot.test.ts +203 -0
- package/tests/contractGateSnapshotStore.test.ts +223 -0
- package/tests/env.test.ts +73 -0
- package/tests/helpers.ts +19 -0
- package/tests/mfCache.test.ts +150 -0
- package/tests/moduleFederationCss.test.ts +392 -0
- package/tests/registration.test.ts +112 -0
- package/tests/telemetry.test.ts +360 -0
- package/tests/telemetryAutopilot.test.ts +993 -0
- package/tests/telemetryCanaryOrchestrator.test.ts +140 -0
- package/tests/telemetryLifecycle.test.ts +168 -0
- package/tests/telemetryTraceparent.test.ts +167 -0
- package/tests/tsconfig.json +11 -0
- package/tsconfig.json +10 -0
package/src/telemetry.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { parseTraceparent } from '@modern-js/create-request/server';
|
|
2
|
+
import type {
|
|
3
|
+
Context,
|
|
4
|
+
Next,
|
|
5
|
+
ServerEnv,
|
|
6
|
+
ServerPlugin,
|
|
7
|
+
ServerTelemetryUserConfig,
|
|
8
|
+
} from '@modern-js/server-core';
|
|
9
|
+
import type { CoreMonitor } from '@modern-js/types';
|
|
10
|
+
import { logger } from '@modern-js/utils';
|
|
11
|
+
import { ContractGateAutopilot } from './contractGateAutopilot';
|
|
12
|
+
import {
|
|
13
|
+
type ContractGateSnapshotStore,
|
|
14
|
+
DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH,
|
|
15
|
+
resolveContractGateSnapshotPath,
|
|
16
|
+
resolveContractGateSnapshotStore,
|
|
17
|
+
} from './contractGateSnapshotStore';
|
|
18
|
+
import { parseServerRuntimeExtensionsEnv } from './env';
|
|
19
|
+
import {
|
|
20
|
+
createRuntimeFallbackSignalRuntimeState,
|
|
21
|
+
DEFAULT_RUNTIME_FALLBACK_FAILURE_HOLD_MS,
|
|
22
|
+
DEFAULT_RUNTIME_FALLBACK_GATE_NAME,
|
|
23
|
+
DEFAULT_RUNTIME_FALLBACK_MAX_BODY_BYTES,
|
|
24
|
+
DEFAULT_RUNTIME_STATUS_ENDPOINT,
|
|
25
|
+
enforceRuntimeFallbackSignalAuth,
|
|
26
|
+
enforceRuntimeFallbackSignalAuthToken,
|
|
27
|
+
enforceRuntimeFallbackSignalTrustPolicy,
|
|
28
|
+
getRuntimeSignalErrorStatusCode,
|
|
29
|
+
normalizeRequiredRuntimeFallbackSignalAuthConfig,
|
|
30
|
+
normalizeRuntimeFallbackSignalAuthConfig,
|
|
31
|
+
normalizeRuntimeFallbackTrustPolicy,
|
|
32
|
+
parseRuntimeFallbackSignalPayload,
|
|
33
|
+
persistRuntimeFallbackContractGate,
|
|
34
|
+
type RuntimeFallbackSignalAuthConfig,
|
|
35
|
+
type RuntimeFallbackSignalConfig,
|
|
36
|
+
type RuntimeSignalError,
|
|
37
|
+
resolveRuntimeFallbackSignalEndpoint,
|
|
38
|
+
} from './runtimeFallbackSignal';
|
|
39
|
+
import {
|
|
40
|
+
createOtlpTelemetryExporter,
|
|
41
|
+
createVictoriaMetricsTelemetryExporter,
|
|
42
|
+
maybeWarnLegacyOtlpEndpoint,
|
|
43
|
+
type TelemetryCanaryDecision,
|
|
44
|
+
TelemetryCanaryOrchestrator,
|
|
45
|
+
TelemetryRegistry,
|
|
46
|
+
type TelemetryRegistryOptions,
|
|
47
|
+
toTelemetryEnvelope,
|
|
48
|
+
} from './telemetryCore';
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
createRuntimeFallbackSignalRuntimeState,
|
|
52
|
+
createRuntimeSignalError,
|
|
53
|
+
DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT,
|
|
54
|
+
DEFAULT_RUNTIME_STATUS_ENDPOINT,
|
|
55
|
+
enforceRuntimeFallbackSignalAuthToken,
|
|
56
|
+
enforceRuntimeFallbackSignalTrustPolicy,
|
|
57
|
+
getRuntimeSignalErrorStatusCode,
|
|
58
|
+
normalizeRequiredRuntimeFallbackSignalAuthConfig,
|
|
59
|
+
normalizeRuntimeFallbackSignalAuthConfig,
|
|
60
|
+
normalizeRuntimeFallbackTrustPolicy,
|
|
61
|
+
parseRuntimeFallbackSignalPayloadFromRawBody,
|
|
62
|
+
type RuntimeFallbackSignalAuthConfig,
|
|
63
|
+
type RuntimeFallbackSignalRuntimeState,
|
|
64
|
+
type RuntimeFallbackSignalSource,
|
|
65
|
+
type RuntimeFallbackSignalTrustContext,
|
|
66
|
+
type RuntimeFallbackSignalTrustPolicy,
|
|
67
|
+
type RuntimeSignalError,
|
|
68
|
+
type RuntimeSignalErrorCode,
|
|
69
|
+
resolveRuntimeFallbackSignalEndpoint,
|
|
70
|
+
} from './runtimeFallbackSignal';
|
|
71
|
+
export {
|
|
72
|
+
createOtlpTelemetryExporter,
|
|
73
|
+
createTelemetryAwareMetrics,
|
|
74
|
+
createVictoriaMetricsTelemetryExporter,
|
|
75
|
+
type OtlpExporterOptions,
|
|
76
|
+
type TelemetryCanaryAction,
|
|
77
|
+
type TelemetryCanaryContractGateStatus,
|
|
78
|
+
type TelemetryCanaryDecision,
|
|
79
|
+
type TelemetryCanaryFailure,
|
|
80
|
+
type TelemetryCanaryFailureReason,
|
|
81
|
+
TelemetryCanaryOrchestrator,
|
|
82
|
+
type TelemetryCanaryOrchestratorOptions,
|
|
83
|
+
type TelemetryCanaryState,
|
|
84
|
+
type TelemetryCanaryStatusSnapshot,
|
|
85
|
+
type TelemetryEnvelope,
|
|
86
|
+
type TelemetryExporter,
|
|
87
|
+
type TelemetryExporterHealthStatus,
|
|
88
|
+
type TelemetryQueueStats,
|
|
89
|
+
TelemetryRegistry,
|
|
90
|
+
type TelemetryRegistryOptions,
|
|
91
|
+
type TelemetrySignalType,
|
|
92
|
+
type TelemetrySloAlert,
|
|
93
|
+
type TelemetrySloAlertType,
|
|
94
|
+
TelemetryStartupHealthError,
|
|
95
|
+
type VictoriaMetricsExporterOptions,
|
|
96
|
+
} from './telemetryCore';
|
|
97
|
+
|
|
98
|
+
function emitCanaryDecisionMetric(
|
|
99
|
+
registry: TelemetryRegistry,
|
|
100
|
+
decision: TelemetryCanaryDecision,
|
|
101
|
+
action: 'promote' | 'rollback',
|
|
102
|
+
) {
|
|
103
|
+
try {
|
|
104
|
+
registry.enqueueMetric({
|
|
105
|
+
name: `telemetry.canary.${action}`,
|
|
106
|
+
value: 1,
|
|
107
|
+
unit: 'count',
|
|
108
|
+
tags: {
|
|
109
|
+
action,
|
|
110
|
+
state: decision.state,
|
|
111
|
+
failures: String(decision.failures.length),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
} catch (_error) {
|
|
115
|
+
// Canary decision metrics are best-effort and must never break request flow.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const hasEnabledTelemetryExporters = (
|
|
120
|
+
config: ServerTelemetryUserConfig | undefined,
|
|
121
|
+
) =>
|
|
122
|
+
Boolean(
|
|
123
|
+
config?.exporters?.otlp?.enabled ||
|
|
124
|
+
config?.exporters?.victoriaMetrics?.enabled,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds the registry SLO options from `server.telemetry.slo`, attaching a
|
|
129
|
+
* logger sink so threshold breaches surface as server warnings (parity with
|
|
130
|
+
* the pre-extraction prod-server harness).
|
|
131
|
+
*/
|
|
132
|
+
export const resolveTelemetrySloOptions = (
|
|
133
|
+
sloConfig: ServerTelemetryUserConfig['slo'],
|
|
134
|
+
warnSink: (message: string) => void = message => logger.warn(message),
|
|
135
|
+
): NonNullable<TelemetryRegistryOptions['slo']> => ({
|
|
136
|
+
queueUtilizationWarnThreshold: sloConfig?.queueUtilizationWarnThreshold,
|
|
137
|
+
queueDroppedWarnThreshold: sloConfig?.queueDroppedWarnThreshold,
|
|
138
|
+
alertCooldownMs: sloConfig?.alertCooldownMs,
|
|
139
|
+
onAlert: alert => {
|
|
140
|
+
warnSink(
|
|
141
|
+
`[telemetry.slo] ${alert.type} threshold=${alert.threshold} value=${alert.value} depth=${alert.queueDepth}/${alert.queueCapacity} dropped=${alert.totalDropped}`,
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const getRequestRemoteAddress = (c: Context<ServerEnv>) => {
|
|
147
|
+
const env = c.env as
|
|
148
|
+
| {
|
|
149
|
+
node?: {
|
|
150
|
+
req?: {
|
|
151
|
+
socket?: {
|
|
152
|
+
remoteAddress?: string;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
| undefined;
|
|
158
|
+
const remoteAddress = env?.node?.req?.socket?.remoteAddress;
|
|
159
|
+
return typeof remoteAddress === 'string' && remoteAddress.trim().length > 0
|
|
160
|
+
? remoteAddress.trim()
|
|
161
|
+
: undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Active telemetry lane disposers, flushed by a single shared process
|
|
166
|
+
* `beforeExit` hook (a per-lane listener would accumulate listeners across
|
|
167
|
+
* dev-server restarts and embedded multi-server setups).
|
|
168
|
+
*/
|
|
169
|
+
const activeTelemetryLaneClosers = new Set<() => Promise<void>>();
|
|
170
|
+
let telemetryBeforeExitHookInstalled = false;
|
|
171
|
+
const ensureTelemetryBeforeExitHook = () => {
|
|
172
|
+
if (telemetryBeforeExitHookInstalled) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
telemetryBeforeExitHookInstalled = true;
|
|
176
|
+
process.on('beforeExit', () => {
|
|
177
|
+
for (const close of [...activeTelemetryLaneClosers]) {
|
|
178
|
+
void close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const injectTelemetryPlugin = (): ServerPlugin => ({
|
|
184
|
+
name: '@modern-js/inject-telemetry',
|
|
185
|
+
setup(api) {
|
|
186
|
+
const serverConfig = api.getServerConfig();
|
|
187
|
+
const telemetryConfig = serverConfig?.server?.telemetry;
|
|
188
|
+
if (!telemetryConfig) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
telemetryConfig.enabled !== true &&
|
|
194
|
+
!hasEnabledTelemetryExporters(telemetryConfig)
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { middlewares, metaName, appDirectory } = api.getServerContext();
|
|
200
|
+
const serviceName = telemetryConfig.service || metaName || 'modern-js';
|
|
201
|
+
const moduleName = telemetryConfig.module || 'server';
|
|
202
|
+
const environmentName =
|
|
203
|
+
telemetryConfig.environment ||
|
|
204
|
+
parseServerRuntimeExtensionsEnv().environmentName;
|
|
205
|
+
|
|
206
|
+
const registry = new TelemetryRegistry({
|
|
207
|
+
service: serviceName,
|
|
208
|
+
module: moduleName,
|
|
209
|
+
environment: environmentName,
|
|
210
|
+
samplingRate: telemetryConfig.samplingRate,
|
|
211
|
+
flushIntervalMs: telemetryConfig.flushIntervalMs,
|
|
212
|
+
maxBatchSize: telemetryConfig.maxBatchSize,
|
|
213
|
+
maxQueueSize: telemetryConfig.maxQueueSize,
|
|
214
|
+
redactionKeys: telemetryConfig.redactionKeys,
|
|
215
|
+
slo: resolveTelemetrySloOptions(telemetryConfig.slo),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
let canaryOrchestrator: TelemetryCanaryOrchestrator | undefined;
|
|
219
|
+
let contractGateAutopilot: ContractGateAutopilot | undefined;
|
|
220
|
+
let runtimeFallbackSignalConfig: RuntimeFallbackSignalConfig | undefined;
|
|
221
|
+
let runtimeStatusAuthConfig: RuntimeFallbackSignalAuthConfig | undefined;
|
|
222
|
+
let gateSnapshotStorePromise:
|
|
223
|
+
| Promise<ContractGateSnapshotStore>
|
|
224
|
+
| undefined;
|
|
225
|
+
|
|
226
|
+
const canaryConfig = telemetryConfig.canary;
|
|
227
|
+
if (canaryConfig?.enabled) {
|
|
228
|
+
const contractGates = canaryConfig.contractGates as
|
|
229
|
+
| Record<string, boolean | { passed: boolean; reason?: string }>
|
|
230
|
+
| undefined;
|
|
231
|
+
|
|
232
|
+
canaryOrchestrator = new TelemetryCanaryOrchestrator({
|
|
233
|
+
registry,
|
|
234
|
+
evaluationIntervalMs: canaryConfig.evaluationIntervalMs,
|
|
235
|
+
minConsecutiveHealthyEvaluations:
|
|
236
|
+
canaryConfig.minConsecutiveHealthyEvaluations,
|
|
237
|
+
rollbackConsecutiveFailures: canaryConfig.rollbackConsecutiveFailures,
|
|
238
|
+
maxQueueUtilization: canaryConfig.maxQueueUtilization,
|
|
239
|
+
maxTotalDropped: canaryConfig.maxTotalDropped,
|
|
240
|
+
maxUnhealthyExporters: canaryConfig.maxUnhealthyExporters,
|
|
241
|
+
requiredContractGates: Object.keys(contractGates || {}),
|
|
242
|
+
onPromote: decision => {
|
|
243
|
+
emitCanaryDecisionMetric(registry, decision, 'promote');
|
|
244
|
+
},
|
|
245
|
+
onRollback: decision => {
|
|
246
|
+
emitCanaryDecisionMetric(registry, decision, 'rollback');
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (contractGates) {
|
|
251
|
+
canaryOrchestrator.setContractGates(contractGates);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const autopilotEnabled = canaryConfig.autopilot?.enabled ?? true;
|
|
255
|
+
if (autopilotEnabled) {
|
|
256
|
+
const gateSnapshotPath = resolveContractGateSnapshotPath(
|
|
257
|
+
appDirectory,
|
|
258
|
+
canaryConfig.autopilot?.gateSnapshotPath,
|
|
259
|
+
);
|
|
260
|
+
gateSnapshotStorePromise = resolveContractGateSnapshotStore({
|
|
261
|
+
appDirectory,
|
|
262
|
+
gateSnapshotPath:
|
|
263
|
+
gateSnapshotPath || DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH,
|
|
264
|
+
stateStore: canaryConfig.autopilot?.stateStore,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const runtimeSignalConfig =
|
|
268
|
+
canaryConfig.autopilot?.runtimeFallbackSignal;
|
|
269
|
+
// The signal endpoint can persist failing contract gates and force
|
|
270
|
+
// the canary orchestrator into rollback, so it is opt-in (default
|
|
271
|
+
// OFF) and requires a configured auth token when enabled.
|
|
272
|
+
const runtimeSignalEnabled = runtimeSignalConfig?.enabled === true;
|
|
273
|
+
if (runtimeSignalEnabled && gateSnapshotStorePromise) {
|
|
274
|
+
runtimeFallbackSignalConfig = {
|
|
275
|
+
endpoint: resolveRuntimeFallbackSignalEndpoint(
|
|
276
|
+
runtimeSignalConfig?.endpoint,
|
|
277
|
+
),
|
|
278
|
+
gateName:
|
|
279
|
+
runtimeSignalConfig?.gateName?.trim() ||
|
|
280
|
+
DEFAULT_RUNTIME_FALLBACK_GATE_NAME,
|
|
281
|
+
gateSnapshotStore: gateSnapshotStorePromise,
|
|
282
|
+
failureHoldMs: Math.max(
|
|
283
|
+
1_000,
|
|
284
|
+
runtimeSignalConfig?.failureHoldMs ??
|
|
285
|
+
DEFAULT_RUNTIME_FALLBACK_FAILURE_HOLD_MS,
|
|
286
|
+
),
|
|
287
|
+
maxBodyBytes: Math.max(
|
|
288
|
+
512,
|
|
289
|
+
runtimeSignalConfig?.maxBodyBytes ??
|
|
290
|
+
DEFAULT_RUNTIME_FALLBACK_MAX_BODY_BYTES,
|
|
291
|
+
),
|
|
292
|
+
auth: normalizeRequiredRuntimeFallbackSignalAuthConfig(
|
|
293
|
+
runtimeSignalConfig?.auth,
|
|
294
|
+
),
|
|
295
|
+
trustPolicy: normalizeRuntimeFallbackTrustPolicy(
|
|
296
|
+
runtimeSignalConfig?.trustPolicy,
|
|
297
|
+
),
|
|
298
|
+
runtimeState: createRuntimeFallbackSignalRuntimeState(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// The runtime status endpoint exposes detail only to authenticated
|
|
304
|
+
// callers. Auth can be configured through runtimeFallbackSignal.auth
|
|
305
|
+
// even when the signal endpoint itself stays disabled.
|
|
306
|
+
runtimeStatusAuthConfig =
|
|
307
|
+
runtimeFallbackSignalConfig?.auth ??
|
|
308
|
+
normalizeRuntimeFallbackSignalAuthConfig(
|
|
309
|
+
canaryConfig.autopilot?.runtimeFallbackSignal?.auth,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (runtimeFallbackSignalConfig) {
|
|
314
|
+
const signalConfig = runtimeFallbackSignalConfig;
|
|
315
|
+
middlewares.push({
|
|
316
|
+
name: 'telemetry-runtime-fallback-signal',
|
|
317
|
+
path: signalConfig.endpoint,
|
|
318
|
+
method: 'post',
|
|
319
|
+
order: 'pre',
|
|
320
|
+
handler: async (c: Context<ServerEnv>) => {
|
|
321
|
+
try {
|
|
322
|
+
enforceRuntimeFallbackSignalAuth(c, signalConfig);
|
|
323
|
+
const { payload } = await parseRuntimeFallbackSignalPayload(
|
|
324
|
+
c,
|
|
325
|
+
signalConfig.maxBodyBytes,
|
|
326
|
+
);
|
|
327
|
+
const trustResult = enforceRuntimeFallbackSignalTrustPolicy(
|
|
328
|
+
payload,
|
|
329
|
+
signalConfig,
|
|
330
|
+
{
|
|
331
|
+
remoteAddress: getRequestRemoteAddress(c),
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
if (trustResult.deduped) {
|
|
335
|
+
return c.json({ ok: true, deduped: true }, 202);
|
|
336
|
+
}
|
|
337
|
+
await persistRuntimeFallbackContractGate(payload, signalConfig);
|
|
338
|
+
return c.json({ ok: true }, 202);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const signalError = error as RuntimeSignalError;
|
|
341
|
+
const status = getRuntimeSignalErrorStatusCode(signalError);
|
|
342
|
+
return c.json(
|
|
343
|
+
{
|
|
344
|
+
ok: false,
|
|
345
|
+
error:
|
|
346
|
+
signalError instanceof Error
|
|
347
|
+
? signalError.message
|
|
348
|
+
: String(signalError),
|
|
349
|
+
},
|
|
350
|
+
status,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
middlewares.push({
|
|
358
|
+
name: 'telemetry-runtime-status',
|
|
359
|
+
path: DEFAULT_RUNTIME_STATUS_ENDPOINT,
|
|
360
|
+
method: 'get',
|
|
361
|
+
order: 'pre',
|
|
362
|
+
handler: async (c: Context<ServerEnv>) => {
|
|
363
|
+
try {
|
|
364
|
+
// Telemetry/canary/trust internals are only disclosed to
|
|
365
|
+
// authenticated callers. Without a configured auth token the
|
|
366
|
+
// endpoint stays a bare health probe.
|
|
367
|
+
if (!runtimeStatusAuthConfig?.enabled) {
|
|
368
|
+
return c.json({
|
|
369
|
+
ok: true,
|
|
370
|
+
timestamp: Date.now(),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
enforceRuntimeFallbackSignalAuthToken(
|
|
375
|
+
c.req.header(runtimeStatusAuthConfig.headerName),
|
|
376
|
+
runtimeStatusAuthConfig,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return c.json({
|
|
380
|
+
ok: true,
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
telemetry: {
|
|
383
|
+
queueStats: registry.getQueueStats(),
|
|
384
|
+
exporterHealth: registry.getExporterHealth(),
|
|
385
|
+
},
|
|
386
|
+
canary: canaryOrchestrator
|
|
387
|
+
? {
|
|
388
|
+
enabled: true,
|
|
389
|
+
...canaryOrchestrator.getStatusSnapshot(),
|
|
390
|
+
}
|
|
391
|
+
: {
|
|
392
|
+
enabled: false,
|
|
393
|
+
},
|
|
394
|
+
runtimeFallbackSignal: runtimeFallbackSignalConfig
|
|
395
|
+
? {
|
|
396
|
+
enabled: true,
|
|
397
|
+
endpoint: runtimeFallbackSignalConfig.endpoint,
|
|
398
|
+
gateName: runtimeFallbackSignalConfig.gateName,
|
|
399
|
+
failureHoldMs: runtimeFallbackSignalConfig.failureHoldMs,
|
|
400
|
+
maxBodyBytes: runtimeFallbackSignalConfig.maxBodyBytes,
|
|
401
|
+
auth: {
|
|
402
|
+
enabled: runtimeFallbackSignalConfig.auth.enabled,
|
|
403
|
+
headerName: runtimeFallbackSignalConfig.auth.headerName,
|
|
404
|
+
},
|
|
405
|
+
trustPolicy: {
|
|
406
|
+
allowedApps:
|
|
407
|
+
runtimeFallbackSignalConfig.trustPolicy.allowedApps,
|
|
408
|
+
allowedEntryOrigins:
|
|
409
|
+
runtimeFallbackSignalConfig.trustPolicy
|
|
410
|
+
.allowedEntryOrigins,
|
|
411
|
+
enforceRuntimeDigest:
|
|
412
|
+
runtimeFallbackSignalConfig.trustPolicy
|
|
413
|
+
.enforceRuntimeDigest,
|
|
414
|
+
expectedRuntimeDigestsCount: Object.keys(
|
|
415
|
+
runtimeFallbackSignalConfig.trustPolicy
|
|
416
|
+
.expectedRuntimeDigests,
|
|
417
|
+
).length,
|
|
418
|
+
maxSignalsPerWindow:
|
|
419
|
+
runtimeFallbackSignalConfig.trustPolicy
|
|
420
|
+
.maxSignalsPerWindow,
|
|
421
|
+
windowMs: runtimeFallbackSignalConfig.trustPolicy.windowMs,
|
|
422
|
+
dedupeWindowMs:
|
|
423
|
+
runtimeFallbackSignalConfig.trustPolicy.dedupeWindowMs,
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
: {
|
|
427
|
+
enabled: false,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const signalError = error as RuntimeSignalError;
|
|
432
|
+
return c.json(
|
|
433
|
+
{
|
|
434
|
+
ok: false,
|
|
435
|
+
error:
|
|
436
|
+
signalError instanceof Error
|
|
437
|
+
? signalError.message
|
|
438
|
+
: String(signalError),
|
|
439
|
+
},
|
|
440
|
+
getRuntimeSignalErrorStatusCode(signalError),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
middlewares.push({
|
|
447
|
+
name: 'inject-telemetry',
|
|
448
|
+
handler: async (c: Context<ServerEnv>, next: Next) => {
|
|
449
|
+
const monitors = c.get('monitors');
|
|
450
|
+
if (monitors) {
|
|
451
|
+
const traceContext = parseTraceparent(c.req.header('traceparent'));
|
|
452
|
+
const monitor: CoreMonitor = event => {
|
|
453
|
+
registry.enqueue(
|
|
454
|
+
toTelemetryEnvelope(event, {
|
|
455
|
+
service: serviceName,
|
|
456
|
+
module: moduleName,
|
|
457
|
+
environment: environmentName,
|
|
458
|
+
traceId: traceContext?.traceId,
|
|
459
|
+
spanId: traceContext?.spanId,
|
|
460
|
+
attributes: {
|
|
461
|
+
requestMethod: c.req.method,
|
|
462
|
+
requestPath: c.req.path,
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
monitors.push(monitor);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await next();
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let telemetryLaneClosed = false;
|
|
475
|
+
const closeTelemetryLane = async () => {
|
|
476
|
+
if (telemetryLaneClosed) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
telemetryLaneClosed = true;
|
|
480
|
+
activeTelemetryLaneClosers.delete(closeTelemetryLane);
|
|
481
|
+
contractGateAutopilot?.stop();
|
|
482
|
+
canaryOrchestrator?.stop();
|
|
483
|
+
await registry.shutdown();
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
let prepared = false;
|
|
487
|
+
api.onPrepare(async () => {
|
|
488
|
+
if (prepared) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
prepared = true;
|
|
492
|
+
|
|
493
|
+
// Shutdown path for the telemetry lane: flush pending envelopes and
|
|
494
|
+
// stop canary/autopilot pollers when the node server closes (this also
|
|
495
|
+
// covers dev-server restarts, which close the previous node server
|
|
496
|
+
// before assembling a new one) with process beforeExit as the
|
|
497
|
+
// final-flush floor when no node server handle exists.
|
|
498
|
+
const { nodeServer } = api.getServerContext() as {
|
|
499
|
+
nodeServer?: {
|
|
500
|
+
once?: (event: string, listener: () => void) => unknown;
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
if (nodeServer && typeof nodeServer.once === 'function') {
|
|
504
|
+
nodeServer.once('close', () => {
|
|
505
|
+
void closeTelemetryLane();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
activeTelemetryLaneClosers.add(closeTelemetryLane);
|
|
509
|
+
ensureTelemetryBeforeExitHook();
|
|
510
|
+
|
|
511
|
+
if (telemetryConfig.exporters?.otlp?.enabled) {
|
|
512
|
+
maybeWarnLegacyOtlpEndpoint(telemetryConfig.exporters.otlp.endpoint);
|
|
513
|
+
await registry.register(
|
|
514
|
+
createOtlpTelemetryExporter(telemetryConfig.exporters.otlp),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (telemetryConfig.exporters?.victoriaMetrics?.enabled) {
|
|
519
|
+
await registry.register(
|
|
520
|
+
createVictoriaMetricsTelemetryExporter(
|
|
521
|
+
telemetryConfig.exporters.victoriaMetrics,
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
await registry.startupHealthCheck({
|
|
527
|
+
failLoud: telemetryConfig.failLoudStartup ?? true,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (!canaryOrchestrator) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
canaryOrchestrator.start();
|
|
535
|
+
if (gateSnapshotStorePromise) {
|
|
536
|
+
const gateSnapshotStore = await gateSnapshotStorePromise;
|
|
537
|
+
contractGateAutopilot = new ContractGateAutopilot({
|
|
538
|
+
orchestrator: canaryOrchestrator,
|
|
539
|
+
gateSnapshotPath: resolveContractGateSnapshotPath(
|
|
540
|
+
appDirectory,
|
|
541
|
+
canaryConfig?.autopilot?.gateSnapshotPath,
|
|
542
|
+
),
|
|
543
|
+
gateSnapshotStore,
|
|
544
|
+
pollIntervalMs: canaryConfig?.autopilot?.pollIntervalMs,
|
|
545
|
+
gateStaleAfterMs: canaryConfig?.autopilot?.gateStaleAfterMs,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (contractGateAutopilot) {
|
|
549
|
+
await contractGateAutopilot.start();
|
|
550
|
+
}
|
|
551
|
+
canaryOrchestrator.evaluate();
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
});
|