@tungthedev/streams-server 0.2.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/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
import type { OtlpTraceExportError, OtlpTraceExportResult } from "../profile";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_OTLP_LIMITS,
|
|
6
|
+
normalizeOtelDecodedSpanResult,
|
|
7
|
+
type DecodedOtelEvent,
|
|
8
|
+
type DecodedOtelLink,
|
|
9
|
+
type DecodedOtelSpan,
|
|
10
|
+
type OtelTraceOtlpLimits,
|
|
11
|
+
type OtelTracesStreamProfile,
|
|
12
|
+
} from "./normalize";
|
|
13
|
+
|
|
14
|
+
const JSON_TEXT_DECODER = new TextDecoder();
|
|
15
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
16
|
+
const PROTOBUF_CONTENT_TYPE = "application/x-protobuf";
|
|
17
|
+
|
|
18
|
+
type ResourceSpansDecoded = {
|
|
19
|
+
resourceAttributes: Record<string, unknown>;
|
|
20
|
+
resourceSchemaUrl: string | null;
|
|
21
|
+
scopeSpans: ScopeSpansDecoded[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ScopeSpansDecoded = {
|
|
25
|
+
scope: {
|
|
26
|
+
name: string | null;
|
|
27
|
+
version: string | null;
|
|
28
|
+
schemaUrl: string | null;
|
|
29
|
+
attributes: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
spans: Array<Omit<DecodedOtelSpan, "resourceAttributes" | "resourceSchemaUrl" | "instrumentationScope">>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type DecodedExport = {
|
|
35
|
+
spans: DecodedOtelSpan[];
|
|
36
|
+
rejectedSpans: number;
|
|
37
|
+
warnings: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function baseContentType(value: string): string {
|
|
41
|
+
return value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hexFromBytes(bytes: Uint8Array): string {
|
|
45
|
+
return Array.from(bytes)
|
|
46
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeString(value: unknown): string | null {
|
|
55
|
+
if (typeof value !== "string") return null;
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
return trimmed === "" ? null : trimmed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeNanoString(value: unknown): string | null {
|
|
61
|
+
if (value == null) return null;
|
|
62
|
+
if (typeof value === "bigint") return value >= 0n ? value.toString() : null;
|
|
63
|
+
if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0) return BigInt(value).toString();
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (/^(0|[1-9][0-9]*)$/.test(trimmed)) return trimmed;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendWarning(warnings: string[], message: string): void {
|
|
72
|
+
if (warnings.includes(message)) return;
|
|
73
|
+
if (warnings.length < 8) warnings.push(message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkAnyValueDepthResult(depth: number, limits: OtelTraceOtlpLimits): Result<void, { message: string }> {
|
|
77
|
+
if (depth > limits.maxAnyValueDepth) {
|
|
78
|
+
return Result.err({ message: `OTLP AnyValue nesting too deep (max ${limits.maxAnyValueDepth})` });
|
|
79
|
+
}
|
|
80
|
+
return Result.ok(undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function anyValueFromJsonResult(raw: unknown, limits: OtelTraceOtlpLimits, depth = 0): Result<unknown, { message: string }> {
|
|
84
|
+
const depthRes = checkAnyValueDepthResult(depth, limits);
|
|
85
|
+
if (Result.isError(depthRes)) return depthRes;
|
|
86
|
+
if (!isPlainObject(raw)) return Result.ok(structuredClone(raw));
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(raw, "stringValue")) return Result.ok(normalizeString(raw.stringValue) ?? "");
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(raw, "boolValue")) return Result.ok(raw.boolValue === true);
|
|
89
|
+
if (Object.prototype.hasOwnProperty.call(raw, "intValue")) {
|
|
90
|
+
const value = raw.intValue;
|
|
91
|
+
if (typeof value === "string" && /^-?(0|[1-9][0-9]*)$/.test(value.trim())) return Result.ok(value.trim());
|
|
92
|
+
if (typeof value === "number" && Number.isFinite(value)) return Result.ok(Math.trunc(value));
|
|
93
|
+
return Result.ok(null);
|
|
94
|
+
}
|
|
95
|
+
if (Object.prototype.hasOwnProperty.call(raw, "doubleValue")) {
|
|
96
|
+
return Result.ok(typeof raw.doubleValue === "number" ? raw.doubleValue : Number(raw.doubleValue));
|
|
97
|
+
}
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(raw, "bytesValue")) return Result.ok(normalizeString(raw.bytesValue) ?? "");
|
|
99
|
+
if (isPlainObject(raw.arrayValue) && Array.isArray(raw.arrayValue.values)) {
|
|
100
|
+
if (raw.arrayValue.values.length > limits.maxArrayValuesPerAnyValue) {
|
|
101
|
+
return Result.err({ message: `OTLP AnyValue array too large (max ${limits.maxArrayValuesPerAnyValue})` });
|
|
102
|
+
}
|
|
103
|
+
const out: unknown[] = [];
|
|
104
|
+
for (const item of raw.arrayValue.values) {
|
|
105
|
+
const valueRes = anyValueFromJsonResult(item, limits, depth + 1);
|
|
106
|
+
if (Result.isError(valueRes)) return valueRes;
|
|
107
|
+
out.push(valueRes.value);
|
|
108
|
+
}
|
|
109
|
+
return Result.ok(out);
|
|
110
|
+
}
|
|
111
|
+
if (isPlainObject(raw.kvlistValue) && Array.isArray(raw.kvlistValue.values)) {
|
|
112
|
+
return keyValuesFromJsonResult(raw.kvlistValue.values, limits, depth + 1, true);
|
|
113
|
+
}
|
|
114
|
+
return Result.ok(structuredClone(raw));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function keyValuesFromJsonResult(
|
|
118
|
+
raw: unknown,
|
|
119
|
+
limits: OtelTraceOtlpLimits,
|
|
120
|
+
depth: number,
|
|
121
|
+
enforceCollectionLimit: boolean
|
|
122
|
+
): Result<Record<string, unknown>, { message: string }> {
|
|
123
|
+
const depthRes = checkAnyValueDepthResult(depth, limits);
|
|
124
|
+
if (Result.isError(depthRes)) return depthRes;
|
|
125
|
+
const out: Record<string, unknown> = {};
|
|
126
|
+
if (!Array.isArray(raw)) return Result.ok(out);
|
|
127
|
+
if (enforceCollectionLimit && raw.length > limits.maxKvListValuesPerAnyValue) {
|
|
128
|
+
return Result.err({ message: `OTLP AnyValue kvlist too large (max ${limits.maxKvListValuesPerAnyValue})` });
|
|
129
|
+
}
|
|
130
|
+
for (const item of raw) {
|
|
131
|
+
if (!isPlainObject(item)) continue;
|
|
132
|
+
const key = normalizeString(item.key);
|
|
133
|
+
if (!key) continue;
|
|
134
|
+
const valueRes = anyValueFromJsonResult(item.value, limits, depth);
|
|
135
|
+
if (Result.isError(valueRes)) return valueRes;
|
|
136
|
+
out[key] = valueRes.value;
|
|
137
|
+
}
|
|
138
|
+
return Result.ok(out);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function eventFromJsonResult(raw: unknown, limits: OtelTraceOtlpLimits): Result<DecodedOtelEvent | null, { message: string }> {
|
|
142
|
+
if (!isPlainObject(raw)) return Result.ok(null);
|
|
143
|
+
const attrsRes = keyValuesFromJsonResult(raw.attributes, limits, 0, false);
|
|
144
|
+
if (Result.isError(attrsRes)) return attrsRes;
|
|
145
|
+
return Result.ok({
|
|
146
|
+
timeUnixNano: normalizeNanoString(raw.timeUnixNano),
|
|
147
|
+
name: normalizeString(raw.name) ?? "",
|
|
148
|
+
attributes: attrsRes.value,
|
|
149
|
+
droppedAttributesCount: typeof raw.droppedAttributesCount === "number" ? raw.droppedAttributesCount : Number(raw.droppedAttributesCount ?? 0),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function linkFromJsonResult(raw: unknown, limits: OtelTraceOtlpLimits): Result<DecodedOtelLink | null, { message: string }> {
|
|
154
|
+
if (!isPlainObject(raw)) return Result.ok(null);
|
|
155
|
+
const traceId = normalizeString(raw.traceId);
|
|
156
|
+
const spanId = normalizeString(raw.spanId);
|
|
157
|
+
if (!traceId || !spanId) return Result.ok(null);
|
|
158
|
+
const attrsRes = keyValuesFromJsonResult(raw.attributes, limits, 0, false);
|
|
159
|
+
if (Result.isError(attrsRes)) return attrsRes;
|
|
160
|
+
return Result.ok({
|
|
161
|
+
traceId,
|
|
162
|
+
spanId,
|
|
163
|
+
traceState: normalizeString(raw.traceState),
|
|
164
|
+
attributes: attrsRes.value,
|
|
165
|
+
droppedAttributesCount: typeof raw.droppedAttributesCount === "number" ? raw.droppedAttributesCount : Number(raw.droppedAttributesCount ?? 0),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function spanFromJsonResult(
|
|
170
|
+
raw: unknown,
|
|
171
|
+
limits: OtelTraceOtlpLimits
|
|
172
|
+
): Result<Omit<DecodedOtelSpan, "resourceAttributes" | "resourceSchemaUrl" | "instrumentationScope"> | null, { message: string }> {
|
|
173
|
+
if (!isPlainObject(raw)) return Result.ok(null);
|
|
174
|
+
const traceId = normalizeString(raw.traceId);
|
|
175
|
+
const spanId = normalizeString(raw.spanId);
|
|
176
|
+
if (!traceId || !spanId) return Result.ok(null);
|
|
177
|
+
const status = isPlainObject(raw.status) ? raw.status : {};
|
|
178
|
+
const attrsRes = keyValuesFromJsonResult(raw.attributes, limits, 0, false);
|
|
179
|
+
if (Result.isError(attrsRes)) return attrsRes;
|
|
180
|
+
const events: DecodedOtelEvent[] = [];
|
|
181
|
+
if (Array.isArray(raw.events)) {
|
|
182
|
+
for (const eventRaw of raw.events) {
|
|
183
|
+
const eventRes = eventFromJsonResult(eventRaw, limits);
|
|
184
|
+
if (Result.isError(eventRes)) return eventRes;
|
|
185
|
+
if (eventRes.value) events.push(eventRes.value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const links: DecodedOtelLink[] = [];
|
|
189
|
+
if (Array.isArray(raw.links)) {
|
|
190
|
+
for (const linkRaw of raw.links) {
|
|
191
|
+
const linkRes = linkFromJsonResult(linkRaw, limits);
|
|
192
|
+
if (Result.isError(linkRes)) return linkRes;
|
|
193
|
+
if (linkRes.value) links.push(linkRes.value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Result.ok({
|
|
197
|
+
traceId,
|
|
198
|
+
spanId,
|
|
199
|
+
parentSpanId: normalizeString(raw.parentSpanId),
|
|
200
|
+
traceState: normalizeString(raw.traceState),
|
|
201
|
+
traceFlags: typeof raw.flags === "number" ? raw.flags : Number(raw.flags ?? raw.traceFlags ?? 0),
|
|
202
|
+
name: normalizeString(raw.name) ?? "",
|
|
203
|
+
kind: raw.kind as number | string | null | undefined,
|
|
204
|
+
startUnixNano: normalizeNanoString(raw.startTimeUnixNano),
|
|
205
|
+
endUnixNano: normalizeNanoString(raw.endTimeUnixNano),
|
|
206
|
+
status: {
|
|
207
|
+
code: status.code as number | string | null | undefined,
|
|
208
|
+
message: normalizeString(status.message),
|
|
209
|
+
},
|
|
210
|
+
attributes: attrsRes.value,
|
|
211
|
+
events,
|
|
212
|
+
links,
|
|
213
|
+
droppedAttributesCount: typeof raw.droppedAttributesCount === "number" ? raw.droppedAttributesCount : Number(raw.droppedAttributesCount ?? 0),
|
|
214
|
+
droppedEventsCount: typeof raw.droppedEventsCount === "number" ? raw.droppedEventsCount : Number(raw.droppedEventsCount ?? 0),
|
|
215
|
+
droppedLinksCount: typeof raw.droppedLinksCount === "number" ? raw.droppedLinksCount : Number(raw.droppedLinksCount ?? 0),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type OtlpDecodeCounters = {
|
|
220
|
+
resourceSpans: number;
|
|
221
|
+
scopeSpans: number;
|
|
222
|
+
spans: number;
|
|
223
|
+
rejectedSpans: number;
|
|
224
|
+
warnings: string[];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function incrementLimitCounter(
|
|
228
|
+
counters: OtlpDecodeCounters,
|
|
229
|
+
key: "resourceSpans" | "scopeSpans",
|
|
230
|
+
max: number,
|
|
231
|
+
label: string
|
|
232
|
+
): Result<void, { message: string }> {
|
|
233
|
+
counters[key] += 1;
|
|
234
|
+
if (counters[key] > max) return Result.err({ message: `too many ${label} in OTLP request (max ${max})` });
|
|
235
|
+
return Result.ok(undefined);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function acceptSpanForDecode(counters: OtlpDecodeCounters, limits: OtelTraceOtlpLimits): boolean {
|
|
239
|
+
counters.spans += 1;
|
|
240
|
+
if (counters.spans <= limits.maxSpansPerRequest) return true;
|
|
241
|
+
counters.rejectedSpans += 1;
|
|
242
|
+
appendWarning(counters.warnings, `too many spans in OTLP request (max ${limits.maxSpansPerRequest})`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function decodeJsonExportResult(body: Uint8Array, limits: OtelTraceOtlpLimits): Result<DecodedExport, { message: string }> {
|
|
247
|
+
let parsed: unknown;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(JSON_TEXT_DECODER.decode(body));
|
|
250
|
+
} catch {
|
|
251
|
+
return Result.err({ message: "invalid OTLP JSON" });
|
|
252
|
+
}
|
|
253
|
+
if (!isPlainObject(parsed)) return Result.err({ message: "OTLP JSON request must be an object" });
|
|
254
|
+
const out: DecodedOtelSpan[] = [];
|
|
255
|
+
const counters: OtlpDecodeCounters = { resourceSpans: 0, scopeSpans: 0, spans: 0, rejectedSpans: 0, warnings: [] };
|
|
256
|
+
const resourceSpans = Array.isArray(parsed.resourceSpans) ? parsed.resourceSpans : [];
|
|
257
|
+
for (const resourceSpanRaw of resourceSpans) {
|
|
258
|
+
const resourceLimitRes = incrementLimitCounter(counters, "resourceSpans", limits.maxResourceSpansPerRequest, "resourceSpans");
|
|
259
|
+
if (Result.isError(resourceLimitRes)) return resourceLimitRes;
|
|
260
|
+
if (!isPlainObject(resourceSpanRaw)) continue;
|
|
261
|
+
const resource = isPlainObject(resourceSpanRaw.resource) ? resourceSpanRaw.resource : {};
|
|
262
|
+
const resourceAttributesRes = keyValuesFromJsonResult(resource.attributes, limits, 0, false);
|
|
263
|
+
if (Result.isError(resourceAttributesRes)) return resourceAttributesRes;
|
|
264
|
+
const resourceAttributes = resourceAttributesRes.value;
|
|
265
|
+
const resourceSchemaUrl = normalizeString(resourceSpanRaw.schemaUrl);
|
|
266
|
+
const scopeSpans = [
|
|
267
|
+
...(Array.isArray(resourceSpanRaw.scopeSpans) ? resourceSpanRaw.scopeSpans : []),
|
|
268
|
+
...(Array.isArray(resourceSpanRaw.instrumentationLibrarySpans) ? resourceSpanRaw.instrumentationLibrarySpans : []),
|
|
269
|
+
];
|
|
270
|
+
for (const scopeSpanRaw of scopeSpans) {
|
|
271
|
+
const scopeLimitRes = incrementLimitCounter(counters, "scopeSpans", limits.maxScopeSpansPerRequest, "scopeSpans");
|
|
272
|
+
if (Result.isError(scopeLimitRes)) return scopeLimitRes;
|
|
273
|
+
if (!isPlainObject(scopeSpanRaw)) continue;
|
|
274
|
+
const scopeRaw = isPlainObject(scopeSpanRaw.scope) ? scopeSpanRaw.scope : isPlainObject(scopeSpanRaw.instrumentationLibrary) ? scopeSpanRaw.instrumentationLibrary : {};
|
|
275
|
+
const scopeAttrsRes = keyValuesFromJsonResult(scopeRaw.attributes, limits, 0, false);
|
|
276
|
+
if (Result.isError(scopeAttrsRes)) return scopeAttrsRes;
|
|
277
|
+
const scope = {
|
|
278
|
+
name: normalizeString(scopeRaw.name),
|
|
279
|
+
version: normalizeString(scopeRaw.version),
|
|
280
|
+
schemaUrl: normalizeString(scopeSpanRaw.schemaUrl),
|
|
281
|
+
attributes: scopeAttrsRes.value,
|
|
282
|
+
};
|
|
283
|
+
const spans = Array.isArray(scopeSpanRaw.spans) ? scopeSpanRaw.spans : [];
|
|
284
|
+
for (const spanRaw of spans) {
|
|
285
|
+
if (!acceptSpanForDecode(counters, limits)) continue;
|
|
286
|
+
const spanRes = spanFromJsonResult(spanRaw, limits);
|
|
287
|
+
if (Result.isError(spanRes)) return spanRes;
|
|
288
|
+
const span = spanRes.value;
|
|
289
|
+
if (!span) continue;
|
|
290
|
+
out.push({
|
|
291
|
+
...span,
|
|
292
|
+
resourceAttributes,
|
|
293
|
+
resourceSchemaUrl,
|
|
294
|
+
instrumentationScope: scope,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return Result.ok({ spans: out, rejectedSpans: counters.rejectedSpans, warnings: counters.warnings });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class ProtoReader {
|
|
303
|
+
private pos = 0;
|
|
304
|
+
|
|
305
|
+
constructor(private readonly bytes: Uint8Array) {}
|
|
306
|
+
|
|
307
|
+
eof(): boolean {
|
|
308
|
+
return this.pos >= this.bytes.byteLength;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
readTag(): Result<{ field: number; wire: number }, { message: string }> {
|
|
312
|
+
const tagRes = this.readVarint();
|
|
313
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
314
|
+
const tag = Number(tagRes.value);
|
|
315
|
+
if (tag === 0) return Result.err({ message: "invalid protobuf tag" });
|
|
316
|
+
return Result.ok({ field: tag >>> 3, wire: tag & 7 });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
readVarint(): Result<bigint, { message: string }> {
|
|
320
|
+
let shift = 0n;
|
|
321
|
+
let out = 0n;
|
|
322
|
+
while (shift <= 63n) {
|
|
323
|
+
if (this.pos >= this.bytes.byteLength) return Result.err({ message: "truncated protobuf varint" });
|
|
324
|
+
const byte = this.bytes[this.pos++]!;
|
|
325
|
+
out |= BigInt(byte & 0x7f) << shift;
|
|
326
|
+
if ((byte & 0x80) === 0) return Result.ok(out);
|
|
327
|
+
shift += 7n;
|
|
328
|
+
}
|
|
329
|
+
return Result.err({ message: "protobuf varint too long" });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
readFixed32(): Result<number, { message: string }> {
|
|
333
|
+
if (this.pos + 4 > this.bytes.byteLength) return Result.err({ message: "truncated protobuf fixed32" });
|
|
334
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.pos, 4);
|
|
335
|
+
this.pos += 4;
|
|
336
|
+
return Result.ok(view.getUint32(0, true));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
readFixed64(): Result<bigint, { message: string }> {
|
|
340
|
+
if (this.pos + 8 > this.bytes.byteLength) return Result.err({ message: "truncated protobuf fixed64" });
|
|
341
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.pos, 8);
|
|
342
|
+
this.pos += 8;
|
|
343
|
+
return Result.ok(view.getBigUint64(0, true));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
readDouble(): Result<number, { message: string }> {
|
|
347
|
+
if (this.pos + 8 > this.bytes.byteLength) return Result.err({ message: "truncated protobuf double" });
|
|
348
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.pos, 8);
|
|
349
|
+
this.pos += 8;
|
|
350
|
+
return Result.ok(view.getFloat64(0, true));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
readBytes(): Result<Uint8Array, { message: string }> {
|
|
354
|
+
const lenRes = this.readVarint();
|
|
355
|
+
if (Result.isError(lenRes)) return lenRes;
|
|
356
|
+
const len = Number(lenRes.value);
|
|
357
|
+
if (!Number.isSafeInteger(len) || len < 0 || this.pos + len > this.bytes.byteLength) {
|
|
358
|
+
return Result.err({ message: "truncated protobuf bytes" });
|
|
359
|
+
}
|
|
360
|
+
const out = this.bytes.slice(this.pos, this.pos + len);
|
|
361
|
+
this.pos += len;
|
|
362
|
+
return Result.ok(out);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
readString(): Result<string, { message: string }> {
|
|
366
|
+
const bytesRes = this.readBytes();
|
|
367
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
368
|
+
return Result.ok(JSON_TEXT_DECODER.decode(bytesRes.value));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
skip(wire: number): Result<void, { message: string }> {
|
|
372
|
+
if (wire === 0) {
|
|
373
|
+
const res = this.readVarint();
|
|
374
|
+
return Result.isError(res) ? res : Result.ok(undefined);
|
|
375
|
+
}
|
|
376
|
+
if (wire === 1) {
|
|
377
|
+
const res = this.readFixed64();
|
|
378
|
+
return Result.isError(res) ? res : Result.ok(undefined);
|
|
379
|
+
}
|
|
380
|
+
if (wire === 2) {
|
|
381
|
+
const res = this.readBytes();
|
|
382
|
+
return Result.isError(res) ? res : Result.ok(undefined);
|
|
383
|
+
}
|
|
384
|
+
if (wire === 5) {
|
|
385
|
+
const res = this.readFixed32();
|
|
386
|
+
return Result.isError(res) ? res : Result.ok(undefined);
|
|
387
|
+
}
|
|
388
|
+
return Result.err({ message: `unsupported protobuf wire type ${wire}` });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function signedInt64(value: bigint): string {
|
|
393
|
+
return value > 9_223_372_036_854_775_807n ? (value - 18_446_744_073_709_551_616n).toString() : value.toString();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function decodeAnyValue(bytes: Uint8Array, limits: OtelTraceOtlpLimits, depth = 0): Result<unknown, { message: string }> {
|
|
397
|
+
const depthRes = checkAnyValueDepthResult(depth, limits);
|
|
398
|
+
if (Result.isError(depthRes)) return depthRes;
|
|
399
|
+
const reader = new ProtoReader(bytes);
|
|
400
|
+
let value: unknown = null;
|
|
401
|
+
while (!reader.eof()) {
|
|
402
|
+
const tagRes = reader.readTag();
|
|
403
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
404
|
+
const { field, wire } = tagRes.value;
|
|
405
|
+
if (field === 1 && wire === 2) {
|
|
406
|
+
const res = reader.readString();
|
|
407
|
+
if (Result.isError(res)) return res;
|
|
408
|
+
value = res.value;
|
|
409
|
+
} else if (field === 2 && wire === 0) {
|
|
410
|
+
const res = reader.readVarint();
|
|
411
|
+
if (Result.isError(res)) return res;
|
|
412
|
+
value = res.value !== 0n;
|
|
413
|
+
} else if (field === 3 && wire === 0) {
|
|
414
|
+
const res = reader.readVarint();
|
|
415
|
+
if (Result.isError(res)) return res;
|
|
416
|
+
value = signedInt64(res.value);
|
|
417
|
+
} else if (field === 4 && wire === 1) {
|
|
418
|
+
const res = reader.readDouble();
|
|
419
|
+
if (Result.isError(res)) return res;
|
|
420
|
+
value = res.value;
|
|
421
|
+
} else if (field === 5 && wire === 2) {
|
|
422
|
+
const bytesRes = reader.readBytes();
|
|
423
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
424
|
+
const arrayRes = decodeArrayValue(bytesRes.value, limits, depth + 1);
|
|
425
|
+
if (Result.isError(arrayRes)) return arrayRes;
|
|
426
|
+
value = arrayRes.value;
|
|
427
|
+
} else if (field === 6 && wire === 2) {
|
|
428
|
+
const bytesRes = reader.readBytes();
|
|
429
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
430
|
+
const kvRes = decodeKeyValueList(bytesRes.value, limits, depth + 1, true);
|
|
431
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
432
|
+
value = kvRes.value;
|
|
433
|
+
} else if (field === 7 && wire === 2) {
|
|
434
|
+
const bytesRes = reader.readBytes();
|
|
435
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
436
|
+
value = Buffer.from(bytesRes.value).toString("base64");
|
|
437
|
+
} else {
|
|
438
|
+
const skipRes = reader.skip(wire);
|
|
439
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return Result.ok(value);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function decodeArrayValue(bytes: Uint8Array, limits: OtelTraceOtlpLimits, depth: number): Result<unknown[], { message: string }> {
|
|
446
|
+
const depthRes = checkAnyValueDepthResult(depth, limits);
|
|
447
|
+
if (Result.isError(depthRes)) return depthRes;
|
|
448
|
+
const reader = new ProtoReader(bytes);
|
|
449
|
+
const out: unknown[] = [];
|
|
450
|
+
while (!reader.eof()) {
|
|
451
|
+
const tagRes = reader.readTag();
|
|
452
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
453
|
+
if (tagRes.value.field === 1 && tagRes.value.wire === 2) {
|
|
454
|
+
if (out.length >= limits.maxArrayValuesPerAnyValue) {
|
|
455
|
+
return Result.err({ message: `OTLP AnyValue array too large (max ${limits.maxArrayValuesPerAnyValue})` });
|
|
456
|
+
}
|
|
457
|
+
const bytesRes = reader.readBytes();
|
|
458
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
459
|
+
const valueRes = decodeAnyValue(bytesRes.value, limits, depth);
|
|
460
|
+
if (Result.isError(valueRes)) return valueRes;
|
|
461
|
+
out.push(valueRes.value);
|
|
462
|
+
} else {
|
|
463
|
+
const skipRes = reader.skip(tagRes.value.wire);
|
|
464
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return Result.ok(out);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function decodeKeyValue(bytes: Uint8Array, limits: OtelTraceOtlpLimits, depth: number): Result<{ key: string; value: unknown } | null, { message: string }> {
|
|
471
|
+
const reader = new ProtoReader(bytes);
|
|
472
|
+
let key = "";
|
|
473
|
+
let value: unknown = null;
|
|
474
|
+
while (!reader.eof()) {
|
|
475
|
+
const tagRes = reader.readTag();
|
|
476
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
477
|
+
const { field, wire } = tagRes.value;
|
|
478
|
+
if (field === 1 && wire === 2) {
|
|
479
|
+
const keyRes = reader.readString();
|
|
480
|
+
if (Result.isError(keyRes)) return keyRes;
|
|
481
|
+
key = keyRes.value;
|
|
482
|
+
} else if (field === 2 && wire === 2) {
|
|
483
|
+
const bytesRes = reader.readBytes();
|
|
484
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
485
|
+
const valueRes = decodeAnyValue(bytesRes.value, limits, depth);
|
|
486
|
+
if (Result.isError(valueRes)) return valueRes;
|
|
487
|
+
value = valueRes.value;
|
|
488
|
+
} else {
|
|
489
|
+
const skipRes = reader.skip(wire);
|
|
490
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return Result.ok(key === "" ? null : { key, value });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function decodeKeyValueList(
|
|
497
|
+
bytes: Uint8Array,
|
|
498
|
+
limits: OtelTraceOtlpLimits,
|
|
499
|
+
depth: number,
|
|
500
|
+
enforceCollectionLimit: boolean
|
|
501
|
+
): Result<Record<string, unknown>, { message: string }> {
|
|
502
|
+
const depthRes = checkAnyValueDepthResult(depth, limits);
|
|
503
|
+
if (Result.isError(depthRes)) return depthRes;
|
|
504
|
+
const reader = new ProtoReader(bytes);
|
|
505
|
+
const out: Record<string, unknown> = {};
|
|
506
|
+
let count = 0;
|
|
507
|
+
while (!reader.eof()) {
|
|
508
|
+
const tagRes = reader.readTag();
|
|
509
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
510
|
+
if (tagRes.value.field === 1 && tagRes.value.wire === 2) {
|
|
511
|
+
count += 1;
|
|
512
|
+
if (enforceCollectionLimit && count > limits.maxKvListValuesPerAnyValue) {
|
|
513
|
+
return Result.err({ message: `OTLP AnyValue kvlist too large (max ${limits.maxKvListValuesPerAnyValue})` });
|
|
514
|
+
}
|
|
515
|
+
const bytesRes = reader.readBytes();
|
|
516
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
517
|
+
const kvRes = decodeKeyValue(bytesRes.value, limits, depth);
|
|
518
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
519
|
+
if (kvRes.value) out[kvRes.value.key] = kvRes.value.value;
|
|
520
|
+
} else {
|
|
521
|
+
const skipRes = reader.skip(tagRes.value.wire);
|
|
522
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return Result.ok(out);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function decodeResource(bytes: Uint8Array, limits: OtelTraceOtlpLimits): Result<Record<string, unknown>, { message: string }> {
|
|
529
|
+
return decodeKeyValueList(bytes, limits, 0, false);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function decodeScope(bytes: Uint8Array, limits: OtelTraceOtlpLimits): Result<ScopeSpansDecoded["scope"], { message: string }> {
|
|
533
|
+
const reader = new ProtoReader(bytes);
|
|
534
|
+
const scope: ScopeSpansDecoded["scope"] = { name: null, version: null, schemaUrl: null, attributes: {} };
|
|
535
|
+
while (!reader.eof()) {
|
|
536
|
+
const tagRes = reader.readTag();
|
|
537
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
538
|
+
const { field, wire } = tagRes.value;
|
|
539
|
+
if (field === 1 && wire === 2) {
|
|
540
|
+
const res = reader.readString();
|
|
541
|
+
if (Result.isError(res)) return res;
|
|
542
|
+
scope.name = res.value;
|
|
543
|
+
} else if (field === 2 && wire === 2) {
|
|
544
|
+
const res = reader.readString();
|
|
545
|
+
if (Result.isError(res)) return res;
|
|
546
|
+
scope.version = res.value;
|
|
547
|
+
} else if (field === 3 && wire === 2) {
|
|
548
|
+
const bytesRes = reader.readBytes();
|
|
549
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
550
|
+
const kvRes = decodeKeyValue(bytesRes.value, limits, 0);
|
|
551
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
552
|
+
if (kvRes.value) scope.attributes[kvRes.value.key] = kvRes.value.value;
|
|
553
|
+
} else {
|
|
554
|
+
const skipRes = reader.skip(wire);
|
|
555
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return Result.ok(scope);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function decodeStatus(bytes: Uint8Array): Result<{ code?: number; message?: string | null }, { message: string }> {
|
|
562
|
+
const reader = new ProtoReader(bytes);
|
|
563
|
+
const status: { code?: number; message?: string | null } = {};
|
|
564
|
+
while (!reader.eof()) {
|
|
565
|
+
const tagRes = reader.readTag();
|
|
566
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
567
|
+
const { field, wire } = tagRes.value;
|
|
568
|
+
if ((field === 1 || field === 3) && wire === 0) {
|
|
569
|
+
const res = reader.readVarint();
|
|
570
|
+
if (Result.isError(res)) return res;
|
|
571
|
+
status.code = Number(res.value);
|
|
572
|
+
} else if (field === 2 && wire === 2) {
|
|
573
|
+
const res = reader.readString();
|
|
574
|
+
if (Result.isError(res)) return res;
|
|
575
|
+
status.message = res.value;
|
|
576
|
+
} else {
|
|
577
|
+
const skipRes = reader.skip(wire);
|
|
578
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return Result.ok(status);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function decodeEvent(bytes: Uint8Array, limits: OtelTraceOtlpLimits): Result<DecodedOtelEvent, { message: string }> {
|
|
585
|
+
const reader = new ProtoReader(bytes);
|
|
586
|
+
const event: DecodedOtelEvent = { timeUnixNano: null, name: "", attributes: {}, droppedAttributesCount: 0 };
|
|
587
|
+
while (!reader.eof()) {
|
|
588
|
+
const tagRes = reader.readTag();
|
|
589
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
590
|
+
const { field, wire } = tagRes.value;
|
|
591
|
+
if (field === 1 && (wire === 1 || wire === 0)) {
|
|
592
|
+
const res = wire === 1 ? reader.readFixed64() : reader.readVarint();
|
|
593
|
+
if (Result.isError(res)) return res;
|
|
594
|
+
event.timeUnixNano = res.value.toString();
|
|
595
|
+
} else if (field === 2 && wire === 2) {
|
|
596
|
+
const res = reader.readString();
|
|
597
|
+
if (Result.isError(res)) return res;
|
|
598
|
+
event.name = res.value;
|
|
599
|
+
} else if (field === 3 && wire === 2) {
|
|
600
|
+
const bytesRes = reader.readBytes();
|
|
601
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
602
|
+
const kvRes = decodeKeyValue(bytesRes.value, limits, 0);
|
|
603
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
604
|
+
if (kvRes.value) event.attributes[kvRes.value.key] = kvRes.value.value;
|
|
605
|
+
} else if (field === 4 && wire === 0) {
|
|
606
|
+
const res = reader.readVarint();
|
|
607
|
+
if (Result.isError(res)) return res;
|
|
608
|
+
event.droppedAttributesCount = Number(res.value);
|
|
609
|
+
} else {
|
|
610
|
+
const skipRes = reader.skip(wire);
|
|
611
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return Result.ok(event);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function decodeLink(bytes: Uint8Array, limits: OtelTraceOtlpLimits): Result<DecodedOtelLink, { message: string }> {
|
|
618
|
+
const reader = new ProtoReader(bytes);
|
|
619
|
+
const link: DecodedOtelLink = { traceId: "", spanId: "", traceState: null, attributes: {}, droppedAttributesCount: 0 };
|
|
620
|
+
while (!reader.eof()) {
|
|
621
|
+
const tagRes = reader.readTag();
|
|
622
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
623
|
+
const { field, wire } = tagRes.value;
|
|
624
|
+
if (field === 1 && wire === 2) {
|
|
625
|
+
const res = reader.readBytes();
|
|
626
|
+
if (Result.isError(res)) return res;
|
|
627
|
+
link.traceId = hexFromBytes(res.value);
|
|
628
|
+
} else if (field === 2 && wire === 2) {
|
|
629
|
+
const res = reader.readBytes();
|
|
630
|
+
if (Result.isError(res)) return res;
|
|
631
|
+
link.spanId = hexFromBytes(res.value);
|
|
632
|
+
} else if (field === 3 && wire === 2) {
|
|
633
|
+
const res = reader.readString();
|
|
634
|
+
if (Result.isError(res)) return res;
|
|
635
|
+
link.traceState = res.value;
|
|
636
|
+
} else if (field === 4 && wire === 2) {
|
|
637
|
+
const bytesRes = reader.readBytes();
|
|
638
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
639
|
+
const kvRes = decodeKeyValue(bytesRes.value, limits, 0);
|
|
640
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
641
|
+
if (kvRes.value) link.attributes[kvRes.value.key] = kvRes.value.value;
|
|
642
|
+
} else if (field === 5 && wire === 0) {
|
|
643
|
+
const res = reader.readVarint();
|
|
644
|
+
if (Result.isError(res)) return res;
|
|
645
|
+
link.droppedAttributesCount = Number(res.value);
|
|
646
|
+
} else {
|
|
647
|
+
const skipRes = reader.skip(wire);
|
|
648
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return Result.ok(link);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function decodeSpan(
|
|
655
|
+
bytes: Uint8Array,
|
|
656
|
+
limits: OtelTraceOtlpLimits
|
|
657
|
+
): Result<Omit<DecodedOtelSpan, "resourceAttributes" | "resourceSchemaUrl" | "instrumentationScope">, { message: string }> {
|
|
658
|
+
const reader = new ProtoReader(bytes);
|
|
659
|
+
const span: Omit<DecodedOtelSpan, "resourceAttributes" | "resourceSchemaUrl" | "instrumentationScope"> = {
|
|
660
|
+
traceId: "",
|
|
661
|
+
spanId: "",
|
|
662
|
+
parentSpanId: null,
|
|
663
|
+
traceState: null,
|
|
664
|
+
traceFlags: null,
|
|
665
|
+
name: "",
|
|
666
|
+
kind: 0,
|
|
667
|
+
startUnixNano: null,
|
|
668
|
+
endUnixNano: null,
|
|
669
|
+
status: { code: 0, message: null },
|
|
670
|
+
attributes: {},
|
|
671
|
+
events: [],
|
|
672
|
+
links: [],
|
|
673
|
+
droppedAttributesCount: 0,
|
|
674
|
+
droppedEventsCount: 0,
|
|
675
|
+
droppedLinksCount: 0,
|
|
676
|
+
};
|
|
677
|
+
while (!reader.eof()) {
|
|
678
|
+
const tagRes = reader.readTag();
|
|
679
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
680
|
+
const { field, wire } = tagRes.value;
|
|
681
|
+
if (field === 1 && wire === 2) {
|
|
682
|
+
const res = reader.readBytes();
|
|
683
|
+
if (Result.isError(res)) return res;
|
|
684
|
+
span.traceId = hexFromBytes(res.value);
|
|
685
|
+
} else if (field === 2 && wire === 2) {
|
|
686
|
+
const res = reader.readBytes();
|
|
687
|
+
if (Result.isError(res)) return res;
|
|
688
|
+
span.spanId = hexFromBytes(res.value);
|
|
689
|
+
} else if (field === 3 && wire === 2) {
|
|
690
|
+
const res = reader.readString();
|
|
691
|
+
if (Result.isError(res)) return res;
|
|
692
|
+
span.traceState = res.value;
|
|
693
|
+
} else if (field === 4 && wire === 2) {
|
|
694
|
+
const res = reader.readBytes();
|
|
695
|
+
if (Result.isError(res)) return res;
|
|
696
|
+
span.parentSpanId = res.value.byteLength === 0 ? null : hexFromBytes(res.value);
|
|
697
|
+
} else if (field === 5 && wire === 2) {
|
|
698
|
+
const res = reader.readString();
|
|
699
|
+
if (Result.isError(res)) return res;
|
|
700
|
+
span.name = res.value;
|
|
701
|
+
} else if (field === 6 && wire === 0) {
|
|
702
|
+
const res = reader.readVarint();
|
|
703
|
+
if (Result.isError(res)) return res;
|
|
704
|
+
span.kind = Number(res.value);
|
|
705
|
+
} else if ((field === 7 || field === 8) && (wire === 1 || wire === 0)) {
|
|
706
|
+
const res = wire === 1 ? reader.readFixed64() : reader.readVarint();
|
|
707
|
+
if (Result.isError(res)) return res;
|
|
708
|
+
if (field === 7) span.startUnixNano = res.value.toString();
|
|
709
|
+
else span.endUnixNano = res.value.toString();
|
|
710
|
+
} else if (field === 9 && wire === 2) {
|
|
711
|
+
const bytesRes = reader.readBytes();
|
|
712
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
713
|
+
const kvRes = decodeKeyValue(bytesRes.value, limits, 0);
|
|
714
|
+
if (Result.isError(kvRes)) return kvRes;
|
|
715
|
+
if (kvRes.value) span.attributes[kvRes.value.key] = kvRes.value.value;
|
|
716
|
+
} else if (field === 10 && wire === 0) {
|
|
717
|
+
const res = reader.readVarint();
|
|
718
|
+
if (Result.isError(res)) return res;
|
|
719
|
+
span.droppedAttributesCount = Number(res.value);
|
|
720
|
+
} else if (field === 11 && wire === 2) {
|
|
721
|
+
const bytesRes = reader.readBytes();
|
|
722
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
723
|
+
const eventRes = decodeEvent(bytesRes.value, limits);
|
|
724
|
+
if (Result.isError(eventRes)) return eventRes;
|
|
725
|
+
span.events.push(eventRes.value);
|
|
726
|
+
} else if (field === 12 && wire === 0) {
|
|
727
|
+
const res = reader.readVarint();
|
|
728
|
+
if (Result.isError(res)) return res;
|
|
729
|
+
span.droppedEventsCount = Number(res.value);
|
|
730
|
+
} else if (field === 13 && wire === 2) {
|
|
731
|
+
const bytesRes = reader.readBytes();
|
|
732
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
733
|
+
const linkRes = decodeLink(bytesRes.value, limits);
|
|
734
|
+
if (Result.isError(linkRes)) return linkRes;
|
|
735
|
+
span.links.push(linkRes.value);
|
|
736
|
+
} else if (field === 14 && wire === 0) {
|
|
737
|
+
const res = reader.readVarint();
|
|
738
|
+
if (Result.isError(res)) return res;
|
|
739
|
+
span.droppedLinksCount = Number(res.value);
|
|
740
|
+
} else if (field === 15 && wire === 2) {
|
|
741
|
+
const bytesRes = reader.readBytes();
|
|
742
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
743
|
+
const statusRes = decodeStatus(bytesRes.value);
|
|
744
|
+
if (Result.isError(statusRes)) return statusRes;
|
|
745
|
+
span.status = statusRes.value;
|
|
746
|
+
} else if (field === 16 && (wire === 5 || wire === 0)) {
|
|
747
|
+
if (wire === 5) {
|
|
748
|
+
const res = reader.readFixed32();
|
|
749
|
+
if (Result.isError(res)) return res;
|
|
750
|
+
span.traceFlags = res.value;
|
|
751
|
+
} else {
|
|
752
|
+
const res = reader.readVarint();
|
|
753
|
+
if (Result.isError(res)) return res;
|
|
754
|
+
span.traceFlags = Number(res.value);
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
const skipRes = reader.skip(wire);
|
|
758
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return Result.ok(span);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function decodeScopeSpans(bytes: Uint8Array, limits: OtelTraceOtlpLimits, counters: OtlpDecodeCounters): Result<ScopeSpansDecoded, { message: string }> {
|
|
765
|
+
const reader = new ProtoReader(bytes);
|
|
766
|
+
const out: ScopeSpansDecoded = {
|
|
767
|
+
scope: { name: null, version: null, schemaUrl: null, attributes: {} },
|
|
768
|
+
spans: [],
|
|
769
|
+
};
|
|
770
|
+
while (!reader.eof()) {
|
|
771
|
+
const tagRes = reader.readTag();
|
|
772
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
773
|
+
const { field, wire } = tagRes.value;
|
|
774
|
+
if ((field === 1 || field === 1000) && wire === 2) {
|
|
775
|
+
const bytesRes = reader.readBytes();
|
|
776
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
777
|
+
const scopeRes = decodeScope(bytesRes.value, limits);
|
|
778
|
+
if (Result.isError(scopeRes)) return scopeRes;
|
|
779
|
+
out.scope = { ...out.scope, ...scopeRes.value };
|
|
780
|
+
} else if (field === 2 && wire === 2) {
|
|
781
|
+
const bytesRes = reader.readBytes();
|
|
782
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
783
|
+
if (!acceptSpanForDecode(counters, limits)) continue;
|
|
784
|
+
const spanRes = decodeSpan(bytesRes.value, limits);
|
|
785
|
+
if (Result.isError(spanRes)) return spanRes;
|
|
786
|
+
out.spans.push(spanRes.value);
|
|
787
|
+
} else if (field === 3 && wire === 2) {
|
|
788
|
+
const res = reader.readString();
|
|
789
|
+
if (Result.isError(res)) return res;
|
|
790
|
+
out.scope.schemaUrl = res.value;
|
|
791
|
+
} else {
|
|
792
|
+
const skipRes = reader.skip(wire);
|
|
793
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return Result.ok(out);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function decodeResourceSpans(bytes: Uint8Array, limits: OtelTraceOtlpLimits, counters: OtlpDecodeCounters): Result<ResourceSpansDecoded, { message: string }> {
|
|
800
|
+
const reader = new ProtoReader(bytes);
|
|
801
|
+
const out: ResourceSpansDecoded = { resourceAttributes: {}, resourceSchemaUrl: null, scopeSpans: [] };
|
|
802
|
+
while (!reader.eof()) {
|
|
803
|
+
const tagRes = reader.readTag();
|
|
804
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
805
|
+
const { field, wire } = tagRes.value;
|
|
806
|
+
if (field === 1 && wire === 2) {
|
|
807
|
+
const bytesRes = reader.readBytes();
|
|
808
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
809
|
+
const resourceRes = decodeResource(bytesRes.value, limits);
|
|
810
|
+
if (Result.isError(resourceRes)) return resourceRes;
|
|
811
|
+
out.resourceAttributes = resourceRes.value;
|
|
812
|
+
} else if ((field === 2 || field === 1000) && wire === 2) {
|
|
813
|
+
const scopeLimitRes = incrementLimitCounter(counters, "scopeSpans", limits.maxScopeSpansPerRequest, "scopeSpans");
|
|
814
|
+
if (Result.isError(scopeLimitRes)) return scopeLimitRes;
|
|
815
|
+
const bytesRes = reader.readBytes();
|
|
816
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
817
|
+
const scopeRes = decodeScopeSpans(bytesRes.value, limits, counters);
|
|
818
|
+
if (Result.isError(scopeRes)) return scopeRes;
|
|
819
|
+
out.scopeSpans.push(scopeRes.value);
|
|
820
|
+
} else if (field === 3 && wire === 2) {
|
|
821
|
+
const res = reader.readString();
|
|
822
|
+
if (Result.isError(res)) return res;
|
|
823
|
+
out.resourceSchemaUrl = res.value;
|
|
824
|
+
} else {
|
|
825
|
+
const skipRes = reader.skip(wire);
|
|
826
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return Result.ok(out);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function decodeProtobufExportResult(body: Uint8Array, limits: OtelTraceOtlpLimits): Result<DecodedExport, { message: string }> {
|
|
833
|
+
const reader = new ProtoReader(body);
|
|
834
|
+
const out: DecodedOtelSpan[] = [];
|
|
835
|
+
const counters: OtlpDecodeCounters = { resourceSpans: 0, scopeSpans: 0, spans: 0, rejectedSpans: 0, warnings: [] };
|
|
836
|
+
while (!reader.eof()) {
|
|
837
|
+
const tagRes = reader.readTag();
|
|
838
|
+
if (Result.isError(tagRes)) return tagRes;
|
|
839
|
+
if (tagRes.value.field === 1 && tagRes.value.wire === 2) {
|
|
840
|
+
const resourceLimitRes = incrementLimitCounter(counters, "resourceSpans", limits.maxResourceSpansPerRequest, "resourceSpans");
|
|
841
|
+
if (Result.isError(resourceLimitRes)) return resourceLimitRes;
|
|
842
|
+
const bytesRes = reader.readBytes();
|
|
843
|
+
if (Result.isError(bytesRes)) return bytesRes;
|
|
844
|
+
const resourceSpansRes = decodeResourceSpans(bytesRes.value, limits, counters);
|
|
845
|
+
if (Result.isError(resourceSpansRes)) return resourceSpansRes;
|
|
846
|
+
for (const scopeSpans of resourceSpansRes.value.scopeSpans) {
|
|
847
|
+
for (const span of scopeSpans.spans) {
|
|
848
|
+
out.push({
|
|
849
|
+
...span,
|
|
850
|
+
resourceAttributes: resourceSpansRes.value.resourceAttributes,
|
|
851
|
+
resourceSchemaUrl: resourceSpansRes.value.resourceSchemaUrl,
|
|
852
|
+
instrumentationScope: scopeSpans.scope,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
const skipRes = reader.skip(tagRes.value.wire);
|
|
858
|
+
if (Result.isError(skipRes)) return skipRes;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return Result.ok({ spans: out, rejectedSpans: counters.rejectedSpans, warnings: counters.warnings });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function decodeBody(args: {
|
|
865
|
+
contentType: string;
|
|
866
|
+
contentEncoding: string | null;
|
|
867
|
+
body: Uint8Array;
|
|
868
|
+
maxDecodedBytes: number;
|
|
869
|
+
limits: OtelTraceOtlpLimits;
|
|
870
|
+
}): Result<DecodedExport & { responseEncoding: "protobuf" | "json" }, OtlpTraceExportError> {
|
|
871
|
+
let body = args.body;
|
|
872
|
+
const maxDecodedBytes = Math.min(args.maxDecodedBytes, args.limits.maxDecodedBytes);
|
|
873
|
+
const encoding = args.contentEncoding?.trim().toLowerCase() ?? "";
|
|
874
|
+
if (encoding !== "" && encoding !== "identity" && encoding !== "gzip") {
|
|
875
|
+
return Result.err({ status: 415, message: "unsupported content-encoding" });
|
|
876
|
+
}
|
|
877
|
+
if (encoding === "gzip") {
|
|
878
|
+
if (body.byteLength > args.limits.maxCompressedBytes) {
|
|
879
|
+
return Result.err({ status: 413, message: `compressed OTLP body too large (max ${args.limits.maxCompressedBytes})` });
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
body = new Uint8Array(gunzipSync(body, { maxOutputLength: maxDecodedBytes }));
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
885
|
+
if (code === "ERR_BUFFER_TOO_LARGE") {
|
|
886
|
+
return Result.err({ status: 413, message: `decoded OTLP body too large (max ${maxDecodedBytes})` });
|
|
887
|
+
}
|
|
888
|
+
return Result.err({ status: 400, message: "invalid gzip body" });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (body.byteLength > maxDecodedBytes) {
|
|
892
|
+
return Result.err({ status: 413, message: `decoded OTLP body too large (max ${maxDecodedBytes})` });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const contentType = baseContentType(args.contentType);
|
|
896
|
+
if (contentType === JSON_CONTENT_TYPE) {
|
|
897
|
+
const spansRes = decodeJsonExportResult(body, args.limits);
|
|
898
|
+
if (Result.isError(spansRes)) return Result.err({ status: 400, message: spansRes.error.message });
|
|
899
|
+
return Result.ok({ ...spansRes.value, responseEncoding: "json" });
|
|
900
|
+
}
|
|
901
|
+
if (contentType === PROTOBUF_CONTENT_TYPE) {
|
|
902
|
+
const spansRes = decodeProtobufExportResult(body, args.limits);
|
|
903
|
+
if (Result.isError(spansRes)) return Result.err({ status: 400, message: spansRes.error.message });
|
|
904
|
+
return Result.ok({ ...spansRes.value, responseEncoding: "protobuf" });
|
|
905
|
+
}
|
|
906
|
+
return Result.err({ status: 415, message: "OTLP traces require application/x-protobuf or application/json" });
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function decodeOtlpTraceExportRequestResult(args: {
|
|
910
|
+
stream: string;
|
|
911
|
+
profile: OtelTracesStreamProfile;
|
|
912
|
+
contentType: string;
|
|
913
|
+
contentEncoding: string | null;
|
|
914
|
+
body: Uint8Array;
|
|
915
|
+
maxDecodedBytes: number;
|
|
916
|
+
}): Result<OtlpTraceExportResult, OtlpTraceExportError> {
|
|
917
|
+
const limits = { ...DEFAULT_OTLP_LIMITS, ...(args.profile.otlpLimits ?? {}) };
|
|
918
|
+
const decodedRes = decodeBody({ ...args, limits });
|
|
919
|
+
if (Result.isError(decodedRes)) return decodedRes;
|
|
920
|
+
const records: OtlpTraceExportResult["records"] = [];
|
|
921
|
+
const warnings: string[] = [...decodedRes.value.warnings];
|
|
922
|
+
let rejectedSpans = decodedRes.value.rejectedSpans;
|
|
923
|
+
for (const span of decodedRes.value.spans) {
|
|
924
|
+
const normalizedRes = normalizeOtelDecodedSpanResult(args.profile, span);
|
|
925
|
+
if (Result.isError(normalizedRes)) {
|
|
926
|
+
rejectedSpans += 1;
|
|
927
|
+
if (warnings.length < 8) warnings.push(normalizedRes.error.message);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
records.push({
|
|
931
|
+
value: normalizedRes.value,
|
|
932
|
+
routingKey: normalizedRes.value.traceId,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return Result.ok({
|
|
936
|
+
records,
|
|
937
|
+
acceptedSpans: records.length,
|
|
938
|
+
rejectedSpans,
|
|
939
|
+
warnings,
|
|
940
|
+
responseEncoding: decodedRes.value.responseEncoding,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function writeVarint(out: number[], value: bigint): void {
|
|
945
|
+
let n = value;
|
|
946
|
+
while (n >= 0x80n) {
|
|
947
|
+
out.push(Number((n & 0x7fn) | 0x80n));
|
|
948
|
+
n >>= 7n;
|
|
949
|
+
}
|
|
950
|
+
out.push(Number(n));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function writeTag(out: number[], field: number, wire: number): void {
|
|
954
|
+
writeVarint(out, BigInt((field << 3) | wire));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function writeString(out: number[], field: number, value: string): void {
|
|
958
|
+
const bytes = new TextEncoder().encode(value);
|
|
959
|
+
writeTag(out, field, 2);
|
|
960
|
+
writeVarint(out, BigInt(bytes.byteLength));
|
|
961
|
+
out.push(...bytes);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function writeInt64(out: number[], field: number, value: bigint): void {
|
|
965
|
+
writeTag(out, field, 0);
|
|
966
|
+
writeVarint(out, value);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function writeMessage(out: number[], field: number, body: number[]): void {
|
|
970
|
+
writeTag(out, field, 2);
|
|
971
|
+
writeVarint(out, BigInt(body.length));
|
|
972
|
+
out.push(...body);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function encodeOtlpTraceExportResponse(result: Pick<OtlpTraceExportResult, "rejectedSpans" | "warnings" | "responseEncoding">): {
|
|
976
|
+
contentType: string;
|
|
977
|
+
body: Uint8Array | string;
|
|
978
|
+
} {
|
|
979
|
+
const message =
|
|
980
|
+
result.rejectedSpans > 0
|
|
981
|
+
? `${result.rejectedSpans} spans rejected${result.warnings.length > 0 ? `: ${result.warnings.join("; ")}` : ""}`
|
|
982
|
+
: "";
|
|
983
|
+
if (result.responseEncoding === "json") {
|
|
984
|
+
if (result.rejectedSpans === 0) return { contentType: "application/json; charset=utf-8", body: "{}" };
|
|
985
|
+
return {
|
|
986
|
+
contentType: "application/json; charset=utf-8",
|
|
987
|
+
body: JSON.stringify({
|
|
988
|
+
partialSuccess: {
|
|
989
|
+
rejectedSpans: result.rejectedSpans,
|
|
990
|
+
errorMessage: message,
|
|
991
|
+
},
|
|
992
|
+
}),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (result.rejectedSpans === 0) return { contentType: PROTOBUF_CONTENT_TYPE, body: new Uint8Array() };
|
|
996
|
+
const partial: number[] = [];
|
|
997
|
+
writeInt64(partial, 1, BigInt(result.rejectedSpans));
|
|
998
|
+
writeString(partial, 2, message);
|
|
999
|
+
const response: number[] = [];
|
|
1000
|
+
writeMessage(response, 1, partial);
|
|
1001
|
+
return { contentType: PROTOBUF_CONTENT_TYPE, body: new Uint8Array(response) };
|
|
1002
|
+
}
|