@senad-d/observme 0.1.3 → 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 +13 -0
- package/README.md +12 -3
- 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/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/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/semconv/metrics.ts +6 -0
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,
|
|
@@ -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),
|
package/src/pi/handler-types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Counter, Histogram, Meter, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
|
|
1
|
+
import type { Counter, Histogram, Meter, ObservableGauge, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
|
|
2
2
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
3
3
|
import type { EnsureProjectConfig as BootstrapEnsureProjectConfig } from "../config/bootstrap-project-config.ts";
|
|
4
4
|
import type {
|
|
@@ -13,6 +13,7 @@ import type { ObservMeOtelSdkController } from "../otel/sdk.ts";
|
|
|
13
13
|
import type { ObservMeTraceSdk } from "../otel/traces.ts";
|
|
14
14
|
import type { BoundedMap } from "../util/bounded-map.ts";
|
|
15
15
|
import type { AgentLineageContext } from "./agent-lineage.ts";
|
|
16
|
+
import type { ActiveAgentLeaseController } from "./active-agent-lease.ts";
|
|
16
17
|
import type { AgentTreeTracker } from "./agent-tree-tracker.ts";
|
|
17
18
|
import type {
|
|
18
19
|
AgentWaitJoinState,
|
|
@@ -22,7 +23,10 @@ import type {
|
|
|
22
23
|
|
|
23
24
|
export type AttributePrimitive = boolean | number | string | string[];
|
|
24
25
|
export type AttributeMap = Record<string, AttributePrimitive>;
|
|
25
|
-
export type TelemetryMeter = Pick<
|
|
26
|
+
export type TelemetryMeter = Pick<
|
|
27
|
+
Meter,
|
|
28
|
+
"createCounter" | "createHistogram" | "createObservableGauge" | "createUpDownCounter"
|
|
29
|
+
>;
|
|
26
30
|
export type TelemetryTracer = Pick<Tracer, "startSpan">;
|
|
27
31
|
export type TelemetryLogger = Pick<Logger, "emit">;
|
|
28
32
|
export type Handler = (event: unknown, ctx: ObservMeHandlerContext) => Promise<void> | void;
|
|
@@ -88,6 +92,7 @@ export interface RegisterHandlersOptions {
|
|
|
88
92
|
readonly startTelemetry?: StartSessionTelemetry;
|
|
89
93
|
readonly env?: NodeJS.ProcessEnv;
|
|
90
94
|
readonly now?: () => number;
|
|
95
|
+
readonly wallClockNow?: () => number;
|
|
91
96
|
readonly configDirName?: string;
|
|
92
97
|
readonly trustedParentContext?: boolean;
|
|
93
98
|
readonly requireCompleteParentEnvelope?: boolean;
|
|
@@ -100,6 +105,7 @@ export interface StartSessionTelemetryOptions {
|
|
|
100
105
|
readonly config: ObservMeConfig;
|
|
101
106
|
readonly lineage: AgentLineageContext;
|
|
102
107
|
readonly now?: () => number;
|
|
108
|
+
readonly wallClockNow?: () => number;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
export interface TurnSequenceRegistry {
|
|
@@ -119,6 +125,7 @@ export interface ObservMeTelemetrySession {
|
|
|
119
125
|
readonly logger: TelemetryLogger;
|
|
120
126
|
readonly metrics: ObservMeMetrics;
|
|
121
127
|
readonly spans: SpanRegistry;
|
|
128
|
+
readonly activeAgentLease?: ActiveAgentLeaseController;
|
|
122
129
|
agentTree: AgentTreeTracker;
|
|
123
130
|
sessionSpan?: Span;
|
|
124
131
|
sessionAttributes?: AttributeMap;
|
|
@@ -179,6 +186,7 @@ export interface ObservMeMetrics {
|
|
|
179
186
|
readonly eventsObserved: Counter;
|
|
180
187
|
readonly activeSpans: UpDownCounter;
|
|
181
188
|
readonly activeAgents: UpDownCounter;
|
|
189
|
+
readonly agentLeaseExpiresUnixTimeSeconds: ObservableGauge;
|
|
182
190
|
readonly workflowDurationMs: Histogram;
|
|
183
191
|
readonly agentRunDurationMs: Histogram;
|
|
184
192
|
readonly agentLifetimeDurationMs: Histogram;
|
package/src/semconv/metrics.ts
CHANGED
|
@@ -66,6 +66,12 @@ export const OBSERVME_HISTOGRAM_METRIC_NAMES = {
|
|
|
66
66
|
export const OBSERVME_GAUGE_METRIC_NAMES = {
|
|
67
67
|
ACTIVE_SPANS: "observme_active_spans",
|
|
68
68
|
ACTIVE_AGENTS: "observme_active_agents",
|
|
69
|
+
AGENT_LEASE_EXPIRES_UNIXTIME_SECONDS: "observme_agent_lease_expires_unixtime_seconds",
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
export const OBSERVME_AGENT_LEASE_METRIC_OPTIONS = {
|
|
73
|
+
unit: "s",
|
|
74
|
+
description: "Absolute Unix timestamp in seconds when the active Pi agent lease expires.",
|
|
69
75
|
} as const;
|
|
70
76
|
|
|
71
77
|
export const OFFICIAL_GENAI_METRIC_NAMES = {
|