// Worker-safe OTLP transform: no server singletons (env/clickhouse/repository/prisma). import { SemanticInternalAttributes } from "@trigger.dev/core/v3"; import type { AnyValue, KeyValue, ResourceLogs, ResourceMetrics, ResourceSpans, Span, Span_Event, } from "@trigger.dev/otlp-importer"; import { SeverityNumber, Span_SpanKind, Status_StatusCode } from "@trigger.dev/otlp-importer"; import type { MetricsV1Input } from "@internal/clickhouse"; import { generateSpanId } from "./eventRepository/common.server"; import type { CreatableEventKind, CreatableEventStatus, CreateEventInput, } from "./eventRepository/eventRepository.types"; // Filters mirror OTLPExporter's #filterResource* methods, minus the debug logging, so a // worker can run them without the server logger. export function filterResourceSpans(resourceSpans: ResourceSpans[]): ResourceSpans[] { return resourceSpans.filter((resourceSpan) => { const triggerAttribute = resourceSpan.resource?.attributes.find( (attribute) => attribute.key === SemanticInternalAttributes.TRIGGER ); const executionEnvironmentAttribute = resourceSpan.resource?.attributes.find( (attribute) => attribute.key === SemanticInternalAttributes.EXECUTION_ENVIRONMENT ); if (!triggerAttribute && !executionEnvironmentAttribute) return true; const executionEnvironment = isStringValue(executionEnvironmentAttribute?.value) ? executionEnvironmentAttribute.value.stringValue : undefined; if (executionEnvironment === "trigger") return true; return isBoolValue(triggerAttribute?.value) ? triggerAttribute.value.boolValue : false; }); } export function filterResourceLogs(resourceLogs: ResourceLogs[]): ResourceLogs[] { return resourceLogs.filter((resourceLog) => { const attribute = resourceLog.resource?.attributes.find( (attribute) => attribute.key === SemanticInternalAttributes.TRIGGER ); if (!attribute) return false; return isBoolValue(attribute.value) ? attribute.value.boolValue : false; }); } export function filterResourceMetrics(resourceMetrics: ResourceMetrics[]): ResourceMetrics[] { return resourceMetrics.filter((rm) => { const triggerAttribute = rm.resource?.attributes.find( (attribute) => attribute.key === SemanticInternalAttributes.TRIGGER ); if (!triggerAttribute) return false; return isBoolValue(triggerAttribute.value) ? triggerAttribute.value.boolValue : false; }); } export function convertLogsToCreateableEvents( resourceLog: ResourceLogs, spanAttributeValueLengthLimit: number, defaultEventStore: string ): { events: Array; taskEventStore: string } { const resourceAttributes = resourceLog.resource?.attributes ?? []; const resourceProperties = extractEventProperties(resourceAttributes); const userDefinedResourceAttributes = truncateAttributes( convertKeyValueItemsToMap(resourceAttributes ?? [], [], undefined, [ SemanticInternalAttributes.USAGE, SemanticInternalAttributes.SPAN, SemanticInternalAttributes.METADATA, SemanticInternalAttributes.STYLE, SemanticInternalAttributes.METRIC_EVENTS, SemanticInternalAttributes.TRIGGER, "process", "sdk", "service", "ctx", "cli", "cloud", ]), spanAttributeValueLengthLimit ); const taskEventStore = extractStringAttribute(resourceAttributes, [SemanticInternalAttributes.TASK_EVENT_STORE]) ?? defaultEventStore; const events = resourceLog.scopeLogs.flatMap((scopeLog) => { return scopeLog.logRecords .map((log) => { const logLevel = logLevelToEventLevel(log.severityNumber); if (!log.traceId || !log.spanId) { return; } const logProperties = extractEventProperties( log.attributes ?? [], SemanticInternalAttributes.METADATA ); const properties = truncateAttributes( convertKeyValueItemsToMap(log.attributes ?? [], [], undefined, [ SemanticInternalAttributes.USAGE, SemanticInternalAttributes.SPAN, SemanticInternalAttributes.METADATA, SemanticInternalAttributes.STYLE, SemanticInternalAttributes.METRIC_EVENTS, SemanticInternalAttributes.TRIGGER, ]), spanAttributeValueLengthLimit ) ?? {}; return { traceId: binaryToHex(log.traceId), spanId: generateSpanId(), parentId: binaryToHex(log.spanId), message: isStringValue(log.body) ? log.body.stringValue.slice(0, 4096) : `${log.severityText} log`, isPartial: false, kind: "INTERNAL" as const, level: logLevelToEventLevel(log.severityNumber), isError: logLevel === "ERROR", status: logLevelToEventStatus(log.severityNumber), startTime: log.timeUnixNano, properties, resourceProperties: userDefinedResourceAttributes, style: convertKeyValueItemsToMap( pickAttributes(log.attributes ?? [], SemanticInternalAttributes.STYLE), [] ), metadata: logProperties.metadata ?? resourceProperties.metadata ?? {}, environmentId: logProperties.environmentId ?? resourceProperties.environmentId ?? "unknown", environmentType: "DEVELOPMENT" as const, // We've deprecated this but we need to keep it for backwards compatibility organizationId: logProperties.organizationId ?? resourceProperties.organizationId ?? "unknown", projectId: logProperties.projectId ?? resourceProperties.projectId ?? "unknown", runId: logProperties.runId ?? resourceProperties.runId ?? "unknown", taskSlug: logProperties.taskSlug ?? resourceProperties.taskSlug ?? "unknown", machineId: logProperties.machineId ?? resourceProperties.machineId, attemptNumber: extractNumberAttribute( log.attributes ?? [], [SemanticInternalAttributes.METADATA, SemanticInternalAttributes.ATTEMPT_NUMBER].join( "." ) ) ?? resourceProperties.attemptNumber, }; }) .filter(Boolean); }); return { events, taskEventStore }; } export function convertSpansToCreateableEvents( resourceSpan: ResourceSpans, spanAttributeValueLengthLimit: number, defaultEventStore: string ): { events: Array; taskEventStore: string } { const resourceAttributes = resourceSpan.resource?.attributes ?? []; const resourceProperties = extractEventProperties(resourceAttributes); const userDefinedResourceAttributes = truncateAttributes( convertKeyValueItemsToMap(resourceAttributes ?? [], [], undefined, [ SemanticInternalAttributes.USAGE, SemanticInternalAttributes.SPAN, SemanticInternalAttributes.METADATA, SemanticInternalAttributes.STYLE, SemanticInternalAttributes.METRIC_EVENTS, SemanticInternalAttributes.TRIGGER, "process", "sdk", "service", "ctx", "cli", "cloud", ]), spanAttributeValueLengthLimit ); const taskEventStore = extractStringAttribute(resourceAttributes, [SemanticInternalAttributes.TASK_EVENT_STORE]) ?? defaultEventStore; const events = resourceSpan.scopeSpans.flatMap((scopeSpan) => { return scopeSpan.spans .map((span) => { const isPartial = isPartialSpan(span); if (!span.traceId || !span.spanId) { return; } const spanProperties = extractEventProperties( span.attributes ?? [], SemanticInternalAttributes.METADATA ); const runTags = extractArrayAttribute( span.attributes ?? [], SemanticInternalAttributes.RUN_TAGS ); const properties = truncateAttributes( convertKeyValueItemsToMap(span.attributes ?? [], [], undefined, [ SemanticInternalAttributes.USAGE, SemanticInternalAttributes.SPAN, SemanticInternalAttributes.METADATA, SemanticInternalAttributes.STYLE, SemanticInternalAttributes.METRIC_EVENTS, SemanticInternalAttributes.TRIGGER, ]), spanAttributeValueLengthLimit ) ?? {}; return { traceId: binaryToHex(span.traceId), spanId: isPartial ? extractStringAttribute( span?.attributes ?? [], SemanticInternalAttributes.SPAN_ID, binaryToHex(span.spanId) ) : binaryToHex(span.spanId), parentId: binaryToHex(span.parentSpanId), message: span.name, isPartial, isError: span.status?.code === Status_StatusCode.ERROR, kind: spanKindToEventKind(span.kind), level: "TRACE" as const, status: spanStatusToEventStatus(span.status), startTime: span.startTimeUnixNano, events: spanEventsToEventEvents(span.events ?? []), duration: span.endTimeUnixNano - span.startTimeUnixNano, properties, resourceProperties: userDefinedResourceAttributes, style: convertKeyValueItemsToMap( pickAttributes(span.attributes ?? [], SemanticInternalAttributes.STYLE), [] ), metadata: spanProperties.metadata ?? resourceProperties.metadata ?? {}, environmentId: spanProperties.environmentId ?? resourceProperties.environmentId ?? "unknown", environmentType: "DEVELOPMENT" as const, organizationId: spanProperties.organizationId ?? resourceProperties.organizationId ?? "unknown", projectId: spanProperties.projectId ?? resourceProperties.projectId ?? "unknown", runId: spanProperties.runId ?? resourceProperties.runId ?? "unknown", taskSlug: spanProperties.taskSlug ?? resourceProperties.taskSlug ?? "unknown", machineId: spanProperties.machineId ?? resourceProperties.machineId, runTags, attemptNumber: extractNumberAttribute( span.attributes ?? [], [SemanticInternalAttributes.METADATA, SemanticInternalAttributes.ATTEMPT_NUMBER].join( "." ) ) ?? resourceProperties.attemptNumber, }; }) .filter(Boolean); }); return { events, taskEventStore }; } function floorToTenSecondBucket(timeUnixNano: bigint | number): string { const epochMs = Number(BigInt(timeUnixNano) / BigInt(1_000_000)); const flooredMs = Math.floor(epochMs / 10_000) * 10_000; const date = new Date(flooredMs); // Format as ClickHouse DateTime: YYYY-MM-DD HH:MM:SS return date .toISOString() .replace("T", " ") .replace(/\.\d{3}Z$/, ""); } export function convertMetricsToClickhouseRows( resourceMetrics: ResourceMetrics, spanAttributeValueLengthLimit: number ): MetricsV1Input[] { const resourceAttributes = resourceMetrics.resource?.attributes ?? []; const resourceProperties = extractEventProperties(resourceAttributes); const organizationId = resourceProperties.organizationId ?? "unknown"; const projectId = resourceProperties.projectId ?? "unknown"; const environmentId = resourceProperties.environmentId ?? "unknown"; const resourceCtx = { taskSlug: resourceProperties.taskSlug, runId: resourceProperties.runId, attemptNumber: resourceProperties.attemptNumber, machineId: extractStringAttribute(resourceAttributes, SemanticInternalAttributes.MACHINE_ID), workerId: extractStringAttribute(resourceAttributes, SemanticInternalAttributes.WORKER_ID), workerVersion: extractStringAttribute( resourceAttributes, SemanticInternalAttributes.WORKER_VERSION ), }; const rows: MetricsV1Input[] = []; for (const scopeMetrics of resourceMetrics.scopeMetrics) { for (const metric of scopeMetrics.metrics) { const metricName = metric.name; // Process gauge data points if (metric.gauge) { for (const dp of metric.gauge.dataPoints) { const value: number = dp.asDouble !== undefined ? dp.asDouble : dp.asInt !== undefined ? Number(dp.asInt) : 0; const resolved = resolveDataPointContext(dp.attributes ?? [], resourceCtx); rows.push({ organization_id: organizationId, project_id: projectId, environment_id: environmentId, metric_name: metricName, metric_type: "gauge", metric_subject: resolved.machineId ?? "unknown", bucket_start: floorToTenSecondBucket(dp.timeUnixNano), value, attributes: resolved.attributes, }); } } // Process sum data points if (metric.sum) { for (const dp of metric.sum.dataPoints) { const value: number = dp.asDouble !== undefined ? dp.asDouble : dp.asInt !== undefined ? Number(dp.asInt) : 0; const resolved = resolveDataPointContext(dp.attributes ?? [], resourceCtx); rows.push({ organization_id: organizationId, project_id: projectId, environment_id: environmentId, metric_name: metricName, metric_type: "sum", metric_subject: resolved.machineId ?? "unknown", bucket_start: floorToTenSecondBucket(dp.timeUnixNano), value, attributes: resolved.attributes, }); } } // Process histogram data points if (metric.histogram) { for (const dp of metric.histogram.dataPoints) { const resolved = resolveDataPointContext(dp.attributes ?? [], resourceCtx); const count = Number(dp.count); const sum = dp.sum ?? 0; rows.push({ organization_id: organizationId, project_id: projectId, environment_id: environmentId, metric_name: metricName, metric_type: "histogram", metric_subject: resolved.machineId ?? "unknown", bucket_start: floorToTenSecondBucket(dp.timeUnixNano), value: count > 0 ? sum / count : 0, attributes: resolved.attributes, }); } } } } return rows; } // Prefixes injected by TaskContextMetricExporter — these are extracted into // the nested `trigger` key and should not appear as top-level user attributes. const INTERNAL_METRIC_ATTRIBUTE_PREFIXES = ["ctx.", "worker."]; interface ResourceContext { taskSlug: string | undefined; runId: string | undefined; attemptNumber: number | undefined; machineId: string | undefined; workerId: string | undefined; workerVersion: string | undefined; } function resolveDataPointContext( dpAttributes: KeyValue[], resourceCtx: ResourceContext ): { machineId: string | undefined; attributes: Record; } { const runId = resourceCtx.runId ?? extractStringAttribute(dpAttributes, SemanticInternalAttributes.RUN_ID); const taskSlug = resourceCtx.taskSlug ?? extractStringAttribute(dpAttributes, SemanticInternalAttributes.TASK_SLUG); const attemptNumber = resourceCtx.attemptNumber ?? extractNumberAttribute(dpAttributes, SemanticInternalAttributes.ATTEMPT_NUMBER); const machineId = resourceCtx.machineId ?? extractStringAttribute(dpAttributes, SemanticInternalAttributes.MACHINE_ID); const workerId = resourceCtx.workerId ?? extractStringAttribute(dpAttributes, SemanticInternalAttributes.WORKER_ID); const workerVersion = resourceCtx.workerVersion ?? extractStringAttribute(dpAttributes, SemanticInternalAttributes.WORKER_VERSION); const machineName = extractStringAttribute( dpAttributes, SemanticInternalAttributes.MACHINE_PRESET_NAME ); const environmentType = extractStringAttribute( dpAttributes, SemanticInternalAttributes.ENVIRONMENT_TYPE ); // Build the trigger context object with only defined values const trigger: Record = {}; if (runId) trigger.run_id = runId; if (taskSlug) trigger.task_slug = taskSlug; if (attemptNumber !== undefined) trigger.attempt_number = attemptNumber; if (machineId) trigger.machine_id = machineId; if (machineName) trigger.machine_name = machineName; if (workerId) trigger.worker_id = workerId; if (workerVersion) trigger.worker_version = workerVersion; if (environmentType) trigger.environment_type = environmentType; // Build user attributes, filtering out internal ctx/worker keys const result: Record = {}; if (Object.keys(trigger).length > 0) { result.trigger = trigger; } for (const attr of dpAttributes) { if (INTERNAL_METRIC_ATTRIBUTE_PREFIXES.some((prefix) => attr.key.startsWith(prefix))) { continue; } if (isStringValue(attr.value)) { result[attr.key] = attr.value.stringValue; } else if (isIntValue(attr.value)) { result[attr.key] = Number(attr.value.intValue); } else if (isDoubleValue(attr.value)) { result[attr.key] = attr.value.doubleValue; } else if (isBoolValue(attr.value)) { result[attr.key] = attr.value.boolValue; } } return { machineId, attributes: result }; } function extractEventProperties(attributes: KeyValue[], prefix?: string) { return { metadata: convertSelectedKeyValueItemsToMap(attributes, [SemanticInternalAttributes.METADATA]), environmentId: extractStringAttribute(attributes, [ prefix, SemanticInternalAttributes.ENVIRONMENT_ID, ]), organizationId: extractStringAttribute(attributes, [ prefix, SemanticInternalAttributes.ORGANIZATION_ID, ]), projectId: extractStringAttribute(attributes, [prefix, SemanticInternalAttributes.PROJECT_ID]), runId: extractStringAttribute(attributes, [prefix, SemanticInternalAttributes.RUN_ID]), attemptNumber: extractNumberAttribute(attributes, [ prefix, SemanticInternalAttributes.ATTEMPT_NUMBER, ]), taskSlug: extractStringAttribute(attributes, [prefix, SemanticInternalAttributes.TASK_SLUG]), machineId: extractStringAttribute(attributes, [prefix, SemanticInternalAttributes.MACHINE_ID]), }; } function pickAttributes(attributes: KeyValue[], prefix: string): KeyValue[] { return attributes .filter((attribute) => attribute.key.startsWith(prefix)) .map((attribute) => { return { key: attribute.key.replace(`${prefix}.`, ""), value: attribute.value, }; }); } function convertKeyValueItemsToMap( attributes: KeyValue[], filteredKeys: string[] = [], prefix?: string, filteredPrefixes: string[] = [] ): Record | undefined { if (!attributes) return; if (!attributes.length) return; let filteredAttributes = attributes.filter((attribute) => !filteredKeys.includes(attribute.key)); if (!filteredAttributes.length) return; if (filteredPrefixes.length) { filteredAttributes = filteredAttributes.filter( (attribute) => !filteredPrefixes.some((prefix) => attribute.key.startsWith(prefix)) ); } if (!filteredAttributes.length) return; const result = filteredAttributes.reduce( (map: Record, attribute) => { map[`${prefix ? `${prefix}.` : ""}${attribute.key}`] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) ? Number(attribute.value.intValue) : isDoubleValue(attribute.value) ? attribute.value.doubleValue : isBoolValue(attribute.value) ? attribute.value.boolValue : isBytesValue(attribute.value) ? binaryToHex(attribute.value.bytesValue) : isArrayValue(attribute.value) ? serializeArrayValue(attribute.value.arrayValue!.values) : undefined; return map; }, {} ); return result; } function convertSelectedKeyValueItemsToMap( attributes: KeyValue[], selectedPrefixes: string[] = [], prefix?: string ): Record | undefined { if (!attributes) return; if (!attributes.length) return; let selectedAttributes = attributes.filter((attribute) => selectedPrefixes.some((prefix) => attribute.key.startsWith(prefix)) ); if (!selectedAttributes.length) return; const result = selectedAttributes.reduce( (map: Record, attribute) => { map[`${prefix ? `${prefix}.` : ""}${attribute.key}`] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) ? Number(attribute.value.intValue) : isDoubleValue(attribute.value) ? attribute.value.doubleValue : isBoolValue(attribute.value) ? attribute.value.boolValue : isBytesValue(attribute.value) ? binaryToHex(attribute.value.bytesValue) : isArrayValue(attribute.value) ? serializeArrayValue(attribute.value.arrayValue!.values) : undefined; return map; }, {} ); return result; } function spanEventsToEventEvents(events: Span_Event[]): CreateEventInput["events"] { return events.map((event) => { return { name: event.name, time: convertUnixNanoToDate(event.timeUnixNano), properties: convertKeyValueItemsToMap(event.attributes ?? []), }; }); } function spanStatusToEventStatus(status: Span["status"]): CreatableEventStatus { if (!status) return "UNSET"; switch (status.code) { case Status_StatusCode.OK: { return "OK"; } case Status_StatusCode.ERROR: { return "ERROR"; } case Status_StatusCode.UNSET: { return "UNSET"; } default: { return "UNSET"; } } } function spanKindToEventKind(kind: Span["kind"]): CreatableEventKind { switch (kind) { case Span_SpanKind.CLIENT: { return "CLIENT"; } case Span_SpanKind.SERVER: { return "SERVER"; } case Span_SpanKind.CONSUMER: { return "CONSUMER"; } case Span_SpanKind.PRODUCER: { return "PRODUCER"; } default: { return "INTERNAL"; } } } function logLevelToEventLevel(level: SeverityNumber): CreateEventInput["level"] { switch (level) { case SeverityNumber.TRACE: case SeverityNumber.TRACE2: case SeverityNumber.TRACE3: case SeverityNumber.TRACE4: { return "TRACE"; } case SeverityNumber.DEBUG: case SeverityNumber.DEBUG2: case SeverityNumber.DEBUG3: case SeverityNumber.DEBUG4: { return "DEBUG"; } case SeverityNumber.INFO: case SeverityNumber.INFO2: case SeverityNumber.INFO3: case SeverityNumber.INFO4: { return "INFO"; } case SeverityNumber.WARN: case SeverityNumber.WARN2: case SeverityNumber.WARN3: case SeverityNumber.WARN4: { return "WARN"; } case SeverityNumber.ERROR: case SeverityNumber.ERROR2: case SeverityNumber.ERROR3: case SeverityNumber.ERROR4: { return "ERROR"; } case SeverityNumber.FATAL: case SeverityNumber.FATAL2: case SeverityNumber.FATAL3: case SeverityNumber.FATAL4: { return "ERROR"; } default: { return "INFO"; } } } function logLevelToEventStatus(level: SeverityNumber): CreatableEventStatus { switch (level) { case SeverityNumber.TRACE: case SeverityNumber.TRACE2: case SeverityNumber.TRACE3: case SeverityNumber.TRACE4: { return "OK"; } case SeverityNumber.DEBUG: case SeverityNumber.DEBUG2: case SeverityNumber.DEBUG3: case SeverityNumber.DEBUG4: { return "OK"; } case SeverityNumber.INFO: case SeverityNumber.INFO2: case SeverityNumber.INFO3: case SeverityNumber.INFO4: { return "OK"; } case SeverityNumber.WARN: case SeverityNumber.WARN2: case SeverityNumber.WARN3: case SeverityNumber.WARN4: { return "OK"; } case SeverityNumber.ERROR: case SeverityNumber.ERROR2: case SeverityNumber.ERROR3: case SeverityNumber.ERROR4: { return "ERROR"; } case SeverityNumber.FATAL: case SeverityNumber.FATAL2: case SeverityNumber.FATAL3: case SeverityNumber.FATAL4: { return "ERROR"; } default: { return "OK"; } } } function convertUnixNanoToDate(unixNano: bigint | number): Date { return new Date(Number(BigInt(unixNano) / BigInt(1_000_000))); } function extractStringAttribute( attributes: KeyValue[], name: string | Array ): string | undefined; function extractStringAttribute( attributes: KeyValue[], name: string | Array, fallback: string ): string; function extractStringAttribute( attributes: KeyValue[], name: string | Array, fallback?: string ): string | undefined { const key = Array.isArray(name) ? name.filter(Boolean).join(".") : name; const attribute = attributes.find((attribute) => attribute.key === key); if (!attribute) return fallback; return isStringValue(attribute?.value) ? attribute.value.stringValue : fallback; } function extractNumberAttribute( attributes: KeyValue[], name: string | Array ): number | undefined; function extractNumberAttribute( attributes: KeyValue[], name: string | Array, fallback: number ): number; function extractNumberAttribute( attributes: KeyValue[], name: string | Array, fallback?: number ): number | undefined { const key = Array.isArray(name) ? name.filter(Boolean).join(".") : name; const attribute = attributes.find((attribute) => attribute.key === key); if (!attribute) return fallback; return isIntValue(attribute?.value) ? Number(attribute.value.intValue) : fallback; } function extractArrayAttribute( attributes: KeyValue[], name: string | Array ): string[] | undefined { const key = Array.isArray(name) ? name.filter(Boolean).join(".") : name; const attribute = attributes.find((attribute) => attribute.key === key); if (!attribute?.value?.arrayValue?.values) return undefined; return attribute.value.arrayValue.values .filter((v): v is { stringValue: string } => isStringValue(v)) .map((v) => v.stringValue); } function isPartialSpan(span: Span): boolean { if (!span.attributes) return false; const attribute = span.attributes.find( (attribute) => attribute.key === SemanticInternalAttributes.SPAN_PARTIAL ); if (!attribute) return false; return isBoolValue(attribute.value) ? attribute.value.boolValue : false; } export function isBoolValue(value: AnyValue | undefined): value is { boolValue: boolean } { if (!value) return false; return typeof value.boolValue === "boolean"; } export function isStringValue(value: AnyValue | undefined): value is { stringValue: string } { if (!value) return false; return typeof value.stringValue === "string"; } function isIntValue(value: AnyValue | undefined): value is { intValue: bigint } { if (!value) return false; return typeof value.intValue === "number" || typeof value.intValue === "bigint"; } function isDoubleValue(value: AnyValue | undefined): value is { doubleValue: number } { if (!value) return false; return typeof value.doubleValue === "number"; } function isBytesValue(value: AnyValue | undefined): value is { bytesValue: Buffer } { if (!value) return false; return Buffer.isBuffer(value.bytesValue); } function isArrayValue( value: AnyValue | undefined ): value is { arrayValue: { values: AnyValue[] } } { if (!value) return false; return value.arrayValue != null && Array.isArray(value.arrayValue.values); } /** * Serialize an OTEL array value into a JSON string. * For arrays of strings, produces a JSON array: `["item1","item2"]` * For mixed types, extracts primitives and serializes. */ function serializeArrayValue(values: AnyValue[]): string { const items = values.map((v) => { if (isStringValue(v)) return v.stringValue; if (isIntValue(v)) return Number(v.intValue); if (isDoubleValue(v)) return v.doubleValue; if (isBoolValue(v)) return v.boolValue; return null; }); return JSON.stringify(items); } function binaryToHex(buffer: Buffer | string): string; function binaryToHex(buffer: Buffer | string | undefined): string | undefined; function binaryToHex(buffer: Buffer | string | undefined): string | undefined { if (!buffer) return undefined; if (typeof buffer === "string") return buffer; return Buffer.from(Array.from(buffer)).toString("hex"); } function truncateAttributes( attributes: Record | undefined, maximumLength: number = 1024 ): Record | undefined { if (!attributes) return undefined; const truncatedAttributes: Record = {}; for (const [key, value] of Object.entries(attributes)) { if (!key) continue; if (typeof value === "string") { truncatedAttributes[key] = truncateAndDetectUnpairedSurrogate(value, maximumLength); } else { truncatedAttributes[key] = value; } } return truncatedAttributes; } function truncateAndDetectUnpairedSurrogate(str: string, maximumLength: number): string { const truncatedString = smartTruncateString(str, maximumLength); if (hasUnpairedSurrogateAtEnd(truncatedString)) { return smartTruncateString(truncatedString, [...truncatedString].length - 1); } return truncatedString; } const ASCII_ONLY_REGEX = /^[\p{ASCII}]*$/u; function smartTruncateString(str: string, maximumLength: number): string { if (!str) return ""; if (str.length <= maximumLength) return str; const checkLength = Math.min(str.length, maximumLength * 2 + 2); if (ASCII_ONLY_REGEX.test(str.slice(0, checkLength))) { return str.slice(0, maximumLength); } return [...str.slice(0, checkLength)].slice(0, maximumLength).join(""); } function hasUnpairedSurrogateAtEnd(str: string): boolean { if (str.length === 0) return false; const lastCode = str.charCodeAt(str.length - 1); // Check if last character is an unpaired high surrogate if (lastCode >= 0xd800 && lastCode <= 0xdbff) { return true; // High surrogate at end = unpaired } // Check if last character is an unpaired low surrogate if (lastCode >= 0xdc00 && lastCode <= 0xdfff) { // Low surrogate is only valid if preceded by high surrogate if (str.length === 1) return true; // Single low surrogate const secondLastCode = str.charCodeAt(str.length - 2); if (secondLastCode < 0xd800 || secondLastCode > 0xdbff) { return true; // Low surrogate not preceded by high surrogate } } return false; }