@senad-d/observme 0.1.0
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 +57 -0
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/ObservMe-Production-Docs/00-README.md +79 -0
- package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
- package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
- package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
- package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
- package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
- package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
- package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
- package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
- package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
- package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
- package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
- package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
- package/ObservMe-Production-Docs/13-source-notes.md +33 -0
- package/ObservMe-Production-Docs/pi-session-format.md +427 -0
- package/README.md +356 -0
- package/SECURITY.md +45 -0
- package/dashboards/observme-agent-node-graphs.json +250 -0
- package/dashboards/observme-agents.json +1880 -0
- package/dashboards/observme-alerts.yaml +113 -0
- package/dashboards/observme-branches-compactions.json +1042 -0
- package/dashboards/observme-cost.json +1254 -0
- package/dashboards/observme-errors.json +1659 -0
- package/dashboards/observme-export-health.json +1802 -0
- package/dashboards/observme-latency.json +1494 -0
- package/dashboards/observme-llm-conversations.json +730 -0
- package/dashboards/observme-logs-llm.json +644 -0
- package/dashboards/observme-models.json +933 -0
- package/dashboards/observme-overview.json +2129 -0
- package/dashboards/observme-slo-health.json +737 -0
- package/dashboards/observme-slos.yaml +56 -0
- package/dashboards/observme-tools.json +902 -0
- package/dashboards/observme-trace-journey.json +1793 -0
- package/docs/STRUCTURE.md +49 -0
- package/docs/agent-subagent-observability-requirements.md +997 -0
- package/docs/compatibility-matrix.md +39 -0
- package/docs/configuration-tui-design-standard.md +767 -0
- package/docs/configuration.md +38 -0
- package/docs/review-validation.md +115 -0
- package/docs/validation-flow.md +117 -0
- package/examples/collector.yaml +123 -0
- package/examples/observme.yaml +131 -0
- package/img/demo.gif +0 -0
- package/img/icon.svg +47 -0
- package/package.json +103 -0
- package/src/commands/obs-agents-runtime.ts +150 -0
- package/src/commands/obs-agents.ts +491 -0
- package/src/commands/obs-args.ts +63 -0
- package/src/commands/obs-backfill.ts +1334 -0
- package/src/commands/obs-command-support.ts +43 -0
- package/src/commands/obs-cost.ts +228 -0
- package/src/commands/obs-diagnostics.ts +22 -0
- package/src/commands/obs-errors.ts +156 -0
- package/src/commands/obs-health.ts +301 -0
- package/src/commands/obs-link.ts +90 -0
- package/src/commands/obs-logs.ts +194 -0
- package/src/commands/obs-loki-summary.ts +184 -0
- package/src/commands/obs-session.ts +259 -0
- package/src/commands/obs-status.ts +359 -0
- package/src/commands/obs-tools.ts +274 -0
- package/src/commands/obs-trace.ts +411 -0
- package/src/commands/obs.ts +211 -0
- package/src/config/bootstrap-project-config.ts +300 -0
- package/src/config/defaults.ts +143 -0
- package/src/config/load-config.ts +631 -0
- package/src/config/project-paths.ts +61 -0
- package/src/config/schema.ts +405 -0
- package/src/config/validate.ts +456 -0
- package/src/constants.ts +4 -0
- package/src/diagnostics/sanitize.ts +6 -0
- package/src/extension.ts +38 -0
- package/src/otel/logs.ts +160 -0
- package/src/otel/metrics.ts +165 -0
- package/src/otel/otlp-endpoint.ts +10 -0
- package/src/otel/sdk.ts +114 -0
- package/src/otel/shutdown.ts +102 -0
- package/src/otel/traces.ts +166 -0
- package/src/pi/agent-lineage.ts +378 -0
- package/src/pi/agent-tree-tracker.ts +258 -0
- package/src/pi/event-handlers/agent-turn.ts +155 -0
- package/src/pi/event-handlers/lifecycle.ts +642 -0
- package/src/pi/event-handlers/llm.ts +115 -0
- package/src/pi/event-handlers/session-events.ts +159 -0
- package/src/pi/event-handlers/tool-bash.ts +275 -0
- package/src/pi/handler-internals.ts +2154 -0
- package/src/pi/handler-runtime.ts +633 -0
- package/src/pi/handler-types.ts +261 -0
- package/src/pi/handlers.ts +75 -0
- package/src/pi/subagent-spawn.ts +975 -0
- package/src/pi/subagent-types.ts +29 -0
- package/src/privacy/content-capture.ts +104 -0
- package/src/privacy/hash.ts +93 -0
- package/src/privacy/redact.ts +619 -0
- package/src/privacy/secret-patterns.ts +185 -0
- package/src/privacy/truncate.ts +69 -0
- package/src/query/grafana-readiness.ts +164 -0
- package/src/query/grafana-transport.ts +481 -0
- package/src/query/grafana.ts +371 -0
- package/src/query/loki.ts +332 -0
- package/src/query/prometheus.ts +388 -0
- package/src/query/tempo.ts +332 -0
- package/src/safety/sensitive-input.ts +208 -0
- package/src/semconv/attributes.ts +279 -0
- package/src/semconv/metrics.ts +146 -0
- package/src/semconv/spans.ts +19 -0
- package/src/semconv/values.ts +13 -0
- package/src/util/bounded-map.ts +97 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Meter } from "@opentelemetry/api";
|
|
2
|
+
import { createNoopMeter, metrics } from "@opentelemetry/api";
|
|
3
|
+
import type { Resource } from "@opentelemetry/resources";
|
|
4
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
5
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
6
|
+
import type { IMetricReader, PushMetricExporter } from "@opentelemetry/sdk-metrics";
|
|
7
|
+
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
8
|
+
import type { MetricsConfig, ObservMeConfig } from "../config/schema.ts";
|
|
9
|
+
import { appendOtlpSignalPath } from "./otlp-endpoint.ts";
|
|
10
|
+
import type { StartOtelSdkFactoryOptions } from "./sdk.ts";
|
|
11
|
+
|
|
12
|
+
export const OBSERVME_METER_NAME = "@senad-d/observme";
|
|
13
|
+
export const OTLP_METRIC_SIGNAL_PATH = "/v1/metrics";
|
|
14
|
+
|
|
15
|
+
export const DOCUMENTED_METRIC_EXPORT_DEFAULTS = {
|
|
16
|
+
exportIntervalMillis: 15000,
|
|
17
|
+
exportTimeoutMillis: 3000,
|
|
18
|
+
} satisfies Pick<MetricsConfig, "exportIntervalMillis" | "exportTimeoutMillis">;
|
|
19
|
+
|
|
20
|
+
export type MetricPipelineState = "idle" | "disabled" | "started" | "shutdown";
|
|
21
|
+
|
|
22
|
+
export interface OtlpMetricExporterOptions {
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly headers: Record<string, string>;
|
|
25
|
+
readonly timeoutMillis: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MetricReaderOptions {
|
|
29
|
+
readonly exportIntervalMillis: number;
|
|
30
|
+
readonly exportTimeoutMillis: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MetricProviderOptions {
|
|
34
|
+
readonly resource: Resource;
|
|
35
|
+
readonly readers: IMetricReader[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MetricProviderLike {
|
|
39
|
+
getMeter: (name: string) => Meter;
|
|
40
|
+
forceFlush?: () => Promise<void> | void;
|
|
41
|
+
shutdown?: () => Promise<void> | void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type MetricExporterFactory = (options: OtlpMetricExporterOptions) => PushMetricExporter;
|
|
45
|
+
export type MetricReaderFactory = (exporter: PushMetricExporter, options: MetricReaderOptions) => IMetricReader;
|
|
46
|
+
export type MetricProviderFactory = (options: MetricProviderOptions) => MetricProviderLike;
|
|
47
|
+
|
|
48
|
+
export interface ObservMeMetricSdkOptions {
|
|
49
|
+
readonly config: ObservMeConfig;
|
|
50
|
+
readonly meterName?: string;
|
|
51
|
+
readonly registerGlobal?: boolean;
|
|
52
|
+
readonly exporterFactory?: MetricExporterFactory;
|
|
53
|
+
readonly readerFactory?: MetricReaderFactory;
|
|
54
|
+
readonly meterProviderFactory?: MetricProviderFactory;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MetricExporterWiring {
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
readonly exporter: OtlpMetricExporterOptions;
|
|
60
|
+
readonly reader: MetricReaderOptions;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const noopMeter = createNoopMeter();
|
|
64
|
+
|
|
65
|
+
export class ObservMeMetricSdk {
|
|
66
|
+
readonly #config: ObservMeConfig;
|
|
67
|
+
readonly #meterName: string;
|
|
68
|
+
readonly #registerGlobal: boolean;
|
|
69
|
+
readonly #exporterFactory: MetricExporterFactory;
|
|
70
|
+
readonly #readerFactory: MetricReaderFactory;
|
|
71
|
+
readonly #meterProviderFactory: MetricProviderFactory;
|
|
72
|
+
#provider?: MetricProviderLike;
|
|
73
|
+
#meter: Meter = noopMeter;
|
|
74
|
+
#state: MetricPipelineState = "idle";
|
|
75
|
+
|
|
76
|
+
constructor(options: ObservMeMetricSdkOptions) {
|
|
77
|
+
this.#config = options.config;
|
|
78
|
+
this.#meterName = options.meterName ?? OBSERVME_METER_NAME;
|
|
79
|
+
this.#registerGlobal = options.registerGlobal ?? true;
|
|
80
|
+
this.#exporterFactory = options.exporterFactory ?? createOtlpMetricExporter;
|
|
81
|
+
this.#readerFactory = options.readerFactory ?? createPeriodicMetricReader;
|
|
82
|
+
this.#meterProviderFactory = options.meterProviderFactory ?? createMetricProvider;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get state(): MetricPipelineState {
|
|
86
|
+
return this.#state;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get meter(): Meter {
|
|
90
|
+
return this.#meter;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
start(): void {
|
|
94
|
+
if (this.#state === "started" || this.#state === "disabled") return;
|
|
95
|
+
if (!this.#config.metrics.enabled) {
|
|
96
|
+
this.#state = "disabled";
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const wiring = buildMetricExporterWiring(this.#config);
|
|
101
|
+
const exporter = this.#exporterFactory(wiring.exporter);
|
|
102
|
+
const reader = this.#readerFactory(exporter, wiring.reader);
|
|
103
|
+
this.#provider = this.#meterProviderFactory({
|
|
104
|
+
resource: createMetricResource(this.#config),
|
|
105
|
+
readers: [reader],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (this.#registerGlobal) metrics.setGlobalMeterProvider(this.#provider);
|
|
109
|
+
this.#meter = this.#provider.getMeter(this.#meterName);
|
|
110
|
+
this.#state = "started";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async forceFlush(): Promise<void> {
|
|
114
|
+
await this.#provider?.forceFlush?.();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async shutdown(): Promise<void> {
|
|
118
|
+
await this.#provider?.shutdown?.();
|
|
119
|
+
this.#state = "shutdown";
|
|
120
|
+
this.#meter = noopMeter;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createMetricSessionScopedOtelSdk(options: StartOtelSdkFactoryOptions): ObservMeMetricSdk {
|
|
125
|
+
return new ObservMeMetricSdk({ config: options.config });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildMetricExporterWiring(config: ObservMeConfig): MetricExporterWiring {
|
|
129
|
+
return {
|
|
130
|
+
enabled: config.metrics.enabled,
|
|
131
|
+
exporter: buildOtlpMetricExporterOptions(config),
|
|
132
|
+
reader: {
|
|
133
|
+
exportIntervalMillis: config.metrics.exportIntervalMillis,
|
|
134
|
+
exportTimeoutMillis: config.metrics.exportTimeoutMillis,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildOtlpMetricExporterOptions(config: ObservMeConfig): OtlpMetricExporterOptions {
|
|
140
|
+
return {
|
|
141
|
+
url: resolveMetricEndpoint(config),
|
|
142
|
+
headers: { ...config.otlp.headers },
|
|
143
|
+
timeoutMillis: config.otlp.timeoutMs,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveMetricEndpoint(config: ObservMeConfig): string {
|
|
148
|
+
return config.otlp.signalEndpoints?.metrics ?? appendOtlpSignalPath(config.otlp.endpoint, OTLP_METRIC_SIGNAL_PATH);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createOtlpMetricExporter(options: OtlpMetricExporterOptions): PushMetricExporter {
|
|
152
|
+
return new OTLPMetricExporter(options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createPeriodicMetricReader(exporter: PushMetricExporter, options: MetricReaderOptions): IMetricReader {
|
|
156
|
+
return new PeriodicExportingMetricReader({ exporter, ...options });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createMetricProvider(options: MetricProviderOptions): MetricProviderLike {
|
|
160
|
+
return new MeterProvider(options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createMetricResource(config: ObservMeConfig): Resource {
|
|
164
|
+
return resourceFromAttributes(config.resource.attributes);
|
|
165
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function appendOtlpSignalPath(baseEndpoint: string, signalPath: string): string {
|
|
2
|
+
const trimmedBaseEndpoint = removeTrailingSlashes(baseEndpoint);
|
|
3
|
+
return `${trimmedBaseEndpoint}${signalPath}`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function removeTrailingSlashes(value: string): string {
|
|
7
|
+
let end = value.length;
|
|
8
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
9
|
+
return value.slice(0, end);
|
|
10
|
+
}
|
package/src/otel/sdk.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
2
|
+
import type { AgentLineageContext } from "../pi/agent-lineage.ts";
|
|
3
|
+
import type { BoundedOtelOperationResult, OtelShutdownLogSink, ShutdownableOtelSdk } from "./shutdown.ts";
|
|
4
|
+
import { flushOtelSdk, shutdownOtelSdk } from "./shutdown.ts";
|
|
5
|
+
|
|
6
|
+
export type OtelSdkLifecycleState = "idle" | "starting" | "started" | "shutting_down" | "shutdown";
|
|
7
|
+
|
|
8
|
+
export interface StartOtelSdkFactoryOptions {
|
|
9
|
+
readonly config: ObservMeConfig;
|
|
10
|
+
readonly agent?: AgentLineageContext;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionScopedOtelSdk extends ShutdownableOtelSdk {
|
|
14
|
+
start?: () => Promise<void> | void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SessionScopedOtelSdkFactory = (options: StartOtelSdkFactoryOptions) => SessionScopedOtelSdk;
|
|
18
|
+
|
|
19
|
+
export interface ObservMeOtelSdkControllerOptions {
|
|
20
|
+
readonly config: ObservMeConfig;
|
|
21
|
+
readonly agent?: AgentLineageContext;
|
|
22
|
+
readonly sdkFactory?: SessionScopedOtelSdkFactory;
|
|
23
|
+
readonly logger?: OtelShutdownLogSink;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ObservMeOtelSdkControllerSnapshot {
|
|
27
|
+
readonly state: OtelSdkLifecycleState;
|
|
28
|
+
readonly started: boolean;
|
|
29
|
+
readonly shutdown: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ObservMeOtelSdkController {
|
|
33
|
+
readonly #config: ObservMeConfig;
|
|
34
|
+
readonly #agent?: AgentLineageContext;
|
|
35
|
+
readonly #sdkFactory: SessionScopedOtelSdkFactory;
|
|
36
|
+
readonly #logger?: OtelShutdownLogSink;
|
|
37
|
+
#sdk?: SessionScopedOtelSdk;
|
|
38
|
+
#state: OtelSdkLifecycleState = "idle";
|
|
39
|
+
|
|
40
|
+
constructor(options: ObservMeOtelSdkControllerOptions) {
|
|
41
|
+
this.#config = options.config;
|
|
42
|
+
this.#agent = options.agent;
|
|
43
|
+
this.#sdkFactory = options.sdkFactory ?? createNoopSessionScopedOtelSdk;
|
|
44
|
+
this.#logger = options.logger;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get state(): OtelSdkLifecycleState {
|
|
48
|
+
return this.#state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get sdk(): SessionScopedOtelSdk | undefined {
|
|
52
|
+
return this.#sdk;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
snapshot(): ObservMeOtelSdkControllerSnapshot {
|
|
56
|
+
return {
|
|
57
|
+
state: this.#state,
|
|
58
|
+
started: this.#state === "started",
|
|
59
|
+
shutdown: this.#state === "shutdown",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async start(): Promise<SessionScopedOtelSdk> {
|
|
64
|
+
if (this.#state === "started" && this.#sdk) return this.#sdk;
|
|
65
|
+
if (this.#state === "shutdown") throw new Error("ObservMe OTEL SDK controller cannot be restarted after shutdown.");
|
|
66
|
+
|
|
67
|
+
this.#state = "starting";
|
|
68
|
+
this.#sdk = this.#sdkFactory({ config: this.#config, agent: this.#agent });
|
|
69
|
+
await this.#sdk.start?.();
|
|
70
|
+
this.#state = "started";
|
|
71
|
+
return this.#sdk;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async flush(timeoutMs: number = this.#config.shutdown.flushTimeoutMs): Promise<BoundedOtelOperationResult> {
|
|
75
|
+
return flushOtelSdk(this.#sdk, timeoutMs, this.#logger);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async shutdown(timeoutMs: number = this.#config.shutdown.flushTimeoutMs): Promise<BoundedOtelOperationResult> {
|
|
79
|
+
if (this.#state === "shutdown") return { operation: "shutdown", completed: true, timedOut: false };
|
|
80
|
+
|
|
81
|
+
this.#state = "shutting_down";
|
|
82
|
+
const result = await shutdownOtelSdk(this.#sdk, timeoutMs, this.#logger);
|
|
83
|
+
this.#state = "shutdown";
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createOtelSdkController(options: ObservMeOtelSdkControllerOptions): ObservMeOtelSdkController {
|
|
89
|
+
return new ObservMeOtelSdkController(options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function startOtelSdk(options: ObservMeOtelSdkControllerOptions): Promise<ObservMeOtelSdkController> {
|
|
93
|
+
const controller = createOtelSdkController(options);
|
|
94
|
+
await controller.start();
|
|
95
|
+
return controller;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createNoopSessionScopedOtelSdk(): SessionScopedOtelSdk {
|
|
99
|
+
return new NoopSessionScopedOtelSdk();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class NoopSessionScopedOtelSdk implements SessionScopedOtelSdk {
|
|
103
|
+
async start(): Promise<void> {
|
|
104
|
+
await Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async forceFlush(): Promise<void> {
|
|
108
|
+
await Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async shutdown(): Promise<void> {
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
|
|
3
|
+
|
|
4
|
+
export interface FlushableOtelSdk {
|
|
5
|
+
forceFlush?: () => Promise<void> | void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ShutdownableOtelSdk extends FlushableOtelSdk {
|
|
9
|
+
shutdown?: () => Promise<void> | void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BoundedOtelOperationResult {
|
|
13
|
+
readonly operation: "flush" | "shutdown";
|
|
14
|
+
readonly completed: boolean;
|
|
15
|
+
readonly timedOut: boolean;
|
|
16
|
+
readonly error?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OtelShutdownLogSink {
|
|
20
|
+
warn?: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function flushOtelSdk(
|
|
24
|
+
sdk: FlushableOtelSdk | undefined,
|
|
25
|
+
timeoutMs: number,
|
|
26
|
+
logger?: OtelShutdownLogSink,
|
|
27
|
+
): Promise<BoundedOtelOperationResult> {
|
|
28
|
+
if (!sdk?.forceFlush) return completedOperation("flush");
|
|
29
|
+
|
|
30
|
+
const result = await runBoundedOtelOperation("flush", sdk.forceFlush.bind(sdk), timeoutMs);
|
|
31
|
+
logBoundedOperationIssue(result, logger);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function shutdownOtelSdk(
|
|
36
|
+
sdk: ShutdownableOtelSdk | undefined,
|
|
37
|
+
timeoutMs: number,
|
|
38
|
+
logger?: OtelShutdownLogSink,
|
|
39
|
+
): Promise<BoundedOtelOperationResult> {
|
|
40
|
+
if (!sdk?.shutdown) return completedOperation("shutdown");
|
|
41
|
+
|
|
42
|
+
const result = await runBoundedOtelOperation("shutdown", sdk.shutdown.bind(sdk), timeoutMs);
|
|
43
|
+
logBoundedOperationIssue(result, logger);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runBoundedOtelOperation(
|
|
48
|
+
operation: "flush" | "shutdown",
|
|
49
|
+
action: () => Promise<void> | void,
|
|
50
|
+
timeoutMs: number,
|
|
51
|
+
): Promise<BoundedOtelOperationResult> {
|
|
52
|
+
const normalizedTimeoutMs = normalizeTimeoutMs(timeoutMs);
|
|
53
|
+
const timeoutController = new AbortController();
|
|
54
|
+
const timeoutResult = timeoutOperation(operation, normalizedTimeoutMs, timeoutController.signal);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const completed = completeOperation(operation, action);
|
|
58
|
+
return await Promise.race([completed, timeoutResult]);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { operation, completed: false, timedOut: false, error };
|
|
61
|
+
} finally {
|
|
62
|
+
timeoutController.abort();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeTimeoutMs(timeoutMs: number): number {
|
|
67
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) return 0;
|
|
68
|
+
return timeoutMs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function completeOperation(
|
|
72
|
+
operation: "flush" | "shutdown",
|
|
73
|
+
action: () => Promise<void> | void,
|
|
74
|
+
): Promise<BoundedOtelOperationResult> {
|
|
75
|
+
await action();
|
|
76
|
+
return completedOperation(operation);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function timeoutOperation(
|
|
80
|
+
operation: "flush" | "shutdown",
|
|
81
|
+
timeoutMs: number,
|
|
82
|
+
signal: AbortSignal,
|
|
83
|
+
): Promise<BoundedOtelOperationResult> {
|
|
84
|
+
return delay(timeoutMs, { operation, completed: false, timedOut: true } as const, { signal });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function completedOperation(operation: "flush" | "shutdown"): BoundedOtelOperationResult {
|
|
88
|
+
return { operation, completed: true, timedOut: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function logBoundedOperationIssue(result: BoundedOtelOperationResult, logger: OtelShutdownLogSink | undefined): void {
|
|
92
|
+
if (result.timedOut) {
|
|
93
|
+
logger?.warn?.(`ObservMe OTEL ${result.operation} exceeded timeout and was abandoned.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.error) logger?.warn?.(`ObservMe OTEL ${result.operation} failed: ${formatError(result.error)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatError(error: unknown): string {
|
|
101
|
+
return sanitizeDiagnosticText(readDiagnosticMessage(error));
|
|
102
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { Tracer } from "@opentelemetry/api";
|
|
2
|
+
import type { Resource } from "@opentelemetry/resources";
|
|
3
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
5
|
+
import type { Sampler, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
import { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
8
|
+
import type { ObservMeConfig, TraceBatchConfig } from "../config/schema.ts";
|
|
9
|
+
import { appendOtlpSignalPath } from "./otlp-endpoint.ts";
|
|
10
|
+
import type { StartOtelSdkFactoryOptions } from "./sdk.ts";
|
|
11
|
+
|
|
12
|
+
export const OBSERVME_TRACER_NAME = "@senad-d/observme";
|
|
13
|
+
export const OTLP_TRACE_SIGNAL_PATH = "/v1/traces";
|
|
14
|
+
|
|
15
|
+
export const DOCUMENTED_TRACE_BATCH_DEFAULTS = {
|
|
16
|
+
maxQueueSize: 2048,
|
|
17
|
+
maxExportBatchSize: 512,
|
|
18
|
+
scheduledDelayMillis: 1000,
|
|
19
|
+
exportTimeoutMillis: 3000,
|
|
20
|
+
} satisfies TraceBatchConfig;
|
|
21
|
+
|
|
22
|
+
export type TracePipelineState = "idle" | "disabled" | "started" | "shutdown";
|
|
23
|
+
|
|
24
|
+
export interface OtlpTraceExporterOptions {
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly headers: Record<string, string>;
|
|
27
|
+
readonly timeoutMillis: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TraceProviderOptions {
|
|
31
|
+
readonly resource: Resource;
|
|
32
|
+
readonly sampler: Sampler;
|
|
33
|
+
readonly spanProcessors: SpanProcessor[];
|
|
34
|
+
readonly forceFlushTimeoutMillis: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TraceProviderLike {
|
|
38
|
+
register?: () => void;
|
|
39
|
+
getTracer: (name: string) => Tracer;
|
|
40
|
+
forceFlush?: () => Promise<void> | void;
|
|
41
|
+
shutdown?: () => Promise<void> | void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TraceExporterFactory = (options: OtlpTraceExporterOptions) => SpanExporter;
|
|
45
|
+
export type TraceSpanProcessorFactory = (exporter: SpanExporter, options: TraceBatchConfig) => SpanProcessor;
|
|
46
|
+
export type TraceProviderFactory = (options: TraceProviderOptions) => TraceProviderLike;
|
|
47
|
+
|
|
48
|
+
export interface ObservMeTraceSdkOptions {
|
|
49
|
+
readonly config: ObservMeConfig;
|
|
50
|
+
readonly tracerName?: string;
|
|
51
|
+
readonly registerGlobal?: boolean;
|
|
52
|
+
readonly exporterFactory?: TraceExporterFactory;
|
|
53
|
+
readonly spanProcessorFactory?: TraceSpanProcessorFactory;
|
|
54
|
+
readonly tracerProviderFactory?: TraceProviderFactory;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TraceExporterWiring {
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
readonly exporter: OtlpTraceExporterOptions;
|
|
60
|
+
readonly batch: TraceBatchConfig;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ObservMeTraceSdk {
|
|
64
|
+
readonly #config: ObservMeConfig;
|
|
65
|
+
readonly #tracerName: string;
|
|
66
|
+
readonly #registerGlobal: boolean;
|
|
67
|
+
readonly #exporterFactory: TraceExporterFactory;
|
|
68
|
+
readonly #spanProcessorFactory: TraceSpanProcessorFactory;
|
|
69
|
+
readonly #tracerProviderFactory: TraceProviderFactory;
|
|
70
|
+
#provider?: TraceProviderLike;
|
|
71
|
+
#tracer?: Tracer;
|
|
72
|
+
#state: TracePipelineState = "idle";
|
|
73
|
+
|
|
74
|
+
constructor(options: ObservMeTraceSdkOptions) {
|
|
75
|
+
this.#config = options.config;
|
|
76
|
+
this.#tracerName = options.tracerName ?? OBSERVME_TRACER_NAME;
|
|
77
|
+
this.#registerGlobal = options.registerGlobal ?? true;
|
|
78
|
+
this.#exporterFactory = options.exporterFactory ?? createOtlpTraceExporter;
|
|
79
|
+
this.#spanProcessorFactory = options.spanProcessorFactory ?? createBatchSpanProcessor;
|
|
80
|
+
this.#tracerProviderFactory = options.tracerProviderFactory ?? createNodeTraceProvider;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get state(): TracePipelineState {
|
|
84
|
+
return this.#state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get tracer(): Tracer | undefined {
|
|
88
|
+
return this.#tracer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start(): void {
|
|
92
|
+
if (this.#state === "started" || this.#state === "disabled") return;
|
|
93
|
+
if (!this.#config.traces.enabled) {
|
|
94
|
+
this.#state = "disabled";
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const wiring = buildTraceExporterWiring(this.#config);
|
|
99
|
+
const exporter = this.#exporterFactory(wiring.exporter);
|
|
100
|
+
const processor = this.#spanProcessorFactory(exporter, wiring.batch);
|
|
101
|
+
this.#provider = this.#tracerProviderFactory({
|
|
102
|
+
resource: createTraceResource(this.#config),
|
|
103
|
+
sampler: createTraceSampler(this.#config),
|
|
104
|
+
spanProcessors: [processor],
|
|
105
|
+
forceFlushTimeoutMillis: this.#config.shutdown.flushTimeoutMs,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (this.#registerGlobal) this.#provider.register?.();
|
|
109
|
+
this.#tracer = this.#provider.getTracer(this.#tracerName);
|
|
110
|
+
this.#state = "started";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async forceFlush(): Promise<void> {
|
|
114
|
+
await this.#provider?.forceFlush?.();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async shutdown(): Promise<void> {
|
|
118
|
+
await this.#provider?.shutdown?.();
|
|
119
|
+
this.#state = "shutdown";
|
|
120
|
+
this.#tracer = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createTraceSessionScopedOtelSdk(options: StartOtelSdkFactoryOptions): ObservMeTraceSdk {
|
|
125
|
+
return new ObservMeTraceSdk({ config: options.config });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildTraceExporterWiring(config: ObservMeConfig): TraceExporterWiring {
|
|
129
|
+
return {
|
|
130
|
+
enabled: config.traces.enabled,
|
|
131
|
+
exporter: buildOtlpTraceExporterOptions(config),
|
|
132
|
+
batch: { ...config.traces.batch },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildOtlpTraceExporterOptions(config: ObservMeConfig): OtlpTraceExporterOptions {
|
|
137
|
+
return {
|
|
138
|
+
url: resolveTraceEndpoint(config),
|
|
139
|
+
headers: { ...config.otlp.headers },
|
|
140
|
+
timeoutMillis: config.otlp.timeoutMs,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveTraceEndpoint(config: ObservMeConfig): string {
|
|
145
|
+
return config.otlp.signalEndpoints?.traces ?? appendOtlpSignalPath(config.otlp.endpoint, OTLP_TRACE_SIGNAL_PATH);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createOtlpTraceExporter(options: OtlpTraceExporterOptions): SpanExporter {
|
|
149
|
+
return new OTLPTraceExporter(options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createBatchSpanProcessor(exporter: SpanExporter, options: TraceBatchConfig): SpanProcessor {
|
|
153
|
+
return new BatchSpanProcessor(exporter, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createNodeTraceProvider(options: TraceProviderOptions): TraceProviderLike {
|
|
157
|
+
return new NodeTracerProvider(options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createTraceResource(config: ObservMeConfig): Resource {
|
|
161
|
+
return resourceFromAttributes(config.resource.attributes);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createTraceSampler(config: ObservMeConfig): Sampler {
|
|
165
|
+
return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(config.traces.sampleRatio) });
|
|
166
|
+
}
|