Skip to content

feat(otlp-transformer): add custom logs protobuf serializer#6390

Merged
pichlermarc merged 12 commits intoopen-telemetry:mainfrom
dynatrace-oss-contrib:feat/custom-logs-proto-serializer
Mar 27, 2026
Merged

feat(otlp-transformer): add custom logs protobuf serializer#6390
pichlermarc merged 12 commits intoopen-telemetry:mainfrom
dynatrace-oss-contrib:feat/custom-logs-proto-serializer

Conversation

@pichlermarc
Copy link
Copy Markdown
Member

@pichlermarc pichlermarc commented Feb 10, 2026

Which problem is this PR solving?

Currently we run into warnings that CSPs are being violated by protobuf.js (#4987). Also, to convert to protobuf.js' format, we do have to go through an intermediate representation, which is costly in terms of allocations.

This PR moves toward fixing this by introducing a custom protobuf serializer that can be used instead.

Note: my previous PR #6228 had a simple dynamic growing approach. However, since there is a risk that memory behavior will change for the worse from the current protobuf.js-based implementation, which does a double-pass to determine the size of the final message, I opted to also implement a double-pass approach. The first pass determines the buffer size and the second pass actually writes to said buffer. This makes the approach slightly slower than what I proposed in #6228 (also slower than JSON serialization, but faster than the protobuf.js-based approach), but more predictable than the doubling the buffer size or always allocating 64KB, which may not be needed.

Towards fixing:

Part of:

Supersedes #6228

Relevant benchmark results:

Old:

transform 512 logs (protobuf) x 502 ops/sec ±0.14% (96 runs sampled)
transform 512 logs (json) x 732 ops/sec ±0.19% (96 runs sampled)

New:

transform 512 logs (protobuf) x 719 ops/sec ±0.20% (97 runs sampled)
transform 512 logs (json) x 734 ops/sec ±0.33% (97 runs sampled)

Disclosure of AI use: I generated the initial prototype of this with GitHub Copilot and Claude Sonnet 4.5, but then applied a whole bunch of optimizations and changes to it to make it fit what we're trying to do here.

Short description of the changes

Type of change

  • New feature

How Has This Been Tested?

  • Added unit tests
  • Existing unit tests
  • Existing E2E tests
  • Existing benchmarks

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 10, 2026

Codecov Report

❌ Patch coverage is 98.96104% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.71%. Comparing base (84bfa90) to head (d152412).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
...ansformer/src/common/protobuf/common-serializer.ts 97.87% 2 Missing ⚠️
...p-transformer/src/logs/protobuf/logs-serializer.ts 98.18% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6390      +/-   ##
==========================================
- Coverage   95.75%   95.71%   -0.04%     
==========================================
  Files         364      369       +5     
  Lines       12095    12461     +366     
  Branches     2884     2945      +61     
==========================================
+ Hits        11581    11927     +346     
- Misses        514      534      +20     
Files with missing lines Coverage Δ
...mer/src/common/protobuf/protobuf-size-estimator.ts 100.00% <100.00%> (ø)
...transformer/src/common/protobuf/protobuf-writer.ts 100.00% <100.00%> (ø)
...ages/otlp-transformer/src/common/protobuf/utils.ts 100.00% <100.00%> (ø)
...ackages/otlp-transformer/src/logs/protobuf/logs.ts 100.00% <100.00%> (ø)
...ansformer/src/common/protobuf/common-serializer.ts 97.87% <97.87%> (ø)
...p-transformer/src/logs/protobuf/logs-serializer.ts 98.18% <98.18%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pichlermarc pichlermarc force-pushed the feat/custom-logs-proto-serializer branch from 3a90e81 to bef2eb7 Compare February 10, 2026 14:11
Comment thread experimental/packages/otlp-transformer/src/common/protobuf/common-serializer.ts Outdated
Comment thread experimental/packages/otlp-transformer/src/common/protobuf/utils.ts Outdated
Comment thread experimental/packages/otlp-transformer/src/common/protobuf/common-serializer.ts Outdated
Comment thread experimental/packages/otlp-transformer/src/common/protobuf/common-serializer.ts Outdated
Comment thread experimental/packages/otlp-transformer/src/logs/protobuf/logs-serializer.ts Outdated
@dyladan
Copy link
Copy Markdown
Member

dyladan commented Mar 20, 2026

Future enhancements not for this PR

  • some things like scope and resource never change. We can cache the results of their serialization and avoid redoing work on future passes
  • time_unix_nano, observed_time_unix_nano, severity_number, dropped_attributes_count, flags, trace_id, span_id are always fixed sizes. We can skip them in the size estimation or shortcut them (e.g. no need to calculate the size of a trace_id when we know it is always 16 bytes + 1 byte tag + 1 byte len).

@pichlermarc
Copy link
Copy Markdown
Member Author

thanks @dyladan for the review. I'll go over the remaining comments on monday :)
the things that I applied for now already yielded additional perf improvements, now we're almost back at JSON levels even though we're doing the double-pass. 🙌

avoiding the string operations for preorganizing the log records is a good call. In practice we do always put the same object on there so it can be used as a key. I changed the comment in sdk-logs to reflect that. people may construct ReadableLogRecords themselves, but we can require them put the same object on there too; it will be needed for #6413 anyway if we want to have any hope of implementing scopeAttributes in a usable way.

@pichlermarc pichlermarc force-pushed the feat/custom-logs-proto-serializer branch from 05024f7 to e5511af Compare March 23, 2026 13:12
import type { IExportLogsServiceRequest } from '../internal-types';
import type { IExportLogsServiceResponse } from '../export-response';

import { createExportLogsServiceRequest } from '../internal';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we @deprecate or even fully remove this function now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still use it during JSON serialization so we still need to keep it around for now.

With this PR some opportunity of improving the JSON path open up since we don't need to support protobuf on the same code-path anymore (removing Encoder comes to mind, it's primary purpose is to facilitate protobuf and JSON on the same code-path), but I'd keep this PR scoped to just protobuf for now.

