feat(sentinel): restore JD01 cadence cronjob visibility
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// 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<string, unknown>;
|
||||
readonly cicd?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function emitWebProbeSentinelSpan(context: SentinelOtelContext, name: string, attributes: Record<string, unknown> = {}, 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<string, unknown> {
|
||||
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.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<string, unknown>): readonly Record<string, unknown>[] {
|
||||
return Object.entries(values)
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => ({ key, value: otelValue(value) }));
|
||||
}
|
||||
|
||||
function otelValue(value: unknown): Record<string, unknown> {
|
||||
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<string, unknown>)[segment];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
Reference in New Issue
Block a user