Skip to content

Commit

Permalink
Merge pull request quarkusio#18699 from cyrille-leclerc/features/supp…
Browse files Browse the repository at this point in the history
…ort-otlp-grpc-headers

[OpenTelemetry OTLP Exporter extension] Add support for gRPC headers
  • Loading branch information
kenfinnigan authored Jul 19, 2021
2 parents 24e6793 + ef3b11f commit 6add354
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 14 deletions.
5 changes: 4 additions & 1 deletion docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,14 @@ The first approach is by providing the properties within the `src/main/resources
quarkus.application.name=myservice // <1>
quarkus.opentelemetry.enabled=true // <2>
quarkus.opentelemetry.tracer.exporter.otlp.endpoint=http://localhost:55680 // <3>
quarkus.opentelemetry.tracer.exporter.otlp.headers=Authorization=Bearer my_secret // <4>
----

<1> All spans created from the application will include an OpenTelemetry `Resource` indicating the span was created by the `myservice` application. If not set, it will default to the artifact id.
<2> Whether OpenTelemetry is enabled or not. The default is `true`, but shown here to indicate how it can be disabled
<3> gRPC endpoint for sending spans.
<3> gRPC endpoint for sending spans
<4> Optional gRPC headers commonly used for authentication

== Run the application

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.opentelemetry.exporter.otlp.runtime;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
Expand All @@ -27,6 +28,17 @@ public static class OtlpExporterRuntimeConfig {
@ConfigItem()
public Optional<String> endpoint;

/**
* Key-value pairs to be used as headers associated with gRPC requests.
* The format is similar to the {@code OTEL_EXPORTER_OTLP_HEADERS} environment variable,
* a list of key-value pairs separated by the "=" character.
* See <a href=
* "https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables">
* Specifying headers</a> for more details.
*/
@ConfigItem()
Optional<List<String>> headers;

/**
* The maximum amount of time to wait for the collector to process exported spans before an exception is thrown.
* A value of `0` will disable the timeout: the exporter will continue waiting until either exported spans are
Expand All @@ -35,5 +47,6 @@ public static class OtlpExporterRuntimeConfig {
*/
@ConfigItem(defaultValue = "10S")
public Duration exportTimeout;

}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.quarkus.opentelemetry.exporter.otlp.runtime;

import java.util.Map;
import java.util.Optional;

import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.CDI;

import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.quarkus.opentelemetry.runtime.OpenTelemetryUtil;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.annotations.Recorder;

Expand All @@ -18,14 +21,17 @@ public void installBatchSpanProcessorForOtlp(OtlpExporterConfig.OtlpExporterRunt
// Default the endpoint for development only
runtimeConfig.endpoint = Optional.of("http://localhost:4317");
}

// Only create the OtlpGrpcSpanExporter if an endpoint was set in runtime config
if (runtimeConfig.endpoint.isPresent() && runtimeConfig.endpoint.get().trim().length() > 0) {
try {
OtlpGrpcSpanExporter otlpSpanExporter = OtlpGrpcSpanExporter.builder()
OtlpGrpcSpanExporterBuilder otlpGrpcSpanExporterBuilder = OtlpGrpcSpanExporter.builder()
.setEndpoint(runtimeConfig.endpoint.get())
.setTimeout(runtimeConfig.exportTimeout)
.build();
.setTimeout(runtimeConfig.exportTimeout);
if (runtimeConfig.headers.isPresent()) {
Map<String, String> headers = OpenTelemetryUtil.convertKeyValueListToMap(runtimeConfig.headers.get());
headers.forEach(otlpGrpcSpanExporterBuilder::addHeader);
}
OtlpGrpcSpanExporter otlpSpanExporter = otlpGrpcSpanExporterBuilder.build();

// Create BatchSpanProcessor for OTLP and install into LateBoundBatchSpanProcessor
LateBoundBatchSpanProcessor delayedProcessor = CDI.current()
Expand Down
11 changes: 11 additions & 0 deletions extensions/opentelemetry/opentelemetry/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.opentelemetry.runtime;

import java.util.AbstractMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
Expand Down Expand Up @@ -48,4 +50,24 @@ public static ContextPropagators mapPropagators(List<String> propagators) {

return ContextPropagators.create(TextMapPropagator.composite(selectedPropagators));
}

/**
* Converts a list of "key=value" pairs into a map.
* Empty entries will be removed.
* In case of duplicate keys, the latest takes precedence.
*
* @param headers nullable list of "key=value" pairs
* @return non null map of key-value pairs
*/
public static Map<String, String> convertKeyValueListToMap(List<String> headers) {
if (headers == null) {
return new LinkedHashMap();
}

return headers.stream()
.filter(header -> !header.isEmpty())
.map(keyValuePair -> keyValuePair.split("=", 2))
.map(keyValuePair -> new AbstractMap.SimpleImmutableEntry<>(keyValuePair[0].trim(), keyValuePair[1].trim()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new));
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package io.quarkus.opentelemetry.runtime.tracing;

import java.util.AbstractMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.quarkus.opentelemetry.runtime.OpenTelemetryUtil;

public class TracerUtil {
private TracerUtil() {
Expand All @@ -19,11 +16,7 @@ private TracerUtil() {
public static Resource mapResourceAttributes(List<String> resourceAttributes) {
AttributesBuilder attributesBuilder = Attributes.builder();

resourceAttributes.stream()
.map(keyValuePair -> keyValuePair.split("=", 2))
.map(keyValuePair -> new AbstractMap.SimpleImmutableEntry<>(keyValuePair[0].trim(), keyValuePair[1].trim()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new))
.forEach(attributesBuilder::put);
OpenTelemetryUtil.convertKeyValueListToMap(resourceAttributes).forEach(attributesBuilder::put);

return Resource.create(attributesBuilder.build());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.opentelemetry.runtime;

import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class OpenTelemetryUtilTest {

@Test
public void testConvertKeyValueListToMap() {
Map<String, String> actual = OpenTelemetryUtil
.convertKeyValueListToMap(Arrays.asList(
"service.name=myservice",
"service.version=1.0",
"deployment.environment=production"));
Assertions.assertThat(actual).containsExactly(
new AbstractMap.SimpleEntry<>("service.name", "myservice"),
new AbstractMap.SimpleEntry<>("service.version", "1.0"),
new AbstractMap.SimpleEntry<>("deployment.environment", "production"));
}

@Test
public void testConvertKeyValueListToMap_skip_empty_values() {
Map<String, String> actual = OpenTelemetryUtil
.convertKeyValueListToMap(Arrays.asList(
"service.name=myservice",
"service.version=1.0",
"deployment.environment=production",
""));
Assertions.assertThat(actual).containsExactly(
new AbstractMap.SimpleEntry<>("service.name", "myservice"),
new AbstractMap.SimpleEntry<>("service.version", "1.0"),
new AbstractMap.SimpleEntry<>("deployment.environment", "production"));
}

@Test
public void testConvertKeyValueListToMap_last_value_takes_precedence() {
Map<String, String> actual = OpenTelemetryUtil
.convertKeyValueListToMap(Arrays.asList(
"service.name=myservice to be overwritten",
"service.version=1.0",
"deployment.environment=production",
"service.name=myservice",
""));
Assertions.assertThat(actual).containsExactly(
new AbstractMap.SimpleEntry<>("service.name", "myservice"),
new AbstractMap.SimpleEntry<>("service.version", "1.0"),
new AbstractMap.SimpleEntry<>("deployment.environment", "production"));
}

@Test
public void testConvertKeyValueListToMap_empty_value() {
Map<String, String> actual = OpenTelemetryUtil
.convertKeyValueListToMap(Collections.emptyList());
Assertions.assertThat(actual).containsExactly();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.opentelemetry.runtime.tracing;

import java.util.Arrays;
import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;

public class TracerUtilTest {

@Test
public void testMapResourceAttributes() {
List<String> resourceAttributes = Arrays.asList(
"service.name=myservice",
"service.namespace=mynamespace",
"service.version=1.0",
"deployment.environment=production");
Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes);
Attributes attributes = resource.getAttributes();
Assertions.assertThat(attributes.size()).isEqualTo(4);
Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_NAME)).isEqualTo("myservice");
Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_NAMESPACE)).isEqualTo("mynamespace");
Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_VERSION)).isEqualTo("1.0");
Assertions.assertThat(attributes.get(ResourceAttributes.DEPLOYMENT_ENVIRONMENT)).isEqualTo("production");
}
}

0 comments on commit 6add354

Please sign in to comment.