pi-crew 0.2.2 → 0.2.4
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -413
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -0
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-plan-2026-05-12.md +463 -0
- package/docs/followup-review-2026-05-12.md +297 -0
- package/docs/followup-review-round3-2026-05-12.md +342 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -99
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -103
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -158
- package/src/extension/registration/team-tool.ts +159 -98
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -95
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -228
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +16 -4
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -79
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -88
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -463
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -264
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -599
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -122
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -90
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -395
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -458
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -189
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -125
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -178
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -240
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -148
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -117
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/env-filter.ts +30 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/resolve-shell.ts +34 -0
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sleep.ts +2 -1
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -72
- package/src/worktree/worktree-manager.ts +188 -146
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { MetricRegistry } from "./metric-registry.ts";
|
|
3
|
-
|
|
4
|
-
function recordValue(value: unknown): Record<string, unknown> {
|
|
5
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function stringValue(value: unknown, fallback: string): string {
|
|
9
|
-
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function numberValue(value: unknown, fallback = 0): number {
|
|
13
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const CANCELLATION_REASON_LABELS = new Set(["caller_cancelled", "leader_interrupted", "provider_timeout", "worker_timeout", "tool_timeout", "shutdown", "unknown"]);
|
|
17
|
-
|
|
18
|
-
function cancellationReasonLabel(value: unknown): string {
|
|
19
|
-
const raw = stringValue(value, "unknown");
|
|
20
|
-
return CANCELLATION_REASON_LABELS.has(raw) ? raw : "unknown";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface EventToMetricSubscription {
|
|
24
|
-
dispose(): void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, registry: MetricRegistry): EventToMetricSubscription {
|
|
28
|
-
const runCount = registry.counter("crew.run.count", "Total runs by status");
|
|
29
|
-
const taskCount = registry.counter("crew.task.count", "Total tasks by status");
|
|
30
|
-
const subagentCount = registry.counter("crew.subagent.count", "Total subagent records by status");
|
|
31
|
-
const mailboxCount = registry.counter("crew.mailbox.count", "Total mailbox messages by direction");
|
|
32
|
-
const retryAttemptCount = registry.counter("crew.task.retry_attempt_total", "Retry attempts by run and task");
|
|
33
|
-
const deadletterCount = registry.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
|
|
34
|
-
const overflowCount = registry.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
|
|
35
|
-
const waitingCount = registry.counter("crew.task.waiting_total", "Tasks entering waiting state");
|
|
36
|
-
const supervisorContactCount = registry.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
|
|
37
|
-
registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
|
|
38
|
-
const runDuration = registry.histogram("crew.run.duration_ms", "Run end-to-end duration, milliseconds");
|
|
39
|
-
const taskDuration = registry.histogram("crew.task.duration_ms", "Task duration, milliseconds");
|
|
40
|
-
registry.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]);
|
|
41
|
-
const tokenUsage = registry.histogram("crew.task.tokens_total", "Token usage per task");
|
|
42
|
-
|
|
43
|
-
const handlers: Array<[string, (data: unknown) => void]> = [
|
|
44
|
-
["crew.run.completed", (data) => { const item = recordValue(data); runCount.inc({ status: "completed" }); runDuration.observe({ team: stringValue(item.team, "unknown") }, numberValue(item.durationMs)); }],
|
|
45
|
-
["crew.run.failed", () => runCount.inc({ status: "failed" })],
|
|
46
|
-
["crew.run.cancelled", (data) => { const item = recordValue(data); runCount.inc({ status: "cancelled", reason: cancellationReasonLabel(item.reason) }); }],
|
|
47
|
-
["crew.task.completed", (data) => { const item = recordValue(data); taskCount.inc({ status: "completed" }); taskDuration.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.durationMs)); tokenUsage.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.tokens)); }],
|
|
48
|
-
["crew.task.failed", () => taskCount.inc({ status: "failed" })],
|
|
49
|
-
["crew.task.retry_attempt", (data) => { const item = recordValue(data); taskCount.inc({ status: "retry" }); retryAttemptCount.inc({ runId: stringValue(item.runId, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
50
|
-
["crew.task.deadletter", (data) => { const item = recordValue(data); deadletterCount.inc({ reason: stringValue(item.reason, "unknown") }); }],
|
|
51
|
-
["crew.task.overflow", (data) => { const item = recordValue(data); overflowCount.inc({ phase: stringValue(item.phase, "unknown"), previous_phase: stringValue(item.previousPhase, "none") }); }],
|
|
52
|
-
["task.waiting", (data) => { const item = recordValue(data); waitingCount.inc({ taskId: stringValue(item.taskId, "unknown"), runId: stringValue(item.runId, "unknown") }); }],
|
|
53
|
-
["supervisor.contact", (data) => { const item = recordValue(data); supervisorContactCount.inc({ reason: stringValue(item.reason, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
54
|
-
["crew.subagent.completed", (data) => { const item = recordValue(data); subagentCount.inc({ status: stringValue(item.status, "completed") }); }],
|
|
55
|
-
["crew.subagent.failed", () => subagentCount.inc({ status: "failed" })],
|
|
56
|
-
["crew.mailbox.message", (data) => { const item = recordValue(data); mailboxCount.inc({ direction: stringValue(item.direction, "unknown") }); }],
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const unsubscribers: Array<() => void> = [];
|
|
60
|
-
for (const [event, handler] of handlers) {
|
|
61
|
-
const unsubscribe = events?.on?.(event, (data: unknown) => {
|
|
62
|
-
try { handler(data); } catch { /* metric handlers must never break event delivery */ }
|
|
63
|
-
});
|
|
64
|
-
if (typeof unsubscribe === "function") unsubscribers.push(unsubscribe);
|
|
65
|
-
}
|
|
66
|
-
let disposed = false;
|
|
67
|
-
return { dispose() { if (disposed) return; disposed = true; for (const unsubscribe of unsubscribers.splice(0)) unsubscribe(); } };
|
|
68
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { MetricRegistry } from "./metric-registry.ts";
|
|
3
|
+
|
|
4
|
+
function recordValue(value: unknown): Record<string, unknown> {
|
|
5
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function stringValue(value: unknown, fallback: string): string {
|
|
9
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function numberValue(value: unknown, fallback = 0): number {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CANCELLATION_REASON_LABELS = new Set(["caller_cancelled", "leader_interrupted", "provider_timeout", "worker_timeout", "tool_timeout", "shutdown", "unknown"]);
|
|
17
|
+
|
|
18
|
+
function cancellationReasonLabel(value: unknown): string {
|
|
19
|
+
const raw = stringValue(value, "unknown");
|
|
20
|
+
return CANCELLATION_REASON_LABELS.has(raw) ? raw : "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface EventToMetricSubscription {
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, registry: MetricRegistry): EventToMetricSubscription {
|
|
28
|
+
const runCount = registry.counter("crew.run.count", "Total runs by status");
|
|
29
|
+
const taskCount = registry.counter("crew.task.count", "Total tasks by status");
|
|
30
|
+
const subagentCount = registry.counter("crew.subagent.count", "Total subagent records by status");
|
|
31
|
+
const mailboxCount = registry.counter("crew.mailbox.count", "Total mailbox messages by direction");
|
|
32
|
+
const retryAttemptCount = registry.counter("crew.task.retry_attempt_total", "Retry attempts by run and task");
|
|
33
|
+
const deadletterCount = registry.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
|
|
34
|
+
const overflowCount = registry.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
|
|
35
|
+
const waitingCount = registry.counter("crew.task.waiting_total", "Tasks entering waiting state");
|
|
36
|
+
const supervisorContactCount = registry.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
|
|
37
|
+
registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
|
|
38
|
+
const runDuration = registry.histogram("crew.run.duration_ms", "Run end-to-end duration, milliseconds", [1000, 5000, 15000, 30000, 60000, 300000, 600000, 1800000]);
|
|
39
|
+
const taskDuration = registry.histogram("crew.task.duration_ms", "Task duration, milliseconds", [50, 200, 500, 1000, 5000, 30000, 120000]);
|
|
40
|
+
registry.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]);
|
|
41
|
+
const tokenUsage = registry.histogram("crew.task.tokens_total", "Token usage per task", [100, 500, 2000, 10000, 50000, 200000]);
|
|
42
|
+
|
|
43
|
+
const handlers: Array<[string, (data: unknown) => void]> = [
|
|
44
|
+
["crew.run.completed", (data) => { const item = recordValue(data); runCount.inc({ status: "completed" }); runDuration.observe({ team: stringValue(item.team, "unknown") }, numberValue(item.durationMs)); }],
|
|
45
|
+
["crew.run.failed", () => runCount.inc({ status: "failed" })],
|
|
46
|
+
["crew.run.cancelled", (data) => { const item = recordValue(data); runCount.inc({ status: "cancelled", reason: cancellationReasonLabel(item.reason) }); }],
|
|
47
|
+
["crew.task.completed", (data) => { const item = recordValue(data); taskCount.inc({ status: "completed" }); taskDuration.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.durationMs)); tokenUsage.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.tokens)); }],
|
|
48
|
+
["crew.task.failed", () => taskCount.inc({ status: "failed" })],
|
|
49
|
+
["crew.task.retry_attempt", (data) => { const item = recordValue(data); taskCount.inc({ status: "retry" }); retryAttemptCount.inc({ runId: stringValue(item.runId, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
50
|
+
["crew.task.deadletter", (data) => { const item = recordValue(data); deadletterCount.inc({ reason: stringValue(item.reason, "unknown") }); }],
|
|
51
|
+
["crew.task.overflow", (data) => { const item = recordValue(data); overflowCount.inc({ phase: stringValue(item.phase, "unknown"), previous_phase: stringValue(item.previousPhase, "none") }); }],
|
|
52
|
+
["task.waiting", (data) => { const item = recordValue(data); waitingCount.inc({ taskId: stringValue(item.taskId, "unknown"), runId: stringValue(item.runId, "unknown") }); }],
|
|
53
|
+
["supervisor.contact", (data) => { const item = recordValue(data); supervisorContactCount.inc({ reason: stringValue(item.reason, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
54
|
+
["crew.subagent.completed", (data) => { const item = recordValue(data); subagentCount.inc({ status: stringValue(item.status, "completed") }); }],
|
|
55
|
+
["crew.subagent.failed", () => subagentCount.inc({ status: "failed" })],
|
|
56
|
+
["crew.mailbox.message", (data) => { const item = recordValue(data); mailboxCount.inc({ direction: stringValue(item.direction, "unknown") }); }],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const unsubscribers: Array<() => void> = [];
|
|
60
|
+
for (const [event, handler] of handlers) {
|
|
61
|
+
const unsubscribe = events?.on?.(event, (data: unknown) => {
|
|
62
|
+
try { handler(data); } catch { /* metric handlers must never break event delivery */ }
|
|
63
|
+
});
|
|
64
|
+
if (typeof unsubscribe === "function") unsubscribers.push(unsubscribe);
|
|
65
|
+
}
|
|
66
|
+
let disposed = false;
|
|
67
|
+
return { dispose() { if (disposed) return; disposed = true; for (const unsubscribe of unsubscribers.splice(0)) unsubscribe(); } };
|
|
68
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import type { MetricSnapshot } from "../metrics-primitives.ts";
|
|
2
|
-
|
|
3
|
-
export interface MetricExporter {
|
|
4
|
-
name: string;
|
|
5
|
-
push(snapshots: MetricSnapshot[]): Promise<void>;
|
|
6
|
-
dispose(): void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class CompositeExporter implements MetricExporter {
|
|
10
|
-
name = "composite";
|
|
11
|
-
private readonly exporters: MetricExporter[];
|
|
12
|
-
|
|
13
|
-
constructor(exporters: MetricExporter[]) {
|
|
14
|
-
this.exporters = exporters;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async push(snapshots: MetricSnapshot[]): Promise<void> {
|
|
18
|
-
await Promise.allSettled(this.exporters.map((exporter) => exporter.push(snapshots)));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
dispose(): void {
|
|
22
|
-
for (const exporter of this.exporters) {
|
|
23
|
-
try {
|
|
24
|
-
exporter.dispose();
|
|
25
|
-
} catch {
|
|
26
|
-
// Best-effort cleanup; one exporter failing shouldn't prevent others.
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
import type { MetricSnapshot } from "../metrics-primitives.ts";
|
|
2
|
+
|
|
3
|
+
export interface MetricExporter {
|
|
4
|
+
name: string;
|
|
5
|
+
push(snapshots: MetricSnapshot[]): Promise<void>;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class CompositeExporter implements MetricExporter {
|
|
10
|
+
name = "composite";
|
|
11
|
+
private readonly exporters: MetricExporter[];
|
|
12
|
+
|
|
13
|
+
constructor(exporters: MetricExporter[]) {
|
|
14
|
+
this.exporters = exporters;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async push(snapshots: MetricSnapshot[]): Promise<void> {
|
|
18
|
+
await Promise.allSettled(this.exporters.map((exporter) => exporter.push(snapshots)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dispose(): void {
|
|
22
|
+
for (const exporter of this.exporters) {
|
|
23
|
+
try {
|
|
24
|
+
exporter.dispose();
|
|
25
|
+
} catch {
|
|
26
|
+
// Best-effort cleanup; one exporter failing shouldn't prevent others.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,92 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}],
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
1
|
+
import { gzip } from "node:zlib";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
4
|
+
import { redactSecrets } from "../../utils/redaction.ts";
|
|
5
|
+
import type { MetricRegistry } from "../metric-registry.ts";
|
|
6
|
+
import type { MetricSnapshot } from "../metrics-primitives.ts";
|
|
7
|
+
import type { MetricExporter } from "./adapter.ts";
|
|
8
|
+
|
|
9
|
+
const gzipAsync = promisify(gzip);
|
|
10
|
+
|
|
11
|
+
export interface OTLPExporterOptions {
|
|
12
|
+
endpoint: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
intervalMs?: number;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pointValues(snapshot: MetricSnapshot): unknown[] {
|
|
19
|
+
const MAX_LABEL_LENGTH = 256;
|
|
20
|
+
if (snapshot.type === "histogram") {
|
|
21
|
+
return snapshot.values.map((value) => ({
|
|
22
|
+
attributes: Object.entries(value.labels).map(([key, item]) => {
|
|
23
|
+
const redacted = redactSecrets({ [key]: item }) as Record<string, string>;
|
|
24
|
+
const val = String(redacted[key] ?? item);
|
|
25
|
+
return { key, value: { stringValue: val.length > MAX_LABEL_LENGTH ? val.slice(0, MAX_LABEL_LENGTH) : val } };
|
|
26
|
+
}),
|
|
27
|
+
count: "count" in value ? value.count : undefined,
|
|
28
|
+
sum: "sum" in value ? value.sum : undefined,
|
|
29
|
+
bucketCounts: "counts" in value ? value.counts : undefined,
|
|
30
|
+
explicitBounds: "buckets" in value ? value.buckets : undefined,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return snapshot.values.map((value) => ({
|
|
34
|
+
attributes: Object.entries(value.labels).map(([key, item]) => {
|
|
35
|
+
const redacted = redactSecrets({ [key]: item }) as Record<string, string>;
|
|
36
|
+
const val = String(redacted[key] ?? item);
|
|
37
|
+
return { key, value: { stringValue: val.length > MAX_LABEL_LENGTH ? val.slice(0, MAX_LABEL_LENGTH) : val } };
|
|
38
|
+
}),
|
|
39
|
+
asDouble: "value" in value ? value.value : undefined,
|
|
40
|
+
count: "count" in value ? value.count : undefined,
|
|
41
|
+
sum: "sum" in value ? value.sum : undefined,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function convertToOTLP(snapshots: MetricSnapshot[]): unknown {
|
|
46
|
+
return {
|
|
47
|
+
resourceMetrics: [{
|
|
48
|
+
resource: { attributes: [{ key: "service.name", value: { stringValue: "pi-crew" } }] },
|
|
49
|
+
scopeMetrics: [{
|
|
50
|
+
scope: { name: "pi-crew" },
|
|
51
|
+
metrics: snapshots.map((snapshot) => ({ name: snapshot.name, description: snapshot.description, [snapshot.type === "histogram" ? "histogram" : snapshot.type === "gauge" ? "gauge" : "sum"]: { dataPoints: pointValues(snapshot) } })),
|
|
52
|
+
}],
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class OTLPExporter implements MetricExporter {
|
|
58
|
+
name = "otlp";
|
|
59
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
60
|
+
private readonly opts: OTLPExporterOptions;
|
|
61
|
+
private readonly registry: MetricRegistry;
|
|
62
|
+
|
|
63
|
+
constructor(opts: OTLPExporterOptions, registry: MetricRegistry) {
|
|
64
|
+
this.opts = opts;
|
|
65
|
+
this.registry = registry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
start(): void {
|
|
69
|
+
this.dispose();
|
|
70
|
+
this.timer = setInterval(() => { void this.push(this.registry.snapshot()); }, this.opts.intervalMs ?? 60_000);
|
|
71
|
+
this.timer.unref();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async push(snapshots: MetricSnapshot[]): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const timeoutMs = this.opts.timeoutMs ?? 10_000;
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
// 4.2: gzip body. OTLP HTTP exporters of every flavour accept
|
|
81
|
+
// `content-encoding: gzip`; collectors expect uncompressed JSON
|
|
82
|
+
// otherwise. Saves bandwidth on metric-heavy runs (often 3-5x).
|
|
83
|
+
const json = JSON.stringify(convertToOTLP(snapshots));
|
|
84
|
+
const body = await gzipAsync(Buffer.from(json));
|
|
85
|
+
const response = await fetch(this.opts.endpoint, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json", "content-encoding": "gzip", ...(this.opts.headers ?? {}) },
|
|
88
|
+
body,
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
logInternalError("otlp-export-http", new Error(`HTTP ${response.status}: ${response.statusText}`), `endpoint=${this.opts.endpoint}`);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logInternalError("otlp-export", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dispose(): void {
|
|
103
|
+
if (this.timer) clearInterval(this.timer);
|
|
104
|
+
this.timer = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import type { HistogramPoint, MetricLabels, MetricPoint, MetricSnapshot } from "../metrics-primitives.ts";
|
|
2
|
-
|
|
3
|
-
function prometheusName(name: string): string {
|
|
4
|
-
return name.replace(/[^a-zA-Z0-9_:]/g, "_").replace(/^[0-9]/, "_$&");
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function escapeLabel(value: string): string {
|
|
8
|
-
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, "\\\"");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function labelsText(labels: MetricLabels): string {
|
|
12
|
-
const entries = Object.entries(labels);
|
|
13
|
-
if (!entries.length) return "";
|
|
14
|
-
return `{${entries.map(([key, value]) => `${key}="${escapeLabel(String(value))}"`).join(",")}}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function metricType(type: MetricSnapshot["type"]): string {
|
|
18
|
-
return type === "histogram" ? "histogram" : type === "gauge" ? "gauge" : "counter";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isHistogramPoint(value: MetricPoint | HistogramPoint): value is HistogramPoint {
|
|
22
|
-
return "buckets" in value && "counts" in value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function formatPrometheusValue(num: number): string {
|
|
26
|
-
if (Number.isNaN(num)) return "Nan";
|
|
27
|
-
if (num === Number.POSITIVE_INFINITY) return "+Inf";
|
|
28
|
-
if (num === Number.NEGATIVE_INFINITY) return "-Inf";
|
|
29
|
-
return String(num);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function formatPrometheus(snapshots: MetricSnapshot[]): string {
|
|
33
|
-
const lines: string[] = [];
|
|
34
|
-
for (const snapshot of snapshots) {
|
|
35
|
-
const name = prometheusName(snapshot.name);
|
|
36
|
-
lines.push(`# HELP ${name} ${snapshot.description}`);
|
|
37
|
-
lines.push(`# TYPE ${name} ${metricType(snapshot.type)}`);
|
|
38
|
-
for (const value of snapshot.values) {
|
|
39
|
-
if (isHistogramPoint(value)) {
|
|
40
|
-
let cumulative = 0;
|
|
41
|
-
for (let index = 0; index < value.buckets.length; index += 1) {
|
|
42
|
-
cumulative += value.counts[index] ?? 0;
|
|
43
|
-
const le = Number.isFinite(value.buckets[index]) ? String(value.buckets[index]) : "+Inf";
|
|
44
|
-
lines.push(`${name}_bucket${labelsText({ ...value.labels, le })} ${cumulative}`);
|
|
45
|
-
}
|
|
46
|
-
lines.push(`${name}_sum${labelsText(value.labels)} ${value.sum}`);
|
|
47
|
-
lines.push(`${name}_count${labelsText(value.labels)} ${value.count}`);
|
|
48
|
-
} else {
|
|
49
|
-
lines.push(`${name}${labelsText(value.labels)} ${formatPrometheusValue(value.value)}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return `${lines.join("\n")}\n`;
|
|
54
|
-
}
|
|
1
|
+
import type { HistogramPoint, MetricLabels, MetricPoint, MetricSnapshot } from "../metrics-primitives.ts";
|
|
2
|
+
|
|
3
|
+
function prometheusName(name: string): string {
|
|
4
|
+
return name.replace(/[^a-zA-Z0-9_:]/g, "_").replace(/^[0-9]/, "_$&");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function escapeLabel(value: string): string {
|
|
8
|
+
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, "\\\"");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function labelsText(labels: MetricLabels): string {
|
|
12
|
+
const entries = Object.entries(labels);
|
|
13
|
+
if (!entries.length) return "";
|
|
14
|
+
return `{${entries.map(([key, value]) => `${key}="${escapeLabel(String(value))}"`).join(",")}}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function metricType(type: MetricSnapshot["type"]): string {
|
|
18
|
+
return type === "histogram" ? "histogram" : type === "gauge" ? "gauge" : "counter";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isHistogramPoint(value: MetricPoint | HistogramPoint): value is HistogramPoint {
|
|
22
|
+
return "buckets" in value && "counts" in value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatPrometheusValue(num: number): string {
|
|
26
|
+
if (Number.isNaN(num)) return "Nan";
|
|
27
|
+
if (num === Number.POSITIVE_INFINITY) return "+Inf";
|
|
28
|
+
if (num === Number.NEGATIVE_INFINITY) return "-Inf";
|
|
29
|
+
return String(num);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatPrometheus(snapshots: MetricSnapshot[]): string {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
for (const snapshot of snapshots) {
|
|
35
|
+
const name = prometheusName(snapshot.name);
|
|
36
|
+
lines.push(`# HELP ${name} ${snapshot.description}`);
|
|
37
|
+
lines.push(`# TYPE ${name} ${metricType(snapshot.type)}`);
|
|
38
|
+
for (const value of snapshot.values) {
|
|
39
|
+
if (isHistogramPoint(value)) {
|
|
40
|
+
let cumulative = 0;
|
|
41
|
+
for (let index = 0; index < value.buckets.length; index += 1) {
|
|
42
|
+
cumulative += value.counts[index] ?? 0;
|
|
43
|
+
const le = Number.isFinite(value.buckets[index]) ? String(value.buckets[index]) : "+Inf";
|
|
44
|
+
lines.push(`${name}_bucket${labelsText({ ...value.labels, le })} ${cumulative}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(`${name}_sum${labelsText(value.labels)} ${value.sum}`);
|
|
47
|
+
lines.push(`${name}_count${labelsText(value.labels)} ${value.count}`);
|
|
48
|
+
} else {
|
|
49
|
+
lines.push(`${name}${labelsText(value.labels)} ${formatPrometheusValue(value.value)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return `${lines.join("\n")}\n`;
|
|
54
|
+
}
|