// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel. // Responsibility: Best-effort OTLP span emitter for web-probe sentinel scheduler, cadence and quick-verify events. import { randomBytes } from "node:crypto"; export interface SentinelOtelContext { readonly node: string; readonly lane: string; readonly sentinelId: string; readonly namespace?: string | null; readonly runtime?: Record; readonly cicd?: Record; } export function emitWebProbeSentinelSpan(context: SentinelOtelContext, name: string, attributes: Record = {}, ok = true): void { const config = resolveOtelConfig(context); if (!config.enabled || config.endpoint === null) return; const start = BigInt(Date.now()) * 1_000_000n; const end = start + 1_000_000n; const traceId = randomHex(16); const spanId = randomHex(8); const payload = { resourceSpans: [{ resource: { attributes: otelAttributes({ "service.name": config.serviceName, "deployment.environment": context.lane, "unidesk.node": context.node, "hwlab.lane": context.lane, "k8s.namespace.name": context.namespace ?? stringAtNullable(context.runtime, "namespace"), "unidesk.values_redacted": true, }), }, scopeSpans: [{ scope: { name: "unidesk.web_probe_sentinel", version: "PJ2026-01060508" }, spans: [{ traceId, spanId, name, kind: 1, startTimeUnixNano: start.toString(), endTimeUnixNano: end.toString(), attributes: otelAttributes({ "unidesk.node": context.node, "hwlab.lane": context.lane, "sentinelId": context.sentinelId, "valuesRedacted": true, ...attributes, }), status: { code: ok ? 1 : 2 }, }], }], }], }; void fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), }).catch(() => undefined); } export function webProbeSentinelOtelSummary(context: SentinelOtelContext): Record { const config = resolveOtelConfig(context); return { enabled: config.enabled, endpointConfigured: config.endpoint !== null, serviceName: config.serviceName, coverage: config.enabled && config.endpoint !== null ? "best-effort-otlp-spans" : "instrumentation-gap", expectedSpans: [ "web_probe_sentinel.scheduler.heartbeat", "web_probe_sentinel.cadence.expected", "web_probe_sentinel.cadence.cronjob_rendered", "web_probe_sentinel.cadence.cronjob_observed", "web_probe_sentinel.manual_trigger", "web_probe_sentinel.quick_verify.job_start", "web_probe_sentinel.quick_verify.job_finish", "web_probe_sentinel.record_run", "web_probe_sentinel.scheduler_gap.detected", ], valuesRedacted: true, }; } function resolveOtelConfig(context: SentinelOtelContext): { readonly enabled: boolean; readonly endpoint: string | null; readonly serviceName: string } { const runtime = context.runtime ?? {}; const cicd = context.cicd ?? {}; const enabledFromYaml = booleanAtNullable(runtime, "observability.otel.enabled") ?? booleanAtNullable(cicd, "observability.otel.enabled"); const disabledByEnv = /^(1|true)$/iu.test(process.env.OTEL_SDK_DISABLED ?? ""); const endpoint = stringAtNullable(runtime, "observability.otel.tracesEndpoint") ?? stringAtNullable(runtime, "observability.otel.endpoint") ?? stringAtNullable(cicd, "observability.otel.tracesEndpoint") ?? stringAtNullable(cicd, "observability.otel.endpoint") ?? nonEmptyString(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT); const serviceName = stringAtNullable(runtime, "observability.otel.serviceName") ?? stringAtNullable(cicd, "observability.otel.serviceName") ?? nonEmptyString(process.env.OTEL_SERVICE_NAME) ?? `hwlab-web-probe-sentinel-${context.node.toLowerCase()}`; return { enabled: !disabledByEnv && (enabledFromYaml === true || endpoint !== null), endpoint, serviceName, }; } function otelAttributes(values: Record): readonly Record[] { return Object.entries(values) .filter(([, value]) => value !== undefined && value !== null) .map(([key, value]) => ({ key, value: otelValue(value) })); } function otelValue(value: unknown): Record { if (typeof value === "boolean") return { boolValue: value }; if (typeof value === "number" && Number.isFinite(value)) { return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value }; } return { stringValue: typeof value === "string" ? value : JSON.stringify(value) }; } function randomHex(bytes: number): string { return randomBytes(bytes).toString("hex"); } function stringAtNullable(value: unknown, path: string): string | null { const found = valueAtPath(value, path); return typeof found === "string" && found.length > 0 ? found : null; } function booleanAtNullable(value: unknown, path: string): boolean | null { const found = valueAtPath(value, path); return typeof found === "boolean" ? found : null; } function nonEmptyString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function valueAtPath(value: unknown, path: string): unknown { let current: unknown = value; for (const segment of path.split(".")) { if (typeof current !== "object" || current === null || Array.isArray(current)) return undefined; current = (current as Record)[segment]; } return current; }