@ironbee-ai/cli 0.13.0 → 0.14.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/CHANGELOG.md +6 -0
- package/README.md +9 -0
- package/dist/clients/claude/hooks/activity-start.d.ts.map +1 -1
- package/dist/clients/claude/hooks/activity-start.js +4 -0
- package/dist/clients/claude/hooks/activity-start.js.map +1 -1
- package/dist/clients/claude/hooks/session-start.d.ts.map +1 -1
- package/dist/clients/claude/hooks/session-start.js +4 -0
- package/dist/clients/claude/hooks/session-start.js.map +1 -1
- package/dist/clients/claude/index.d.ts +10 -0
- package/dist/clients/claude/index.d.ts.map +1 -1
- package/dist/clients/claude/index.js +105 -0
- package/dist/clients/claude/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +1 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/otel.d.ts +20 -0
- package/dist/commands/otel.d.ts.map +1 -0
- package/dist/commands/otel.js +132 -0
- package/dist/commands/otel.js.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +47 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +57 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/event.d.ts +112 -0
- package/dist/lib/event.d.ts.map +1 -1
- package/dist/lib/event.js +1 -0
- package/dist/lib/event.js.map +1 -1
- package/dist/lib/gitignore.d.ts.map +1 -1
- package/dist/lib/gitignore.js +6 -0
- package/dist/lib/gitignore.js.map +1 -1
- package/dist/lib/version.d.ts +5 -0
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +5 -0
- package/dist/lib/version.js.map +1 -1
- package/dist/otel/collector.d.ts +66 -0
- package/dist/otel/collector.d.ts.map +1 -0
- package/dist/otel/collector.js +317 -0
- package/dist/otel/collector.js.map +1 -0
- package/dist/otel/context/build.d.ts +37 -0
- package/dist/otel/context/build.d.ts.map +1 -0
- package/dist/otel/context/build.js +103 -0
- package/dist/otel/context/build.js.map +1 -0
- package/dist/otel/context/classify.d.ts +40 -0
- package/dist/otel/context/classify.d.ts.map +1 -0
- package/dist/otel/context/classify.js +228 -0
- package/dist/otel/context/classify.js.map +1 -0
- package/dist/otel/context/extract.d.ts +39 -0
- package/dist/otel/context/extract.d.ts.map +1 -0
- package/dist/otel/context/extract.js +76 -0
- package/dist/otel/context/extract.js.map +1 -0
- package/dist/otel/context/markers.d.ts +37 -0
- package/dist/otel/context/markers.d.ts.map +1 -0
- package/dist/otel/context/markers.js +179 -0
- package/dist/otel/context/markers.js.map +1 -0
- package/dist/otel/context/util.d.ts +15 -0
- package/dist/otel/context/util.d.ts.map +1 -0
- package/dist/otel/context/util.js +33 -0
- package/dist/otel/context/util.js.map +1 -0
- package/dist/otel/daemon/ensure.d.ts +52 -0
- package/dist/otel/daemon/ensure.d.ts.map +1 -0
- package/dist/otel/daemon/ensure.js +226 -0
- package/dist/otel/daemon/ensure.js.map +1 -0
- package/dist/otel/daemon/forward.d.ts +47 -0
- package/dist/otel/daemon/forward.d.ts.map +1 -0
- package/dist/otel/daemon/forward.js +0 -0
- package/dist/otel/daemon/forward.js.map +1 -0
- package/dist/otel/daemon/paths.d.ts +24 -0
- package/dist/otel/daemon/paths.d.ts.map +1 -0
- package/dist/otel/daemon/paths.js +47 -0
- package/dist/otel/daemon/paths.js.map +1 -0
- package/dist/otel/daemon/process.d.ts +21 -0
- package/dist/otel/daemon/process.d.ts.map +1 -0
- package/dist/otel/daemon/process.js +149 -0
- package/dist/otel/daemon/process.js.map +1 -0
- package/dist/otel/daemon/reprocess.d.ts +27 -0
- package/dist/otel/daemon/reprocess.d.ts.map +1 -0
- package/dist/otel/daemon/reprocess.js +112 -0
- package/dist/otel/daemon/reprocess.js.map +1 -0
- package/dist/otel/log-handler.d.ts +37 -0
- package/dist/otel/log-handler.d.ts.map +1 -0
- package/dist/otel/log-handler.js +332 -0
- package/dist/otel/log-handler.js.map +1 -0
- package/dist/otel/metric-handler.d.ts +12 -0
- package/dist/otel/metric-handler.d.ts.map +1 -0
- package/dist/otel/metric-handler.js +18 -0
- package/dist/otel/metric-handler.js.map +1 -0
- package/dist/otel/trace-handler.d.ts +12 -0
- package/dist/otel/trace-handler.d.ts.map +1 -0
- package/dist/otel/trace-handler.js +18 -0
- package/dist/otel/trace-handler.js.map +1 -0
- package/dist/otel/types.d.ts +105 -0
- package/dist/otel/types.d.ts.map +1 -0
- package/dist/otel/types.js +15 -0
- package/dist/otel/types.js.map +1 -0
- package/dist/tui/config/schema.d.ts.map +1 -1
- package/dist/tui/config/schema.js +44 -0
- package/dist/tui/config/schema.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IronBee — read a raw `*.request.json` body file and build its
|
|
4
|
+
* `session_context` event. Handles the security gate (path must be inside
|
|
5
|
+
* `.ironbee/otel`), the partial-write guard (size vs `body_length`), and
|
|
6
|
+
* identity resolution (session_id / project_name / model / base fields). Returns
|
|
7
|
+
* the built event + the owning project dir, or `null` to skip.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.processBodyFile = processBodyFile;
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const logger_1 = require("../../lib/logger");
|
|
14
|
+
const util_1 = require("../context/util");
|
|
15
|
+
const classify_1 = require("../context/classify");
|
|
16
|
+
const build_1 = require("../context/build");
|
|
17
|
+
const paths_1 = require("./paths");
|
|
18
|
+
const PARTIAL_WRITE_RETRIES = 3;
|
|
19
|
+
const PARTIAL_WRITE_DELAY_MS = 50;
|
|
20
|
+
function delay(ms) {
|
|
21
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
22
|
+
}
|
|
23
|
+
function isObject(v) {
|
|
24
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
25
|
+
}
|
|
26
|
+
function readJsonObject(path) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, "utf8"));
|
|
29
|
+
return isObject(parsed) ? parsed : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The model the request was actually sent with — `body.model` is the ground
|
|
37
|
+
* truth and is always present on a real Messages request. Preferred over the
|
|
38
|
+
* OTLP event's `model` attribute, which is empty on the catch-up path
|
|
39
|
+
* (`orphanMeta`) and which the collector rejects (empty `model` → HTTP 400).
|
|
40
|
+
*/
|
|
41
|
+
function modelFromBody(body) {
|
|
42
|
+
if (isObject(body) && typeof body.model === "string" && body.model.length > 0) {
|
|
43
|
+
return body.model;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Recover `session_id` from the body's double-encoded `metadata.user_id` (catch-up fallback). */
|
|
48
|
+
function sessionIdFromBody(body) {
|
|
49
|
+
if (!isObject(body) || !isObject(body.metadata)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const userId = body.metadata.user_id;
|
|
53
|
+
if (typeof userId !== "string") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(userId);
|
|
58
|
+
if (isObject(parsed) && typeof parsed.session_id === "string") {
|
|
59
|
+
return parsed.session_id;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// malformed → no fallback
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** Best-effort read of base fields from the session's state.json (may be absent). */
|
|
68
|
+
function readSessionState(projectDir, sessionId) {
|
|
69
|
+
const j = readJsonObject((0, path_1.join)(projectDir, ".ironbee", "sessions", sessionId, "state.json"));
|
|
70
|
+
if (!j) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
const out = {};
|
|
74
|
+
if (typeof j.userEmail === "string") {
|
|
75
|
+
out.user_email = j.userEmail;
|
|
76
|
+
}
|
|
77
|
+
if (j.usageType === "api" || j.usageType === "subscription") {
|
|
78
|
+
out.usage_type = j.usageType;
|
|
79
|
+
}
|
|
80
|
+
if (typeof j.usagePlan === "string") {
|
|
81
|
+
out.usage_plan = j.usagePlan;
|
|
82
|
+
}
|
|
83
|
+
if (typeof j.projectName === "string") {
|
|
84
|
+
out.projectName = j.projectName;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read + build. Returns `null` (skip) on: a path outside `.ironbee/otel`
|
|
90
|
+
* (security gate), a missing / unreadable file, unparseable JSON, or no
|
|
91
|
+
* resolvable session_id.
|
|
92
|
+
*/
|
|
93
|
+
async function processBodyFile(meta) {
|
|
94
|
+
const projectDir = (0, paths_1.projectDirFromBodyRef)(meta.body_ref);
|
|
95
|
+
if (projectDir === null) {
|
|
96
|
+
logger_1.logger.warn(`otel: refusing body_ref outside a .ironbee/otel dir: ${meta.body_ref}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Read with a partial-write guard: a size mismatch vs body_length means the
|
|
100
|
+
// write was still in flight when the event fired → brief retry.
|
|
101
|
+
let content = null;
|
|
102
|
+
for (let attempt = 0; attempt < PARTIAL_WRITE_RETRIES; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
content = (0, fs_1.readFileSync)(meta.body_ref, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null; // gone / unreadable
|
|
108
|
+
}
|
|
109
|
+
if (meta.body_length === null || (0, util_1.byteLen)(content) === meta.body_length || attempt === PARTIAL_WRITE_RETRIES - 1) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
await delay(PARTIAL_WRITE_DELAY_MS);
|
|
113
|
+
}
|
|
114
|
+
if (content === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
let body;
|
|
118
|
+
try {
|
|
119
|
+
body = JSON.parse(content);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
logger_1.logger.debug(`otel: unparseable body file ${meta.body_ref}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const sessionId = meta.session_id ?? sessionIdFromBody(body);
|
|
126
|
+
if (!sessionId) {
|
|
127
|
+
logger_1.logger.debug(`otel: no session_id for ${meta.body_ref}; skipping`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const state = readSessionState(projectDir, sessionId);
|
|
131
|
+
const projectName = meta.project_name ?? state.projectName ?? (0, path_1.basename)(projectDir);
|
|
132
|
+
const classification = (0, classify_1.classifyRequestBody)(body);
|
|
133
|
+
const scMeta = {
|
|
134
|
+
session_id: sessionId,
|
|
135
|
+
project_name: projectName,
|
|
136
|
+
// Prefer the body's authoritative model; the event attr is "" on catch-up.
|
|
137
|
+
model: modelFromBody(body) ?? meta.model,
|
|
138
|
+
query_source: meta.query_source,
|
|
139
|
+
sequence_number: meta.sequence_number,
|
|
140
|
+
timestamp: meta.timestamp,
|
|
141
|
+
request_uuid: (0, paths_1.requestUuidFromBodyRef)(meta.body_ref),
|
|
142
|
+
user_email: state.user_email,
|
|
143
|
+
usage_type: state.usage_type,
|
|
144
|
+
usage_plan: state.usage_plan,
|
|
145
|
+
};
|
|
146
|
+
const event = (0, build_1.buildSessionContextEvent)(scMeta, classification);
|
|
147
|
+
return { event, projectDir, bodyRef: meta.body_ref };
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../../src/otel/daemon/process.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IronBee — OTEL orphan body reprocessing (catch-up + on-demand retry).
|
|
3
|
+
*
|
|
4
|
+
* Scans every registered project's `<project>/.ironbee/otel/` for leftover
|
|
5
|
+
* `*.request.json` files (requests whose announcing OTLP event was dropped while
|
|
6
|
+
* the daemon was down, or whose forward failed) and resubmits each to the
|
|
7
|
+
* forward pipeline. Orphan `*.response.json` files are deleted outright (the
|
|
8
|
+
* response is disposable).
|
|
9
|
+
*/
|
|
10
|
+
import { ApiRequestBodyMeta } from "../context/extract";
|
|
11
|
+
/** Minimal sink the scan submits to (ForwardPipeline satisfies it structurally). */
|
|
12
|
+
export interface OrphanSubmitter {
|
|
13
|
+
submit(meta: ApiRequestBodyMeta): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Count unprocessed `*.request.json` files across registered projects' otel dirs
|
|
17
|
+
* (the on-disk undelivered backlog) for `/health`'s `orphan_files`. Fully
|
|
18
|
+
* defensive — any read error contributes 0 rather than throwing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function countOrphanFiles(): number;
|
|
21
|
+
/**
|
|
22
|
+
* Resubmit every unprocessed `*.request.json` across registered projects and
|
|
23
|
+
* sweep orphan `*.response.json`. Returns the number of request files
|
|
24
|
+
* resubmitted. Synchronous scan; `submit` itself is fire-and-forget.
|
|
25
|
+
*/
|
|
26
|
+
export declare function reprocessOrphans(pipeline: OrphanSubmitter): number;
|
|
27
|
+
//# sourceMappingURL=reprocess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reprocess.d.ts","sourceRoot":"","sources":["../../../src/otel/daemon/reprocess.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC5B,MAAM,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAoBzC;AAiBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CA0ClE"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IronBee — OTEL orphan body reprocessing (catch-up + on-demand retry).
|
|
4
|
+
*
|
|
5
|
+
* Scans every registered project's `<project>/.ironbee/otel/` for leftover
|
|
6
|
+
* `*.request.json` files (requests whose announcing OTLP event was dropped while
|
|
7
|
+
* the daemon was down, or whose forward failed) and resubmits each to the
|
|
8
|
+
* forward pipeline. Orphan `*.response.json` files are deleted outright (the
|
|
9
|
+
* response is disposable).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.countOrphanFiles = countOrphanFiles;
|
|
13
|
+
exports.reprocessOrphans = reprocessOrphans;
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
const projects_registry_1 = require("../../lib/projects-registry");
|
|
17
|
+
const logger_1 = require("../../lib/logger");
|
|
18
|
+
/**
|
|
19
|
+
* Count unprocessed `*.request.json` files across registered projects' otel dirs
|
|
20
|
+
* (the on-disk undelivered backlog) for `/health`'s `orphan_files`. Fully
|
|
21
|
+
* defensive — any read error contributes 0 rather than throwing.
|
|
22
|
+
*/
|
|
23
|
+
function countOrphanFiles() {
|
|
24
|
+
let projects;
|
|
25
|
+
try {
|
|
26
|
+
projects = (0, projects_registry_1.listProjects)();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
let count = 0;
|
|
32
|
+
for (const proj of projects) {
|
|
33
|
+
try {
|
|
34
|
+
for (const name of (0, fs_1.readdirSync)((0, path_1.join)(proj.path, ".ironbee", "otel"))) {
|
|
35
|
+
if (name.endsWith(".request.json")) {
|
|
36
|
+
count += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// no otel dir for this project — skip
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return count;
|
|
45
|
+
}
|
|
46
|
+
/** Catch-up meta for a file with no announcing event: timestamp = file mtime. */
|
|
47
|
+
function orphanMeta(bodyRef, mtimeMs) {
|
|
48
|
+
return {
|
|
49
|
+
body_ref: bodyRef,
|
|
50
|
+
sequence_number: null,
|
|
51
|
+
model: "",
|
|
52
|
+
query_source: null,
|
|
53
|
+
body_length: null,
|
|
54
|
+
session_id: null,
|
|
55
|
+
project_name: null,
|
|
56
|
+
timestamp: mtimeMs,
|
|
57
|
+
from_catchup: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resubmit every unprocessed `*.request.json` across registered projects and
|
|
62
|
+
* sweep orphan `*.response.json`. Returns the number of request files
|
|
63
|
+
* resubmitted. Synchronous scan; `submit` itself is fire-and-forget.
|
|
64
|
+
*/
|
|
65
|
+
function reprocessOrphans(pipeline) {
|
|
66
|
+
let projects;
|
|
67
|
+
try {
|
|
68
|
+
projects = (0, projects_registry_1.listProjects)();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
logger_1.logger.debug(`otel: reprocess could not read project registry: ${e instanceof Error ? e.message : String(e)}`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
let submitted = 0;
|
|
75
|
+
for (const proj of projects) {
|
|
76
|
+
const otelDir = (0, path_1.join)(proj.path, ".ironbee", "otel");
|
|
77
|
+
let names;
|
|
78
|
+
try {
|
|
79
|
+
names = (0, fs_1.readdirSync)(otelDir);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue; // no otel dir for this project
|
|
83
|
+
}
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
const full = (0, path_1.join)(otelDir, name);
|
|
86
|
+
if (name.endsWith(".request.json")) {
|
|
87
|
+
let mtimeMs = Date.now();
|
|
88
|
+
try {
|
|
89
|
+
mtimeMs = (0, fs_1.statSync)(full).mtimeMs;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
pipeline.submit(orphanMeta(full, mtimeMs));
|
|
95
|
+
submitted += 1;
|
|
96
|
+
}
|
|
97
|
+
else if (name.endsWith(".response.json")) {
|
|
98
|
+
try {
|
|
99
|
+
(0, fs_1.unlinkSync)(full);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// disposable in v1; ignore
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (submitted > 0) {
|
|
108
|
+
logger_1.logger.debug(`otel: reprocess resubmitted ${submitted} orphan request bodies`);
|
|
109
|
+
}
|
|
110
|
+
return submitted;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=reprocess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reprocess.js","sourceRoot":"","sources":["../../../src/otel/daemon/reprocess.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IronBee — OTEL log signal handler
|
|
3
|
+
*
|
|
4
|
+
* Decodes the OTLP/HTTP JSON logs envelope (resourceLogs → scopeLogs →
|
|
5
|
+
* logRecords), filters down to the event names we care about, and prints each
|
|
6
|
+
* surviving record to the console. In `OTEL_LOG_RAW_API_BODIES=file:<dir>`
|
|
7
|
+
* mode the raw Anthropic Messages API bodies live on disk; a `body_ref`
|
|
8
|
+
* attribute points at the file, which this handler reads and renders inline.
|
|
9
|
+
*
|
|
10
|
+
* Only `handleLogs` is consumed by `collector.ts`; everything else is internal.
|
|
11
|
+
*/
|
|
12
|
+
import { ApiRequestBodyMeta } from "./context/extract";
|
|
13
|
+
/**
|
|
14
|
+
* Log event names (the `event.name` attribute Claude Code sets on each record)
|
|
15
|
+
* the collector actually processes — every other event is ignored (noop). Kept
|
|
16
|
+
* deliberately narrow: the raw API request/response bodies are all we want
|
|
17
|
+
* today. Add an entry here to start surfacing another event.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HANDLED_LOG_EVENTS: ReadonlySet<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Handle one OTLP/HTTP JSON logs request body. Throws on invalid JSON (the
|
|
22
|
+
* collector maps that to HTTP 400); printing failures are caught and logged so
|
|
23
|
+
* one malformed record never takes the server down.
|
|
24
|
+
*/
|
|
25
|
+
/** Minimal sink the live daemon routes request bodies to (ForwardPipeline satisfies it). */
|
|
26
|
+
export interface OTELLogSink {
|
|
27
|
+
submit(meta: ApiRequestBodyMeta): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Handle one OTLP/HTTP JSON logs request body. With a `sink` (the live daemon),
|
|
31
|
+
* `api_request_body` records are submitted to the forward pipeline and
|
|
32
|
+
* `api_response_body` files are deleted; without one (dev / direct callers),
|
|
33
|
+
* handled records are pretty-printed to the console. Throws on invalid JSON
|
|
34
|
+
* (the collector maps that to HTTP 400).
|
|
35
|
+
*/
|
|
36
|
+
export declare function handleLogs(raw: Buffer, sink?: OTELLogSink): void;
|
|
37
|
+
//# sourceMappingURL=log-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-handler.d.ts","sourceRoot":"","sources":["../../src/otel/log-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH,OAAO,EAA6B,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGlF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAGjD,CAAC;AA8QH;;;;GAIG;AACH,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IACxB,MAAM,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAC1C;AA4CD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,IAAI,CAYhE"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IronBee — OTEL log signal handler
|
|
4
|
+
*
|
|
5
|
+
* Decodes the OTLP/HTTP JSON logs envelope (resourceLogs → scopeLogs →
|
|
6
|
+
* logRecords), filters down to the event names we care about, and prints each
|
|
7
|
+
* surviving record to the console. In `OTEL_LOG_RAW_API_BODIES=file:<dir>`
|
|
8
|
+
* mode the raw Anthropic Messages API bodies live on disk; a `body_ref`
|
|
9
|
+
* attribute points at the file, which this handler reads and renders inline.
|
|
10
|
+
*
|
|
11
|
+
* Only `handleLogs` is consumed by `collector.ts`; everything else is internal.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.HANDLED_LOG_EVENTS = void 0;
|
|
15
|
+
exports.handleLogs = handleLogs;
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const output_1 = require("../lib/output");
|
|
18
|
+
const extract_1 = require("./context/extract");
|
|
19
|
+
const paths_1 = require("./daemon/paths");
|
|
20
|
+
/**
|
|
21
|
+
* Log event names (the `event.name` attribute Claude Code sets on each record)
|
|
22
|
+
* the collector actually processes — every other event is ignored (noop). Kept
|
|
23
|
+
* deliberately narrow: the raw API request/response bodies are all we want
|
|
24
|
+
* today. Add an entry here to start surfacing another event.
|
|
25
|
+
*/
|
|
26
|
+
exports.HANDLED_LOG_EVENTS = new Set([
|
|
27
|
+
"api_request_body",
|
|
28
|
+
"api_response_body",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Attribute Claude Code sets in `OTEL_LOG_RAW_API_BODIES=file:<dir>` mode on
|
|
32
|
+
* the `api_request_body` / `api_response_body` events. Its value is an
|
|
33
|
+
* absolute path to a `.request.json` / `.response.json` file holding the
|
|
34
|
+
* untruncated body; the handler reads that file and surfaces the body.
|
|
35
|
+
*/
|
|
36
|
+
const BODY_REF_KEY = "body_ref";
|
|
37
|
+
/**
|
|
38
|
+
* Max characters of a resolved `body_ref` body (or any long attribute value)
|
|
39
|
+
* printed to the console before truncation. Claude Code's file-mode raw API
|
|
40
|
+
* bodies carry the full conversation history and can be hundreds of KB, so a
|
|
41
|
+
* cap keeps the terminal usable. Override with `OTEL_BODY_PRINT_LIMIT` (chars;
|
|
42
|
+
* `0` = unlimited). The full body always remains on disk at the `body_ref`.
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_BODY_PRINT_LIMIT = 8192;
|
|
45
|
+
/** Resolve the console body-print cap once, honoring `OTEL_BODY_PRINT_LIMIT`. */
|
|
46
|
+
function resolveBodyPrintLimit() {
|
|
47
|
+
const raw = process.env.OTEL_BODY_PRINT_LIMIT;
|
|
48
|
+
if (raw === undefined || raw === "") {
|
|
49
|
+
return DEFAULT_BODY_PRINT_LIMIT;
|
|
50
|
+
}
|
|
51
|
+
const n = Number(raw);
|
|
52
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_BODY_PRINT_LIMIT;
|
|
53
|
+
}
|
|
54
|
+
const BODY_PRINT_LIMIT = resolveBodyPrintLimit();
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// OTLP/JSON value decoding
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** Collapse an OTLP `AnyValue` to a plain JS value (recursing into arrays / kvlists). */
|
|
59
|
+
function decodeAnyValue(value) {
|
|
60
|
+
if (!value) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (value.stringValue !== undefined) {
|
|
64
|
+
return value.stringValue;
|
|
65
|
+
}
|
|
66
|
+
if (value.boolValue !== undefined) {
|
|
67
|
+
return value.boolValue;
|
|
68
|
+
}
|
|
69
|
+
if (value.intValue !== undefined) {
|
|
70
|
+
// int64 arrives as a decimal string; keep a number when it round-trips safely.
|
|
71
|
+
const n = Number(value.intValue);
|
|
72
|
+
return Number.isSafeInteger(n) ? n : value.intValue;
|
|
73
|
+
}
|
|
74
|
+
if (value.doubleValue !== undefined) {
|
|
75
|
+
return value.doubleValue;
|
|
76
|
+
}
|
|
77
|
+
if (value.bytesValue !== undefined) {
|
|
78
|
+
return value.bytesValue;
|
|
79
|
+
}
|
|
80
|
+
if (value.arrayValue) {
|
|
81
|
+
const values = value.arrayValue.values ?? [];
|
|
82
|
+
return values.map((v) => decodeAnyValue(v));
|
|
83
|
+
}
|
|
84
|
+
if (value.kvlistValue) {
|
|
85
|
+
return decodeAttributes(value.kvlistValue.values ?? []);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Turn an OTLP `KeyValue[]` (resource / scope / record attributes) into a plain object. */
|
|
90
|
+
function decodeAttributes(attrs) {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const kv of attrs) {
|
|
93
|
+
if (kv && typeof kv.key === "string") {
|
|
94
|
+
out[kv.key] = decodeAnyValue(kv.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** Cheaply read a record's event name (the `event.name` attribute, else the body string). */
|
|
100
|
+
function extractEventName(record) {
|
|
101
|
+
for (const kv of record.attributes ?? []) {
|
|
102
|
+
if (kv.key === "event.name" && kv.value?.stringValue !== undefined) {
|
|
103
|
+
return kv.value.stringValue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (record.body?.stringValue !== undefined) {
|
|
107
|
+
return record.body.stringValue;
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
/** Epoch-nanoseconds string → ISO timestamp; falls back to "now" / the raw value. */
|
|
112
|
+
function nanoToIso(nano) {
|
|
113
|
+
if (!nano) {
|
|
114
|
+
return new Date().toISOString();
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const ms = Number(BigInt(nano) / 1000000n);
|
|
118
|
+
return new Date(ms).toISOString();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return nano;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Map an OTEL `severityNumber` to its text band when `severityText` is absent. */
|
|
125
|
+
function severityFromNumber(n) {
|
|
126
|
+
if (n === undefined) {
|
|
127
|
+
return "INFO";
|
|
128
|
+
}
|
|
129
|
+
if (n <= 4) {
|
|
130
|
+
return "TRACE";
|
|
131
|
+
}
|
|
132
|
+
if (n <= 8) {
|
|
133
|
+
return "DEBUG";
|
|
134
|
+
}
|
|
135
|
+
if (n <= 12) {
|
|
136
|
+
return "INFO";
|
|
137
|
+
}
|
|
138
|
+
if (n <= 16) {
|
|
139
|
+
return "WARN";
|
|
140
|
+
}
|
|
141
|
+
if (n <= 20) {
|
|
142
|
+
return "ERROR";
|
|
143
|
+
}
|
|
144
|
+
return "FATAL";
|
|
145
|
+
}
|
|
146
|
+
/** Render a decoded value for a single console line, capped at the body-print limit. */
|
|
147
|
+
function formatScalar(value) {
|
|
148
|
+
if (value === null || value === undefined) {
|
|
149
|
+
return output_1.pc.dim("null");
|
|
150
|
+
}
|
|
151
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
152
|
+
return truncateForConsole(text);
|
|
153
|
+
}
|
|
154
|
+
/** Truncate `text` to BODY_PRINT_LIMIT chars (0 = unlimited) with a footer noting the omission. */
|
|
155
|
+
function truncateForConsole(text) {
|
|
156
|
+
if (BODY_PRINT_LIMIT <= 0 || text.length <= BODY_PRINT_LIMIT) {
|
|
157
|
+
return text;
|
|
158
|
+
}
|
|
159
|
+
const omitted = text.length - BODY_PRINT_LIMIT;
|
|
160
|
+
return `${text.slice(0, BODY_PRINT_LIMIT)}${output_1.pc.dim(`… (+${omitted} chars truncated)`)}`;
|
|
161
|
+
}
|
|
162
|
+
/** Pretty-print JSON when parseable; otherwise return the raw string unchanged. */
|
|
163
|
+
function prettyJsonMaybe(raw) {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return raw;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolve a `body_ref` attribute: read the file Claude Code wrote in
|
|
173
|
+
* `OTEL_LOG_RAW_API_BODIES=file:<dir>` mode and print its (pretty-printed,
|
|
174
|
+
* capped) contents under the event. Read failures degrade to a warning — the
|
|
175
|
+
* collector keeps running and the path is still printed for manual inspection.
|
|
176
|
+
*/
|
|
177
|
+
function printBodyRef(ref) {
|
|
178
|
+
console.log(` ${output_1.pc.dim(`${BODY_REF_KEY}:`)} ${ref}`);
|
|
179
|
+
let raw;
|
|
180
|
+
try {
|
|
181
|
+
raw = (0, fs_1.readFileSync)(ref, "utf-8");
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
185
|
+
console.log(` ${output_1.pc.yellow(`⚠ could not read body file: ${msg}`)}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const rendered = truncateForConsole(prettyJsonMaybe(raw));
|
|
189
|
+
for (const line of rendered.split("\n")) {
|
|
190
|
+
console.log(` ${output_1.pc.dim("│")} ${line}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Console rendering
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/** Print every key/value of an attribute object at the given indent. */
|
|
197
|
+
function printAttributes(attrs, indent) {
|
|
198
|
+
for (const key of Object.keys(attrs)) {
|
|
199
|
+
console.log(`${indent}${output_1.pc.dim(`${key}:`)} ${formatScalar(attrs[key])}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** One-line label for an instrumentation scope (`name@version`). */
|
|
203
|
+
function formatScope(scope) {
|
|
204
|
+
if (!scope || !scope.name) {
|
|
205
|
+
return output_1.pc.dim("(unnamed)");
|
|
206
|
+
}
|
|
207
|
+
return scope.version ? `${scope.name}@${scope.version}` : scope.name;
|
|
208
|
+
}
|
|
209
|
+
/** Print a single log record (header + body + every event attribute) at record indent. */
|
|
210
|
+
function printLogRecord(record) {
|
|
211
|
+
const ts = nanoToIso(record.timeUnixNano ?? record.observedTimeUnixNano);
|
|
212
|
+
const severity = record.severityText ?? severityFromNumber(record.severityNumber);
|
|
213
|
+
const attrs = decodeAttributes(record.attributes ?? []);
|
|
214
|
+
const body = decodeAnyValue(record.body);
|
|
215
|
+
const eventName = extractEventName(record) || "(log record)";
|
|
216
|
+
console.log(` ${output_1.pc.dim(ts)} ${output_1.pc.cyan(severity.padEnd(5))} ${output_1.pc.bold(eventName)}`);
|
|
217
|
+
if (body !== undefined && body !== eventName) {
|
|
218
|
+
console.log(` ${output_1.pc.dim("body:")} ${formatScalar(body)}`);
|
|
219
|
+
}
|
|
220
|
+
for (const key of Object.keys(attrs)) {
|
|
221
|
+
if (key === "event.name") {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (key === BODY_REF_KEY && typeof attrs[key] === "string") {
|
|
225
|
+
printBodyRef(attrs[key]);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
console.log(` ${output_1.pc.dim(`${key}:`)} ${formatScalar(attrs[key])}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Render the OTLP hierarchy resource → scope → record. Each handled record is
|
|
233
|
+
* printed beneath its full resource attribute set and its owning scope, so the
|
|
234
|
+
* surrounding context (service identity, SDK / OS / host info, scope name) is
|
|
235
|
+
* always visible. A resource whose every record is ignored produces no output.
|
|
236
|
+
*/
|
|
237
|
+
function printLogsPayload(payload) {
|
|
238
|
+
for (const rl of payload.resourceLogs ?? []) {
|
|
239
|
+
// Filter to handled events first, grouped by scope.
|
|
240
|
+
const scopes = [];
|
|
241
|
+
let total = 0;
|
|
242
|
+
for (const sl of rl.scopeLogs ?? []) {
|
|
243
|
+
const records = (sl.logRecords ?? []).filter((r) => exports.HANDLED_LOG_EVENTS.has(extractEventName(r)));
|
|
244
|
+
if (records.length > 0) {
|
|
245
|
+
scopes.push({ scope: sl.scope, records });
|
|
246
|
+
total += records.length;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (total === 0) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const resourceAttrs = decodeAttributes(rl.resource?.attributes ?? []);
|
|
253
|
+
const serviceName = String(resourceAttrs["service.name"] ?? "unknown-service");
|
|
254
|
+
console.log(output_1.pc.dim("─".repeat(60)));
|
|
255
|
+
console.log(`${output_1.pc.bold((0, output_1.orange)("OTEL"))} ${output_1.pc.dim("logs from")} ${output_1.pc.bold(serviceName)}`
|
|
256
|
+
+ ` ${output_1.pc.dim(`(${total} record${total === 1 ? "" : "s"})`)}`);
|
|
257
|
+
if (Object.keys(resourceAttrs).length > 0) {
|
|
258
|
+
console.log(` ${output_1.pc.dim("resource:")}`);
|
|
259
|
+
printAttributes(resourceAttrs, " ");
|
|
260
|
+
}
|
|
261
|
+
for (const s of scopes) {
|
|
262
|
+
console.log(` ${output_1.pc.dim("scope:")} ${formatScope(s.scope)}`);
|
|
263
|
+
for (const record of s.records) {
|
|
264
|
+
printLogRecord(record);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Read a string attribute off a log record. */
|
|
270
|
+
function recordStringAttr(record, key) {
|
|
271
|
+
for (const kv of record.attributes ?? []) {
|
|
272
|
+
if (kv && kv.key === key && typeof kv.value?.stringValue === "string") {
|
|
273
|
+
return kv.value.stringValue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
/** Consume-once delete of an `api_response_body` file (v1 derives nothing from it). */
|
|
279
|
+
function deleteResponseBody(record) {
|
|
280
|
+
const ref = recordStringAttr(record, "body_ref");
|
|
281
|
+
if (ref && (0, paths_1.isInsideOTELDir)(ref)) {
|
|
282
|
+
try {
|
|
283
|
+
(0, fs_1.unlinkSync)(ref);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// gone already / race — fine
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Route handled records to the forward pipeline (live daemon path). */
|
|
291
|
+
function routeToSink(payload, sink) {
|
|
292
|
+
for (const rl of payload.resourceLogs ?? []) {
|
|
293
|
+
const resourceAttrs = rl.resource?.attributes ?? [];
|
|
294
|
+
for (const sl of rl.scopeLogs ?? []) {
|
|
295
|
+
for (const record of sl.logRecords ?? []) {
|
|
296
|
+
const name = extractEventName(record);
|
|
297
|
+
if (name === "api_request_body") {
|
|
298
|
+
const meta = (0, extract_1.extractApiRequestBodyMeta)(record, resourceAttrs);
|
|
299
|
+
if (meta) {
|
|
300
|
+
sink.submit(meta);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (name === "api_response_body") {
|
|
304
|
+
deleteResponseBody(record);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Handle one OTLP/HTTP JSON logs request body. With a `sink` (the live daemon),
|
|
312
|
+
* `api_request_body` records are submitted to the forward pipeline and
|
|
313
|
+
* `api_response_body` files are deleted; without one (dev / direct callers),
|
|
314
|
+
* handled records are pretty-printed to the console. Throws on invalid JSON
|
|
315
|
+
* (the collector maps that to HTTP 400).
|
|
316
|
+
*/
|
|
317
|
+
function handleLogs(raw, sink) {
|
|
318
|
+
const payload = JSON.parse(raw.toString("utf-8"));
|
|
319
|
+
try {
|
|
320
|
+
if (sink) {
|
|
321
|
+
routeToSink(payload, sink);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
printLogsPayload(payload);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
329
|
+
output_1.log.error(`otel: failed to handle log records: ${msg}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=log-handler.js.map
|