diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index a415796a..8cb9aa16 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -23,8 +23,10 @@ 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:opentelemetry-sdk-extension-incubator") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") compileOnly(libs.bundles.semconv) @@ -54,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/ElasticDistroResource.java b/custom/src/main/java/co/elastic/otel/ElasticDistroResource.java new file mode 100644 index 00000000..16a6a2b4 --- /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 ae44183a..71911963 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/ElasticDeclarativeConfigurationCustomizer.java b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDeclarativeConfigurationCustomizer.java new file mode 100644 index 00000000..1da64b0a --- /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 new file mode 100644 index 00000000..70f74846 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/declarativeconfig/ElasticDistroComponentProvider.java @@ -0,0 +1,50 @@ +/* + * 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 { + + static final String NAME = "elastic_distribution"; + + @Override + public Class getType() { + return Resource.class; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Object create(DeclarativeConfigProperties config) { + return ElasticDistroResource.get(); + } +} 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 00000000..ddba54ce --- /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) {} + } +}