From 5ba137ca5f10ca8ea369eb09b54d5600d18bfd0a Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 12 Jan 2023 12:59:23 +0100 Subject: [PATCH 1/9] Added OpenTelemetry Metrics SDK instrumentation. --- .../configuration/MetricsConfiguration.java | 23 + .../testutils/DisabledIfNotOnClasspath.java | 40 ++ .../DisabledIfNotOnClasspathCondition.java | 55 ++ .../testutils/assertions/Assertions.java | 7 + .../assertions/metrics/MetricJson.java | 47 ++ .../assertions/metrics/MetricsetAssert.java | 131 ++++ .../metrics/MetricsetEventJson.java | 24 + .../assertions/metrics/MetricsetJson.java | 41 ++ .../assertions/metrics/MetricsetsAssert.java | 114 ++++ .../pom.xml | 29 + .../ElasticOtelMetricsExporter.java | 112 +++ .../otelmetricexport/MetricSetGenerator.java | 288 ++++++++ .../OtelMetricSerializer.java | 218 ++++++ .../agent/otelmetricexport/package-info.java | 22 + .../pom.xml | 47 ++ .../OtelMetricsSdkRootPackagesCustomizer.java | 34 + ...dkMeterProviderBuilderInstrumentation.java | 96 +++ .../apm/agent/otelmetricsdk/package-info.java | 22 + ...bci.PluginClassLoaderRootPackageCustomizer | 1 + ...ic.apm.agent.sdk.ElasticApmInstrumentation | 1 + .../AbstractOtelMetricsTest.java | 641 ++++++++++++++++++ .../otelmetricsdk/DummyMetricReader.java | 47 ++ .../otelmetricsdk/MetricExportTimingTest.java | 36 + .../PrivateUserSdkOtelMetricsTest.java | 126 ++++ .../UnsupportedSdkVersionIgnoredTest.java | 47 ++ .../apm-opentelemetry-plugin/pom.xml | 13 +- .../apm-opentelemetry-test/pom.xml | 27 + .../opentelemetry/OpenTelemetryVersionIT.java | 114 ++++ .../opentelemetry/OpenTelemetryVersionIT.java | 71 -- apm-agent-plugins/apm-opentelemetry/pom.xml | 8 +- apm-agent/pom.xml | 5 + 31 files changed, 2405 insertions(+), 82 deletions(-) create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspath.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspathCondition.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricJson.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/pom.xml create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/ElasticOtelMetricsExporter.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/package-info.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/pom.xml create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/OtelMetricsSdkRootPackagesCustomizer.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/package-info.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/DummyMetricReader.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/PrivateUserSdkOtelMetricsTest.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/UnsupportedSdkVersionIgnoredTest.java create mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/agent/opentelemetry/OpenTelemetryVersionIT.java delete mode 100644 apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/opentelemetry/OpenTelemetryVersionIT.java diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java index 56dbb74b6f..3db93a500c 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java @@ -18,8 +18,13 @@ */ package co.elastic.apm.agent.configuration; +import co.elastic.apm.agent.configuration.converter.ListValueConverter; import org.stagemonitor.configuration.ConfigurationOption; import org.stagemonitor.configuration.ConfigurationOptionProvider; +import org.stagemonitor.configuration.converter.DoubleValueConverter; + +import java.util.Arrays; +import java.util.List; public class MetricsConfiguration extends ConfigurationOptionProvider { @@ -37,6 +42,20 @@ public class MetricsConfiguration extends ConfigurationOptionProvider { .tags("added[1.22.0]") .buildWithDefault(true); + private final ConfigurationOption> customMetricsHistogramBoundaries = ConfigurationOption.builder(new ListValueConverter<>(DoubleValueConverter.INSTANCE), List.class) + .key("custom_metrics_histogram_boundaries") + .configurationCategory(METRICS_CATEGORY) + .description("TODO") + .dynamic(true) + .tags("added[1.36.0]") + .buildWithDefault(Arrays.asList( + 0.00390625, 0.00552427, 0.0078125, 0.0110485, 0.015625, 0.0220971, 0.03125, 0.0441942, + 0.0625, 0.0883883, 0.125, 0.176777, 0.25, 0.353553, 0.5, 0.707107, 1.0, 1.41421, 2.0, + 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0, 45.2548, 64.0, 90.5097, 128.0, + 181.019, 256.0, 362.039, 512.0, 724.077, 1024.0, 1448.15, 2048.0, 2896.31, 4096.0, 5792.62, + 8192.0, 11585.2, 16384.0, 23170.5, 32768.0, 46341.0, 65536.0, 92681.9, 131072.0 + )); + private final ConfigurationOption metricSetLimit = ConfigurationOption.integerOption() .key("metric_set_limit") .configurationCategory(METRICS_CATEGORY) @@ -81,4 +100,8 @@ public boolean isReporterHealthMetricsEnabled() { public boolean isOverheadMetricsEnabled() { return overheadMetricsEnabled.get(); } + + public List getCustomMetricsHistogramBoundaries() { + return customMetricsHistogramBoundaries.get(); + } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspath.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspath.java new file mode 100644 index 0000000000..cecbdc2d2b --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspath.java @@ -0,0 +1,40 @@ +/* + * 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.apm.agent.testutils; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfNotOnClasspathCondition.class) +public @interface DisabledIfNotOnClasspath { + + /** + * The full qualified names of classes. + * If any class cannot be loaded by the test, it will not be executed. + */ + String[] value(); +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspathCondition.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspathCondition.java new file mode 100644 index 0000000000..83fb76705a --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/DisabledIfNotOnClasspathCondition.java @@ -0,0 +1,55 @@ +/* + * 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.apm.agent.testutils; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; + +public class DisabledIfNotOnClasspathCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + return findAnnotation(element, DisabledIfNotOnClasspath.class) + .map(annotation -> Arrays.stream(annotation.value()) + .filter(className -> !canLoadClass(context, className)) + .findFirst() + .map(className -> disabled(element + " is @DisableIfNotOnClasspath", className)) + .orElseGet(() -> enabled("All required classes found"))) + .orElse(enabled("@DisableIfNotOnClasspath is not present")); + } + + private static boolean canLoadClass(ExtensionContext context, String className) { + ClassLoader classLoader = context.getRequiredTestClass().getClassLoader(); + try { + Class.forName(className, false, classLoader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java index 3e5bdd8b89..7bac1ed940 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java @@ -23,6 +23,9 @@ import co.elastic.apm.agent.impl.context.ServiceTarget; import co.elastic.apm.agent.impl.transaction.AbstractSpan; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.testutils.assertions.metrics.MetricsetsAssert; + +import java.util.Collection; public class Assertions extends org.assertj.core.api.Assertions { @@ -48,4 +51,8 @@ public static DbAssert assertThat(Db db) { public static AbstractSpanAssert assertThat(AbstractSpan span) { return new AbstractSpanAssert<>(span, AbstractSpanAssert.class); } + + public static MetricsetsAssert assertThatMetricSets(Collection metricsetsJson) { + return new MetricsetsAssert(metricsetsJson); + } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricJson.java new file mode 100644 index 0000000000..5488b901e8 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricJson.java @@ -0,0 +1,47 @@ +/* + * 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.apm.agent.testutils.assertions.metrics; + +import javax.annotation.Nullable; +import java.util.List; + +public class MetricJson { + + @Nullable + public Number value; + + @Nullable + public List values; + + @Nullable + public List counts; + + @Nullable + public String type; + + @Override + public String toString() { + return "MetricJson{" + + "value=" + value + + ", values=" + values + + ", counts=" + counts + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java new file mode 100644 index 0000000000..e93991ba81 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java @@ -0,0 +1,131 @@ +/* + * 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.apm.agent.testutils.assertions.metrics; + +import co.elastic.apm.agent.testutils.assertions.BaseAssert; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MetricsetAssert extends BaseAssert { + + protected MetricsetAssert(MetricsetJson metricSet) { + super(metricSet, MetricsetAssert.class); + } + + public MetricsetAssert hasMetricsCount(int expected) { + int count = Optional.ofNullable(actual.samples).map(Map::size).orElse(0); + if (count != expected) { + failWithMessage("Expected metricset to contain %d metrics but contained %d.", expected, count); + } + return this; + } + + public MetricsetAssert containsMetric(String name) { + extractMetric(name); + return this; + } + + public MetricsetAssert metricSatisfies(String name, Consumer assertions) { + assertions.accept(extractMetric(name)); + return this; + } + + public MetricsetAssert containsValueMetric(String name, Number expected) { + MetricJson metric = extractMetric(name); + if (!Objects.equals(metric.value, expected)) { + failWithMessage("Expected metric '%s' to have metric value '%s' but was '%s'", name, expected, metric.value); + } + if (!Objects.equals(metric.values, null)) { + failWithMessage("Expected metric '%s' to have metric value but did contain histogram values %s", name, metric.values); + } + if (!Objects.equals(metric.counts, null)) { + failWithMessage("Expected metric '%s' to have metric value but did contain histogram counts %s", name, metric.counts); + } + return this; + } + + public MetricsetAssert containsHistogramMetric(String name, Collection expectedValues, Collection expectedCounts) { + MetricJson metric = extractMetric(name); + if (!Objects.equals(metric.values, expectedValues)) { + failWithMessage("Expected metric '%s' to have histogram values '%s' but was '%s'", name, expectedValues, metric.values); + } + if (!Objects.equals(metric.counts, expectedCounts)) { + failWithMessage("Expected metric '%s' to have histogram counts '%s' but was '%s'", name, expectedCounts, metric.counts); + } + if (!Objects.equals(metric.type, "histogram")) { + failWithMessage("Expected metric '%s' to have a histogram type but actual type was %s", name, metric.type); + } + if (!Objects.equals(metric.value, null)) { + failWithMessage("Expected metric '%s' to have a histogram value but it did contain plain value %s", name, metric.value); + } + return this; + } + + public MetricsetAssert containsMetrics(String... names) { + for (String name : names) { + containsMetric(name); + } + return this; + } + + public MetricsetAssert containsExactlyMetrics(String... names) { + if (actual.samples == null) { + if (names.length > 0) { + failWithMessage("Expected metricset to contain metrics %s but 'samples' was null", Arrays.toString(names)); + } + } + for (String name : names) { + containsMetric(name); + } + if (names.length > actual.samples.size()) { + Set expectedNames = new HashSet<>(Arrays.asList(names)); + for (String name : actual.samples.keySet()) { + if (!expectedNames.contains(name)) { + failWithMessage("Expected metricset to not contain metric '%s' but was found", name); + } + } + } + return this; + } + + private MetricJson extractMetric(String name) { + if (actual.samples == null) { + failWithMessage("Expected metric with name '%s' to exist but metricset 'samples' was null", name); + } + MetricJson metric = actual.samples.get(name); + if (metric == null) { + failWithMessage("Expected metric with name '%s' to exist but metricset only contains %s", name, actual.samples.keySet()); + } + return metric; + } + + public MetricsetAssert hasTimestampInRange(long min, long max) { + assertThat(actual.timestamp).isBetween(min, max); + return this; + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java new file mode 100644 index 0000000000..0616151d31 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java @@ -0,0 +1,24 @@ +/* + * 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.apm.agent.testutils.assertions.metrics; + +public class MetricsetEventJson { + + public MetricsetJson metricset; +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java new file mode 100644 index 0000000000..10a9274e6f --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java @@ -0,0 +1,41 @@ +/* + * 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.apm.agent.testutils.assertions.metrics; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MetricsetJson { + + @Nullable + public Map samples; + + public Map tags = new HashMap<>(); + + @Nullable + public Long timestamp; + + @JsonIgnore + public String json; +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java new file mode 100644 index 0000000000..33a3254b78 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java @@ -0,0 +1,114 @@ +/* + * 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.apm.agent.testutils.assertions.metrics; + +import co.elastic.apm.agent.testutils.assertions.BaseAssert; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MetricsetsAssert extends BaseAssert> { + + static final ObjectMapper jsonMapper = new ObjectMapper(); + + public MetricsetsAssert(Collection serializedMetricSets) { + this(deserializeMetricSets(serializedMetricSets)); + } + + private static List deserializeMetricSets(Collection serializedMetricSets) { + return serializedMetricSets.stream() + .map(bytes -> { + try { + MetricsetJson metricset = jsonMapper.readValue(bytes, MetricsetEventJson.class).metricset; + metricset.json = new String(bytes).replace("\n", ""); + return metricset; + } catch (IOException e) { + throw new RuntimeException("Failed to deserialize metricset", e); + } + }) + .collect(Collectors.toList()); + } + + protected MetricsetsAssert(List metricSets) { + super(metricSets, MetricsetsAssert.class); + } + + + public MetricsetsAssert hasMetricsetWithLabelsSatisfying(String key, Object value, Consumer assertions) { + return hasMetricsetWithLabelsSatisfying(Collections.singletonMap(key, value), assertions); + } + + public MetricsetsAssert hasMetricsetWithLabelsSatisfying(String key1, Object value1, String key2, Object value2, Consumer assertions) { + return hasMetricsetWithLabelsSatisfying(Map.of(key1, value1, key2, value2), assertions); + } + + public MetricsetAssert first() { + if (actual.isEmpty()) { + failWithMessage("Expected at least one metricset but none was found"); + } + return new MetricsetAssert(actual.get(0)); + } + + public MetricsetsAssert hasMetricsetWithLabelsSatisfying(Map labels, Consumer assertions) { + assertions.accept(new MetricsetAssert(extractMetricset(labels))); + return this; + } + + public MetricsetsAssert hasMetricsetCount(int expected) { + if (actual.size() != expected) { + failWithMessage("Excpected to contain %d metricsets but actually was %d. Contained metricsets:\n%s", expected, actual.size(), getAllMetricsetsAsString()); + } + return this; + } + + private MetricsetJson extractMetricset(Map labels) { + Optional ms = actual.stream().filter(ms2 -> Objects.equals(ms2.tags, labels)).findFirst(); + if (ms.isEmpty()) { + String allLabels = getAllLabels(); + failWithMessage("Expected metricset with labels %s to exist but was not found in %d metricsets. Found metricset labels are:\n%s", labels, actual.size(), allLabels); + } + return ms.get(); + } + + @NotNull + private String getAllLabels() { + return actual.stream() + .map(ms2 -> ms2.tags) + .map(Objects::toString) + .collect(Collectors.joining("\n")); + } + + + @NotNull + private String getAllMetricsetsAsString() { + return actual.stream() + .map(ms -> ms.json) + .collect(Collectors.joining("\n")); + } + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/pom.xml b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/pom.xml new file mode 100644 index 0000000000..2f1069ae63 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/pom.xml @@ -0,0 +1,29 @@ + + + + apm-opentelemetry + co.elastic.apm + 1.35.1-SNAPSHOT + + 4.0.0 + + apm-opentelemetry-metrics-exporter + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + + + + + + io.opentelemetry + opentelemetry-sdk-metrics + ${version.opentelemetry} + provided + + + + diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/ElasticOtelMetricsExporter.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/ElasticOtelMetricsExporter.java new file mode 100644 index 0000000000..a38b2f849f --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/ElasticOtelMetricsExporter.java @@ -0,0 +1,112 @@ +/* + * 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.apm.agent.otelmetricexport; + +import co.elastic.apm.agent.configuration.MetricsConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.report.Reporter; +import co.elastic.apm.agent.report.ReporterConfiguration; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.util.ExecutorUtils; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; + +import java.time.Duration; +import java.util.Collection; + +public class ElasticOtelMetricsExporter implements MetricExporter { + + private static final Logger logger = LoggerFactory.getLogger(ElasticOtelMetricsExporter.class); + + private static final AggregationTemporalitySelector TEMPORALITY_SELECTOR = AggregationTemporalitySelector.deltaPreferred(); + + private final Aggregation defaultHistogramAggregation; + + private final OtelMetricSerializer serializer; + + private final Reporter reporter; + + public static void createAndRegisterOn(SdkMeterProviderBuilder builder, ElasticApmTracer tracer) { + MetricsConfiguration metricsConfig = tracer.getConfig(MetricsConfiguration.class); + ReporterConfiguration reporterConfig = tracer.getConfig(ReporterConfiguration.class); + + ElasticOtelMetricsExporter exporter = new ElasticOtelMetricsExporter(tracer.getReporter(), metricsConfig, reporterConfig); + + PeriodicMetricReader metricReader = PeriodicMetricReader.builder(exporter) + .setExecutor(ExecutorUtils.createSingleThreadSchedulingDaemonPool("otel-metrics-exporter")) + .setInterval(Duration.ofMillis(reporterConfig.getMetricsIntervalMs())) + .build(); + + builder.registerMetricReader(metricReader); + } + + private ElasticOtelMetricsExporter(Reporter reporter, MetricsConfiguration metricsConfig, ReporterConfiguration reporterConfig) { + serializer = new OtelMetricSerializer(metricsConfig, reporterConfig); + this.reporter = reporter; + this.defaultHistogramAggregation = Aggregation.explicitBucketHistogram(metricsConfig.getCustomMetricsHistogramBoundaries()); + } + + + @Override + public CompletableResultCode export(Collection metrics) { + logger.debug("Metrics export called with {} metrics", metrics.size()); + + for (MetricData metric : metrics) { + serializer.addValues(metric); + } + + serializer.flushAndReset(reporter); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + logger.debug("Flush called"); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + logger.debug("Shutdown called"); + return CompletableResultCode.ofSuccess(); + } + + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return TEMPORALITY_SELECTOR.getAggregationTemporality(instrumentType); + } + + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + if (instrumentType == InstrumentType.HISTOGRAM) { + return defaultHistogramAggregation; + } else { + return Aggregation.defaultAggregation(); + } + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java new file mode 100644 index 0000000000..81c02aa325 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java @@ -0,0 +1,288 @@ +/* + * 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.apm.agent.otelmetricexport; + +import co.elastic.apm.agent.report.Reporter; +import co.elastic.apm.agent.report.serialize.DslJsonSerializer; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import com.dslplatform.json.BoolConverter; +import com.dslplatform.json.DslJson; +import com.dslplatform.json.JsonWriter; +import com.dslplatform.json.Nullable; +import com.dslplatform.json.NumberConverter; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.common.Attributes; + +import java.util.List; +import java.util.Map; + +import static com.dslplatform.json.JsonWriter.ARRAY_END; +import static com.dslplatform.json.JsonWriter.ARRAY_START; +import static com.dslplatform.json.JsonWriter.COMMA; +import static com.dslplatform.json.JsonWriter.OBJECT_END; +import static com.dslplatform.json.JsonWriter.OBJECT_START; + +class MetricSetGenerator { + + private static final Logger logger = LoggerFactory.getLogger(MetricSetGenerator.class); + + private static final byte NEW_LINE = '\n'; + + private static final int INITIAL_BUFFER_SIZE = 2048; + + private static final DslJson DSL_JSON = new DslJson<>(new DslJson.Settings<>()); + + private final StringBuilder replaceBuilder; + private final JsonWriter jw; + private boolean anySamplesWritten; + + public MetricSetGenerator(Attributes attributes, CharSequence instrumentationScopeName, long epochMicros, StringBuilder replaceBuilder) { + this.replaceBuilder = replaceBuilder; + anySamplesWritten = false; + jw = DSL_JSON.newWriter(INITIAL_BUFFER_SIZE); + jw.writeByte(JsonWriter.OBJECT_START); + { + DslJsonSerializer.writeFieldName("metricset", jw); + jw.writeByte(JsonWriter.OBJECT_START); + { + DslJsonSerializer.writeFieldName("timestamp", jw); + NumberConverter.serialize(epochMicros, jw); + jw.writeByte(JsonWriter.COMMA); + serializeAttributes(instrumentationScopeName, attributes); + DslJsonSerializer.writeFieldName("samples", jw); + jw.writeByte(JsonWriter.OBJECT_START); + } + } + } + + public void addValue(CharSequence metricName, double value) { + addValue(metricName, true, 0, value); + } + + public void addValue(CharSequence metricName, long value) { + addValue(metricName, false, value, 0.0); + } + + private void addValue(CharSequence metricName, boolean isDouble, long longVal, double doubleVal) { + if (anySamplesWritten) { + jw.writeByte(COMMA); + } + serializeFieldKey(metricName); + jw.writeByte(JsonWriter.OBJECT_START); + { + serializeFieldKeyAscii("value"); + if (isDouble) { + NumberConverter.serialize(doubleVal, jw); + } else { + NumberConverter.serialize(longVal, jw); + } + } + jw.writeByte(JsonWriter.OBJECT_END); + anySamplesWritten = true; + } + + + public void addExplicitBucketHistogram(CharSequence metricName, List boundaries, List counts) { + if (isEmptyHistogram(boundaries, counts)) { + return; + } + if (anySamplesWritten) { + jw.writeByte(COMMA); + } + serializeFieldKey(metricName); + jw.writeByte(JsonWriter.OBJECT_START); + { + serializeFieldKeyAscii("values"); + convertAndSerializeHistogramBucketBoundaries(boundaries, counts); + jw.writeByte(COMMA); + serializeFieldKeyAscii("counts"); + convertAndSerializeHistogramBucketCounts(counts); + jw.writeByte(COMMA); + jw.writeAscii("\"type\":\"histogram\""); + } + jw.writeByte(JsonWriter.OBJECT_END); + anySamplesWritten = true; + } + + private boolean isEmptyHistogram(List boundaries, List counts) { + for (long count : counts) { + if (count != 0) { + return false; + } + } + return true; + } + + private void convertAndSerializeHistogramBucketCounts(List counts) { + jw.writeByte(ARRAY_START); + boolean firstElement = true; + for (long count : counts) { + if (count != 0) { + if (!firstElement) { + jw.writeByte(COMMA); + } + firstElement = false; + NumberConverter.serialize(count, jw); + } + } + jw.writeByte(ARRAY_END); + } + + private void convertAndSerializeHistogramBucketBoundaries(List boundaries, List counts) { + jw.writeByte(ARRAY_START); + boolean firstElement = true; + //Bucket boundary conversion algorithm is copied from APM server + int bucketCount = counts.size(); + for (int i = 0; i < bucketCount; i++) { + if (counts.get(i) != 0) { + if (!firstElement) { + jw.writeByte(COMMA); + } + firstElement = false; + if (i == 0) { + double bounds = boundaries.get(0); + if (bounds > 0) { + bounds /= 2; + } + NumberConverter.serialize(bounds, jw); + } else if (i == bucketCount - 1) { + NumberConverter.serialize(boundaries.get(bucketCount - 2), jw); + } else { + double lower = boundaries.get(i - 1); + double upper = boundaries.get(i); + NumberConverter.serialize(lower + (upper - lower) / 2, jw); + } + } + } + jw.writeByte(ARRAY_END); + } + + private void serializeFieldKey(CharSequence fieldName) { + jw.writeString(fieldName); + jw.writeByte(JsonWriter.SEMI); + } + + private void serializeFieldKeyAscii(String fieldName) { + jw.writeByte(JsonWriter.QUOTE); + jw.writeAscii(fieldName); + jw.writeByte(JsonWriter.QUOTE); + jw.writeByte(JsonWriter.SEMI); + } + + private void serializeAttributes(CharSequence instrumentationScopeName, Attributes attributes) { + Map, Object> attributeMap = attributes.asMap(); + if (attributeMap.isEmpty() && instrumentationScopeName.length() == 0) { + return; + } + DslJsonSerializer.writeFieldName("tags", jw); + jw.writeByte(OBJECT_START); + boolean anyWritten = false; + if (instrumentationScopeName.length() > 0) { + jw.writeAscii("\"otel_instrumentation_scope_name\":"); + jw.writeString(instrumentationScopeName); + anyWritten = true; + } + for (Map.Entry, Object> entry : attributeMap.entrySet()) { + AttributeKey key = entry.getKey(); + Object value = entry.getValue(); + anyWritten |= serializeAttribute(key, value, anyWritten); + } + jw.writeByte(OBJECT_END); + jw.writeByte(COMMA); + } + + private boolean serializeAttribute(AttributeKey key, @Nullable Object value, boolean prependComma) { + if (isValidAttributeValue(key, value)) { + if (prependComma) { + jw.writeByte(COMMA); + } + DslJsonSerializer.writeStringValue(DslJsonSerializer.sanitizePropertyName(key.getKey(), replaceBuilder), replaceBuilder, jw); + jw.writeByte(JsonWriter.SEMI); + + AttributeType type = key.getType(); + switch (type) { + case STRING: + jw.writeString((CharSequence) value); + return true; + case BOOLEAN: + BoolConverter.serialize((Boolean) value, jw); + return true; + case LONG: + NumberConverter.serialize(((Number) value).longValue(), jw); + return true; + case DOUBLE: + NumberConverter.serialize(((Number) value).doubleValue(), jw); + return true; + case STRING_ARRAY: + case BOOLEAN_ARRAY: + case LONG_ARRAY: + case DOUBLE_ARRAY: + return false; //Array types are not supported yet + default: + throw new IllegalStateException("Unhandled enum value: " + type); + } + } + return false; + } + + private boolean isValidAttributeValue(AttributeKey key, @Nullable Object value) { + if (value == null) { + return false; + } + switch (key.getType()) { + case STRING: + return value instanceof CharSequence; + case BOOLEAN: + return value instanceof Boolean; + case LONG: + case DOUBLE: + return value instanceof Number; + case STRING_ARRAY: + case BOOLEAN_ARRAY: + case LONG_ARRAY: + case DOUBLE_ARRAY: + return false; //Array types are not supported at the moment + } + return false; + } + + private void serializeMetricSetEnd() { + { + /*"metricset":*/ + { + /*"samples":*/ + { + jw.writeByte(JsonWriter.OBJECT_END); + } + } + jw.writeByte(JsonWriter.OBJECT_END); + } + jw.writeByte(JsonWriter.OBJECT_END); + jw.writeByte(NEW_LINE); + } + + public void finishAndReport(Reporter reporter) { + if (anySamplesWritten) { + serializeMetricSetEnd(); + reporter.report(jw); + } + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java new file mode 100644 index 0000000000..91dab92129 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java @@ -0,0 +1,218 @@ +/* + * 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.apm.agent.otelmetricexport; + +import co.elastic.apm.agent.configuration.MetricsConfiguration; +import co.elastic.apm.agent.matcher.WildcardMatcher; +import co.elastic.apm.agent.report.Reporter; +import co.elastic.apm.agent.report.ReporterConfiguration; +import co.elastic.apm.agent.report.serialize.DslJsonSerializer; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.Data; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.HistogramData; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class OtelMetricSerializer { + + private static final Logger logger = LoggerFactory.getLogger(OtelMetricSerializer.class); + private final MetricsConfiguration metricsConfig; + private final ReporterConfiguration reporterConfig; + private final StringBuilder serializationTempBuilder; + private final StringBuilder metricNameBuilder; + + private final Set metricsWithBadAggregations = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private final Map> metricSets; + + @Nullable + private InstrumentationScopeAndTimestamp lastCreatedInstrScopeAndTimestamp; + + public OtelMetricSerializer(MetricsConfiguration metricsConfig, ReporterConfiguration reporterConfig) { + this.metricsConfig = metricsConfig; + this.reporterConfig = reporterConfig; + metricSets = new HashMap<>(); + serializationTempBuilder = new StringBuilder(); + metricNameBuilder = new StringBuilder(); + } + + public void addValues(MetricData metric) { + String orginalName = metric.getName(); + + CharSequence name; + boolean isEnabled; + if (metricsConfig.isDedotCustomMetrics()) { + name = DslJsonSerializer.sanitizePropertyName(orginalName, metricNameBuilder); + isEnabled = !isMetricDisabled(name) && !isMetricDisabled(orginalName); + } else { + name = orginalName; + isEnabled = !isMetricDisabled(name); + } + if (isEnabled) { + addMetricValues(name, metric); + } + } + + private boolean isMetricDisabled(CharSequence name) { + for (WildcardMatcher matcher : reporterConfig.getDisableMetrics()) { + if (matcher.matches(name)) { + return true; + } + } + return false; + } + + private void addMetricValues(CharSequence sanitizedName, MetricData metric) { + boolean isDelta; + String instrumentationScopeName = metric.getInstrumentationScopeInfo().getName(); + switch (metric.getType()) { + case LONG_GAUGE: + addLongValues(sanitizedName, instrumentationScopeName, metric.getLongGaugeData(), false); + break; + case DOUBLE_GAUGE: + addDoubleValues(sanitizedName, instrumentationScopeName, metric.getDoubleGaugeData(), false); + break; + case LONG_SUM: + isDelta = metric.getLongSumData().getAggregationTemporality().equals(AggregationTemporality.DELTA); + addLongValues(sanitizedName, instrumentationScopeName, metric.getLongSumData(), isDelta); + break; + case DOUBLE_SUM: + isDelta = metric.getDoubleSumData().getAggregationTemporality().equals(AggregationTemporality.DELTA); + addDoubleValues(sanitizedName, instrumentationScopeName, metric.getDoubleSumData(), isDelta); + break; + case HISTOGRAM: + addHistogramValues(sanitizedName, instrumentationScopeName, metric.getHistogramData()); + break; + case SUMMARY: + case EXPONENTIAL_HISTOGRAM: + default: + if (metricsWithBadAggregations.add(metric.getName())) { + logger.warn("Ignoring metric '%s' due to unsupported aggregation '%s'", metric.getName(), metric.getType()); + } + break; + } + } + + private void addHistogramValues(CharSequence name, CharSequence instrScopeName, HistogramData histogramData) { + for (HistogramPointData histo : histogramData.getPoints()) { + long timestampMicros = histo.getEpochNanos() / 1000L; + MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, histo.getAttributes()); + metricSet.addExplicitBucketHistogram(name, histo.getBoundaries(), histo.getCounts()); + } + } + + private void addDoubleValues(CharSequence name, CharSequence instrScopeName, Data metricValues, boolean omitZeroes) { + for (DoublePointData data : metricValues.getPoints()) { + long timestampMicros = data.getEpochNanos() / 1000L; + if (!omitZeroes || data.getValue() != 0) { + MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); + metricSet.addValue(name, data.getValue()); + } + } + } + + private void addLongValues(CharSequence name, CharSequence instrScopeName, Data metricValues, boolean omitZeroes) { + for (LongPointData data : metricValues.getPoints()) { + if (!omitZeroes || data.getValue() != 0) { + long timestampMicros = data.getEpochNanos() / 1000L; + MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); + metricSet.addValue(name, data.getValue()); + } + } + } + + + private MetricSetGenerator getOrCreateMetricSet(CharSequence instrScopeName, long timestampMicros, Attributes attributes) { + //This function is often called in a loop with the same instrumentation scope name and timestamp + //In order to minimize allocations, we make use of this fact and remember the map key from the last iteration and reuse it if possible + InstrumentationScopeAndTimestamp key; + if (lastCreatedInstrScopeAndTimestamp != null && lastCreatedInstrScopeAndTimestamp.equals(timestampMicros, instrScopeName)) { + key = lastCreatedInstrScopeAndTimestamp; + } else { + key = new InstrumentationScopeAndTimestamp(instrScopeName, timestampMicros); + lastCreatedInstrScopeAndTimestamp = key; + } + + Map timestampMetricSets = metricSets.get(key); + if (timestampMetricSets == null) { + timestampMetricSets = new HashMap<>(); + metricSets.put(key, timestampMetricSets); + } + + MetricSetGenerator ms = timestampMetricSets.get(attributes); + if (ms == null) { + ms = new MetricSetGenerator(attributes, key.instrumentationScopeName, key.timestamp, serializationTempBuilder); + timestampMetricSets.put(attributes, ms); + } + return ms; + } + + public void flushAndReset(Reporter reporter) { + for (Map map : metricSets.values()) { + for (MetricSetGenerator metricSet : map.values()) { + metricSet.finishAndReport(reporter); + } + } + metricSets.clear(); + } + + private static class InstrumentationScopeAndTimestamp { + private final long timestamp; + private final CharSequence instrumentationScopeName; + + public InstrumentationScopeAndTimestamp(CharSequence instrumentationScopeName, long timestamp) { + this.instrumentationScopeName = instrumentationScopeName; + this.timestamp = timestamp; + } + + public boolean equals(long timestamp, CharSequence instrumentationScopeName) { + return this.timestamp == timestamp && this.instrumentationScopeName.equals(instrumentationScopeName); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InstrumentationScopeAndTimestamp that = (InstrumentationScopeAndTimestamp) o; + if (timestamp != that.timestamp) return false; + return instrumentationScopeName.equals(that.instrumentationScopeName); + } + + @Override + public int hashCode() { + int result = (int) (timestamp ^ (timestamp >>> 32)); + result = 31 * result + instrumentationScopeName.hashCode(); + return result; + } + } + + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/package-info.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/package-info.java new file mode 100644 index 0000000000..362b96baf6 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +@NonnullApi +package co.elastic.apm.agent.otelmetricexport; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/pom.xml b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/pom.xml new file mode 100644 index 0000000000..9932f299cc --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + co.elastic.apm + apm-opentelemetry + 1.35.1-SNAPSHOT + + + apm-opentelemetry-metricsdk-plugin + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + + + + + + io.opentelemetry + opentelemetry-sdk-metrics + ${version.opentelemetry} + provided + + + ${project.groupId} + apm-opentelemetry-metrics-exporter + ${project.version} + + + + + + + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/OtelMetricsSdkRootPackagesCustomizer.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/OtelMetricsSdkRootPackagesCustomizer.java new file mode 100644 index 0000000000..a107fe0bf8 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/OtelMetricsSdkRootPackagesCustomizer.java @@ -0,0 +1,34 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer; + +import java.util.Arrays; +import java.util.Collection; + +public class OtelMetricsSdkRootPackagesCustomizer extends PluginClassLoaderRootPackageCustomizer { + @Override + public Collection pluginClassLoaderRootPackages() { + return Arrays.asList( + "co.elastic.apm.agent.otelmetricsdk", + "co.elastic.apm.agent.otelmetricexport" + ); + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java new file mode 100644 index 0000000000..79d5c53560 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java @@ -0,0 +1,96 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.GlobalTracer; +import co.elastic.apm.agent.otelmetricexport.ElasticOtelMetricsExporter; +import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakSet; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.Arrays; +import java.util.Collection; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class SdkMeterProviderBuilderInstrumentation extends ElasticApmInstrumentation { + @Override + public ElementMatcher getTypeMatcher() { + return named("io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder"); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("build"); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList("opentelemetry", "opentelemetry-metrics", "experimental"); + } + + @Override + public String getAdviceClassName() { + return getClass().getName() + "$SdkMeterProviderBuilderAdvice"; + } + + /** + * Instruments {@link SdkMeterProviderBuilder#build()}. + */ + public static class SdkMeterProviderBuilderAdvice { + + private static final Logger logger = LoggerFactory.getLogger(SdkMeterProviderBuilderInstrumentation.SdkMeterProviderBuilderAdvice.class); + + private static final WeakSet ALREADY_REGISTERED_BUILDERS = WeakConcurrent.buildSet(); + private static final ElasticApmTracer tracer = GlobalTracer.requireTracerImpl(); + + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static void onEnter(@Advice.This SdkMeterProviderBuilder thiz) { + if (checkMetricsSdkVersionSupported()) { + if (ALREADY_REGISTERED_BUILDERS.add(thiz)) { + ElasticOtelMetricsExporter.createAndRegisterOn(thiz, tracer); + } + } + } + + private static boolean checkMetricsSdkVersionSupported() { + // pre 1.16.0 versions did either not include the MetricsExporter class + // or the exporter does not support configuring aggregations + try { + Class instrumentTypeClass = Class.forName("io.opentelemetry.sdk.metrics.InstrumentType"); + MetricExporter.class.getMethod("getDefaultAggregation", instrumentTypeClass); + return true; + } catch (ClassNotFoundException | NoSuchMethodException e) { + logger.warn("Detected OpenTelemetry metrics SDK instance with a version older than 1.16.0. Skipping instrumentation because it is not supported."); + return false; + } + } + + } + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/package-info.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/package-info.java new file mode 100644 index 0000000000..7d81087edd --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +@NonnullApi +package co.elastic.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer new file mode 100644 index 0000000000..f6d31be7e1 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer @@ -0,0 +1 @@ +co.elastic.apm.agent.otelmetricsdk.OtelMetricsSdkRootPackagesCustomizer diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..b9613d00df --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1 @@ +co.elastic.apm.agent.otelmetricsdk.SdkMeterProviderBuilderInstrumentation diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java new file mode 100644 index 0000000000..986b168d67 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java @@ -0,0 +1,641 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.configuration.MetricsConfiguration; +import co.elastic.apm.agent.matcher.WildcardMatcher; +import co.elastic.apm.agent.report.ReporterConfiguration; +import co.elastic.apm.agent.testutils.DisabledIfNotOnClasspath; +import co.elastic.apm.agent.util.AtomicDouble; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleCounter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat; +import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThatMetricSets; +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.mockito.Mockito.doReturn; + +public abstract class AbstractOtelMetricsTest extends AbstractInstrumentationTest { + + private ReporterConfiguration reporterConfig; + + /** + * The meter provider is lazily initialized on first usage (when {@link #createMeter(String)} is called). + * This allows tests to adjust configurations prior to the initialization. + */ + @Nullable + private MeterProvider meterProvider; + + @BeforeEach + public void setup() { + reporterConfig = tracer.getConfig(ReporterConfiguration.class); + // we use explicit flush in tests instead of periodic reporting to prevent flakyness + doReturn(1_000_000L).when(reporterConfig).getMetricsIntervalMs(); + meterProvider = null; + } + + protected synchronized MeterProvider getMeterProvider() { + if (meterProvider == null) { + meterProvider = createOrLookupMeterProvider(); + } + return meterProvider; + } + + protected abstract MeterProvider createOrLookupMeterProvider(); + + protected abstract Meter createMeter(String name); + + protected void verifyAdditionalExportersCalled() { + } + + protected abstract void invokeSdkForceFlush(); + + protected void resetReporterAndFlushMetrics() { + reporter.reset(); + invokeSdkForceFlush(); + verifyAdditionalExportersCalled(); + } + + @Test + public void testAttributeTypes() { + Meter testMeter = createMeter("test"); + LongCounter counter = testMeter.counterBuilder("my_counter").build(); + + Attributes attribs = Attributes.builder() + .put(stringKey("string_attrib"), "foo") + .put(doubleKey("double_attrib"), Double.MAX_VALUE) + .put(longKey("long_attrib"), Long.MAX_VALUE) + .put(booleanKey("bool_attrib"), false) + .put(stringArrayKey("string_array_attrib"), List.of("foo", "bar")) + .put(doubleArrayKey("double_array_attrib"), List.of(1.0, Double.MAX_VALUE)) + .put(longArrayKey("long_array_attrib"), List.of(1L, Long.MAX_VALUE)) + .put(booleanArrayKey("bool_array_attrib"), List.of(true, false)) + .build(); + + counter.add(42, attribs); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .hasMetricsetWithLabelsSatisfying(Map.of( + "otel_instrumentation_scope_name", "test", + "string_attrib", "foo", + "double_attrib", Double.MAX_VALUE, + "long_attrib", Long.MAX_VALUE, + "bool_attrib", false + ), metrics -> metrics + .hasMetricsCount(1) + .containsValueMetric("my_counter", 42) + ); + } + + @Test + public void testAttributeNameSanitization() { + Meter testMeter = createMeter("test"); + LongCounter counter = testMeter.counterBuilder("my_counter").build(); + + Attributes attributes = Attributes.of(stringKey("please*sanitize.me\""), "dont*sanitize.me\""); + counter.add(42, attributes); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .hasMetricsetWithLabelsSatisfying( + "otel_instrumentation_scope_name", "test", + "please_sanitize_me_", "dont*sanitize.me\"" + , metrics -> metrics + .hasMetricsCount(1) + .containsValueMetric("my_counter", 42) + ); + } + + @Test + public void testTimestampPresent() { + Meter testMeter = createMeter("test"); + LongCounter counter = testMeter.counterBuilder("my_counter").build(); + + long timestampMicros = System.currentTimeMillis() * 1000L; + counter.add(42); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + //check for a range of +- 1 second to be safe against CI slowness / clock inaccuracy + .hasTimestampInRange(timestampMicros - 1_000_000L, timestampMicros + 1_000_000L); + } + + @Test + public void testMetricsCombinedInMetricsets() { + Meter testMeter = createMeter("test"); + ObservableDoubleGauge gauge1 = testMeter + .gaugeBuilder("my_gauge") + .buildWithCallback((obs) -> { + obs.record(42); + obs.record(142, Attributes.of(stringKey("foo"), "bar")); + }); + ObservableLongGauge gauge2 = testMeter + .gaugeBuilder("my_other_gauge").ofLongs() + .buildWithCallback((obs) -> { + obs.record(43); + obs.record(143, Attributes.of(stringKey("foo"), "bar")); + }); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(2) + .hasMetricsetWithLabelsSatisfying( + "otel_instrumentation_scope_name", "test", + metrics -> metrics.containsValueMetric("my_gauge", 42.0) + .containsValueMetric("my_other_gauge", 43) + .hasMetricsCount(2) + ) + .hasMetricsetWithLabelsSatisfying( + "otel_instrumentation_scope_name", "test", + "foo", "bar", + metrics -> metrics + .containsValueMetric("my_gauge", 142.0) + .containsValueMetric("my_other_gauge", 143) + .hasMetricsCount(2) + ); + } + + @Test + public void testSameMetricDifferentMeter() { + Meter meter1 = createMeter("meter1"); + meter1.counterBuilder("my_counter").build().add(10); + + Meter meter2 = createMeter("meter2"); + meter2.counterBuilder("my_counter").build().add(20); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(2) + .hasMetricsetWithLabelsSatisfying( + "otel_instrumentation_scope_name", "meter1", + metrics -> metrics + .containsValueMetric("my_counter", 10) + .hasMetricsCount(1) + ) + .hasMetricsetWithLabelsSatisfying( + "otel_instrumentation_scope_name", "meter2", + metrics -> metrics + .containsValueMetric("my_counter", 20) + .hasMetricsCount(1) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testDedotMetricNames(boolean enableDedot) { + MetricsConfiguration config = tracer.getConfig(MetricsConfiguration.class); + doReturn(enableDedot).when(config).isDedotCustomMetrics(); + + Meter meter1 = createMeter("test"); + meter1.counterBuilder("foo.bar").build().add(10); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric(enableDedot ? "foo_bar" : "foo.bar", 10) + .hasMetricsCount(1); + } + + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testMetricDisabling(boolean enableDedoting) { + MetricsConfiguration config = tracer.getConfig(MetricsConfiguration.class); + doReturn(enableDedoting).when(config).isDedotCustomMetrics(); + doReturn(List.of( + WildcardMatcher.valueOf("metric.a"), + WildcardMatcher.valueOf("metric_b") + )).when(reporterConfig).getDisableMetrics(); + + Meter meter1 = createMeter("test"); + meter1.counterBuilder("metric.a").build().add(10); + meter1.counterBuilder("metric.b").build().add(20); + meter1.counterBuilder("metric.c").build().add(30); + + resetReporterAndFlushMetrics(); + if (enableDedoting) { + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("metric_c", 30) + .hasMetricsCount(1); + } else { + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("metric.b", 20) + .containsValueMetric("metric.c", 30) + .hasMetricsCount(2); + } + } + + @Test + @DisabledIfNotOnClasspath("io.opentelemetry.api.metrics.BatchCallback") + public void testBatchObservation() { + // This runnable is required so that JUnit does not accidentally try to load the + // potentially absent used types (e.g. ObservableDoubleMeasurement) while inspecting the test class + new Runnable() { + @Override + public void run() { + + Meter testMeter = createMeter("test"); + + ObservableDoubleMeasurement doubleGauge = testMeter + .gaugeBuilder("double_gauge") + .buildObserver(); + ObservableLongMeasurement longGauge = testMeter + .gaugeBuilder("long_gauge") + .ofLongs() + .buildObserver(); + ObservableDoubleMeasurement doubleCounter = testMeter + .counterBuilder("double_counter") + .ofDoubles() + .buildObserver(); + ObservableLongMeasurement longCounter = testMeter + .counterBuilder("long_counter") + .buildObserver(); + ObservableDoubleMeasurement doubleUpDownCounter = testMeter + .upDownCounterBuilder("double_updown_counter") + .ofDoubles() + .buildObserver(); + ObservableLongMeasurement longUpDownCounter = testMeter + .upDownCounterBuilder("long_updown_counter") + .buildObserver(); + + testMeter.batchCallback(() -> { + doubleGauge.record(1.5); + longGauge.record(15); + doubleCounter.record(2.5); + longCounter.record(25); + doubleUpDownCounter.record(3.5); + longUpDownCounter.record(35); + }, doubleGauge, longGauge, doubleCounter, longCounter, doubleUpDownCounter, longUpDownCounter); + + resetReporterAndFlushMetrics(); + + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_gauge", 1.5) + .containsValueMetric("long_gauge", 15) + .containsValueMetric("double_counter", 2.5) + .containsValueMetric("long_counter", 25) + .containsValueMetric("double_updown_counter", 3.5) + .containsValueMetric("long_updown_counter", 35) + .hasMetricsCount(6); + } + }.run(); + } + + @Test + public void testObservableCounter() { + Meter testMeter = createMeter("test"); + + AtomicDouble doubleVal = new AtomicDouble(); + AtomicLong longVal = new AtomicLong(); + + ObservableDoubleCounter doubleCnt = testMeter + .counterBuilder("double_counter") + .ofDoubles() + .buildWithCallback((obs) -> { + obs.record(doubleVal.get()); + }); + + ObservableLongCounter longCnt = testMeter + .counterBuilder("long_counter") + .buildWithCallback((obs) -> { + obs.record(longVal.get()); + }); + + doubleVal.set(100.5); + longVal.set(100); + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 100.5) + .containsValueMetric("long_counter", 100) + .hasMetricsCount(2); + + + doubleVal.set(111); + longVal.set(110); + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 10.5) + .containsValueMetric("long_counter", 10) + .hasMetricsCount(2); + + //unchanged counters are not exported + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(0); + } + + @Test + public void testCounter() { + Meter testMeter = createMeter("test"); + + DoubleCounter doubleCnt = testMeter + .counterBuilder("double_counter") + .ofDoubles() + .build(); + + LongCounter longCnt = testMeter + .counterBuilder("long_counter") + .build(); + + doubleCnt.add(1.5); + longCnt.add(2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 1.5) + .containsValueMetric("long_counter", 2) + .hasMetricsCount(2); + + doubleCnt.add(2.5); + doubleCnt.add(2); + longCnt.add(3); + longCnt.add(1); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 4.5) + .containsValueMetric("long_counter", 4) + .hasMetricsCount(2); + + resetReporterAndFlushMetrics(); + //unchanged counters are not exported + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(0); + } + + @Test + public void testObservableUpDownCounter() { + Meter testMeter = createMeter("test"); + + AtomicDouble doubleVal = new AtomicDouble(); + AtomicLong longVal = new AtomicLong(); + + ObservableDoubleUpDownCounter doubleCnt = testMeter + .upDownCounterBuilder("double_counter") + .ofDoubles() + .buildWithCallback((obs) -> { + obs.record(doubleVal.get()); + }); + + ObservableLongUpDownCounter longCnt = testMeter + .upDownCounterBuilder("long_counter") + .buildWithCallback((obs) -> { + obs.record(longVal.get()); + }); + + doubleVal.set(100.5); + longVal.set(100); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 100.5) + .containsValueMetric("long_counter", 100) + .hasMetricsCount(2); + + doubleVal.set(0.0); + longVal.set(0); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 0.0) + .containsValueMetric("long_counter", 0) + .hasMetricsCount(2); + + doubleVal.set(90.2); + longVal.set(42); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 90.2) + .containsValueMetric("long_counter", 42) + .hasMetricsCount(2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 90.2) + .containsValueMetric("long_counter", 42); + } + + + @Test + public void testUpDownCounter() { + Meter testMeter = createMeter("test"); + + DoubleUpDownCounter doubleCnt = testMeter + .upDownCounterBuilder("double_counter") + .ofDoubles() + .build(); + + LongUpDownCounter longCnt = testMeter + .upDownCounterBuilder("long_counter") + .build(); + + doubleCnt.add(1.5); + longCnt.add(2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 1.5) + .containsValueMetric("long_counter", 2) + .hasMetricsCount(2); + + + doubleCnt.add(-1.5); + longCnt.add(-2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", 0.0) + .containsValueMetric("long_counter", 0) + .hasMetricsCount(2); + + doubleCnt.add(-150); + doubleCnt.add(49.5); + longCnt.add(-10); + longCnt.add(1); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", -100.5) + .containsValueMetric("long_counter", -9) + .hasMetricsCount(2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("double_counter", -100.5) + .containsValueMetric("long_counter", -9) + .hasMetricsCount(2); + } + + + @Test + public void testHistogram() { + MetricsConfiguration metricsConfig = config.getConfig(MetricsConfiguration.class); + doReturn(List.of(1.0, 5.0)).when(metricsConfig).getCustomMetricsHistogramBoundaries(); + + Meter testMeter = createMeter("test"); + DoubleHistogram doubleHisto = testMeter.histogramBuilder("double_histo").build(); + LongHistogram longHisto = testMeter.histogramBuilder("long_histo").ofLongs().build(); + + doubleHisto.record(0.5); + doubleHisto.record(1.5); + doubleHisto.record(1.5); + doubleHisto.record(5.5); + doubleHisto.record(5.5); + doubleHisto.record(5.5); + + longHisto.record(0); + longHisto.record(2); + longHisto.record(2); + longHisto.record(6); + longHisto.record(6); + longHisto.record(6); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsHistogramMetric("double_histo", List.of(0.5, 3.0, 5.0), List.of(1L, 2L, 3L)) + .containsHistogramMetric("long_histo", List.of(0.5, 3.0, 5.0), List.of(1L, 2L, 3L)) + .hasMetricsCount(2); + + //make sure only delta is reported and empty buckets are omitted + doubleHisto.record(1.5); + longHisto.record(2); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsHistogramMetric("double_histo", List.of(3.0), List.of(1L)) + .containsHistogramMetric("long_histo", List.of(3.0), List.of(1L)) + .hasMetricsCount(2); + + //empty histograms must not be exported + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(0); + } + + + /** + * Smoke test which just checks that nothing breaks with the default histogram buckets. + */ + @Test + public void testDefaultHistogramBuckets() { + MetricsConfiguration metricsConfig = config.getConfig(MetricsConfiguration.class); + List boundaries = metricsConfig.getCustomMetricsHistogramBoundaries(); + assertThat(boundaries).isNotEmpty(); + + Meter testMeter = createMeter("test"); + DoubleHistogram doubleHisto = testMeter.histogramBuilder("double_histo").build(); + LongHistogram longHisto = testMeter.histogramBuilder("long_histo").ofLongs().build(); + + long totalSum = 0l; + for (int i = 1; i < 1000; i++) { + double value = 0.00001d * Math.pow(1.1, i); + for (int j = 0; j < i; j++) { + totalSum++; + longHisto.record(Math.round(value)); + doubleHisto.record(value); + } + } + final long totalSumFinal = totalSum; + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .metricSatisfies("double_histo", + metric -> assertThat(metric.counts.stream().mapToLong(Long::longValue).sum()).isEqualTo(totalSumFinal)) + .metricSatisfies("long_histo", + metric -> assertThat(metric.counts.stream().mapToLong(Long::longValue).sum()).isEqualTo(totalSumFinal)) + .hasMetricsCount(2); + } + + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/DummyMetricReader.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/DummyMetricReader.java new file mode 100644 index 0000000000..573b16aad0 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/DummyMetricReader.java @@ -0,0 +1,47 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricReader; + +public class DummyMetricReader implements MetricReader { + @Override + public void register(CollectionRegistration collectionRegistration) { + + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java new file mode 100644 index 0000000000..569ce99c20 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java @@ -0,0 +1,36 @@ +package co.elastic.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.report.ReporterConfiguration; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +public class MetricExportTimingTest extends AbstractInstrumentationTest { + + /** + * This test has been separated from the other because we don't want it to run in the integration tests: It would be too slow. + */ + @Test + void testMetricExportIntervalRespected() throws Exception { + ReporterConfiguration reporterConfig = tracer.getConfig(ReporterConfiguration.class); + doReturn(50L).when(reporterConfig).getMetricsIntervalMs(); + + try (SdkMeterProvider meterProvider = SdkMeterProvider.builder().build()) { + Meter meter = meterProvider.meterBuilder("test").build(); + + meter.gaugeBuilder("my_gauge").buildWithCallback(obs -> obs.record(42.0)); + + Thread.sleep(1000); + + //To account for CI slowness, we try to check that the number of exports is just in the correct ballpark + assertThat(reporter.getBytes().size()) + .isBetween(10, 21); + + } + + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/PrivateUserSdkOtelMetricsTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/PrivateUserSdkOtelMetricsTest.java new file mode 100644 index 0000000000..f31884653b --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/PrivateUserSdkOtelMetricsTest.java @@ -0,0 +1,126 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.configuration.MetricsConfiguration; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThatMetricSets; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +public class PrivateUserSdkOtelMetricsTest extends AbstractOtelMetricsTest { + + private MetricReader additionalReader; + + @Nullable + private Consumer sdkCustomizer; + + @Override + protected MeterProvider createOrLookupMeterProvider() { + additionalReader = Mockito.spy(new DummyMetricReader()); + + SdkMeterProviderBuilder sdkMeterProviderBuilder = SdkMeterProvider.builder() + .registerMetricReader(additionalReader); + if (sdkCustomizer != null) { + sdkCustomizer.accept(sdkMeterProviderBuilder); + } + return sdkMeterProviderBuilder.build(); + } + + @Override + protected void verifyAdditionalExportersCalled() { + verify(additionalReader).forceFlush(); + Mockito.reset(additionalReader); + } + + @BeforeEach + public void cleanSdkCustomizer() { + sdkCustomizer = null; + } + + @AfterEach + public void cleanupUserSdk() { + ((SdkMeterProvider) getMeterProvider()).shutdown().join(10, TimeUnit.SECONDS); + } + + @Override + protected Meter createMeter(String name) { + Meter meter = getMeterProvider().get(name); + try { + //make sure that we use our own SDK classes instead of the ones provided by the agent + assertThat(meter.getClass()).isSameAs(Class.forName("io.opentelemetry.sdk.metrics.SdkMeter")); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return meter; + } + + @Override + protected void invokeSdkForceFlush() { + ((SdkMeterProvider) getMeterProvider()).forceFlush(); + } + + @Test + public void testCustomHistogramView() { + sdkCustomizer = builder -> builder.registerView( + InstrumentSelector.builder().setName("custom_histo").build(), + View.builder().setAggregation(Aggregation.explicitBucketHistogram(List.of(1.0, 5.0))).build() + ); + + MetricsConfiguration metricsConfig = config.getConfig(MetricsConfiguration.class); + doReturn(List.of(42.0)).when(metricsConfig).getCustomMetricsHistogramBoundaries(); + + Meter testMeter = createMeter("test"); + DoubleHistogram histo = testMeter.histogramBuilder("custom_histo").build(); + + histo.record(0.5); + histo.record(1.5); + histo.record(1.5); + histo.record(5.5); + histo.record(5.5); + histo.record(5.5); + + resetReporterAndFlushMetrics(); + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsHistogramMetric("custom_histo", List.of(0.5, 3.0, 5.0), List.of(1L, 2L, 3L)) + .hasMetricsCount(1); + } + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/UnsupportedSdkVersionIgnoredTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/UnsupportedSdkVersionIgnoredTest.java new file mode 100644 index 0000000000..8953907c95 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/UnsupportedSdkVersionIgnoredTest.java @@ -0,0 +1,47 @@ +/* + * 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.apm.agent.otelmetricsdk; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.testutils.TestClassWithDependencyRunner; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Only executed via integration tests (OpenTelemetryVersionIT). + */ +public class UnsupportedSdkVersionIgnoredTest extends AbstractInstrumentationTest { + + @Test + @TestClassWithDependencyRunner.DisableOutsideOfRunner + void checkUnsupportedVersionIgnored() { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + Meter testMeter = meterProvider.get("test"); + LongCounter counter = testMeter.counterBuilder("counter").build(); + + counter.add(42); + + meterProvider.forceFlush(); + assertThat(reporter.getBytes()).isEmpty(); + } +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/pom.xml b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/pom.xml index 9a4e67f73c..4f4678428d 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/pom.xml +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/pom.xml @@ -12,28 +12,21 @@ ${project.basedir}/../../.. - - - 1.0.0 - 1.0.0-alpha io.opentelemetry opentelemetry-api - ${version.opentelemetry} provided + io.opentelemetry opentelemetry-semconv - ${version.opentelemetry.semconv} - provided + ${version.opentelemetry}-alpha + test ${project.groupId} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml index 5ed78019ae..0980551c8a 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml @@ -15,6 +15,7 @@ + ${project.groupId} apm-opentelemetry-plugin @@ -28,6 +29,20 @@ + + ${project.groupId} + apm-opentelemetry-metricsdk-plugin + test-jar + ${project.version} + test + + + io.opentelemetry + * + + + + ${project.groupId} apm-opentelemetry-plugin @@ -40,6 +55,18 @@ + + ${project.groupId} + apm-opentelemetry-metrics-plugin + ${project.version} + test + + + io.opentelemetry + * + + + org.apache.ivy ivy diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/agent/opentelemetry/OpenTelemetryVersionIT.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/agent/opentelemetry/OpenTelemetryVersionIT.java new file mode 100644 index 0000000000..45f12ec4d0 --- /dev/null +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/agent/opentelemetry/OpenTelemetryVersionIT.java @@ -0,0 +1,114 @@ +/* + * 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.apm.agent.opentelemetry; + +import co.elastic.apm.agent.testutils.TestClassWithDependencyRunner; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +public class OpenTelemetryVersionIT { + + @ParameterizedTest + @ValueSource(strings = { + "1.0.1", + "1.1.0", + "1.2.0", + "1.3.0", + "1.4.1", + "1.5.0", + "1.6.0", + "1.7.1", + "1.9.0", + "1.10.1", + "1.11.0", + "1.12.0", + "1.13.0", + "1.14.0", + "1.15.0", + "1.16.0", + "1.17.0", + "1.18.0", + "1.19.0", + "1.20.0", + "1.21.0" + }) + void testTracingVersion(String version) throws Exception { + List dependencies = List.of( + "io.opentelemetry:opentelemetry-api:" + version, + "io.opentelemetry:opentelemetry-context:" + version, + "io.opentelemetry:opentelemetry-semconv:" + version + "-alpha"); + TestClassWithDependencyRunner runner = new TestClassWithDependencyRunner(dependencies, + "co.elastic.apm.agent.opentelemetry.sdk.ElasticOpenTelemetryTest", + "co.elastic.apm.agent.opentelemetry.sdk.AbstractOpenTelemetryTest", + "co.elastic.apm.agent.opentelemetry.sdk.ElasticOpenTelemetryTest$MapGetter"); + runner.run(); + } + + @ParameterizedTest + @ValueSource(strings = { + "1.16.0", + "1.17.0", + "1.18.0", + "1.19.0", + "1.20.0", + "1.21.0" + }) + void testUserProvidedMetricsSdkVersion(String version) throws Exception { + List dependencies = List.of( + "io.opentelemetry:opentelemetry-api:" + version, + "io.opentelemetry:opentelemetry-sdk-metrics:" + version, + "io.opentelemetry:opentelemetry-sdk-common:" + version, + "io.opentelemetry:opentelemetry-context:" + version, + "io.opentelemetry:opentelemetry-semconv:" + version + "-alpha"); + TestClassWithDependencyRunner runner = new TestClassWithDependencyRunner(dependencies, + "co.elastic.apm.agent.otelmetricsdk.PrivateUserSdkOtelMetricsTest", + "co.elastic.apm.agent.otelmetricsdk.AbstractOtelMetricsTest", + "co.elastic.apm.agent.otelmetricsdk.AbstractOtelMetricsTest$1", + "co.elastic.apm.agent.otelmetricsdk.DummyMetricReader"); + runner.run(); + } + + @ParameterizedTest + @CsvSource(value = { + "1.10.0|1.10.0-alpha", + "1.11.0|1.11.0-alpha", + "1.12.0|1.12.0-alpha", + "1.13.0|1.13.0-alpha", + "1.14.0|1.14.0", + "1.15.0|1.15.0", + }, delimiterString = "|") + void testUnsupportedMetricsSdkVersionsIgnored(String apiVersion, String sdkVersion) throws Exception { + List dependencies = List.of( + "io.opentelemetry:opentelemetry-api:" + apiVersion, + "io.opentelemetry:opentelemetry-sdk-metrics:" + sdkVersion, + "io.opentelemetry:opentelemetry-sdk-common:" + apiVersion, + "io.opentelemetry:opentelemetry-context:" + apiVersion, + "io.opentelemetry:opentelemetry-semconv:" + apiVersion + "-alpha"); + + + TestClassWithDependencyRunner runner = new TestClassWithDependencyRunner(dependencies, + "co.elastic.apm.agent.otelmetricsdk.UnsupportedSdkVersionIgnoredTest", + "co.elastic.apm.agent.otelmetricsdk.DummyMetricReader"); + runner.run(); + } + +} diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/opentelemetry/OpenTelemetryVersionIT.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/opentelemetry/OpenTelemetryVersionIT.java deleted file mode 100644 index 7ebb8bf421..0000000000 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/src/test/java/co/elastic/apm/opentelemetry/OpenTelemetryVersionIT.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.apm.opentelemetry; - -import co.elastic.apm.agent.testutils.JUnit4TestClassWithDependencyRunner; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Arrays; -import java.util.List; - -@RunWith(Parameterized.class) -public class OpenTelemetryVersionIT { - private final JUnit4TestClassWithDependencyRunner runner; - - public OpenTelemetryVersionIT(String version) throws Exception { - List dependencies = List.of( - "io.opentelemetry:opentelemetry-api:" + version, - "io.opentelemetry:opentelemetry-context:" + version, - "io.opentelemetry:opentelemetry-semconv:" + version + "-alpha"); - runner = new JUnit4TestClassWithDependencyRunner(dependencies, - "co.elastic.apm.agent.opentelemetry.sdk.ElasticOpenTelemetryTest", - "co.elastic.apm.agent.opentelemetry.sdk.AbstractOpenTelemetryTest", - "co.elastic.apm.agent.opentelemetry.sdk.ElasticOpenTelemetryTest$MapGetter"); - } - - @Parameterized.Parameters(name= "{0}") - public static Iterable data() { - return Arrays.asList(new Object[][]{ - {"1.0.1"}, - {"1.1.0"}, - {"1.2.0"}, - {"1.3.0"}, - {"1.4.1"}, - {"1.5.0"}, - {"1.6.0"}, - {"1.7.1"}, - {"1.9.0"}, - {"1.10.1"}, - {"1.11.0"}, - {"1.12.0"}, - {"1.13.0"}, - {"1.14.0"}, - {"1.15.0"}, - {"1.16.0"}, - {"1.17.0"} - }); - } - - @Test - public void testVersions() throws Exception { - runner.run(); - } -} diff --git a/apm-agent-plugins/apm-opentelemetry/pom.xml b/apm-agent-plugins/apm-opentelemetry/pom.xml index 91a560b672..9862ef54ef 100644 --- a/apm-agent-plugins/apm-opentelemetry/pom.xml +++ b/apm-agent-plugins/apm-opentelemetry/pom.xml @@ -15,15 +15,21 @@ ${project.basedir}/../.. + + 1.22.0 8 8 true - apm-opentelemetry-plugin + apm-opentelemetry-metricsdk-plugin + apm-opentelemetry-metrics-exporter apm-opentelemetry-test diff --git a/apm-agent/pom.xml b/apm-agent/pom.xml index 8e036b4b09..2579664d33 100644 --- a/apm-agent/pom.xml +++ b/apm-agent/pom.xml @@ -196,6 +196,11 @@ apm-opentelemetry-plugin ${project.version} + + ${project.groupId} + apm-opentelemetry-metricsdk-plugin + ${project.version} + ${project.groupId} apm-logback-plugin-impl From 6cb4f940cc7f56675bc11edce8c5995143a7d34f Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 17 Jan 2023 09:29:16 +0100 Subject: [PATCH 2/9] Fix rebase conflicts --- .../otelmetricexport/MetricSetGenerator.java | 2 +- .../otelmetricsdk/MetricExportTimingTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java index 81c02aa325..63e44b6c82 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java @@ -282,7 +282,7 @@ private void serializeMetricSetEnd() { public void finishAndReport(Reporter reporter) { if (anySamplesWritten) { serializeMetricSetEnd(); - reporter.report(jw); + reporter.reportMetrics(jw); } } } diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java index 569ce99c20..21a934aa7d 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/MetricExportTimingTest.java @@ -1,3 +1,21 @@ +/* + * 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.apm.agent.otelmetricsdk; import co.elastic.apm.agent.AbstractInstrumentationTest; From 3359ca6122370056c646c581f639de9989f6303a Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 17 Jan 2023 09:52:49 +0100 Subject: [PATCH 3/9] Fix bad dependency --- .../apm-opentelemetry/apm-opentelemetry-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml index 0980551c8a..cfec9c15ea 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-test/pom.xml @@ -57,7 +57,7 @@ ${project.groupId} - apm-opentelemetry-metrics-plugin + apm-opentelemetry-metricsdk-plugin ${project.version} test From d3b849e42c052ec2dff3e7d532f6b5e374736df4 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 17 Jan 2023 13:22:36 +0100 Subject: [PATCH 4/9] Fixed and generated docs --- .../configuration/MetricsConfiguration.java | 4 +- docs/configuration.asciidoc | 42 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java index 3db93a500c..7a4596ed2b 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java @@ -45,9 +45,9 @@ public class MetricsConfiguration extends ConfigurationOptionProvider { private final ConfigurationOption> customMetricsHistogramBoundaries = ConfigurationOption.builder(new ListValueConverter<>(DoubleValueConverter.INSTANCE), List.class) .key("custom_metrics_histogram_boundaries") .configurationCategory(METRICS_CATEGORY) - .description("TODO") + .description("Defines the default bucket boundaries to use for OpenTelemetry histograms.") .dynamic(true) - .tags("added[1.36.0]") + .tags("added[1.36.0]", "experimental") .buildWithDefault(Arrays.asList( 0.00390625, 0.00552427, 0.0078125, 0.0110485, 0.015625, 0.0220971, 0.03125, 0.0441942, 0.0625, 0.0883883, 0.125, 0.176777, 0.25, 0.353553, 0.5, 0.707107, 1.0, 1.41421, 2.0, diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 565aa9a5b5..a2075cab69 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -165,6 +165,7 @@ Click on a key to get more information. ** <> * <> ** <> +** <> ** <> ** <> ** <> @@ -778,7 +779,7 @@ you should add an additional entry to this list (make sure to also include the d ==== `enable_instrumentations` (added[1.28.0]) A list of instrumentations which should be selectively enabled. -Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentelemetry-metrics`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. When set to non-empty value, only listed instrumentations will be enabled if they are not disabled through <> or <>. When not set or empty (default), all instrumentations enabled by default will be enabled unless they are disabled through <> or <>. @@ -806,7 +807,7 @@ NOTE: Changing this value at runtime can slow down the application temporarily. ==== `disable_instrumentations` (added[1.0.0,Changing this value at runtime is possible since version 1.15.0]) A list of instrumentations which should be disabled. -Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentelemetry-metrics`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. For version `1.25.0` and later, use <> to enable experimental instrumentations. NOTE: Changing this value at runtime can slow down the application temporarily. @@ -2326,6 +2327,31 @@ The first metric maps `foo` to a number and the second metric maps `foo` as an o | `elastic.apm.dedot_custom_metrics` | `dedot_custom_metrics` | `ELASTIC_APM_DEDOT_CUSTOM_METRICS` |============ +// This file is auto generated. Please make your changes in *Configuration.java (for example CoreConfiguration.java) and execute ConfigurationExporter +[float] +[[config-custom-metrics-histogram-boundaries]] +==== `custom_metrics_histogram_boundaries` (added[1.36.0] experimental) + +NOTE: This feature is currently experimental, which means it is disabled by default and it is not guaranteed to be backwards compatible in future releases. + +Defines the default bucket boundaries to use for OpenTelemetry histograms. + +<> + + +[options="header"] +|============ +| Default | Type | Dynamic +| `0.00390625, 0.00552427, 0.0078125, 0.0110485, 0.015625, 0.0220971, 0.03125, 0.0441942, 0.0625, 0.0883883, 0.125, 0.176777, 0.25, 0.353553, 0.5, 0.707107, 1.0, 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0, 45.2548, 64.0, 90.5097, 128.0, 181.019, 256.0, 362.039, 512.0, 724.077, 1024.0, 1448.15, 2048.0, 2896.31, 4096.0, 5792.62, 8192.0, 11585.2, 16384.0, 23170.5, 32768.0, 46341.0, 65536.0, 92681.9, 131072.0` | List | true +|============ + + +[options="header"] +|============ +| Java System Properties | Property file | Environment +| `elastic.apm.custom_metrics_histogram_boundaries` | `custom_metrics_histogram_boundaries` | `ELASTIC_APM_CUSTOM_METRICS_HISTOGRAM_BOUNDARIES` +|============ + // This file is auto generated. Please make your changes in *Configuration.java (for example CoreConfiguration.java) and execute ConfigurationExporter [float] [[config-metric-set-limit]] @@ -3461,7 +3487,7 @@ Example: `5ms`. # sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,*auth*,*principal*,set-cookie # A list of instrumentations which should be selectively enabled. -# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentelemetry-metrics`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. # When set to non-empty value, only listed instrumentations will be enabled if they are not disabled through <> or <>. # When not set or empty (default), all instrumentations enabled by default will be enabled unless they are disabled through <> or <>. # @@ -3474,7 +3500,7 @@ Example: `5ms`. # enable_instrumentations= # A list of instrumentations which should be disabled. -# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `aws-sdk`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `finagle-httpclient`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `jul-error`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentelemetry-metrics`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `servlet-service-name`, `servlet-version`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webclient`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `tomcat-ecs`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. # For version `1.25.0` and later, use <> to enable experimental instrumentations. # # NOTE: Changing this value at runtime can slow down the application temporarily. @@ -4292,6 +4318,14 @@ Example: `5ms`. # # dedot_custom_metrics=true +# Defines the default bucket boundaries to use for OpenTelemetry histograms. +# +# This setting can be changed at runtime +# Type: comma separated list +# Default value: 0.00390625,0.00552427,0.0078125,0.0110485,0.015625,0.0220971,0.03125,0.0441942,0.0625,0.0883883,0.125,0.176777,0.25,0.353553,0.5,0.707107,1.0,1.41421,2.0,2.82843,4.0,5.65685,8.0,11.3137,16.0,22.6274,32.0,45.2548,64.0,90.5097,128.0,181.019,256.0,362.039,512.0,724.077,1024.0,1448.15,2048.0,2896.31,4096.0,5792.62,8192.0,11585.2,16384.0,23170.5,32768.0,46341.0,65536.0,92681.9,131072.0 +# +# custom_metrics_histogram_boundaries=0.00390625,0.00552427,0.0078125,0.0110485,0.015625,0.0220971,0.03125,0.0441942,0.0625,0.0883883,0.125,0.176777,0.25,0.353553,0.5,0.707107,1.0,1.41421,2.0,2.82843,4.0,5.65685,8.0,11.3137,16.0,22.6274,32.0,45.2548,64.0,90.5097,128.0,181.019,256.0,362.039,512.0,724.077,1024.0,1448.15,2048.0,2896.31,4096.0,5792.62,8192.0,11585.2,16384.0,23170.5,32768.0,46341.0,65536.0,92681.9,131072.0 + # Limits the number of active metric sets. # The metrics sets have associated labels, and the metrics sets are held internally in a map using the labels as keys. The map is limited in size by this option to prevent unbounded growth. If you hit the limit, you'll receive a warning in the agent log. # The recommended option to workaround the limit is to try to limit the cardinality of the labels, eg naming your transactions so that there are fewer distinct transaction names. From aa5211cda97ecca822f08640d98dc622f38cb487 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Mon, 6 Feb 2023 14:51:23 +0100 Subject: [PATCH 5/9] Removed dedotting from Otel metrics --- .../OtelMetricSerializer.java | 54 +++++++------------ .../AbstractOtelMetricsTest.java | 41 +++++--------- 2 files changed, 31 insertions(+), 64 deletions(-) diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java index 3831687da4..6336f16b7a 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java @@ -22,7 +22,6 @@ import co.elastic.apm.agent.configuration.MetricsConfiguration; import co.elastic.apm.agent.report.Reporter; import co.elastic.apm.agent.report.ReporterConfiguration; -import co.elastic.apm.agent.report.serialize.DslJsonSerializer; import co.elastic.apm.agent.sdk.logging.Logger; import co.elastic.apm.agent.sdk.logging.LoggerFactory; import io.opentelemetry.api.common.Attributes; @@ -47,7 +46,6 @@ public class OtelMetricSerializer { private final MetricsConfiguration metricsConfig; private final ReporterConfiguration reporterConfig; private final StringBuilder serializationTempBuilder; - private final StringBuilder metricNameBuilder; private final Set metricsWithBadAggregations = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -61,66 +59,52 @@ public OtelMetricSerializer(MetricsConfiguration metricsConfig, ReporterConfigur this.reporterConfig = reporterConfig; metricSets = new HashMap<>(); serializationTempBuilder = new StringBuilder(); - metricNameBuilder = new StringBuilder(); } public void addValues(MetricData metric) { - String orginalName = metric.getName(); - - CharSequence name; - boolean isEnabled; - if (metricsConfig.isDedotCustomMetrics()) { - name = DslJsonSerializer.sanitizePropertyName(orginalName, metricNameBuilder); - isEnabled = !isMetricDisabled(name) && !isMetricDisabled(orginalName); - } else { - name = orginalName; - isEnabled = !isMetricDisabled(name); - } - if (isEnabled) { - addMetricValues(name, metric); - } - } - - private boolean isMetricDisabled(CharSequence name) { - for (WildcardMatcher matcher : reporterConfig.getDisableMetrics()) { - if (matcher.matches(name)) { - return true; - } + String metricName = metric.getName(); + if (isMetricDisabled(metricName)) { + return; } - return false; - } - - private void addMetricValues(CharSequence sanitizedName, MetricData metric) { boolean isDelta; String instrumentationScopeName = metric.getInstrumentationScopeInfo().getName(); switch (metric.getType()) { case LONG_GAUGE: - addLongValues(sanitizedName, instrumentationScopeName, metric.getLongGaugeData(), false); + addLongValues(metricName, instrumentationScopeName, metric.getLongGaugeData(), false); break; case DOUBLE_GAUGE: - addDoubleValues(sanitizedName, instrumentationScopeName, metric.getDoubleGaugeData(), false); + addDoubleValues(metricName, instrumentationScopeName, metric.getDoubleGaugeData(), false); break; case LONG_SUM: isDelta = metric.getLongSumData().getAggregationTemporality().equals(AggregationTemporality.DELTA); - addLongValues(sanitizedName, instrumentationScopeName, metric.getLongSumData(), isDelta); + addLongValues(metricName, instrumentationScopeName, metric.getLongSumData(), isDelta); break; case DOUBLE_SUM: isDelta = metric.getDoubleSumData().getAggregationTemporality().equals(AggregationTemporality.DELTA); - addDoubleValues(sanitizedName, instrumentationScopeName, metric.getDoubleSumData(), isDelta); + addDoubleValues(metricName, instrumentationScopeName, metric.getDoubleSumData(), isDelta); break; case HISTOGRAM: - addHistogramValues(sanitizedName, instrumentationScopeName, metric.getHistogramData()); + addHistogramValues(metricName, instrumentationScopeName, metric.getHistogramData()); break; case SUMMARY: case EXPONENTIAL_HISTOGRAM: default: - if (metricsWithBadAggregations.add(metric.getName())) { - logger.warn("Ignoring metric '%s' due to unsupported aggregation '%s'", metric.getName(), metric.getType()); + if (metricsWithBadAggregations.add(metricName)) { + logger.warn("Ignoring metric '%s' due to unsupported aggregation '%s'", metricName, metric.getType()); } break; } } + private boolean isMetricDisabled(CharSequence name) { + for (WildcardMatcher matcher : reporterConfig.getDisableMetrics()) { + if (matcher.matches(name)) { + return true; + } + } + return false; + } + private void addHistogramValues(CharSequence name, CharSequence instrScopeName, HistogramData histogramData) { for (HistogramPointData histo : histogramData.getPoints()) { long timestampMicros = histo.getEpochNanos() / 1000L; diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java index d4933d7c06..7c3df257dc 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java @@ -43,8 +43,6 @@ import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import javax.annotation.Nullable; import java.util.List; @@ -233,11 +231,10 @@ public void testSameMetricDifferentMeter() { ); } - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testDedotMetricNames(boolean enableDedot) { + @Test + public void testDedotSettingIgnored() { MetricsConfiguration config = tracer.getConfig(MetricsConfiguration.class); - doReturn(enableDedot).when(config).isDedotCustomMetrics(); + doReturn(true).when(config).isDedotCustomMetrics(); Meter meter1 = createMeter("test"); meter1.counterBuilder("foo.bar").build().add(10); @@ -246,41 +243,27 @@ public void testDedotMetricNames(boolean enableDedot) { assertThatMetricSets(reporter.getBytes()) .hasMetricsetCount(1) .first() - .containsValueMetric(enableDedot ? "foo_bar" : "foo.bar", 10) + .containsValueMetric("foo.bar", 10) .hasMetricsCount(1); } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testMetricDisabling(boolean enableDedoting) { + @Test + public void testMetricDisabling() { MetricsConfiguration config = tracer.getConfig(MetricsConfiguration.class); - doReturn(enableDedoting).when(config).isDedotCustomMetrics(); doReturn(List.of( - WildcardMatcher.valueOf("metric.a"), - WildcardMatcher.valueOf("metric_b") + WildcardMatcher.valueOf("metric.a") )).when(reporterConfig).getDisableMetrics(); Meter meter1 = createMeter("test"); meter1.counterBuilder("metric.a").build().add(10); meter1.counterBuilder("metric.b").build().add(20); - meter1.counterBuilder("metric.c").build().add(30); resetReporterAndFlushMetrics(); - if (enableDedoting) { - assertThatMetricSets(reporter.getBytes()) - .hasMetricsetCount(1) - .first() - .containsValueMetric("metric_c", 30) - .hasMetricsCount(1); - } else { - assertThatMetricSets(reporter.getBytes()) - .hasMetricsetCount(1) - .first() - .containsValueMetric("metric.b", 20) - .containsValueMetric("metric.c", 30) - .hasMetricsCount(2); - } + assertThatMetricSets(reporter.getBytes()) + .hasMetricsetCount(1) + .first() + .containsValueMetric("metric.b", 20) + .hasMetricsCount(1); } @Test From 2f533b09bee7a13c2d72ff9cc288b5193260c285 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Mon, 6 Feb 2023 15:07:57 +0100 Subject: [PATCH 6/9] Review fixes --- .../configuration/MetricsConfiguration.java | 20 +++++++++++++++++-- .../assertions/metrics/MetricsetJson.java | 2 +- ...enerator.java => MetricSetSerializer.java} | 8 ++------ .../OtelMetricSerializer.java | 20 +++++++++---------- 4 files changed, 31 insertions(+), 19 deletions(-) rename apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/{MetricSetGenerator.java => MetricSetSerializer.java} (96%) diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java index 7a4596ed2b..e06f22e72e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/MetricsConfiguration.java @@ -23,7 +23,10 @@ import org.stagemonitor.configuration.ConfigurationOptionProvider; import org.stagemonitor.configuration.converter.DoubleValueConverter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; public class MetricsConfiguration extends ConfigurationOptionProvider { @@ -33,7 +36,7 @@ public class MetricsConfiguration extends ConfigurationOptionProvider { private final ConfigurationOption dedotCustomMetrics = ConfigurationOption.booleanOption() .key("dedot_custom_metrics") .configurationCategory(METRICS_CATEGORY) - .description("Replaces dots with underscores in the metric names for custom metrics, such as Micrometer metrics.\n" + + .description("Replaces dots with underscores in the metric names for Micrometer metrics.\n" + "\n" + "WARNING: Setting this to `false` can lead to mapping conflicts as dots indicate nesting in Elasticsearch.\n" + "An example of when a conflict happens is two metrics with the name `foo` and `foo.bar`.\n" + @@ -47,7 +50,20 @@ public class MetricsConfiguration extends ConfigurationOptionProvider { .configurationCategory(METRICS_CATEGORY) .description("Defines the default bucket boundaries to use for OpenTelemetry histograms.") .dynamic(true) - .tags("added[1.36.0]", "experimental") + .tags("added[1.37.0]", "experimental") + .addValidator(new ConfigurationOption.Validator>() { + @Override + public void assertValid(List buckets) { + if (new HashSet(buckets).size() != buckets.size()) { + throw new IllegalArgumentException("Bucket Boundaries contain duplicates!"); + } + List sorted = new ArrayList<>(buckets); + Collections.sort(sorted); + if (!sorted.equals(buckets)) { + throw new IllegalArgumentException("Bucket Boundaries need to be sorted in ascending order!"); + } + } + }) .buildWithDefault(Arrays.asList( 0.00390625, 0.00552427, 0.0078125, 0.0110485, 0.015625, 0.0220971, 0.03125, 0.0441942, 0.0625, 0.0883883, 0.125, 0.176777, 0.25, 0.353553, 0.5, 0.707107, 1.0, 1.41421, 2.0, diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java index 10a9274e6f..8965ce99fd 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java @@ -25,7 +25,7 @@ import java.util.HashMap; import java.util.Map; -@JsonIgnoreProperties(ignoreUnknown = true) +@JsonIgnoreProperties({"faas", "service", "span", "transaction"}) public class MetricsetJson { @Nullable diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetSerializer.java similarity index 96% rename from apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java rename to apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetSerializer.java index 63e44b6c82..a4f6d41ef1 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetGenerator.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/MetricSetSerializer.java @@ -20,8 +20,6 @@ import co.elastic.apm.agent.report.Reporter; import co.elastic.apm.agent.report.serialize.DslJsonSerializer; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; import com.dslplatform.json.BoolConverter; import com.dslplatform.json.DslJson; import com.dslplatform.json.JsonWriter; @@ -40,9 +38,7 @@ import static com.dslplatform.json.JsonWriter.OBJECT_END; import static com.dslplatform.json.JsonWriter.OBJECT_START; -class MetricSetGenerator { - - private static final Logger logger = LoggerFactory.getLogger(MetricSetGenerator.class); +class MetricSetSerializer { private static final byte NEW_LINE = '\n'; @@ -54,7 +50,7 @@ class MetricSetGenerator { private final JsonWriter jw; private boolean anySamplesWritten; - public MetricSetGenerator(Attributes attributes, CharSequence instrumentationScopeName, long epochMicros, StringBuilder replaceBuilder) { + public MetricSetSerializer(Attributes attributes, CharSequence instrumentationScopeName, long epochMicros, StringBuilder replaceBuilder) { this.replaceBuilder = replaceBuilder; anySamplesWritten = false; jw = DSL_JSON.newWriter(INITIAL_BUFFER_SIZE); diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java index 6336f16b7a..7cf8b62d70 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metrics-exporter/src/main/java/co/elastic/apm/agent/otelmetricexport/OtelMetricSerializer.java @@ -49,7 +49,7 @@ public class OtelMetricSerializer { private final Set metricsWithBadAggregations = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Map> metricSets; + private final Map> metricSets; @Nullable private InstrumentationScopeAndTimestamp lastCreatedInstrScopeAndTimestamp; @@ -108,7 +108,7 @@ private boolean isMetricDisabled(CharSequence name) { private void addHistogramValues(CharSequence name, CharSequence instrScopeName, HistogramData histogramData) { for (HistogramPointData histo : histogramData.getPoints()) { long timestampMicros = histo.getEpochNanos() / 1000L; - MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, histo.getAttributes()); + MetricSetSerializer metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, histo.getAttributes()); metricSet.addExplicitBucketHistogram(name, histo.getBoundaries(), histo.getCounts()); } } @@ -117,7 +117,7 @@ private void addDoubleValues(CharSequence name, CharSequence instrScopeName, Dat for (DoublePointData data : metricValues.getPoints()) { long timestampMicros = data.getEpochNanos() / 1000L; if (!omitZeroes || data.getValue() != 0) { - MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); + MetricSetSerializer metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); metricSet.addValue(name, data.getValue()); } } @@ -127,14 +127,14 @@ private void addLongValues(CharSequence name, CharSequence instrScopeName, Data< for (LongPointData data : metricValues.getPoints()) { if (!omitZeroes || data.getValue() != 0) { long timestampMicros = data.getEpochNanos() / 1000L; - MetricSetGenerator metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); + MetricSetSerializer metricSet = getOrCreateMetricSet(instrScopeName, timestampMicros, data.getAttributes()); metricSet.addValue(name, data.getValue()); } } } - private MetricSetGenerator getOrCreateMetricSet(CharSequence instrScopeName, long timestampMicros, Attributes attributes) { + private MetricSetSerializer getOrCreateMetricSet(CharSequence instrScopeName, long timestampMicros, Attributes attributes) { //This function is often called in a loop with the same instrumentation scope name and timestamp //In order to minimize allocations, we make use of this fact and remember the map key from the last iteration and reuse it if possible InstrumentationScopeAndTimestamp key; @@ -145,23 +145,23 @@ private MetricSetGenerator getOrCreateMetricSet(CharSequence instrScopeName, lon lastCreatedInstrScopeAndTimestamp = key; } - Map timestampMetricSets = metricSets.get(key); + Map timestampMetricSets = metricSets.get(key); if (timestampMetricSets == null) { timestampMetricSets = new HashMap<>(); metricSets.put(key, timestampMetricSets); } - MetricSetGenerator ms = timestampMetricSets.get(attributes); + MetricSetSerializer ms = timestampMetricSets.get(attributes); if (ms == null) { - ms = new MetricSetGenerator(attributes, key.instrumentationScopeName, key.timestamp, serializationTempBuilder); + ms = new MetricSetSerializer(attributes, key.instrumentationScopeName, key.timestamp, serializationTempBuilder); timestampMetricSets.put(attributes, ms); } return ms; } public void flushAndReset(Reporter reporter) { - for (Map map : metricSets.values()) { - for (MetricSetGenerator metricSet : map.values()) { + for (Map map : metricSets.values()) { + for (MetricSetSerializer metricSet : map.values()) { metricSet.finishAndReport(reporter); } } From 5e414a58af8eae8ae96e3b7b040f9d4be0d24a23 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Mon, 6 Feb 2023 15:43:58 +0100 Subject: [PATCH 7/9] Regenerated docs --- docs/configuration.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 1878b707e9..8e2433adee 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -2358,7 +2358,7 @@ This configuration option helps to make MessageListener type matching faster and [[config-dedot-custom-metrics]] ==== `dedot_custom_metrics` (added[1.22.0]) -Replaces dots with underscores in the metric names for custom metrics, such as Micrometer metrics. +Replaces dots with underscores in the metric names for Micrometer metrics. WARNING: Setting this to `false` can lead to mapping conflicts as dots indicate nesting in Elasticsearch. An example of when a conflict happens is two metrics with the name `foo` and `foo.bar`. @@ -2383,7 +2383,7 @@ The first metric maps `foo` to a number and the second metric maps `foo` as an o // This file is auto generated. Please make your changes in *Configuration.java (for example CoreConfiguration.java) and execute ConfigurationExporter [float] [[config-custom-metrics-histogram-boundaries]] -==== `custom_metrics_histogram_boundaries` (added[1.36.0] experimental) +==== `custom_metrics_histogram_boundaries` (added[1.37.0] experimental) NOTE: This feature is currently experimental, which means it is disabled by default and it is not guaranteed to be backwards compatible in future releases. @@ -4380,7 +4380,7 @@ Example: `5ms`. # Metrics # ############################################ -# Replaces dots with underscores in the metric names for custom metrics, such as Micrometer metrics. +# Replaces dots with underscores in the metric names for Micrometer metrics. # # WARNING: Setting this to `false` can lead to mapping conflicts as dots indicate nesting in Elasticsearch. # An example of when a conflict happens is two metrics with the name `foo` and `foo.bar`. From 0bd52f47b29236b2b3f0519d4a607e31f13dbc64 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 7 Feb 2023 12:46:01 +0100 Subject: [PATCH 8/9] Review fixes --- .../testutils/assertions/Assertions.java | 6 ++-- ...ricsetAssert.java => MetricSetAssert.java} | 22 +++++++------- ...EventJson.java => MetricSetEventJson.java} | 4 +-- ...{MetricsetJson.java => MetricSetJson.java} | 2 +- ...csetsAssert.java => MetricSetsAssert.java} | 30 +++++++++---------- ...dkMeterProviderBuilderInstrumentation.java | 4 ++- .../AbstractOtelMetricsTest.java | 15 +++++++--- 7 files changed, 46 insertions(+), 37 deletions(-) rename apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/{MetricsetAssert.java => MetricSetAssert.java} (87%) rename apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/{MetricsetEventJson.java => MetricSetEventJson.java} (92%) rename apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/{MetricsetJson.java => MetricSetJson.java} (97%) rename apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/{MetricsetsAssert.java => MetricSetsAssert.java} (74%) diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java index 7bac1ed940..ac42d7b53b 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/Assertions.java @@ -23,7 +23,7 @@ import co.elastic.apm.agent.impl.context.ServiceTarget; import co.elastic.apm.agent.impl.transaction.AbstractSpan; import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.testutils.assertions.metrics.MetricsetsAssert; +import co.elastic.apm.agent.testutils.assertions.metrics.MetricSetsAssert; import java.util.Collection; @@ -52,7 +52,7 @@ public static DbAssert assertThat(Db db) { return new AbstractSpanAssert<>(span, AbstractSpanAssert.class); } - public static MetricsetsAssert assertThatMetricSets(Collection metricsetsJson) { - return new MetricsetsAssert(metricsetsJson); + public static MetricSetsAssert assertThatMetricSets(Collection metricsetsJson) { + return new MetricSetsAssert(metricsetsJson); } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetAssert.java similarity index 87% rename from apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java rename to apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetAssert.java index e93991ba81..f7682df4f7 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetAssert.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetAssert.java @@ -31,13 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat; -public class MetricsetAssert extends BaseAssert { +public class MetricSetAssert extends BaseAssert { - protected MetricsetAssert(MetricsetJson metricSet) { - super(metricSet, MetricsetAssert.class); + protected MetricSetAssert(MetricSetJson metricSet) { + super(metricSet, MetricSetAssert.class); } - public MetricsetAssert hasMetricsCount(int expected) { + public MetricSetAssert hasMetricsCount(int expected) { int count = Optional.ofNullable(actual.samples).map(Map::size).orElse(0); if (count != expected) { failWithMessage("Expected metricset to contain %d metrics but contained %d.", expected, count); @@ -45,17 +45,17 @@ public MetricsetAssert hasMetricsCount(int expected) { return this; } - public MetricsetAssert containsMetric(String name) { + public MetricSetAssert containsMetric(String name) { extractMetric(name); return this; } - public MetricsetAssert metricSatisfies(String name, Consumer assertions) { + public MetricSetAssert metricSatisfies(String name, Consumer assertions) { assertions.accept(extractMetric(name)); return this; } - public MetricsetAssert containsValueMetric(String name, Number expected) { + public MetricSetAssert containsValueMetric(String name, Number expected) { MetricJson metric = extractMetric(name); if (!Objects.equals(metric.value, expected)) { failWithMessage("Expected metric '%s' to have metric value '%s' but was '%s'", name, expected, metric.value); @@ -69,7 +69,7 @@ public MetricsetAssert containsValueMetric(String name, Number expected) { return this; } - public MetricsetAssert containsHistogramMetric(String name, Collection expectedValues, Collection expectedCounts) { + public MetricSetAssert containsHistogramMetric(String name, Collection expectedValues, Collection expectedCounts) { MetricJson metric = extractMetric(name); if (!Objects.equals(metric.values, expectedValues)) { failWithMessage("Expected metric '%s' to have histogram values '%s' but was '%s'", name, expectedValues, metric.values); @@ -86,14 +86,14 @@ public MetricsetAssert containsHistogramMetric(String name, Collection e return this; } - public MetricsetAssert containsMetrics(String... names) { + public MetricSetAssert containsMetrics(String... names) { for (String name : names) { containsMetric(name); } return this; } - public MetricsetAssert containsExactlyMetrics(String... names) { + public MetricSetAssert containsExactlyMetrics(String... names) { if (actual.samples == null) { if (names.length > 0) { failWithMessage("Expected metricset to contain metrics %s but 'samples' was null", Arrays.toString(names)); @@ -124,7 +124,7 @@ private MetricJson extractMetric(String name) { return metric; } - public MetricsetAssert hasTimestampInRange(long min, long max) { + public MetricSetAssert hasTimestampInRange(long min, long max) { assertThat(actual.timestamp).isBetween(min, max); return this; } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetEventJson.java similarity index 92% rename from apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java rename to apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetEventJson.java index 0616151d31..23fe5131b9 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetEventJson.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetEventJson.java @@ -18,7 +18,7 @@ */ package co.elastic.apm.agent.testutils.assertions.metrics; -public class MetricsetEventJson { +public class MetricSetEventJson { - public MetricsetJson metricset; + public MetricSetJson metricset; } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetJson.java similarity index 97% rename from apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java rename to apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetJson.java index 8965ce99fd..f086e14908 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetJson.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetJson.java @@ -26,7 +26,7 @@ import java.util.Map; @JsonIgnoreProperties({"faas", "service", "span", "transaction"}) -public class MetricsetJson { +public class MetricSetJson { @Nullable public Map samples; diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetsAssert.java similarity index 74% rename from apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java rename to apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetsAssert.java index 33a3254b78..04e76da03f 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricsetsAssert.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/testutils/assertions/metrics/MetricSetsAssert.java @@ -32,19 +32,19 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class MetricsetsAssert extends BaseAssert> { +public class MetricSetsAssert extends BaseAssert> { static final ObjectMapper jsonMapper = new ObjectMapper(); - public MetricsetsAssert(Collection serializedMetricSets) { + public MetricSetsAssert(Collection serializedMetricSets) { this(deserializeMetricSets(serializedMetricSets)); } - private static List deserializeMetricSets(Collection serializedMetricSets) { + private static List deserializeMetricSets(Collection serializedMetricSets) { return serializedMetricSets.stream() .map(bytes -> { try { - MetricsetJson metricset = jsonMapper.readValue(bytes, MetricsetEventJson.class).metricset; + MetricSetJson metricset = jsonMapper.readValue(bytes, MetricSetEventJson.class).metricset; metricset.json = new String(bytes).replace("\n", ""); return metricset; } catch (IOException e) { @@ -54,40 +54,40 @@ private static List deserializeMetricSets(Collection seri .collect(Collectors.toList()); } - protected MetricsetsAssert(List metricSets) { - super(metricSets, MetricsetsAssert.class); + protected MetricSetsAssert(List metricSets) { + super(metricSets, MetricSetsAssert.class); } - public MetricsetsAssert hasMetricsetWithLabelsSatisfying(String key, Object value, Consumer assertions) { + public MetricSetsAssert hasMetricsetWithLabelsSatisfying(String key, Object value, Consumer assertions) { return hasMetricsetWithLabelsSatisfying(Collections.singletonMap(key, value), assertions); } - public MetricsetsAssert hasMetricsetWithLabelsSatisfying(String key1, Object value1, String key2, Object value2, Consumer assertions) { + public MetricSetsAssert hasMetricsetWithLabelsSatisfying(String key1, Object value1, String key2, Object value2, Consumer assertions) { return hasMetricsetWithLabelsSatisfying(Map.of(key1, value1, key2, value2), assertions); } - public MetricsetAssert first() { + public MetricSetAssert first() { if (actual.isEmpty()) { failWithMessage("Expected at least one metricset but none was found"); } - return new MetricsetAssert(actual.get(0)); + return new MetricSetAssert(actual.get(0)); } - public MetricsetsAssert hasMetricsetWithLabelsSatisfying(Map labels, Consumer assertions) { - assertions.accept(new MetricsetAssert(extractMetricset(labels))); + public MetricSetsAssert hasMetricsetWithLabelsSatisfying(Map labels, Consumer assertions) { + assertions.accept(new MetricSetAssert(extractMetricset(labels))); return this; } - public MetricsetsAssert hasMetricsetCount(int expected) { + public MetricSetsAssert hasMetricsetCount(int expected) { if (actual.size() != expected) { failWithMessage("Excpected to contain %d metricsets but actually was %d. Contained metricsets:\n%s", expected, actual.size(), getAllMetricsetsAsString()); } return this; } - private MetricsetJson extractMetricset(Map labels) { - Optional ms = actual.stream().filter(ms2 -> Objects.equals(ms2.tags, labels)).findFirst(); + private MetricSetJson extractMetricset(Map labels) { + Optional ms = actual.stream().filter(ms2 -> Objects.equals(ms2.tags, labels)).findFirst(); if (ms.isEmpty()) { String allLabels = getAllLabels(); failWithMessage("Expected metricset with labels %s to exist but was not found in %d metricsets. Found metricset labels are:\n%s", labels, actual.size(), allLabels); diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java index 79d5c53560..8bf744349a 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/main/java/co/elastic/apm/agent/otelmetricsdk/SdkMeterProviderBuilderInstrumentation.java @@ -26,6 +26,7 @@ import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent; import co.elastic.apm.agent.sdk.weakconcurrent.WeakSet; +import co.elastic.apm.agent.util.LoggerUtils; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.export.MetricExporter; import net.bytebuddy.asm.Advice; @@ -65,6 +66,7 @@ public String getAdviceClassName() { public static class SdkMeterProviderBuilderAdvice { private static final Logger logger = LoggerFactory.getLogger(SdkMeterProviderBuilderInstrumentation.SdkMeterProviderBuilderAdvice.class); + private static final Logger unsupportedVersionLogger = LoggerUtils.logOnce(logger); private static final WeakSet ALREADY_REGISTERED_BUILDERS = WeakConcurrent.buildSet(); private static final ElasticApmTracer tracer = GlobalTracer.requireTracerImpl(); @@ -86,7 +88,7 @@ private static boolean checkMetricsSdkVersionSupported() { MetricExporter.class.getMethod("getDefaultAggregation", instrumentTypeClass); return true; } catch (ClassNotFoundException | NoSuchMethodException e) { - logger.warn("Detected OpenTelemetry metrics SDK instance with a version older than 1.16.0. Skipping instrumentation because it is not supported."); + unsupportedVersionLogger.warn("Detected OpenTelemetry metrics SDK instance with a version older than 1.16.0. Skipping instrumentation because it is not supported."); return false; } } diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java index 7c3df257dc..3c046b7b0b 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-metricsdk-plugin/src/test/java/co/elastic/apm/agent/otelmetricsdk/AbstractOtelMetricsTest.java @@ -45,8 +45,10 @@ import org.junit.jupiter.api.Test; import javax.annotation.Nullable; +import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat; @@ -160,15 +162,20 @@ public void testTimestampPresent() { Meter testMeter = createMeter("test"); LongCounter counter = testMeter.counterBuilder("my_counter").build(); - long timestampMicros = System.currentTimeMillis() * 1000L; - counter.add(42); + Instant instantBefore = Instant.now(); + long microsBefore = TimeUnit.SECONDS.toMicros(instantBefore.getEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(instantBefore.getNano()); + counter.add(42); resetReporterAndFlushMetrics(); + + Instant instantAfter = Instant.now(); + long microsAfter = TimeUnit.SECONDS.toMicros(instantAfter.getEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(instantAfter.getNano()); + assertThatMetricSets(reporter.getBytes()) .hasMetricsetCount(1) .first() - //check for a range of +- 1 second to be safe against CI slowness / clock inaccuracy - .hasTimestampInRange(timestampMicros - 1_000_000L, timestampMicros + 1_000_000L); + //check for a slightly bigger range due to potential clock differences + .hasTimestampInRange(microsBefore - 10_000L, microsAfter + 10_000L); } @Test From a00b86964ccdde26c7c29f33b0651d76134b6e00 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Wed, 8 Feb 2023 10:10:37 +0100 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index b350bc2f2f..79eead5859 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -28,6 +28,7 @@ endif::[] * Add the <> config option to disable injection of `tracecontext` on outgoing communication - {pull}2996[#2996] * Add the <> config option to suppress async profiler warning messages - {pull}3002[#3002] +* Added support for OpenTelemetry metrics - {pull}2968[#2968] [float] ===== Bug fixes