Skip to content

Commit 38ca257

Browse files
authored
feat(otlp-transformer): replace protobufjs metrics serialization with custom implementation (#6629)
1 parent 013c600 commit 38ca257

25 files changed

Lines changed: 1130 additions & 151 deletions

bundler-tests/browser/nextjs-15-edge/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"build": "rm -rf .next && next build",
7-
"test:bundle": "npm i && node test-bundle.mjs"
7+
"test:bundle": "npm i && npm run build"
88
},
99
"dependencies": {
1010
"@opentelemetry/api": "file:../../../api",

bundler-tests/browser/nextjs-15-edge/test-bundle.mjs

Lines changed: 0 additions & 37 deletions
This file was deleted.

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1010

1111
### :rocket: Features
1212

13+
* feat(otlp-transformer): replace protobufjs metrics serialization with custom implementation [#6625](https://github.com/open-telemetry/opentelemetry-js/pull/6629) @pichlermarc
1314
* feat(configuration): show all config validation errors, if there are multiple [#6683](https://github.com/open-telemetry/opentelemetry-js/pull/6683) @trentm
1415
* feat(sdk-node): allow startNodeSDK() without an arg [#6688](https://github.com/open-telemetry/opentelemetry-js/pull/6688) @trentm
1516

experimental/packages/otlp-transformer/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
"compile": "tsc --build tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
1818
"clean": "tsc --build --clean tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
1919
"protos": "npm run submodule && npm run protos:generate",
20-
"protos:generate:js": "pbjs -t static-module -p ./protos -w commonjs --null-defaults -o ./src/generated/root.js ./protos/opentelemetry/proto/common/v1/common.proto ./protos/opentelemetry/proto/resource/v1/resource.proto ./protos/opentelemetry/proto/metrics/v1/metrics.proto ./protos/opentelemetry/proto/collector/metrics/v1/metrics_service.proto && npm run protos:generate:js:tests",
20+
"protos:generate:js": "npm run protos:generate:js:tests",
2121
"protos:generate:js:tests": "npm run protos:generate:js:testbed && npm run protos:generate:js:test-signals",
2222
"protos:generate:js:testbed": "pbjs -t static-module -p ./test/fixtures -w commonjs --null-defaults -o ./test/generated/testbed.js ./test/fixtures/testbed.proto",
23-
"protos:generate:js:test-signals": "pbjs -r signals -t static-module -p ./protos -w commonjs --null-defaults -o ./test/generated/signals.js ./protos/opentelemetry/proto/common/v1/common.proto ./protos/opentelemetry/proto/resource/v1/resource.proto ./protos/opentelemetry/proto/logs/v1/logs.proto ./protos/opentelemetry/proto/collector/logs/v1/logs_service.proto ./protos/opentelemetry/proto/trace/v1/trace.proto ./protos/opentelemetry/proto/collector/trace/v1/trace_service.proto",
24-
"protos:generate:ts": "pbts -o ./src/generated/root.d.ts ./src/generated/root.js && pbts -o ./test/generated/testbed.d.ts ./test/generated/testbed.js && pbts -o ./test/generated/signals.d.ts ./test/generated/signals.js",
23+
"protos:generate:js:test-signals": "pbjs -r signals -t static-module -p ./protos -w commonjs --null-defaults -o ./test/generated/signals.js ./protos/opentelemetry/proto/common/v1/common.proto ./protos/opentelemetry/proto/resource/v1/resource.proto ./protos/opentelemetry/proto/logs/v1/logs.proto ./protos/opentelemetry/proto/collector/logs/v1/logs_service.proto ./protos/opentelemetry/proto/trace/v1/trace.proto ./protos/opentelemetry/proto/collector/trace/v1/trace_service.proto ./protos/opentelemetry/proto/metrics/v1/metrics.proto ./protos/opentelemetry/proto/collector/metrics/v1/metrics_service.proto",
24+
"protos:generate:ts": "pbts -o ./test/generated/testbed.d.ts ./test/generated/testbed.js && pbts -o ./test/generated/signals.d.ts ./test/generated/signals.js",
2525
"protos:generate": "npm run protos:generate:js && npm run protos:generate:ts",
2626
"lint": "eslint . --ext .ts",
2727
"lint:fix": "eslint . --ext .ts --fix",
@@ -79,6 +79,7 @@
7979
"karma-webpack": "5.0.1",
8080
"mocha": "11.7.5",
8181
"nyc": "17.1.0",
82+
"protobufjs": "8.0.1",
8283
"protobufjs-cli": "2.0.1",
8384
"ts-loader": "9.5.4",
8485
"typescript": "5.0.4",
@@ -90,8 +91,7 @@
9091
"@opentelemetry/resources": "2.7.1",
9192
"@opentelemetry/sdk-logs": "0.217.0",
9293
"@opentelemetry/sdk-metrics": "2.7.1",
93-
"@opentelemetry/sdk-trace-base": "2.7.1",
94-
"protobufjs": "8.0.1"
94+
"@opentelemetry/sdk-trace-base": "2.7.1"
9595
},
9696
"homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-transformer",
9797
"sideEffects": false

experimental/packages/otlp-transformer/src/common/protobuf/common-serializer.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
import type { Attributes, HrTime } from '@opentelemetry/api';
66
import type { AnyValue, LogAttributes } from '@opentelemetry/api-logs';
7+
import type { InstrumentationScope } from '@opentelemetry/core';
8+
import type { Resource } from '@opentelemetry/resources';
79
import type { IProtobufWriter } from './i-protobuf-writer';
810

911
/**
@@ -187,3 +189,60 @@ export function writeAnyValue(writer: IProtobufWriter, value: AnyValue): void {
187189
}
188190
// Else: unsupported type, write nothing
189191
}
192+
193+
/**
194+
* Write an InstrumentationScope message.
195+
*
196+
* Proto fields (InstrumentationScope):
197+
* 1 name string (wire type 2)
198+
* 2 version string (wire type 2)
199+
*/
200+
export function writeInstrumentationScope(
201+
writer: IProtobufWriter,
202+
scope: InstrumentationScope,
203+
fieldNumber: number
204+
): void {
205+
writer.writeTag(fieldNumber, 2);
206+
const start = writer.startLengthDelimited();
207+
const startPos = writer.pos;
208+
209+
// name (field 1, string)
210+
writer.writeTag(1, 2);
211+
writer.writeString(scope.name);
212+
213+
// version (field 2, string) - skip if empty
214+
if (scope.version) {
215+
writer.writeTag(2, 2);
216+
writer.writeString(scope.version);
217+
}
218+
219+
writer.finishLengthDelimited(start, writer.pos - startPos);
220+
}
221+
222+
/**
223+
* Write a Resource message and its enclosing tag.
224+
*
225+
* Proto fields (Resource):
226+
* 1 attributes repeated KeyValue (wire type 2)
227+
* 2 dropped_attributes_count uint32 (wire type 0)
228+
*/
229+
export function writeResource(
230+
writer: IProtobufWriter,
231+
resource: Resource,
232+
fieldNumber: number
233+
): void {
234+
writer.writeTag(fieldNumber, 2);
235+
const resourceStart = writer.startLengthDelimited();
236+
const resourceStartPos = writer.pos;
237+
238+
// attributes (field 1, repeated KeyValue)
239+
if (resource.attributes) {
240+
writeAttributes(writer, resource.attributes, 1);
241+
}
242+
243+
// dropped_attributes_count (field 2, uint32) - set to 0 as we don't track this
244+
writer.writeTag(2, 0);
245+
writer.writeVarint(0);
246+
247+
writer.finishLengthDelimited(resourceStart, writer.pos - resourceStartPos);
248+
}

experimental/packages/otlp-transformer/src/common/protobuf/i-protobuf-writer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface IProtobufWriter {
77
pos: number;
88
writeTag(fieldNumber: number, wireType: number): void;
99
writeVarint(value: number): void;
10+
writeSint32(value: number): void;
11+
writeSfixed64(value: number): void;
1012
writeFixed32(value: number): void;
1113
writeFixed64(low: number, high: number): void;
1214
writeBytes(bytes: Uint8Array): void;

experimental/packages/otlp-transformer/src/common/protobuf/protobuf-reader.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,37 @@ export class ProtobufReader {
3737
/**
3838
* Read a base-128 varint.
3939
* Returns a JS `number`; precision above 2^53 is silently lost.
40+
* Throws if the buffer is truncated mid-varint.
4041
*/
4142
readVarint(): number {
4243
let result = 0;
4344
let shift = 0;
45+
let terminated = false;
4446
while (this.pos < this._buf.length) {
4547
const b = this._buf[this.pos++];
4648
result += (b & 0x7f) * Math.pow(2, shift);
4749
shift += 7;
48-
if ((b & 0x80) === 0) break;
50+
if ((b & 0x80) === 0) {
51+
terminated = true;
52+
break;
53+
}
54+
}
55+
if (!terminated) {
56+
throw new Error(
57+
'Truncated buffer: unexpected end of data while reading varint'
58+
);
4959
}
5060
return result;
5161
}
5262

5363
/** Read a length-delimited byte sequence (bytes field or embedded message). */
5464
readBytes(): Uint8Array {
5565
const len = this.readVarint();
66+
if (this.pos + len > this._buf.length) {
67+
throw new Error(
68+
`Truncated buffer: expected ${len} bytes at position ${this.pos}, but only ${this._buf.length - this.pos} available`
69+
);
70+
}
5671
const slice = this._buf.subarray(this.pos, this.pos + len);
5772
this.pos += len;
5873
return slice;

experimental/packages/otlp-transformer/src/common/protobuf/protobuf-size-estimator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export class ProtobufSizeEstimator implements IProtobufWriter {
5151
this.pos += estimateVarintSize(value);
5252
}
5353

54+
writeSint32(value: number): void {
55+
// Zigzag encode: (n << 1) ^ (n >> 31)
56+
this.pos += estimateVarintSize(((value << 1) ^ (value >> 31)) >>> 0);
57+
}
58+
59+
writeSfixed64(_value: number): void {
60+
this.pos += 8;
61+
}
62+
5463
writeFixed32(_value: number): void {
5564
this.pos += 4;
5665
}

experimental/packages/otlp-transformer/src/common/protobuf/protobuf-writer.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,42 @@ export class ProtobufWriter implements IProtobufWriter {
126126
}
127127
}
128128

129+
/**
130+
* Write a sint32 value using zigzag encoding
131+
*/
132+
writeSint32(value: number): void {
133+
// Zigzag encode: (n << 1) ^ (n >> 31)
134+
this.writeVarint(((value << 1) ^ (value >> 31)) >>> 0);
135+
}
136+
137+
/**
138+
* Write a signed 64-bit fixed integer (sfixed64) from a JS number.
139+
* Handles negative values via two's complement.
140+
*/
141+
writeSfixed64(value: number): void {
142+
let low: number;
143+
let high: number;
144+
145+
if (value >= 0) {
146+
low = value >>> 0;
147+
high = (value / 0x100000000) >>> 0;
148+
} else {
149+
// Two's complement for negative values
150+
const abs = Math.abs(value);
151+
low = abs >>> 0;
152+
high = (abs / 0x100000000) >>> 0;
153+
// Invert bits and add 1
154+
low = ~low >>> 0;
155+
high = ~high >>> 0;
156+
low = (low + 1) >>> 0;
157+
if (low === 0) {
158+
high = (high + 1) >>> 0;
159+
}
160+
}
161+
162+
this.writeFixed64(low, high);
163+
}
164+
129165
/**
130166
* Write a varint (variable-length integer)
131167
*/

experimental/packages/otlp-transformer/src/i-serializer.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,14 @@
88
*/
99
export interface ISerializer<Request, Response> {
1010
serializeRequest(request: Request): Uint8Array | undefined;
11+
12+
/**
13+
* Deserialize the response from the backend. The response is expected to be in the form of a
14+
* {@link Uint8Array} and will be deserialized into the expected response type.
15+
*
16+
* @param data
17+
* @throws {unknown} if the deserialization fails
18+
* @returns the deserialized response
19+
*/
1120
deserializeResponse(data: Uint8Array): Response;
1221
}

0 commit comments

Comments
 (0)