From 2eea9c62e12a3b5b935977186d293406aa6e26d9 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:22:35 +0200 Subject: [PATCH 1/2] add distro resource attributes for dc --- custom/build.gradle.kts | 1 + .../elastic/otel/ElasticDistroResource.java | 48 +++++++++++++++++++ .../otel/ElasticDistroResourceProvider.java | 24 ++-------- .../ElasticDistroComponentProvider.java | 48 +++++++++++++++++++ 4 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 custom/src/main/java/co/elastic/otel/ElasticDistroResource.java create mode 100644 custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index a415796a8..8f892a96e 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { } compileOnly("io.opentelemetry:opentelemetry-sdk") + compileOnly("io.opentelemetry:opentelemetry-api-incubator") compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") diff --git a/custom/src/main/java/co/elastic/otel/ElasticDistroResource.java b/custom/src/main/java/co/elastic/otel/ElasticDistroResource.java new file mode 100644 index 000000000..16a6a2b41 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/ElasticDistroResource.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.javaagent.tooling.AgentVersion; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.incubating.TelemetryIncubatingAttributes; + +public class ElasticDistroResource { + + private ElasticDistroResource() {} + + public static Resource get() { + if (AgentVersion.VERSION == null) { + return Resource.empty(); + } + try { + Class.forName("co.elastic.otel.agent.ElasticAgent"); + } catch (ClassNotFoundException e) { + // this means that we are running as an extension of the vanilla agent + // and not as distro. + return Resource.empty(); + } + return Resource.create( + Attributes.of( + TelemetryIncubatingAttributes.TELEMETRY_DISTRO_NAME, + "elastic", + TelemetryIncubatingAttributes.TELEMETRY_DISTRO_VERSION, + AgentVersion.VERSION)); + } +} diff --git a/custom/src/main/java/co/elastic/otel/ElasticDistroResourceProvider.java b/custom/src/main/java/co/elastic/otel/ElasticDistroResourceProvider.java index ae44183a0..719119638 100644 --- a/custom/src/main/java/co/elastic/otel/ElasticDistroResourceProvider.java +++ b/custom/src/main/java/co/elastic/otel/ElasticDistroResourceProvider.java @@ -19,33 +19,19 @@ package co.elastic.otel; import com.google.auto.service.AutoService; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.javaagent.tooling.AgentVersion; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.incubating.TelemetryIncubatingAttributes; +/** + * Provides {@code telemetry.distro.name} and {@code telemetry.distro.version} resource attributes + * for automatic configuration + */ @AutoService(ResourceProvider.class) public class ElasticDistroResourceProvider implements ResourceProvider { @Override public Resource createResource(ConfigProperties configProperties) { - if (AgentVersion.VERSION == null) { - return Resource.empty(); - } - try { - Class.forName("co.elastic.otel.agent.ElasticAgent"); - } catch (ClassNotFoundException e) { - // this means that we are running as an extension of the vanilla agent - // and not as distro. - return Resource.empty(); - } - return Resource.create( - Attributes.of( - TelemetryIncubatingAttributes.TELEMETRY_DISTRO_NAME, - "elastic", - TelemetryIncubatingAttributes.TELEMETRY_DISTRO_VERSION, - AgentVersion.VERSION)); + return ElasticDistroResource.get(); } } diff --git a/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java new file mode 100644 index 000000000..1f2d5d03c --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.declarativeconfig; + +import co.elastic.otel.ElasticDistroResource; +import com.google.auto.service.AutoService; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** + * Provides {@code telemetry.distro.name} and {@code telemetry.distro.version} resource attributes + * for declarative configuration + */ +@AutoService(ComponentProvider.class) +public class ElasticDistroComponentProvider implements ComponentProvider { + + @Override + public Class getType() { + return Resource.class; + } + + @Override + public String getName() { + return "elastic_opentelemetry_javaagent_distribution"; + } + + @Override + public Object create(DeclarativeConfigProperties config) { + return ElasticDistroResource.get(); + } +} From 9760e374a2ee0a1a7327e3d4c2a9c7b8df0ffd4c Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:33:09 +0200 Subject: [PATCH 2/2] add some tests for distro resource --- custom/build.gradle.kts | 3 + ...ticDeclarativeConfigurationCustomizer.java | 81 ++++++++++++ .../ElasticDistroComponentProvider.java | 4 +- ...eclarativeConfigurationCustomizerTest.java | 122 ++++++++++++++++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizer.java create mode 100644 custom/src/test/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizerTest.java diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index 8f892a96e..8cb9aa167 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { compileOnly("io.opentelemetry:opentelemetry-api-incubator") compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") compileOnly(libs.bundles.semconv) @@ -55,8 +56,10 @@ dependencies { // test dependencies testImplementation(project(":testing-common")) testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation("io.opentelemetry:opentelemetry-api-incubator") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("io.opentelemetry:opentelemetry-exporter-otlp") testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") { //The following dependency isn't actually needed, but breaks the classpath when testing with Java 8 diff --git a/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizer.java b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizer.java new file mode 100644 index 000000000..1da64b0a7 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizer.java @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.declarativeconfig; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalResourceDetectionModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalResourceDetectorModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ResourceModel; +import java.util.List; +import java.util.Set; + +@AutoService(DeclarativeConfigurationCustomizerProvider.class) +public class ElasticDeclarativeConfigurationCustomizer + implements DeclarativeConfigurationCustomizerProvider { + + @Override + public void customize(DeclarativeConfigurationCustomizer customizer) { + customizer.addModelCustomizer( + model -> { + customizeResources(model); + return model; + }); + } + + private static void customizeResources(OpenTelemetryConfigurationModel model) { + // this is equivalent to adding the following explicitly in declarative configuration + // + // detection/development: + // detectors: + // - <... other detectors ...> + // - elastic_distribution: + + ResourceModel resource = model.getResource(); + if (resource == null) { + resource = new ResourceModel(); + model.withResource(resource); + } + + ExperimentalResourceDetectionModel detectionDevelopment = resource.getDetectionDevelopment(); + if (null == detectionDevelopment) { + detectionDevelopment = new ExperimentalResourceDetectionModel(); + resource.withDetectionDevelopment(detectionDevelopment); + } + List detectors = + requireNonNull(detectionDevelopment.getDetectors()); + + Set names = + detectors.stream() + .flatMap(detector -> detector.getAdditionalProperties().keySet().stream()) + .collect(toSet()); + + // add at the end to make it have priority over upstream distro provider (which is added 1st) + if (!names.contains(ElasticDistroComponentProvider.NAME)) { + ExperimentalResourceDetectorModel detector = new ExperimentalResourceDetectorModel(); + detector.getAdditionalProperties().put(ElasticDistroComponentProvider.NAME, null); + detectors.add(detector); + } + } +} diff --git a/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java index 1f2d5d03c..70f74846f 100644 --- a/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java +++ b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java @@ -31,6 +31,8 @@ @AutoService(ComponentProvider.class) public class ElasticDistroComponentProvider implements ComponentProvider { + static final String NAME = "elastic_distribution"; + @Override public Class getType() { return Resource.class; @@ -38,7 +40,7 @@ public Class getType() { @Override public String getName() { - return "elastic_opentelemetry_javaagent_distribution"; + return NAME; } @Override diff --git a/custom/src/test/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizerTest.java b/custom/src/test/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizerTest.java new file mode 100644 index 000000000..ddba54ce8 --- /dev/null +++ b/custom/src/test/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizerTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.declarativeconfig; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.javaagent.tooling.resources.ResourceCustomizerProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ElasticDeclarativeConfigurationCustomizerTest { + + // because declarative config relies on json mapping annotations, we can leverage this for testing + // the configuration customization. + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void defaultConfig() { + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + model = applyConfigCustomize(model, new ElasticDeclarativeConfigurationCustomizer()); + + // ensures that we add our resource detector even if the model does not provide any + checkJson( + model.getResource(), + "{\"attributes\":[],\"detection/development\":{\"detectors\":[{\"elastic_distribution\":null}]}}"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void upstreamProvider(boolean elasticFirst) { + // upstream provider is always added first in the list, even if we add ours first + // this ordering behavior is implemented in upstream provider + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + DeclarativeConfigurationCustomizerProvider first; + DeclarativeConfigurationCustomizerProvider second; + if (elasticFirst) { + first = new ElasticDeclarativeConfigurationCustomizer(); + second = new ResourceCustomizerProvider(); + } else { + first = new ElasticDeclarativeConfigurationCustomizer(); + second = new ResourceCustomizerProvider(); + } + + model = applyConfigCustomize(model, first); + model = applyConfigCustomize(model, second); + checkJson( + model.getResource(), + "{\"attributes\":[],\"detection/development\":{\"detectors\":[{\"opentelemetry_javaagent_distribution\":null},{\"elastic_distribution\":null}]}}"); + } + + private void checkJson(Object o, String expected) { + try { + assertThat(objectMapper.writeValueAsString(o)).isEqualTo(expected); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + private OpenTelemetryConfigurationModel applyConfigCustomize( + OpenTelemetryConfigurationModel originalModel, + DeclarativeConfigurationCustomizerProvider customizerProvider) { + AtomicReference resultModel = new AtomicReference<>(); + customizerProvider.customize( + new TestCustomizer() { + @Override + public void addModelCustomizer( + Function + customizer) { + resultModel.set(customizer.apply(originalModel)); + } + }); + return resultModel.get(); + } + + private static class TestCustomizer implements DeclarativeConfigurationCustomizer { + + @Override + public void addModelCustomizer( + Function customizer) {} + + @Override + public void addSpanExporterCustomizer( + Class exporterType, BiFunction customizer) {} + + @Override + public void addMetricExporterCustomizer( + Class exporterType, BiFunction customizer) {} + + @Override + public void addLogRecordExporterCustomizer( + Class exporterType, BiFunction customizer) {} + } +}