Skip to content

HttpExporterBuilder.copy() loses original ClassLoader breaking Java agent #7568

@chengchen

Description

@chengchen

This relates to this original issue, and I also posted there to get more attention: open-telemetry/opentelemetry-java-instrumentation#14125

Describe the bug
When extending OtlpMetricExporterProvider to dynamically inject authentication headers, calling OtlpHttpMetricExporter.toBuilder() causes a classloader mismatch.
The HttpExporterBuilder.copy() method used internally does not retain the original MultipleParentClassLoader from the component loader and instead uses AgentClassLoader.
This results in the new AgentClassLoader being unable to find the JDK sender, breaking metric exporting (in our case we have to use the JDK sender instead of okhttp).

Steps to reproduce

  1. Create a custom exporter provider by extending OtlpMetricExporterProvider like this:
public class AuthenticatedOtlpMetricExporterProvider extends OtlpMetricExporterProvider {
    @Override
    public MetricExporter createExporter(ConfigProperties config) {
        ClientCredentialsAuthenticator auth = ...

        MetricExporter metricExporter = super.createExporter(config);

        if (metricExporter instanceof OtlpHttpMetricExporter) {
            OtlpHttpMetricExporter.Builder otlpHttpBuilder =
                ((OtlpHttpMetricExporter) metricExporter).toBuilder();
            otlpHttpBuilder.setHeaders(auth::getHeaders);
            return otlpHttpBuilder.build();

        } else if (metricExporter instanceof OtlpGrpcMetricExporter) {
            OtlpGrpcMetricExporter.Builder otlpGrpcBuilder =
                ((OtlpGrpcMetricExporter) metricExporter).toBuilder();
            otlpGrpcBuilder.setHeaders(auth::getHeaders);
            return otlpGrpcBuilder.build();

        } else {
            throw new ConfigurationException("Unsupported OTLP metrics exporter: " +
                metricExporter.getClass().getName());
        }
    }

    @Override
    public String getName() {
        return "otlpauth";
    }
}

What did you expect to see?
Calling .toBuilder() on an exporter should produce a builder instance that retains the original component / class loader, allowing the new exporter instance to function correctly.

What did you see instead?
.toBuilder() calls HttpExporterBuilder.copy() which instantiates a new builder without preserving the original component loader. This results in the use of AgentClassLoader and breaks class loading, making the JDK sender unavailable.

What version and what artifacts are you using?
1.52.0 where ComponentLoader was introduced by @jack-berg in #7446

Environment
(not relevant)
Compiler: Temurin 17.0.7
OS: Ubuntu 24.04
Runtime: OpenJDK 17
OS (runtime): Ubuntu 24.04

Metadata

Metadata

Assignees

Labels

BugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions