@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,955 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
import type { PreparedJsonRecord } from "../profile";
|
|
4
|
+
import { expectPlainObjectResult, isPlainObject } from "../profile";
|
|
5
|
+
|
|
6
|
+
export type OTelSpanKind = "unspecified" | "internal" | "server" | "client" | "producer" | "consumer";
|
|
7
|
+
export type OTelStatusCode = "unset" | "ok" | "error";
|
|
8
|
+
export type DbStatementMode = "drop" | "raw";
|
|
9
|
+
export type UrlMode = "drop_query" | "raw";
|
|
10
|
+
|
|
11
|
+
export type OtelTraceAttributeLimits = {
|
|
12
|
+
maxAttributeValueBytes: number;
|
|
13
|
+
maxAttributesPerSpan: number;
|
|
14
|
+
maxEventsPerSpan: number;
|
|
15
|
+
maxLinksPerSpan: number;
|
|
16
|
+
maxStatementBytes: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type OtelTraceOtlpLimits = {
|
|
20
|
+
maxCompressedBytes: number;
|
|
21
|
+
maxDecodedBytes: number;
|
|
22
|
+
maxResourceSpansPerRequest: number;
|
|
23
|
+
maxScopeSpansPerRequest: number;
|
|
24
|
+
maxSpansPerRequest: number;
|
|
25
|
+
maxAnyValueDepth: number;
|
|
26
|
+
maxArrayValuesPerAnyValue: number;
|
|
27
|
+
maxKvListValuesPerAnyValue: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type OtelTraceStoreConfig = {
|
|
31
|
+
rawResourceAttributes: boolean;
|
|
32
|
+
rawSpanAttributes: boolean;
|
|
33
|
+
rawEvents: boolean;
|
|
34
|
+
rawLinks: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type OtelTracesStreamProfile = {
|
|
38
|
+
kind: "otel-traces";
|
|
39
|
+
redactKeys?: string[];
|
|
40
|
+
requestIdAttributes?: string[];
|
|
41
|
+
attributeLimits?: Partial<OtelTraceAttributeLimits>;
|
|
42
|
+
store?: Partial<OtelTraceStoreConfig>;
|
|
43
|
+
dbStatementMode?: DbStatementMode;
|
|
44
|
+
urlMode?: UrlMode;
|
|
45
|
+
otlpLimits?: Partial<OtelTraceOtlpLimits>;
|
|
46
|
+
observability?: {
|
|
47
|
+
request?: {
|
|
48
|
+
eventsStream: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type DecodedOtelEvent = {
|
|
54
|
+
timeUnixNano: string | null;
|
|
55
|
+
name: string;
|
|
56
|
+
attributes: Record<string, unknown>;
|
|
57
|
+
droppedAttributesCount?: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type DecodedOtelLink = {
|
|
61
|
+
traceId: string;
|
|
62
|
+
spanId: string;
|
|
63
|
+
traceState: string | null;
|
|
64
|
+
attributes: Record<string, unknown>;
|
|
65
|
+
droppedAttributesCount?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type DecodedOtelSpan = {
|
|
69
|
+
traceId: string;
|
|
70
|
+
spanId: string;
|
|
71
|
+
parentSpanId?: string | null;
|
|
72
|
+
traceState?: string | null;
|
|
73
|
+
traceFlags?: number | null;
|
|
74
|
+
name: string;
|
|
75
|
+
kind?: number | string | null;
|
|
76
|
+
startUnixNano?: string | null;
|
|
77
|
+
endUnixNano?: string | null;
|
|
78
|
+
timestamp?: string | null;
|
|
79
|
+
status?: {
|
|
80
|
+
code?: number | string | null;
|
|
81
|
+
message?: string | null;
|
|
82
|
+
};
|
|
83
|
+
resourceSchemaUrl?: string | null;
|
|
84
|
+
resourceAttributes: Record<string, unknown>;
|
|
85
|
+
instrumentationScope?: {
|
|
86
|
+
name?: string | null;
|
|
87
|
+
version?: string | null;
|
|
88
|
+
schemaUrl?: string | null;
|
|
89
|
+
attributes?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
attributes: Record<string, unknown>;
|
|
92
|
+
events: DecodedOtelEvent[];
|
|
93
|
+
links: DecodedOtelLink[];
|
|
94
|
+
droppedAttributesCount?: number;
|
|
95
|
+
droppedEventsCount?: number;
|
|
96
|
+
droppedLinksCount?: number;
|
|
97
|
+
requestId?: string | null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type CanonicalOtelSpan = {
|
|
101
|
+
schemaVersion: 1;
|
|
102
|
+
signal: "trace.span";
|
|
103
|
+
timestamp: string;
|
|
104
|
+
endTimestamp: string | null;
|
|
105
|
+
startUnixNano: string | null;
|
|
106
|
+
endUnixNano: string | null;
|
|
107
|
+
duration: number | null;
|
|
108
|
+
traceId: string;
|
|
109
|
+
spanId: string;
|
|
110
|
+
parentSpanId: string | null;
|
|
111
|
+
traceState: string | null;
|
|
112
|
+
traceFlags: {
|
|
113
|
+
sampled: boolean;
|
|
114
|
+
raw: number | null;
|
|
115
|
+
};
|
|
116
|
+
name: string;
|
|
117
|
+
kind: OTelSpanKind;
|
|
118
|
+
status: {
|
|
119
|
+
code: OTelStatusCode;
|
|
120
|
+
message: string | null;
|
|
121
|
+
};
|
|
122
|
+
service: string | null;
|
|
123
|
+
serviceNamespace: string | null;
|
|
124
|
+
serviceInstanceId: string | null;
|
|
125
|
+
environment: string | null;
|
|
126
|
+
version: string | null;
|
|
127
|
+
region: string | null;
|
|
128
|
+
requestId: string | null;
|
|
129
|
+
http: {
|
|
130
|
+
method: string | null;
|
|
131
|
+
route: string | null;
|
|
132
|
+
path: string | null;
|
|
133
|
+
target: string | null;
|
|
134
|
+
url: string | null;
|
|
135
|
+
statusCode: number | null;
|
|
136
|
+
userAgent: string | null;
|
|
137
|
+
};
|
|
138
|
+
db: {
|
|
139
|
+
system: string | null;
|
|
140
|
+
name: string | null;
|
|
141
|
+
operation: string | null;
|
|
142
|
+
statement: string | null;
|
|
143
|
+
};
|
|
144
|
+
rpc: {
|
|
145
|
+
system: string | null;
|
|
146
|
+
service: string | null;
|
|
147
|
+
method: string | null;
|
|
148
|
+
};
|
|
149
|
+
messaging: {
|
|
150
|
+
system: string | null;
|
|
151
|
+
destination: string | null;
|
|
152
|
+
operation: string | null;
|
|
153
|
+
};
|
|
154
|
+
error: {
|
|
155
|
+
isError: boolean;
|
|
156
|
+
type: string | null;
|
|
157
|
+
message: string | null;
|
|
158
|
+
stacktrace: string | null;
|
|
159
|
+
};
|
|
160
|
+
instrumentationScope: {
|
|
161
|
+
name: string | null;
|
|
162
|
+
version: string | null;
|
|
163
|
+
schemaUrl: string | null;
|
|
164
|
+
attributes: Record<string, unknown>;
|
|
165
|
+
};
|
|
166
|
+
resource: {
|
|
167
|
+
schemaUrl: string | null;
|
|
168
|
+
attributes: Record<string, unknown>;
|
|
169
|
+
};
|
|
170
|
+
attributes: Record<string, unknown>;
|
|
171
|
+
events: Array<{
|
|
172
|
+
timestamp: string | null;
|
|
173
|
+
timeUnixNano: string | null;
|
|
174
|
+
name: string;
|
|
175
|
+
attributes: Record<string, unknown>;
|
|
176
|
+
droppedAttributesCount?: number;
|
|
177
|
+
}>;
|
|
178
|
+
eventNames: string[];
|
|
179
|
+
links: Array<{
|
|
180
|
+
traceId: string;
|
|
181
|
+
spanId: string;
|
|
182
|
+
traceState: string | null;
|
|
183
|
+
attributes: Record<string, unknown>;
|
|
184
|
+
droppedAttributesCount?: number;
|
|
185
|
+
}>;
|
|
186
|
+
dropped: {
|
|
187
|
+
attributes: number;
|
|
188
|
+
events: number;
|
|
189
|
+
links: number;
|
|
190
|
+
};
|
|
191
|
+
redaction: {
|
|
192
|
+
keys: string[];
|
|
193
|
+
};
|
|
194
|
+
identity: {
|
|
195
|
+
spanKey: string;
|
|
196
|
+
dedupeKey: string;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
201
|
+
const TEXT_DECODER = new TextDecoder();
|
|
202
|
+
const REDACTED_VALUE = "[REDACTED]";
|
|
203
|
+
|
|
204
|
+
export const DEFAULT_OTEL_TRACE_REDACT_KEYS = [
|
|
205
|
+
"password",
|
|
206
|
+
"token",
|
|
207
|
+
"secret",
|
|
208
|
+
"authorization",
|
|
209
|
+
"cookie",
|
|
210
|
+
"apikey",
|
|
211
|
+
"api_key",
|
|
212
|
+
"set-cookie",
|
|
213
|
+
"x-api-key",
|
|
214
|
+
] as const;
|
|
215
|
+
|
|
216
|
+
export const DEFAULT_REQUEST_ID_ATTRIBUTES = [
|
|
217
|
+
"request.id",
|
|
218
|
+
"http.request_id",
|
|
219
|
+
"http.request.header.x_request_id",
|
|
220
|
+
"http.request.header.x-request-id",
|
|
221
|
+
"http.request.header.x_correlation_id",
|
|
222
|
+
"http.request.header.x-correlation-id",
|
|
223
|
+
"correlation.id",
|
|
224
|
+
] as const;
|
|
225
|
+
|
|
226
|
+
export const DEFAULT_ATTRIBUTE_LIMITS: OtelTraceAttributeLimits = {
|
|
227
|
+
maxAttributeValueBytes: 8192,
|
|
228
|
+
maxAttributesPerSpan: 256,
|
|
229
|
+
maxEventsPerSpan: 128,
|
|
230
|
+
maxLinksPerSpan: 128,
|
|
231
|
+
maxStatementBytes: 4096,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const DEFAULT_OTLP_LIMITS: OtelTraceOtlpLimits = {
|
|
235
|
+
maxCompressedBytes: 4 * 1024 * 1024,
|
|
236
|
+
maxDecodedBytes: 16 * 1024 * 1024,
|
|
237
|
+
maxResourceSpansPerRequest: 1024,
|
|
238
|
+
maxScopeSpansPerRequest: 4096,
|
|
239
|
+
maxSpansPerRequest: 50_000,
|
|
240
|
+
maxAnyValueDepth: 16,
|
|
241
|
+
maxArrayValuesPerAnyValue: 256,
|
|
242
|
+
maxKvListValuesPerAnyValue: 256,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const DEFAULT_STORE_CONFIG: OtelTraceStoreConfig = {
|
|
246
|
+
rawResourceAttributes: true,
|
|
247
|
+
rawSpanAttributes: true,
|
|
248
|
+
rawEvents: true,
|
|
249
|
+
rawLinks: true,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const DEFAULT_URL_MODE: UrlMode = "drop_query";
|
|
253
|
+
|
|
254
|
+
function normalizeString(value: unknown): string | null {
|
|
255
|
+
if (typeof value !== "string") return null;
|
|
256
|
+
const trimmed = value.trim();
|
|
257
|
+
return trimmed === "" ? null : trimmed;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeNumber(value: unknown): number | null {
|
|
261
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
262
|
+
if (typeof value === "bigint") return Number(value);
|
|
263
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
264
|
+
const parsed = Number(value);
|
|
265
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeInteger(value: unknown): number | null {
|
|
271
|
+
const n = normalizeNumber(value);
|
|
272
|
+
return n != null && Number.isInteger(n) ? n : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeNanoString(value: unknown): string | null {
|
|
276
|
+
if (value == null) return null;
|
|
277
|
+
if (typeof value === "bigint") return value >= 0n ? value.toString() : null;
|
|
278
|
+
if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0) {
|
|
279
|
+
return BigInt(value).toString();
|
|
280
|
+
}
|
|
281
|
+
if (typeof value === "string") {
|
|
282
|
+
const trimmed = value.trim();
|
|
283
|
+
if (/^(0|[1-9][0-9]*)$/.test(trimmed)) return trimmed;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isoFromUnixNano(nanoString: string | null): string | null {
|
|
289
|
+
if (!nanoString) return null;
|
|
290
|
+
try {
|
|
291
|
+
const ms = BigInt(nanoString) / 1_000_000n;
|
|
292
|
+
const date = new Date(Number(ms));
|
|
293
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
294
|
+
return date.toISOString();
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function durationMs(startUnixNano: string | null, endUnixNano: string | null): Result<number | null, { message: string }> {
|
|
301
|
+
if (!startUnixNano || !endUnixNano) return Result.ok(null);
|
|
302
|
+
const start = BigInt(startUnixNano);
|
|
303
|
+
const end = BigInt(endUnixNano);
|
|
304
|
+
if (end < start) return Result.err({ message: "endTimeUnixNano must be greater than or equal to startTimeUnixNano" });
|
|
305
|
+
return Result.ok(Number(end - start) / 1_000_000);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function normalizeHexIdResult(raw: unknown, chars: number, field: string): Result<string, { message: string }> {
|
|
309
|
+
const value = normalizeString(raw)?.toLowerCase() ?? "";
|
|
310
|
+
if (!new RegExp(`^[0-9a-f]{${chars}}$`).test(value)) {
|
|
311
|
+
return Result.err({ message: `${field} must be ${chars} lowercase hex characters` });
|
|
312
|
+
}
|
|
313
|
+
if (/^0+$/.test(value)) return Result.err({ message: `${field} must not be all zeroes` });
|
|
314
|
+
return Result.ok(value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeParentSpanIdResult(raw: unknown): Result<string | null, { message: string }> {
|
|
318
|
+
const value = normalizeString(raw);
|
|
319
|
+
if (!value) return Result.ok(null);
|
|
320
|
+
const lowered = value.toLowerCase();
|
|
321
|
+
if (/^0+$/.test(lowered)) return Result.ok(null);
|
|
322
|
+
return normalizeHexIdResult(lowered, 16, "parentSpanId");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeSpanKind(value: unknown): OTelSpanKind {
|
|
326
|
+
if (typeof value === "number") {
|
|
327
|
+
if (value === 1) return "internal";
|
|
328
|
+
if (value === 2) return "server";
|
|
329
|
+
if (value === 3) return "client";
|
|
330
|
+
if (value === 4) return "producer";
|
|
331
|
+
if (value === 5) return "consumer";
|
|
332
|
+
return "unspecified";
|
|
333
|
+
}
|
|
334
|
+
const raw = normalizeString(value)?.toLowerCase().replace(/^span_kind_/, "");
|
|
335
|
+
if (raw === "internal" || raw === "server" || raw === "client" || raw === "producer" || raw === "consumer") return raw;
|
|
336
|
+
return "unspecified";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeStatusCode(value: unknown): OTelStatusCode {
|
|
340
|
+
if (typeof value === "number") {
|
|
341
|
+
if (value === 1) return "ok";
|
|
342
|
+
if (value === 2) return "error";
|
|
343
|
+
return "unset";
|
|
344
|
+
}
|
|
345
|
+
const raw = normalizeString(value)?.toLowerCase().replace(/^status_code_/, "");
|
|
346
|
+
if (raw === "ok" || raw === "error") return raw;
|
|
347
|
+
return "unset";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
351
|
+
const bytes = TEXT_ENCODER.encode(value);
|
|
352
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
353
|
+
return TEXT_DECODER.decode(bytes.slice(0, Math.max(0, maxBytes)));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function truncateNullableString(value: string | null, maxBytes: number): string | null {
|
|
357
|
+
return value == null ? null : truncateUtf8(value, maxBytes);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function stripUrlQueryAndFragment(value: string): string {
|
|
361
|
+
const fragmentStart = value.indexOf("#");
|
|
362
|
+
const withoutFragment = fragmentStart >= 0 ? value.slice(0, fragmentStart) : value;
|
|
363
|
+
const queryStart = withoutFragment.indexOf("?");
|
|
364
|
+
return queryStart >= 0 ? withoutFragment.slice(0, queryStart) : withoutFragment;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function sanitizeUrl(value: string | null, urlMode: UrlMode, maxBytes: number): string | null {
|
|
368
|
+
if (!value) return null;
|
|
369
|
+
const sanitized = urlMode === "raw" ? value : stripUrlQueryAndFragment(value);
|
|
370
|
+
const normalized = normalizeString(sanitized);
|
|
371
|
+
return normalized ? truncateUtf8(normalized, maxBytes) : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function redactionKeyCandidates(key: string): Set<string> {
|
|
375
|
+
const lowered = key.trim().toLowerCase();
|
|
376
|
+
const out = new Set<string>();
|
|
377
|
+
if (lowered === "") return out;
|
|
378
|
+
out.add(lowered);
|
|
379
|
+
|
|
380
|
+
const dotted = lowered.split(".").filter((part) => part !== "");
|
|
381
|
+
for (let i = 0; i < dotted.length; i++) out.add(dotted.slice(i).join("."));
|
|
382
|
+
const terminal = dotted.at(-1) ?? lowered;
|
|
383
|
+
out.add(terminal);
|
|
384
|
+
out.add(terminal.replace(/[-_]/g, ""));
|
|
385
|
+
|
|
386
|
+
const tokens = lowered.split(/[._-]+/).filter((part) => part !== "");
|
|
387
|
+
for (let length = 1; length <= Math.min(4, tokens.length); length++) {
|
|
388
|
+
const suffix = tokens.slice(tokens.length - length);
|
|
389
|
+
out.add(suffix.join("."));
|
|
390
|
+
out.add(suffix.join("-"));
|
|
391
|
+
out.add(suffix.join("_"));
|
|
392
|
+
out.add(suffix.join(""));
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function shouldRedactAttributeKey(key: string, redactKeys: Set<string>): boolean {
|
|
398
|
+
for (const candidate of redactionKeyCandidates(key)) {
|
|
399
|
+
if (redactKeys.has(candidate)) return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function sanitizeAttributeValue(value: unknown, redactKeys: Set<string>, path: string, maxBytes: number): { value: unknown; redacted: string[] } {
|
|
405
|
+
if (typeof value === "string") return { value: truncateUtf8(value, maxBytes), redacted: [] };
|
|
406
|
+
if (typeof value === "number") return { value: Number.isFinite(value) ? value : null, redacted: [] };
|
|
407
|
+
if (typeof value === "boolean" || value === null) return { value, redacted: [] };
|
|
408
|
+
if (typeof value === "bigint") return { value: value.toString(), redacted: [] };
|
|
409
|
+
if (value instanceof Uint8Array) return { value: Buffer.from(value).toString("base64"), redacted: [] };
|
|
410
|
+
if (Array.isArray(value)) {
|
|
411
|
+
const out: unknown[] = [];
|
|
412
|
+
const redacted: string[] = [];
|
|
413
|
+
for (let i = 0; i < value.length; i++) {
|
|
414
|
+
const child = sanitizeAttributeValue(value[i], redactKeys, `${path}.${i}`, maxBytes);
|
|
415
|
+
out.push(child.value);
|
|
416
|
+
redacted.push(...child.redacted);
|
|
417
|
+
}
|
|
418
|
+
return { value: out, redacted };
|
|
419
|
+
}
|
|
420
|
+
if (!isPlainObject(value)) return { value: null, redacted: [] };
|
|
421
|
+
const out: Record<string, unknown> = {};
|
|
422
|
+
const redacted: string[] = [];
|
|
423
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
424
|
+
const childPath = path === "" ? key : `${path}.${key}`;
|
|
425
|
+
if (shouldRedactAttributeKey(key, redactKeys)) {
|
|
426
|
+
out[key] = REDACTED_VALUE;
|
|
427
|
+
redacted.push(childPath);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const child = sanitizeAttributeValue(childValue, redactKeys, childPath, maxBytes);
|
|
431
|
+
out[key] = child.value;
|
|
432
|
+
redacted.push(...child.redacted);
|
|
433
|
+
}
|
|
434
|
+
return { value: out, redacted };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function limitAttributes(
|
|
438
|
+
attrs: Record<string, unknown>,
|
|
439
|
+
args: {
|
|
440
|
+
maxAttributes: number;
|
|
441
|
+
maxAttributeValueBytes: number;
|
|
442
|
+
dropped: number;
|
|
443
|
+
redactKeys: Set<string>;
|
|
444
|
+
path: string;
|
|
445
|
+
}
|
|
446
|
+
): { attributes: Record<string, unknown>; dropped: number; redacted: string[] } {
|
|
447
|
+
const out: Record<string, unknown> = {};
|
|
448
|
+
const redacted: string[] = [];
|
|
449
|
+
let count = 0;
|
|
450
|
+
let dropped = Math.max(0, Math.trunc(args.dropped));
|
|
451
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
452
|
+
if (count >= args.maxAttributes) {
|
|
453
|
+
dropped += 1;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
count += 1;
|
|
457
|
+
const keyPath = args.path === "" ? key : `${args.path}.${key}`;
|
|
458
|
+
if (shouldRedactAttributeKey(key, args.redactKeys)) {
|
|
459
|
+
out[key] = REDACTED_VALUE;
|
|
460
|
+
redacted.push(keyPath);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const sanitized = sanitizeAttributeValue(value, args.redactKeys, keyPath, args.maxAttributeValueBytes);
|
|
464
|
+
out[key] = sanitized.value;
|
|
465
|
+
redacted.push(...sanitized.redacted);
|
|
466
|
+
}
|
|
467
|
+
return { attributes: out, dropped, redacted };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getString(attrs: Record<string, unknown>, ...keys: string[]): string | null {
|
|
471
|
+
for (const key of keys) {
|
|
472
|
+
const value = normalizeString(attrs[key]);
|
|
473
|
+
if (value) return value;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function getInteger(attrs: Record<string, unknown>, ...keys: string[]): number | null {
|
|
479
|
+
for (const key of keys) {
|
|
480
|
+
const value = normalizeInteger(attrs[key]);
|
|
481
|
+
if (value != null) return value;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getRequestId(attrs: Record<string, unknown>, direct: string | null, requestIdAttributes: readonly string[]): string | null {
|
|
487
|
+
if (direct) return direct;
|
|
488
|
+
for (const key of requestIdAttributes) {
|
|
489
|
+
const value = normalizeString(attrs[key]);
|
|
490
|
+
if (value) return value;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function extractExceptionFromEvents(events: DecodedOtelEvent[]): { type: string | null; message: string | null; stacktrace: string | null } {
|
|
496
|
+
for (const event of events) {
|
|
497
|
+
const type = getString(event.attributes, "exception.type");
|
|
498
|
+
const message = getString(event.attributes, "exception.message");
|
|
499
|
+
const stacktrace = getString(event.attributes, "exception.stacktrace");
|
|
500
|
+
if ((normalizeString(event.name)?.toLowerCase() ?? "") !== "exception" && !type && !message) continue;
|
|
501
|
+
return {
|
|
502
|
+
type,
|
|
503
|
+
message,
|
|
504
|
+
stacktrace,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return { type: null, message: null, stacktrace: null };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function sha256Hex(value: string): string {
|
|
511
|
+
return createHash("sha256").update(value).digest("hex");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function normalizeOtelDecodedSpanResult(
|
|
515
|
+
profile: OtelTracesStreamProfile,
|
|
516
|
+
input: DecodedOtelSpan
|
|
517
|
+
): Result<CanonicalOtelSpan, { message: string }> {
|
|
518
|
+
const traceIdRes = normalizeHexIdResult(input.traceId, 32, "traceId");
|
|
519
|
+
if (Result.isError(traceIdRes)) return traceIdRes;
|
|
520
|
+
const spanIdRes = normalizeHexIdResult(input.spanId, 16, "spanId");
|
|
521
|
+
if (Result.isError(spanIdRes)) return spanIdRes;
|
|
522
|
+
const parentSpanIdRes = normalizeParentSpanIdResult(input.parentSpanId);
|
|
523
|
+
if (Result.isError(parentSpanIdRes)) return parentSpanIdRes;
|
|
524
|
+
|
|
525
|
+
const limits = { ...DEFAULT_ATTRIBUTE_LIMITS, ...(profile.attributeLimits ?? {}) };
|
|
526
|
+
const store = { ...DEFAULT_STORE_CONFIG, ...(profile.store ?? {}) };
|
|
527
|
+
const urlMode = profile.urlMode ?? DEFAULT_URL_MODE;
|
|
528
|
+
const redactKeys = new Set([...DEFAULT_OTEL_TRACE_REDACT_KEYS, ...(profile.redactKeys ?? [])].map((key) => key.toLowerCase()));
|
|
529
|
+
const requestIdAttributes = profile.requestIdAttributes ?? [...DEFAULT_REQUEST_ID_ATTRIBUTES];
|
|
530
|
+
|
|
531
|
+
const resourceRes = limitAttributes(input.resourceAttributes, {
|
|
532
|
+
maxAttributes: limits.maxAttributesPerSpan,
|
|
533
|
+
maxAttributeValueBytes: limits.maxAttributeValueBytes,
|
|
534
|
+
dropped: 0,
|
|
535
|
+
redactKeys,
|
|
536
|
+
path: "resource.attributes",
|
|
537
|
+
});
|
|
538
|
+
const scopeRes = limitAttributes(input.instrumentationScope?.attributes ?? {}, {
|
|
539
|
+
maxAttributes: limits.maxAttributesPerSpan,
|
|
540
|
+
maxAttributeValueBytes: limits.maxAttributeValueBytes,
|
|
541
|
+
dropped: 0,
|
|
542
|
+
redactKeys,
|
|
543
|
+
path: "instrumentationScope.attributes",
|
|
544
|
+
});
|
|
545
|
+
const attrsRes = limitAttributes(input.attributes, {
|
|
546
|
+
maxAttributes: limits.maxAttributesPerSpan,
|
|
547
|
+
maxAttributeValueBytes: limits.maxAttributeValueBytes,
|
|
548
|
+
dropped: input.droppedAttributesCount ?? 0,
|
|
549
|
+
redactKeys,
|
|
550
|
+
path: "attributes",
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const startUnixNano = normalizeNanoString(input.startUnixNano);
|
|
554
|
+
const endUnixNano = normalizeNanoString(input.endUnixNano);
|
|
555
|
+
const durationRes = durationMs(startUnixNano, endUnixNano);
|
|
556
|
+
if (Result.isError(durationRes)) return durationRes;
|
|
557
|
+
const timestamp = isoFromUnixNano(startUnixNano) ?? normalizeString(input.timestamp) ?? new Date().toISOString();
|
|
558
|
+
const endTimestamp = isoFromUnixNano(endUnixNano);
|
|
559
|
+
|
|
560
|
+
const normalizedEvents: CanonicalOtelSpan["events"] = [];
|
|
561
|
+
const eventDerivationInput: DecodedOtelEvent[] = [];
|
|
562
|
+
let droppedEvents = Math.max(0, Math.trunc(input.droppedEventsCount ?? 0));
|
|
563
|
+
const eventNames: string[] = [];
|
|
564
|
+
for (const event of input.events) {
|
|
565
|
+
if (normalizedEvents.length >= limits.maxEventsPerSpan) {
|
|
566
|
+
droppedEvents += 1;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const eventAttrs = limitAttributes(event.attributes, {
|
|
570
|
+
maxAttributes: limits.maxAttributesPerSpan,
|
|
571
|
+
maxAttributeValueBytes: limits.maxAttributeValueBytes,
|
|
572
|
+
dropped: event.droppedAttributesCount ?? 0,
|
|
573
|
+
redactKeys,
|
|
574
|
+
path: `events.${normalizedEvents.length}.attributes`,
|
|
575
|
+
});
|
|
576
|
+
const eventName = normalizeString(event.name) ?? "";
|
|
577
|
+
eventNames.push(eventName);
|
|
578
|
+
eventDerivationInput.push({
|
|
579
|
+
timeUnixNano: normalizeNanoString(event.timeUnixNano),
|
|
580
|
+
name: eventName,
|
|
581
|
+
attributes: eventAttrs.attributes,
|
|
582
|
+
droppedAttributesCount: eventAttrs.dropped,
|
|
583
|
+
});
|
|
584
|
+
normalizedEvents.push({
|
|
585
|
+
timestamp: isoFromUnixNano(normalizeNanoString(event.timeUnixNano)),
|
|
586
|
+
timeUnixNano: normalizeNanoString(event.timeUnixNano),
|
|
587
|
+
name: eventName,
|
|
588
|
+
attributes: store.rawEvents ? eventAttrs.attributes : {},
|
|
589
|
+
droppedAttributesCount: eventAttrs.dropped,
|
|
590
|
+
});
|
|
591
|
+
resourceRes.redacted.push(...eventAttrs.redacted);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const normalizedLinks: CanonicalOtelSpan["links"] = [];
|
|
595
|
+
let droppedLinks = Math.max(0, Math.trunc(input.droppedLinksCount ?? 0));
|
|
596
|
+
for (const link of input.links) {
|
|
597
|
+
if (normalizedLinks.length >= limits.maxLinksPerSpan) {
|
|
598
|
+
droppedLinks += 1;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const linkTraceIdRes = normalizeHexIdResult(link.traceId, 32, "links.traceId");
|
|
602
|
+
if (Result.isError(linkTraceIdRes)) {
|
|
603
|
+
droppedLinks += 1;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const linkSpanIdRes = normalizeHexIdResult(link.spanId, 16, "links.spanId");
|
|
607
|
+
if (Result.isError(linkSpanIdRes)) {
|
|
608
|
+
droppedLinks += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const linkAttrs = limitAttributes(link.attributes, {
|
|
612
|
+
maxAttributes: limits.maxAttributesPerSpan,
|
|
613
|
+
maxAttributeValueBytes: limits.maxAttributeValueBytes,
|
|
614
|
+
dropped: link.droppedAttributesCount ?? 0,
|
|
615
|
+
redactKeys,
|
|
616
|
+
path: `links.${normalizedLinks.length}.attributes`,
|
|
617
|
+
});
|
|
618
|
+
normalizedLinks.push({
|
|
619
|
+
traceId: linkTraceIdRes.value,
|
|
620
|
+
spanId: linkSpanIdRes.value,
|
|
621
|
+
traceState: normalizeString(link.traceState),
|
|
622
|
+
attributes: store.rawLinks ? linkAttrs.attributes : {},
|
|
623
|
+
droppedAttributesCount: linkAttrs.dropped,
|
|
624
|
+
});
|
|
625
|
+
resourceRes.redacted.push(...linkAttrs.redacted);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const resourceAttrs = resourceRes.attributes;
|
|
629
|
+
const spanAttrs = attrsRes.attributes;
|
|
630
|
+
const service = getString(resourceAttrs, "service.name");
|
|
631
|
+
const statusCode = normalizeStatusCode(input.status?.code);
|
|
632
|
+
const exception = extractExceptionFromEvents(eventDerivationInput);
|
|
633
|
+
const attrErrorType = getString(spanAttrs, "exception.type", "error.type");
|
|
634
|
+
const attrErrorMessage = getString(spanAttrs, "exception.message", "error.message");
|
|
635
|
+
const attrErrorStack = getString(spanAttrs, "exception.stacktrace", "error.stacktrace");
|
|
636
|
+
const httpStatusCode = getInteger(spanAttrs, "http.response.status_code", "http.status_code");
|
|
637
|
+
const statusMessage = truncateNullableString(normalizeString(input.status?.message), limits.maxAttributeValueBytes);
|
|
638
|
+
const errorMessage = attrErrorMessage ?? exception.message ?? statusMessage;
|
|
639
|
+
const traceFlagsRaw = normalizeInteger(input.traceFlags);
|
|
640
|
+
const dbStatementRaw = getString(spanAttrs, "db.statement", "db.query.text");
|
|
641
|
+
const dbStatement =
|
|
642
|
+
profile.dbStatementMode === "raw" && dbStatementRaw
|
|
643
|
+
? truncateUtf8(dbStatementRaw, limits.maxStatementBytes)
|
|
644
|
+
: null;
|
|
645
|
+
|
|
646
|
+
const canonical: CanonicalOtelSpan = {
|
|
647
|
+
schemaVersion: 1,
|
|
648
|
+
signal: "trace.span",
|
|
649
|
+
timestamp,
|
|
650
|
+
endTimestamp,
|
|
651
|
+
startUnixNano,
|
|
652
|
+
endUnixNano,
|
|
653
|
+
duration: durationRes.value,
|
|
654
|
+
traceId: traceIdRes.value,
|
|
655
|
+
spanId: spanIdRes.value,
|
|
656
|
+
parentSpanId: parentSpanIdRes.value,
|
|
657
|
+
traceState: normalizeString(input.traceState),
|
|
658
|
+
traceFlags: {
|
|
659
|
+
sampled: traceFlagsRaw == null ? false : (traceFlagsRaw & 1) === 1,
|
|
660
|
+
raw: traceFlagsRaw,
|
|
661
|
+
},
|
|
662
|
+
name: normalizeString(input.name) ?? "",
|
|
663
|
+
kind: normalizeSpanKind(input.kind),
|
|
664
|
+
status: {
|
|
665
|
+
code: statusCode,
|
|
666
|
+
message: statusMessage,
|
|
667
|
+
},
|
|
668
|
+
service,
|
|
669
|
+
serviceNamespace: getString(resourceAttrs, "service.namespace"),
|
|
670
|
+
serviceInstanceId: getString(resourceAttrs, "service.instance.id"),
|
|
671
|
+
environment: getString(resourceAttrs, "deployment.environment.name", "deployment.environment"),
|
|
672
|
+
version: getString(resourceAttrs, "service.version"),
|
|
673
|
+
region: getString(resourceAttrs, "cloud.region"),
|
|
674
|
+
requestId: getRequestId(spanAttrs, normalizeString(input.requestId), requestIdAttributes),
|
|
675
|
+
http: {
|
|
676
|
+
method: getString(spanAttrs, "http.request.method", "http.method"),
|
|
677
|
+
route: getString(spanAttrs, "http.route"),
|
|
678
|
+
path: getString(spanAttrs, "url.path", "http.target"),
|
|
679
|
+
target: getString(spanAttrs, "http.target"),
|
|
680
|
+
url: sanitizeUrl(getString(spanAttrs, "url.full", "http.url"), urlMode, limits.maxAttributeValueBytes),
|
|
681
|
+
statusCode: httpStatusCode,
|
|
682
|
+
userAgent: getString(spanAttrs, "user_agent.original", "http.user_agent"),
|
|
683
|
+
},
|
|
684
|
+
db: {
|
|
685
|
+
system: getString(spanAttrs, "db.system"),
|
|
686
|
+
name: getString(spanAttrs, "db.name", "db.namespace"),
|
|
687
|
+
operation: getString(spanAttrs, "db.operation", "db.operation.name"),
|
|
688
|
+
statement: dbStatement,
|
|
689
|
+
},
|
|
690
|
+
rpc: {
|
|
691
|
+
system: getString(spanAttrs, "rpc.system"),
|
|
692
|
+
service: getString(spanAttrs, "rpc.service"),
|
|
693
|
+
method: getString(spanAttrs, "rpc.method"),
|
|
694
|
+
},
|
|
695
|
+
messaging: {
|
|
696
|
+
system: getString(spanAttrs, "messaging.system"),
|
|
697
|
+
destination: getString(spanAttrs, "messaging.destination", "messaging.destination.name"),
|
|
698
|
+
operation: getString(spanAttrs, "messaging.operation", "messaging.operation.name"),
|
|
699
|
+
},
|
|
700
|
+
error: {
|
|
701
|
+
isError: statusCode === "error" || (httpStatusCode != null && httpStatusCode >= 500) || !!attrErrorType || !!exception.type,
|
|
702
|
+
type: attrErrorType ?? exception.type,
|
|
703
|
+
message: errorMessage,
|
|
704
|
+
stacktrace: truncateNullableString(attrErrorStack ?? exception.stacktrace, limits.maxAttributeValueBytes),
|
|
705
|
+
},
|
|
706
|
+
instrumentationScope: {
|
|
707
|
+
name: normalizeString(input.instrumentationScope?.name),
|
|
708
|
+
version: normalizeString(input.instrumentationScope?.version),
|
|
709
|
+
schemaUrl: normalizeString(input.instrumentationScope?.schemaUrl),
|
|
710
|
+
attributes: scopeRes.attributes,
|
|
711
|
+
},
|
|
712
|
+
resource: {
|
|
713
|
+
schemaUrl: normalizeString(input.resourceSchemaUrl),
|
|
714
|
+
attributes: store.rawResourceAttributes ? resourceAttrs : {},
|
|
715
|
+
},
|
|
716
|
+
attributes: store.rawSpanAttributes ? spanAttrs : {},
|
|
717
|
+
events: store.rawEvents ? normalizedEvents : [],
|
|
718
|
+
eventNames,
|
|
719
|
+
links: store.rawLinks ? normalizedLinks : [],
|
|
720
|
+
dropped: {
|
|
721
|
+
attributes: attrsRes.dropped,
|
|
722
|
+
events: droppedEvents,
|
|
723
|
+
links: droppedLinks,
|
|
724
|
+
},
|
|
725
|
+
redaction: {
|
|
726
|
+
keys: [...resourceRes.redacted, ...scopeRes.redacted, ...attrsRes.redacted].sort(),
|
|
727
|
+
},
|
|
728
|
+
identity: {
|
|
729
|
+
spanKey: `${traceIdRes.value}:${spanIdRes.value}`,
|
|
730
|
+
dedupeKey: sha256Hex(`${traceIdRes.value}\0${spanIdRes.value}\0${startUnixNano ?? ""}\0${service ?? ""}\0${normalizeString(input.name) ?? ""}`),
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
return Result.ok(canonical);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function objectFromUnknown(value: unknown): Record<string, unknown> {
|
|
738
|
+
return isPlainObject(value) ? structuredClone(value) : {};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function eventFromCanonical(value: unknown): DecodedOtelEvent | null {
|
|
742
|
+
if (!isPlainObject(value)) return null;
|
|
743
|
+
return {
|
|
744
|
+
timeUnixNano: normalizeNanoString(value.timeUnixNano),
|
|
745
|
+
name: normalizeString(value.name) ?? "",
|
|
746
|
+
attributes: objectFromUnknown(value.attributes),
|
|
747
|
+
droppedAttributesCount: normalizeInteger(value.droppedAttributesCount) ?? 0,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function linkFromCanonical(value: unknown): DecodedOtelLink | null {
|
|
752
|
+
if (!isPlainObject(value)) return null;
|
|
753
|
+
const traceId = normalizeString(value.traceId);
|
|
754
|
+
const spanId = normalizeString(value.spanId);
|
|
755
|
+
if (!traceId || !spanId) return null;
|
|
756
|
+
return {
|
|
757
|
+
traceId,
|
|
758
|
+
spanId,
|
|
759
|
+
traceState: normalizeString(value.traceState),
|
|
760
|
+
attributes: objectFromUnknown(value.attributes),
|
|
761
|
+
droppedAttributesCount: normalizeInteger(value.droppedAttributesCount) ?? 0,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function canonicalString(value: unknown, fallback: string | null): string | null {
|
|
766
|
+
const normalized = normalizeString(value);
|
|
767
|
+
return normalized ?? fallback;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function canonicalLimitedString(value: unknown, fallback: string | null, maxBytes: number): string | null {
|
|
771
|
+
return truncateNullableString(canonicalString(value, fallback), maxBytes);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function canonicalNumber(value: unknown, fallback: number | null): number | null {
|
|
775
|
+
const normalized = normalizeNumber(value);
|
|
776
|
+
return normalized ?? fallback;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function canonicalInteger(value: unknown, fallback: number | null): number | null {
|
|
780
|
+
const normalized = normalizeInteger(value);
|
|
781
|
+
return normalized ?? fallback;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function canonicalBoolean(value: unknown, fallback: boolean): boolean {
|
|
785
|
+
return typeof value === "boolean" ? value : fallback;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function preserveCanonicalEventNames(value: unknown, fallback: string[]): string[] {
|
|
789
|
+
const out = new Set(fallback);
|
|
790
|
+
if (Array.isArray(value)) {
|
|
791
|
+
for (const item of value) {
|
|
792
|
+
const normalized = normalizeString(item);
|
|
793
|
+
if (normalized) out.add(normalized);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return Array.from(out);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function preserveRedactionKeys(value: unknown, fallback: string[]): string[] {
|
|
800
|
+
const out = new Set(fallback);
|
|
801
|
+
if (isPlainObject(value) && Array.isArray(value.keys)) {
|
|
802
|
+
for (const item of value.keys) {
|
|
803
|
+
const normalized = normalizeString(item);
|
|
804
|
+
if (normalized) out.add(normalized);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return Array.from(out).sort();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function preserveCanonicalDerivedFields(
|
|
811
|
+
canonical: CanonicalOtelSpan,
|
|
812
|
+
raw: Record<string, unknown>,
|
|
813
|
+
profile: OtelTracesStreamProfile,
|
|
814
|
+
limits: OtelTraceAttributeLimits
|
|
815
|
+
): CanonicalOtelSpan {
|
|
816
|
+
if (raw.schemaVersion !== 1 || raw.signal !== "trace.span") return canonical;
|
|
817
|
+
const out: CanonicalOtelSpan = structuredClone(canonical);
|
|
818
|
+
const urlMode = profile.urlMode ?? DEFAULT_URL_MODE;
|
|
819
|
+
|
|
820
|
+
out.duration = canonicalNumber(raw.duration, out.duration);
|
|
821
|
+
out.service = canonicalLimitedString(raw.service, out.service, limits.maxAttributeValueBytes);
|
|
822
|
+
out.serviceNamespace = canonicalLimitedString(raw.serviceNamespace, out.serviceNamespace, limits.maxAttributeValueBytes);
|
|
823
|
+
out.serviceInstanceId = canonicalLimitedString(raw.serviceInstanceId, out.serviceInstanceId, limits.maxAttributeValueBytes);
|
|
824
|
+
out.environment = canonicalLimitedString(raw.environment, out.environment, limits.maxAttributeValueBytes);
|
|
825
|
+
out.version = canonicalLimitedString(raw.version, out.version, limits.maxAttributeValueBytes);
|
|
826
|
+
out.region = canonicalLimitedString(raw.region, out.region, limits.maxAttributeValueBytes);
|
|
827
|
+
out.requestId = canonicalLimitedString(raw.requestId, out.requestId, limits.maxAttributeValueBytes);
|
|
828
|
+
|
|
829
|
+
const status = isPlainObject(raw.status) ? raw.status : {};
|
|
830
|
+
out.status = {
|
|
831
|
+
code: out.status.code,
|
|
832
|
+
message: canonicalLimitedString(status.message, out.status.message, limits.maxAttributeValueBytes),
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const http = isPlainObject(raw.http) ? raw.http : {};
|
|
836
|
+
out.http = {
|
|
837
|
+
method: canonicalLimitedString(http.method, out.http.method, limits.maxAttributeValueBytes),
|
|
838
|
+
route: canonicalLimitedString(http.route, out.http.route, limits.maxAttributeValueBytes),
|
|
839
|
+
path: canonicalLimitedString(http.path, out.http.path, limits.maxAttributeValueBytes),
|
|
840
|
+
target: canonicalLimitedString(http.target, out.http.target, limits.maxAttributeValueBytes),
|
|
841
|
+
url: sanitizeUrl(canonicalString(http.url, out.http.url), urlMode, limits.maxAttributeValueBytes),
|
|
842
|
+
statusCode: canonicalInteger(http.statusCode, out.http.statusCode),
|
|
843
|
+
userAgent: canonicalLimitedString(http.userAgent, out.http.userAgent, limits.maxAttributeValueBytes),
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const db = isPlainObject(raw.db) ? raw.db : {};
|
|
847
|
+
out.db = {
|
|
848
|
+
system: canonicalLimitedString(db.system, out.db.system, limits.maxAttributeValueBytes),
|
|
849
|
+
name: canonicalLimitedString(db.name, out.db.name, limits.maxAttributeValueBytes),
|
|
850
|
+
operation: canonicalLimitedString(db.operation, out.db.operation, limits.maxAttributeValueBytes),
|
|
851
|
+
statement:
|
|
852
|
+
profile.dbStatementMode === "raw"
|
|
853
|
+
? canonicalLimitedString(db.statement, out.db.statement, limits.maxStatementBytes)
|
|
854
|
+
: null,
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const rpc = isPlainObject(raw.rpc) ? raw.rpc : {};
|
|
858
|
+
out.rpc = {
|
|
859
|
+
system: canonicalLimitedString(rpc.system, out.rpc.system, limits.maxAttributeValueBytes),
|
|
860
|
+
service: canonicalLimitedString(rpc.service, out.rpc.service, limits.maxAttributeValueBytes),
|
|
861
|
+
method: canonicalLimitedString(rpc.method, out.rpc.method, limits.maxAttributeValueBytes),
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const messaging = isPlainObject(raw.messaging) ? raw.messaging : {};
|
|
865
|
+
out.messaging = {
|
|
866
|
+
system: canonicalLimitedString(messaging.system, out.messaging.system, limits.maxAttributeValueBytes),
|
|
867
|
+
destination: canonicalLimitedString(messaging.destination, out.messaging.destination, limits.maxAttributeValueBytes),
|
|
868
|
+
operation: canonicalLimitedString(messaging.operation, out.messaging.operation, limits.maxAttributeValueBytes),
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const error = isPlainObject(raw.error) ? raw.error : {};
|
|
872
|
+
out.error = {
|
|
873
|
+
isError: canonicalBoolean(error.isError, out.error.isError),
|
|
874
|
+
type: canonicalLimitedString(error.type, out.error.type, limits.maxAttributeValueBytes),
|
|
875
|
+
message: canonicalLimitedString(error.message, out.error.message, limits.maxAttributeValueBytes),
|
|
876
|
+
stacktrace: canonicalLimitedString(error.stacktrace, out.error.stacktrace, limits.maxAttributeValueBytes),
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
out.eventNames = preserveCanonicalEventNames(raw.eventNames, out.eventNames);
|
|
880
|
+
out.redaction.keys = preserveRedactionKeys(raw.redaction, out.redaction.keys);
|
|
881
|
+
|
|
882
|
+
const dropped = isPlainObject(raw.dropped) ? raw.dropped : {};
|
|
883
|
+
out.dropped = {
|
|
884
|
+
attributes: canonicalInteger(dropped.attributes, out.dropped.attributes) ?? 0,
|
|
885
|
+
events: canonicalInteger(dropped.events, out.dropped.events) ?? 0,
|
|
886
|
+
links: canonicalInteger(dropped.links, out.dropped.links) ?? 0,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
out.identity = {
|
|
890
|
+
spanKey: `${out.traceId}:${out.spanId}`,
|
|
891
|
+
dedupeKey: sha256Hex(`${out.traceId}\0${out.spanId}\0${out.startUnixNano ?? ""}\0${out.service ?? ""}\0${out.name}`),
|
|
892
|
+
};
|
|
893
|
+
return out;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function decodedSpanFromCanonicalLikeResult(value: unknown): Result<DecodedOtelSpan, { message: string }> {
|
|
897
|
+
const objRes = expectPlainObjectResult(value, "otel-traces record");
|
|
898
|
+
if (Result.isError(objRes)) return objRes;
|
|
899
|
+
const obj = objRes.value;
|
|
900
|
+
const traceId = normalizeString(obj.traceId);
|
|
901
|
+
const spanId = normalizeString(obj.spanId);
|
|
902
|
+
if (!traceId) return Result.err({ message: "traceId is required" });
|
|
903
|
+
if (!spanId) return Result.err({ message: "spanId is required" });
|
|
904
|
+
const resource = isPlainObject(obj.resource) ? obj.resource : {};
|
|
905
|
+
const scope = isPlainObject(obj.instrumentationScope) ? obj.instrumentationScope : {};
|
|
906
|
+
const status = isPlainObject(obj.status) ? obj.status : {};
|
|
907
|
+
const traceFlags = isPlainObject(obj.traceFlags) ? obj.traceFlags : {};
|
|
908
|
+
return Result.ok({
|
|
909
|
+
traceId,
|
|
910
|
+
spanId,
|
|
911
|
+
parentSpanId: normalizeString(obj.parentSpanId),
|
|
912
|
+
traceState: normalizeString(obj.traceState),
|
|
913
|
+
traceFlags: normalizeInteger(traceFlags.raw),
|
|
914
|
+
name: normalizeString(obj.name) ?? "",
|
|
915
|
+
kind: obj.kind as number | string | null | undefined,
|
|
916
|
+
startUnixNano: normalizeNanoString(obj.startUnixNano),
|
|
917
|
+
endUnixNano: normalizeNanoString(obj.endUnixNano),
|
|
918
|
+
timestamp: normalizeString(obj.timestamp),
|
|
919
|
+
status: {
|
|
920
|
+
code: status.code as number | string | null | undefined,
|
|
921
|
+
message: normalizeString(status.message),
|
|
922
|
+
},
|
|
923
|
+
resourceSchemaUrl: normalizeString(resource.schemaUrl),
|
|
924
|
+
resourceAttributes: objectFromUnknown(resource.attributes),
|
|
925
|
+
instrumentationScope: {
|
|
926
|
+
name: normalizeString(scope.name),
|
|
927
|
+
version: normalizeString(scope.version),
|
|
928
|
+
schemaUrl: normalizeString(scope.schemaUrl),
|
|
929
|
+
attributes: objectFromUnknown(scope.attributes),
|
|
930
|
+
},
|
|
931
|
+
attributes: objectFromUnknown(obj.attributes),
|
|
932
|
+
events: Array.isArray(obj.events) ? obj.events.map(eventFromCanonical).filter((event): event is DecodedOtelEvent => !!event) : [],
|
|
933
|
+
links: Array.isArray(obj.links) ? obj.links.map(linkFromCanonical).filter((link): link is DecodedOtelLink => !!link) : [],
|
|
934
|
+
droppedAttributesCount: isPlainObject(obj.dropped) ? (normalizeInteger(obj.dropped.attributes) ?? 0) : 0,
|
|
935
|
+
droppedEventsCount: isPlainObject(obj.dropped) ? (normalizeInteger(obj.dropped.events) ?? 0) : 0,
|
|
936
|
+
droppedLinksCount: isPlainObject(obj.dropped) ? (normalizeInteger(obj.dropped.links) ?? 0) : 0,
|
|
937
|
+
requestId: normalizeString(obj.requestId),
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function normalizeOtelTraceRecordResult(
|
|
942
|
+
profile: OtelTracesStreamProfile,
|
|
943
|
+
value: unknown
|
|
944
|
+
): Result<PreparedJsonRecord, { message: string }> {
|
|
945
|
+
const decodedRes = decodedSpanFromCanonicalLikeResult(value);
|
|
946
|
+
if (Result.isError(decodedRes)) return decodedRes;
|
|
947
|
+
const normalizedRes = normalizeOtelDecodedSpanResult(profile, decodedRes.value);
|
|
948
|
+
if (Result.isError(normalizedRes)) return normalizedRes;
|
|
949
|
+
const limits = { ...DEFAULT_ATTRIBUTE_LIMITS, ...(profile.attributeLimits ?? {}) };
|
|
950
|
+
const normalized = preserveCanonicalDerivedFields(normalizedRes.value, isPlainObject(value) ? value : {}, profile, limits);
|
|
951
|
+
return Result.ok({
|
|
952
|
+
value: normalized,
|
|
953
|
+
routingKey: normalized.traceId,
|
|
954
|
+
});
|
|
955
|
+
}
|