diff --git a/package-lock.json b/package-lock.json index 84d042f0..d94277ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3758,39 +3758,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.57.2.tgz", - "integrity": "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-logs": "0.57.2", - "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "protobufjs": "^7.3.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/propagator-b3": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz", @@ -3837,51 +3804,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.57.2.tgz", - "integrity": "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz", - "integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", @@ -5105,70 +5027,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.29", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", @@ -5872,6 +5730,7 @@ "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.10.0" @@ -5881,6 +5740,7 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/normalize-package-data": { @@ -12188,12 +12048,6 @@ "node": ">= 12.0.0" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -15033,30 +14887,6 @@ "node": ">= 6" } }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -19573,7 +19403,6 @@ "@opentelemetry/api": "1.9.0", "@opentelemetry/core": "1.30.1", "@opentelemetry/instrumentation": "^0.203.0", - "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-node": "1.30.1" }, diff --git a/packages/otel/package.json b/packages/otel/package.json index 1d90ebb7..a80de45a 100644 --- a/packages/otel/package.json +++ b/packages/otel/package.json @@ -120,7 +120,6 @@ "@opentelemetry/api": "1.9.0", "@opentelemetry/core": "1.30.1", "@opentelemetry/instrumentation": "^0.203.0", - "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-node": "1.30.1" } diff --git a/packages/otel/src/exporters/netlify.test.ts b/packages/otel/src/exporters/netlify.test.ts new file mode 100644 index 00000000..659d47c7 --- /dev/null +++ b/packages/otel/src/exporters/netlify.test.ts @@ -0,0 +1,133 @@ +import { serializeSpans } from './netlify.ts' +import { SpanKind, SpanStatusCode, TraceFlags } from '@opentelemetry/api' +import { TraceState } from '@opentelemetry/core' +import { Resource } from '@opentelemetry/resources' +import { ReadableSpan } from '@opentelemetry/sdk-trace-node' +import { describe, test, expect } from 'vitest' + +function createSpan(): ReadableSpan { + return { + name: 'span-name', + kind: SpanKind.INTERNAL, + spanContext: () => ({ + spanId: '0000000000000002', + traceFlags: TraceFlags.SAMPLED, + traceId: '00000000000000000000000000000001', + isRemote: false, + traceState: new TraceState('span=bar'), + }), + startTime: [1640715557, 342725388], + endTime: [1640715558, 642725388], + status: { + code: SpanStatusCode.OK, + }, + attributes: { 'string-attribute': 'some attribute value' }, + links: [ + { + context: { + spanId: '0000000000000003', + traceId: '00000000000000000000000000000002', + traceFlags: TraceFlags.SAMPLED, + isRemote: false, + traceState: new TraceState('link=foo'), + }, + attributes: { + 'link-attribute': 'string value', + }, + }, + ], + events: [ + { + name: 'event', + time: [1640715558, 542725388], + attributes: { + 'event-attribute': 'string value', + }, + }, + ], + duration: [1, 300000000], + ended: true, + resource: new Resource({ + 'service.name': 'serviceName', + 'service.version': 'serviceVersion', + 'process.runtime.name': 'nodejs', + 'process.runtime.version': 'runtimeVersion', + 'deployment.environment': 'deploymentEnvironment', + 'http.url': 'siteUrl', + 'netlify.site.id': 'siteId', + 'netlify.site.name': 'siteName', + }), + instrumentationLibrary: { + name: '@netlify/otel', + version: '1.0.0', + }, + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0, + } +} + +describe('`serializeSpans`', () => { + test('Returns expected results', () => { + const sampleSpans = [createSpan()] + const result = serializeSpans(sampleSpans) + + const expectedResult = { + resourceSpans: [ + { + resource: { + attributes: [ + { key: 'service.name', value: { stringValue: 'serviceName' } }, + { key: 'service.version', value: { stringValue: 'serviceVersion' } }, + { key: 'process.runtime.name', value: { stringValue: 'nodejs' } }, + { key: 'process.runtime.version', value: { stringValue: 'runtimeVersion' } }, + { key: 'deployment.environment', value: { stringValue: 'deploymentEnvironment' } }, + { key: 'http.url', value: { stringValue: 'siteUrl' } }, + { key: 'netlify.site.id', value: { stringValue: 'siteId' } }, + { key: 'netlify.site.name', value: { stringValue: 'siteName' } }, + ], + droppedAttributesCount: 0, + }, + scopeSpans: [ + { + scope: { name: '@netlify/otel', version: '1.0.0' }, + spans: [ + { + traceId: '00000000000000000000000000000001', + spanId: '0000000000000002', + name: 'span-name', + kind: 1, + startTimeUnixNano: '1640715557342725388', + endTimeUnixNano: '1640715558642725388', + attributes: [{ key: 'string-attribute', value: { stringValue: 'some attribute value' } }], + droppedAttributesCount: 0, + events: [ + { + name: 'event', + timeUnixNano: '1640715558542725388', + attributes: [{ key: 'event-attribute', value: { stringValue: 'string value' } }], + droppedAttributesCount: 0, + }, + ], + droppedEventsCount: 0, + status: { code: 1 }, + links: [ + { + spanId: '0000000000000003', + traceId: '00000000000000000000000000000002', + attributes: [{ key: 'link-attribute', value: { stringValue: 'string value' } }], + droppedAttributesCount: 0, + }, + ], + droppedLinksCount: 0, + }, + ], + }, + ], + }, + ], + } + + expect(result).toEqual(expectedResult) + }) +}) diff --git a/packages/otel/src/exporters/netlify.ts b/packages/otel/src/exporters/netlify.ts index 90ed53fb..55d8d4c9 100644 --- a/packages/otel/src/exporters/netlify.ts +++ b/packages/otel/src/exporters/netlify.ts @@ -1,13 +1,11 @@ -import { diag, type DiagLogger } from '@opentelemetry/api' +import { diag, SpanKind, type DiagLogger } from '@opentelemetry/api' import { BindOnceFuture, ExportResult, ExportResultCode } from '@opentelemetry/core' -import { JsonTraceSerializer } from '@opentelemetry/otlp-transformer' import type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-node' import { TRACE_PREFIX } from '../constants.ts' export class NetlifySpanExporter implements SpanExporter { #shutdownOnce: BindOnceFuture #logger: DiagLogger - static #decoder = new TextDecoder() constructor() { this.#shutdownOnce = new BindOnceFuture(this.#shutdown, this) @@ -27,7 +25,7 @@ export class NetlifySpanExporter implements SpanExporter { return } - console.log(TRACE_PREFIX, NetlifySpanExporter.#decoder.decode(JsonTraceSerializer.serializeRequest(spans))) + console.log(TRACE_PREFIX, JSON.stringify(serializeSpans(spans))) resultCallback({ code: ExportResultCode.SUCCESS }) } @@ -46,3 +44,104 @@ export class NetlifySpanExporter implements SpanExporter { return Promise.resolve() } } + +// Replaces JsonTraceSerializer.serializeRequest(spans) +export function serializeSpans(spans: ReadableSpan[]): Record { + return { + resourceSpans: spans.map((span) => { + const spanContext = span.spanContext() + return { + resource: { + attributes: toAttributes(span.resource.attributes), + droppedAttributesCount: span.droppedAttributesCount, + }, + scopeSpans: [ + { + scope: { + name: span.instrumentationLibrary.name, + version: span.instrumentationLibrary.version, + }, + spans: [ + { + traceId: spanContext.traceId, + spanId: spanContext.spanId, + parentSpanId: span.parentSpanId, + + name: span.name, + kind: span.kind || SpanKind.SERVER, + + startTimeUnixNano: hrTimeToNanos(span.startTime), + endTimeUnixNano: hrTimeToNanos(span.endTime), + + attributes: toAttributes(span.attributes), + droppedAttributesCount: span.droppedAttributesCount, + + events: span.events.map((event) => ({ + name: event.name, + timeUnixNano: hrTimeToNanos(event.time), + attributes: toAttributes(event.attributes ?? {}), + droppedAttributesCount: event.droppedAttributesCount ?? 0, + })), + droppedEventsCount: span.droppedEventsCount, + + status: { + code: span.status.code, + message: span.status.message, + }, + + links: span.links.map((link) => ({ + spanId: link.context.spanId, + traceId: link.context.traceId, + attributes: toAttributes(link.attributes ?? {}), + droppedAttributesCount: link.droppedAttributesCount ?? 0, + })), + droppedLinksCount: span.droppedLinksCount, + }, + ], + }, + ], + } + }), + } +} + +// Reference: opentelemetry-js/experimental/packages/otlp-transformer/src/common/internal.ts + +type IAnyValue = Record + +export function toAttributes(attributes: Record): IAnyValue[] { + return Object.keys(attributes).map((key) => toKeyValue(key, attributes[key])) +} + +function toKeyValue(key: string, value: unknown): IAnyValue { + return { + key: key, + value: toAnyValue(value), + } +} + +function toAnyValue(value: unknown): IAnyValue { + const t = typeof value + if (t === 'string') return { stringValue: value as string } + if (t === 'number') { + if (!Number.isInteger(value)) return { doubleValue: value as number } + return { intValue: value as number } + } + if (t === 'boolean') return { boolValue: value as boolean } + if (value instanceof Uint8Array) return { bytesValue: value } + if (Array.isArray(value)) return { arrayValue: { values: value.map(toAnyValue) } } + if (t === 'object' && value != null) + return { + kvlistValue: { + values: Object.entries(value as object).map(([k, v]) => toKeyValue(k, v)), + }, + } + + return {} +} + +function hrTimeToNanos(hrTime: [number, number]) { + const NANOSECONDS = BigInt(1_000_000_000) + const nanos = BigInt(Math.trunc(hrTime[0])) * NANOSECONDS + BigInt(Math.trunc(hrTime[1])) + return nanos.toString() +}