It'll also be a lot easier to do once we've migrated all signals to a hand-rolled proto implementation; all signals share some common code that can be simplified at once.

@pichlermarc pichlermarc added this pull request to the merge queue Mar 27, 2026
Merged via the queue into open-telemetry:main with commit 533f300 Mar 27, 2026
27 checks passed
@pichlermarc pichlermarc deleted the feat/custom-logs-proto-serializer branch March 27, 2026 10:03
intech added a commit to Connectum-Framework/connectum that referenced this pull request Apr 19, 2026
## Summary

Quarterly OpenTelemetry dependency bump: `@opentelemetry/api-logs` and
friends `0.212.0 → 0.215.0`, stable packages (`resources`,
`sdk-metrics`, `sdk-trace-node`) `2.5.1 → 2.7.0`, `semantic-conventions`
`1.39.0 → 1.40.0`.

## Breaking change impact check

Two upstream breaking changes were analyzed against
`packages/otel/src/`:

1. **Custom `LogRecordExporter.forceFlush()` now required** — N/A.
`@connectum/otel` uses only stock exporters (`OTLPLogExporterHTTP`,
`OTLPLogExporterGRPC`, `ConsoleLogRecordExporter`); no `implements
LogRecordExporter` anywhere in source.
2. **gRPC exporter config `headers` field removed** — N/A. The internal
`CollectorOptions` interface has only `concurrencyLimit` and `url`; no
`headers` is passed into the gRPC exporter constructors.

## Feature auto-gains (no API changes)

- Hand-rolled `ProtobufLogsSerializer` (PR
open-telemetry/opentelemetry-js#6390, v0.215.0) — ~43% throughput
improvement for logs protobuf serialization.
- `cardinalitySelector` option in `PeriodicExportingMetricReader` (PR
#6460, v2.7.0) — protects against label cardinality explosion (e.g. per
`rpc.method`). Can be wired in a follow-up.
- SDK self-monitoring metrics: span creation (PR #6213, v2.6.0) and log
creation (PR #6433, v2.7.0).
- Prototype pollution safety patch in `mergeTwoObjects` (PR #6587,
v2.7.0).
- Stable RPC semantic conventions from semconv 1.28–1.30
(`rpc.response.status_code`, `error.type`).

## Quality gates

- L2 build: 16/16 successful
- L2 typecheck: clean (`tsc --noEmit`)
- L2 test: 29/29 successful, 139 tests in `@connectum/events` alone, 0
failures
- L3 lint (biome): 13/13, no fixes applied

## Changeset

Patch bump for `@connectum/otel` (no public API changes, only underlying
SDK version).

## Test plan

- [x] `pnpm install` regenerates lockfile cleanly
- [x] `pnpm build && pnpm typecheck && pnpm test` pass
- [x] `pnpm lint` passes
- [ ] CI green on PR
- [ ] Smoke test `performance-test-server` example post-merge
(cardinality/throughput benchmark)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated OpenTelemetry SDK and related packages to the latest stable
versions, including upstream bug fixes and feature improvements.
* Updated semantic conventions to the latest version for improved
standards compliance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intech added a commit to Connectum-Framework/connectum that referenced this pull request Apr 19, 2026
…101)

## Summary
- Adds Performance Characteristics section to @connectum/otel README
- Documents OTLP protobuf serializer status per signal
(logs/traces/metrics)
- Explains otlp-transformer version pin rationale
- Provides guidance for high-volume span workloads

## Context
OTel JS attempted to migrate otlp-transformer to @bufbuild/protobuf in
[PR #6179](open-telemetry/opentelemetry-js#6179)
but reverted in [PR
#6225](open-telemetry/opentelemetry-js#6225) due
to ~13x serialization slowdown (see [issue
#6221](open-telemetry/opentelemetry-js#6221)).
Hand-rolled ProtobufLogsSerializer landed in v0.215.0 via [PR
#6390](open-telemetry/opentelemetry-js#6390);
traces and metrics migrations are still pending (see
[#6570](open-telemetry/opentelemetry-js#6570)).

This doc makes the current state explicit for @connectum/otel consumers.

## Test plan
- [x] Markdown renders correctly (biome check passes)
- [x] Links to upstream issues/PRs resolve
- [x] No impact on package code

🤖 Generated with [Claude Code](https://claude.com/claude-code)
intech pushed a commit to Connectum-Framework/connectum that referenced this pull request Apr 19, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`main` is currently in **pre mode** so this branch has prereleases
rather than normal releases. If you want to exit prereleases, run
`changeset pre exit` on `main`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @connectum/auth@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/events@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/events-amqp@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/events@1.0.0-rc.11

## @connectum/events-kafka@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/events@1.0.0-rc.11

## @connectum/events-nats@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/events@1.0.0-rc.11

## @connectum/events-redis@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/events@1.0.0-rc.11

## @connectum/healthcheck@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/interceptors@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/otel@1.0.0-rc.11

### Patch Changes

- [#98](#98)
[`15f4dbb`](15f4dbb)
Thanks [@intech](https://github.com/intech)! - Bump OpenTelemetry SDK to
0.215.0 / v2.7.0 and semantic conventions to 1.40.0.

    Highlights (auto-gain, no API changes in `@connectum/otel`):

- Hand-rolled `ProtobufLogsSerializer` (PR
open-telemetry/opentelemetry-js#6390, v0.215.0) — +67–73% throughput for
typical batch sizes (100–1024 logs); +72% at 512 logs, +67% at 1024 logs
per upstream benchmarks in PR
[#6228](https://github.com/Connectum-Framework/connectum/issues/6228)
- `cardinalitySelector` support in `PeriodicExportingMetricReader` (PR
[#6460](https://github.com/Connectum-Framework/connectum/issues/6460),
v2.7.0) — protection against cardinality explosion on high-variance
attributes
- SDK self-observability: span + log creation metrics (PRs
[#6213](https://github.com/Connectum-Framework/connectum/issues/6213),
[#6433](https://github.com/Connectum-Framework/connectum/issues/6433))
- Internal `mergeTwoObjects` safety checks (PR
[#6587](https://github.com/Connectum-Framework/connectum/issues/6587),
v2.7.0) — additional guards against unsafe key merges
- Updated semantic conventions (semconv v1.40.0) — stable RPC attributes
including `rpc.response.status_code` and `error.type` (stabilized in
semconv v1.39.0)

Breaking changes upstream that do NOT affect `@connectum/otel`
(verified):

- Custom `LogRecordExporter.forceFlush()` requirement — not applicable
(we use stock exporters only)
- gRPC exporter config `headers` field removal — not applicable
(`CollectorOptions` has no `headers`)

- [#99](#99)
[`5b3f01d`](5b3f01d)
Thanks [@intech](https://github.com/intech)! - security(deps): force
patched versions of protobufjs and basic-ftp via pnpm overrides

    Resolves Dependabot alerts on main branch:

- **GHSA-xq3m-2v4x-88gg** (Critical) — Arbitrary code execution in
protobufjs &lt; 7.5.5
        (transitive via `@grpc/proto-loader` under OTel gRPC exporters).
- **GHSA-xq3m-2v4x-88gg** (Critical) — Arbitrary code execution in
protobufjs 8.0.0
        (transitive via `@opentelemetry/otlp-transformer`).
- **GHSA-chqc-8p9q-pq6q** (High) — basic-ftp 5.2.0 FTP Command Injection
via CRLF
        (dev-only transitive via `@exodus/test` → puppeteer-core).
- **GHSA-6v7q-wjvx-w8wg** (High) — basic-ftp ≤ 5.2.1 incomplete CRLF
protection
        (dev-only transitive via `@exodus/test` → puppeteer-core).

No runtime API changes. Only `pnpm.overrides` in the monorepo root were
adjusted
    to force patched transitive versions: `protobufjs@<7.5.5 → 7.5.5`,
    `protobufjs@>=8.0.0 <8.0.1 → 8.0.1`, `basic-ftp@<5.2.2 → 5.2.2`.

## @connectum/reflection@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/testing@1.0.0-rc.11

### Patch Changes

-   Updated dependencies \[]:
    -   @connectum/core@1.0.0-rc.11

## @connectum/cli@1.0.0-rc.11



## @connectum/core@1.0.0-rc.11

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
intech added a commit to Connectum-Framework/protobuf-es that referenced this pull request Apr 19, 2026
Adds toBinaryFast() — opt-in fast path using two-pass size estimation
and a pre-allocated buffer for write. Ports the pattern from
open-telemetry/opentelemetry-js#6390 (ProtobufLogsSerializer) to the
protobuf-es reflective encode.

Motivation
----------
The existing toBinary uses BinaryWriter with fork/join per length-
delimited field — every nested message and every packed repeated
scalar pushes chunk/buf state onto a stack, serializes into its own
chunk list, then re-emits a varint length prefix and concatenates.
On OTel-shaped workloads (ResourceSpans -> ScopeSpans -> Span ->
KeyValue) that produces many small Uint8Array/number[] allocations
and a final double-copy in finish(). The two-pass variant walks the
descriptor once to compute the exact encoded size, allocates a
single Uint8Array of that size, then writes bytes into it at fixed
offsets. Length prefixes computed in pass 1 are cached per submessage
object and reused in pass 2, so pass 2 is a straight-line write loop.

Results (Node 25.8.1, x86_64, tinybench, OTel-like 100-span payload)
--------------------------------------------------------------------

create() + toBinary() combined workload:
  create + toBinary       353 ops/s  baseline
  create + toBinaryFast  1758 ops/s  +397% (4.98x)

toBinary() on pre-built message:
  toBinary                385 ops/s  baseline
  toBinaryFast           2417 ops/s  +528% (6.28x)

Cross-library (vs protobufjs pbjs static-module):
  protobuf-es toBinary pre-built      428 ops/s
  protobuf-es toBinaryFast pre-built 3868 ops/s
  protobufjs encode pre-built        3259 ops/s
  -> toBinaryFast beats protobufjs by +19% on encode path.

Memory (1000 iters, forced GC, heapUsed delta):
  protobuf-es create+toBinary      10,211 B/op
  protobuf-es create+toBinaryFast   4,670 B/op   -54%
  protobufjs  create+encode         7,450 B/op
  -> toBinaryFast now uses less heap than protobufjs.

Scope (MVP)
-----------
Supported: all 15 scalar types, enum, repeated scalar (packed and
unpacked), nested messages, repeated messages. Correctness verified
with semantic round-trip (decode(toBinaryFast) structurally-equal to
decode(toBinary)) on the OTel ExportTraceRequest fixture and on
SimpleMessage; both fixtures in fact produce byte-identical output
in the current code path.

Fallback: schemas using maps, oneofs, extensions, or delimited/group
encoding fall back to toBinary. The decision is cached per
DescMessage in a WeakMap, so the support check does not dominate the
hot path after the first call.

Unknown fields are dropped by the fast path. Callers that must
round-trip unknown fields should continue to use toBinary.

Testing
-------
- Existing protobuf-test suite: 2823/2823 passing.
- Correctness verification: benchmarks/src/verify-correctness.ts
  exercises ExportTraceRequest and SimpleMessage fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants