diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml new file mode 100644 index 000000000..590c12946 --- /dev/null +++ b/.github/workflows/acceptance-tests.yml @@ -0,0 +1,41 @@ +name: OpenTelemetry Acceptance Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + acceptance-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check out oats + uses: actions/checkout@v4 + with: + repository: grafana/oats + ref: 7cd5ca42fff009fd67ea986d42c79134ac99ae48 + path: oats + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: 'maven' + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache-dependency-path: oats/go.sum + - name: Run the Maven verify phase + run: | + ./mvnw clean install -DskipTests + - name: Run acceptance tests + run: ./scripts/run-acceptance-tests.sh + - name: upload log file + uses: actions/upload-artifact@v4 + if: failure() + with: + name: docker-compose.log + path: oats/yaml/build/**/*.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9e3d5779..79cd4b6ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,8 @@ This repository uses [Google Java Format](https://github.com/google/google-java- Run `./mvnw spotless:apply` to format the code (only changed files) before committing. +Use `-Dspotless.check.skip=true` to skip the formatting check during development. + ## Running Tests If you're getting errors when running tests: diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 4b7d53b79..99c4ecee8 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -22,6 +22,18 @@ 3.0.2 + + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom-alpha + ${otel.instrumentation.version} + pom + import + + + + org.openjdk.jmh @@ -58,17 +70,14 @@ io.opentelemetry opentelemetry-api - ${otel.version} io.opentelemetry opentelemetry-sdk - ${otel.version} io.opentelemetry opentelemetry-sdk-testing - ${otel.version} diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile new file mode 100644 index 000000000..23f980005 --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:21-jre + +COPY target/example-exporter-opentelemetry.jar ./app.jar +# check that the resource attributs from the agent are used, epsecially the service.instance.id should be the same +ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.8.0/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar +ENV JAVA_TOOL_OPTIONS=-javaagent:/usr/src/app/opentelemetry-javaagent.jar + +#ENTRYPOINT [ "java", "-Dotel.javaagent.debug=true","-jar", "./app.jar" ] # for debugging +ENTRYPOINT [ "java", "-jar", "./app.jar" ] diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/docker-compose.yml b/examples/example-exporter-opentelemetry/oats-tests/agent/docker-compose.yml new file mode 100644 index 000000000..9c0fc66fb --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/agent/docker-compose.yml @@ -0,0 +1,13 @@ +# OATS is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats/tree/main/yaml +version: '3.4' + +services: + java: + build: + context: ../.. + dockerfile: oats-tests/agent/Dockerfile + environment: + OTEL_SERVICE_NAME: "rolldice" + OTEL_EXPORTER_OTLP_ENDPOINT: http://lgtm:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_METRIC_EXPORT_INTERVAL: "5000" # so we don't have to wait 60s for metrics diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml new file mode 100644 index 000000000..08e4d8d3f --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml @@ -0,0 +1,12 @@ +# OATS is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats/tree/main/yaml +docker-compose: + generator: docker-lgtm + files: + - ./docker-compose.yml +expected: + custom-checks: + - script: ./service-instance-id-check.py + metrics: + - promql: 'uptime_seconds_total{}' + value: '>= 0' + diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/service-instance-id-check.py b/examples/example-exporter-opentelemetry/oats-tests/agent/service-instance-id-check.py new file mode 100755 index 000000000..196c629c9 --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/agent/service-instance-id-check.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# This script is used to check if the service instance id is present in the exported data +# The script will return 0 if the service instance id is present in the exported data + +from urllib.request import urlopen +import urllib.parse +import json + +url = ' http://localhost:9090/api/v1/label/instance/values' +res = json.loads(urlopen(url).read().decode('utf-8')) + +values = list(res['data']) +print(values) + +if "localhost:8888" in values: + values.remove("localhost:8888") + +# both the agent and the exporter should report the same instance id +assert len(values) == 1 + +path = 'target_info{instance="%s"}' % values[0] +path = urllib.parse.quote_plus(path) +url = 'http://localhost:9090/api/v1/query?query=%s' % path +res = json.loads(urlopen(url).read().decode('utf-8')) + +infos = res['data']['result'] +print(infos) + +# they should not have the same target info +# e.g. only the agent has telemetry_distro_name +assert len(infos) == 2 + diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile new file mode 100644 index 000000000..22191f07e --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre + +COPY target/example-exporter-opentelemetry.jar ./app.jar + +ENTRYPOINT [ "java", "-jar", "./app.jar" ] diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/docker-compose.yml b/examples/example-exporter-opentelemetry/oats-tests/http/docker-compose.yml new file mode 100644 index 000000000..cec15bf19 --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/http/docker-compose.yml @@ -0,0 +1,13 @@ +# OATS is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats/tree/main/yaml +version: '3.4' + +services: + java: + build: + context: ../.. + dockerfile: oats-tests/http/Dockerfile + environment: + OTEL_SERVICE_NAME: "rolldice" + OTEL_EXPORTER_OTLP_ENDPOINT: http://lgtm:4318 + OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf + OTEL_METRIC_EXPORT_INTERVAL: "5000" # so we don't have to wait 60s for metrics diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml new file mode 100644 index 000000000..a3af9ffc2 --- /dev/null +++ b/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml @@ -0,0 +1,10 @@ +# OATS is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats/tree/main/yaml +docker-compose: + generator: docker-lgtm + files: + - ./docker-compose.yml +expected: + metrics: + - promql: 'uptime_seconds_total{}' + value: '>= 0' + diff --git a/examples/example-exporter-opentelemetry/src/main/java/io/prometheus/metrics/examples/opentelemetry/Main.java b/examples/example-exporter-opentelemetry/src/main/java/io/prometheus/metrics/examples/opentelemetry/Main.java index defe85074..b1aa440b1 100644 --- a/examples/example-exporter-opentelemetry/src/main/java/io/prometheus/metrics/examples/opentelemetry/Main.java +++ b/examples/example-exporter-opentelemetry/src/main/java/io/prometheus/metrics/examples/opentelemetry/Main.java @@ -9,6 +9,7 @@ public class Main { public static void main(String[] args) throws Exception { + System.out.println("Starting example application"); // Note: Some JVM metrics are also defined as OpenTelemetry's semantic conventions. // We have plans to implement a configuration option for JvmMetrics to use OpenTelemetry @@ -34,6 +35,7 @@ public static void main(String[] args) throws Exception { while (true) { Thread.sleep(1000); + System.out.println("Incrementing counter"); counter.inc(); } } diff --git a/otel-agent-resources/pom.xml b/otel-agent-resources/pom.xml new file mode 100644 index 000000000..176c2c6eb --- /dev/null +++ b/otel-agent-resources/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + + io.prometheus + client_java + 0.0.1-releasetest1 + + + otel-agent-resources + bundle + + OpenTelemetry Agent Resource Extractor + + Reads the OpenTelemetry Agent resources the GlobalOpenTelemetry instance + + + + io.prometheus.otel.resource.attributes + + 1.29.0 + + + + + + + + + src/main/resources + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + validate + + copy + + + + + + + io.opentelemetry + opentelemetry-api + ${otel-dynamic-load.version} + ${project.basedir}/src/main/resources/lib/ + + + io.opentelemetry + opentelemetry-context + ${otel-dynamic-load.version} + ${project.basedir}/src/main/resources/lib/ + + + + + + + diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromOtelAgent.java b/otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java similarity index 88% rename from prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromOtelAgent.java rename to otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java index ce6baafe2..215b79de5 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromOtelAgent.java +++ b/otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java @@ -1,4 +1,4 @@ -package io.prometheus.metrics.exporter.opentelemetry; +package io.prometheus.otelagent; import static java.nio.file.Files.createTempDirectory; @@ -11,6 +11,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; public class ResourceAttributesFromOtelAgent { @@ -31,8 +33,11 @@ public class ResourceAttributesFromOtelAgent { * *

After that we discard the class loader so that all OTel specific classes are unloaded. No * runtime dependency on any OTel version remains. + * + *

The test for this class is in + * examples/example-exporter-opentelemetry/oats-tests/agent/service-instance-id-check.py */ - public static void addIfAbsent(Map result, String instrumentationScopeName) { + public static Map getResourceAttributes(String instrumentationScopeName) { try { Path tmpDir = createTempDirectory(instrumentationScopeName + "-"); try { @@ -43,7 +48,7 @@ public static void addIfAbsent(Map result, String instrumentatio classLoader.loadClass("io.opentelemetry.api.GlobalOpenTelemetry"); Object globalOpenTelemetry = globalOpenTelemetryClass.getMethod("get").invoke(null); if (globalOpenTelemetry.getClass().getSimpleName().contains("ApplicationOpenTelemetry")) { - // GlobalOpenTelemetry is injected by the OTel Java aqent + // GlobalOpenTelemetry is injected by the OTel Java agent Object applicationMeterProvider = callMethod("getMeterProvider", globalOpenTelemetry); Object agentMeterProvider = getField("agentMeterProvider", applicationMeterProvider); Object sdkMeterProvider = getField("delegate", agentMeterProvider); @@ -52,11 +57,13 @@ public static void addIfAbsent(Map result, String instrumentatio Object attributes = callMethod("getAttributes", resource); Map attributeMap = (Map) callMethod("asMap", attributes); + Map result = new HashMap<>(); for (Map.Entry entry : attributeMap.entrySet()) { if (entry.getKey() != null && entry.getValue() != null) { - result.putIfAbsent(entry.getKey().toString(), entry.getValue().toString()); + result.put(entry.getKey().toString(), entry.getValue().toString()); } } + return Collections.unmodifiableMap(result); } } } finally { @@ -65,6 +72,7 @@ public static void addIfAbsent(Map result, String instrumentatio } catch (Exception ignored) { // ignore } + return Collections.emptyMap(); } private static Object getField(String name, Object obj) throws Exception { diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/resources/lib/.gitignore b/otel-agent-resources/src/main/resources/lib/.gitignore similarity index 100% rename from prometheus-metrics-exporter-opentelemetry/src/main/resources/lib/.gitignore rename to otel-agent-resources/src/main/resources/lib/.gitignore diff --git a/pom.xml b/pom.xml index 4b62d968f..273f97881 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ UTF-8 --module-name-need-to-be-overriden-- 5.11.2 - 1.42.1 + 2.8.0-alpha 8 @@ -73,6 +73,7 @@ prometheus-metrics-instrumentation-dropwizard5 prometheus-metrics-instrumentation-guava prometheus-metrics-simpleclient-bridge + otel-agent-resources @@ -100,6 +101,12 @@ 3.26.3 test + + org.slf4j + slf4j-simple + 1.7.36 + test + @@ -236,7 +243,7 @@ ${java.version} true - -Xlint:all + -Xlint:all,-serial,-processing -Werror -XDcompilePolicy=simple diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java index b8498d6c5..d009d2603 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java @@ -8,22 +8,23 @@ public class ExporterOpenTelemetryProperties { // See // https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md - private static String PROTOCOL = "protocol"; // otel.exporter.otlp.protocol - private static String ENDPOINT = "endpoint"; // otel.exporter.otlp.endpoint - private static String HEADERS = "headers"; // otel.exporter.otlp.headers - private static String INTERVAL_SECONDS = "intervalSeconds"; // otel.metric.export.interval - private static String TIMEOUT_SECONDS = "timeoutSeconds"; // otel.exporter.otlp.timeout - private static String SERVICE_NAME = "serviceName"; // otel.service.name - private static String SERVICE_NAMESPACE = "serviceNamespace"; - private static String SERVICE_INSTANCE_ID = "serviceInstanceId"; - private static String SERVICE_VERSION = "serviceVersion"; - private static String RESOURCE_ATTRIBUTES = "resourceAttributes"; // otel.resource.attributes + private static final String PROTOCOL = "protocol"; // otel.exporter.otlp.protocol + private static final String ENDPOINT = "endpoint"; // otel.exporter.otlp.endpoint + private static final String HEADERS = "headers"; // otel.exporter.otlp.headers + private static final String INTERVAL_SECONDS = "intervalSeconds"; // otel.metric.export.interval + private static final String TIMEOUT_SECONDS = "timeoutSeconds"; // otel.exporter.otlp.timeout + private static final String SERVICE_NAME = "serviceName"; // otel.service.name + private static final String SERVICE_NAMESPACE = "serviceNamespace"; + private static final String SERVICE_INSTANCE_ID = "serviceInstanceId"; + private static final String SERVICE_VERSION = "serviceVersion"; + private static final String RESOURCE_ATTRIBUTES = + "resourceAttributes"; // otel.resource.attributes private final String protocol; private final String endpoint; private final Map headers; - private final Integer intervalSeconds; - private final Integer timeoutSeconds; + private final String interval; + private final String timeout; private final String serviceName; private final String serviceNamespace; private final String serviceInstanceId; @@ -34,8 +35,8 @@ private ExporterOpenTelemetryProperties( String protocol, String endpoint, Map headers, - Integer intervalSeconds, - Integer timeoutSeconds, + String interval, + String timeout, String serviceName, String serviceNamespace, String serviceInstanceId, @@ -44,8 +45,8 @@ private ExporterOpenTelemetryProperties( this.protocol = protocol; this.endpoint = endpoint; this.headers = headers; - this.intervalSeconds = intervalSeconds; - this.timeoutSeconds = timeoutSeconds; + this.interval = interval; + this.timeout = timeout; this.serviceName = serviceName; this.serviceNamespace = serviceNamespace; this.serviceInstanceId = serviceInstanceId; @@ -65,12 +66,12 @@ public Map getHeaders() { return headers; } - public Integer getIntervalSeconds() { - return intervalSeconds; + public String getInterval() { + return interval; } - public Integer getTimeoutSeconds() { - return timeoutSeconds; + public String getTimeout() { + return timeout; } public String getServiceName() { @@ -102,27 +103,20 @@ static ExporterOpenTelemetryProperties load(String prefix, Map p String protocol = Util.loadString(prefix + "." + PROTOCOL, properties); String endpoint = Util.loadString(prefix + "." + ENDPOINT, properties); Map headers = Util.loadMap(prefix + "." + HEADERS, properties); - Integer intervalSeconds = Util.loadInteger(prefix + "." + INTERVAL_SECONDS, properties); - Integer timeoutSeconds = Util.loadInteger(prefix + "." + TIMEOUT_SECONDS, properties); + String interval = Util.loadStringAddSuffix(prefix + "." + INTERVAL_SECONDS, properties, "s"); + String timeout = Util.loadStringAddSuffix(prefix + "." + TIMEOUT_SECONDS, properties, "s"); String serviceName = Util.loadString(prefix + "." + SERVICE_NAME, properties); String serviceNamespace = Util.loadString(prefix + "." + SERVICE_NAMESPACE, properties); String serviceInstanceId = Util.loadString(prefix + "." + SERVICE_INSTANCE_ID, properties); String serviceVersion = Util.loadString(prefix + "." + SERVICE_VERSION, properties); Map resourceAttributes = Util.loadMap(prefix + "." + RESOURCE_ATTRIBUTES, properties); - Util.assertValue(intervalSeconds, t -> t > 0, "Expecting value > 0", prefix, INTERVAL_SECONDS); - Util.assertValue(timeoutSeconds, t -> t > 0, "Expecting value > 0", prefix, TIMEOUT_SECONDS); - if (protocol != null && !protocol.equals("grpc") && !protocol.equals("http/protobuf")) { - throw new PrometheusPropertiesException( - protocol - + ": Unsupported OpenTelemetry exporter protocol. Expecting grpc or http/protobuf"); - } return new ExporterOpenTelemetryProperties( protocol, endpoint, headers, - intervalSeconds, - timeoutSeconds, + interval, + timeout, serviceName, serviceNamespace, serviceInstanceId, @@ -138,14 +132,14 @@ public static class Builder { private String protocol; private String endpoint; - private Map headers = new HashMap<>(); - private Integer intervalSeconds; - private Integer timeoutSeconds; + private final Map headers = new HashMap<>(); + private String interval; + private String timeout; private String serviceName; private String serviceNamespace; private String serviceInstanceId; private String serviceVersion; - private Map resourceAttributes = new HashMap<>(); + private final Map resourceAttributes = new HashMap<>(); private Builder() {} @@ -173,7 +167,7 @@ public Builder intervalSeconds(int intervalSeconds) { if (intervalSeconds <= 0) { throw new IllegalArgumentException(intervalSeconds + ": Expecting intervalSeconds > 0"); } - this.intervalSeconds = intervalSeconds; + this.interval = intervalSeconds + "s"; return this; } @@ -181,7 +175,7 @@ public Builder timeoutSeconds(int timeoutSeconds) { if (timeoutSeconds <= 0) { throw new IllegalArgumentException(timeoutSeconds + ": Expecting timeoutSeconds > 0"); } - this.timeoutSeconds = timeoutSeconds; + this.timeout = timeoutSeconds + "s"; return this; } @@ -215,8 +209,8 @@ public ExporterOpenTelemetryProperties build() { protocol, endpoint, headers, - intervalSeconds, - timeoutSeconds, + interval, + timeout, serviceName, serviceNamespace, serviceInstanceId, diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java index 8adcd0af7..15a61f7fe 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java @@ -9,7 +9,7 @@ class Util { - private static String getProperty(String name, Map properties) { + static String getProperty(String name, Map properties) { Object object = properties.remove(name); if (object != null) { return object.toString(); @@ -46,6 +46,14 @@ static String loadString(String name, Map properties) return getProperty(name, properties); } + static String loadStringAddSuffix(String name, Map properties, String suffix) { + Object object = properties.remove(name); + if (object != null) { + return object + suffix; + } + return null; + } + static List loadStringList(String name, Map properties) throws PrometheusPropertiesException { String property = getProperty(name, properties); @@ -87,7 +95,7 @@ static Map loadMap(String name, Map properties) String[] pairs = property.split(","); for (String pair : pairs) { if (pair.contains("=")) { - String[] keyValue = pair.split("=", 1); + String[] keyValue = pair.split("=", 2); if (keyValue.length == 2) { String key = keyValue[0].trim(); String value = keyValue[1].trim(); diff --git a/prometheus-metrics-exporter-opentelemetry/pom.xml b/prometheus-metrics-exporter-opentelemetry/pom.xml index 3e293ad39..d9ecaaa1c 100644 --- a/prometheus-metrics-exporter-opentelemetry/pom.xml +++ b/prometheus-metrics-exporter-opentelemetry/pom.xml @@ -21,26 +21,52 @@ io.prometheus.metrics.exporter.opentelemetry + + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom-alpha + ${otel.instrumentation.version} + pom + import + + + + io.prometheus prometheus-metrics-core ${project.version} + + io.prometheus + otel-agent-resources + ${project.version} + io.opentelemetry opentelemetry-api - ${otel.version} io.opentelemetry opentelemetry-sdk - ${otel.version} io.opentelemetry opentelemetry-exporter-otlp - ${otel.version} + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + + + io.opentelemetry + opentelemetry-sdk-extension-incubator + + + io.opentelemetry.instrumentation + opentelemetry-resources @@ -68,19 +94,10 @@ 1.7.1-alpha test - - io.opentelemetry - opentelemetry-sdk-trace - ${otel.version} - test - - - src/main/resources - src/main/resources-filtered true @@ -98,9 +115,9 @@ regex-property - otel.string-version - ${otel.version} - \. + otel.instrumentation.string-version + ${otel.instrumentation.version} + [\.-] _ true @@ -120,6 +137,8 @@ io.opentelemetry:* + io.opentelemetry.semconv:* + io.opentelemetry.instrumentation:* com.squareup.*:* org.jetbrains:* org.jetbrains.*:* @@ -129,37 +148,49 @@ io.opentelemetry - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version} + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version} + + + + io.opentelemetry.instrumentation + + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.instrumentation + + + + io.opentelemetry.semconv + + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.semconv okhttp3 - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version}.okhttp3 + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.okhttp3 kotlin - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version}.kotlin + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.kotlin org.intellij - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version}.org.intellij + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.org.intellij org.jetbrains - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version}.org.jetbrains + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.org.jetbrains okio - io.prometheus.metrics.shaded.io_opentelemetry_${otel.string-version}.okio + io.prometheus.metrics.shaded.io_opentelemetry_${otel.instrumentation.string-version}.okio diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java index 82f503ac9..a00b2ef85 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java @@ -1,62 +1,16 @@ package io.prometheus.metrics.exporter.opentelemetry; -import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; -import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.ResourceBuilder; -import io.prometheus.metrics.config.ExporterOpenTelemetryProperties; +import io.opentelemetry.sdk.metrics.export.MetricReader; import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.model.registry.PrometheusRegistry; -import java.time.Duration; import java.util.HashMap; -import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; public class OpenTelemetryExporter implements AutoCloseable { - private final PeriodicMetricReader reader; + private final MetricReader reader; - private OpenTelemetryExporter( - Builder builder, PrometheusProperties config, PrometheusRegistry registry) { - InstrumentationScopeInfo instrumentationScopeInfo = - PrometheusInstrumentationScope.loadInstrumentationScopeInfo(); - ExporterOpenTelemetryProperties properties = config.getExporterOpenTelemetryProperties(); - Resource resource = initResourceAttributes(builder, properties, instrumentationScopeInfo); - MetricExporter exporter; - if (ConfigHelper.getProtocol(builder, properties).equals("grpc")) { - OtlpGrpcMetricExporterBuilder exporterBuilder = - OtlpGrpcMetricExporter.builder() - .setTimeout(Duration.ofSeconds(ConfigHelper.getTimeoutSeconds(builder, properties))) - .setEndpoint(ConfigHelper.getEndpoint(builder, properties)); - for (Map.Entry header : - ConfigHelper.getHeaders(builder, properties).entrySet()) { - exporterBuilder.addHeader(header.getKey(), header.getValue()); - } - exporter = exporterBuilder.build(); - } else { - OtlpHttpMetricExporterBuilder exporterBuilder = - OtlpHttpMetricExporter.builder() - .setTimeout(Duration.ofSeconds(ConfigHelper.getTimeoutSeconds(builder, properties))) - .setEndpoint(ConfigHelper.getEndpoint(builder, properties)); - for (Map.Entry header : - ConfigHelper.getHeaders(builder, properties).entrySet()) { - exporterBuilder.addHeader(header.getKey(), header.getValue()); - } - exporter = exporterBuilder.build(); - } - reader = - PeriodicMetricReader.builder(exporter) - .setInterval(Duration.ofSeconds(ConfigHelper.getIntervalSeconds(builder, properties))) - .build(); - - PrometheusMetricProducer prometheusMetricProducer = - new PrometheusMetricProducer(registry, instrumentationScopeInfo, resource); - reader.register(prometheusMetricProducer); + public OpenTelemetryExporter(MetricReader reader) { + this.reader = reader; } @Override @@ -64,29 +18,6 @@ public void close() { reader.shutdown(); } - private Resource initResourceAttributes( - Builder builder, - ExporterOpenTelemetryProperties properties, - InstrumentationScopeInfo instrumentationScopeInfo) { - String serviceName = ConfigHelper.getServiceName(builder, properties); - String serviceNamespace = ConfigHelper.getServiceNamespace(builder, properties); - String serviceInstanceId = ConfigHelper.getServiceInstanceId(builder, properties); - String serviceVersion = ConfigHelper.getServiceVersion(builder, properties); - Map resourceAttributes = - ResourceAttributes.get( - instrumentationScopeInfo.getName(), - serviceName, - serviceNamespace, - serviceInstanceId, - serviceVersion, - ConfigHelper.getResourceAttributes(builder, properties)); - ResourceBuilder resourceBuilder = Resource.builder(); - for (Map.Entry entry : resourceAttributes.entrySet()) { - resourceBuilder.put(entry.getKey(), entry.getValue()); - } - return resourceBuilder.build(); - } - public static Builder builder() { return new Builder(PrometheusProperties.get()); } @@ -99,16 +30,16 @@ public static class Builder { private final PrometheusProperties config; private PrometheusRegistry registry = null; - private String protocol; - private String endpoint; - private final Map headers = new HashMap<>(); - private Integer intervalSeconds; - private Integer timeoutSeconds; - private String serviceName; - private String serviceNamespace; - private String serviceInstanceId; - private String serviceVersion; - private final Map resourceAttributes = new HashMap<>(); + String protocol; + String endpoint; + final Map headers = new HashMap<>(); + String interval; + String timeout; + String serviceName; + String serviceNamespace; + String serviceInstanceId; + String serviceVersion; + final Map resourceAttributes = new HashMap<>(); private Builder(PrometheusProperties config) { this.config = config; @@ -181,7 +112,7 @@ public Builder intervalSeconds(int intervalSeconds) { if (intervalSeconds <= 0) { throw new IllegalStateException(intervalSeconds + ": expecting a push interval > 0s"); } - this.intervalSeconds = intervalSeconds; + this.interval = intervalSeconds + "s"; return this; } @@ -196,7 +127,7 @@ public Builder timeoutSeconds(int timeoutSeconds) { if (timeoutSeconds <= 0) { throw new IllegalStateException(timeoutSeconds + ": expecting a push interval > 0s"); } - this.timeoutSeconds = timeoutSeconds; + this.timeout = timeoutSeconds + "s"; return this; } @@ -266,241 +197,7 @@ public OpenTelemetryExporter buildAndStart() { if (registry == null) { registry = PrometheusRegistry.defaultRegistry; } - return new OpenTelemetryExporter(this, config, registry); - } - } - - private static class ConfigHelper { - - private static String getProtocol( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String protocol = config.getProtocol(); - if (protocol != null) { - return protocol; - } - protocol = getString("otel.exporter.otlp.protocol"); - if (protocol != null) { - if (!protocol.equals("grpc") && !protocol.equals("http/protobuf")) { - throw new IllegalStateException( - protocol - + ": Unsupported OpenTelemetry exporter protocol. Expecting grpc or http/protobuf."); - } - return protocol; - } - if (builder.protocol != null) { - return builder.protocol; - } - return "grpc"; - } - - private static String getEndpoint( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String endpoint = config.getEndpoint(); - if (endpoint == null) { - endpoint = getString("otel.exporter.otlp.metrics.endpoint"); - } - if (endpoint == null) { - endpoint = getString("otel.exporter.otlp.endpoint"); - } - if (endpoint == null) { - endpoint = builder.endpoint; - } - if (endpoint == null) { - if (getProtocol(builder, config).equals("grpc")) { - endpoint = "http://localhost:4317"; - } else { // http/protobuf - endpoint = "http://localhost:4318/v1/metrics"; - } - } - if (getProtocol(builder, config).equals("grpc")) { - return endpoint; - } else { // http/protobuf - if (!endpoint.endsWith("v1/metrics")) { - if (!endpoint.endsWith("/")) { - return endpoint + "/v1/metrics"; - } else { - return endpoint + "v1/metrics"; - } - } else { - return endpoint; - } - } - } - - private static Map getHeaders( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - Map headers = config.getHeaders(); - if (!headers.isEmpty()) { - return headers; - } - headers = getMap("otel.exporter.otlp.headers"); - if (!headers.isEmpty()) { - return headers; - } - if (!builder.headers.isEmpty()) { - return builder.headers; - } - return new HashMap<>(); - } - - private static int getIntervalSeconds( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - Integer intervalSeconds = config.getIntervalSeconds(); - if (intervalSeconds != null) { - return intervalSeconds; - } - intervalSeconds = getPositiveInteger("otel.metric.export.interval"); - if (intervalSeconds != null) { - return (int) TimeUnit.MILLISECONDS.toSeconds(intervalSeconds); - } - if (builder.intervalSeconds != null) { - return builder.intervalSeconds; - } - return 60; - } - - private static int getTimeoutSeconds( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - Integer timeoutSeconds = config.getTimeoutSeconds(); - if (timeoutSeconds != null) { - return timeoutSeconds; - } - Integer timeoutMilliseconds = getPositiveInteger("otel.exporter.otlp.metrics.timeout"); - if (timeoutMilliseconds == null) { - timeoutMilliseconds = getPositiveInteger("otel.exporter.otlp.timeout"); - } - if (timeoutMilliseconds != null) { - return (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMilliseconds); - } - if (builder.timeoutSeconds != null) { - return builder.timeoutSeconds; - } - return 10; - } - - private static String getServiceName( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String serviceName = config.getServiceName(); - if (serviceName != null) { - return serviceName; - } - serviceName = getString("otel.service.name"); - if (serviceName != null) { - return serviceName; - } - if (builder.serviceName != null) { - return builder.serviceName; - } - return null; - } - - private static String getServiceNamespace( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String serviceNamespace = config.getServiceNamespace(); - if (serviceNamespace != null) { - return serviceNamespace; - } - if (builder.serviceNamespace != null) { - return builder.serviceNamespace; - } - return null; - } - - private static String getServiceInstanceId( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String serviceInstanceId = config.getServiceInstanceId(); - if (serviceInstanceId != null) { - return serviceInstanceId; - } - if (builder.serviceInstanceId != null) { - return builder.serviceInstanceId; - } - return null; - } - - private static String getServiceVersion( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - String serviceVersion = config.getServiceVersion(); - if (serviceVersion != null) { - return serviceVersion; - } - if (builder.serviceVersion != null) { - return builder.serviceVersion; - } - return null; - } - - private static Map getResourceAttributes( - OpenTelemetryExporter.Builder builder, ExporterOpenTelemetryProperties config) { - Map resourceAttributes = config.getResourceAttributes(); - if (!resourceAttributes.isEmpty()) { - return resourceAttributes; - } - resourceAttributes = getMap("otel.resource.attributes"); - if (!resourceAttributes.isEmpty()) { - return resourceAttributes; - } - if (!builder.resourceAttributes.isEmpty()) { - return builder.resourceAttributes; - } - return new HashMap<>(); - } - - private static String getString(String otelPropertyName) { - String otelEnvVarName = - otelPropertyName.replace(".", "_").replace("-", "_").toUpperCase(Locale.ROOT); - if (System.getenv(otelEnvVarName) != null) { - return System.getenv(otelEnvVarName); - } - if (System.getProperty(otelPropertyName) != null) { - return System.getProperty(otelPropertyName); - } - return null; - } - - private static Integer getInteger(String otelPropertyName) { - String result = getString(otelPropertyName); - if (result == null) { - return null; - } else { - try { - return Integer.parseInt(result); - } catch (NumberFormatException e) { - throw new IllegalStateException(otelPropertyName + "=" + result + " - illegal value."); - } - } - } - - private static Integer getPositiveInteger(String otelPropertyName) { - Integer result = getInteger(otelPropertyName); - if (result == null) { - return null; - } - if (result <= 0) { - throw new IllegalStateException(otelPropertyName + "=" + result + ": Expecting value > 0."); - } - return result; - } - - private static Map getMap(String otelPropertyName) { - Map result = new HashMap<>(); - String property = getString(otelPropertyName); - if (property != null) { - String[] pairs = property.split(","); - for (String pair : pairs) { - if (pair.contains("=")) { - String[] keyValue = pair.split("=", 1); - if (keyValue.length == 2) { - String key = keyValue[0].trim(); - String value = keyValue[1].trim(); - if (key.length() > 0 && value.length() > 0) { - result.putIfAbsent(key, value); - } - } - } - } - } - return result; + return new OpenTelemetryExporter(OtelAutoConfig.createReader(this, config, registry)); } } } diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java new file mode 100644 index 000000000..fb6ebff7e --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java @@ -0,0 +1,117 @@ +package io.prometheus.metrics.exporter.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.ResourceConfiguration; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.prometheus.metrics.config.ExporterOpenTelemetryProperties; +import io.prometheus.metrics.config.PrometheusProperties; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.otelagent.ResourceAttributesFromOtelAgent; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public class OtelAutoConfig { + + private static final String SERVICE_INSTANCE_ID = "service.instance.id"; + + static MetricReader createReader( + OpenTelemetryExporter.Builder builder, + PrometheusProperties config, + PrometheusRegistry registry) { + AtomicReference readerRef = new AtomicReference<>(); + InstrumentationScopeInfo instrumentationScopeInfo = + PrometheusInstrumentationScope.loadInstrumentationScopeInfo(); + + AutoConfiguredOpenTelemetrySdk sdk = + createAutoConfiguredOpenTelemetrySdk( + builder, + readerRef, + config.getExporterOpenTelemetryProperties(), + instrumentationScopeInfo); + + MetricReader reader = readerRef.get(); + reader.register( + new PrometheusMetricProducer(registry, instrumentationScopeInfo, getResourceField(sdk))); + return reader; + } + + static AutoConfiguredOpenTelemetrySdk createAutoConfiguredOpenTelemetrySdk( + OpenTelemetryExporter.Builder builder, + AtomicReference readerRef, + ExporterOpenTelemetryProperties properties, + InstrumentationScopeInfo instrumentationScopeInfo) { + PropertyMapper propertyMapper = PropertyMapper.create(properties, builder); + + return AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(() -> propertyMapper.configLowPriority) + .addPropertiesCustomizer( + c -> PropertyMapper.customizeProperties(propertyMapper.configHighPriority, c)) + .addMetricReaderCustomizer( + (reader, unused) -> { + readerRef.set(reader); + return reader; + }) + .addResourceCustomizer( + (resource, c) -> + getResource(builder, resource, instrumentationScopeInfo, c, properties)) + .build(); + } + + private static Resource getResource( + OpenTelemetryExporter.Builder builder, + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + ConfigProperties configProperties, + ExporterOpenTelemetryProperties properties) { + return resource + .merge( + PropertiesResourceProvider.mergeResource( + builder.resourceAttributes, + builder.serviceName, + builder.serviceNamespace, + builder.serviceInstanceId, + builder.serviceVersion)) + .merge(ResourceConfiguration.createEnvironmentResource(configProperties)) + .merge( + PropertiesResourceProvider.mergeResource( + properties.getResourceAttributes(), + properties.getServiceName(), + properties.getServiceNamespace(), + properties.getServiceInstanceId(), + properties.getServiceVersion())) + .merge(Resource.create(otelResourceAttributes(instrumentationScopeInfo))); + } + + /** + * Only copy the service instance id from the Otel agent resource attributes. + * + *

All other attributes are calculated from the configuration using OTel SDK AutoConfig. + */ + private static Attributes otelResourceAttributes( + InstrumentationScopeInfo instrumentationScopeInfo) { + AttributesBuilder builder = Attributes.builder(); + Map attributes = + ResourceAttributesFromOtelAgent.getResourceAttributes(instrumentationScopeInfo.getName()); + String id = attributes.get(SERVICE_INSTANCE_ID); + if (id != null) { + builder.put(SERVICE_INSTANCE_ID, id); + } + return builder.build(); + } + + static Resource getResourceField(AutoConfiguredOpenTelemetrySdk sdk) { + try { + Method method = AutoConfiguredOpenTelemetrySdk.class.getDeclaredMethod("getResource"); + method.setAccessible(true); + return (Resource) method.invoke(sdk); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertiesResourceProvider.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertiesResourceProvider.java new file mode 100644 index 000000000..1bb6b19bf --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertiesResourceProvider.java @@ -0,0 +1,35 @@ +package io.prometheus.metrics.exporter.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.Map; + +final class PropertiesResourceProvider { + + static Resource mergeResource( + Map resourceAttributes, + String serviceName, + String serviceNamespace, + String serviceInstanceId, + String serviceVersion) { + Map resource = new HashMap<>(resourceAttributes); + if (serviceName != null) { + resource.put("service.name", serviceName); + } + if (serviceNamespace != null) { + resource.put("service.namespace", serviceNamespace); + } + if (serviceInstanceId != null) { + resource.put("service.instance.id", serviceInstanceId); + } + if (serviceVersion != null) { + resource.put("service.version", serviceVersion); + } + + AttributesBuilder builder = Attributes.builder(); + resource.forEach(builder::put); + return Resource.create(builder.build()); + } +} diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertyMapper.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertyMapper.java new file mode 100644 index 000000000..be30197fa --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PropertyMapper.java @@ -0,0 +1,112 @@ +package io.prometheus.metrics.exporter.opentelemetry; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.prometheus.metrics.config.ExporterOpenTelemetryProperties; +import io.prometheus.metrics.config.PrometheusPropertiesException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +class PropertyMapper { + + private static final String METRICS_ENDPOINT = "otel.exporter.otlp.metrics.endpoint"; + Map configLowPriority = new HashMap<>(); + Map configHighPriority = new HashMap<>(); + + static PropertyMapper create( + ExporterOpenTelemetryProperties properties, OpenTelemetryExporter.Builder builder) + throws PrometheusPropertiesException { + return new PropertyMapper() + .addString( + builder.protocol, properties.getProtocol(), "otel.exporter.otlp.metrics.protocol") + .addString(builder.endpoint, properties.getEndpoint(), METRICS_ENDPOINT) + .addString( + mapToOtelString(builder.headers), + mapToOtelString(properties.getHeaders()), + "otel.exporter.otlp.metrics.headers") + .addString(builder.interval, properties.getInterval(), "otel.metric.export.interval") + .addString(builder.timeout, properties.getTimeout(), "otel.exporter.otlp.metrics.timeout") + .addString(builder.serviceName, properties.getServiceName(), "otel.service.name"); + } + + PropertyMapper addString(String builderValue, String propertyValue, String otelKey) { + if (builderValue != null) { + // the low priority config should not be used for the metrics settings, so that both general + // and metrics settings + // can be used to override the values + configLowPriority.put(otelKey.replace("otlp.metrics", "otlp"), builderValue); + } + if (propertyValue != null) { + configHighPriority.put(otelKey, propertyValue); + } + return this; + } + + private static String mapToOtelString(Map map) { + if (map.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + sb.append(entry.getKey()).append("=").append(entry.getValue()).append(","); + } + return sb.substring(0, sb.length() - 1); + } + + static Map customizeProperties(Map result, ConfigProperties c) { + Map map = fixEndpointPaths(result, c); + map.put("otel.logs.exporter", "none"); + map.put("otel.traces.exporter", "none"); + return map; + } + + static Map fixEndpointPaths(Map result, ConfigProperties c) { + transformEndpointPath( + result, + c, + METRICS_ENDPOINT, + endpoint -> { + if (!endpoint.endsWith("v1/metrics")) { + if (!endpoint.endsWith("/")) { + return endpoint + "/v1/metrics"; + } else { + return endpoint + "v1/metrics"; + } + } + return endpoint; + }); + + transformEndpointPath( + result, + c, + "otel.exporter.otlp.endpoint", + endpoint -> { + if (endpoint.endsWith("v1/metrics")) { + return endpoint.substring(0, endpoint.length() - "v1/metrics".length()); + } + return endpoint; + }); + + return result; + } + + static void transformEndpointPath( + Map result, + ConfigProperties c, + String key, + Function valueMapper) { + String endpoint = c.getString(key); + if (endpoint == null) { + return; + } + String protocol = c.getString("otel.exporter.otlp.metrics.protocol"); + if (protocol == null) { + protocol = c.getString("otel.exporter.otlp.protocol"); + } + + if (!"grpc".equals(protocol)) { // http/protobuf + endpoint = valueMapper.apply(endpoint); + result.put(key, endpoint); + } + } +} diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributes.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributes.java deleted file mode 100644 index 2c88badfc..000000000 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributes.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.prometheus.metrics.exporter.opentelemetry; - -import java.util.HashMap; -import java.util.Map; - -public class ResourceAttributes { - - // TODO: The OTel Java instrumentation also has a SpringBootServiceNameDetector, we should port - // this over. - public static Map get( - String instrumentationScopeName, - String serviceName, - String serviceNamespace, - String serviceInstanceId, - String serviceVersion, - Map configuredResourceAttributes) { - Map result = new HashMap<>(); - ResourceAttributesFromOtelAgent.addIfAbsent(result, instrumentationScopeName); - putIfAbsent(result, "service.name", serviceName); - putIfAbsent(result, "service.namespace", serviceNamespace); - putIfAbsent(result, "service.instance.id", serviceInstanceId); - putIfAbsent(result, "service.version", serviceVersion); - for (Map.Entry attribute : configuredResourceAttributes.entrySet()) { - putIfAbsent(result, attribute.getKey(), attribute.getValue()); - } - ResourceAttributesFromJarFileName.addIfAbsent(result); - ResourceAttributesDefaults.addIfAbsent(result); - return result; - } - - private static void putIfAbsent(Map result, String key, String value) { - if (value != null) { - result.putIfAbsent(key, value); - } - } -} diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesDefaults.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesDefaults.java deleted file mode 100644 index 19328fd73..000000000 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesDefaults.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.prometheus.metrics.exporter.opentelemetry; - -import java.util.Map; -import java.util.UUID; - -public class ResourceAttributesDefaults { - - private static final String instanceId = UUID.randomUUID().toString(); - - public static void addIfAbsent(Map result) { - result.putIfAbsent("service.instance.id", instanceId); - result.putIfAbsent("service.name", "unknown_service:java"); - } -} diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromJarFileName.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromJarFileName.java deleted file mode 100644 index 7cf7a51aa..000000000 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/ResourceAttributesFromJarFileName.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.prometheus.metrics.exporter.opentelemetry; - -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; - -// See io.opentelemetry.instrumentation.resources.JarServiceNameDetector -public class ResourceAttributesFromJarFileName { - - public static void addIfAbsent(Map result) { - if (result.containsKey("service.name")) { - return; - } - Path jarPath = getJarPathFromSunCommandLine(); - if (jarPath == null) { - return; - } - String serviceName = getServiceName(jarPath); - result.putIfAbsent("service.name", serviceName); - } - - private static Path getJarPathFromSunCommandLine() { - String programArguments = System.getProperty("sun.java.command"); - if (programArguments == null) { - return null; - } - // Take the path until the first space. If the path doesn't exist extend it up to the next - // space. Repeat until a path that exists is found or input runs out. - int next = 0; - while (true) { - int nextSpace = programArguments.indexOf(' ', next); - if (nextSpace == -1) { - return pathIfExists(programArguments); - } - Path path = pathIfExists(programArguments.substring(0, nextSpace)); - next = nextSpace + 1; - if (path != null) { - return path; - } - } - } - - private static Path pathIfExists(String programArguments) { - Path candidate; - try { - candidate = Paths.get(programArguments); - } catch (InvalidPathException e) { - return null; - } - return Files.isRegularFile(candidate) ? candidate : null; - } - - private static String getServiceName(Path jarPath) { - String jarName = jarPath.getFileName().toString(); - int dotIndex = jarName.lastIndexOf("."); - return dotIndex == -1 ? jarName : jarName.substring(0, dotIndex); - } -} diff --git a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java new file mode 100644 index 000000000..9b9b12420 --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java @@ -0,0 +1,296 @@ +package io.prometheus.metrics.exporter.opentelemetry; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.prometheus.metrics.config.ExporterOpenTelemetryProperties; +import io.prometheus.metrics.config.PrometheusPropertiesLoader; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OtelAutoConfigTest { + + static class TestCase { + Map systemProperties = new HashMap<>(); + Map> expectedProperties = Collections.emptyMap(); + Map expectedResourceAttributes = Collections.emptyMap(); + Consumer exporterBuilder; + Consumer propertiesBuilder; + + public TestCase() {} + + public TestCase expectedProperties(Map> expectedProperties) { + this.expectedProperties = expectedProperties; + return this; + } + + public TestCase expectedResourceAttributes(Map expectedResourceAttributes) { + this.expectedResourceAttributes = expectedResourceAttributes; + return this; + } + + public TestCase systemProperties(Map systemProperties) { + this.systemProperties.putAll(systemProperties); + return this; + } + + public TestCase exporterBuilder(Consumer exporterBuilder) { + this.exporterBuilder = exporterBuilder; + return this; + } + + public TestCase propertiesBuilder( + Consumer propertiesBuilder) { + this.propertiesBuilder = propertiesBuilder; + return this; + } + } + + public static Stream testCases() { + return Stream.of( + Arguments.of( + "values from builder", + new TestCase() + .expectedProperties( + ImmutableMap.>builder() + .put("otel.exporter.otlp.protocol", Optional.of("http/protobuf")) + .put("otel.exporter.otlp.endpoint", Optional.of("http://builder:4318")) + .put("otel.exporter.otlp.headers", Optional.of("h=builder-v")) + .put("otel.metric.export.interval", Optional.of("2s")) + .put("otel.exporter.otlp.timeout", Optional.of("3s")) + .put("otel.service.name", Optional.of("builder-service")) + .build()) + .expectedResourceAttributes( + ImmutableMap.of( + "key", + "builder-value", + "service.name", + "builder-service", + "service.namespace", + "builder-namespace", + "service.instance.id", + "builder-instance", + "service.version", + "builder-version")) + .exporterBuilder(OtelAutoConfigTest::setBuilderValues)), + Arguments.of( + "builder endpoint with path", + new TestCase() + .expectedProperties( + ImmutableMap.of( + "otel.exporter.otlp.endpoint", Optional.of("http://builder:4318/"))) + .exporterBuilder(builder -> builder.endpoint("http://builder:4318/v1/metrics"))), + Arguments.of( + "values from otel have precedence over builder", + new TestCase() + .expectedProperties( + ImmutableMap.>builder() + .put("otel.exporter.otlp.protocol", Optional.of("grpc")) + .put("otel.exporter.otlp.metrics.protocol", Optional.empty()) + .put("otel.exporter.otlp.endpoint", Optional.of("http://otel:4317")) + .put("otel.exporter.otlp.metrics.endpoint", Optional.empty()) + .put("otel.exporter.otlp.headers", Optional.of("h=otel-v")) + .put("otel.exporter.otlp.metrics.headers", Optional.empty()) + .put("otel.metric.export.interval", Optional.of("12s")) + .put("otel.exporter.otlp.timeout", Optional.of("13s")) + .put("otel.exporter.otlp.metrics.timeout", Optional.empty()) + .put("otel.service.name", Optional.of("otel-service")) + .build()) + .expectedResourceAttributes( + ImmutableMap.of( + "key", + "otel-value", + "service.name", + "otel-service", + "service.namespace", + "otel-namespace", + "service.instance.id", + "otel-instance", + "service.version", + "otel-version")) + .exporterBuilder(OtelAutoConfigTest::setBuilderValues) + .systemProperties(otelOverrides())), + Arguments.of( + "values from prom properties have precedence over builder and otel", + new TestCase() + .expectedProperties( + ImmutableMap.>builder() + .put("otel.exporter.otlp.metrics.protocol", Optional.of("http/protobuf")) + .put("otel.exporter.otlp.protocol", Optional.of("grpc")) + .put("otel.exporter.otlp.metrics.endpoint", Optional.of("http://prom:4317")) + .put("otel.exporter.otlp.endpoint", Optional.of("http://otel:4317")) + .put("otel.exporter.otlp.metrics.headers", Optional.of("h=prom-v")) + .put("otel.exporter.otlp.headers", Optional.of("h=otel-v")) + .put("otel.metric.export.interval", Optional.of("22s")) + .put("otel.exporter.otlp.metrics.timeout", Optional.of("23s")) + .put("otel.exporter.otlp.timeout", Optional.of("13s")) + .put("otel.service.name", Optional.of("prom-service")) + .build()) + .expectedResourceAttributes( + ImmutableMap.of( + "key", + "prom-value", + "service.name", + "prom-service", + "service.namespace", + "prom-namespace", + "service.instance.id", + "prom-instance", + "service.version", + "prom-version")) + .exporterBuilder(OtelAutoConfigTest::setBuilderValues) + .systemProperties(otelOverrides()) + .systemProperties( + ImmutableMap.builder() + .put("io.prometheus.exporter.opentelemetry.protocol", "http/protobuf") + .put("io.prometheus.exporter.opentelemetry.endpoint", "http://prom:4317") + .put("io.prometheus.exporter.opentelemetry.headers", "h=prom-v") + .put("io.prometheus.exporter.opentelemetry.intervalSeconds", "22") + .put("io.prometheus.exporter.opentelemetry.timeoutSeconds", "23") + .put("io.prometheus.exporter.opentelemetry.serviceName", "prom-service") + .put( + "io.prometheus.exporter.opentelemetry.serviceNamespace", + "prom-namespace") + .put( + "io.prometheus.exporter.opentelemetry.serviceInstanceId", + "prom-instance") + .put("io.prometheus.exporter.opentelemetry.serviceVersion", "prom-version") + .put( + "io.prometheus.exporter.opentelemetry.resourceAttributes", + "key=prom-value") + .build())), + Arguments.of( + "values from prom properties builder have precedence over builder and otel", + new TestCase() + .expectedProperties( + ImmutableMap.>builder() + .put("otel.exporter.otlp.metrics.protocol", Optional.of("http/protobuf")) + .put("otel.exporter.otlp.protocol", Optional.of("grpc")) + .put("otel.exporter.otlp.metrics.endpoint", Optional.of("http://prom:4317")) + .put("otel.exporter.otlp.endpoint", Optional.of("http://otel:4317")) + .put("otel.exporter.otlp.metrics.headers", Optional.of("h=prom-v")) + .put("otel.exporter.otlp.headers", Optional.of("h=otel-v")) + .put("otel.metric.export.interval", Optional.of("22s")) + .put("otel.exporter.otlp.metrics.timeout", Optional.of("23s")) + .put("otel.exporter.otlp.timeout", Optional.of("13s")) + .put("otel.service.name", Optional.of("prom-service")) + .build()) + .expectedResourceAttributes( + ImmutableMap.of( + "key", + "prom-value", + "service.name", + "prom-service", + "service.namespace", + "prom-namespace", + "service.instance.id", + "prom-instance", + "service.version", + "prom-version")) + .exporterBuilder(OtelAutoConfigTest::setBuilderValues) + .systemProperties(otelOverrides()) + .propertiesBuilder( + builder -> + builder + .protocol("http/protobuf") + .endpoint("http://prom:4317") + .header("h", "prom-v") + .intervalSeconds(22) + .timeoutSeconds(23) + .serviceName("prom-service") + .serviceNamespace("prom-namespace") + .serviceInstanceId("prom-instance") + .serviceVersion("prom-version") + .resourceAttribute("key", "prom-value")))); + } + + private static ImmutableMap otelOverrides() { + return ImmutableMap.builder() + .put("otel.exporter.otlp.protocol", "grpc") + .put("otel.exporter.otlp.endpoint", "http://otel:4317") + .put("otel.exporter.otlp.headers", "h=otel-v") + .put("otel.metric.export.interval", "12s") + .put("otel.exporter.otlp.timeout", "13s") + .put("otel.service.name", "otel-service") + .put( + "otel.resource.attributes", + "key=otel-value,service.namespace=otel-namespace,service.instance.id=otel-instance,service.version=otel-version") + .build(); + } + + private static void setBuilderValues(OpenTelemetryExporter.Builder builder) { + builder + .protocol("http/protobuf") + .endpoint("http://builder:4318") + .header("h", "builder-v") + .intervalSeconds(2) + .timeoutSeconds(3) + .serviceName("builder-service") + .serviceNamespace("builder-namespace") + .serviceInstanceId("builder-instance") + .serviceVersion("builder-version") + .resourceAttribute("key", "builder-value"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testCases") + void properties(String name, TestCase testCase) { + testCase.systemProperties.forEach(System::setProperty); + + try { + OpenTelemetryExporter.Builder builder = OpenTelemetryExporter.builder(); + if (testCase.exporterBuilder != null) { + testCase.exporterBuilder.accept(builder); + } + AutoConfiguredOpenTelemetrySdk sdk = + OtelAutoConfig.createAutoConfiguredOpenTelemetrySdk( + builder, + new AtomicReference<>(), + getExporterOpenTelemetryProperties(testCase), + PrometheusInstrumentationScope.loadInstrumentationScopeInfo()); + + ConfigProperties config = AutoConfigureUtil.getConfig(sdk); + Map, Object> map = + OtelAutoConfig.getResourceField(sdk).getAttributes().asMap(); + testCase.expectedProperties.forEach( + (key, value) -> { + AbstractStringAssert o = assertThat(config.getString(key)).describedAs("key=" + key); + if (value.isPresent()) { + o.isEqualTo(value.get()); + } else { + o.isNull(); + } + }); + testCase.expectedResourceAttributes.forEach( + (key, value) -> + assertThat(map.get(AttributeKey.stringKey(key))) + .describedAs("key=" + key) + .hasToString(value)); + } finally { + testCase.systemProperties.keySet().forEach(System::clearProperty); + } + } + + private static ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties( + TestCase testCase) { + if (testCase.propertiesBuilder == null) { + return PrometheusPropertiesLoader.load().getExporterOpenTelemetryProperties(); + } + ExporterOpenTelemetryProperties.Builder builder = ExporterOpenTelemetryProperties.builder(); + testCase.propertiesBuilder.accept(builder); + return builder.build(); + } +} diff --git a/prometheus-metrics-tracer/pom.xml b/prometheus-metrics-tracer/pom.xml index 84938cff4..953b09dc5 100644 --- a/prometheus-metrics-tracer/pom.xml +++ b/prometheus-metrics-tracer/pom.xml @@ -17,14 +17,16 @@ - - - io.opentelemetry - opentelemetry-api - ${otel.version} - - - + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom-alpha + ${otel.instrumentation.version} + pom + import + + + prometheus-metrics-tracer-common diff --git a/scripts/run-acceptance-tests.sh b/scripts/run-acceptance-tests.sh new file mode 100755 index 000000000..151783f77 --- /dev/null +++ b/scripts/run-acceptance-tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd oats/yaml +go install github.com/onsi/ginkgo/v2/ginkgo +export TESTCASE_SKIP_BUILD=true +export TESTCASE_TIMEOUT=5m +export TESTCASE_BASE_PATH=../../examples +ginkgo -r # is parallel causing problems? -p