@senad-d/observme 0.1.2 → 0.1.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/.env.example +4 -0
- package/CHANGELOG.md +14 -0
- package/README.md +15 -4
- 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 +15 -7
- package/docs/compatibility-matrix.md +10 -2
- package/docs/configuration.md +5 -0
- package/docs/extension-integration.md +14 -2
- package/docs/reference/04-telemetry-semantic-conventions.md +38 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- 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 +61 -5
- package/docs/reference/12-configuration-reference.md +18 -1
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/observme.yaml +1 -0
- package/package.json +3 -1
- package/src/config/bootstrap-project-config.ts +1 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/load-config.ts +11 -0
- package/src/config/schema.ts +9 -0
- package/src/config/validate.ts +20 -1
- package/src/integration.ts +46 -4
- package/src/pi/active-agent-lease.ts +101 -0
- package/src/pi/event-handlers/lifecycle.ts +120 -46
- package/src/pi/handler-runtime.ts +21 -3
- package/src/pi/handler-types.ts +10 -2
- package/src/pi/integration-api.ts +184 -13
- package/src/semconv/metrics.ts +6 -0
package/examples/README.md
CHANGED
|
@@ -11,6 +11,7 @@ The npm package ships configuration examples and an extension-integration exampl
|
|
|
11
11
|
- Local self-signed TLS and IPv4 preference are enabled for that profile.
|
|
12
12
|
- Prompt, response, thinking, tool, Bash, and path capture remain disabled.
|
|
13
13
|
- Redaction remains enabled and unsafe capture remains disabled.
|
|
14
|
+
- Metrics export every 15 seconds and renew a 60-second active-agent lease; keep producer and Prometheus clocks synchronized within 5 seconds.
|
|
14
15
|
|
|
15
16
|
To use it in a trusted project:
|
|
16
17
|
|
|
@@ -27,13 +28,17 @@ Read [`../docs/configuration.md`](../docs/configuration.md) for the quick guide
|
|
|
27
28
|
|
|
28
29
|
- traces are exported to Tempo;
|
|
29
30
|
- logs are exported to Loki;
|
|
30
|
-
- metrics are
|
|
31
|
+
- metrics are exposed on a Prometheus scrape endpoint;
|
|
32
|
+
- the five-minute `metric_expiration` applies to all exported metric types only as stale-series/cardinality cleanup, not active-agent liveness;
|
|
33
|
+
- lease-aware PromQL joins the positive lifecycle claim and lease on generated `observme_instance_id`; raw `observme_active_agents` sums are diagnostics, not production live totals;
|
|
31
34
|
- high-cardinality workflow, agent, session, and spawn attributes are removed from metrics;
|
|
32
35
|
- accidental content attributes are removed from logs as defense in depth.
|
|
33
36
|
|
|
34
37
|
The example uses Collector components commonly provided by the Collector Contrib distribution. Verify that your deployed distribution includes every receiver, processor, and exporter, then replace backend endpoints and security settings for your environment.
|
|
35
38
|
|
|
36
|
-
Read [`../docs/reference/05-otel-pipeline-and-collector.md`](../docs/reference/05-otel-pipeline-and-collector.md) for pipeline design and [`../docs/reference/11-deployment-runbooks.md`](../docs/reference/11-deployment-runbooks.md) for deployment and incident checks.
|
|
39
|
+
Read [`../docs/reference/05-otel-pipeline-and-collector.md`](../docs/reference/05-otel-pipeline-and-collector.md) for pipeline design, [`../docs/reference/09-dashboards-alerts-slos.md`](../docs/reference/09-dashboards-alerts-slos.md#131-canonical-active-agent-promql) for canonical queries and raw-query migration, and [`../docs/reference/11-deployment-runbooks.md`](../docs/reference/11-deployment-runbooks.md) for deployment and incident checks.
|
|
40
|
+
|
|
41
|
+
For GitHub Actions, prefer graceful Pi/sidecar shutdown and an `if: always()` cleanup step, but do not depend on either after force cancellation. Lease expiry provides correctness without a Collector restart. GitHub-hosted runner clocks meet the supported expectation; self-hosted runners must use reliable time synchronization.
|
|
37
42
|
|
|
38
43
|
## `integrations/subagent-runner.ts`
|
|
39
44
|
|
package/examples/collector.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Production-oriented Collector reference for Tempo, Loki, and
|
|
1
|
+
# Production-oriented Collector reference for Tempo, Loki, and Prometheus.
|
|
2
2
|
# Verify that your Collector distribution contains every configured component,
|
|
3
3
|
# replace backend endpoints/security for your deployment, and never add raw
|
|
4
4
|
# content or high-cardinality execution identifiers to metric labels.
|
|
@@ -97,14 +97,14 @@ exporters:
|
|
|
97
97
|
retry_on_failure:
|
|
98
98
|
enabled: true
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
endpoint:
|
|
100
|
+
prometheus:
|
|
101
|
+
endpoint: 0.0.0.0:8889
|
|
102
|
+
# Exporter-wide stale-series/cardinality cleanup for gauges, counters, and
|
|
103
|
+
# histograms. Five minutes exceeds ObservMe's default one-minute active-agent
|
|
104
|
+
# lease; lease-aware PromQL, not expiration, determines liveness.
|
|
105
|
+
metric_expiration: 5m
|
|
102
106
|
resource_to_telemetry_conversion:
|
|
103
107
|
enabled: true
|
|
104
|
-
sending_queue:
|
|
105
|
-
enabled: true
|
|
106
|
-
retry_on_failure:
|
|
107
|
-
enabled: true
|
|
108
108
|
|
|
109
109
|
debug:
|
|
110
110
|
verbosity: basic
|
|
@@ -120,7 +120,7 @@ service:
|
|
|
120
120
|
metrics:
|
|
121
121
|
receivers: [otlp]
|
|
122
122
|
processors: [memory_limiter, resource/observme, resource/drop_high_cardinality_metric_attrs, batch]
|
|
123
|
-
exporters: [
|
|
123
|
+
exporters: [prometheus]
|
|
124
124
|
|
|
125
125
|
logs:
|
|
126
126
|
receivers: [otlp]
|
package/examples/observme.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senad-d/observme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
package/src/config/defaults.ts
CHANGED
|
@@ -147,6 +147,11 @@ export function envToConfig(env: NodeJS.ProcessEnv = process.env): DeepPartial<O
|
|
|
147
147
|
setString(config, ["otlp", "signalEndpoints", "metrics"], env.OBSERVME_OTLP_METRICS_ENDPOINT);
|
|
148
148
|
setString(config, ["otlp", "signalEndpoints", "logs"], env.OBSERVME_OTLP_LOGS_ENDPOINT);
|
|
149
149
|
setNumber(config, ["otlp", "timeoutMs"], env.OBSERVME_OTLP_TIMEOUT_MS);
|
|
150
|
+
setNumberForValidation(
|
|
151
|
+
config,
|
|
152
|
+
["metrics", "activeAgentLeaseDurationMillis"],
|
|
153
|
+
env.OBSERVME_ACTIVE_AGENT_LEASE_DURATION_MS,
|
|
154
|
+
);
|
|
150
155
|
setAuthorizationHeader(config, env.OBSERVME_OTLP_TOKEN);
|
|
151
156
|
setNumber(config, ["workflow", "maxDepthWarning"], env.OBSERVME_WORKFLOW_MAX_DEPTH_WARNING);
|
|
152
157
|
setNumber(config, ["workflow", "maxFanoutWarning"], env.OBSERVME_WORKFLOW_MAX_FANOUT_WARNING);
|
|
@@ -592,6 +597,12 @@ function setNumber(target: DeepPartial<ObservMeConfig>, path: string[], value: s
|
|
|
592
597
|
setPathValue(target as Record<string, unknown>, path, parsed);
|
|
593
598
|
}
|
|
594
599
|
|
|
600
|
+
function setNumberForValidation(target: DeepPartial<ObservMeConfig>, path: string[], value: string | undefined) {
|
|
601
|
+
if (value === undefined || value === "") return;
|
|
602
|
+
const parsed = Number(value);
|
|
603
|
+
setPathValue(target as Record<string, unknown>, path, Number.isFinite(parsed) ? parsed : value);
|
|
604
|
+
}
|
|
605
|
+
|
|
595
606
|
function setBoolean(target: DeepPartial<ObservMeConfig>, path: string[], value: string | undefined) {
|
|
596
607
|
const parsed = parseBooleanEnv(value);
|
|
597
608
|
if (parsed === undefined) return;
|
package/src/config/schema.ts
CHANGED
|
@@ -8,6 +8,10 @@ export type ObservMeEnvironment = (typeof observMeEnvironments)[number];
|
|
|
8
8
|
export type OtlpProtocol = "http/protobuf";
|
|
9
9
|
export type PrivacyPathMode = (typeof privacyPathModes)[number];
|
|
10
10
|
|
|
11
|
+
export const ACTIVE_AGENT_LEASE_DURATION_MILLIS_MINIMUM = 10_000;
|
|
12
|
+
export const ACTIVE_AGENT_LEASE_DURATION_MILLIS_MAXIMUM = 300_000;
|
|
13
|
+
export const ACTIVE_AGENT_LEASE_EXPORT_SAFETY_MARGIN_MILLIS = 5_000;
|
|
14
|
+
|
|
11
15
|
export interface OtlpTlsConfig {
|
|
12
16
|
enabled: boolean;
|
|
13
17
|
insecureSkipVerify: boolean;
|
|
@@ -71,6 +75,7 @@ export interface MetricsConfig {
|
|
|
71
75
|
enabled: boolean;
|
|
72
76
|
exportIntervalMillis: number;
|
|
73
77
|
exportTimeoutMillis: number;
|
|
78
|
+
activeAgentLeaseDurationMillis: number;
|
|
74
79
|
labels?: string[];
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -288,6 +293,10 @@ export const observMeConfigSchema = Type.Object(
|
|
|
288
293
|
enabled: Type.Boolean(),
|
|
289
294
|
exportIntervalMillis: positiveIntegerSchema,
|
|
290
295
|
exportTimeoutMillis: positiveIntegerSchema,
|
|
296
|
+
activeAgentLeaseDurationMillis: Type.Integer({
|
|
297
|
+
minimum: ACTIVE_AGENT_LEASE_DURATION_MILLIS_MINIMUM,
|
|
298
|
+
maximum: ACTIVE_AGENT_LEASE_DURATION_MILLIS_MAXIMUM,
|
|
299
|
+
}),
|
|
291
300
|
labels: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
292
301
|
},
|
|
293
302
|
{ additionalProperties: false },
|
package/src/config/validate.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Compile } from "typebox/compile";
|
|
2
2
|
import { defaultObservMeConfig } from "./defaults.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ACTIVE_AGENT_LEASE_EXPORT_SAFETY_MARGIN_MILLIS,
|
|
5
|
+
observMeConfigSchema,
|
|
6
|
+
} from "./schema.ts";
|
|
4
7
|
import type { ObservMeConfig } from "./schema.ts";
|
|
5
8
|
import { validateCustomRedactionPatterns } from "../privacy/redact.ts";
|
|
6
9
|
|
|
@@ -58,6 +61,7 @@ const knownConfigValidationIssueCodes = new Set([
|
|
|
58
61
|
"insecure_production_transport",
|
|
59
62
|
"invalid_signal_endpoint_path",
|
|
60
63
|
"high_cardinality_metric_label",
|
|
64
|
+
"active_agent_lease_too_short_for_export_interval",
|
|
61
65
|
"custom_redaction_pattern_limit",
|
|
62
66
|
"custom_redaction_pattern_too_long",
|
|
63
67
|
"custom_redaction_pattern_unsupported_construct",
|
|
@@ -82,6 +86,7 @@ export function validateObservMeConfig(
|
|
|
82
86
|
...validateTransportSecurity(config),
|
|
83
87
|
...validateSignalEndpoints(config),
|
|
84
88
|
...validateMetricLabels(config),
|
|
89
|
+
...validateActiveAgentLeaseDuration(config),
|
|
85
90
|
...validateCustomRedactionPatternConfig(config),
|
|
86
91
|
...validateProjectTrust(options),
|
|
87
92
|
...validateLineageEnvironment(config, options.env ?? process.env),
|
|
@@ -307,6 +312,20 @@ function validateMetricLabels(config: ObservMeConfig): ValidationIssue[] {
|
|
|
307
312
|
}));
|
|
308
313
|
}
|
|
309
314
|
|
|
315
|
+
function validateActiveAgentLeaseDuration(config: ObservMeConfig): ValidationIssue[] {
|
|
316
|
+
const requiredDuration =
|
|
317
|
+
(2 * config.metrics.exportIntervalMillis) + ACTIVE_AGENT_LEASE_EXPORT_SAFETY_MARGIN_MILLIS;
|
|
318
|
+
if (config.metrics.activeAgentLeaseDurationMillis >= requiredDuration) return [];
|
|
319
|
+
|
|
320
|
+
return [
|
|
321
|
+
{
|
|
322
|
+
code: "active_agent_lease_too_short_for_export_interval",
|
|
323
|
+
message:
|
|
324
|
+
"metrics.activeAgentLeaseDurationMillis must be at least twice metrics.exportIntervalMillis plus the clock-skew safety margin.",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
|
|
310
329
|
function validateCustomRedactionPatternConfig(config: ObservMeConfig): ValidationIssue[] {
|
|
311
330
|
return validateCustomRedactionPatterns(config.privacy.customRedactionPatterns).map(issue => ({
|
|
312
331
|
code: issue.code,
|
package/src/integration.ts
CHANGED
|
@@ -10,6 +10,10 @@ export type ObservMeChildStatus = "starting" | "active" | "completed" | "failed"
|
|
|
10
10
|
export type ObservMeJoinStatus = "completed" | "failed" | "cancelled" | "timeout" | "unknown" | "waiting";
|
|
11
11
|
export type ObservMeIntegrationFailureReason =
|
|
12
12
|
| "session_unavailable"
|
|
13
|
+
| "invalid_request"
|
|
14
|
+
| "spawn_already_exists"
|
|
15
|
+
| "wait_already_exists"
|
|
16
|
+
| "join_already_exists"
|
|
13
17
|
| "spawn_not_found"
|
|
14
18
|
| "wait_not_found"
|
|
15
19
|
| "join_not_found"
|
|
@@ -118,17 +122,55 @@ interface IntegrationResponseHolder {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
export function requestObservMeIntegration(host: ObservMeIntegrationHost): ObservMeIntegrationApi | undefined {
|
|
125
|
+
const events = resolveIntegrationEventBus(host);
|
|
126
|
+
if (!events) return undefined;
|
|
127
|
+
|
|
121
128
|
const holder: IntegrationResponseHolder = {};
|
|
122
129
|
const request: ObservMeIntegrationRequest = {
|
|
123
130
|
supportedVersions: [OBSERVME_INTEGRATION_VERSION],
|
|
124
131
|
respond: receiveObservMeIntegration.bind(undefined, holder),
|
|
125
132
|
};
|
|
126
133
|
|
|
127
|
-
|
|
134
|
+
try {
|
|
135
|
+
events.emit(OBSERVME_INTEGRATION_CHANNEL, request);
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
128
139
|
return holder.api;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
function
|
|
132
|
-
if (
|
|
133
|
-
|
|
142
|
+
function resolveIntegrationEventBus(host: unknown): ObservMeIntegrationEventBus | undefined {
|
|
143
|
+
if (!host || typeof host !== "object") return undefined;
|
|
144
|
+
try {
|
|
145
|
+
const events = (host as Partial<ObservMeIntegrationHost>).events;
|
|
146
|
+
return events && typeof events.emit === "function" ? events : undefined;
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function receiveObservMeIntegration(holder: IntegrationResponseHolder, value: unknown): void {
|
|
153
|
+
if (holder.api || !isObservMeIntegrationApi(value)) return;
|
|
154
|
+
holder.api = value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isObservMeIntegrationApi(value: unknown): value is ObservMeIntegrationApi {
|
|
158
|
+
if (!value || typeof value !== "object") return false;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const api = value as Partial<ObservMeIntegrationApi>;
|
|
162
|
+
return (
|
|
163
|
+
api.version === OBSERVME_INTEGRATION_VERSION &&
|
|
164
|
+
typeof api.getContext === "function" &&
|
|
165
|
+
typeof api.startSubagent === "function" &&
|
|
166
|
+
typeof api.completeSubagent === "function" &&
|
|
167
|
+
typeof api.failSubagent === "function" &&
|
|
168
|
+
typeof api.startWait === "function" &&
|
|
169
|
+
typeof api.endWait === "function" &&
|
|
170
|
+
typeof api.startJoin === "function" &&
|
|
171
|
+
typeof api.endJoin === "function"
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
134
176
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Attributes, ObservableCallback, ObservableGauge, ObservableResult } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
export interface ActiveAgentLeaseController {
|
|
4
|
+
readonly active: boolean;
|
|
5
|
+
readonly disposed: boolean;
|
|
6
|
+
activate: () => void;
|
|
7
|
+
deactivate: () => void;
|
|
8
|
+
dispose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CreateActiveAgentLeaseOptions {
|
|
12
|
+
readonly instrument: ObservableGauge;
|
|
13
|
+
readonly leaseDurationMillis: number;
|
|
14
|
+
readonly attributes?: Attributes;
|
|
15
|
+
readonly wallClockNow?: () => number;
|
|
16
|
+
readonly enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ActiveAgentLeaseState {
|
|
20
|
+
readonly leaseDurationMillis: number;
|
|
21
|
+
readonly attributes: Attributes;
|
|
22
|
+
readonly wallClockNow: () => number;
|
|
23
|
+
active: boolean;
|
|
24
|
+
disposed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createActiveAgentLease(
|
|
28
|
+
options: CreateActiveAgentLeaseOptions,
|
|
29
|
+
): ActiveAgentLeaseController | undefined {
|
|
30
|
+
if (options.enabled === false) return undefined;
|
|
31
|
+
return new SessionActiveAgentLease(options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function computeActiveAgentLeaseExpiryUnixSeconds(
|
|
35
|
+
wallClockUnixMillis: number,
|
|
36
|
+
leaseDurationMillis: number,
|
|
37
|
+
): number | undefined {
|
|
38
|
+
if (!Number.isFinite(wallClockUnixMillis) || !Number.isFinite(leaseDurationMillis)) return undefined;
|
|
39
|
+
|
|
40
|
+
const expiryMillis = wallClockUnixMillis + leaseDurationMillis;
|
|
41
|
+
if (!Number.isFinite(expiryMillis)) return undefined;
|
|
42
|
+
|
|
43
|
+
const expiryUnixSeconds = expiryMillis / 1000;
|
|
44
|
+
return Number.isFinite(expiryUnixSeconds) ? expiryUnixSeconds : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function observeActiveAgentLease(state: ActiveAgentLeaseState, result: ObservableResult): void {
|
|
48
|
+
if (!state.active || state.disposed) return;
|
|
49
|
+
|
|
50
|
+
const expiryUnixSeconds = computeActiveAgentLeaseExpiryUnixSeconds(
|
|
51
|
+
state.wallClockNow(),
|
|
52
|
+
state.leaseDurationMillis,
|
|
53
|
+
);
|
|
54
|
+
if (expiryUnixSeconds === undefined) return;
|
|
55
|
+
|
|
56
|
+
result.observe(expiryUnixSeconds, state.attributes);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class SessionActiveAgentLease implements ActiveAgentLeaseController {
|
|
60
|
+
readonly #instrument: ObservableGauge;
|
|
61
|
+
readonly #state: ActiveAgentLeaseState;
|
|
62
|
+
readonly #callback: ObservableCallback;
|
|
63
|
+
|
|
64
|
+
constructor(options: CreateActiveAgentLeaseOptions) {
|
|
65
|
+
this.#instrument = options.instrument;
|
|
66
|
+
this.#state = {
|
|
67
|
+
leaseDurationMillis: options.leaseDurationMillis,
|
|
68
|
+
attributes: { ...options.attributes },
|
|
69
|
+
wallClockNow: options.wallClockNow ?? Date.now,
|
|
70
|
+
active: false,
|
|
71
|
+
disposed: false,
|
|
72
|
+
};
|
|
73
|
+
this.#callback = observeActiveAgentLease.bind(undefined, this.#state);
|
|
74
|
+
this.#instrument.addCallback(this.#callback);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get active(): boolean {
|
|
78
|
+
return this.#state.active;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get disposed(): boolean {
|
|
82
|
+
return this.#state.disposed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
activate(): void {
|
|
86
|
+
if (this.#state.disposed) return;
|
|
87
|
+
this.#state.active = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
deactivate(): void {
|
|
91
|
+
this.#state.active = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
dispose(): void {
|
|
95
|
+
if (this.#state.disposed) return;
|
|
96
|
+
|
|
97
|
+
this.#state.active = false;
|
|
98
|
+
this.#state.disposed = true;
|
|
99
|
+
this.#instrument.removeCallback(this.#callback);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -183,45 +183,54 @@ async function handleSessionStart(
|
|
|
183
183
|
options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
|
|
184
184
|
failOpenInvalidPropagation: true,
|
|
185
185
|
});
|
|
186
|
-
const session = await startTelemetryFn({
|
|
187
|
-
|
|
188
|
-
updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
|
|
189
|
-
clearObsStatusExportError();
|
|
190
|
-
state.session = session;
|
|
191
|
-
const attributes = buildSessionAttributes(event, ctx, session.config, lineage, recovery);
|
|
192
|
-
const labels = metricLabels(session.config, lineage);
|
|
193
|
-
|
|
194
|
-
session.sessionAttributes = attributes;
|
|
195
|
-
const traceParent = resolveSessionTraceParent(lineage);
|
|
196
|
-
session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
|
|
197
|
-
emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
|
|
198
|
-
recordSessionTracePropagationFailure(session, traceParent);
|
|
199
|
-
startObsSessionRuntimeState({
|
|
200
|
-
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
201
|
-
traceId: readSpanTraceId(session.sessionSpan),
|
|
202
|
-
traceUrlTemplate: session.config.query.links.traceUrlTemplate,
|
|
203
|
-
});
|
|
204
|
-
startObsAgentsRuntimeState({
|
|
186
|
+
const session = await startTelemetryFn({
|
|
187
|
+
config,
|
|
205
188
|
lineage,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
traceId: readSpanTraceId(session.sessionSpan),
|
|
189
|
+
now: options.now,
|
|
190
|
+
wallClockNow: options.wallClockNow,
|
|
209
191
|
});
|
|
210
|
-
session.
|
|
211
|
-
session
|
|
212
|
-
session.metrics.activeAgents.add(1, labels);
|
|
213
|
-
session.activeAgentRecorded = true;
|
|
214
|
-
session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
215
|
-
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
216
|
-
if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
|
|
192
|
+
session.now = options.now ?? session.now ?? monotonicNowMs;
|
|
193
|
+
state.session = session;
|
|
217
194
|
|
|
218
|
-
|
|
219
|
-
session.
|
|
220
|
-
|
|
195
|
+
try {
|
|
196
|
+
updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
|
|
197
|
+
clearObsStatusExportError();
|
|
198
|
+
const attributes = buildSessionAttributes(event, ctx, session.config, lineage, recovery);
|
|
199
|
+
const labels = metricLabels(session.config, lineage);
|
|
200
|
+
|
|
201
|
+
session.sessionAttributes = attributes;
|
|
202
|
+
const traceParent = resolveSessionTraceParent(lineage);
|
|
203
|
+
session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
|
|
204
|
+
emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
|
|
205
|
+
recordSessionTracePropagationFailure(session, traceParent);
|
|
206
|
+
startObsSessionRuntimeState({
|
|
207
|
+
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
208
|
+
traceId: readSpanTraceId(session.sessionSpan),
|
|
209
|
+
traceUrlTemplate: session.config.query.links.traceUrlTemplate,
|
|
210
|
+
});
|
|
211
|
+
startObsAgentsRuntimeState({
|
|
212
|
+
lineage,
|
|
213
|
+
agentTree: session.agentTree,
|
|
214
|
+
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
215
|
+
traceId: readSpanTraceId(session.sessionSpan),
|
|
216
|
+
});
|
|
217
|
+
session.workflowStartedAtMs = session.now();
|
|
218
|
+
session.metrics.sessionsStarted.add(1, labels);
|
|
219
|
+
session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
220
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
221
|
+
if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
|
|
222
|
+
|
|
223
|
+
if (isRootWorkflow(lineage)) {
|
|
224
|
+
session.metrics.workflowsStarted.add(1, labels);
|
|
225
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
|
|
229
|
+
activateSessionActiveAgent(session, labels);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
await cleanUpFailedSessionStart(session, ctx, state);
|
|
232
|
+
throw error;
|
|
221
233
|
}
|
|
222
|
-
|
|
223
|
-
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
|
|
224
|
-
state.session = session;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
function createSessionShutdownHandler(state: HandlerSessionState): Handler {
|
|
@@ -429,6 +438,26 @@ function recordDuplicateSessionStartShutdownError(session: ObservMeTelemetrySess
|
|
|
429
438
|
);
|
|
430
439
|
}
|
|
431
440
|
|
|
441
|
+
async function cleanUpFailedSessionStart(
|
|
442
|
+
session: ObservMeTelemetrySession,
|
|
443
|
+
ctx: ObservMeHandlerContext,
|
|
444
|
+
state: HandlerSessionState,
|
|
445
|
+
): Promise<void> {
|
|
446
|
+
const labels = metricLabels(session.config, session.lineage);
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
deactivateSessionActiveAgent(session, labels);
|
|
450
|
+
endAllActiveSpans(session);
|
|
451
|
+
endActiveSpan(session, session.sessionSpan);
|
|
452
|
+
await clearExtensionStatus(ctx);
|
|
453
|
+
await recordControllerOperationResult(session, "flush");
|
|
454
|
+
await recordControllerOperationResult(session, "shutdown");
|
|
455
|
+
} finally {
|
|
456
|
+
disposeSessionActiveAgentLease(session);
|
|
457
|
+
clearTelemetrySessionRuntimeState(session, state);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
432
461
|
async function shutDownTelemetrySession(
|
|
433
462
|
session: ObservMeTelemetrySession,
|
|
434
463
|
event: unknown,
|
|
@@ -439,19 +468,64 @@ async function shutDownTelemetrySession(
|
|
|
439
468
|
const shutdownAttributes = buildShutdownAttributes(event, session);
|
|
440
469
|
const failed = workflowFailed(event);
|
|
441
470
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
471
|
+
try {
|
|
472
|
+
deactivateSessionActiveAgent(session, labels);
|
|
473
|
+
session.metrics.sessionsShutdown.add(1, labels);
|
|
474
|
+
recordWorkflowShutdownTelemetry(session, shutdownAttributes, failed, labels);
|
|
475
|
+
endAllActiveSpans(session);
|
|
476
|
+
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_SHUTDOWN, shutdownAttributes);
|
|
477
|
+
if (failed) session.sessionSpan?.setStatus({ code: SpanStatusCode.ERROR });
|
|
478
|
+
endActiveSpan(session, session.sessionSpan);
|
|
479
|
+
await clearExtensionStatus(ctx);
|
|
480
|
+
await recordControllerOperationResult(session, "flush");
|
|
481
|
+
await recordControllerOperationResult(session, "shutdown");
|
|
482
|
+
} finally {
|
|
483
|
+
disposeSessionActiveAgentLease(session);
|
|
484
|
+
clearTelemetrySessionRuntimeState(session, state);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function activateSessionActiveAgent(
|
|
489
|
+
session: ObservMeTelemetrySession,
|
|
490
|
+
labels: Record<string, string>,
|
|
491
|
+
): void {
|
|
492
|
+
if (session.activeAgentRecorded) return;
|
|
493
|
+
|
|
494
|
+
session.metrics.activeAgents.add(1, labels);
|
|
495
|
+
session.activeAgentRecorded = true;
|
|
496
|
+
session.activeAgentLease?.activate();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function deactivateSessionActiveAgent(
|
|
500
|
+
session: ObservMeTelemetrySession,
|
|
501
|
+
labels: Record<string, string>,
|
|
502
|
+
): void {
|
|
503
|
+
session.activeAgentLease?.deactivate();
|
|
504
|
+
if (!session.activeAgentRecorded) return;
|
|
505
|
+
|
|
506
|
+
session.activeAgentRecorded = false;
|
|
507
|
+
session.metrics.activeAgents.add(-1, labels);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function disposeSessionActiveAgentLease(session: ObservMeTelemetrySession): void {
|
|
511
|
+
session.activeAgentLease?.dispose();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function clearTelemetrySessionRuntimeState(
|
|
515
|
+
session: ObservMeTelemetrySession,
|
|
516
|
+
state: HandlerSessionState,
|
|
517
|
+
): void {
|
|
452
518
|
clearObsSessionRuntimeState();
|
|
453
519
|
clearObsAgentsRuntimeState();
|
|
454
|
-
state.session = undefined;
|
|
520
|
+
if (state.session === session) state.session = undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void> {
|
|
524
|
+
try {
|
|
525
|
+
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
|
|
526
|
+
} catch {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
455
529
|
}
|
|
456
530
|
|
|
457
531
|
async function recordControllerOperationResult(
|
|
@@ -11,6 +11,7 @@ import { inheritTenantSaltEnvironment } from "../privacy/hash.ts";
|
|
|
11
11
|
import { LOG_ATTRIBUTES, RESOURCE_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
12
12
|
import {
|
|
13
13
|
LOG_EVENT_NAMES,
|
|
14
|
+
OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
|
|
14
15
|
OBSERVME_COUNTER_METRIC_NAMES,
|
|
15
16
|
OBSERVME_GAUGE_METRIC_NAMES,
|
|
16
17
|
OBSERVME_HISTOGRAM_METRIC_NAMES,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
OFFICIAL_GENAI_METRIC_NAMES,
|
|
19
20
|
} from "../semconv/metrics.ts";
|
|
20
21
|
import { BoundedMap, type BoundedMapEviction } from "../util/bounded-map.ts";
|
|
22
|
+
import { createActiveAgentLease } from "./active-agent-lease.ts";
|
|
21
23
|
import type { AgentLineageContext } from "./agent-lineage.ts";
|
|
22
24
|
import { buildResourceLineageAttributes } from "./agent-lineage.ts";
|
|
23
25
|
import type { AgentTreeNode } from "./agent-tree-tracker.ts";
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
evictToolCallState,
|
|
31
33
|
evictWaitJoinState,
|
|
32
34
|
isRecord,
|
|
35
|
+
metricLabels,
|
|
33
36
|
normalizeMetricValue,
|
|
34
37
|
readBoolean,
|
|
35
38
|
readString,
|
|
@@ -180,6 +183,16 @@ export async function startSessionTelemetry(options: StartSessionTelemetryOption
|
|
|
180
183
|
const metrics = createObservMeMetrics(metricSdk.meter);
|
|
181
184
|
const sessionReference: HandlerSessionState = {};
|
|
182
185
|
const getTelemetryDropTarget = resolveSessionTelemetryDropTarget.bind(undefined, sessionReference, metrics);
|
|
186
|
+
const spans = createSpanRegistry(config, metrics, getTelemetryDropTarget);
|
|
187
|
+
const agentTree = createAgentTreeTracker(config, options.lineage, metrics, getTelemetryDropTarget);
|
|
188
|
+
const turnSequences = createTurnSequenceRegistry(config, metrics, getTelemetryDropTarget);
|
|
189
|
+
const activeAgentLease = createActiveAgentLease({
|
|
190
|
+
instrument: metrics.agentLeaseExpiresUnixTimeSeconds,
|
|
191
|
+
leaseDurationMillis: config.metrics.activeAgentLeaseDurationMillis,
|
|
192
|
+
attributes: metricLabels(config, options.lineage),
|
|
193
|
+
wallClockNow: options.wallClockNow,
|
|
194
|
+
enabled: config.metrics.enabled,
|
|
195
|
+
});
|
|
183
196
|
const session: ObservMeTelemetrySession = {
|
|
184
197
|
config,
|
|
185
198
|
lineage: options.lineage,
|
|
@@ -188,14 +201,15 @@ export async function startSessionTelemetry(options: StartSessionTelemetryOption
|
|
|
188
201
|
meter: metricSdk.meter,
|
|
189
202
|
logger: logSdk.logger,
|
|
190
203
|
metrics,
|
|
191
|
-
spans
|
|
192
|
-
|
|
204
|
+
spans,
|
|
205
|
+
activeAgentLease,
|
|
206
|
+
agentTree,
|
|
193
207
|
now: options.now ?? monotonicNowMs,
|
|
194
208
|
activeAgentRecorded: false,
|
|
195
209
|
agentRunSequence: 0,
|
|
196
210
|
llmRequestSequence: 0,
|
|
197
211
|
toolCallSequence: 0,
|
|
198
|
-
turnSequences
|
|
212
|
+
turnSequences,
|
|
199
213
|
};
|
|
200
214
|
|
|
201
215
|
sessionReference.session = session;
|
|
@@ -252,6 +266,10 @@ export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
|
|
|
252
266
|
eventsObserved: meter.createCounter(OBSERVME_COUNTER_METRIC_NAMES.EVENTS_OBSERVED_TOTAL),
|
|
253
267
|
activeSpans: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_SPANS),
|
|
254
268
|
activeAgents: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_AGENTS),
|
|
269
|
+
agentLeaseExpiresUnixTimeSeconds: meter.createObservableGauge(
|
|
270
|
+
OBSERVME_GAUGE_METRIC_NAMES.AGENT_LEASE_EXPIRES_UNIXTIME_SECONDS,
|
|
271
|
+
OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
|
|
272
|
+
),
|
|
255
273
|
workflowDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.WORKFLOW_DURATION_MS),
|
|
256
274
|
agentRunDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_RUN_DURATION_MS),
|
|
257
275
|
agentLifetimeDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_LIFETIME_DURATION_MS),
|