@deepseek-ai/dsh-session-telemetry-otel 0.0.1-rc.1
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 +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +54 -0
- package/README.zh.md +54 -0
- package/lib/index.js +198 -0
- package/lib/invariant.js +24 -0
- package/lib/types/index.d.ts +95 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/session/session-telemetry-otel/README.md
|
|
5
|
+
README.md: 585995ce409255df9608bc33b76625374bc67669
|
|
6
|
+
README.zh.md: 7f0b93363fbb4aebb80f0d3cc8108e58ce3f647f
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-session-telemetry-otel
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
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 `@deepseek-ai/dsh-session-telemetry-otel`, operational records on `@deepseek-ai/dsh-session-telemetry-otel/ops`. Resource identity contains `service.name`/`service.version` from `dsh-llm`'s `APP_IDENTITY` plus this package's anonymous `user.id` (`$DSH_HOME/.userid`, a random UUID created on first use and reset by deleting the file), carried once per export batch rather than per record.
|
|
6
|
+
|
|
7
|
+
## Config
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: telemetry-otel
|
|
11
|
+
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
|
12
|
+
config:
|
|
13
|
+
mode: FULL # FULL (default), FEEDBACK_ONLY, or DISABLED
|
|
14
|
+
shutdownTimeoutMillis: 3000 # optional; defaults to 3000
|
|
15
|
+
exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter
|
|
16
|
+
url: https://collector.example.com/v1/logs
|
|
17
|
+
headers:
|
|
18
|
+
authorization: !!js `Bearer ${process.env.OTLP_TOKEN}`
|
|
19
|
+
processor: {} # optional; passed verbatim to BatchLogRecordProcessor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| `mode` | Behavior |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `FULL` | Default. Each projected record, including lifecycle ops records, is handed to the OTel SDK immediately. |
|
|
25
|
+
| `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. |
|
|
26
|
+
| `DISABLED` | No coordinator, provider, processor, or exporter is constructed. No telemetry record leaves the process. A `feedback/record` logs `session telemetry is DISABLED; nothing will be shared and this feedback remains local`; the event remains in the local session log. |
|
|
27
|
+
|
|
28
|
+
Programmatic TypeScript configuration uses the exported `TelemetryMode` enum (`TelemetryMode.FULL`, `TelemetryMode.FEEDBACK_ONLY`, or `TelemetryMode.DISABLED`); raw string literals are not assignable. Serialized Cordis configuration continues to use the string values shown above.
|
|
29
|
+
|
|
30
|
+
Upload authorization is positive and fail-closed. An unknown direct-construction mode fails before transport configuration is read. Only `FULL` accepts direct `ctx.telemetry.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.
|
|
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 DSH-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 `telemetry/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 `telemetry/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/README.zh.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-session-telemetry-otel
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
[遥测(telemetry)seam](../session-telemetry/) 的 OpenTelemetry 后端,也是部署方唯一要加载的条目。其 `mode` 决定 seam 是实时跟随会话事件、仅在记录反馈时回放权威日志,还是将遥测留在本地。上传模式会原样组合 OTel JS SDK(`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP 日志导出器),把每条已交接记录映射到 `logger.emit()`,并使用两个插桩作用域(instrumentation scope):ledger 记录挂在 `@deepseek-ai/dsh-session-telemetry-otel` 下,运维记录挂在 `@deepseek-ai/dsh-session-telemetry-otel/ops` 下。资源身份包含 `service.name`/`service.version`(来自 `dsh-llm` 的 `APP_IDENTITY`),以及本包的匿名 `user.id`(`$DSH_HOME/.userid`;首次使用时创建的随机 UUID,删除该文件可重置);这些身份随每个导出批次携带一次,而非逐条记录携带。
|
|
6
|
+
|
|
7
|
+
## 配置
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: telemetry-otel
|
|
11
|
+
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
|
12
|
+
config:
|
|
13
|
+
mode: FULL # FULL (default), FEEDBACK_ONLY, or DISABLED
|
|
14
|
+
shutdownTimeoutMillis: 3000 # optional; defaults to 3000
|
|
15
|
+
exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter
|
|
16
|
+
url: https://collector.example.com/v1/logs
|
|
17
|
+
headers:
|
|
18
|
+
authorization: !!js `Bearer ${process.env.OTLP_TOKEN}`
|
|
19
|
+
processor: {} # optional; passed verbatim to BatchLogRecordProcessor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| `mode` | 行为 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `FULL` | 默认值。每条已投影记录都立即交给 OTel SDK,包括生命周期运维记录。 |
|
|
25
|
+
| `FEEDBACK_ONLY` | 每个 `feedback/record` 都会回放权威会话日志中截至该事件的后缀,并进行投影与脱敏。后续记录等待下一个反馈事件;如果没有后续反馈,则留在本地。 |
|
|
26
|
+
| `DISABLED` | 不构造协调器、提供方、处理器或导出器。没有遥测记录会离开进程。`feedback/record` 会记录 `session telemetry is DISABLED; nothing will be shared and this feedback remains local`;该事件留在本地会话日志中。 |
|
|
27
|
+
|
|
28
|
+
程序化 TypeScript 配置使用导出的 `TelemetryMode` 枚举(`TelemetryMode.FULL`、`TelemetryMode.FEEDBACK_ONLY` 或 `TelemetryMode.DISABLED`);原始字符串字面量不可赋值。序列化后的 Cordis 配置继续使用上表所示的字符串值。
|
|
29
|
+
|
|
30
|
+
上传授权采用显式许可,且为 fail-closed。通过直接构造传入未知模式时,会在读取传输配置前失败。只有 `FULL` 接受对 `ctx.telemetry.emit()` 的直接调用。`FEEDBACK_ONLY` 向其按需协调器提供私有后端能力,并且仅在 `feedback/record` 对象已经存储于 `session.events[event.seq]` 且对象身份完全相同时,才将其视为同意;独立发出的总线值会被忽略。即使存在导出器选项,`DISABLED` 也绝不会构造 SDK 流水线。
|
|
31
|
+
|
|
32
|
+
`exporter.url` 在 `FULL` 与 `FEEDBACK_ONLY` 中必填,无默认值,且必须能解析为 `http(s)`;在 `DISABLED` 中可省略且不使用。在上传模式中,`shutdownTimeoutMillis` 是由 DSH 管理的有限正数外层截止时间,默认值为 3000 ms;`processor.maxExportBatchSize` 不是正整数时也会在插件加载时失败,因为 SDK 会接受该值,随后却在关闭时挂起。两个 SDK 配置块都整体透传(passthrough):`OTLPExporterNodeConfigBase` 的每个字段(`headers`、`timeoutMillis`、`compression`、`keepAlive` 等)都会到达导出器;批处理、导出节奏(`scheduledDelayMillis`)、重试、队列上限,以及持续失败下的丢失策略,都是通过 `processor` 调节的 SDK 行为。该后端不实现 `flush()`:常规 flush 由批处理器负责。关闭期间,OTel 会先等待 `exporter.forceFlush()`,再等待受处理器 `exportTimeoutMillis` 限制的完成 promise;如果该传输 promise 始终不结算,本包会在 `shutdownTimeoutMillis` 到期时放弃等待,通过协调器记录已隔离的关闭失败,并让应用继续拆卸。该截止时间无法取消 SDK 传输,因此届时仍待处理的记录可能在进程退出时丢失。
|
|
33
|
+
|
|
34
|
+
## 哪些数据会离开本机
|
|
35
|
+
|
|
36
|
+
在上传模式中,记录携带完整的 `event.data`,内容以 seam 的 `telemetry/record` waterfall(瀑布式事件)返回的结果为准:用户与 assistant 消息内容、工具参数与工具结果(命令输出、文件内容)、完整的系统提示词与工具 schema(`request/header`)、todo 文本、压缩(compaction)摘要、钩子的 `stderrSummary`、反馈文本,以及会话 `cwd`(一个本地路径)。seam 不带任何脱敏规则:未挂载 `telemetry/record` 监听器时,导出的就是捕获原样的副本,因此向可信边界之外导出的部署方要挂载自己的规则(见 [seam README](../session-telemetry/README.md#the-redact-waterfall))。`FULL` 在追加时运行脱敏;`FEEDBACK_ONLY` 不保留遥测副本,而是在反馈触发权威日志回放时运行当时挂载的规则。无论如何,提供方凭据都不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。`DISABLED` 不会构造 SDK 流水线,也不会将任何捕获内容交给后端。
|
|
37
|
+
|
|
38
|
+
## 字段映射
|
|
39
|
+
|
|
40
|
+
seam 记录 → SDK 日志记录:`time` → `timestamp`/`observedTimestamp`;`severity` → `severityNumber`/`severityText`(INFO 9 / WARN 13 / ERROR 17);`body` → 结构化日志 body;`attributes` 原样照搬。接收端基于 `(session.id, event.seq)` 去重,并按严重级别告警。在 `FULL` 中,接收端还可通过缺少 `shutdown` 记录检测崩溃:该标记在会话自身 dispose(资源释放)或应用关闭时发出;标记之后出现更多事件,说明遥测发生了重载。在 `FEEDBACK_ONLY` 中,已释放的前缀通常不包含随后的 `shutdown` 标记,因此缺少该标记不是崩溃信号。跨谱系(lineage)的流并不自足:恢复的会话在其自身 id 的流上从上一个进程停止之处继续;fork 出的会话的流从继承边界开始,其前缀位于父会话的流中,由接收端基于 `session.parent_id` + `session.seed_length` 拼接。恢复后的本地日志可能包含从未导出的合成关闭事件;协议流忠实于实际交给 SDK 的记录。
|
|
41
|
+
|
|
42
|
+
## 模型体验
|
|
43
|
+
|
|
44
|
+
无。该后端只把 seam 脱敏后的记录转发进 OTel SDK 流水线;它绝不向模型请求贡献任何内容。
|
|
45
|
+
|
|
46
|
+
#### KV Cache 影响
|
|
47
|
+
|
|
48
|
+
无;本包既不组装也不发送提供方请求。
|
|
49
|
+
|
|
50
|
+
## 已知限制与暂缓事项
|
|
51
|
+
|
|
52
|
+
- **上游实验性源码树**:`@opentelemetry/sdk-logs` 仍从上游实验性(experimental)源码树发布;SDK API 的变动只会落在本包,也仅落在本包;seam 约定不动。
|
|
53
|
+
- **真实 collector 行为属于 SDK 导出器**:身份验证、TLS、限流及其他真实 OTLP 部署行为遵循上游 SDK,不由本包自有兼容层处理。
|
|
54
|
+
- **反馈时快照**:`FEEDBACK_ONLY` 在反馈前不保留遥测自有副本。记录反馈时,它读取并脱敏当前的权威日志;反馈前发生崩溃时什么都不上传,而反馈前的策略变更会影响该次回放的导出内容。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { Telemetry, TelemetryCoordinator } from "@deepseek-ai/dsh-session-telemetry";
|
|
4
|
+
import { APP_IDENTITY } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-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 DeepSeek 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 @deepseek-ai/dsh-session-telemetry-otel
|
|
23
|
+
*/
|
|
24
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
25
|
+
/** Session-sharing policy selected by {@link Config.mode}. */
|
|
26
|
+
var TelemetryMode;
|
|
27
|
+
(function(TelemetryMode) {
|
|
28
|
+
TelemetryMode["FULL"] = "FULL";
|
|
29
|
+
TelemetryMode["FEEDBACK_ONLY"] = "FEEDBACK_ONLY";
|
|
30
|
+
TelemetryMode["DISABLED"] = "DISABLED";
|
|
31
|
+
})(TelemetryMode || (TelemetryMode = {}));
|
|
32
|
+
/** Default session-sharing policy for schema and direct construction. */
|
|
33
|
+
const DEFAULT_TELEMETRY_MODE = TelemetryMode.FULL;
|
|
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 TelemetryMode.FULL:
|
|
42
|
+
case TelemetryMode.FEEDBACK_ONLY:
|
|
43
|
+
case TelemetryMode.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
|
+
/**
|
|
52
|
+
* Schemastery validator for {@link Config}; cordis runs it before the plugin
|
|
53
|
+
* starts. It checks only the top-level fields; value checks live in the constructor
|
|
54
|
+
* so their errors name the fields. Both SDK option objects pass through unchanged:
|
|
55
|
+
* the SDK defines and validates their fields. Re-declaring them here would
|
|
56
|
+
* silently drop every field this plugin did not repeat.
|
|
57
|
+
*/
|
|
58
|
+
const Config = z.object({
|
|
59
|
+
mode: z.union(Object.values(TelemetryMode)).default(DEFAULT_TELEMETRY_MODE),
|
|
60
|
+
exporter: z.any(),
|
|
61
|
+
processor: z.any(),
|
|
62
|
+
shutdownTimeoutMillis: z.number()
|
|
63
|
+
});
|
|
64
|
+
/** Default outer allowance for the SDK's complete shutdown sequence. */
|
|
65
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3e3;
|
|
66
|
+
const MAX_TIMER_DELAY_MILLIS = 2147483647;
|
|
67
|
+
/** Severity mapping from the Service Definition's three-level vocabulary to OTel severity numbers. */
|
|
68
|
+
const SEVERITY = {
|
|
69
|
+
info: {
|
|
70
|
+
severityNumber: SeverityNumber.INFO,
|
|
71
|
+
severityText: "INFO"
|
|
72
|
+
},
|
|
73
|
+
warn: {
|
|
74
|
+
severityNumber: SeverityNumber.WARN,
|
|
75
|
+
severityText: "WARN"
|
|
76
|
+
},
|
|
77
|
+
error: {
|
|
78
|
+
severityNumber: SeverityNumber.ERROR,
|
|
79
|
+
severityText: "ERROR"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The backend plugin — the only entry a deployment loads. It always registers
|
|
84
|
+
* the `telemetry` service (duplicate load throws). Uploading modes wire the SDK
|
|
85
|
+
* pipeline and compose {@link TelemetryCoordinator}; `DISABLED` constructs no
|
|
86
|
+
* SDK state and listens only to warn when recorded feedback stays local.
|
|
87
|
+
*/
|
|
88
|
+
var TelemetryOtel = class extends Telemetry {
|
|
89
|
+
static inject = ["sessions"];
|
|
90
|
+
static Config = Config;
|
|
91
|
+
directEmit;
|
|
92
|
+
provider;
|
|
93
|
+
shutdownTimeoutMillis;
|
|
94
|
+
constructor(ctx, config) {
|
|
95
|
+
const mode = resolveMode(config.mode);
|
|
96
|
+
super(ctx);
|
|
97
|
+
if (mode === TelemetryMode.DISABLED) {
|
|
98
|
+
this.directEmit = DROP_RECORD;
|
|
99
|
+
this.provider = void 0;
|
|
100
|
+
this.shutdownTimeoutMillis = DEFAULT_SHUTDOWN_TIMEOUT_MILLIS;
|
|
101
|
+
ctx.on("session/event", (_session, event) => {
|
|
102
|
+
if (event.type === "feedback/record") ctx.logger.warn(DISABLED_FEEDBACK_WARNING);
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const url = config.exporter?.url;
|
|
107
|
+
if (url === void 0 || url.length === 0) throw new Error("session-telemetry-otel: exporter.url is required (the full OTLP logs endpoint)");
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = new URL(url);
|
|
111
|
+
} catch {
|
|
112
|
+
throw new Error(`session-telemetry-otel: exporter.url is not a valid URL: ${JSON.stringify(url)}`);
|
|
113
|
+
}
|
|
114
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`session-telemetry-otel: exporter.url must be http(s), got ${parsed.protocol}`);
|
|
115
|
+
const batchSize = config.processor?.maxExportBatchSize;
|
|
116
|
+
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)}`);
|
|
117
|
+
const shutdownTimeoutMillis = config.shutdownTimeoutMillis ?? 3e3;
|
|
118
|
+
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)}`);
|
|
119
|
+
this.shutdownTimeoutMillis = shutdownTimeoutMillis;
|
|
120
|
+
this.provider = new LoggerProvider({
|
|
121
|
+
resource: resourceFromAttributes({
|
|
122
|
+
"service.name": APP_IDENTITY.product,
|
|
123
|
+
"service.version": APP_IDENTITY.version,
|
|
124
|
+
"user.id": getOrCreateAnonymousUserId()
|
|
125
|
+
}),
|
|
126
|
+
processors: [new BatchLogRecordProcessor({
|
|
127
|
+
...config.processor,
|
|
128
|
+
exporter: new OTLPLogExporter(config.exporter)
|
|
129
|
+
})]
|
|
130
|
+
});
|
|
131
|
+
const ledger = this.provider.getLogger("@deepseek-ai/dsh-session-telemetry-otel", version);
|
|
132
|
+
const ops = this.provider.getLogger("@deepseek-ai/dsh-session-telemetry-otel/ops", version);
|
|
133
|
+
const enqueue = (record) => {
|
|
134
|
+
(record.channel === "ops" ? ops : ledger).emit({
|
|
135
|
+
timestamp: record.time,
|
|
136
|
+
observedTimestamp: record.time,
|
|
137
|
+
...SEVERITY[record.severity],
|
|
138
|
+
body: record.body,
|
|
139
|
+
attributes: record.attributes
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const backend = {
|
|
143
|
+
emit: enqueue,
|
|
144
|
+
shutdown: () => this.shutdown()
|
|
145
|
+
};
|
|
146
|
+
if (mode === TelemetryMode.FULL) {
|
|
147
|
+
this.directEmit = enqueue;
|
|
148
|
+
new TelemetryCoordinator(ctx, backend, "live");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.directEmit = DROP_RECORD;
|
|
152
|
+
const coordinator = new TelemetryCoordinator(ctx, backend, "on-demand");
|
|
153
|
+
ctx.on("session/event", (session, event) => {
|
|
154
|
+
if (event.type !== "feedback/record") return;
|
|
155
|
+
if (session.events[event.seq] !== event) {
|
|
156
|
+
ctx.logger.warn(NON_CANONICAL_FEEDBACK_WARNING);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
coordinator.captureSession(session, event.seq);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Hand a direct service record to the SDK only in `FULL`. Direct calls are
|
|
164
|
+
* no-ops in `FEEDBACK_ONLY` and `DISABLED`; feedback replay uses a private
|
|
165
|
+
* backend capability created only for the canonical feedback listener.
|
|
166
|
+
* @param record - the logical record offered directly to the service.
|
|
167
|
+
*/
|
|
168
|
+
emit(record) {
|
|
169
|
+
this.directEmit(record);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Ask the SDK to drain and quiesce, but reject after the backend-owned
|
|
173
|
+
* deadline. OTel's processor export timeout wraps `exportCompleted` only;
|
|
174
|
+
* shutdown awaits `exporter.forceFlush()` first, which can remain pending
|
|
175
|
+
* when the transport never obtains a socket. The provider promise remains
|
|
176
|
+
* observed after the deadline so a later rejection cannot become unhandled.
|
|
177
|
+
* `DISABLED` has no provider and resolves immediately.
|
|
178
|
+
* @returns resolves when the SDK pipeline quiesces or is disabled, or rejects at the configured deadline.
|
|
179
|
+
*/
|
|
180
|
+
async shutdown() {
|
|
181
|
+
if (this.provider === void 0) return;
|
|
182
|
+
const providerShutdown = this.provider.shutdown();
|
|
183
|
+
let timer;
|
|
184
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
185
|
+
timer = setTimeout(() => {
|
|
186
|
+
reject(/* @__PURE__ */ new Error(`session-telemetry-otel: provider shutdown exceeded ${this.shutdownTimeoutMillis}ms`));
|
|
187
|
+
}, this.shutdownTimeoutMillis);
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
await Promise.race([providerShutdown, deadline]);
|
|
191
|
+
} finally {
|
|
192
|
+
/* v8 ignore else -- the Promise executor assigns timer synchronously before this race starts. */
|
|
193
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
export { Config, DEFAULT_SHUTDOWN_TIMEOUT_MILLIS, DEFAULT_TELEMETRY_MODE, TelemetryMode, TelemetryOtel, TelemetryOtel as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-session-telemetry-otel`.
|
|
4
|
+
* @module @deepseek-ai/dsh-session-telemetry-otel/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Service provider for the DeepSeek 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 @deepseek-ai/dsh-session-telemetry-otel
|
|
13
|
+
*/
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import { Telemetry, type TelemetryRecord } from '@deepseek-ai/dsh-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 TelemetryMode {
|
|
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 = TelemetryMode.FULL;
|
|
27
|
+
/**
|
|
28
|
+
* Plugin configuration: one sharing policy, two verbatim SDK option objects,
|
|
29
|
+
* and one DSH-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 immediate `FULL` delivery. */
|
|
34
|
+
mode?: TelemetryMode;
|
|
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 TelemetryCoordinator}; `DISABLED` constructs no
|
|
67
|
+
* SDK state and listens only to warn when recorded feedback stays local.
|
|
68
|
+
*/
|
|
69
|
+
export declare class TelemetryOtel extends Telemetry {
|
|
70
|
+
static inject: string[];
|
|
71
|
+
static Config: z<Config>;
|
|
72
|
+
private readonly directEmit;
|
|
73
|
+
private readonly provider;
|
|
74
|
+
private readonly shutdownTimeoutMillis;
|
|
75
|
+
constructor(ctx: Context, config: Config);
|
|
76
|
+
/**
|
|
77
|
+
* Hand a direct service record to the SDK only in `FULL`. Direct calls are
|
|
78
|
+
* no-ops in `FEEDBACK_ONLY` and `DISABLED`; feedback replay uses a private
|
|
79
|
+
* backend capability created only for the canonical feedback listener.
|
|
80
|
+
* @param record - the logical record offered directly to the service.
|
|
81
|
+
*/
|
|
82
|
+
emit(record: TelemetryRecord): void;
|
|
83
|
+
/**
|
|
84
|
+
* Ask the SDK to drain and quiesce, but reject after the backend-owned
|
|
85
|
+
* deadline. OTel's processor export timeout wraps `exportCompleted` only;
|
|
86
|
+
* shutdown awaits `exporter.forceFlush()` first, which can remain pending
|
|
87
|
+
* when the transport never obtains a socket. The provider promise remains
|
|
88
|
+
* observed after the deadline so a later rejection cannot become unhandled.
|
|
89
|
+
* `DISABLED` has no provider and resolves immediately.
|
|
90
|
+
* @returns resolves when the SDK pipeline quiesces or is disabled, or rejects at the configured deadline.
|
|
91
|
+
*/
|
|
92
|
+
shutdown(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export default TelemetryOtel;
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-session-telemetry-otel`.
|
|
3
|
+
* @module @deepseek-ai/dsh-session-telemetry-otel/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/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,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-session-telemetry-otel",
|
|
3
|
+
"description": "OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/session/session-telemetry-otel"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@opentelemetry/api": "^1.9.1",
|
|
36
|
+
"@opentelemetry/api-logs": "^0.220.0",
|
|
37
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.220.0",
|
|
38
|
+
"@opentelemetry/otlp-exporter-base": "^0.220.0",
|
|
39
|
+
"@opentelemetry/resources": "^2.9.0",
|
|
40
|
+
"@opentelemetry/sdk-logs": "^0.220.0",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-command-feedback": "^0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-user-id": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-session-telemetry": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-command-feedback": "^0.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-session-telemetry": "^0.0.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-user-id": "^0.0.1-rc.1",
|
|
60
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
61
|
+
}
|
|
62
|
+
}
|