@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.
@@ -1,4 +1,4 @@
1
- # Production-oriented Collector reference for Tempo, Loki, and Mimir.
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
- prometheusremotewrite/mimir:
101
- endpoint: http://mimir:9009/api/v1/push
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: [prometheusremotewrite/mimir]
123
+ exporters: [prometheus]
124
124
 
125
125
  logs:
126
126
  receivers: [otlp]
@@ -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",
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",
@@ -101,6 +101,7 @@ export const PROJECT_OBSERVME_YAML_TEMPLATE = `observme:
101
101
  enabled: true
102
102
  exportIntervalMillis: 15000
103
103
  exportTimeoutMillis: 3000
104
+ activeAgentLeaseDurationMillis: 60000
104
105
 
105
106
  logs:
106
107
  enabled: true
@@ -60,6 +60,7 @@ export const defaultObservMeConfig = {
60
60
  enabled: true,
61
61
  exportIntervalMillis: 15000,
62
62
  exportTimeoutMillis: 3000,
63
+ activeAgentLeaseDurationMillis: 60000,
63
64
  },
64
65
  logs: {
65
66
  enabled: true,
@@ -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;
@@ -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 },
@@ -1,6 +1,9 @@
1
1
  import { Compile } from "typebox/compile";
2
2
  import { defaultObservMeConfig } from "./defaults.ts";
3
- import { observMeConfigSchema } from "./schema.ts";
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({ config, lineage, now: options.now });
187
- session.now = options.now ?? session.now ?? monotonicNowMs;
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
- agentTree: session.agentTree,
207
- sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
208
- traceId: readSpanTraceId(session.sessionSpan),
189
+ now: options.now,
190
+ wallClockNow: options.wallClockNow,
209
191
  });
210
- session.workflowStartedAtMs = session.now();
211
- session.metrics.sessionsStarted.add(1, labels);
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
- if (isRootWorkflow(lineage)) {
219
- session.metrics.workflowsStarted.add(1, labels);
220
- emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
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
- session.metrics.sessionsShutdown.add(1, labels);
443
- if (session.activeAgentRecorded) session.metrics.activeAgents.add(-1, labels);
444
- recordWorkflowShutdownTelemetry(session, shutdownAttributes, failed, labels);
445
- endAllActiveSpans(session);
446
- session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_SHUTDOWN, shutdownAttributes);
447
- if (failed) session.sessionSpan?.setStatus({ code: SpanStatusCode.ERROR });
448
- endActiveSpan(session, session.sessionSpan);
449
- await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
450
- await recordControllerOperationResult(session, "flush");
451
- await recordControllerOperationResult(session, "shutdown");
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: createSpanRegistry(config, metrics, getTelemetryDropTarget),
192
- agentTree: createAgentTreeTracker(config, options.lineage, metrics, getTelemetryDropTarget),
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: createTurnSequenceRegistry(config, metrics, getTelemetryDropTarget),
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),
@@ -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<Meter, "createCounter" | "createHistogram" | "createUpDownCounter">;
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;
@@ -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 = {