Skip to content

[otlptrace] add SyncExporter #8860

Description

@etilite

Problem Statement

The current Exporter/Client contract does not guarantee that the telemetry passed to Client.UploadTraces is fully consumed when the method returns. A client is allowed to retain the request or process it asynchronously.

This prevents exporters from safely reusing the memory backing the protobuf request after UploadTraces returns.

This is particularly relevant for arena-backed protobuf serialization. An exporter could significantly reduce allocations by reusing an arena between exports, but resetting or reusing the arena while a client still references the request can corrupt data.

PR #8425 demonstrated this issue: the arena-based implementation provided substantial allocation and CPU improvements, but reusing the arena was incompatible with the existing Client ownership contract.

The problem is therefore not specific to arenas. It is the absence of an explicit API contract that tells an exporter when ownership of the serialized request can be safely reclaimed.

Proposed Solution

Introduce a SyncExporter together with a SyncClient interface that explicitly provides a synchronous upload operation.

SyncClient extends the existing Client interface and adds UploadTracesSync:

type SyncClient interface {
    Client

    UploadTracesSync(ctx context.Context, td ptrace.Traces) error
}

The new exporter would be constructed with a SyncClient:

func NewSync(
    ctx context.Context,
    client SyncClient,
    opts ...Option,
) (*SyncExporter, error)

SyncExporter would use UploadTracesSync exclusively.

The contract of UploadTracesSync is that the client MUST fully consume the supplied telemetry before returning and MUST NOT retain, reference, mutate, or asynchronously process it after the method returns.

This provides the exporter with an explicit lifetime boundary:

SyncExporter
    |
    | create arena-backed request
    v
UploadTracesSync
    |
    | fully consume request
    v
UploadTracesSync returns
    |
    | request can no longer be accessed
    v
arena can be reset/reused

This allows SyncExporter to use arena-backed protobuf serialization and safely reuse the arena across exports.

The arena remains an implementation detail of the exporter. No arena-backed object should escape the synchronous export operation.

NewSync should also use the existing Option mechanism to allow configuration of serialization and arena behavior. Potential options include:

  • initial arena/batch size;
  • expected attributes per span;
  • expected bytes per span or attribute;
  • arena growth thresholds;
  • maximum arena size eligible for reuse;
  • other workload-specific pre-allocation and serialization parameters.

The exact options can be determined during implementation. The goal is to allow workloads with predictable telemetry characteristics to tune pre-allocation without exposing arena ownership or lifecycle details.

The existing upstream HTTP and gRPC clients should be able to implement SyncClient by adding UploadTracesSync. They can reuse their existing transport and serialization logic while providing the stronger ownership guarantee required by SyncExporter.

This does not change the existing Client contract. A client that only implements Client continues to have the existing semantics and can be used by the existing exporter. A client must explicitly implement UploadTracesSync to be used with SyncExporter.

Arena pooling should also have a bounded retention policy. Large arenas should not be retained indefinitely merely because they were used for a large export.

Alternatives

Reuse the existing UploadTraces method

SyncExporter could continue using Client.UploadTraces and rely on documentation stating that a particular client behaves synchronously.

This is not sufficient because the existing Client contract permits clients to retain the request after UploadTraces returns. The exporter therefore has no API-level guarantee that the memory can be reused safely.

Detect synchronous clients through type assertions

The exporter could detect whether a client implements some synchronous behavior and select the arena-reuse path dynamically.

This makes the safety of memory reuse depend on implementation details and would make the ownership contract less explicit. A dedicated UploadTracesSync operation makes the stronger guarantee part of the API instead.

Allocate a new arena for every export

The exporter can avoid unsafe reuse by allocating a fresh arena for every export and allowing it to be reclaimed after the client is finished with the request.

This is compatible with the existing Client contract and can still reduce some allocations, but it cannot safely reuse the arena when the client processes requests asynchronously. The proposed synchronous API provides the stronger contract needed for full arena reuse.

Prior Art

The proposal follows a common API design pattern of providing a stronger operation with an explicit lifetime/ownership contract rather than changing the semantics of an existing general-purpose interface.

Within OpenTelemetry Go, the existing Client interface already separates the exporter from the underlying transport. SyncClient extends this model rather than introducing a separate transport abstraction.

The existing upstream OTLP HTTP and gRPC clients are also prior art for the implementation: both already perform the relevant request processing through their transport clients and should be able to expose the synchronous operation without requiring separate HTTP or gRPC client implementations.

PR #8425 is the immediate prior art within OpenTelemetry Go. It demonstrated both the performance opportunity from arena-backed serialization and the correctness problem caused by reusing arena memory under the existing Client contract.

Additional Context

The main distinction introduced by this proposal is an explicit ownership boundary:

  • Client.UploadTraces retains its existing semantics and may be asynchronous.
  • SyncClient.UploadTracesSync guarantees that the supplied telemetry is no longer accessed after the method returns.
  • SyncExporter can therefore safely reclaim and reuse memory immediately after UploadTracesSync returns.

The proposal intentionally keeps the synchronous behavior opt-in. Existing exporters and clients do not need to change their semantics.

The expected implementation path for upstream OTLP clients is straightforward: add UploadTracesSync to the existing HTTP and gRPC clients, reusing their current transport implementation while guaranteeing that the supplied protobuf request is consumed before the method returns.

The arena implementation should be kept internal and should avoid unbounded memory retention. Configuration through NewSync options can allow tuning of arena sizing and pre-allocation for workloads where telemetry shape is known in advance.

The goal is to provide a well-defined API contract that enables the allocation and CPU optimizations explored in PR #8425 without weakening the correctness guarantees of the existing exporter API.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions