Skip to content

Conversation

schleyfox
Copy link

Which problem is this PR solving?

Allows a different Tracer/Span implementation to be used by the TracerProvider and the SDKs in cases where behavior needs to be customized by passing a tracerFactory option to NodeSDK and BasicTracerProvider. This allows customizing this one component of otel without having to write my own SDK and BasicTracerProvider and support code.

I observed significant increases in CPU usage from garbage collection/memory pressure in an application when increasing the percentage of traces that were recorded (even if not sampled). Doing some memory benchmarking/profiling, it turns out that creating a single 20 attribute span (200B data) allocates 4.8KiB across 111 objects while a NonRecordingSpan takes 1.4 KiB across 40 objects. With optimization, I was able to reduce this to 650B across 18 objects per Span. Unfortunately, these optimizations are unlikely to be broadly applicable or acceptable to this project as the bulk of the improvement came from removing attribute sanitization (previously identified in #4558), runtime type checking of attributes, and the enforcement of limits on attribute count or size (and the other optimizations are objectively quite ugly). These will have to stay as custom implementations of Tracer/Span.

There was no way for me to inject a custom Tracer implementation without fully reimplementing NodeSDK, so this adds an optional way to pass a tracerFactory through the sdk and the tracer provider like:

const sdk = new NodeSDK({
  tracerFactory: lightweightTracerFactory,
  ...
})

You can see the intended usage (and full benchmarks/code) in https://github.com/schleyfox/otel-js-lightweight-tracer

Short description of the changes

Adds an optional tracerFactory function to TracerConfig that is used by BasicTracerProvider to construct a Tracer.

Notes

From a naming perspective, it seems silly to have a tracerFactory that gets passed to a tracerProvider, ideally, you'd just inject a custom tracerProvider like you would a sampler or traceExporter, but that's awkward because constructing the tracerProvider depends on the resource and spanProcessors that are created when the sdk is started.

Code ref
  public start(): void {
  ...

    if (this._autoDetectResources) {
      const internalConfig: ResourceDetectionConfig = {
        detectors: this._resourceDetectors,
      };

      this._resource = this._resource.merge(detectResources(internalConfig));
    }

    this._resource =
      this._serviceName === undefined
        ? this._resource
        : this._resource.merge(
            resourceFromAttributes({
              [ATTR_SERVICE_NAME]: this._serviceName,
            })
          );

    const spanProcessors = this._tracerProviderConfig
      ? this._tracerProviderConfig.spanProcessors
      : getSpanProcessorsFromEnv();

    this._tracerProvider = new NodeTracerProvider({
      ...this._configuration,
      resource: this._resource,
      spanProcessors,
    }); 

The TracerProvider also handles not just the creation of Tracers, but also their caching/management. Logic that I had no desire to reimplement (subclassing BasicTracingProvider would work, but also seems misaligned with the direction in #5283)

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Checklist:

  • Followed the style guidelines of this project
  • Unit tests have been added
  • Documentation has been updated

Allows a different Tracer/Span implementation to be used by the
TracerProvider and the SDKs in cases where behavior needs to be
customized.
@schleyfox schleyfox requested a review from a team as a code owner August 14, 2025 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant