@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/attachment.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
3
5
|
import { types as utilTypes } from "node:util";
|
|
6
|
+
import { MAX_BLOB_BYTES } from "./blobs.js";
|
|
4
7
|
import { MAX_FILE_DATA_BYTES } from "./config.js";
|
|
5
8
|
import { inlineFileBytes, isMediaContentType, isMediaName } from "./inline-files.js";
|
|
6
9
|
/** How deep in a value an attachment's part may sit and still be uploaded: Hue reads media parts
|
|
@@ -19,41 +22,137 @@ function stringBytes(text) {
|
|
|
19
22
|
return undefined;
|
|
20
23
|
return Buffer.from(text, "base64");
|
|
21
24
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
/** How much of a file given by path is read at once, to hash it or to upload it. */
|
|
26
|
+
const FILE_SLICE = 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* A regular file given by path: its bytes when it is no larger than 25 MiB, and otherwise, up to
|
|
29
|
+
* Hue's 1 GB upload limit, its size and SHA-256 from one reading pass a slice at a time, never
|
|
30
|
+
* holding it whole. Undefined for a file over its limit and for anything that is no regular file
|
|
31
|
+
* (a named pipe, a device), which is opened without waiting and never read; throws for one that
|
|
32
|
+
* cannot be opened.
|
|
33
|
+
*/
|
|
34
|
+
export function readPath(path) {
|
|
35
|
+
// Resolved first: the file read now is the one read again when it is uploaded.
|
|
36
|
+
const absolute = resolve(path);
|
|
37
|
+
// Without waiting: a named pipe would block the application until something wrote to it.
|
|
38
|
+
const descriptor = openSync(absolute, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
25
39
|
try {
|
|
26
|
-
|
|
40
|
+
const info = fstatSync(descriptor);
|
|
41
|
+
if (!info.isFile())
|
|
42
|
+
return undefined;
|
|
43
|
+
if (info.size <= MAX_FILE_DATA_BYTES) {
|
|
44
|
+
const bytes = fileBytes(descriptor);
|
|
45
|
+
return bytes ? { bytes } : undefined;
|
|
46
|
+
}
|
|
47
|
+
if (info.size > MAX_BLOB_BYTES)
|
|
27
48
|
return undefined;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let total = 0;
|
|
49
|
+
const hash = createHash("sha256");
|
|
50
|
+
const slice = Buffer.allocUnsafe(FILE_SLICE);
|
|
51
|
+
let size = 0;
|
|
32
52
|
for (;;) {
|
|
33
|
-
const
|
|
34
|
-
if (room <= 0)
|
|
35
|
-
return undefined;
|
|
36
|
-
const buffer = Buffer.alloc(room);
|
|
37
|
-
const read = readSync(descriptor, buffer, 0, room, null);
|
|
53
|
+
const read = readSync(descriptor, slice, 0, FILE_SLICE, null);
|
|
38
54
|
if (read === 0)
|
|
39
55
|
break;
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
size += read;
|
|
57
|
+
// A file that grew past the limit while it was read is refused, not read on.
|
|
58
|
+
if (size > MAX_BLOB_BYTES)
|
|
59
|
+
return undefined;
|
|
60
|
+
hash.update(slice.subarray(0, read));
|
|
42
61
|
}
|
|
43
|
-
return
|
|
62
|
+
return { file: { path: absolute, size, sha256: hash.digest("hex") } };
|
|
44
63
|
}
|
|
45
64
|
finally {
|
|
46
65
|
closeSync(descriptor);
|
|
47
66
|
}
|
|
48
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* A file given by path as an upload body, read from disk a slice at a time. Each slice is hashed as
|
|
70
|
+
* it is read and sent once the next one is read; the last is sent only once the file ended at its
|
|
71
|
+
* recorded size with its recorded SHA-256. A file that changed since it was recorded, or can no
|
|
72
|
+
* longer be read, ends the read with an error before its body is complete and sets `changed`, so
|
|
73
|
+
* the upload is abandoned and the value is counted as not uploaded; so does a path that is no
|
|
74
|
+
* longer a regular file, which is opened without waiting (a named pipe in its place would wait for
|
|
75
|
+
* a writer) and never read. Hue's presigned PUT also signs the size and SHA-256, so the store
|
|
76
|
+
* refuses a body that differs.
|
|
77
|
+
*/
|
|
78
|
+
export function fileSource(file) {
|
|
79
|
+
const source = {
|
|
80
|
+
size: file.size,
|
|
81
|
+
async *chunks() {
|
|
82
|
+
// Each read starts fresh: a retried upload reads the file again.
|
|
83
|
+
source.changed = false;
|
|
84
|
+
const changed = () => {
|
|
85
|
+
source.changed = true;
|
|
86
|
+
return new Error("The file changed after it was recorded");
|
|
87
|
+
};
|
|
88
|
+
const handle = await open(file.path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0)).catch(() => {
|
|
89
|
+
throw changed();
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
const info = await handle.stat().catch(() => undefined);
|
|
93
|
+
if (!info?.isFile())
|
|
94
|
+
throw changed();
|
|
95
|
+
const hash = createHash("sha256");
|
|
96
|
+
let size = 0;
|
|
97
|
+
let pending;
|
|
98
|
+
// Two buffers in turn: the slice withheld until the next is read, and the next. A reader
|
|
99
|
+
// is done with a slice before it asks for the next, so each buffer is free when reused.
|
|
100
|
+
const buffers = [Buffer.allocUnsafe(FILE_SLICE), Buffer.allocUnsafe(FILE_SLICE)];
|
|
101
|
+
for (let turn = 0;; turn ^= 1) {
|
|
102
|
+
const slice = buffers[turn];
|
|
103
|
+
const { bytesRead } = await handle.read(slice, 0, FILE_SLICE, null).catch(() => {
|
|
104
|
+
throw changed();
|
|
105
|
+
});
|
|
106
|
+
if (bytesRead === 0)
|
|
107
|
+
break;
|
|
108
|
+
size += bytesRead;
|
|
109
|
+
if (size > file.size)
|
|
110
|
+
throw changed();
|
|
111
|
+
const read = slice.subarray(0, bytesRead);
|
|
112
|
+
hash.update(read);
|
|
113
|
+
if (pending)
|
|
114
|
+
yield pending;
|
|
115
|
+
pending = read;
|
|
116
|
+
}
|
|
117
|
+
if (size !== file.size || hash.digest("hex") !== file.sha256)
|
|
118
|
+
throw changed();
|
|
119
|
+
if (pending)
|
|
120
|
+
yield pending;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await handle.close().catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return source;
|
|
128
|
+
}
|
|
129
|
+
/** A file's bytes, read through one descriptor and never past the in-memory limit. */
|
|
130
|
+
function fileBytes(descriptor) {
|
|
131
|
+
// Read to its end, which may lie past its size as stated (a file that grew), but never more
|
|
132
|
+
// than one byte past the limit.
|
|
133
|
+
const parts = [];
|
|
134
|
+
let total = 0;
|
|
135
|
+
for (;;) {
|
|
136
|
+
const room = Math.min(1 << 20, MAX_FILE_DATA_BYTES + 1 - total);
|
|
137
|
+
if (room <= 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
const buffer = Buffer.alloc(room);
|
|
140
|
+
const read = readSync(descriptor, buffer, 0, room, null);
|
|
141
|
+
if (read === 0)
|
|
142
|
+
break;
|
|
143
|
+
parts.push(buffer.subarray(0, read));
|
|
144
|
+
total += read;
|
|
145
|
+
}
|
|
146
|
+
return Buffer.concat(parts, total);
|
|
147
|
+
}
|
|
49
148
|
function attachmentBytes(options) {
|
|
50
149
|
const { data, path } = options;
|
|
51
150
|
if ((data === undefined) === (path === undefined))
|
|
52
151
|
return undefined;
|
|
152
|
+
if (path !== undefined)
|
|
153
|
+
return typeof path === "string" && path ? readPath(path) : undefined;
|
|
53
154
|
let bytes;
|
|
54
|
-
if (typeof
|
|
55
|
-
bytes = path ? fileBytes(path) : undefined;
|
|
56
|
-
else if (typeof data === "string") {
|
|
155
|
+
if (typeof data === "string") {
|
|
57
156
|
if (data.length > 4 * MAX_FILE_DATA_BYTES)
|
|
58
157
|
return undefined;
|
|
59
158
|
bytes = stringBytes(data);
|
|
@@ -70,7 +169,7 @@ function attachmentBytes(options) {
|
|
|
70
169
|
return undefined;
|
|
71
170
|
bytes = new Uint8Array(data.slice(0));
|
|
72
171
|
}
|
|
73
|
-
return bytes && bytes.byteLength <= MAX_FILE_DATA_BYTES ? bytes : undefined;
|
|
172
|
+
return bytes && bytes.byteLength <= MAX_FILE_DATA_BYTES ? { bytes } : undefined;
|
|
74
173
|
}
|
|
75
174
|
/**
|
|
76
175
|
* A file placed in a recorded value, the equivalent of Langfuse's `LangfuseMedia` and
|
|
@@ -80,10 +179,16 @@ function attachmentBytes(options) {
|
|
|
80
179
|
* names the file by digest (`{ type: "blob", mime_type, modality, sha256, size, name }`) and
|
|
81
180
|
* uploads its bytes during export, listed under the record's `hue.blobs`.
|
|
82
181
|
*
|
|
83
|
-
* The bytes are read and hashed when the attachment is created; `data`
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
182
|
+
* The bytes are read and hashed when the attachment is created; `data` is limited to 25 MiB, as
|
|
183
|
+
* the bytes are held in memory until they are uploaded. A `path` names a regular file, opened
|
|
184
|
+
* without waiting: anything else, such as a named pipe, is invalid. One up to 25 MiB is read then
|
|
185
|
+
* too; a larger regular file, up to Hue's 1 GB upload limit, is hashed then a slice at a time and read
|
|
186
|
+
* again from disk when it is uploaded, so it must stay in place and unchanged until the client
|
|
187
|
+
* flushes: a file whose size or SHA-256 then differs is not uploaded and counts as an upload
|
|
188
|
+
* fallback, its part keeping the digest it was recorded with. Creating one never throws: an
|
|
189
|
+
* attachment without exactly one of `data` and `path`, with unreadable data, larger data or an
|
|
190
|
+
* invalid `contentType` records `null` where it is placed and counts an instrumentation failure;
|
|
191
|
+
* an invalid `name` alone is omitted and counted.
|
|
87
192
|
*/
|
|
88
193
|
export class Attachment {
|
|
89
194
|
/** The media type given. */
|
|
@@ -97,21 +202,24 @@ export class Attachment {
|
|
|
97
202
|
constructor(options) {
|
|
98
203
|
const contentType = options?.contentType;
|
|
99
204
|
const name = options?.name;
|
|
100
|
-
let
|
|
205
|
+
let read;
|
|
101
206
|
try {
|
|
102
|
-
|
|
207
|
+
read = isMediaContentType(contentType) ? attachmentBytes(options) : undefined;
|
|
103
208
|
}
|
|
104
209
|
catch {
|
|
105
210
|
// An unreadable path, or data that is not bytes: the attachment is invalid.
|
|
106
|
-
|
|
211
|
+
read = undefined;
|
|
107
212
|
}
|
|
108
|
-
const
|
|
213
|
+
const bytes = read && "bytes" in read ? read.bytes : undefined;
|
|
214
|
+
const file = read && "file" in read ? read.file : undefined;
|
|
215
|
+
const sha256 = bytes ? createHash("sha256").update(bytes).digest("hex") : file?.sha256;
|
|
109
216
|
this.contentType = typeof contentType === "string" ? contentType : "";
|
|
110
217
|
this.name = isMediaName(name) ? name : undefined;
|
|
111
218
|
this.sha256 = sha256;
|
|
112
|
-
this.size = bytes?.byteLength;
|
|
219
|
+
this.size = bytes?.byteLength ?? file?.size;
|
|
113
220
|
states.set(this, {
|
|
114
|
-
...(bytes ? { bytes, sha256 } : {}),
|
|
221
|
+
...(bytes ? { bytes, sha256, size: bytes.byteLength } : {}),
|
|
222
|
+
...(file ? { file, sha256, size: file.size } : {}),
|
|
115
223
|
contentType: this.contentType,
|
|
116
224
|
...(this.name === undefined ? {} : { name: this.name }),
|
|
117
225
|
invalidName: name !== undefined && name !== null && this.name === undefined,
|
|
@@ -144,14 +252,14 @@ export function modalityOf(contentType) {
|
|
|
144
252
|
/** An attachment's digest-form GenAI part, the name only with content capture; undefined for an
|
|
145
253
|
* invalid attachment. */
|
|
146
254
|
export function attachmentPart(state, captureContent) {
|
|
147
|
-
if (
|
|
255
|
+
if (state.size === undefined || !state.sha256)
|
|
148
256
|
return undefined;
|
|
149
257
|
return {
|
|
150
258
|
type: "blob",
|
|
151
259
|
mime_type: state.contentType,
|
|
152
260
|
modality: modalityOf(state.contentType),
|
|
153
261
|
sha256: state.sha256,
|
|
154
|
-
size: state.
|
|
262
|
+
size: state.size,
|
|
155
263
|
...(captureContent && state.name !== undefined ? { name: state.name } : {}),
|
|
156
264
|
};
|
|
157
265
|
}
|
package/dist/blobs.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type HrTime } from "@opentelemetry/api";
|
|
2
2
|
import { type MediaDigestMemo, type FileDigest } from "./inline-files.js";
|
|
3
|
+
import { type ByteSource } from "./large-media.js";
|
|
3
4
|
/**
|
|
4
5
|
* Large values and binary media travel apart from their span through reservation, presigned PUT,
|
|
5
|
-
* and completion.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* and completion. Export hashes each value, writes its reference (text keeps a 16 KiB prefix,
|
|
7
|
+
* binary media digest metadata on its part) and its `hue.blobs` entry, and sends the record; the
|
|
8
|
+
* upload runs afterwards in the background, as Langfuse uploads media. A value refused when its
|
|
9
|
+
* record is exported keeps its inline fallback or file metadata; an upload that fails after export
|
|
10
|
+
* leaves its entry, which Hue reads as missing. Both are reported as warnings and counted.
|
|
8
11
|
*/
|
|
9
12
|
/** The span attribute listing the values the SDK uploaded apart from the span: one compact JSON
|
|
10
13
|
* object per value, `{"key", "sha256", "size", "content_type"}`, with an optional `name`. */
|
|
@@ -16,18 +19,28 @@ export declare const BLOB_PREFIX_BYTES: number;
|
|
|
16
19
|
/** The entries one record's `hue.blobs` may list; Hue reads no more. */
|
|
17
20
|
export declare const MAX_BLOB_ENTRIES = 64;
|
|
18
21
|
/**
|
|
19
|
-
* The values over the inline limit that queued
|
|
20
|
-
* apart from `maxQueueBytes` (a string as two bytes a code unit, bytes as
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* The values over the inline limit that queued records and unsettled uploads may hold whole at
|
|
23
|
+
* once, charged apart from `maxQueueBytes` (a string as two bytes a code unit, bytes as
|
|
24
|
+
* themselves): each is held until its record is exported and its upload settles. A value the
|
|
25
|
+
* budget cannot hold is cut when it is queued, as before uploads existed; a single value larger
|
|
26
|
+
* than the whole budget is held while nothing else is.
|
|
23
27
|
*/
|
|
24
28
|
export declare const MAX_HELD_BLOB_BYTES: number;
|
|
25
|
-
/**
|
|
29
|
+
/** The longest wait before an upload's next attempt, a `Retry-After` included. */
|
|
30
|
+
export declare const MAX_RETRY_AFTER_MILLIS = 60000;
|
|
31
|
+
/** How long one export may spend hashing its values: past it a value keeps its fallback. Also
|
|
32
|
+
* the import's least upload deadline, and part of an upload's lifetime. */
|
|
26
33
|
export declare function uploadBudgetMillis(timeoutMillis: number): number;
|
|
34
|
+
/** How long one upload may take from when its record's export queued it: six export timeouts for
|
|
35
|
+
* its requests and up to a minute of Retry-After of its own. */
|
|
36
|
+
export declare function uploadLifetimeMillis(timeoutMillis: number): number;
|
|
27
37
|
/** Why content kept its fallback instead of being uploaded. */
|
|
28
38
|
export type FallbackReason = "unsupported" | "failed" | "tooLarge" | "budget";
|
|
29
39
|
/** The fixed description of each fallback, as an export issue reports it. */
|
|
30
40
|
export declare const FALLBACK_MESSAGES: Record<FallbackReason, string>;
|
|
41
|
+
/** The fixed description of each failure of an upload that ran after its record was exported:
|
|
42
|
+
* the record lists the value under `hue.blobs`, and Hue reads it as missing. */
|
|
43
|
+
export declare const LATE_FALLBACK_MESSAGES: Record<FallbackReason, string>;
|
|
31
44
|
/** A span's own attribute value the redaction left whole for the upload step to place. */
|
|
32
45
|
export interface OffloadCandidate {
|
|
33
46
|
key: string;
|
|
@@ -43,6 +56,9 @@ export interface OffloadCollector {
|
|
|
43
56
|
* export inspects only these. Absent for a record admission did not copy, and for a log record,
|
|
44
57
|
* which export inspects within its own allowance. */
|
|
45
58
|
inspected?: ReadonlySet<string> | undefined;
|
|
59
|
+
/** The span's own attributes admission held whole for upload past the inspection bounds: export
|
|
60
|
+
* reads their media in place (`readLargeMediaText`). */
|
|
61
|
+
large?: ReadonlySet<string> | undefined;
|
|
46
62
|
/** Set for a log record: its values over the inline limit are cut, never uploaded whole; only
|
|
47
63
|
* the media in its inspected attributes and its body upload, with the attachments placed in
|
|
48
64
|
* its body (`attachments`, keyed `body#<pointer>`). */
|
|
@@ -56,7 +72,8 @@ export interface OffloadCollector {
|
|
|
56
72
|
bodyCut?: Set<string>;
|
|
57
73
|
}
|
|
58
74
|
export interface MediaAttachment {
|
|
59
|
-
bytes
|
|
75
|
+
/** The file's bytes, or for a file given by path over the in-memory limit, its source on disk. */
|
|
76
|
+
bytes: Uint8Array | ByteSource;
|
|
60
77
|
digest: FileDigest;
|
|
61
78
|
contentType: string;
|
|
62
79
|
name?: string;
|
|
@@ -79,6 +96,21 @@ export interface OffloadTally {
|
|
|
79
96
|
}
|
|
80
97
|
export declare function newTally(): OffloadTally;
|
|
81
98
|
export declare function countFallback(tally: OffloadTally, reason: FallbackReason, traceId: string): void;
|
|
99
|
+
/** Adds one tally's outcomes to another's. */
|
|
100
|
+
export declare function mergeTally(into: OffloadTally, from: OffloadTally): void;
|
|
101
|
+
/** Adds `from` to `into` once `settled` all settle. Built apart from the step that queued the
|
|
102
|
+
* uploads, so a pending upload keeps alive the two tallies alone, never that step's record. */
|
|
103
|
+
export declare function mergeWhenSettled(into: OffloadTally, from: OffloadTally, settled: Promise<void>[]): Promise<void>;
|
|
104
|
+
/** The uploads an export queued after placing its records, and what they came to once settled:
|
|
105
|
+
* each record's own are added when its placement returns. */
|
|
106
|
+
export interface LateUploads {
|
|
107
|
+
tally: OffloadTally;
|
|
108
|
+
settled: Promise<void>[];
|
|
109
|
+
/** Told of each upload as it is queued: when it settles, how many of the bytes it charges its
|
|
110
|
+
* record holds too (none for bytes decoded or rewritten apart from the record), and when that
|
|
111
|
+
* charge is released. */
|
|
112
|
+
queued?(settled: Promise<void>, shared: number, released: Promise<void>): void;
|
|
113
|
+
}
|
|
82
114
|
/** A media type accepted by the upload contract. */
|
|
83
115
|
export declare function isBlobContentType(value: unknown): value is string;
|
|
84
116
|
/** The media type an uploaded text is stored under: JSON when it reads as a JSON object or array
|
|
@@ -88,26 +120,35 @@ export declare function textContentType(text: string): string;
|
|
|
88
120
|
* as its own copy so the whole value is not kept alive by it. */
|
|
89
121
|
export declare function blobPrefix(value: string): string;
|
|
90
122
|
export declare function blobPrefix(value: Uint8Array): Uint8Array;
|
|
123
|
+
/** A value's size in bytes as it is uploaded: a string's UTF-8 encoding, counted, not made. */
|
|
124
|
+
export declare function sizeOf(value: string | Uint8Array | ByteSource): number;
|
|
125
|
+
/** The bytes a value holds in memory: none for a source read as it is uploaded. */
|
|
126
|
+
export declare function heldBytesOf(value: Uint8Array | ByteSource): number;
|
|
91
127
|
/** The SHA-256 (lowercase hex) and byte size of what an upload of `value` carries, yielding to
|
|
92
|
-
* the event loop between slices so a large value does not stall the application.
|
|
93
|
-
|
|
128
|
+
* the event loop between slices so a large value does not stall the application. Past
|
|
129
|
+
* `deadline`, it throws rather than read on. */
|
|
130
|
+
export declare function digestOf(value: string | Uint8Array | ByteSource, deadline?: number): Promise<{
|
|
94
131
|
sha256: string;
|
|
95
132
|
size: number;
|
|
96
133
|
}>;
|
|
97
134
|
/** The `hue.blobs` element for one uploaded value. */
|
|
98
135
|
export declare function blobEntry(key: string, sha256: string, size: number, contentType: string, name?: string): string;
|
|
99
|
-
/** What
|
|
100
|
-
*
|
|
136
|
+
/** What an upload may spend: its deadline, and its wait before another attempt (`wait` resolves
|
|
137
|
+
* false when the wait was ended early or may not be taken). An export's own context bounds its
|
|
138
|
+
* hashing only. */
|
|
101
139
|
export interface UploadContext {
|
|
102
140
|
deadline: number;
|
|
103
141
|
wait(millis: number): Promise<boolean>;
|
|
104
142
|
}
|
|
143
|
+
/** An upload's outcome; `changed` when the value's own source no longer held its bytes (a file
|
|
144
|
+
* that changed since it was recorded), which says nothing of another reference's bytes. */
|
|
105
145
|
type Outcome = {
|
|
106
146
|
ok: true;
|
|
107
147
|
contentType: string;
|
|
108
148
|
} | {
|
|
109
149
|
ok: false;
|
|
110
150
|
reason: FallbackReason;
|
|
151
|
+
changed?: boolean;
|
|
111
152
|
};
|
|
112
153
|
/** The upload route's options, from the transport's validated options. */
|
|
113
154
|
export interface UploaderOptions {
|
|
@@ -119,50 +160,98 @@ export interface UploaderOptions {
|
|
|
119
160
|
}
|
|
120
161
|
/**
|
|
121
162
|
* Uploads values to Hue: reserves each with `POST /api/v1/otlp/blobs`, PUTs its bytes to the
|
|
122
|
-
* presigned URL Hue answers (skipped when Hue already stores the value), and completes it.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* {@link
|
|
163
|
+
* presigned URL Hue answers (skipped when Hue already stores the value), and completes it. An
|
|
164
|
+
* export queues its values ({@link enqueue}) and sends its records without waiting for them; the
|
|
165
|
+
* import stores each value before it sends its span ({@link store}). At most
|
|
166
|
+
* {@link UPLOAD_CONCURRENCY} uploads run at once, and {@link HASH_CONCURRENCY} hashes apart from
|
|
167
|
+
* them. A reservation or PUT that gets no answer, a 408, a 429 or a 5xx is tried
|
|
168
|
+
* {@link UPLOAD_ATTEMPTS} times; every request has a deadline and none follows a redirect. A
|
|
169
|
+
* receiver without the route (a 404 without Hue's JSON error, a 405 or 501, or a 200 that is no
|
|
170
|
+
* reservation) is remembered for {@link UNSUPPORTED_PAUSE_MILLIS}: values exported meanwhile keep
|
|
171
|
+
* their fallback, never an entry the receiver cannot resolve.
|
|
128
172
|
*/
|
|
129
173
|
export declare class BlobUploader {
|
|
130
174
|
private readonly options;
|
|
175
|
+
/** Until when the receiver is taken to lack the upload route. */
|
|
131
176
|
private unsupportedUntil;
|
|
132
|
-
private pausedUntil;
|
|
133
177
|
private closed;
|
|
134
|
-
private
|
|
135
|
-
private
|
|
178
|
+
private uploads;
|
|
179
|
+
private hashes;
|
|
180
|
+
private queued;
|
|
181
|
+
private queuedBytes;
|
|
182
|
+
/** What queued uploads keep alive, each charged once, from the first upload holding it until
|
|
183
|
+
* the last settles: two files read in place from one recorded string charge it once, whole. */
|
|
184
|
+
private holding;
|
|
185
|
+
private sleeps;
|
|
136
186
|
private requests;
|
|
137
187
|
private outcomes;
|
|
188
|
+
/** The first wait before another attempt, doubled each time; tests shorten it. */
|
|
189
|
+
private retryDelayMillis;
|
|
190
|
+
/** How long a queued upload may take; tests shorten it. */
|
|
191
|
+
private lifetimeMillis;
|
|
138
192
|
constructor(options: UploaderOptions);
|
|
139
193
|
/** Whether a value over the inline limit is worth holding whole for upload now. */
|
|
140
194
|
available(): boolean;
|
|
141
195
|
/** Whether the receiver lacks the upload route, as it last answered. */
|
|
142
196
|
unsupported(): boolean;
|
|
143
|
-
/** Ends uploads in flight and refuses new ones:
|
|
197
|
+
/** Ends uploads in flight, queued uploads and their waits, and refuses new ones: values not
|
|
198
|
+
* yet exported fall back to the inline cut, and uploads queued by exported records fail. */
|
|
144
199
|
close(): void;
|
|
145
200
|
/** Runs `work` once fewer than {@link UPLOAD_CONCURRENCY} uploads are in flight. */
|
|
146
201
|
limit<R>(work: () => Promise<R>): Promise<R>;
|
|
202
|
+
/** The SHA-256 and size of `value` (see {@link digestOf}), hashed in a place of its own: an
|
|
203
|
+
* export never waits for an upload to hash its values. */
|
|
204
|
+
digest(value: string | Uint8Array | ByteSource, deadline: number): Promise<FileDigest>;
|
|
205
|
+
/** The bytes queued uploads keep alive until they settle. */
|
|
206
|
+
pendingBytes(): number;
|
|
147
207
|
/** Why a value cannot be uploaded right now without trying, if it cannot. */
|
|
148
208
|
refusal(): FallbackReason | undefined;
|
|
149
|
-
|
|
209
|
+
/** Why the value cannot be uploaded at all, whatever Hue would answer. */
|
|
210
|
+
private invalid;
|
|
211
|
+
/**
|
|
212
|
+
* Queues `value`'s upload for an exported record, deciding now what the record lists: a value
|
|
213
|
+
* refused without trying (see {@link refusal}), invalid, or past the queue's
|
|
214
|
+
* {@link MAX_QUEUED_UPLOADS} uploads or {@link MAX_HELD_BLOB_BYTES} keeps its fallback. A queued
|
|
215
|
+
* upload runs within {@link uploadLifetimeMillis} of now, holding `value` until it settles;
|
|
216
|
+
* `done` never rejects. `bytes` is what it adds to {@link pendingBytes}: the strings it keeps
|
|
217
|
+
* alive that no other queued upload holds, charged until `released`, once the last upload
|
|
218
|
+
* holding them settles.
|
|
219
|
+
*/
|
|
220
|
+
enqueue(traceId: string, key: string, value: string | Uint8Array | ByteSource, contentType: string, digest: FileDigest): {
|
|
221
|
+
ok: false;
|
|
222
|
+
reason: FallbackReason;
|
|
223
|
+
} | {
|
|
224
|
+
ok: true;
|
|
225
|
+
done: Promise<Outcome>;
|
|
226
|
+
bytes: number;
|
|
227
|
+
released: Promise<void>;
|
|
228
|
+
};
|
|
229
|
+
/** Waits `millis`; false when {@link close} ended the wait first. The timer keeps the process
|
|
230
|
+
* running, as an export in progress does, within the upload's lifetime. */
|
|
231
|
+
sleep(millis: number): Promise<boolean>;
|
|
232
|
+
/** Waits before attempt `attempt + 1`: `Retry-After` when the answer gave one, otherwise a
|
|
233
|
+
* second doubled each attempt with jitter, at most a minute. The upload's place is given up
|
|
234
|
+
* meanwhile, so uploads queued behind it run. False when the wait would pass the upload's
|
|
235
|
+
* deadline, was ended, or no place came free again before the deadline. */
|
|
236
|
+
private retryWait;
|
|
150
237
|
/**
|
|
151
238
|
* Stores `value` under `key` of the trace: reserved, uploaded unless Hue already holds it, and
|
|
152
239
|
* completed. Only a fresh upload takes a {@link limit} slot: another reference to bytes the
|
|
153
240
|
* trace is already storing shares that upload's outcome without one, so repeated files never
|
|
154
|
-
* hold the slots new files need. The caller must not hold a slot.
|
|
241
|
+
* hold the slots new files need. The caller must not hold a slot. Never rejects.
|
|
155
242
|
*/
|
|
156
|
-
store(traceId: string, key: string, value: string | Uint8Array, contentType: string, digest: FileDigest, upload: UploadContext): Promise<Outcome>;
|
|
243
|
+
store(traceId: string, key: string, value: string | Uint8Array | ByteSource, contentType: string, digest: FileDigest, upload: UploadContext): Promise<Outcome>;
|
|
157
244
|
private storeFresh;
|
|
158
245
|
private jsonHeaders;
|
|
159
|
-
/** Reserves the value: Hue answers that it holds it already, or a presigned PUT.
|
|
160
|
-
*
|
|
246
|
+
/** Reserves the value: Hue answers that it holds it already, or a presigned PUT. No answer, a
|
|
247
|
+
* 408, a 429 or a 5xx other than 501 is tried again, after its `Retry-After` when it gave one,
|
|
248
|
+
* within the upload's deadline. */
|
|
161
249
|
private reserve;
|
|
162
|
-
private delay;
|
|
163
250
|
/** A 200 answer read: `exists`, or an upload whose URL and headers are usable as given. */
|
|
164
251
|
private reservation;
|
|
165
|
-
/** The PUT of the value's bytes, with exactly the headers Hue signed.
|
|
252
|
+
/** The PUT of the value's bytes, with exactly the headers Hue signed. A source that no longer
|
|
253
|
+
* holds its bytes (a file changed since it was recorded) ends the request before its body is
|
|
254
|
+
* complete, and fails the value without a retry. */
|
|
166
255
|
private put;
|
|
167
256
|
/**
|
|
168
257
|
* One HTTP request with a deadline and an idle timeout, never following a redirect, its
|
|
@@ -172,32 +261,39 @@ export declare class BlobUploader {
|
|
|
172
261
|
*/
|
|
173
262
|
private request;
|
|
174
263
|
}
|
|
175
|
-
/** Where one
|
|
264
|
+
/** Where one record's values are uploaded, and what it learned of them. */
|
|
176
265
|
export interface OffloadOptions {
|
|
177
266
|
uploader: BlobUploader;
|
|
178
267
|
traceId: string;
|
|
179
268
|
/** The receiver's inline value limit. */
|
|
180
269
|
valueBytes: number;
|
|
270
|
+
/** The deadline for hashing, and for a blocking store (no `background`) its uploads too. */
|
|
181
271
|
upload: UploadContext;
|
|
182
272
|
tally: OffloadTally;
|
|
183
273
|
memo?: MediaDigestMemo;
|
|
274
|
+
/** For tests: the length past which a long string is read in place (`LONG_STRING_UNITS`). */
|
|
275
|
+
longUnits?: number;
|
|
276
|
+
/** Set by an export: values are placed as uploaded and their uploads queued, each counted in
|
|
277
|
+
* `background.tally` once it settles; `tally` counts the values refused when placed. Absent
|
|
278
|
+
* for the import, which stores each value, under the type Hue stores it as, before placing it. */
|
|
279
|
+
background?: LateUploads;
|
|
184
280
|
}
|
|
185
281
|
/**
|
|
186
282
|
* A span's own attributes with each candidate placed: binary media becomes a digest, while
|
|
187
|
-
* text media over 64 KiB retains a 16 KiB prefix
|
|
188
|
-
* A value still over the inline limit is uploaded and replaced by its first
|
|
189
|
-
*
|
|
283
|
+
* text media over 64 KiB retains a 16 KiB prefix once uploaded or queued, or a digest when
|
|
284
|
+
* refused. A value still over the inline limit is uploaded and replaced by its first 16 KiB (or
|
|
285
|
+
* cut to the limit when refused), each upload listed under `hue.blobs`, each whole value cut or
|
|
190
286
|
* uploaded listed under `hue.truncated`. Every fallback is counted in the tally.
|
|
191
287
|
*/
|
|
192
288
|
export declare function offloadAttributes(attributes: Record<string, unknown>, collector: OffloadCollector, options: OffloadOptions): Promise<Record<string, unknown>>;
|
|
193
289
|
/**
|
|
194
290
|
* A record's own attributes, and a log record's body, with each candidate placed (see
|
|
195
|
-
* {@link offloadAttributes}). A file is uploaded
|
|
196
|
-
* provider part keeps a 16 KiB prefix), its `hue.blobs` entry keyed
|
|
197
|
-
* a log record's body, `body#<pointer>`. A file
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* receiver's marker, as before.
|
|
291
|
+
* {@link offloadAttributes}). A file is uploaded (or, for an export, queued) and placed in its
|
|
292
|
+
* digest form (a text file in a provider part keeps a 16 KiB prefix), its `hue.blobs` entry keyed
|
|
293
|
+
* `<attribute>#<pointer>` or, in a log record's body, `body#<pointer>`. A file refused or not
|
|
294
|
+
* uploaded before it is placed keeps its bytes inline, except in a span's own recorded messages,
|
|
295
|
+
* where it keeps its digest as before. A log record's values over the inline limit are cut, never
|
|
296
|
+
* uploaded whole, and a body still over it is replaced by the receiver's marker, as before.
|
|
201
297
|
*/
|
|
202
298
|
export declare function offloadRecord(attributes: Record<string, unknown>, collector: OffloadCollector, options: OffloadOptions): Promise<{
|
|
203
299
|
attributes: Record<string, unknown>;
|