@hue-run/sdk 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/ai-sdk.js +10 -1
- package/dist/client.d.ts +11 -1
- package/dist/client.js +232 -97
- package/dist/config.d.ts +1 -1
- package/dist/config.js +18 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/privacy.js +38 -20
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +179 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +196 -0
- package/dist/transport.d.ts +6 -0
- package/dist/transport.js +192 -32
- package/dist/types.d.ts +27 -4
- package/package.json +2 -2
package/dist/transport.js
CHANGED
|
@@ -5,7 +5,32 @@ import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSeriali
|
|
|
5
5
|
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
|
|
6
6
|
import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
|
|
7
7
|
import { MAX_BODY_BYTES, validateOptions } from "./config.js";
|
|
8
|
+
import { estimateRecordBytes } from "./safety.js";
|
|
9
|
+
import { snapshotLog, snapshotSpan } from "./snapshot.js";
|
|
8
10
|
import { redactLog, redactSpan } from "./privacy.js";
|
|
11
|
+
function recordData(record, signal) {
|
|
12
|
+
if (signal === "traces") {
|
|
13
|
+
const span = record;
|
|
14
|
+
return {
|
|
15
|
+
name: span.name,
|
|
16
|
+
attributes: span.attributes,
|
|
17
|
+
events: span.events,
|
|
18
|
+
links: span.links,
|
|
19
|
+
status: span.status,
|
|
20
|
+
resource: span.resource.attributes,
|
|
21
|
+
scope: span.instrumentationScope,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const log = record;
|
|
25
|
+
return {
|
|
26
|
+
body: log.body,
|
|
27
|
+
attributes: log.attributes,
|
|
28
|
+
eventName: log.eventName,
|
|
29
|
+
severityText: log.severityText,
|
|
30
|
+
resource: log.resource.attributes,
|
|
31
|
+
scope: log.instrumentationScope,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
9
34
|
export class HueExportError extends Error {
|
|
10
35
|
issues;
|
|
11
36
|
report;
|
|
@@ -28,8 +53,13 @@ export class HueTransport {
|
|
|
28
53
|
accepted = { traces: 0, logs: 0 };
|
|
29
54
|
rejected = { traces: 0, logs: 0 };
|
|
30
55
|
failed = { traces: 0, logs: 0 };
|
|
31
|
-
spans = new
|
|
32
|
-
logs = new
|
|
56
|
+
spans = new Map();
|
|
57
|
+
logs = new Map();
|
|
58
|
+
pendingBytes = 0;
|
|
59
|
+
dropped = { traces: 0, logs: 0 };
|
|
60
|
+
instrumentationFailures = 0;
|
|
61
|
+
diagnosticPending = false;
|
|
62
|
+
lastDiagnosticAt = -Infinity;
|
|
33
63
|
traceExporter;
|
|
34
64
|
logExporter;
|
|
35
65
|
closed = false;
|
|
@@ -40,6 +70,11 @@ export class HueTransport {
|
|
|
40
70
|
Object.defineProperty(this, "options", { enumerable: false });
|
|
41
71
|
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache) => redactSpan(span, this.options, cache));
|
|
42
72
|
this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache) => redactLog(log, this.options, cache));
|
|
73
|
+
if (this.options.enabled === false) {
|
|
74
|
+
this.spanProcessor = { onStart() { }, onEnd() { }, async forceFlush() { }, async shutdown() { } };
|
|
75
|
+
this.logRecordProcessor = { onEmit() { }, async forceFlush() { }, async shutdown() { } };
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
43
78
|
const batching = {
|
|
44
79
|
maxQueueSize: 2048,
|
|
45
80
|
maxExportBatchSize: 128,
|
|
@@ -49,22 +84,49 @@ export class HueTransport {
|
|
|
49
84
|
const spans = new BatchSpanProcessor({ exporter: this.traceExporter, ...batching });
|
|
50
85
|
const logs = new BatchLogRecordProcessor({ exporter: this.logExporter, ...batching });
|
|
51
86
|
this.spanProcessor = {
|
|
52
|
-
onStart: (span, parent) =>
|
|
87
|
+
onStart: (span, parent) => {
|
|
88
|
+
try {
|
|
89
|
+
spans.onStart(span, parent);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
this.instrumentationFailure();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
53
95
|
onEnd: (span) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
let admitted;
|
|
97
|
+
try {
|
|
98
|
+
if (!(span.spanContext().traceFlags & 1))
|
|
99
|
+
return;
|
|
100
|
+
const queued = this.enqueue("traces", span);
|
|
101
|
+
if (!queued)
|
|
102
|
+
return;
|
|
103
|
+
admitted = queued;
|
|
104
|
+
spans.onEnd(admitted);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
if (admitted)
|
|
108
|
+
this.finish("traces", [admitted]);
|
|
109
|
+
this.issue("traces", "invalid", 1, "Telemetry processor could not accept a record");
|
|
110
|
+
}
|
|
59
111
|
},
|
|
60
112
|
forceFlush: () => spans.forceFlush(),
|
|
61
113
|
shutdown: () => spans.shutdown(),
|
|
62
114
|
};
|
|
63
115
|
this.logRecordProcessor = {
|
|
64
116
|
onEmit: (log) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
let admitted;
|
|
118
|
+
try {
|
|
119
|
+
const queued = this.enqueue("logs", log);
|
|
120
|
+
if (!queued)
|
|
121
|
+
return;
|
|
122
|
+
admitted = queued;
|
|
123
|
+
logs.onEmit(queued);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
if (admitted)
|
|
127
|
+
this.finish("logs", [admitted]);
|
|
128
|
+
this.issue("logs", "invalid", 1, "Telemetry processor could not accept a record");
|
|
129
|
+
}
|
|
68
130
|
},
|
|
69
131
|
forceFlush: () => logs.forceFlush(),
|
|
70
132
|
shutdown: () => logs.shutdown(),
|
|
@@ -76,26 +138,40 @@ export class HueTransport {
|
|
|
76
138
|
this.issue(signal, "dropped", 1, this.closed
|
|
77
139
|
? "Telemetry emitted after transport shutdown"
|
|
78
140
|
: "Telemetry queue reached 2048 records");
|
|
79
|
-
return
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const remaining = this.options.maxQueueBytes - this.pendingBytes;
|
|
145
|
+
const snapshot = signal === "traces"
|
|
146
|
+
? snapshotSpan(record, remaining)
|
|
147
|
+
: snapshotLog(record, remaining);
|
|
148
|
+
this.pendingBytes += snapshot.bytes;
|
|
149
|
+
if (signal === "traces")
|
|
150
|
+
this.spans.set(snapshot.record, snapshot.bytes);
|
|
151
|
+
else
|
|
152
|
+
this.logs.set(snapshot.record, snapshot.bytes);
|
|
153
|
+
if (snapshot.unresolvedResource)
|
|
154
|
+
this.issue(signal, "warning", 0, "Unresolved resource attributes omitted from the telemetry snapshot");
|
|
155
|
+
return snapshot.record;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
this.issue(signal, "dropped", 1, "Telemetry snapshot exceeded its byte or complexity budget or contained unsupported data");
|
|
159
|
+
return undefined;
|
|
80
160
|
}
|
|
81
|
-
if (signal === "traces")
|
|
82
|
-
this.spans.add(record);
|
|
83
|
-
else
|
|
84
|
-
this.logs.add(record);
|
|
85
|
-
return true;
|
|
86
161
|
}
|
|
87
162
|
finish(signal, records) {
|
|
88
163
|
for (const record of records) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.logs.delete(record);
|
|
164
|
+
const pending = signal === "traces" ? this.spans : this.logs;
|
|
165
|
+
this.pendingBytes -= pending.get(record) ?? 0;
|
|
166
|
+
pending.delete(record);
|
|
93
167
|
}
|
|
94
168
|
}
|
|
95
169
|
acceptedRecords(signal, count) {
|
|
96
170
|
this.accepted[signal] += count;
|
|
97
171
|
}
|
|
98
172
|
issue(signal, kind, count, message, status) {
|
|
173
|
+
if (kind === "dropped")
|
|
174
|
+
this.dropped[signal] += count;
|
|
99
175
|
if (kind === "rejected")
|
|
100
176
|
this.rejected[signal] += count;
|
|
101
177
|
else if (kind !== "warning")
|
|
@@ -113,13 +189,26 @@ export class HueTransport {
|
|
|
113
189
|
this.issues.push(issue);
|
|
114
190
|
if (this.issues.length > 128)
|
|
115
191
|
this.issues.shift();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
192
|
+
// One diagnostic task at a time, at most once a second. No unbounded promise
|
|
193
|
+
// queue if a user callback never settles; all issues remain in counts/history.
|
|
194
|
+
if (this.options.onExportIssue &&
|
|
195
|
+
!this.diagnosticPending &&
|
|
196
|
+
Date.now() - this.lastDiagnosticAt >= 1000) {
|
|
197
|
+
this.diagnosticPending = true;
|
|
198
|
+
this.lastDiagnosticAt = Date.now();
|
|
199
|
+
void Promise.resolve()
|
|
200
|
+
.then(() => this.options.onExportIssue?.({ ...issue }))
|
|
201
|
+
.then(() => {
|
|
202
|
+
this.diagnosticPending = false;
|
|
203
|
+
}, () => {
|
|
204
|
+
this.diagnosticPending = false;
|
|
205
|
+
});
|
|
121
206
|
}
|
|
122
207
|
}
|
|
208
|
+
instrumentationFailure(signal = "traces") {
|
|
209
|
+
this.instrumentationFailures++;
|
|
210
|
+
this.issue(signal, "invalid", 0, "Telemetry capture or instrumentation failed; application execution was preserved");
|
|
211
|
+
}
|
|
123
212
|
getReport() {
|
|
124
213
|
return {
|
|
125
214
|
acceptedSpans: this.accepted.traces,
|
|
@@ -130,6 +219,10 @@ export class HueTransport {
|
|
|
130
219
|
failedLogs: this.failed.logs,
|
|
131
220
|
pendingSpans: this.spans.size,
|
|
132
221
|
pendingLogs: this.logs.size,
|
|
222
|
+
droppedSpans: this.dropped.traces,
|
|
223
|
+
droppedLogs: this.dropped.logs,
|
|
224
|
+
pendingBytes: this.pendingBytes,
|
|
225
|
+
instrumentationFailures: this.instrumentationFailures,
|
|
133
226
|
};
|
|
134
227
|
}
|
|
135
228
|
getIssues() {
|
|
@@ -208,17 +301,40 @@ class ReportingExporter {
|
|
|
208
301
|
.finally(() => {
|
|
209
302
|
this.transport.finish(this.signal, records);
|
|
210
303
|
this.pending.delete(work);
|
|
211
|
-
})
|
|
304
|
+
})
|
|
305
|
+
.catch(() => { });
|
|
212
306
|
this.pending.add(work);
|
|
213
307
|
}
|
|
214
308
|
async exportRecords(records) {
|
|
215
309
|
const accepted = [];
|
|
216
310
|
const cache = new WeakMap();
|
|
217
311
|
let failed = false;
|
|
312
|
+
let redactedBytes = 0;
|
|
313
|
+
const resourceDeadline = Date.now() + this.transport.options.timeoutMillis;
|
|
218
314
|
for (const record of records) {
|
|
219
315
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
316
|
+
const ready = record.resource.waitForAsyncAttributes?.();
|
|
317
|
+
if (ready) {
|
|
318
|
+
let timer;
|
|
319
|
+
try {
|
|
320
|
+
await Promise.race([
|
|
321
|
+
ready,
|
|
322
|
+
new Promise((_resolve, reject) => {
|
|
323
|
+
timer = setTimeout(() => reject(new Error("Resource deadline exceeded")), Math.max(1, resourceDeadline - Date.now()));
|
|
324
|
+
}),
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const redacted = this.redact(record, cache);
|
|
332
|
+
const bytes = 512 +
|
|
333
|
+
estimateRecordBytes(recordData(redacted, this.signal), this.transport.options.maxQueueBytes - redactedBytes);
|
|
334
|
+
if (redactedBytes + bytes > this.transport.options.maxQueueBytes)
|
|
335
|
+
throw new RangeError("Redacted batch exceeds byte budget");
|
|
336
|
+
redactedBytes += bytes;
|
|
337
|
+
accepted.push(redacted);
|
|
222
338
|
}
|
|
223
339
|
catch {
|
|
224
340
|
failed = true;
|
|
@@ -227,6 +343,15 @@ class ReportingExporter {
|
|
|
227
343
|
}
|
|
228
344
|
let batch = [];
|
|
229
345
|
for (const record of accepted) {
|
|
346
|
+
let recordBytes;
|
|
347
|
+
try {
|
|
348
|
+
recordBytes = this.serializer.serializeRequest([record])?.byteLength ?? Infinity;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
failed = true;
|
|
352
|
+
this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be serialized");
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
230
355
|
const candidate = [...batch, record];
|
|
231
356
|
// Leave room for gzip headers/blocks when otherwise incompressible data is near the wire cap.
|
|
232
357
|
if ((this.serializer.serializeRequest(candidate)?.byteLength ?? 0) <= MAX_BODY_BYTES - 1024) {
|
|
@@ -236,7 +361,7 @@ class ReportingExporter {
|
|
|
236
361
|
if (batch.length && !(await this.send(batch)))
|
|
237
362
|
failed = true;
|
|
238
363
|
batch = [];
|
|
239
|
-
if (
|
|
364
|
+
if (recordBytes > MAX_BODY_BYTES - 1024) {
|
|
240
365
|
failed = true;
|
|
241
366
|
this.transport.issue(this.signal, "invalid", 1, "Telemetry record exceeds the 1 MiB request limit");
|
|
242
367
|
}
|
|
@@ -252,9 +377,17 @@ class ReportingExporter {
|
|
|
252
377
|
const options = this.transport.options;
|
|
253
378
|
let rejected = 0;
|
|
254
379
|
let validResponse = true;
|
|
380
|
+
let receivedResponse = false;
|
|
381
|
+
let expired = false;
|
|
382
|
+
const agents = new Set();
|
|
383
|
+
const deadline = Date.now() + options.timeoutMillis;
|
|
384
|
+
let timer;
|
|
255
385
|
const serializer = {
|
|
256
386
|
serializeRequest: (data) => this.serializer.serializeRequest(data),
|
|
257
387
|
deserializeResponse: (bytes) => {
|
|
388
|
+
if (expired)
|
|
389
|
+
return {};
|
|
390
|
+
receivedResponse = true;
|
|
258
391
|
try {
|
|
259
392
|
const response = this.serializer.deserializeResponse(bytes);
|
|
260
393
|
const partial = response.partialSuccess;
|
|
@@ -283,11 +416,33 @@ class ReportingExporter {
|
|
|
283
416
|
timeoutMillis: options.timeoutMillis,
|
|
284
417
|
concurrencyLimit: 1,
|
|
285
418
|
compression: CompressionAlgorithm.GZIP,
|
|
419
|
+
httpAgentOptions: async (protocol) => {
|
|
420
|
+
if (expired || Date.now() >= deadline)
|
|
421
|
+
throw new Error("Hue export deadline exceeded");
|
|
422
|
+
const { Agent } = await import(protocol === "https:" ? "node:https" : "node:http");
|
|
423
|
+
const agent = new Agent({ keepAlive: false });
|
|
424
|
+
agents.add(agent);
|
|
425
|
+
if (expired)
|
|
426
|
+
agent.destroy();
|
|
427
|
+
return agent;
|
|
428
|
+
},
|
|
286
429
|
}, this.signal === "traces" ? "TRACES" : "LOGS", `v1/${this.signal}`, { "Content-Type": "application/x-protobuf" }), serializer, this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, undefined);
|
|
287
430
|
const exporter = new OTLPExporterBase(delegate);
|
|
288
431
|
try {
|
|
289
|
-
const result = await new Promise((resolve) =>
|
|
432
|
+
const result = await new Promise((resolve) => {
|
|
433
|
+
timer = setTimeout(() => {
|
|
434
|
+
expired = true;
|
|
435
|
+
for (const agent of agents)
|
|
436
|
+
agent.destroy();
|
|
437
|
+
resolve({ code: ExportResultCode.FAILED });
|
|
438
|
+
}, Math.max(1, deadline - Date.now()));
|
|
439
|
+
exporter.export(records, resolve);
|
|
440
|
+
});
|
|
290
441
|
if (result.code === ExportResultCode.SUCCESS) {
|
|
442
|
+
if (!receivedResponse) {
|
|
443
|
+
this.transport.issue(this.signal, "failed", records.length, "Hue response ended without a complete OTLP acknowledgement; acceptance is uncertain");
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
291
446
|
if (validResponse)
|
|
292
447
|
this.transport.acceptedRecords(this.signal, records.length - rejected);
|
|
293
448
|
return validResponse;
|
|
@@ -305,7 +460,12 @@ class ReportingExporter {
|
|
|
305
460
|
return false;
|
|
306
461
|
}
|
|
307
462
|
finally {
|
|
308
|
-
|
|
463
|
+
clearTimeout(timer);
|
|
464
|
+
for (const agent of agents)
|
|
465
|
+
agent.destroy();
|
|
466
|
+
// Delegate cleanup cannot extend the hard request wait. Sockets are closed
|
|
467
|
+
// and the cleanup promise is always observed, even after a caller timeout.
|
|
468
|
+
void exporter.shutdown().catch(() => { });
|
|
309
469
|
}
|
|
310
470
|
}
|
|
311
471
|
async forceFlush() {
|
package/dist/types.d.ts
CHANGED
|
@@ -4,17 +4,26 @@ export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
|
4
4
|
[key: string]: JsonValue;
|
|
5
5
|
};
|
|
6
6
|
export type Signal = "traces" | "logs";
|
|
7
|
-
|
|
8
|
-
apiKey: string;
|
|
9
|
-
serviceName: string;
|
|
7
|
+
interface SharedHueOptions {
|
|
10
8
|
captureContent: boolean;
|
|
11
9
|
baseUrl?: string;
|
|
12
10
|
serviceVersion?: string;
|
|
13
11
|
/** Runs on string values before Hue export, including custom attribute values. */
|
|
14
12
|
redact?: (value: string, path: string) => string;
|
|
15
|
-
onExportIssue?: (issue: ExportIssue) => void
|
|
13
|
+
onExportIssue?: (issue: ExportIssue) => void | Promise<void>;
|
|
16
14
|
timeoutMillis?: number;
|
|
15
|
+
/** Aggregate estimated retained telemetry bytes across both signals, including in-flight work. Default 8 MiB. */
|
|
16
|
+
maxQueueBytes?: number;
|
|
17
17
|
}
|
|
18
|
+
export type HueOptions = SharedHueOptions & ({
|
|
19
|
+
enabled?: true;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
serviceName: string;
|
|
22
|
+
} | {
|
|
23
|
+
enabled: false;
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
serviceName?: string;
|
|
26
|
+
});
|
|
18
27
|
export interface ExportIssue {
|
|
19
28
|
sequence: number;
|
|
20
29
|
signal: Signal;
|
|
@@ -32,6 +41,19 @@ export interface ExportReport {
|
|
|
32
41
|
failedLogs: number;
|
|
33
42
|
pendingSpans: number;
|
|
34
43
|
pendingLogs: number;
|
|
44
|
+
droppedSpans: number;
|
|
45
|
+
droppedLogs: number;
|
|
46
|
+
pendingBytes: number;
|
|
47
|
+
instrumentationFailures: number;
|
|
48
|
+
}
|
|
49
|
+
export interface SafeLifecycleOptions {
|
|
50
|
+
/** Caller wait budget, 1–60000 ms. Default 1000. Does not cancel borrowed providers. */
|
|
51
|
+
timeoutMillis?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface SafeLifecycleResult {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
timedOut: boolean;
|
|
56
|
+
report: ExportReport;
|
|
35
57
|
}
|
|
36
58
|
export interface SpanOptions {
|
|
37
59
|
kind?: SpanKind;
|
|
@@ -82,3 +104,4 @@ export interface TraceVerification {
|
|
|
82
104
|
verified: boolean;
|
|
83
105
|
receipt: TraceReceipt | null;
|
|
84
106
|
}
|
|
107
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hue-run/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@ai-sdk/otel": "^1.0.99",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
|
-
"ai": "^7.0.99"
|
|
71
|
+
"ai": "^6.0.0 || ^7.0.99"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@ai-sdk/otel": {
|