@senad-d/observme 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -0
- package/CHANGELOG.md +48 -0
- package/README.md +36 -11
- package/dashboards/observme-agent-node-graphs.json +5 -5
- package/dashboards/observme-agents.json +169 -4
- package/dashboards/observme-alerts.yaml +16 -3
- package/dashboards/observme-overview.json +6 -6
- package/dashboards/observme-trace-journey.json +4 -4
- package/docs/agent-subagent-observability-requirements.md +19 -10
- package/docs/compatibility-matrix.md +21 -4
- package/docs/configuration.md +7 -2
- package/docs/extension-integration.md +20 -13
- package/docs/reference/03-pi-event-and-session-model.md +6 -6
- package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/09-dashboards-alerts-slos.md +132 -3
- package/docs/reference/10-testing-release-operations.md +44 -14
- package/docs/reference/11-deployment-runbooks.md +82 -5
- package/docs/reference/12-configuration-reference.md +48 -5
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +3 -2
- package/package.json +14 -4
- package/src/commands/obs-agents.ts +17 -12
- package/src/commands/obs-backfill.ts +2 -2
- package/src/commands/obs-command-support.ts +2 -1
- package/src/commands/obs-cost.ts +15 -7
- package/src/commands/obs-health.ts +22 -2
- package/src/commands/obs-loki-summary.ts +4 -8
- package/src/commands/obs-session.ts +35 -28
- package/src/commands/obs-status.ts +56 -13
- package/src/commands/obs-tools.ts +19 -8
- package/src/commands/obs-trace.ts +9 -0
- package/src/commands/obs.ts +6 -1
- package/src/config/bootstrap-project-config.ts +50 -32
- package/src/config/defaults.ts +1 -2
- package/src/config/load-config.ts +270 -81
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +18 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +88 -12
- package/src/extension.ts +2 -12
- package/src/integration.ts +6 -2
- package/src/otel/logs.ts +24 -13
- package/src/otel/metrics.ts +24 -13
- package/src/otel/otlp-endpoint.ts +41 -2
- package/src/otel/otlp-http-options.ts +11 -0
- package/src/otel/sdk.ts +155 -9
- package/src/otel/shutdown.ts +39 -11
- package/src/otel/traces.ts +34 -16
- package/src/pi/active-agent-lease.ts +101 -0
- package/src/pi/agent-tree-tracker.ts +14 -1
- package/src/pi/compatibility.ts +121 -0
- package/src/pi/event-handlers/agent-turn.ts +16 -9
- package/src/pi/event-handlers/lifecycle.ts +312 -106
- package/src/pi/event-handlers/llm.ts +17 -9
- package/src/pi/event-handlers/session-events.ts +53 -19
- package/src/pi/event-handlers/tool-bash.ts +21 -27
- package/src/pi/handler-internals.ts +16 -26
- package/src/pi/handler-runtime.ts +159 -54
- package/src/pi/handler-types.ts +40 -17
- package/src/pi/handlers.ts +12 -1
- package/src/pi/integration-api.ts +27 -15
- package/src/pi/session-correlation.ts +167 -0
- package/src/pi/subagent-spawn.ts +164 -31
- package/src/privacy/redact.ts +574 -68
- package/src/privacy/secret-patterns.ts +6 -1
- package/src/query/grafana-readiness.ts +18 -2
- package/src/query/grafana-transport.ts +150 -27
- package/src/query/grafana-url.ts +36 -0
- package/src/query/grafana.ts +4 -136
- package/src/query/loki.ts +2 -4
- package/src/query/prometheus.ts +8 -10
- package/src/query/tempo.ts +2 -4
- package/src/query/trace-link.ts +186 -0
- package/src/safety/display-bounds.ts +84 -0
- package/src/semconv/metrics.ts +7 -0
- package/src/semconv/values.ts +2 -0
package/src/otel/sdk.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
2
|
+
import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
|
|
2
3
|
import type { AgentLineageContext } from "../pi/agent-lineage.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
BoundedOtelOperationResult,
|
|
6
|
+
OtelOperationSettlement,
|
|
7
|
+
OtelShutdownLogSink,
|
|
8
|
+
ShutdownableOtelSdk,
|
|
9
|
+
} from "./shutdown.ts";
|
|
4
10
|
import { flushOtelSdk, shutdownOtelSdk } from "./shutdown.ts";
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
// `failed` is terminal for startup; shutdown cleanup can remain owned for observation or retry.
|
|
13
|
+
export type OtelSdkLifecycleState =
|
|
14
|
+
| "idle"
|
|
15
|
+
| "starting"
|
|
16
|
+
| "started"
|
|
17
|
+
| "failed"
|
|
18
|
+
| "shutting_down"
|
|
19
|
+
| "shutdown_pending"
|
|
20
|
+
| "shutdown_failed"
|
|
21
|
+
| "shutdown";
|
|
7
22
|
|
|
8
23
|
export interface StartOtelSdkFactoryOptions {
|
|
9
24
|
readonly config: ObservMeConfig;
|
|
@@ -35,6 +50,10 @@ export class ObservMeOtelSdkController {
|
|
|
35
50
|
readonly #sdkFactory: SessionScopedOtelSdkFactory;
|
|
36
51
|
readonly #logger?: OtelShutdownLogSink;
|
|
37
52
|
#sdk?: SessionScopedOtelSdk;
|
|
53
|
+
#startPromise?: Promise<SessionScopedOtelSdk>;
|
|
54
|
+
#shutdownPromise?: Promise<BoundedOtelOperationResult>;
|
|
55
|
+
#pendingShutdown?: Promise<OtelOperationSettlement>;
|
|
56
|
+
#startupCleanupResult?: BoundedOtelOperationResult;
|
|
38
57
|
#state: OtelSdkLifecycleState = "idle";
|
|
39
58
|
|
|
40
59
|
constructor(options: ObservMeOtelSdkControllerOptions) {
|
|
@@ -62,13 +81,42 @@ export class ObservMeOtelSdkController {
|
|
|
62
81
|
|
|
63
82
|
async start(): Promise<SessionScopedOtelSdk> {
|
|
64
83
|
if (this.#state === "started" && this.#sdk) return this.#sdk;
|
|
84
|
+
if (this.#state === "starting" && this.#startPromise) return this.#startPromise;
|
|
85
|
+
if (this.#state === "failed") throw new Error("ObservMe OTEL SDK controller cannot be restarted after failed startup.");
|
|
65
86
|
if (this.#state === "shutdown") throw new Error("ObservMe OTEL SDK controller cannot be restarted after shutdown.");
|
|
87
|
+
if (this.#state !== "idle") {
|
|
88
|
+
throw new Error(`ObservMe OTEL SDK controller cannot start while cleanup state is ${this.#state}.`);
|
|
89
|
+
}
|
|
66
90
|
|
|
67
91
|
this.#state = "starting";
|
|
68
|
-
this.#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
this.#startPromise = this.startOnce();
|
|
93
|
+
return this.#startPromise;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async startOnce(): Promise<SessionScopedOtelSdk> {
|
|
97
|
+
try {
|
|
98
|
+
this.#sdk = this.#sdkFactory({ config: this.#config, agent: this.#agent });
|
|
99
|
+
await this.#sdk.start?.();
|
|
100
|
+
this.#state = "started";
|
|
101
|
+
return this.#sdk;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof ObservMeOtelStartupError) {
|
|
104
|
+
this.#startupCleanupResult = error.cleanup ?? completedShutdown();
|
|
105
|
+
} else {
|
|
106
|
+
this.#startupCleanupResult = sanitizeStartupCleanupResult(
|
|
107
|
+
await shutdownOtelSdk(
|
|
108
|
+
this.#sdk,
|
|
109
|
+
this.#config.shutdown.flushTimeoutMs,
|
|
110
|
+
this.#logger,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
this.#sdk = undefined;
|
|
115
|
+
this.#state = "failed";
|
|
116
|
+
throw toOtelStartupError(error, this.#startupCleanupResult);
|
|
117
|
+
} finally {
|
|
118
|
+
this.#startPromise = undefined;
|
|
119
|
+
}
|
|
72
120
|
}
|
|
73
121
|
|
|
74
122
|
async flush(timeoutMs: number = this.#config.shutdown.flushTimeoutMs): Promise<BoundedOtelOperationResult> {
|
|
@@ -76,12 +124,86 @@ export class ObservMeOtelSdkController {
|
|
|
76
124
|
}
|
|
77
125
|
|
|
78
126
|
async shutdown(timeoutMs: number = this.#config.shutdown.flushTimeoutMs): Promise<BoundedOtelOperationResult> {
|
|
79
|
-
if (this.#state === "shutdown") return
|
|
127
|
+
if (this.#state === "shutdown") return completedShutdown();
|
|
128
|
+
if (this.#state === "failed") return this.#startupCleanupResult ?? completedShutdown();
|
|
129
|
+
if (this.#state === "shutting_down" && this.#shutdownPromise) return this.#shutdownPromise;
|
|
130
|
+
if (this.#state === "shutdown_pending" && this.#pendingShutdown) {
|
|
131
|
+
return pendingShutdown(this.#pendingShutdown);
|
|
132
|
+
}
|
|
80
133
|
|
|
81
134
|
this.#state = "shutting_down";
|
|
82
|
-
|
|
135
|
+
this.#shutdownPromise = this.shutdownOnce(timeoutMs);
|
|
136
|
+
return this.#shutdownPromise;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async shutdownOnce(timeoutMs: number): Promise<BoundedOtelOperationResult> {
|
|
140
|
+
try {
|
|
141
|
+
const result = await shutdownOtelSdk(this.#sdk, timeoutMs, this.#logger);
|
|
142
|
+
this.applyShutdownResult(result);
|
|
143
|
+
return result;
|
|
144
|
+
} finally {
|
|
145
|
+
this.#shutdownPromise = undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private applyShutdownResult(result: BoundedOtelOperationResult): void {
|
|
150
|
+
if (result.timedOut && result.settlement) {
|
|
151
|
+
this.#pendingShutdown = result.settlement;
|
|
152
|
+
this.#state = "shutdown_pending";
|
|
153
|
+
void result.settlement.then(this.observePendingShutdown.bind(this, result.settlement));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (result.error || !result.completed) {
|
|
158
|
+
this.#state = "shutdown_failed";
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.releaseShutdownOwnership();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private observePendingShutdown(
|
|
166
|
+
pending: Promise<OtelOperationSettlement>,
|
|
167
|
+
settlement: OtelOperationSettlement,
|
|
168
|
+
): void {
|
|
169
|
+
if (this.#pendingShutdown !== pending) return;
|
|
170
|
+
|
|
171
|
+
this.#pendingShutdown = undefined;
|
|
172
|
+
if (settlement.error || !settlement.completed) {
|
|
173
|
+
this.#state = "shutdown_failed";
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.releaseShutdownOwnership();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private releaseShutdownOwnership(): void {
|
|
181
|
+
this.#sdk = undefined;
|
|
83
182
|
this.#state = "shutdown";
|
|
84
|
-
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function toOtelStartupError(
|
|
187
|
+
error: unknown,
|
|
188
|
+
cleanup?: BoundedOtelOperationResult,
|
|
189
|
+
): Error {
|
|
190
|
+
if (error instanceof ObservMeOtelStartupError) return error;
|
|
191
|
+
|
|
192
|
+
const detail = sanitizeDiagnosticText(readDiagnosticMessage(error));
|
|
193
|
+
const safeCleanup = cleanup ? sanitizeStartupCleanupResult(cleanup) : undefined;
|
|
194
|
+
return new ObservMeOtelStartupError(
|
|
195
|
+
`ObservMe OTEL startup failed: ${detail}. ${startupCleanupGuidance(safeCleanup)}`,
|
|
196
|
+
safeCleanup,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class ObservMeOtelStartupError extends Error {
|
|
201
|
+
readonly cleanup?: BoundedOtelOperationResult;
|
|
202
|
+
|
|
203
|
+
constructor(message: string, cleanup?: BoundedOtelOperationResult) {
|
|
204
|
+
super(message);
|
|
205
|
+
this.name = "ObservMeOtelStartupError";
|
|
206
|
+
this.cleanup = cleanup;
|
|
85
207
|
}
|
|
86
208
|
}
|
|
87
209
|
|
|
@@ -99,6 +221,30 @@ export function createNoopSessionScopedOtelSdk(): SessionScopedOtelSdk {
|
|
|
99
221
|
return new NoopSessionScopedOtelSdk();
|
|
100
222
|
}
|
|
101
223
|
|
|
224
|
+
function startupCleanupGuidance(cleanup: BoundedOtelOperationResult | undefined): string {
|
|
225
|
+
if (!cleanup) return "Check OTLP settings and Collector availability before retrying.";
|
|
226
|
+
if (cleanup.timedOut) return "Cleanup exceeded its timeout; restart Pi before retrying.";
|
|
227
|
+
if (cleanup.error) return "Cleanup also failed; restart Pi before retrying.";
|
|
228
|
+
return "Started providers were cleaned up; check OTLP settings and Collector availability before retrying.";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function completedShutdown(): BoundedOtelOperationResult {
|
|
232
|
+
return { operation: "shutdown", completed: true, timedOut: false };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pendingShutdown(settlement: Promise<OtelOperationSettlement>): BoundedOtelOperationResult {
|
|
236
|
+
return { operation: "shutdown", completed: false, timedOut: true, settlement };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function sanitizeStartupCleanupResult(result: BoundedOtelOperationResult): BoundedOtelOperationResult {
|
|
240
|
+
if (!result.error) return result;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
...result,
|
|
244
|
+
error: new Error(sanitizeDiagnosticText(readDiagnosticMessage(result.error))),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
102
248
|
class NoopSessionScopedOtelSdk implements SessionScopedOtelSdk {
|
|
103
249
|
async start(): Promise<void> {
|
|
104
250
|
await Promise.resolve();
|
package/src/otel/shutdown.ts
CHANGED
|
@@ -9,11 +9,19 @@ export interface ShutdownableOtelSdk extends FlushableOtelSdk {
|
|
|
9
9
|
shutdown?: () => Promise<void> | void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface OtelOperationSettlement {
|
|
13
|
+
readonly operation: "flush" | "shutdown";
|
|
14
|
+
readonly completed: boolean;
|
|
15
|
+
readonly timedOut: false;
|
|
16
|
+
readonly error?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export interface BoundedOtelOperationResult {
|
|
13
20
|
readonly operation: "flush" | "shutdown";
|
|
14
21
|
readonly completed: boolean;
|
|
15
22
|
readonly timedOut: boolean;
|
|
16
23
|
readonly error?: unknown;
|
|
24
|
+
readonly settlement?: Promise<OtelOperationSettlement>;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface OtelShutdownLogSink {
|
|
@@ -54,10 +62,10 @@ export async function runBoundedOtelOperation(
|
|
|
54
62
|
const timeoutResult = timeoutOperation(operation, normalizedTimeoutMs, timeoutController.signal);
|
|
55
63
|
|
|
56
64
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return {
|
|
65
|
+
const settlement = settleOperation(operation, action);
|
|
66
|
+
const result = await Promise.race([settlement, timeoutResult]);
|
|
67
|
+
if (!result.timedOut) return result;
|
|
68
|
+
return { ...result, settlement };
|
|
61
69
|
} finally {
|
|
62
70
|
timeoutController.abort();
|
|
63
71
|
}
|
|
@@ -68,12 +76,16 @@ function normalizeTimeoutMs(timeoutMs: number): number {
|
|
|
68
76
|
return timeoutMs;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
async function
|
|
79
|
+
async function settleOperation(
|
|
72
80
|
operation: "flush" | "shutdown",
|
|
73
81
|
action: () => Promise<void> | void,
|
|
74
|
-
): Promise<
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
): Promise<OtelOperationSettlement> {
|
|
83
|
+
try {
|
|
84
|
+
await action();
|
|
85
|
+
return completedOperation(operation);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { operation, completed: false, timedOut: false, error };
|
|
88
|
+
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
async function timeoutOperation(
|
|
@@ -84,17 +96,33 @@ async function timeoutOperation(
|
|
|
84
96
|
return delay(timeoutMs, { operation, completed: false, timedOut: true } as const, { signal });
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
function completedOperation(operation: "flush" | "shutdown"):
|
|
99
|
+
function completedOperation(operation: "flush" | "shutdown"): OtelOperationSettlement {
|
|
88
100
|
return { operation, completed: true, timedOut: false };
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
function logBoundedOperationIssue(result: BoundedOtelOperationResult, logger: OtelShutdownLogSink | undefined): void {
|
|
92
104
|
if (result.timedOut) {
|
|
93
|
-
logger
|
|
105
|
+
warnOtelOperation(logger, `ObservMe OTEL ${result.operation} exceeded timeout and remains pending.`);
|
|
106
|
+
void result.settlement?.then(logLateOperationIssue.bind(undefined, logger));
|
|
94
107
|
return;
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
logLateOperationIssue(logger, result);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function logLateOperationIssue(
|
|
114
|
+
logger: OtelShutdownLogSink | undefined,
|
|
115
|
+
result: Pick<BoundedOtelOperationResult, "operation" | "error">,
|
|
116
|
+
): void {
|
|
117
|
+
if (result.error) warnOtelOperation(logger, `ObservMe OTEL ${result.operation} failed: ${formatError(result.error)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function warnOtelOperation(logger: OtelShutdownLogSink | undefined, message: string): void {
|
|
121
|
+
try {
|
|
122
|
+
logger?.warn?.(message);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
function formatError(error: unknown): string {
|
package/src/otel/traces.ts
CHANGED
|
@@ -3,15 +3,24 @@ import type { Resource } from "@opentelemetry/resources";
|
|
|
3
3
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
4
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
5
5
|
import type { Sampler, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AlwaysOffSampler,
|
|
8
|
+
BasicTracerProvider,
|
|
9
|
+
BatchSpanProcessor,
|
|
10
|
+
ParentBasedSampler,
|
|
11
|
+
TraceIdRatioBasedSampler,
|
|
12
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
7
13
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
8
14
|
import type { ObservMeConfig, TraceBatchConfig } from "../config/schema.ts";
|
|
9
15
|
import { appendOtlpSignalPath } from "./otlp-endpoint.ts";
|
|
16
|
+
import { buildOtlpHttpAgentOptions, type OtlpHttpAgentOptions } from "./otlp-http-options.ts";
|
|
10
17
|
import type { StartOtelSdkFactoryOptions } from "./sdk.ts";
|
|
11
18
|
|
|
12
19
|
export const OBSERVME_TRACER_NAME = "@senad-d/observme";
|
|
13
20
|
export const OTLP_TRACE_SIGNAL_PATH = "/v1/traces";
|
|
14
21
|
|
|
22
|
+
const noopTracer = new BasicTracerProvider({ sampler: new AlwaysOffSampler() }).getTracer(OBSERVME_TRACER_NAME);
|
|
23
|
+
|
|
15
24
|
export const DOCUMENTED_TRACE_BATCH_DEFAULTS = {
|
|
16
25
|
maxQueueSize: 2048,
|
|
17
26
|
maxExportBatchSize: 512,
|
|
@@ -25,6 +34,7 @@ export interface OtlpTraceExporterOptions {
|
|
|
25
34
|
readonly url: string;
|
|
26
35
|
readonly headers: Record<string, string>;
|
|
27
36
|
readonly timeoutMillis: number;
|
|
37
|
+
readonly httpAgentOptions: OtlpHttpAgentOptions;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export interface TraceProviderOptions {
|
|
@@ -35,7 +45,6 @@ export interface TraceProviderOptions {
|
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
export interface TraceProviderLike {
|
|
38
|
-
register?: () => void;
|
|
39
48
|
getTracer: (name: string) => Tracer;
|
|
40
49
|
forceFlush?: () => Promise<void> | void;
|
|
41
50
|
shutdown?: () => Promise<void> | void;
|
|
@@ -48,7 +57,6 @@ export type TraceProviderFactory = (options: TraceProviderOptions) => TraceProvi
|
|
|
48
57
|
export interface ObservMeTraceSdkOptions {
|
|
49
58
|
readonly config: ObservMeConfig;
|
|
50
59
|
readonly tracerName?: string;
|
|
51
|
-
readonly registerGlobal?: boolean;
|
|
52
60
|
readonly exporterFactory?: TraceExporterFactory;
|
|
53
61
|
readonly spanProcessorFactory?: TraceSpanProcessorFactory;
|
|
54
62
|
readonly tracerProviderFactory?: TraceProviderFactory;
|
|
@@ -63,18 +71,18 @@ export interface TraceExporterWiring {
|
|
|
63
71
|
export class ObservMeTraceSdk {
|
|
64
72
|
readonly #config: ObservMeConfig;
|
|
65
73
|
readonly #tracerName: string;
|
|
66
|
-
readonly #registerGlobal: boolean;
|
|
67
74
|
readonly #exporterFactory: TraceExporterFactory;
|
|
68
75
|
readonly #spanProcessorFactory: TraceSpanProcessorFactory;
|
|
69
76
|
readonly #tracerProviderFactory: TraceProviderFactory;
|
|
77
|
+
#exporter?: SpanExporter;
|
|
78
|
+
#processor?: SpanProcessor;
|
|
70
79
|
#provider?: TraceProviderLike;
|
|
71
|
-
#tracer
|
|
80
|
+
#tracer: Tracer = noopTracer;
|
|
72
81
|
#state: TracePipelineState = "idle";
|
|
73
82
|
|
|
74
83
|
constructor(options: ObservMeTraceSdkOptions) {
|
|
75
84
|
this.#config = options.config;
|
|
76
85
|
this.#tracerName = options.tracerName ?? OBSERVME_TRACER_NAME;
|
|
77
|
-
this.#registerGlobal = options.registerGlobal ?? true;
|
|
78
86
|
this.#exporterFactory = options.exporterFactory ?? createOtlpTraceExporter;
|
|
79
87
|
this.#spanProcessorFactory = options.spanProcessorFactory ?? createBatchSpanProcessor;
|
|
80
88
|
this.#tracerProviderFactory = options.tracerProviderFactory ?? createNodeTraceProvider;
|
|
@@ -84,28 +92,27 @@ export class ObservMeTraceSdk {
|
|
|
84
92
|
return this.#state;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
get tracer(): Tracer
|
|
95
|
+
get tracer(): Tracer {
|
|
88
96
|
return this.#tracer;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
start(): void {
|
|
92
100
|
if (this.#state === "started" || this.#state === "disabled") return;
|
|
93
|
-
if (!this.#config.traces.enabled) {
|
|
101
|
+
if (!this.#config.enabled || !this.#config.traces.enabled) {
|
|
94
102
|
this.#state = "disabled";
|
|
95
103
|
return;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
const wiring = buildTraceExporterWiring(this.#config);
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
this.#exporter = this.#exporterFactory(wiring.exporter);
|
|
108
|
+
this.#processor = this.#spanProcessorFactory(this.#exporter, wiring.batch);
|
|
101
109
|
this.#provider = this.#tracerProviderFactory({
|
|
102
110
|
resource: createTraceResource(this.#config),
|
|
103
111
|
sampler: createTraceSampler(this.#config),
|
|
104
|
-
spanProcessors: [processor],
|
|
112
|
+
spanProcessors: [this.#processor],
|
|
105
113
|
forceFlushTimeoutMillis: this.#config.shutdown.flushTimeoutMs,
|
|
106
114
|
});
|
|
107
115
|
|
|
108
|
-
if (this.#registerGlobal) this.#provider.register?.();
|
|
109
116
|
this.#tracer = this.#provider.getTracer(this.#tracerName);
|
|
110
117
|
this.#state = "started";
|
|
111
118
|
}
|
|
@@ -115,9 +122,19 @@ export class ObservMeTraceSdk {
|
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
async shutdown(): Promise<void> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
if (this.#state === "shutdown") return;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (this.#provider?.shutdown) await this.#provider.shutdown();
|
|
129
|
+
else if (this.#processor) await this.#processor.shutdown();
|
|
130
|
+
else await this.#exporter?.shutdown();
|
|
131
|
+
} finally {
|
|
132
|
+
this.#provider = undefined;
|
|
133
|
+
this.#processor = undefined;
|
|
134
|
+
this.#exporter = undefined;
|
|
135
|
+
this.#state = "shutdown";
|
|
136
|
+
this.#tracer = noopTracer;
|
|
137
|
+
}
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
|
|
@@ -127,7 +144,7 @@ export function createTraceSessionScopedOtelSdk(options: StartOtelSdkFactoryOpti
|
|
|
127
144
|
|
|
128
145
|
export function buildTraceExporterWiring(config: ObservMeConfig): TraceExporterWiring {
|
|
129
146
|
return {
|
|
130
|
-
enabled: config.traces.enabled,
|
|
147
|
+
enabled: config.enabled && config.traces.enabled,
|
|
131
148
|
exporter: buildOtlpTraceExporterOptions(config),
|
|
132
149
|
batch: { ...config.traces.batch },
|
|
133
150
|
};
|
|
@@ -138,6 +155,7 @@ export function buildOtlpTraceExporterOptions(config: ObservMeConfig): OtlpTrace
|
|
|
138
155
|
url: resolveTraceEndpoint(config),
|
|
139
156
|
headers: { ...config.otlp.headers },
|
|
140
157
|
timeoutMillis: config.otlp.timeoutMs,
|
|
158
|
+
httpAgentOptions: buildOtlpHttpAgentOptions(config),
|
|
141
159
|
};
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -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
|
+
}
|
|
@@ -48,6 +48,7 @@ interface MutableAgentTreeNode {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const activeChildStatuses = new Set<AgentChildStatus>(["starting", "active"]);
|
|
51
|
+
const terminalChildStatuses = new Set<AgentChildStatus>(["completed", "failed", "cancelled", "orphaned"]);
|
|
51
52
|
const agentStatusOrder: AgentChildStatus[] = ["starting", "active", "completed", "failed", "cancelled", "orphaned"];
|
|
52
53
|
|
|
53
54
|
export class AgentTreeTracker {
|
|
@@ -68,6 +69,11 @@ export class AgentTreeTracker {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
registerAgent(lineage: AgentLineageContext, status: AgentChildStatus = "active"): AgentTreeNode {
|
|
72
|
+
const existingNode = this.#nodes.get(lineage.agentId);
|
|
73
|
+
if (existingNode && !isAgentStatusTransitionAllowed(existingNode.status, status)) {
|
|
74
|
+
return snapshotNode(existingNode, this.#nodes);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
const node = createMutableNode(lineage, status, this.isOrphan(lineage));
|
|
72
78
|
this.retainParentForInsertion(node.parentAgentId);
|
|
73
79
|
this.#nodes.set(lineage.agentId, node);
|
|
@@ -77,7 +83,7 @@ export class AgentTreeTracker {
|
|
|
77
83
|
|
|
78
84
|
updateStatus(agentId: string, status: AgentChildStatus): AgentTreeNode | undefined {
|
|
79
85
|
const node = this.#nodes.get(agentId);
|
|
80
|
-
if (!node) return undefined;
|
|
86
|
+
if (!node || !isAgentStatusTransitionAllowed(node.status, status)) return undefined;
|
|
81
87
|
|
|
82
88
|
node.status = status;
|
|
83
89
|
if (status === "orphaned") node.orphaned = true;
|
|
@@ -177,6 +183,13 @@ export class AgentTreeTracker {
|
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
export function isAgentStatusTransitionAllowed(current: AgentChildStatus, next: AgentChildStatus): boolean {
|
|
187
|
+
if (current === next) return true;
|
|
188
|
+
if (terminalChildStatuses.has(current)) return false;
|
|
189
|
+
if (current === "starting") return true;
|
|
190
|
+
return next !== "starting";
|
|
191
|
+
}
|
|
192
|
+
|
|
180
193
|
export function assertNoHighCardinalityMetricLabels(labels: Record<string, string>): void {
|
|
181
194
|
const forbiddenKey = Object.keys(labels).find(isHighCardinalityLineageKey);
|
|
182
195
|
if (forbiddenKey) throw new Error(`High-cardinality lineage value must not be used as a metric label: ${forbiddenKey}`);
|