@hydraharness/harness-session-telemetry-otel 0.1.1-rc.6
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/LICENSE +21 -0
- package/README.md +54 -0
- package/lib/index.js +210 -0
- package/lib/invariant.js +24 -0
- package/lib/types/index.d.ts +96 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @hydraharness/harness-session-telemetry-otel
|
|
2
|
+
|
|
3
|
+
The OpenTelemetry backend for [the telemetry seam](../session-telemetry/) — the only entry a deployment loads. Its `mode` decides whether the seam follows session events live, replays the canonical log only at recorded feedback, or keeps telemetry local. Uploading modes compose the OTel JS SDK as-is (`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP log exporter) and map each handed-over record onto `logger.emit()`, under two instrumentation scopes: ledger records on `@hydraharness/harness-session-sessionTelemetry-otel`, operational records on `@hydraharness/harness-session-sessionTelemetry-otel/ops`. Resource identity contains `service.name`/`service.version` from `@hydraharness/harness-llm`'s `APP_IDENTITY` plus this package's anonymous `user.id` (`$HYDRA_HOME/.anonymous-user-id`, a random UUID created on first use and reset by deleting the file), carried once per export batch rather than per record.
|
|
4
|
+
|
|
5
|
+
## Config
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
- id: sessionTelemetry-otel
|
|
9
|
+
name: '@hydraharness/harness-session-sessionTelemetry-otel'
|
|
10
|
+
config:
|
|
11
|
+
mode: FULL # explicit opt-in; default: DISABLED
|
|
12
|
+
shutdownTimeoutMillis: 3000 # optional; defaults to 3000
|
|
13
|
+
exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter
|
|
14
|
+
url: https://collector.example.com/v1/logs
|
|
15
|
+
headers:
|
|
16
|
+
authorization: !!js `Bearer ${process.env.OTLP_TOKEN}`
|
|
17
|
+
processor: {} # optional; passed verbatim to BatchLogRecordProcessor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| `mode` | Behavior |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `FULL` | Each projected record, including lifecycle ops records, is handed to the OTel SDK immediately. |
|
|
23
|
+
| `FEEDBACK_ONLY` | Each `feedback/record` replays, projects, and redacts the canonical session-log suffix through that event. Later records wait for another feedback event and remain local if none arrives. |
|
|
24
|
+
| `DISABLED` | Default. No coordinator, provider, processor, or exporter is constructed. No telemetry record leaves the process. A `feedback/record` logs `session sessionTelemetry is DISABLED; nothing will be shared and this feedback remains local`; the event remains in the local session log. |
|
|
25
|
+
|
|
26
|
+
Programmatic TypeScript configuration uses the exported `SessionTelemetryMode` enum (`SessionTelemetryMode.FULL`, `SessionTelemetryMode.FEEDBACK_ONLY`, or `SessionTelemetryMode.DISABLED`); raw string literals are not assignable. Serialized Cordis configuration continues to use the string values shown above.
|
|
27
|
+
|
|
28
|
+
Upload authorization is positive and fail-closed. An unknown direct-construction mode fails before transport configuration is read. Only `FULL` accepts direct `ctx.sessionTelemetry.emit()` calls. `FEEDBACK_ONLY` gives its on-demand coordinator a private backend capability and treats only the exact `feedback/record` object already stored at `session.events[event.seq]` as consent; an independently emitted bus value is ignored. `DISABLED` never constructs the SDK pipeline, even when exporter options are present.
|
|
29
|
+
|
|
30
|
+
The mounted service discloses the resolved mode through the seam's [`SessionTelemetrySharingStatus`](../session-telemetry/README.md#the-sharing-disclosure) `sharing` property (`full` / `feedback-only` / `disabled`), so the `/feedback` acknowledgement can report whether and how the session is shared. The disclosure is set in the constructor and is independent of capture: even `DISABLED` discloses `disabled`.
|
|
31
|
+
|
|
32
|
+
`exporter.url` is required in `FULL` and `FEEDBACK_ONLY`, has no default, and must parse as `http(s)`; it is optional and unused in `DISABLED`. In uploading modes, `shutdownTimeoutMillis` is a positive finite Hydra-owned outer deadline that defaults to 3000 ms, and a non-positive-integer `processor.maxExportBatchSize` also fails at plugin load because the SDK accepts it but then hangs on shutdown. Both SDK blocks pass through whole: every `OTLPExporterNodeConfigBase` field (`headers`, `timeoutMillis`, `compression`, `keepAlive`, …) reaches the exporter, and batching, export cadence (`scheduledDelayMillis`), retry, queue bounds, and loss policy under sustained failure are SDK behavior tuned through `processor`. The backend implements no `flush()`: the batch processor owns ordinary flushing. During shutdown, OTel awaits `exporter.forceFlush()` before the processor's `exportTimeoutMillis`-bounded completion promise; if that transport promise never settles, this package abandons the wait at `shutdownTimeoutMillis`, logs the contained shutdown failure through the coordinator, and lets application teardown continue. The deadline cannot cancel the SDK transport, so records still pending then may be lost at process exit.
|
|
33
|
+
|
|
34
|
+
## What leaves the machine
|
|
35
|
+
|
|
36
|
+
In uploading modes, records carry the complete `event.data` as the seam's `sessionTelemetry/record` waterfall returns it — user and assistant message content, tool arguments and results (command output, file contents), the full system prompt and tool schemas (`request/header`), todo text, compaction summaries, hook `stderrSummary`, feedback text, and the session `cwd` (a local path). The seam ships no redaction rules: with no `sessionTelemetry/record` listener mounted, that is the raw captured copy, so a deployment exporting beyond a trusted boundary mounts its own rules (see [the seam README](../session-telemetry/README.md#the-redact-waterfall)). `FULL` runs redaction at append time; `FEEDBACK_ONLY` retains no telemetry copy and runs the currently mounted rules when feedback triggers canonical-log replay. Provider credentials never appear regardless: adapter API keys are constructor parameters, not session events, so they are structurally absent from the log and therefore from telemetry. `DISABLED` does not construct the SDK pipeline or hand any capture to a backend.
|
|
37
|
+
|
|
38
|
+
## Field mapping
|
|
39
|
+
|
|
40
|
+
Seam record → SDK log record: `time` → `timestamp`/`observedTimestamp`; `severity` → `severityNumber`/`severityText` (INFO 9 / WARN 13 / ERROR 17); `body` → the structured log body; `attributes` verbatim. Receivers dedupe on `(session.id, event.seq)` and alert on severity. In `FULL`, they may also detect crashes by `shutdown`-record absence: the marker is emitted at the session's own disposal or application teardown, and a marker followed by more events is a telemetry reload. In `FEEDBACK_ONLY`, a released prefix normally has no later `shutdown` marker, so its absence is not a crash signal. Streams are not self-contained across lineage: a resumed session continues its own id's stream from where the previous process left off, and a forked session's stream starts at its inherited boundary — its prefix lives in the parent's stream, stitched via `session.parent_id` + `session.seed_length`. A resumed local log may contain synthetic closers that were never exported; the wire stream stays faithful to records actually handed to the SDK.
|
|
41
|
+
|
|
42
|
+
## Model Experience
|
|
43
|
+
|
|
44
|
+
None, as the backend only forwards the seam's redacted records into the OTel SDK pipeline; it never contributes to a model request.
|
|
45
|
+
|
|
46
|
+
#### KV Cache effect
|
|
47
|
+
|
|
48
|
+
None; this package neither assembles nor sends a provider request.
|
|
49
|
+
|
|
50
|
+
## Known Limitations and Deferred Work
|
|
51
|
+
|
|
52
|
+
- **Upstream experimental tree** — `@opentelemetry/sdk-logs` is still published from the upstream experimental tree; SDK API churn lands here and only here — the seam contract does not move.
|
|
53
|
+
- **Live-collector behavior belongs to the SDK exporter** — authentication, TLS, throttling, and other real OTLP deployment behavior follow the upstream SDK rather than a package-owned compatibility layer.
|
|
54
|
+
- **Feedback-time snapshot** — `FEEDBACK_ONLY` retains no telemetry-owned copy before feedback. It reads and redacts the current canonical log when feedback is recorded; a crash before feedback uploads nothing, and policy changes before feedback affect what that replay exports.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import z from "@hydraharness/schemastery";
|
|
3
|
+
import { SessionTelemetryBackend, SessionTelemetryCoordinator } from "@hydraharness/harness-session-telemetry";
|
|
4
|
+
import { APP_IDENTITY } from "@hydraharness/harness-llm";
|
|
5
|
+
import { getOrCreateAnonymousUserId } from "@hydraharness/harness-anonymous-user-id";
|
|
6
|
+
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
7
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
8
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
9
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
|
+
//#region lib/types/index.js
|
|
11
|
+
/**
|
|
12
|
+
* OpenTelemetry Service Provider for the Hydra harness telemetry capability.
|
|
13
|
+
*
|
|
14
|
+
* Composes the OTel JS SDK as-is — a `LoggerProvider` with a
|
|
15
|
+
* `BatchLogRecordProcessor` and an OTLP/HTTP log exporter — and maps each
|
|
16
|
+
* record handed over by the capture coordinator onto `logger.emit()`. After that call,
|
|
17
|
+
* batching, retry, queueing, and loss policy use the SDK's documented behavior, configured
|
|
18
|
+
* verbatim through the `exporter`/`processor` passthroughs. This package owns
|
|
19
|
+
* capture mode and an outer shutdown deadline: the SDK's export timeout does
|
|
20
|
+
* not bound its preceding `forceFlush()` wait.
|
|
21
|
+
*
|
|
22
|
+
* @module @hydraharness/harness-session-telemetry-otel
|
|
23
|
+
*/
|
|
24
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
25
|
+
/** Session-sharing policy selected by {@link Config.mode}. */
|
|
26
|
+
var SessionTelemetryMode;
|
|
27
|
+
(function(SessionTelemetryMode) {
|
|
28
|
+
SessionTelemetryMode["FULL"] = "FULL";
|
|
29
|
+
SessionTelemetryMode["FEEDBACK_ONLY"] = "FEEDBACK_ONLY";
|
|
30
|
+
SessionTelemetryMode["DISABLED"] = "DISABLED";
|
|
31
|
+
})(SessionTelemetryMode || (SessionTelemetryMode = {}));
|
|
32
|
+
/** Default session-sharing policy for schema and direct construction. */
|
|
33
|
+
const DEFAULT_TELEMETRY_MODE = SessionTelemetryMode.DISABLED;
|
|
34
|
+
const DISABLED_FEEDBACK_WARNING = "session telemetry is DISABLED; nothing will be shared and this feedback remains local";
|
|
35
|
+
const NON_CANONICAL_FEEDBACK_WARNING = "session telemetry ignored a feedback event absent from the canonical session log";
|
|
36
|
+
const DROP_RECORD = () => {};
|
|
37
|
+
/** Resolve the default and reject unknown runtime values before transport setup. */
|
|
38
|
+
function resolveMode(mode) {
|
|
39
|
+
const resolved = mode ?? DEFAULT_TELEMETRY_MODE;
|
|
40
|
+
switch (resolved) {
|
|
41
|
+
case SessionTelemetryMode.FULL:
|
|
42
|
+
case SessionTelemetryMode.FEEDBACK_ONLY:
|
|
43
|
+
case SessionTelemetryMode.DISABLED: return resolved;
|
|
44
|
+
default: return assertNever(resolved);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Fail closed when direct construction bypasses the runtime config schema. */
|
|
48
|
+
function assertNever(value) {
|
|
49
|
+
throw new Error(`session-telemetry-otel: unsupported mode ${JSON.stringify(value)}`);
|
|
50
|
+
}
|
|
51
|
+
/** Map the serialized mode onto the seam's backend-independent sharing vocabulary. */
|
|
52
|
+
function sharingStatusFor(mode) {
|
|
53
|
+
switch (mode) {
|
|
54
|
+
case SessionTelemetryMode.FULL: return "full";
|
|
55
|
+
case SessionTelemetryMode.FEEDBACK_ONLY: return "feedback-only";
|
|
56
|
+
case SessionTelemetryMode.DISABLED: return "disabled";
|
|
57
|
+
/* v8 ignore next 2 -- resolveMode already rejected unknown values before this switch; the closed enum cannot reach the default. */
|
|
58
|
+
default: return assertNever(mode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Schemastery validator for {@link Config}; cordis runs it before the plugin
|
|
63
|
+
* starts. It checks only the top-level fields; value checks live in the constructor
|
|
64
|
+
* so their errors name the fields. Both SDK option objects pass through unchanged:
|
|
65
|
+
* the SDK defines and validates their fields. Re-declaring them here would
|
|
66
|
+
* silently drop every field this plugin did not repeat.
|
|
67
|
+
*/
|
|
68
|
+
const Config = z.object({
|
|
69
|
+
mode: z.union(Object.values(SessionTelemetryMode)).default(DEFAULT_TELEMETRY_MODE),
|
|
70
|
+
exporter: z.any(),
|
|
71
|
+
processor: z.any(),
|
|
72
|
+
shutdownTimeoutMillis: z.number()
|
|
73
|
+
});
|
|
74
|
+
/** Default outer allowance for the SDK's complete shutdown sequence. */
|
|
75
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3e3;
|
|
76
|
+
const MAX_TIMER_DELAY_MILLIS = 2147483647;
|
|
77
|
+
/** Severity mapping from the Service Definition's three-level vocabulary to OTel severity numbers. */
|
|
78
|
+
const SEVERITY = {
|
|
79
|
+
info: {
|
|
80
|
+
severityNumber: SeverityNumber.INFO,
|
|
81
|
+
severityText: "INFO"
|
|
82
|
+
},
|
|
83
|
+
warn: {
|
|
84
|
+
severityNumber: SeverityNumber.WARN,
|
|
85
|
+
severityText: "WARN"
|
|
86
|
+
},
|
|
87
|
+
error: {
|
|
88
|
+
severityNumber: SeverityNumber.ERROR,
|
|
89
|
+
severityText: "ERROR"
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The backend plugin — the only entry a deployment loads. It always registers
|
|
94
|
+
* the `telemetry` service (duplicate load throws). Uploading modes wire the SDK
|
|
95
|
+
* pipeline and compose {@link SessionTelemetryCoordinator}; `DISABLED` constructs no
|
|
96
|
+
* SDK state and listens only to warn when recorded feedback stays local.
|
|
97
|
+
*/
|
|
98
|
+
var OpenTelemetrySessionBackend = class extends SessionTelemetryBackend {
|
|
99
|
+
static inject = ["sessions"];
|
|
100
|
+
static Config = Config;
|
|
101
|
+
directEmit;
|
|
102
|
+
provider;
|
|
103
|
+
shutdownTimeoutMillis;
|
|
104
|
+
sharing;
|
|
105
|
+
constructor(ctx, config) {
|
|
106
|
+
const mode = resolveMode(config.mode);
|
|
107
|
+
super(ctx);
|
|
108
|
+
this.sharing = sharingStatusFor(mode);
|
|
109
|
+
if (mode === SessionTelemetryMode.DISABLED) {
|
|
110
|
+
this.directEmit = DROP_RECORD;
|
|
111
|
+
this.provider = void 0;
|
|
112
|
+
this.shutdownTimeoutMillis = DEFAULT_SHUTDOWN_TIMEOUT_MILLIS;
|
|
113
|
+
ctx.on("session/event", (_session, event) => {
|
|
114
|
+
if (event.type === "feedback/record") ctx.logger.warn(DISABLED_FEEDBACK_WARNING);
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const url = config.exporter?.url;
|
|
119
|
+
if (url === void 0 || url.length === 0) throw new Error("session-telemetry-otel: exporter.url is required (the full OTLP logs endpoint)");
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = new URL(url);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(`session-telemetry-otel: exporter.url is not a valid URL: ${JSON.stringify(url)}`);
|
|
125
|
+
}
|
|
126
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`session-telemetry-otel: exporter.url must be http(s), got ${parsed.protocol}`);
|
|
127
|
+
const batchSize = config.processor?.maxExportBatchSize;
|
|
128
|
+
if (batchSize !== void 0 && (!Number.isInteger(batchSize) || batchSize < 1)) throw new Error(`session-telemetry-otel: processor.maxExportBatchSize must be a positive integer, got ${String(batchSize)}`);
|
|
129
|
+
const shutdownTimeoutMillis = config.shutdownTimeoutMillis ?? 3e3;
|
|
130
|
+
if (!Number.isFinite(shutdownTimeoutMillis) || shutdownTimeoutMillis <= 0 || shutdownTimeoutMillis > MAX_TIMER_DELAY_MILLIS) throw new Error(`session-telemetry-otel: shutdownTimeoutMillis must be a positive finite number no greater than ${MAX_TIMER_DELAY_MILLIS}, got ${String(shutdownTimeoutMillis)}`);
|
|
131
|
+
this.shutdownTimeoutMillis = shutdownTimeoutMillis;
|
|
132
|
+
this.provider = new LoggerProvider({
|
|
133
|
+
resource: resourceFromAttributes({
|
|
134
|
+
"service.name": APP_IDENTITY.product,
|
|
135
|
+
"service.version": APP_IDENTITY.version,
|
|
136
|
+
"user.id": getOrCreateAnonymousUserId()
|
|
137
|
+
}),
|
|
138
|
+
processors: [new BatchLogRecordProcessor({
|
|
139
|
+
...config.processor,
|
|
140
|
+
exporter: new OTLPLogExporter(config.exporter)
|
|
141
|
+
})]
|
|
142
|
+
});
|
|
143
|
+
const ledger = this.provider.getLogger("@hydraharness/harness-session-telemetry-otel", version);
|
|
144
|
+
const ops = this.provider.getLogger("@hydraharness/harness-session-telemetry-otel/ops", version);
|
|
145
|
+
const enqueue = (record) => {
|
|
146
|
+
(record.channel === "ops" ? ops : ledger).emit({
|
|
147
|
+
timestamp: record.time,
|
|
148
|
+
observedTimestamp: record.time,
|
|
149
|
+
...SEVERITY[record.severity],
|
|
150
|
+
body: record.body,
|
|
151
|
+
attributes: record.attributes
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const backend = {
|
|
155
|
+
emit: enqueue,
|
|
156
|
+
shutdown: () => this.shutdown()
|
|
157
|
+
};
|
|
158
|
+
if (mode === SessionTelemetryMode.FULL) {
|
|
159
|
+
this.directEmit = enqueue;
|
|
160
|
+
new SessionTelemetryCoordinator(ctx, backend, "live");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.directEmit = DROP_RECORD;
|
|
164
|
+
const coordinator = new SessionTelemetryCoordinator(ctx, backend, "on-demand");
|
|
165
|
+
ctx.on("session/event", (session, event) => {
|
|
166
|
+
if (event.type !== "feedback/record") return;
|
|
167
|
+
if (session.events[event.seq] !== event) {
|
|
168
|
+
ctx.logger.warn(NON_CANONICAL_FEEDBACK_WARNING);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
coordinator.captureSession(session, event.seq);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Hand a direct service record to the SDK only in `FULL`. Direct calls are
|
|
176
|
+
* no-ops in `FEEDBACK_ONLY` and `DISABLED`; feedback replay uses a private
|
|
177
|
+
* backend capability created only for the canonical feedback listener.
|
|
178
|
+
* @param record - the logical record offered directly to the service.
|
|
179
|
+
*/
|
|
180
|
+
emit(record) {
|
|
181
|
+
this.directEmit(record);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Ask the SDK to drain and quiesce, but reject after the backend-owned
|
|
185
|
+
* deadline. OTel's processor export timeout wraps `exportCompleted` only;
|
|
186
|
+
* shutdown awaits `exporter.forceFlush()` first, which can remain pending
|
|
187
|
+
* when the transport never obtains a socket. The provider promise remains
|
|
188
|
+
* observed after the deadline so a later rejection cannot become unhandled.
|
|
189
|
+
* `DISABLED` has no provider and resolves immediately.
|
|
190
|
+
* @returns resolves when the SDK pipeline quiesces or is disabled, or rejects at the configured deadline.
|
|
191
|
+
*/
|
|
192
|
+
async shutdown() {
|
|
193
|
+
if (this.provider === void 0) return;
|
|
194
|
+
const providerShutdown = this.provider.shutdown();
|
|
195
|
+
let timer;
|
|
196
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
197
|
+
timer = setTimeout(() => {
|
|
198
|
+
reject(/* @__PURE__ */ new Error(`session-telemetry-otel: provider shutdown exceeded ${this.shutdownTimeoutMillis}ms`));
|
|
199
|
+
}, this.shutdownTimeoutMillis);
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
await Promise.race([providerShutdown, deadline]);
|
|
203
|
+
} finally {
|
|
204
|
+
/* v8 ignore else -- the Promise executor assigns timer synchronously before this race starts. */
|
|
205
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
export { Config, DEFAULT_SHUTDOWN_TIMEOUT_MILLIS, DEFAULT_TELEMETRY_MODE, OpenTelemetrySessionBackend, OpenTelemetrySessionBackend as default, SessionTelemetryMode };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hydraharness/harness-session-telemetry-otel`.
|
|
4
|
+
* @module @hydraharness/harness-session-telemetry-otel/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hydraharness/harness-session-telemetry-otel";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "session-telemetry-otel-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: mode selection changes capture handoff, SDK setup, and
|
|
13
|
+
* local diagnostics without mutating session or service state an independent
|
|
14
|
+
* companion can compare. Export remains inside the SDK past the backend boundary.
|
|
15
|
+
*/
|
|
16
|
+
const install = () => {};
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
//#endregion
|
|
24
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Service Provider for the Hydra harness telemetry capability.
|
|
3
|
+
*
|
|
4
|
+
* Composes the OTel JS SDK as-is — a `LoggerProvider` with a
|
|
5
|
+
* `BatchLogRecordProcessor` and an OTLP/HTTP log exporter — and maps each
|
|
6
|
+
* record handed over by the capture coordinator onto `logger.emit()`. After that call,
|
|
7
|
+
* batching, retry, queueing, and loss policy use the SDK's documented behavior, configured
|
|
8
|
+
* verbatim through the `exporter`/`processor` passthroughs. This package owns
|
|
9
|
+
* capture mode and an outer shutdown deadline: the SDK's export timeout does
|
|
10
|
+
* not bound its preceding `forceFlush()` wait.
|
|
11
|
+
*
|
|
12
|
+
* @module @hydraharness/harness-session-telemetry-otel
|
|
13
|
+
*/
|
|
14
|
+
import z from '@hydraharness/schemastery';
|
|
15
|
+
import type { Context } from '@hydraharness/cordis';
|
|
16
|
+
import { SessionTelemetryBackend, type SessionTelemetryRecord, type SessionTelemetrySharingStatus } from '@hydraharness/harness-session-telemetry';
|
|
17
|
+
import { type BatchLogRecordProcessorOptions } from '@opentelemetry/sdk-logs';
|
|
18
|
+
import type { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
|
|
19
|
+
/** Session-sharing policy selected by {@link Config.mode}. */
|
|
20
|
+
export declare enum SessionTelemetryMode {
|
|
21
|
+
FULL = "FULL",
|
|
22
|
+
FEEDBACK_ONLY = "FEEDBACK_ONLY",
|
|
23
|
+
DISABLED = "DISABLED"
|
|
24
|
+
}
|
|
25
|
+
/** Default session-sharing policy for schema and direct construction. */
|
|
26
|
+
export declare const DEFAULT_TELEMETRY_MODE = SessionTelemetryMode.DISABLED;
|
|
27
|
+
/**
|
|
28
|
+
* Plugin configuration: one sharing policy, two verbatim SDK option objects,
|
|
29
|
+
* and one Hydra-owned shutdown bound. Uploading modes validate their endpoint
|
|
30
|
+
* and shutdown deadline at plugin load; `DISABLED` reads neither.
|
|
31
|
+
*/
|
|
32
|
+
export interface Config {
|
|
33
|
+
/** Sharing policy; defaults to local-only `DISABLED` behavior. */
|
|
34
|
+
mode?: SessionTelemetryMode;
|
|
35
|
+
/**
|
|
36
|
+
* Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete
|
|
37
|
+
* `OTLPExporterNodeConfigBase` shape (`headers`, `timeoutMillis`,
|
|
38
|
+
* `compression`, `keepAlive`, …), owned and documented by the SDK. `url`
|
|
39
|
+
* is the one field this package requires and validates itself.
|
|
40
|
+
*/
|
|
41
|
+
exporter?: OTLPExporterNodeConfigBase & {
|
|
42
|
+
/** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */
|
|
43
|
+
url?: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Passed verbatim to `BatchLogRecordProcessor` (minus the exporter slot,
|
|
47
|
+
* which this plugin fills); the SDK owns and documents these knobs.
|
|
48
|
+
*/
|
|
49
|
+
processor?: Omit<BatchLogRecordProcessorOptions, 'exporter'>;
|
|
50
|
+
/** Maximum time spent awaiting the SDK provider's complete shutdown path. */
|
|
51
|
+
shutdownTimeoutMillis?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Schemastery validator for {@link Config}; cordis runs it before the plugin
|
|
55
|
+
* starts. It checks only the top-level fields; value checks live in the constructor
|
|
56
|
+
* so their errors name the fields. Both SDK option objects pass through unchanged:
|
|
57
|
+
* the SDK defines and validates their fields. Re-declaring them here would
|
|
58
|
+
* silently drop every field this plugin did not repeat.
|
|
59
|
+
*/
|
|
60
|
+
export declare const Config: z<Config>;
|
|
61
|
+
/** Default outer allowance for the SDK's complete shutdown sequence. */
|
|
62
|
+
export declare const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3000;
|
|
63
|
+
/**
|
|
64
|
+
* The backend plugin — the only entry a deployment loads. It always registers
|
|
65
|
+
* the `telemetry` service (duplicate load throws). Uploading modes wire the SDK
|
|
66
|
+
* pipeline and compose {@link SessionTelemetryCoordinator}; `DISABLED` constructs no
|
|
67
|
+
* SDK state and listens only to warn when recorded feedback stays local.
|
|
68
|
+
*/
|
|
69
|
+
export declare class OpenTelemetrySessionBackend extends SessionTelemetryBackend {
|
|
70
|
+
static inject: string[];
|
|
71
|
+
static Config: z<Config>;
|
|
72
|
+
private readonly directEmit;
|
|
73
|
+
private readonly provider;
|
|
74
|
+
private readonly shutdownTimeoutMillis;
|
|
75
|
+
readonly sharing: SessionTelemetrySharingStatus;
|
|
76
|
+
constructor(ctx: Context, config: Config);
|
|
77
|
+
/**
|
|
78
|
+
* Hand a direct service record to the SDK only in `FULL`. Direct calls are
|
|
79
|
+
* no-ops in `FEEDBACK_ONLY` and `DISABLED`; feedback replay uses a private
|
|
80
|
+
* backend capability created only for the canonical feedback listener.
|
|
81
|
+
* @param record - the logical record offered directly to the service.
|
|
82
|
+
*/
|
|
83
|
+
emit(record: SessionTelemetryRecord): void;
|
|
84
|
+
/**
|
|
85
|
+
* Ask the SDK to drain and quiesce, but reject after the backend-owned
|
|
86
|
+
* deadline. OTel's processor export timeout wraps `exportCompleted` only;
|
|
87
|
+
* shutdown awaits `exporter.forceFlush()` first, which can remain pending
|
|
88
|
+
* when the transport never obtains a socket. The provider promise remains
|
|
89
|
+
* observed after the deadline so a later rejection cannot become unhandled.
|
|
90
|
+
* `DISABLED` has no provider and resolves immediately.
|
|
91
|
+
* @returns resolves when the SDK pipeline quiesces or is disabled, or rejects at the configured deadline.
|
|
92
|
+
*/
|
|
93
|
+
shutdown(): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
export default OpenTelemetrySessionBackend;
|
|
96
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hydraharness/harness-session-telemetry-otel`.
|
|
3
|
+
* @module @hydraharness/harness-session-telemetry-otel/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@hydraharness/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "session-telemetry-otel-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-session-telemetry-otel",
|
|
3
|
+
"description": "OpenTelemetry backend for the Hydra harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline",
|
|
4
|
+
"hydra": {
|
|
5
|
+
"plugin": {
|
|
6
|
+
"application": "Send captured session telemetry to the configured OpenTelemetry reporting backend."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.1-rc.6",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
|
|
16
|
+
"directory": "packages/session/session-telemetry-otel"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"types": "lib/types/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./lib/types/index.d.ts",
|
|
24
|
+
"default": "./lib/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./invariant": {
|
|
27
|
+
"types": "./lib/types/invariant.d.ts",
|
|
28
|
+
"default": "./lib/invariant.js"
|
|
29
|
+
},
|
|
30
|
+
"./src/*": "./src/*",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/index.js",
|
|
35
|
+
"lib/invariant.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@opentelemetry/api": "^1.9.1",
|
|
41
|
+
"@opentelemetry/api-logs": "^0.220.0",
|
|
42
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.220.0",
|
|
43
|
+
"@opentelemetry/otlp-exporter-base": "^0.220.0",
|
|
44
|
+
"@opentelemetry/resources": "^2.9.0",
|
|
45
|
+
"@opentelemetry/sdk-logs": "^0.220.0",
|
|
46
|
+
"@hydraharness/schemastery": "^3.18.2"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
50
|
+
"@hydraharness/harness-command-feedback": "^0.1.1-rc.6",
|
|
51
|
+
"@hydraharness/harness-session": "^0.1.1-rc.6",
|
|
52
|
+
"@hydraharness/harness-anonymous-user-id": "^0.1.1-rc.6",
|
|
53
|
+
"@hydraharness/harness-session-telemetry": "^0.1.1-rc.6",
|
|
54
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
55
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.6"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@hydraharness/cordis-plugin-loader": "^1.0.3",
|
|
59
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
60
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.6",
|
|
61
|
+
"@hydraharness/harness-command-feedback": "^0.1.1-rc.6",
|
|
62
|
+
"@hydraharness/harness-session": "^0.1.1-rc.6",
|
|
63
|
+
"@hydraharness/harness-anonymous-user-id": "^0.1.1-rc.6",
|
|
64
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
65
|
+
"@hydraharness/harness-session-telemetry": "^0.1.1-rc.6"
|
|
66
|
+
}
|
|
67
|
+
}
|