Skip to content

Commit

Permalink
OTLP metrics sender interface (#5691)
Browse files Browse the repository at this point in the history
* Add capability for anyone to define their implementation of OTLP metrics sending

* Fix formatting

* Remove otlp proto from API and introduce Builder

Keeps the API more generic without a dependency on API from the opentelemetry-proto-java project. Also adds a Builder to avoid the need for more public constructors that may change in the future.

* Polish

* Add docs

---------

Co-authored-by: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com>
  • Loading branch information
kasparkivistik and shakuzen authored Feb 10, 2025
1 parent 2948805 commit f99efa6
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 49 deletions.
3 changes: 3 additions & 0 deletions docs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ dependencies {

testImplementation project(':micrometer-test')
testImplementation project(':micrometer-observation-test')
testImplementation project(':micrometer-registry-otlp')
testImplementation libs.aspectjweaver
testImplementation libs.junitJupiter
testImplementation 'org.assertj:assertj-core'
// needed for OtlpMeterRegistryCustomizationTest
testRuntimeOnly libs.okhttp
testImplementation(libs.spring6.context)
testImplementation 'io.projectreactor:reactor-core'
testImplementation 'io.projectreactor:reactor-test'
Expand Down
21 changes: 21 additions & 0 deletions docs/modules/ROOT/pages/implementations/otlp.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ management:

If this config is empty, resource attributes are loaded from the `OTEL_RESOURCE_ATTRIBUTES` environmental variable. You can override `service.name` by setting the `OTEL_SERVICE_NAME` environmental variable, and this takes precedence over other configs.

== Customize metrics sender

The `OtlpMeterRegistry` has an `OtlpMetricsSender` abstraction for sending batches of metrics in OTLP protobuf format.

By default, metrics from the OTLP registry are sent via HTTP to the URL specified by `OtlpConfig#url` using an `HttpUrlConnectionSender`.
You may use a different `HttpSender` implementation by creating and configuring an instance of `OtlpHttpMetricsSender` such as in the following example using the `OkHttpSender` instead of the default `HttpUrlConnectionSender`:

[source,java,subs=+attributes]
-----
include::{include-java}/metrics/OtlpMeterRegistryCustomizationTest.java[tags=customizeHttpSender, indent=0]
-----

You can also provide a custom implementation of `OtlpMetricsSender` that does not use HTTP at all.
For instance, if you made a gRPC implementation, you could configure it in the following way.
Micrometer does not currently provide a gRPC implementation of `OtlpMetricsSender`.

[source,java,subs=+attributes]
-----
include::{include-java}/metrics/OtlpMeterRegistryCustomizationTest.java[tags=customGrpcSender, indent=0]
-----

== Supported metrics
https://opentelemetry.io/docs/specs/otel/metrics/data-model/#metric-points[Metric points, window=_blank] define the different data points that are supported in OTLP. Micrometer supports exporting the below data points in OTLP format,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2025 VMware, Inc.
*
* Licensed 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
*
* https://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 io.micrometer.docs.metrics;

import io.micrometer.common.lang.NonNullApi;
import io.micrometer.core.ipc.http.OkHttpSender;
import io.micrometer.registry.otlp.OtlpConfig;
import io.micrometer.registry.otlp.OtlpHttpMetricsSender;
import io.micrometer.registry.otlp.OtlpMeterRegistry;
import io.micrometer.registry.otlp.OtlpMetricsSender;
import org.junit.jupiter.api.Test;

import java.util.Map;

class OtlpMeterRegistryCustomizationTest {

@Test
void customizeHttpSender() {
// tag::customizeHttpSender[]
OtlpConfig config = OtlpConfig.DEFAULT;
OtlpHttpMetricsSender httpMetricsSender = new OtlpHttpMetricsSender(new OkHttpSender(), config);
OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).metricsSender(httpMetricsSender).build();
// end::customizeHttpSender[]
}

@Test
void customizeOtlpSender() {
// tag::customGrpcSender[]
OtlpConfig config = OtlpConfig.DEFAULT;
OtlpMetricsSender metricsSender = new OtlpGrpcMetricsSender();
OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).metricsSender(metricsSender).build();
// end::customGrpcSender[]
}

@NonNullApi
private static class OtlpGrpcMetricsSender implements OtlpMetricsSender {

@Override
public void send(byte[] metricsData, Map<String, String> headers) {
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2025 VMware, Inc.
*
* Licensed 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
*
* https://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 io.micrometer.registry.otlp;

import java.util.Map;

// intentionally not public while we incubate this concept
// if we want to use this in other registries, it should move to micrometer-core and become public API
interface MetricsSender {

/**
* Send encoded metrics data from a {@link io.micrometer.core.instrument.MeterRegistry
* MeterRegistry}.
* @param metricsData encoded batch of metrics
* @param headers metadata to send as headers with the metrics data
*/
void send(byte[] metricsData, Map<String, String> headers);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2025 VMware, Inc.
*
* Licensed 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
*
* https://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 io.micrometer.registry.otlp;

import io.micrometer.common.util.internal.logging.InternalLogger;
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
import io.micrometer.core.ipc.http.HttpSender;

import java.util.Map;

/**
* An implementation of {@link OtlpMetricsSender} that uses an {@link HttpSender}.
*
* @since 1.15.0
*/
public class OtlpHttpMetricsSender implements OtlpMetricsSender {

private static final InternalLogger logger = InternalLoggerFactory.getInstance(OtlpHttpMetricsSender.class);

private final HttpSender httpSender;

private final OtlpConfig config;

private final String userAgentHeader;

public OtlpHttpMetricsSender(HttpSender httpSender, OtlpConfig config) {
this.httpSender = httpSender;
this.config = config;
this.userAgentHeader = getUserAgentHeader();
}

@Override
public void send(byte[] metricsData, Map<String, String> headers) {
HttpSender.Request.Builder httpRequest = this.httpSender.post(config.url())
.withHeader("User-Agent", userAgentHeader)
.withContent("application/x-protobuf", metricsData);
headers.forEach(httpRequest::withHeader);
try {
HttpSender.Response response = httpRequest.send();
if (!response.isSuccessful()) {
logger.warn(
"Failed to publish metrics (context: {}). Server responded with HTTP status code {} and body {}",
getConfigurationContext(), response.code(), response.body());
}
}
catch (Throwable e) {
logger.warn("Failed to publish metrics (context: {}) ", getConfigurationContext(), e);
}
}

private String getUserAgentHeader() {
String userAgent = "Micrometer-OTLP-Exporter-Java";
String version = getClass().getPackage().getImplementationVersion();
if (version != null) {
userAgent += "/" + version;
}
return userAgent;
}

/**
* Get the configuration context.
* @return A message containing enough information for the log reader to figure out
* what configuration details may have contributed to the failure.
*/
private String getConfigurationContext() {
// While other values may contribute to failures, these two are most common
return "url=" + config.url() + ", resource-attributes=" + config.resourceAttributes();
}

}
Loading

0 comments on commit f99efa6

Please # to comment.