@hue-run/sdk 0.18.1 → 0.19.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/CLI.md +16 -12
- package/EVALUATIONS.md +1 -1
- package/README.md +135 -74
- package/dist/attachment.d.ts +51 -7
- package/dist/attachment.js +141 -33
- package/dist/blobs.d.ts +136 -40
- package/dist/blobs.js +535 -225
- package/dist/cli/langfuse.d.ts +21 -5
- package/dist/cli/langfuse.js +123 -16
- package/dist/client.d.ts +20 -11
- package/dist/client.js +81 -41
- package/dist/inline-files.d.ts +5 -4
- package/dist/inline-files.js +6 -5
- package/dist/json-edits.js +26 -7
- package/dist/large-media.d.ts +118 -0
- package/dist/large-media.js +422 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +19 -1
- package/dist/transport.d.ts +39 -7
- package/dist/transport.js +262 -75
- package/dist/types.d.ts +15 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/transport.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { gzip } from "node:zlib";
|
|
2
3
|
import { ExportResultCode } from "@opentelemetry/core";
|
|
3
4
|
import { createOtlpNetworkExportDelegate, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
|
|
@@ -6,7 +7,8 @@ import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSeriali
|
|
|
6
7
|
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
|
|
7
8
|
import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
|
|
8
9
|
import { isInsecureOrigin, REDACTION_CONTEXT_UNITS, validateOptions } from "./config.js";
|
|
9
|
-
import { BLOBS_KEY, MAX_BLOB_ENTRIES, blobEntry, countFallback, BlobUploader, FALLBACK_MESSAGES, fallbackAttributes, fallbackRecord, MAX_HELD_BLOB_BYTES, newTally, mediaKey, offloadAttributes, offloadRecord, uploadBudgetMillis, } from "./blobs.js";
|
|
10
|
+
import { BLOBS_KEY, MAX_BLOB_ENTRIES, blobEntry, countFallback, BlobUploader, FALLBACK_MESSAGES, fallbackAttributes, fallbackRecord, heldBytesOf, LATE_FALLBACK_MESSAGES, MAX_HELD_BLOB_BYTES, mergeWhenSettled, newTally, mediaKey, offloadAttributes, offloadRecord, sizeOf, uploadBudgetMillis, } from "./blobs.js";
|
|
11
|
+
import { isByteSource } from "./large-media.js";
|
|
10
12
|
import { advertisedLimits, BATCH_TARGET_BYTES, DEFAULT_RECEIVER_LIMITS, fitsWithoutCompression, } from "./limits.js";
|
|
11
13
|
import { announcesLiveSpan, LIVE_SPAN_INTERVAL_MILLIS, MAX_LIVE_SPANS, pendingPlaceholder, PLACEHOLDERS_HEADER, withoutPlaceholderMarkers, } from "./live-spans.js";
|
|
12
14
|
import { estimateRecordBytes, spanSource } from "./safety.js";
|
|
@@ -108,9 +110,11 @@ function mediaEntry(file) {
|
|
|
108
110
|
* them, reported like any refused request.
|
|
109
111
|
*/
|
|
110
112
|
export const MAX_RATE_LIMIT_HOLD_MILLIS = 60_000;
|
|
111
|
-
/** How long one export may take with requests of `timeoutMillis` each: its
|
|
113
|
+
/** How long one export may take with requests of `timeoutMillis` each: hashing its values over
|
|
112
114
|
* the inline limit, its requests, and a hold for a rate-limited receiver on top. Processors and
|
|
113
|
-
* providers wait this long for a drain.
|
|
115
|
+
* providers wait this long for a drain. A drain also waits for the uploads its exports queued,
|
|
116
|
+
* which run beside the requests and end within six timeouts and a minute of being queued, so they
|
|
117
|
+
* fit within the same budget. */
|
|
114
118
|
export function exportBudgetMillis(timeoutMillis) {
|
|
115
119
|
return uploadBudgetMillis(timeoutMillis) + timeoutMillis * 16 + 1000 + MAX_RATE_LIMIT_HOLD_MILLIS;
|
|
116
120
|
}
|
|
@@ -194,7 +198,11 @@ export class HueTransport {
|
|
|
194
198
|
logExporter;
|
|
195
199
|
closed = false;
|
|
196
200
|
shutdownPromise;
|
|
197
|
-
|
|
201
|
+
/** The latest flush's send of its records; the next flush's send starts after it. */
|
|
202
|
+
sendPromise;
|
|
203
|
+
/** Set while a flush sends its records: the processors leave the uploads to that flush, which
|
|
204
|
+
* waits for them after its send, so no flush's send waits behind another's uploads. */
|
|
205
|
+
sendingRecords = new AsyncLocalStorage();
|
|
198
206
|
// Open spans not yet announced. Each is considered once, at the next tick after it starts.
|
|
199
207
|
live = new Set();
|
|
200
208
|
liveTimer;
|
|
@@ -218,11 +226,17 @@ export class HueTransport {
|
|
|
218
226
|
admissionDigests = new WeakMap();
|
|
219
227
|
// The span's own messages each admitted span inspected, or left to export to inspect.
|
|
220
228
|
admissionInspected = new WeakMap();
|
|
229
|
+
// The span's own values each admitted span held whole past the inspection bounds.
|
|
230
|
+
admissionLarge = new WeakMap();
|
|
221
231
|
/** Uploads values over the inline limit apart from their spans; absent when disabled. */
|
|
222
232
|
uploader;
|
|
223
233
|
/** Bytes of values over the inline limit queued spans hold whole for upload, and each span's. */
|
|
224
234
|
heldBytes = 0;
|
|
225
235
|
held = new WeakMap();
|
|
236
|
+
/** Bytes of each record's charge (its own held values and its staged files) that its queued
|
|
237
|
+
* uploads took over before the record was finished: charged to the uploads alone until they
|
|
238
|
+
* settle or the record is finished. */
|
|
239
|
+
heldByUploads = new WeakMap();
|
|
226
240
|
mediaMemo = new MediaDigestMemo();
|
|
227
241
|
// Context identity also matches safe API wrappers to the provider's ended span. Abandoned
|
|
228
242
|
// spans retain no span objects, and share the 2048-span / 128 MiB holding bounds.
|
|
@@ -239,15 +253,23 @@ export class HueTransport {
|
|
|
239
253
|
});
|
|
240
254
|
uploaded = 0;
|
|
241
255
|
uploadFallbacks = 0;
|
|
256
|
+
/** Each export's queued uploads, until they all settle and are reported. */
|
|
257
|
+
uploadReports = new Set();
|
|
258
|
+
/** Settles once every upload report registered so far has: each drain awaits this one promise,
|
|
259
|
+
* so a drain costs the same however many uploads are pending. */
|
|
260
|
+
uploadsSettled = Promise.resolve();
|
|
242
261
|
constructor(options) {
|
|
243
262
|
this.options = validateOptions(options);
|
|
244
263
|
Object.defineProperty(this, "options", { enumerable: false });
|
|
245
264
|
this.liveSpans = this.options.enabled !== false && this.options.liveSpans;
|
|
246
265
|
this.announcesLiveSpans = this.liveSpans;
|
|
247
266
|
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache, offload) => {
|
|
248
|
-
// Export inspects only the messages admission inspected within the record's allowance
|
|
249
|
-
|
|
267
|
+
// Export inspects only the messages admission inspected within the record's allowance,
|
|
268
|
+
// and reads in place the values it held whole past the inspection bounds.
|
|
269
|
+
if (offload) {
|
|
250
270
|
offload.inspected = this.admissionInspected.get(span);
|
|
271
|
+
offload.large = this.admissionLarge.get(span);
|
|
272
|
+
}
|
|
251
273
|
return redactSpan(span, this.options, cache, {
|
|
252
274
|
valueBytes: this.limits.valueBytes,
|
|
253
275
|
cut: this.admissionCuts.get(span),
|
|
@@ -308,7 +330,7 @@ export class HueTransport {
|
|
|
308
330
|
if (!queued) {
|
|
309
331
|
const tally = newTally();
|
|
310
332
|
for (const file of this.mediaAttachments(span))
|
|
311
|
-
if (file.bytes
|
|
333
|
+
if (sizeOf(file.bytes))
|
|
312
334
|
countFallback(tally, "budget", span.spanContext().traceId);
|
|
313
335
|
this.reportOffload(tally);
|
|
314
336
|
this.releaseMedia(span);
|
|
@@ -327,10 +349,25 @@ export class HueTransport {
|
|
|
327
349
|
this.issue("traces", "invalid", 1, "Telemetry processor could not accept a record", undefined, traceIdsOf([span]));
|
|
328
350
|
}
|
|
329
351
|
},
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
// The spans are sent first, then the uploads queued so far are waited for, unless a
|
|
353
|
+
// flush sends them: that flush waits for the uploads after its send.
|
|
354
|
+
forceFlush: async () => {
|
|
355
|
+
try {
|
|
356
|
+
await spans.forceFlush();
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
if (!this.sendingRecords.getStore())
|
|
360
|
+
await this.drainUploads();
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
shutdown: async () => {
|
|
332
364
|
this.stopLiveSpans();
|
|
333
|
-
|
|
365
|
+
try {
|
|
366
|
+
await spans.shutdown();
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
await this.drainUploads();
|
|
370
|
+
}
|
|
334
371
|
},
|
|
335
372
|
};
|
|
336
373
|
this.logRecordProcessor = {
|
|
@@ -361,8 +398,23 @@ export class HueTransport {
|
|
|
361
398
|
this.issue("logs", "invalid", 1, "Telemetry processor could not accept a record", undefined, traceIdsOf([log]));
|
|
362
399
|
}
|
|
363
400
|
},
|
|
364
|
-
forceFlush: () =>
|
|
365
|
-
|
|
401
|
+
forceFlush: async () => {
|
|
402
|
+
try {
|
|
403
|
+
await logs.forceFlush();
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (!this.sendingRecords.getStore())
|
|
407
|
+
await this.drainUploads();
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
shutdown: async () => {
|
|
411
|
+
try {
|
|
412
|
+
await logs.shutdown();
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await this.drainUploads();
|
|
416
|
+
}
|
|
417
|
+
},
|
|
366
418
|
};
|
|
367
419
|
}
|
|
368
420
|
/**
|
|
@@ -394,8 +446,8 @@ export class HueTransport {
|
|
|
394
446
|
const hold = !advisory && this.holdsValues()
|
|
395
447
|
? {
|
|
396
448
|
valueBytes: this.limits.valueBytes,
|
|
397
|
-
budget:
|
|
398
|
-
first: this.heldBytes === 0,
|
|
449
|
+
budget: this.heldRoom(),
|
|
450
|
+
first: this.heldBytes === 0 && !this.uploader?.pendingBytes(),
|
|
399
451
|
}
|
|
400
452
|
: undefined;
|
|
401
453
|
const snapshot = signal === "traces"
|
|
@@ -407,6 +459,8 @@ export class HueTransport {
|
|
|
407
459
|
this.admissionDigests.set(snapshot.record, snapshot.digested);
|
|
408
460
|
if (signal === "traces")
|
|
409
461
|
this.admissionInspected.set(snapshot.record, snapshot.inspected);
|
|
462
|
+
if (snapshot.large.size)
|
|
463
|
+
this.admissionLarge.set(snapshot.record, snapshot.large);
|
|
410
464
|
if (snapshot.held) {
|
|
411
465
|
this.heldBytes += snapshot.held;
|
|
412
466
|
this.held.set(snapshot.record, snapshot.held);
|
|
@@ -526,6 +580,12 @@ export class HueTransport {
|
|
|
526
580
|
this.heldBytes -= held;
|
|
527
581
|
this.held.delete(record);
|
|
528
582
|
}
|
|
583
|
+
// Released whole above: the part its uploads took over stays charged to them.
|
|
584
|
+
const taken = this.heldByUploads.get(record);
|
|
585
|
+
if (taken) {
|
|
586
|
+
this.heldBytes += taken;
|
|
587
|
+
this.heldByUploads.delete(record);
|
|
588
|
+
}
|
|
529
589
|
}
|
|
530
590
|
}
|
|
531
591
|
/** @internal The same bounded digest memo is used by explicit and inline media. */
|
|
@@ -534,7 +594,7 @@ export class HueTransport {
|
|
|
534
594
|
}
|
|
535
595
|
/** @internal Retain a defensive copy within the existing held-value budget. */
|
|
536
596
|
stageMedia(span, file) {
|
|
537
|
-
if (this.closed || !this.offloadsValues() || file
|
|
597
|
+
if (this.closed || !this.offloadsValues() || (file && sizeOf(file.bytes) === 0))
|
|
538
598
|
return true;
|
|
539
599
|
if (!file) {
|
|
540
600
|
const tally = newTally();
|
|
@@ -552,16 +612,18 @@ export class HueTransport {
|
|
|
552
612
|
const files = this.media.get(identity) ?? [];
|
|
553
613
|
if (files.length >= 64 ||
|
|
554
614
|
(!this.media.has(identity) && this.media.size >= 2048) ||
|
|
555
|
-
|
|
615
|
+
heldBytesOf(file.bytes) > this.heldRoom()) {
|
|
556
616
|
this.uploadFallbacks++;
|
|
557
617
|
this.issue("traces", "warning", 1, "File bytes did not fit the SDK's upload budget; their hue.file metadata was recorded", undefined, [span.spanContext().traceId]);
|
|
558
618
|
return false;
|
|
559
619
|
}
|
|
560
|
-
|
|
620
|
+
// A file read from disk as it is uploaded holds no bytes; bytes are copied, so a later change
|
|
621
|
+
// to the caller's buffer cannot alter what was recorded.
|
|
622
|
+
const bytes = isByteSource(file.bytes) ? file.bytes : Uint8Array.from(file.bytes);
|
|
561
623
|
files.push({ ...file, bytes });
|
|
562
624
|
this.media.set(identity, files);
|
|
563
625
|
this.ownMedia(span, identity);
|
|
564
|
-
this.heldBytes += bytes
|
|
626
|
+
this.heldBytes += heldBytesOf(bytes);
|
|
565
627
|
return true;
|
|
566
628
|
}
|
|
567
629
|
/**
|
|
@@ -579,7 +641,7 @@ export class HueTransport {
|
|
|
579
641
|
const kept = previous.filter((file) => {
|
|
580
642
|
if (file.attribute !== attribute)
|
|
581
643
|
return true;
|
|
582
|
-
this.heldBytes -= file.bytes
|
|
644
|
+
this.heldBytes -= heldBytesOf(file.bytes);
|
|
583
645
|
return false;
|
|
584
646
|
});
|
|
585
647
|
if (kept.length)
|
|
@@ -589,7 +651,7 @@ export class HueTransport {
|
|
|
589
651
|
}
|
|
590
652
|
const tally = newTally();
|
|
591
653
|
for (const file of files) {
|
|
592
|
-
if (!file.bytes
|
|
654
|
+
if (!sizeOf(file.bytes))
|
|
593
655
|
continue;
|
|
594
656
|
const refused = this.uploader?.refusal();
|
|
595
657
|
if (!this.holdsValues() || refused) {
|
|
@@ -600,14 +662,14 @@ export class HueTransport {
|
|
|
600
662
|
const staged = this.media.get(identity) ?? [];
|
|
601
663
|
if (staged.length >= MAX_BLOB_ENTRIES ||
|
|
602
664
|
(!this.media.has(identity) && this.media.size >= 2048) ||
|
|
603
|
-
|
|
665
|
+
heldBytesOf(file.bytes) > this.heldRoom()) {
|
|
604
666
|
countFallback(tally, "budget", traceId);
|
|
605
667
|
continue;
|
|
606
668
|
}
|
|
607
669
|
// Attachment bytes are the attachment's own copy: held as they are.
|
|
608
670
|
staged.push({ ...file, attribute });
|
|
609
671
|
this.media.set(identity, staged);
|
|
610
|
-
this.heldBytes += file.bytes
|
|
672
|
+
this.heldBytes += heldBytesOf(file.bytes);
|
|
611
673
|
this.ownMedia(span, identity);
|
|
612
674
|
}
|
|
613
675
|
if (tally.fallbacks.size)
|
|
@@ -624,7 +686,7 @@ export class HueTransport {
|
|
|
624
686
|
const tally = newTally();
|
|
625
687
|
const staged = [];
|
|
626
688
|
for (const file of files) {
|
|
627
|
-
if (!file.bytes
|
|
689
|
+
if (!sizeOf(file.bytes))
|
|
628
690
|
continue;
|
|
629
691
|
const refused = this.uploader?.refusal();
|
|
630
692
|
if (!this.holdsValues() || refused)
|
|
@@ -651,8 +713,8 @@ export class HueTransport {
|
|
|
651
713
|
if (!pending)
|
|
652
714
|
return;
|
|
653
715
|
this.logParts.delete(body);
|
|
654
|
-
const size = pending.files.reduce((total, file) => total + file.bytes
|
|
655
|
-
if (
|
|
716
|
+
const size = pending.files.reduce((total, file) => total + heldBytesOf(file.bytes), 0);
|
|
717
|
+
if (size > this.heldRoom()) {
|
|
656
718
|
const tally = newTally();
|
|
657
719
|
for (const _file of pending.files)
|
|
658
720
|
countFallback(tally, "budget", pending.traceId);
|
|
@@ -663,6 +725,44 @@ export class HueTransport {
|
|
|
663
725
|
this.heldBytes += size;
|
|
664
726
|
this.held.set(queued, (this.held.get(queued) ?? 0) + size);
|
|
665
727
|
}
|
|
728
|
+
/** What the held-value budget has room for: values queued records hold and bytes unsettled
|
|
729
|
+
* uploads hold share it. */
|
|
730
|
+
heldRoom() {
|
|
731
|
+
return Math.max(0, MAX_HELD_BLOB_BYTES - this.heldBytes - (this.uploader?.pendingBytes() ?? 0));
|
|
732
|
+
}
|
|
733
|
+
/** An upload `record`'s export queued, as it is queued: flushes and the processors wait for it
|
|
734
|
+
* from now on, and the `shared` bytes it holds of the record's own are charged to the upload
|
|
735
|
+
* alone, so the held-value budget never counts them twice while the export goes on. A charge
|
|
736
|
+
* `released` before the record is finished is handed back to the record, which still holds
|
|
737
|
+
* them. */
|
|
738
|
+
uploadQueued(record, settled, shared, released) {
|
|
739
|
+
this.awaitUploads(settled);
|
|
740
|
+
const identity = this.mediaIdentity(record);
|
|
741
|
+
const staged = identity
|
|
742
|
+
? (this.media.get(identity) ?? []).reduce((total, file) => total + heldBytesOf(file.bytes), 0)
|
|
743
|
+
: 0;
|
|
744
|
+
const taken = this.heldByUploads.get(record) ?? 0;
|
|
745
|
+
const moved = Math.min(shared, (this.held.get(record) ?? 0) + staged - taken);
|
|
746
|
+
if (moved <= 0)
|
|
747
|
+
return;
|
|
748
|
+
this.heldBytes -= moved;
|
|
749
|
+
this.heldByUploads.set(record, taken + moved);
|
|
750
|
+
// Held weakly: a pending upload never keeps its finished record alive.
|
|
751
|
+
const queued = new WeakRef(record);
|
|
752
|
+
const handBack = () => {
|
|
753
|
+
const owner = queued.deref();
|
|
754
|
+
const still = owner && this.heldByUploads.get(owner);
|
|
755
|
+
if (!owner || !still)
|
|
756
|
+
return;
|
|
757
|
+
const back = Math.min(moved, still);
|
|
758
|
+
this.heldBytes += back;
|
|
759
|
+
if (still > back)
|
|
760
|
+
this.heldByUploads.set(owner, still - back);
|
|
761
|
+
else
|
|
762
|
+
this.heldByUploads.delete(owner);
|
|
763
|
+
};
|
|
764
|
+
void released.then(handBack, handBack);
|
|
765
|
+
}
|
|
666
766
|
/** Registers a span whose staged files are released if it is never exported. */
|
|
667
767
|
ownMedia(span, identity) {
|
|
668
768
|
if (this.mediaOwners.has(identity))
|
|
@@ -714,7 +814,7 @@ export class HueTransport {
|
|
|
714
814
|
if (!files)
|
|
715
815
|
return;
|
|
716
816
|
for (const file of files)
|
|
717
|
-
this.heldBytes -= file.bytes
|
|
817
|
+
this.heldBytes -= heldBytesOf(file.bytes);
|
|
718
818
|
this.media.delete(identity);
|
|
719
819
|
}
|
|
720
820
|
sweepMedia() {
|
|
@@ -753,11 +853,19 @@ export class HueTransport {
|
|
|
753
853
|
return this.offloadsValues() && this.uploader?.available() === true;
|
|
754
854
|
}
|
|
755
855
|
/** @internal Exporter callback: a redacted span with the values its redaction left to the
|
|
756
|
-
* upload step placed:
|
|
757
|
-
|
|
856
|
+
* upload step placed: listed under `hue.blobs` with their uploads queued (each added to `late`
|
|
857
|
+
* once it settles, and charged in place of the `queued` record it came from), or cut as before.
|
|
858
|
+
* Never rejects. */
|
|
859
|
+
async offloadSpan(span, collector, upload, tally, late, queued) {
|
|
758
860
|
// A record without a readable context cannot name its trace: its values are cut.
|
|
759
861
|
const traceId = recordTraceId(span);
|
|
760
862
|
const valueBytes = this.limits.valueBytes;
|
|
863
|
+
// A record placed as a fallback counts its values there: its queued uploads are not added.
|
|
864
|
+
const own = {
|
|
865
|
+
tally: newTally(),
|
|
866
|
+
settled: [],
|
|
867
|
+
queued: (settled, shared, released) => this.uploadQueued(queued, settled, shared, released),
|
|
868
|
+
};
|
|
761
869
|
try {
|
|
762
870
|
if (!this.uploader)
|
|
763
871
|
throw new Error("Uploads are unavailable");
|
|
@@ -768,7 +876,10 @@ export class HueTransport {
|
|
|
768
876
|
upload,
|
|
769
877
|
tally,
|
|
770
878
|
memo: this.mediaMemo,
|
|
879
|
+
background: own,
|
|
771
880
|
});
|
|
881
|
+
if (own.settled.length)
|
|
882
|
+
late.settled.push(mergeWhenSettled(late.tally, own.tally, own.settled));
|
|
772
883
|
return { ...span, attributes: attributes };
|
|
773
884
|
}
|
|
774
885
|
catch {
|
|
@@ -779,10 +890,11 @@ export class HueTransport {
|
|
|
779
890
|
}
|
|
780
891
|
}
|
|
781
892
|
/** @internal Exporter callback: a redacted log record with the media its redaction left to
|
|
782
|
-
* the upload step placed: its attributes' and its body's files
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
|
|
893
|
+
* the upload step placed: its attributes' and its body's files listed under its own `hue.blobs`
|
|
894
|
+
* (keys `<attribute>#<pointer>`, `body#<pointer>`) with their uploads queued under the log
|
|
895
|
+
* record's trace (charged in place of the `queued` record); a file refused keeps its bytes
|
|
896
|
+
* inline, and the record is cut as before. Never rejects. */
|
|
897
|
+
async offloadLog(log, collector, upload, tally, late, queued) {
|
|
786
898
|
const traceId = log.spanContext?.traceId ?? "";
|
|
787
899
|
const valueBytes = this.limits.valueBytes;
|
|
788
900
|
const placed = (result) => ({
|
|
@@ -790,29 +902,39 @@ export class HueTransport {
|
|
|
790
902
|
attributes: result.attributes,
|
|
791
903
|
...(collector.body ? { body: result.body } : {}),
|
|
792
904
|
});
|
|
905
|
+
const own = {
|
|
906
|
+
tally: newTally(),
|
|
907
|
+
settled: [],
|
|
908
|
+
queued: (settled, shared, released) => this.uploadQueued(queued, settled, shared, released),
|
|
909
|
+
};
|
|
793
910
|
try {
|
|
794
911
|
if (!this.uploader)
|
|
795
912
|
throw new Error("Uploads are unavailable");
|
|
796
|
-
|
|
913
|
+
const result = await offloadRecord(log.attributes, collector, {
|
|
797
914
|
uploader: this.uploader,
|
|
798
915
|
traceId,
|
|
799
916
|
valueBytes,
|
|
800
917
|
upload,
|
|
801
918
|
tally,
|
|
802
919
|
memo: this.mediaMemo,
|
|
803
|
-
|
|
920
|
+
background: own,
|
|
921
|
+
});
|
|
922
|
+
if (own.settled.length)
|
|
923
|
+
late.settled.push(mergeWhenSettled(late.tally, own.tally, own.settled));
|
|
924
|
+
return placed(result);
|
|
804
925
|
}
|
|
805
926
|
catch {
|
|
806
927
|
return placed(fallbackRecord(log.attributes, collector, valueBytes, tally, traceId, this.mediaMemo));
|
|
807
928
|
}
|
|
808
929
|
}
|
|
809
930
|
/** @internal Exporter callback: counts an export's uploads and reports each kind of fallback
|
|
810
|
-
* once, as a warning naming its traces: the values were exported
|
|
811
|
-
|
|
931
|
+
* once, as a warning naming its traces: the values were exported cut as before, or, `late`,
|
|
932
|
+
* their uploads failed after their records listed them. */
|
|
933
|
+
reportOffload(tally, signal = "traces", late = false) {
|
|
812
934
|
this.uploaded += tally.uploaded;
|
|
813
935
|
for (const [reason, { count, traceIds }] of tally.fallbacks) {
|
|
814
936
|
this.uploadFallbacks += count;
|
|
815
|
-
this.issue(signal, "warning", count, FALLBACK_MESSAGES[reason], undefined, traceIds.size && traceIds.size <= MAX_ISSUE_TRACE_IDS ? [...traceIds] : undefined);
|
|
937
|
+
this.issue(signal, "warning", count, (late ? LATE_FALLBACK_MESSAGES : FALLBACK_MESSAGES)[reason], undefined, traceIds.size && traceIds.size <= MAX_ISSUE_TRACE_IDS ? [...traceIds] : undefined);
|
|
816
938
|
}
|
|
817
939
|
}
|
|
818
940
|
/** @internal Exporter callback: counts records the collector acknowledged. */
|
|
@@ -880,8 +1002,9 @@ export class HueTransport {
|
|
|
880
1002
|
}
|
|
881
1003
|
/** @internal Ends every rate-limit hold now and refuses new ones, once a shutdown's caller has
|
|
882
1004
|
* stopped waiting: the held records are reported lost to the rate limit, and no timer of theirs
|
|
883
|
-
* keeps the process running for the receiver's `Retry-After`.
|
|
884
|
-
* other starts: their
|
|
1005
|
+
* keeps the process running for the receiver's `Retry-After`. Queued and in-flight uploads end
|
|
1006
|
+
* too, and no other starts: their records' entries stay and the uploads count as failed, and
|
|
1007
|
+
* values not yet exported are exported cut, as before uploads existed. */
|
|
885
1008
|
releaseRateLimitHolds() {
|
|
886
1009
|
this.holdsReleased = true;
|
|
887
1010
|
for (const release of [...this.holds])
|
|
@@ -1005,27 +1128,67 @@ export class HueTransport {
|
|
|
1005
1128
|
this.endedSpans.delete(this.endedSpans.keys().next().value);
|
|
1006
1129
|
}
|
|
1007
1130
|
/**
|
|
1008
|
-
* Waits for the processors' and exporters' in-flight work
|
|
1131
|
+
* Waits for the processors' and exporters' in-flight work, then for the uploads their exports
|
|
1132
|
+
* queued; drain the providers first. Only the sends are serialized: a flush sends its records
|
|
1133
|
+
* once an earlier flush sent its own, never after that flush's uploads.
|
|
1009
1134
|
*
|
|
1010
1135
|
* @throws HueExportError when a new non-warning issue was recorded since the previous observation.
|
|
1011
1136
|
*/
|
|
1012
1137
|
flush() {
|
|
1138
|
+
return this.flushRecords().report;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* @internal A flush in two steps: its send settles `sent`, then it waits on its own for the
|
|
1142
|
+
* uploads queued so far and settles `report` as {@link flush} does. `providers` (the client's
|
|
1143
|
+
* provider flush) runs first, before the flush joins the transport's sends, so a hung borrowed
|
|
1144
|
+
* provider never holds up the transport's own flushes. `sent` waits for it, though, and the
|
|
1145
|
+
* client starts each flush once the previous one's `sent` settles: it holds up the client's
|
|
1146
|
+
* later flushes.
|
|
1147
|
+
*/
|
|
1148
|
+
flushRecords(providers) {
|
|
1013
1149
|
const from = this.observedSequence;
|
|
1014
|
-
const
|
|
1015
|
-
this.
|
|
1150
|
+
const join = () => this.sendRecords();
|
|
1151
|
+
const sent = providers ? this.sendingRecords.run(true, providers).then(join, join) : join();
|
|
1152
|
+
const report = sent.then(async (processors) => {
|
|
1153
|
+
// Exports the processors did not start (a timer's, still in flight) queued uploads too.
|
|
1154
|
+
await this.drainUploads();
|
|
1155
|
+
return this.flushReport(from, processors);
|
|
1156
|
+
});
|
|
1157
|
+
return { sent, report };
|
|
1158
|
+
}
|
|
1159
|
+
/** Sends what the processors and exporters hold, once the previous flush's send ended. */
|
|
1160
|
+
sendRecords() {
|
|
1161
|
+
const send = () => this.sendingRecords.run(true, async () => {
|
|
1162
|
+
const processors = await Promise.allSettled([
|
|
1163
|
+
this.spanProcessor.forceFlush(),
|
|
1164
|
+
this.logRecordProcessor.forceFlush(),
|
|
1165
|
+
]);
|
|
1166
|
+
await Promise.all([this.traceExporter.forceFlush(), this.logExporter.forceFlush()]);
|
|
1167
|
+
return processors;
|
|
1168
|
+
});
|
|
1169
|
+
const sent = (this.sendPromise ?? Promise.resolve()).then(send, send);
|
|
1170
|
+
this.sendPromise = sent;
|
|
1016
1171
|
const clear = () => {
|
|
1017
|
-
if (this.
|
|
1018
|
-
this.
|
|
1172
|
+
if (this.sendPromise === sent)
|
|
1173
|
+
this.sendPromise = undefined;
|
|
1019
1174
|
};
|
|
1020
|
-
void
|
|
1021
|
-
return
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1175
|
+
void sent.then(clear, clear);
|
|
1176
|
+
return sent;
|
|
1177
|
+
}
|
|
1178
|
+
/** @internal Exporter callback: an export's queued uploads, reported once they all settle. */
|
|
1179
|
+
awaitUploads(report) {
|
|
1180
|
+
// Never an unhandled rejection, whatever the report does.
|
|
1181
|
+
const tracked = report.catch(() => { });
|
|
1182
|
+
this.uploadReports.add(tracked);
|
|
1183
|
+
void tracked.then(() => this.uploadReports.delete(tracked));
|
|
1184
|
+
const earlier = this.uploadsSettled;
|
|
1185
|
+
this.uploadsSettled = tracked.then(() => earlier);
|
|
1186
|
+
}
|
|
1187
|
+
/** Waits for the uploads queued so far; each ends within its lifetime, so this is bounded. */
|
|
1188
|
+
drainUploads() {
|
|
1189
|
+
return this.uploadsSettled;
|
|
1190
|
+
}
|
|
1191
|
+
flushReport(from, processors) {
|
|
1029
1192
|
for (const [index, result] of processors.entries())
|
|
1030
1193
|
if (result.status === "rejected") {
|
|
1031
1194
|
const signal = index === 0 ? "traces" : "logs";
|
|
@@ -1040,8 +1203,9 @@ export class HueTransport {
|
|
|
1040
1203
|
return report;
|
|
1041
1204
|
}
|
|
1042
1205
|
/**
|
|
1043
|
-
* Flushes and stops the processors
|
|
1044
|
-
* mode call it after shutting down the
|
|
1206
|
+
* Flushes and stops the processors, waits for queued uploads, then ends the uploader; records
|
|
1207
|
+
* emitted afterwards are dropped and counted. In attach mode call it after shutting down the
|
|
1208
|
+
* application's providers.
|
|
1045
1209
|
*
|
|
1046
1210
|
* @throws HueExportError when the final flush observed new failures.
|
|
1047
1211
|
*/
|
|
@@ -1057,7 +1221,10 @@ export class HueTransport {
|
|
|
1057
1221
|
this.logRecordProcessor.shutdown(),
|
|
1058
1222
|
]);
|
|
1059
1223
|
await Promise.allSettled([this.traceExporter.forceFlush(), this.logExporter.forceFlush()]);
|
|
1224
|
+
await this.drainUploads();
|
|
1060
1225
|
this.uploader?.close();
|
|
1226
|
+
// The reports of uploads the close ended land before shutdown resolves.
|
|
1227
|
+
await this.drainUploads();
|
|
1061
1228
|
for (const identity of this.media.keys())
|
|
1062
1229
|
this.releaseMediaIdentity(identity);
|
|
1063
1230
|
this.logParts.clear();
|
|
@@ -1256,7 +1423,24 @@ class ReportingExporter {
|
|
|
1256
1423
|
real.push(placeholder);
|
|
1257
1424
|
return real;
|
|
1258
1425
|
}
|
|
1426
|
+
/** Places and sends the records; the uploads their placement queued are reported once they
|
|
1427
|
+
* settle, after the records were sent, never awaited here. */
|
|
1259
1428
|
async exportRecords(records) {
|
|
1429
|
+
const late = { tally: newTally(), settled: [] };
|
|
1430
|
+
try {
|
|
1431
|
+
await this.placeAndSend(records, late);
|
|
1432
|
+
}
|
|
1433
|
+
finally {
|
|
1434
|
+
if (late.settled.length)
|
|
1435
|
+
this.reportWhenSettled(late);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/** Reports an export's uploads once they all settle. Built apart from the export, so a pending
|
|
1439
|
+
* upload never keeps the export's records alive. */
|
|
1440
|
+
reportWhenSettled(late) {
|
|
1441
|
+
this.transport.awaitUploads(Promise.all(late.settled).then(() => this.transport.reportOffload(late.tally, this.signal, true)));
|
|
1442
|
+
}
|
|
1443
|
+
async placeAndSend(records, late) {
|
|
1260
1444
|
// Completed records and placeholders travel in separate requests, completed records first,
|
|
1261
1445
|
// so a rejection count is always one kind of record's. Placeholders are advisory: losing one
|
|
1262
1446
|
// is a warning, never an export failure.
|
|
@@ -1278,21 +1462,17 @@ class ReportingExporter {
|
|
|
1278
1462
|
}
|
|
1279
1463
|
};
|
|
1280
1464
|
const resourceDeadline = Date.now() + this.transport.options.timeoutMillis;
|
|
1281
|
-
// How long this export has held its records for a rate-limited receiver
|
|
1282
|
-
// waits included.
|
|
1465
|
+
// How long this export has held its records for a rate-limited receiver.
|
|
1283
1466
|
const hold = { millis: 0 };
|
|
1284
|
-
// A span's values over the inline limit are
|
|
1285
|
-
// export's
|
|
1467
|
+
// A span's values over the inline limit are hashed and placed before it is measured, within
|
|
1468
|
+
// the export's hashing deadline; their uploads are queued, never awaited, and a placeholder's
|
|
1469
|
+
// values never upload.
|
|
1286
1470
|
const upload = {
|
|
1287
1471
|
deadline: Date.now() + uploadBudgetMillis(this.transport.options.timeoutMillis),
|
|
1288
|
-
wait: async (
|
|
1289
|
-
if (hold.millis + millis > MAX_RATE_LIMIT_HOLD_MILLIS)
|
|
1290
|
-
return false;
|
|
1291
|
-
hold.millis += millis;
|
|
1292
|
-
return this.transport.holdForRateLimit(millis);
|
|
1293
|
-
},
|
|
1472
|
+
wait: async () => false,
|
|
1294
1473
|
};
|
|
1295
1474
|
const tally = newTally();
|
|
1475
|
+
// Each record's attachments to upload once it is fitted, with the queued record holding them.
|
|
1296
1476
|
const helpers = new Map();
|
|
1297
1477
|
const placed = [];
|
|
1298
1478
|
for (const { record, markers } of this.ordered(records)) {
|
|
@@ -1341,8 +1521,8 @@ class ReportingExporter {
|
|
|
1341
1521
|
offload.body ||
|
|
1342
1522
|
offload.attachments?.length)
|
|
1343
1523
|
? this.signal === "traces"
|
|
1344
|
-
? this.transport.offloadSpan(redacted, offload, upload, tally)
|
|
1345
|
-
: this.transport.offloadLog(redacted, offload, upload, tally)
|
|
1524
|
+
? this.transport.offloadSpan(redacted, offload, upload, tally, late, record)
|
|
1525
|
+
: this.transport.offloadLog(redacted, offload, upload, tally, late, record)
|
|
1346
1526
|
: undefined;
|
|
1347
1527
|
placed.push({
|
|
1348
1528
|
record,
|
|
@@ -1393,7 +1573,7 @@ class ReportingExporter {
|
|
|
1393
1573
|
[BLOBS_KEY]: [...baseline, ...retained.map(mediaEntry)],
|
|
1394
1574
|
},
|
|
1395
1575
|
};
|
|
1396
|
-
helpers.set(redacted, { files: retained, baseline });
|
|
1576
|
+
helpers.set(redacted, { files: retained, baseline, source: record });
|
|
1397
1577
|
}
|
|
1398
1578
|
}
|
|
1399
1579
|
const bytes = 512 +
|
|
@@ -1595,7 +1775,11 @@ class ReportingExporter {
|
|
|
1595
1775
|
else
|
|
1596
1776
|
delete attributes[BLOBS_KEY];
|
|
1597
1777
|
}
|
|
1598
|
-
attachments = {
|
|
1778
|
+
attachments = {
|
|
1779
|
+
record: { ...fittedRecord, attributes },
|
|
1780
|
+
files,
|
|
1781
|
+
source: helper.source,
|
|
1782
|
+
};
|
|
1599
1783
|
remaining = [];
|
|
1600
1784
|
}
|
|
1601
1785
|
}
|
|
@@ -1613,15 +1797,17 @@ class ReportingExporter {
|
|
|
1613
1797
|
}
|
|
1614
1798
|
admitted.push({ fitted, attachments });
|
|
1615
1799
|
}
|
|
1616
|
-
// Decide every event/reference admission before
|
|
1617
|
-
//
|
|
1800
|
+
// Decide every event/reference admission before queueing helper uploads: a file fitting
|
|
1801
|
+
// removed is never uploaded. Their entries are written again as the fitted record listed
|
|
1802
|
+
// them, and the files upload after the records are sent.
|
|
1618
1803
|
const placed = await Promise.all(admitted.map(async ({ fitted, attachments }) => {
|
|
1619
1804
|
if (!attachments)
|
|
1620
1805
|
return fitted;
|
|
1621
1806
|
try {
|
|
1622
|
-
const uploaded = await this.transport.offloadSpan(attachments.record, { candidates: [], cut: [], attachments: attachments.files }, upload, tally);
|
|
1807
|
+
const uploaded = await this.transport.offloadSpan(attachments.record, { candidates: [], cut: [], attachments: attachments.files }, upload, tally, late, attachments.source);
|
|
1623
1808
|
measured.delete(fitted.record);
|
|
1624
|
-
//
|
|
1809
|
+
// The entries name each file's own type, as the fitted record's did; a file refused
|
|
1810
|
+
// when queued leaves its entry out, so the record is measured again, cheaply.
|
|
1625
1811
|
const final = await probe(uploaded);
|
|
1626
1812
|
if (final?.request)
|
|
1627
1813
|
measured.set(final.record, final.request);
|
|
@@ -1634,8 +1820,9 @@ class ReportingExporter {
|
|
|
1634
1820
|
return undefined;
|
|
1635
1821
|
}
|
|
1636
1822
|
}));
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1823
|
+
// Values refused when their records were placed are reported once, before delivery failures
|
|
1824
|
+
// can consume the diagnostic callback's rate-limit slot; queued uploads are reported once
|
|
1825
|
+
// they settle. Placeholders never upload.
|
|
1639
1826
|
if (!advisory)
|
|
1640
1827
|
this.transport.reportOffload(tally, this.signal);
|
|
1641
1828
|
let batch = [];
|