@senad-d/observme 0.1.3 → 0.1.5
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/.env.example +4 -0
- package/CHANGELOG.md +48 -0
- package/README.md +36 -11
- package/dashboards/observme-agent-node-graphs.json +5 -5
- package/dashboards/observme-agents.json +169 -4
- package/dashboards/observme-alerts.yaml +16 -3
- package/dashboards/observme-overview.json +6 -6
- package/dashboards/observme-trace-journey.json +4 -4
- package/docs/agent-subagent-observability-requirements.md +19 -10
- package/docs/compatibility-matrix.md +21 -4
- package/docs/configuration.md +7 -2
- package/docs/extension-integration.md +20 -13
- package/docs/reference/03-pi-event-and-session-model.md +6 -6
- package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/09-dashboards-alerts-slos.md +132 -3
- package/docs/reference/10-testing-release-operations.md +44 -14
- package/docs/reference/11-deployment-runbooks.md +82 -5
- package/docs/reference/12-configuration-reference.md +48 -5
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +3 -2
- package/package.json +14 -4
- package/src/commands/obs-agents.ts +17 -12
- package/src/commands/obs-backfill.ts +2 -2
- package/src/commands/obs-command-support.ts +2 -1
- package/src/commands/obs-cost.ts +15 -7
- package/src/commands/obs-health.ts +22 -2
- package/src/commands/obs-loki-summary.ts +4 -8
- package/src/commands/obs-session.ts +35 -28
- package/src/commands/obs-status.ts +56 -13
- package/src/commands/obs-tools.ts +19 -8
- package/src/commands/obs-trace.ts +9 -0
- package/src/commands/obs.ts +6 -1
- package/src/config/bootstrap-project-config.ts +50 -32
- package/src/config/defaults.ts +1 -2
- package/src/config/load-config.ts +270 -81
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +18 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +88 -12
- package/src/extension.ts +2 -12
- package/src/integration.ts +6 -2
- package/src/otel/logs.ts +24 -13
- package/src/otel/metrics.ts +24 -13
- package/src/otel/otlp-endpoint.ts +41 -2
- package/src/otel/otlp-http-options.ts +11 -0
- package/src/otel/sdk.ts +155 -9
- package/src/otel/shutdown.ts +39 -11
- package/src/otel/traces.ts +34 -16
- package/src/pi/active-agent-lease.ts +101 -0
- package/src/pi/agent-tree-tracker.ts +14 -1
- package/src/pi/compatibility.ts +121 -0
- package/src/pi/event-handlers/agent-turn.ts +16 -9
- package/src/pi/event-handlers/lifecycle.ts +312 -106
- package/src/pi/event-handlers/llm.ts +17 -9
- package/src/pi/event-handlers/session-events.ts +53 -19
- package/src/pi/event-handlers/tool-bash.ts +21 -27
- package/src/pi/handler-internals.ts +16 -26
- package/src/pi/handler-runtime.ts +159 -54
- package/src/pi/handler-types.ts +40 -17
- package/src/pi/handlers.ts +12 -1
- package/src/pi/integration-api.ts +27 -15
- package/src/pi/session-correlation.ts +167 -0
- package/src/pi/subagent-spawn.ts +164 -31
- package/src/privacy/redact.ts +574 -68
- package/src/privacy/secret-patterns.ts +6 -1
- package/src/query/grafana-readiness.ts +18 -2
- package/src/query/grafana-transport.ts +150 -27
- package/src/query/grafana-url.ts +36 -0
- package/src/query/grafana.ts +4 -136
- package/src/query/loki.ts +2 -4
- package/src/query/prometheus.ts +8 -10
- package/src/query/tempo.ts +2 -4
- package/src/query/trace-link.ts +186 -0
- package/src/safety/display-bounds.ts +84 -0
- package/src/semconv/metrics.ts +7 -0
- package/src/semconv/values.ts +2 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
requestObservMeIntegration,
|
|
4
|
-
type ObservMeChildStatus,
|
|
5
4
|
type ObservMeIntegrationApi,
|
|
6
5
|
type ObservMeJoinStatus,
|
|
7
6
|
type ObservMeProcessEnvironment,
|
|
8
7
|
type ObservMeSpawnReason,
|
|
9
8
|
type ObservMeSpawnType,
|
|
10
9
|
type ObservMeStartedSubagent,
|
|
10
|
+
type ObservMeTerminalChildStatus,
|
|
11
11
|
} from "@senad-d/observme/integration";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -70,7 +70,6 @@ export class ObservableSubagentRunner<Request, Handle, Value> {
|
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
handle = await this.#transport.launch(options.request, context, options.signal);
|
|
73
|
-
completeObservMeLaunch(observme, started);
|
|
74
73
|
} catch (error) {
|
|
75
74
|
failObservMeLaunch(observme, started, error);
|
|
76
75
|
throw error;
|
|
@@ -90,10 +89,12 @@ export class ObservableSubagentRunner<Request, Handle, Value> {
|
|
|
90
89
|
try {
|
|
91
90
|
const result = await this.#transport.wait(handle, signal);
|
|
92
91
|
endObservMeWait(observme, started, wait, result);
|
|
92
|
+
completeObservMeChild(observme, started, childStatus(result.status));
|
|
93
93
|
recordObservMeJoin(observme, started, result);
|
|
94
94
|
return result;
|
|
95
95
|
} catch (error) {
|
|
96
96
|
endObservMeWaitFailure(observme, started, wait);
|
|
97
|
+
completeObservMeChild(observme, started, "failed");
|
|
97
98
|
recordObservMeJoinFailure(observme, started);
|
|
98
99
|
throw error;
|
|
99
100
|
}
|
|
@@ -127,14 +128,16 @@ function createLaunchContext(
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function
|
|
131
|
+
function completeObservMeChild(
|
|
131
132
|
observme: ObservMeIntegrationApi | undefined,
|
|
132
133
|
started: ObservMeStartedSubagent | undefined,
|
|
134
|
+
status: ObservMeTerminalChildStatus,
|
|
133
135
|
): void {
|
|
134
136
|
if (!observme || !started) return;
|
|
135
137
|
observme.completeSubagent(started.spawnId, {
|
|
136
138
|
childAgentId: started.childAgentId,
|
|
137
|
-
childStatus:
|
|
139
|
+
childStatus: status,
|
|
140
|
+
outcome: status,
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
|
|
@@ -245,7 +248,7 @@ function recordObservMeJoinFailure(
|
|
|
245
248
|
});
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
function childStatus(status: ChildRunResult<unknown>["status"]):
|
|
251
|
+
function childStatus(status: ChildRunResult<unknown>["status"]): ObservMeTerminalChildStatus {
|
|
249
252
|
if (status === "completed") return "completed";
|
|
250
253
|
if (status === "cancelled") return "cancelled";
|
|
251
254
|
return "failed";
|
package/examples/observme.yaml
CHANGED
|
@@ -6,7 +6,6 @@ observme:
|
|
|
6
6
|
enabled: true
|
|
7
7
|
environment: development
|
|
8
8
|
tenant: local-dev
|
|
9
|
-
replayOnStart: false
|
|
10
9
|
|
|
11
10
|
otlp:
|
|
12
11
|
endpoint: http://localhost:4318
|
|
@@ -14,7 +13,6 @@ observme:
|
|
|
14
13
|
timeoutMs: 3000
|
|
15
14
|
headers: {}
|
|
16
15
|
tls:
|
|
17
|
-
enabled: false
|
|
18
16
|
insecureSkipVerify: false
|
|
19
17
|
signalEndpoints:
|
|
20
18
|
traces: http://localhost:4318/v1/traces
|
|
@@ -46,6 +44,8 @@ observme:
|
|
|
46
44
|
propagateTraceContext: true
|
|
47
45
|
propagateToSubagents: true
|
|
48
46
|
capabilityEnv: OBSERVME_AGENT_CAPABILITY
|
|
47
|
+
# Opt in to one validated, branch-local custom entry for reload/resume recovery.
|
|
48
|
+
# This entry never participates in LLM context.
|
|
49
49
|
writeCorrelationEntry: false
|
|
50
50
|
|
|
51
51
|
traces:
|
|
@@ -61,6 +61,7 @@ observme:
|
|
|
61
61
|
enabled: true
|
|
62
62
|
exportIntervalMillis: 15000
|
|
63
63
|
exportTimeoutMillis: 3000
|
|
64
|
+
activeAgentLeaseDurationMillis: 60000
|
|
64
65
|
|
|
65
66
|
logs:
|
|
66
67
|
enabled: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senad-d/observme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ObservMe: a Pi extension that instruments Pi agent sessions and exports OpenTelemetry traces, metrics, and logs to an external observability stack (OTel Collector, Grafana Tempo/Loki/Prometheus).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,9 @@
|
|
|
57
57
|
"lint:fix": "eslint . --fix",
|
|
58
58
|
"format:check": "node scripts/check-format.mjs",
|
|
59
59
|
"test": "node --test test/*.test.mjs test/*.test.ts",
|
|
60
|
+
"test:active-agent-lease:contracts": "node --test test/active-agent-lease.test.ts test/alerts.test.mjs test/cardinality.test.ts test/config-defaults.test.mjs test/config-loader.test.mjs test/config-validation.test.mjs test/dashboards.test.mjs test/metrics.test.ts test/pi-handlers.test.mjs test/semconv-metrics.test.mjs",
|
|
60
61
|
"test:integration:collector": "node --test test/integration/collector-debug.test.mjs",
|
|
62
|
+
"test:integration:active-agent-lease": "node --test test/integration/active-agent-lease.test.mjs",
|
|
61
63
|
"test:integration:grafana-stack": "node --test test/integration/grafana-stack.test.mjs",
|
|
62
64
|
"test:coverage": "node scripts/test-coverage.mjs",
|
|
63
65
|
"smoke:discover": "node scripts/smoke-observability.mjs",
|
|
@@ -65,12 +67,20 @@
|
|
|
65
67
|
"smoke:handlers": "node scripts/smoke-handler-execution.mjs",
|
|
66
68
|
"smoke:pi-lifecycle": "node scripts/smoke-pi-lifecycle.mjs",
|
|
67
69
|
"smoke:pi-runtime": "node scripts/smoke-pi-runtime.mjs",
|
|
70
|
+
"validate:pi-compatibility": "npm run typecheck && npm run typecheck:test && node --test test/pi-compatibility.test.ts test/event-mapping.test.ts test/template.test.mjs && npm run smoke:packaged && npm run smoke:handlers && npm run smoke:pi-lifecycle && npm run smoke:pi-runtime",
|
|
68
71
|
"validate:grafana-obs": "node scripts/validate-grafana-obs-flow.mjs",
|
|
69
72
|
"check": "node --check scripts/check-package-contents.mjs && node --check scripts/test-coverage.mjs && node --check scripts/smoke-observability.mjs && node --check scripts/smoke-packaged-install.mjs && node --check scripts/smoke-handler-execution.mjs && node --check scripts/smoke-pi-lifecycle.mjs && node --check scripts/smoke-pi-runtime.mjs && node --check scripts/validate-grafana-obs-flow.mjs",
|
|
70
73
|
"pack:dry-run": "npm pack --dry-run --json",
|
|
71
74
|
"check:pack": "node scripts/check-package-contents.mjs",
|
|
72
75
|
"validate": "npm run lint && npm run test && npm run check:pack && npm run smoke:packaged && npm run smoke:handlers && npm run smoke:pi-lifecycle && npm run smoke:pi-runtime"
|
|
73
76
|
},
|
|
77
|
+
"observmeCompatibility": {
|
|
78
|
+
"pi": {
|
|
79
|
+
"minimumVersion": "0.80.5",
|
|
80
|
+
"releaseTestedVersion": "0.80.6",
|
|
81
|
+
"supportedRange": ">=0.80.5 <0.81.0"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
74
84
|
"pi": {
|
|
75
85
|
"extensions": [
|
|
76
86
|
"./src/extension.ts"
|
|
@@ -86,8 +96,8 @@
|
|
|
86
96
|
"typebox": "*"
|
|
87
97
|
},
|
|
88
98
|
"devDependencies": {
|
|
89
|
-
"@earendil-works/pi-ai": "
|
|
90
|
-
"@earendil-works/pi-coding-agent": "
|
|
99
|
+
"@earendil-works/pi-ai": "0.80.6",
|
|
100
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
91
101
|
"@eslint/js": "^10.0.1",
|
|
92
102
|
"@types/node": "^26.1.1",
|
|
93
103
|
"c8": "^11.0.0",
|
|
@@ -95,7 +105,7 @@
|
|
|
95
105
|
"globals": "^17.7.0",
|
|
96
106
|
"typebox": "^1.3.6",
|
|
97
107
|
"typescript": "6.0.3",
|
|
98
|
-
"typescript-eslint": "
|
|
108
|
+
"typescript-eslint": "8.63.0"
|
|
99
109
|
},
|
|
100
110
|
"dependencies": {
|
|
101
111
|
"@opentelemetry/api": "1.9.1",
|
|
@@ -6,6 +6,11 @@ import type { PrometheusFetch, PrometheusMetricSeries, QueryResult } from "../qu
|
|
|
6
6
|
import { createPrometheusQueryClient } from "../query/prometheus.ts";
|
|
7
7
|
import type { TimeRange, TraceSummary } from "../query/tempo.ts";
|
|
8
8
|
import { createTempoQueryClient } from "../query/tempo.ts";
|
|
9
|
+
import {
|
|
10
|
+
boundObsCommandOutput,
|
|
11
|
+
normalizeObsBackendLabel,
|
|
12
|
+
normalizeObsBackendLabelRecord,
|
|
13
|
+
} from "../safety/display-bounds.ts";
|
|
9
14
|
import { COMMON_SPAN_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
10
15
|
import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
|
|
11
16
|
import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
|
|
@@ -187,8 +192,8 @@ export function renderObsAgents(snapshot: ObsAgentsSnapshot): string {
|
|
|
187
192
|
const latestChild = readLatestChild(snapshot.children);
|
|
188
193
|
const lines = [
|
|
189
194
|
`Workflow: ${formatUnknown(snapshot.workflowId)} root=${formatUnknown(snapshot.workflowRootAgentId ?? snapshot.rootAgentId)}`,
|
|
190
|
-
`Agent: ${formatUnknown(snapshot.agentId)} (${snapshot.role} depth=${snapshot.depth})`,
|
|
191
|
-
`Session: ${formatUnknown(
|
|
195
|
+
`Agent: ${formatUnknown(snapshot.agentId)} (${formatUnknown(snapshot.role)} depth=${snapshot.depth})`,
|
|
196
|
+
`Session: ${formatUnknown(snapshot.sessionId)}`,
|
|
192
197
|
`Subagents spawned in current trace: ${snapshot.fanoutCount}`,
|
|
193
198
|
`Current tree: depth=${snapshot.treeDepth} width=${snapshot.treeWidth} active=${snapshot.activeChildren} orphaned=${snapshot.orphanCount}`,
|
|
194
199
|
`Recent children: ${renderRecentChildren(snapshot.children, snapshot.recentChildrenLimit)}`,
|
|
@@ -200,7 +205,7 @@ export function renderObsAgents(snapshot: ObsAgentsSnapshot): string {
|
|
|
200
205
|
`Aggregate agent metrics (last ${OBS_AGENTS_WINDOW}): ${renderAggregateRows(snapshot.aggregateRows)}`,
|
|
201
206
|
`Lineage drill-down: ${renderLineageDrilldown(snapshot)}`,
|
|
202
207
|
);
|
|
203
|
-
return lines.join("\n");
|
|
208
|
+
return boundObsCommandOutput(lines.join("\n"));
|
|
204
209
|
}
|
|
205
210
|
|
|
206
211
|
class ObsAgentsCommand {
|
|
@@ -322,9 +327,9 @@ function toObsAgentAggregateRow(series: PrometheusMetricSeries): ObsAgentAggrega
|
|
|
322
327
|
if (value === undefined) return undefined;
|
|
323
328
|
|
|
324
329
|
return {
|
|
325
|
-
labels:
|
|
330
|
+
labels: normalizeObsBackendLabelRecord(series.metric),
|
|
326
331
|
value,
|
|
327
|
-
timestampUnixSeconds: series.value?.timestampUnixSeconds,
|
|
332
|
+
timestampUnixSeconds: normalizeObsBackendLabel(series.value?.timestampUnixSeconds),
|
|
328
333
|
};
|
|
329
334
|
}
|
|
330
335
|
|
|
@@ -379,12 +384,12 @@ function selectRecentChildrenForRender(
|
|
|
379
384
|
|
|
380
385
|
function renderRecentChild(child: ObsAgentChildRow): string {
|
|
381
386
|
const orphan = child.orphaned ? " orphaned" : "";
|
|
382
|
-
return `${child.agentId} status=${child.status} depth=${child.depth}${orphan}`;
|
|
387
|
+
return `${formatUnknown(child.agentId)} status=${formatUnknown(child.status)} depth=${child.depth}${orphan}`;
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
function renderLatestChild(child: ObsAgentChildRow, hints: readonly ObsAgentWaitJoinHint[]): string {
|
|
386
391
|
const joinHint = readLatestHintForChild(child.agentId, hints, "join");
|
|
387
|
-
return `${child.agentId} status=${child.status} active=${child.activeChildren} join=${formatDuration(joinHint?.durationMs)}`;
|
|
392
|
+
return `${formatUnknown(child.agentId)} status=${formatUnknown(child.status)} active=${child.activeChildren} join=${formatDuration(joinHint?.durationMs)}`;
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
function readLatestHintForChild(
|
|
@@ -408,7 +413,8 @@ function renderWaitJoinHint(hint: ObsAgentWaitJoinHint | undefined): string {
|
|
|
408
413
|
if (!hint) return "none";
|
|
409
414
|
|
|
410
415
|
const status = hint.joinStatus ?? hint.childStatus ?? (hint.active ? "waiting" : "complete");
|
|
411
|
-
|
|
416
|
+
const target = hint.childAgentId ?? hint.spawnId ?? hint.id;
|
|
417
|
+
return `${formatUnknown(hint.kind)}:${formatUnknown(target)} status=${formatUnknown(status)} duration=${formatDuration(hint.durationMs)}`;
|
|
412
418
|
}
|
|
413
419
|
|
|
414
420
|
function renderAggregateRows(rows: ObsAgentsAggregateRows): string {
|
|
@@ -416,7 +422,7 @@ function renderAggregateRows(rows: ObsAgentsAggregateRows): string {
|
|
|
416
422
|
}
|
|
417
423
|
|
|
418
424
|
function renderLineageDrilldown(snapshot: ObsAgentsSnapshot): string {
|
|
419
|
-
const attrs = Object.keys(snapshot.tempoSearchAttributes).join(", ") || "none";
|
|
425
|
+
const attrs = Object.keys(snapshot.tempoSearchAttributes).map(formatUnknown).join(", ") || "none";
|
|
420
426
|
const traceCount = snapshot.traces.length;
|
|
421
427
|
const latestTrace = normalizeOptionalString(snapshot.traces[0]?.traceId) ?? normalizeOptionalString(snapshot.traceId);
|
|
422
428
|
const traceSuffix = latestTrace ? ` latest_trace=${latestTrace}` : "";
|
|
@@ -454,8 +460,7 @@ function normalizeRecentChildrenLimit(value: number | undefined): number {
|
|
|
454
460
|
}
|
|
455
461
|
|
|
456
462
|
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
457
|
-
|
|
458
|
-
return trimmed || undefined;
|
|
463
|
+
return normalizeObsBackendLabel(value);
|
|
459
464
|
}
|
|
460
465
|
|
|
461
466
|
function isObsAgentsRequest(args: string): boolean {
|
|
@@ -463,7 +468,7 @@ function isObsAgentsRequest(args: string): boolean {
|
|
|
463
468
|
}
|
|
464
469
|
|
|
465
470
|
function formatUnknown(value: string | undefined): string {
|
|
466
|
-
return value ?? "unknown";
|
|
471
|
+
return normalizeObsBackendLabel(value) ?? "unknown";
|
|
467
472
|
}
|
|
468
473
|
|
|
469
474
|
function formatDuration(value: number | undefined): string {
|
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
import type { ExtensionAPI, SessionEntry, SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { LoadSessionConfigOptions } from "../config/load-config.ts";
|
|
4
4
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
5
|
-
import { ObservMeLogSdk } from "../otel/logs.ts";
|
|
5
|
+
import { type ObservMeLogSdk, createLogSessionScopedOtelSdk } from "../otel/logs.ts";
|
|
6
6
|
import type { BoundedOtelOperationResult } from "../otel/shutdown.ts";
|
|
7
7
|
import { flushOtelSdk, shutdownOtelSdk } from "../otel/shutdown.ts";
|
|
8
8
|
import { applyContentCapturePolicy } from "../privacy/content-capture.ts";
|
|
@@ -308,7 +308,7 @@ export function createObsBackfillLogExporter(
|
|
|
308
308
|
config: ObservMeConfig,
|
|
309
309
|
options: ObsBackfillOperationOptions = {},
|
|
310
310
|
): ObsBackfillExporter {
|
|
311
|
-
const sdk =
|
|
311
|
+
const sdk = createLogSessionScopedOtelSdk({ config });
|
|
312
312
|
sdk.start();
|
|
313
313
|
return new ObsBackfillLogExporter(sdk, resolveBackfillOperationTimeoutMs(options, OBS_BACKFILL_DEFAULT_OPERATION_TIMEOUT_MS));
|
|
314
314
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LoadSessionConfigOptions } from "../config/load-config.ts";
|
|
2
2
|
import { loadSessionConfig } from "../config/load-config.ts";
|
|
3
3
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
4
|
+
import { boundObsCommandOutput } from "../safety/display-bounds.ts";
|
|
4
5
|
|
|
5
6
|
export type ObsCommandNotificationType = "info" | "warning" | "error";
|
|
6
7
|
|
|
@@ -33,7 +34,7 @@ export async function notifyObsCommand(
|
|
|
33
34
|
message: string,
|
|
34
35
|
type: ObsCommandNotificationType,
|
|
35
36
|
): Promise<void> {
|
|
36
|
-
await ctx.ui?.notify?.(message, type);
|
|
37
|
+
await ctx.ui?.notify?.(boundObsCommandOutput(message), type);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function normalizeObsCommandTimeoutMs(value: number | undefined, fallback: number, invalidFallback = fallback): number {
|
package/src/commands/obs-cost.ts
CHANGED
|
@@ -3,6 +3,11 @@ import type { LoadSessionConfigOptions } from "../config/load-config.ts";
|
|
|
3
3
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
4
4
|
import type { PrometheusFetch, PrometheusMetricSeries, QueryResult } from "../query/prometheus.ts";
|
|
5
5
|
import { createPrometheusQueryClient } from "../query/prometheus.ts";
|
|
6
|
+
import {
|
|
7
|
+
boundObsCommandOutput,
|
|
8
|
+
normalizeObsBackendLabel,
|
|
9
|
+
selectObsCommandRows,
|
|
10
|
+
} from "../safety/display-bounds.ts";
|
|
6
11
|
import { completeObsSubcommand, parseObsSubcommandArgs } from "./obs-args.ts";
|
|
7
12
|
import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
|
|
8
13
|
import { appendObsRecoveryHint, formatObsCommandFailure } from "./obs-diagnostics.ts";
|
|
@@ -114,15 +119,19 @@ export async function getObsCostSnapshot(
|
|
|
114
119
|
|
|
115
120
|
export function renderObsCost(snapshot: ObsCostSnapshot): string {
|
|
116
121
|
const rows = snapshot.rows.map(normalizeObsCostRow).filter(isObsCostRow);
|
|
117
|
-
const
|
|
122
|
+
const selection = selectObsCommandRows(rows);
|
|
123
|
+
const window = normalizeObsBackendLabel(snapshot.window) ?? OBS_COST_WINDOW;
|
|
124
|
+
const lines = [`Cost by model/provider (last ${window})`];
|
|
118
125
|
|
|
119
126
|
if (rows.length === 0) {
|
|
120
127
|
lines.push(appendObsRecoveryHint("No cost metrics found.", OBS_COST_NO_METRICS_NEXT_ACTION));
|
|
121
|
-
return lines.join("\n");
|
|
128
|
+
return boundObsCommandOutput(lines.join("\n"));
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
lines.push(...rows.map(renderObsCostRow)
|
|
125
|
-
|
|
131
|
+
lines.push(...selection.rows.map(renderObsCostRow));
|
|
132
|
+
if (selection.omittedCount > 0) lines.push(`… ${selection.omittedCount} cost row(s) omitted`);
|
|
133
|
+
lines.push(`Total: ${formatUsd(sumObsCostRows(rows))}`);
|
|
134
|
+
return boundObsCommandOutput(lines.join("\n"));
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
class ObsCostCommand {
|
|
@@ -187,12 +196,11 @@ function parseCostUsd(value: string | number | undefined): number | undefined {
|
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
function normalizeMetricLabel(value: string | undefined): string {
|
|
190
|
-
return
|
|
199
|
+
return normalizeObsBackendLabel(value) ?? "unknown";
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
194
|
-
|
|
195
|
-
return trimmed || undefined;
|
|
203
|
+
return normalizeObsBackendLabel(value);
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
function renderObsCostRow(row: ObsCostRow): string {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { LoadSessionConfigOptions } from "../config/load-config.ts";
|
|
3
3
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
4
|
+
import {
|
|
5
|
+
createObsTransportSecuritySnapshot,
|
|
6
|
+
type ObsTransportSecuritySnapshot,
|
|
7
|
+
} from "../config/transport-security.ts";
|
|
4
8
|
import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
|
|
5
9
|
import { getGrafanaHealth, type GrafanaFetch } from "../query/grafana.ts";
|
|
6
10
|
import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
|
|
@@ -31,6 +35,7 @@ export interface ObsHealthCheckResult {
|
|
|
31
35
|
export interface ObsHealthSnapshot {
|
|
32
36
|
readonly timeoutMs: number;
|
|
33
37
|
readonly checks: readonly ObsHealthCheckResult[];
|
|
38
|
+
readonly transportSecurity?: ObsTransportSecuritySnapshot;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export interface ObsHealthSnapshotOptions {
|
|
@@ -112,11 +117,18 @@ export async function getObsHealthSnapshot(
|
|
|
112
117
|
getGrafanaHealth(config, { fetch: options.fetch, timeoutMs }),
|
|
113
118
|
]);
|
|
114
119
|
|
|
115
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
timeoutMs,
|
|
122
|
+
checks: [collectorCheck, ...grafanaHealth.checks],
|
|
123
|
+
transportSecurity: createObsTransportSecuritySnapshot(config),
|
|
124
|
+
};
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
export function renderObsHealth(snapshot: ObsHealthSnapshot): string {
|
|
119
|
-
return
|
|
128
|
+
return [
|
|
129
|
+
...formatObsHealthTransportSecurity(snapshot.transportSecurity),
|
|
130
|
+
...snapshot.checks.map(renderObsHealthCheck),
|
|
131
|
+
].join("\n");
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
class ObsHealthCommand {
|
|
@@ -229,6 +241,14 @@ function failedHealthResult(label: string, kind: ObsHealthCheckKind, error: unkn
|
|
|
229
241
|
};
|
|
230
242
|
}
|
|
231
243
|
|
|
244
|
+
function formatObsHealthTransportSecurity(security: ObsTransportSecuritySnapshot | undefined): string[] {
|
|
245
|
+
if (!security) return [];
|
|
246
|
+
return [
|
|
247
|
+
`Collector transport security: ${security.collector}`,
|
|
248
|
+
`Grafana transport security: ${security.grafana}`,
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
232
252
|
function renderObsHealthCheck(check: ObsHealthCheckResult): string {
|
|
233
253
|
const status = formatObsHealthStatus(check);
|
|
234
254
|
const detail = formatObsHealthDetail(check);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { normalizeQueryResultCount } from "../config/query-limits.ts";
|
|
1
2
|
import type { LogResult, TimeRange } from "../query/loki.ts";
|
|
3
|
+
import { normalizeObsBackendLabel } from "../safety/display-bounds.ts";
|
|
2
4
|
import { LOG_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
3
5
|
|
|
4
6
|
export interface ObsLokiLogSummaryRow {
|
|
@@ -25,10 +27,8 @@ export interface ObsLokiTimeRangeOptions {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const defaultRangeHours = 1;
|
|
28
|
-
const minimumMaxLogs = 1;
|
|
29
30
|
const millisecondsPerHour = 60 * 60 * 1000;
|
|
30
31
|
const nanosecondsPerMillisecond = 1_000_000n;
|
|
31
|
-
const maximumDisplayValueLength = 96;
|
|
32
32
|
const safeEventBodyPattern = /^[A-Za-z0-9_.:-]{1,96}$/u;
|
|
33
33
|
const integerNanosecondsPattern = /^-?\d+$/u;
|
|
34
34
|
const unknownTimestamp = "unknown-time";
|
|
@@ -51,8 +51,7 @@ export function formatObsLokiWindow(options: ObsLokiTimeRangeOptions = {}): stri
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function normalizeObsLokiMaxLogs(value: number | undefined): number {
|
|
54
|
-
|
|
55
|
-
return Math.trunc(value);
|
|
54
|
+
return normalizeQueryResultCount(value);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
export function toObsLokiLogSummaryRow(log: LogResult): ObsLokiLogSummaryRow {
|
|
@@ -140,10 +139,7 @@ function normalizeObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): ObsLokiLogSum
|
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
function normalizeDisplayValue(value: string | undefined): string | undefined {
|
|
143
|
-
|
|
144
|
-
if (!normalized) return undefined;
|
|
145
|
-
if (normalized.length <= maximumDisplayValueLength) return normalized;
|
|
146
|
-
return `${normalized.slice(0, maximumDisplayValueLength - 1)}…`;
|
|
142
|
+
return normalizeObsBackendLabel(value);
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
function renderObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): string {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
3
|
+
import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
|
|
4
|
+
import { buildGrafanaTraceLink } from "../query/trace-link.ts";
|
|
2
5
|
import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
|
|
6
|
+
import { notifyObsCommand } from "./obs-command-support.ts";
|
|
3
7
|
|
|
4
8
|
export interface ObsSessionCommandContext {
|
|
5
9
|
readonly ui: {
|
|
@@ -15,6 +19,7 @@ export interface ObsSessionSnapshot {
|
|
|
15
19
|
readonly toolCalls: number;
|
|
16
20
|
readonly costUsd: number;
|
|
17
21
|
readonly traceLink?: string;
|
|
22
|
+
readonly traceLinkError?: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface ObsSessionRuntimeStatePatch {
|
|
@@ -25,12 +30,13 @@ export interface ObsSessionRuntimeStatePatch {
|
|
|
25
30
|
readonly toolCalls?: number;
|
|
26
31
|
readonly costUsd?: number;
|
|
27
32
|
readonly traceLink?: string;
|
|
33
|
+
readonly traceLinkError?: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export interface StartObsSessionRuntimeStateOptions {
|
|
31
37
|
readonly sessionId?: string;
|
|
32
38
|
readonly traceId?: string;
|
|
33
|
-
readonly
|
|
39
|
+
readonly config?: ObservMeConfig;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export type ObsSessionProvider = (ctx: ObsSessionCommandContext) => Promise<ObsSessionSnapshot> | ObsSessionSnapshot;
|
|
@@ -47,6 +53,12 @@ interface MutableObsSessionRuntimeState {
|
|
|
47
53
|
toolCalls: number;
|
|
48
54
|
costUsd: number;
|
|
49
55
|
traceLink?: string;
|
|
56
|
+
traceLinkError?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ObsSessionTraceLinkState {
|
|
60
|
+
readonly traceLink?: string;
|
|
61
|
+
readonly traceLinkError?: string;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
const OBS_COMMAND_NAME = "obs";
|
|
@@ -71,15 +83,15 @@ export async function handleObsSessionCommand(
|
|
|
71
83
|
options: RegisterObsSessionCommandOptions = {},
|
|
72
84
|
): Promise<void> {
|
|
73
85
|
if (!isObsSessionRequest(args)) {
|
|
74
|
-
await
|
|
86
|
+
await notifyObsCommand(ctx, "Usage: /obs session", "warning");
|
|
75
87
|
return;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
try {
|
|
79
91
|
const snapshot = await resolveObsSessionSnapshot(ctx, options);
|
|
80
|
-
await
|
|
92
|
+
await notifyObsCommand(ctx, renderObsSession(snapshot), snapshot.traceLinkError ? "warning" : "info");
|
|
81
93
|
} catch (error) {
|
|
82
|
-
await
|
|
94
|
+
await notifyObsCommand(ctx, `ObservMe session unavailable: ${formatError(error)}`, "error");
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -103,12 +115,13 @@ export function renderObsSession(snapshot: ObsSessionSnapshot): string {
|
|
|
103
115
|
];
|
|
104
116
|
|
|
105
117
|
if (normalized.traceLink) lines.push(`Open trace: ${normalized.traceLink}`);
|
|
118
|
+
else if (normalized.traceLinkError) lines.push(`Trace link unavailable: ${normalized.traceLinkError}`);
|
|
106
119
|
return lines.join("\n");
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
export function startObsSessionRuntimeState(options: StartObsSessionRuntimeStateOptions): void {
|
|
110
123
|
const traceId = normalizeTraceId(options.traceId);
|
|
111
|
-
const
|
|
124
|
+
const traceLinkState = createObsSessionTraceLinkState(options.config, traceId);
|
|
112
125
|
|
|
113
126
|
replaceObsSessionRuntimeState({
|
|
114
127
|
sessionId: normalizeString(options.sessionId),
|
|
@@ -117,10 +130,23 @@ export function startObsSessionRuntimeState(options: StartObsSessionRuntimeState
|
|
|
117
130
|
llmCalls: 0,
|
|
118
131
|
toolCalls: 0,
|
|
119
132
|
costUsd: 0,
|
|
120
|
-
|
|
133
|
+
...traceLinkState,
|
|
121
134
|
});
|
|
122
135
|
}
|
|
123
136
|
|
|
137
|
+
function createObsSessionTraceLinkState(
|
|
138
|
+
config: ObservMeConfig | undefined,
|
|
139
|
+
traceId: string | undefined,
|
|
140
|
+
): ObsSessionTraceLinkState {
|
|
141
|
+
if (!config || !traceId) return {};
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
return { traceLink: buildGrafanaTraceLink(config, traceId) };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return { traceLinkError: sanitizeDiagnosticText(readDiagnosticMessage(error)) };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch): void {
|
|
125
151
|
if (patch.sessionId !== undefined) runtimeSessionState.sessionId = normalizeString(patch.sessionId);
|
|
126
152
|
if (patch.traceId !== undefined) runtimeSessionState.traceId = normalizeTraceId(patch.traceId);
|
|
@@ -129,6 +155,7 @@ export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch)
|
|
|
129
155
|
if (patch.toolCalls !== undefined) runtimeSessionState.toolCalls = normalizeCount(patch.toolCalls);
|
|
130
156
|
if (patch.costUsd !== undefined) runtimeSessionState.costUsd = normalizeCost(patch.costUsd);
|
|
131
157
|
if (patch.traceLink !== undefined) runtimeSessionState.traceLink = normalizeString(patch.traceLink);
|
|
158
|
+
if (patch.traceLinkError !== undefined) runtimeSessionState.traceLinkError = normalizeString(patch.traceLinkError);
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
export function recordObsSessionTurn(count = 1): void {
|
|
@@ -152,20 +179,6 @@ export function clearObsSessionRuntimeState(): void {
|
|
|
152
179
|
replaceObsSessionRuntimeState(createEmptyObsSessionRuntimeState());
|
|
153
180
|
}
|
|
154
181
|
|
|
155
|
-
export function buildObsSessionTraceLink(traceId: string | undefined, traceUrlTemplate: string | undefined): string | undefined {
|
|
156
|
-
const template = normalizeString(traceUrlTemplate);
|
|
157
|
-
if (!traceId || !template) return undefined;
|
|
158
|
-
|
|
159
|
-
if (template.includes("{{traceId}}")) return template.replaceAll("{{traceId}}", encodeURIComponent(traceId));
|
|
160
|
-
if (template.includes("${traceId}")) return template.replaceAll("${traceId}", encodeURIComponent(traceId));
|
|
161
|
-
if (template.includes("{traceId}")) return template.replaceAll("{traceId}", encodeURIComponent(traceId));
|
|
162
|
-
if (template.includes("$traceId")) return template.replaceAll("$traceId", encodeURIComponent(traceId));
|
|
163
|
-
if (template.includes("%TRACE_ID%")) return template.replaceAll("%TRACE_ID%", encodeURIComponent(traceId));
|
|
164
|
-
if (template.includes("__TRACE_ID__")) return template.replaceAll("__TRACE_ID__", encodeURIComponent(traceId));
|
|
165
|
-
if (template.includes("...")) return template.replaceAll("...", encodeURIComponent(traceId));
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
182
|
class ObsSessionCommand {
|
|
170
183
|
readonly #options: RegisterObsSessionCommandOptions;
|
|
171
184
|
|
|
@@ -194,6 +207,7 @@ function replaceObsSessionRuntimeState(state: MutableObsSessionRuntimeState): vo
|
|
|
194
207
|
runtimeSessionState.toolCalls = state.toolCalls;
|
|
195
208
|
runtimeSessionState.costUsd = state.costUsd;
|
|
196
209
|
runtimeSessionState.traceLink = state.traceLink;
|
|
210
|
+
runtimeSessionState.traceLinkError = state.traceLinkError;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
function createEmptyObsSessionRuntimeState(): MutableObsSessionRuntimeState {
|
|
@@ -214,6 +228,7 @@ function normalizeObsSessionSnapshot(snapshot: ObsSessionSnapshot): ObsSessionSn
|
|
|
214
228
|
toolCalls: normalizeCount(snapshot.toolCalls),
|
|
215
229
|
costUsd: normalizeCost(snapshot.costUsd),
|
|
216
230
|
traceLink: normalizeString(snapshot.traceLink),
|
|
231
|
+
traceLinkError: normalizeString(snapshot.traceLinkError),
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -246,14 +261,6 @@ function isObsSessionRequest(args: string): boolean {
|
|
|
246
261
|
return isExactObsSubcommandRequest(args, OBS_SESSION_SUBCOMMAND);
|
|
247
262
|
}
|
|
248
263
|
|
|
249
|
-
async function notifySession(
|
|
250
|
-
ctx: ObsSessionCommandContext,
|
|
251
|
-
message: string,
|
|
252
|
-
type: "info" | "warning" | "error",
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
await ctx.ui?.notify?.(message, type);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
264
|
function formatError(error: unknown): string {
|
|
258
265
|
return error instanceof Error ? error.message : String(error);
|
|
259
266
|
}
|