mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-04 13:27:11 +00:00
461 lines
14 KiB
JavaScript
461 lines
14 KiB
JavaScript
// Generate a deployment-scoped dashboard and local-only alert provisioning.
|
|
// Same expressions serve the shipped reference and the short fault qualification.
|
|
import { createHash } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { resolve, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export function build(options = {}) {
|
|
const o = {
|
|
deployment: "default",
|
|
expectedSlots: 2,
|
|
freshSeconds: 45,
|
|
queueSeconds: 120,
|
|
pendingSeconds: 60,
|
|
intervalSeconds: 10,
|
|
errorWindowSeconds: 300,
|
|
...options,
|
|
};
|
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(o.deployment))
|
|
throw Error("Invalid deployment identifier");
|
|
for (const key of [
|
|
"expectedSlots",
|
|
"freshSeconds",
|
|
"queueSeconds",
|
|
"pendingSeconds",
|
|
"intervalSeconds",
|
|
"errorWindowSeconds",
|
|
])
|
|
if (!Number.isInteger(o[key]) || o[key] < 1 || o[key] > 86400)
|
|
throw Error(`Invalid ${key}`);
|
|
if (o.intervalSeconds % 10)
|
|
throw Error("Grafana evaluation interval must be a multiple of 10 seconds");
|
|
const id = `ow-${createHash("sha256").update(o.deployment).digest("hex").slice(0, 12)}`;
|
|
// Stable per mounted reference file: Grafana cannot change a provisioned
|
|
// dashboard's UID in place when deployment configuration is regenerated.
|
|
const dashboardId = "openworker-operations";
|
|
const select = (m) =>
|
|
`(${m} and on(job,instance) target_info{job="openworker",openworker_deployment="${o.deployment}"})`;
|
|
const fresh = (m) => `(${select(m)} > time() - ${o.freshSeconds})`;
|
|
const heartbeat = fresh(
|
|
"openworker_server_sampler_heartbeat_timestamp_seconds",
|
|
);
|
|
const stateFresh = `(${fresh("openworker_server_state_observed_timestamp_seconds")} and on(job,instance) (${select("openworker_server_database_healthy")} == 1) and on(job,instance) ${heartbeat})`;
|
|
const state = (m) =>
|
|
`max(${select(`openworker_server_${m}`)} and on(job,instance) ${stateFresh})`;
|
|
const worker = (m) =>
|
|
`sum(${select(`openworker_worker_${m}`)} and on(job,instance) ${fresh("openworker_worker_heartbeat_timestamp_seconds")})`;
|
|
const errors = (operation) =>
|
|
`(sum((${select(`openworker_operation_last_error_timestamp_seconds{openworker_operation="${operation}"}`)} > bool time() - ${o.errorWindowSeconds})) or vector(0))`;
|
|
const q = {
|
|
monitoringMissing: `(absent(${heartbeat}) or vector(0))`,
|
|
databaseUnhealthy: `min(${select("openworker_server_database_healthy")} and on(job,instance) ${heartbeat})`,
|
|
active: state("sessions_active"),
|
|
queued: state("turns_queued"),
|
|
approvals: state("approvals_pending"),
|
|
questions: state("questions_pending"),
|
|
queueAge: state("oldest_queued_seconds"),
|
|
stalled: state("sessions_stalled"),
|
|
unknown: state("unknown_recent"),
|
|
capacity: `(${worker("capacity")} or vector(0))`,
|
|
occupied: worker("active"),
|
|
stateAge: `time() - max(${select("openworker_server_state_observed_timestamp_seconds")})`,
|
|
stateMissing: `(absent(${stateFresh}) or vector(0))`,
|
|
modelErrors: errors("model.attempt"),
|
|
toolErrors: errors("tool.execute"),
|
|
drops: `(sum(delta(openworker_telemetry_dropped[${o.errorWindowSeconds}s]) and on(job,instance) target_info{job="openworker",openworker_deployment="${o.deployment}"}) or vector(0))`,
|
|
exportFailures: `(sum(delta(openworker_telemetry_export_failures[${o.errorWindowSeconds}s]) and on(job,instance) target_info{job="openworker",openworker_deployment="${o.deployment}"}) or vector(0))`,
|
|
};
|
|
const definitions = [
|
|
[
|
|
"monitoring",
|
|
"Telemetry missing",
|
|
q.monitoringMissing,
|
|
">",
|
|
0,
|
|
"critical",
|
|
"Check sampler/API process, collector and network. Missing data is unknown, not an empty queue.",
|
|
],
|
|
[
|
|
"database",
|
|
"Database observer unavailable",
|
|
q.databaseUnhealthy,
|
|
"<",
|
|
1,
|
|
"critical",
|
|
"Check PostgreSQL reachability and bounded query latency. Restart the failed sampler/API after repair; this alert does not prove every API request failed.",
|
|
],
|
|
[
|
|
"state",
|
|
"Durable state observation stale",
|
|
q.stateMissing,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Do not trust queue or approval counts. Check observer, database and telemetry delivery.",
|
|
],
|
|
[
|
|
"capacity",
|
|
"Worker capacity below target",
|
|
q.capacity,
|
|
"<",
|
|
o.expectedSlots,
|
|
"critical",
|
|
"Inspect worker supervision, database and OpenShell. Restart only after checking durable lease and unknown-effect state. Capacity is advertised, not an end-to-end readiness check.",
|
|
],
|
|
[
|
|
"queue",
|
|
"Queued work aging",
|
|
q.queueAge,
|
|
">",
|
|
o.queueSeconds,
|
|
"warning",
|
|
"Check compatible pool capacity, permissions and parked work. Do not blindly resubmit turns.",
|
|
],
|
|
[
|
|
"stalled",
|
|
"Potentially stalled runs",
|
|
q.stalled,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Inspect durable progress and long-running tools. Waiting approvals/questions are excluded. Never auto-retry an uncertain external effect.",
|
|
],
|
|
[
|
|
"unknown",
|
|
"Recent uncertain effects",
|
|
q.unknown,
|
|
">",
|
|
0,
|
|
"critical",
|
|
"Inspect execution records and reconcile the external system before retrying. Clearing means no recent transitions, NOT that earlier effects were resolved.",
|
|
],
|
|
[
|
|
"model",
|
|
"Observed model errors",
|
|
q.modelErrors,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Inspect sanitized traces/provider health and retry outcomes. Counts processes with a recent exported error, not errors or billing. The first error is detectable without a counter baseline.",
|
|
],
|
|
[
|
|
"tool",
|
|
"Observed tool errors",
|
|
q.toolErrors,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Inspect durable tool result and environment health; distinguish a safe failure from an uncertain effect.",
|
|
],
|
|
[
|
|
"drops",
|
|
"Telemetry records dropped",
|
|
q.drops,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Inspect collector latency/backpressure. The agent is not blocked by telemetry; exported evidence may be incomplete.",
|
|
],
|
|
[
|
|
"export",
|
|
"Telemetry export failures",
|
|
q.exportFailures,
|
|
">",
|
|
0,
|
|
"warning",
|
|
"Check collector and TLS configuration. During complete export failure rely on the missing-telemetry alert instead.",
|
|
],
|
|
];
|
|
const rules = definitions.map(
|
|
([name, title, expr, cmp, threshold, severity, description]) => ({
|
|
uid: `${id}-${name}`,
|
|
title: `${o.deployment} · ${title}`,
|
|
condition: "B",
|
|
data: [
|
|
{
|
|
refId: "A",
|
|
datasourceUid: "prometheus",
|
|
relativeTimeRange: { from: 600, to: 0 },
|
|
model: {
|
|
refId: "A",
|
|
expr,
|
|
instant: true,
|
|
range: false,
|
|
intervalMs: 1000,
|
|
maxDataPoints: 43200,
|
|
},
|
|
},
|
|
{
|
|
refId: "B",
|
|
datasourceUid: "__expr__",
|
|
relativeTimeRange: { from: 0, to: 0 },
|
|
model: {
|
|
refId: "B",
|
|
type: "math",
|
|
expression: `$A ${cmp} ${threshold}`,
|
|
},
|
|
},
|
|
],
|
|
noDataState: "OK",
|
|
execErrState: "Error",
|
|
for: `${o.pendingSeconds}s`,
|
|
annotations: {
|
|
summary: title,
|
|
description,
|
|
runbook: "deploy/reference/OPERATIONS.md",
|
|
__dashboardUid__: dashboardId,
|
|
__panelId__: "13",
|
|
},
|
|
labels: { service: "openworker", deployment: o.deployment, severity },
|
|
isPaused: false,
|
|
}),
|
|
);
|
|
// Grafana file provisioning cannot create a contact point without receivers.
|
|
// An always-muted, loopback-only placeholder prevents external notifications.
|
|
// This policy belongs ONLY in the owned single-tenant reference Grafana.
|
|
const alerts = {
|
|
apiVersion: 1,
|
|
groups: [
|
|
{
|
|
orgId: 1,
|
|
name: `${id}-operations`,
|
|
folder: "OpenWorker",
|
|
interval: `${o.intervalSeconds}s`,
|
|
rules,
|
|
},
|
|
],
|
|
contactPoints: [
|
|
{
|
|
orgId: 1,
|
|
name: "OpenWorker local only",
|
|
receivers: [
|
|
{
|
|
uid: "ow-local-muted",
|
|
type: "webhook",
|
|
disableResolveMessage: true,
|
|
settings: { url: "http://127.0.0.1:9" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
muteTimes: [
|
|
{
|
|
orgId: 1,
|
|
name: "openworker-local-muted",
|
|
time_intervals: [
|
|
{ times: [{ start_time: "00:00", end_time: "24:00" }] },
|
|
],
|
|
},
|
|
],
|
|
policies: [
|
|
{
|
|
orgId: 1,
|
|
receiver: "OpenWorker local only",
|
|
group_by: ["alertname", "deployment"],
|
|
routes: [
|
|
{
|
|
receiver: "OpenWorker local only",
|
|
mute_time_intervals: ["openworker-local-muted"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const panels = [];
|
|
const stat = (title, expr, x, y, description, unit = "short") =>
|
|
panels.push({
|
|
id: panels.length + 1,
|
|
title,
|
|
type: "stat",
|
|
description,
|
|
datasource: { type: "prometheus", uid: "prometheus" },
|
|
gridPos: { x, y, w: 6, h: 4 },
|
|
targets: [{ refId: "A", expr, instant: true }],
|
|
fieldConfig: {
|
|
defaults: {
|
|
unit,
|
|
noValue: "Unknown",
|
|
color: { mode: "fixed", fixedColor: "blue" },
|
|
},
|
|
overrides: [],
|
|
},
|
|
options: {
|
|
reduceOptions: { calcs: ["lastNotNull"], values: false },
|
|
colorMode: "value",
|
|
graphMode: "none",
|
|
textMode: "auto",
|
|
},
|
|
});
|
|
stat(
|
|
"Active sessions",
|
|
q.active,
|
|
0,
|
|
0,
|
|
"Current active turns; paused/terminal sessions excluded. Max across fresh API replicas, not a sum.",
|
|
);
|
|
stat(
|
|
"Queued turns",
|
|
q.queued,
|
|
6,
|
|
0,
|
|
"Durable queued submissions, including work waiting behind a parked turn.",
|
|
);
|
|
stat(
|
|
"Awaiting approval",
|
|
q.approvals,
|
|
12,
|
|
0,
|
|
"Unanswered approval interactions for the current turn.",
|
|
);
|
|
stat(
|
|
"Awaiting answer",
|
|
q.questions,
|
|
18,
|
|
0,
|
|
"Unanswered questions for the current turn.",
|
|
);
|
|
stat(
|
|
"Fresh worker slots",
|
|
q.capacity,
|
|
0,
|
|
4,
|
|
`Expected ${o.expectedSlots}. Only heartbeats newer than ${o.freshSeconds}s count. A heartbeat is not proof of healthy execution.`,
|
|
);
|
|
stat(
|
|
"Occupied slots",
|
|
q.occupied,
|
|
6,
|
|
4,
|
|
"Includes startup, active work and draining. Not a tool count.",
|
|
);
|
|
stat(
|
|
"Oldest queued turn",
|
|
q.queueAge,
|
|
12,
|
|
4,
|
|
"Time since durable submission, including queued work behind an approval.",
|
|
"s",
|
|
);
|
|
stat(
|
|
"Potential stalls",
|
|
q.stalled,
|
|
18,
|
|
4,
|
|
"Active sessions without a recent durable event; legitimate quiet tools can trigger this. Waiting interactions excluded.",
|
|
);
|
|
stat(
|
|
"Recent uncertain transitions",
|
|
q.unknown,
|
|
0,
|
|
8,
|
|
"Count of completion/recovery transitions with unknown effects within configured observer window; not unresolved-effect inventory.",
|
|
);
|
|
stat(
|
|
"Database observer healthy",
|
|
q.databaseUnhealthy,
|
|
6,
|
|
8,
|
|
"1 = all fresh observers healthy; 0 = at least one failed. Missing = unknown, not a database-wide health assertion.",
|
|
);
|
|
stat(
|
|
"State observation age",
|
|
q.stateAge,
|
|
12,
|
|
8,
|
|
"Age of latest successful PG snapshot. Requires synchronized clocks.",
|
|
"s",
|
|
);
|
|
stat(
|
|
"Telemetry missing (1 = yes)",
|
|
q.monitoringMissing,
|
|
18,
|
|
8,
|
|
"Detects absence even if the collector retains old samples.",
|
|
);
|
|
panels.push({
|
|
id: 13,
|
|
title: "Operational alerts",
|
|
type: "alertlist",
|
|
gridPos: { x: 0, y: 12, w: 24, h: 8 },
|
|
options: {
|
|
viewMode: "list",
|
|
groupMode: "default",
|
|
maxItems: 20,
|
|
sortOrder: 1,
|
|
dashboardAlerts: true,
|
|
alertName: "",
|
|
stateFilter: {
|
|
firing: true,
|
|
pending: true,
|
|
noData: true,
|
|
error: true,
|
|
normal: false,
|
|
},
|
|
showOptions: "current",
|
|
},
|
|
});
|
|
panels.push({
|
|
id: 14,
|
|
title: "Observed errors and telemetry loss",
|
|
type: "timeseries",
|
|
gridPos: { x: 0, y: 20, w: 24, h: 7 },
|
|
description:
|
|
"Processes with a recent exported model/tool error; telemetry-loss curves are rolling gauge deltas and can miss initial drops/resets. Absence is not proof of zero failures.",
|
|
datasource: { type: "prometheus", uid: "prometheus" },
|
|
targets: [
|
|
["Processes with model errors", q.modelErrors],
|
|
["Processes with tool errors", q.toolErrors],
|
|
["Dropped records", q.drops],
|
|
["Export failures", q.exportFailures],
|
|
].map(([name, expr], i) => ({
|
|
refId: String.fromCharCode(65 + i),
|
|
expr,
|
|
legendFormat: name,
|
|
})),
|
|
fieldConfig: { defaults: { min: 0, noValue: "Unknown" }, overrides: [] },
|
|
});
|
|
const dashboard = {
|
|
uid: dashboardId,
|
|
title: `OpenWorker · ${o.deployment} · Operations`,
|
|
schemaVersion: 39,
|
|
version: 1,
|
|
editable: false,
|
|
tags: ["openworker", "operations"],
|
|
timezone: "browser",
|
|
refresh: "10s",
|
|
time: { from: "now-30m", to: "now" },
|
|
panels,
|
|
links: [
|
|
{
|
|
title: "Harness traces and operation metrics (all deployments)",
|
|
url: "/d/openworker-harness",
|
|
type: "link",
|
|
},
|
|
],
|
|
};
|
|
return { options: o, id, queries: q, dashboard, alerts };
|
|
}
|
|
export async function write(root, options) {
|
|
const result = build(options);
|
|
for (const [name, value] of [
|
|
["operations-dashboard.json", result.dashboard],
|
|
["operations-alerts.json", result.alerts],
|
|
])
|
|
await writeFile(join(root, name), JSON.stringify(value, null, 2) + "\n", {
|
|
mode: 0o600,
|
|
});
|
|
return result;
|
|
}
|
|
if (
|
|
process.argv[1] &&
|
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
const [root, deployment, slots] = process.argv.slice(2);
|
|
if (!root || !deployment)
|
|
throw Error(
|
|
"Usage: node operations.mjs STATE_DIR DEPLOYMENT [EXPECTED_SLOTS]",
|
|
);
|
|
await write(resolve(root), { deployment, expectedSlots: Number(slots ?? 2) });
|
|
}
|