From e0480a3b2cfe7436231c1e51a972395243fa245b Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Tue, 3 Dec 2024 15:34:35 -0600 Subject: [PATCH 1/4] Add telemetry filter helper feature so developer code can influence whether incoming or outgoing spans are automatically created --- .../telemetry/HelidonTelemetryClientFilter.java | 4 ++++ .../telemetry/HelidonTelemetryContainerFilter.java | 6 ++++++ ...file.telemetry.spi.HelidonTelemetryContainerFilterHelper | 1 + 3 files changed, 11 insertions(+) create mode 100644 tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java index 96f77a2ee65..06246cf584b 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java @@ -21,6 +21,7 @@ import java.util.Set; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper; import io.helidon.tracing.HeaderConsumer; import io.helidon.tracing.HeaderProvider; @@ -59,6 +60,9 @@ class HelidonTelemetryClientFilter implements ClientRequestFilter, ClientRespons Response.Status.Family.CLIENT_ERROR, Response.Status.Family.SERVER_ERROR); + private static final LazyValue> HELPERS = LazyValue.create( + HelidonTelemetryClientFilter::helpers); + private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryClientFilterHelper.class.getName() + ".startSpan"; private final io.helidon.tracing.Tracer helidonTracer; diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java index 6548f912211..32de129b0b5 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java @@ -19,8 +19,11 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; +import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicBoolean; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; import io.helidon.common.context.Contexts; import io.helidon.config.mp.MpConfig; import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; @@ -67,6 +70,9 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryContainerFilterHelper.class + ".startSpan"; + private static final LazyValue> HELPERS = LazyValue.create( + HelidonTelemetryContainerFilter::helpers); + @Deprecated(forRemoval = true, since = "4.1") static final String SPAN_NAME_INCLUDES_METHOD = "telemetry.span.name-includes-method"; diff --git a/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper b/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper new file mode 100644 index 00000000000..233ff0ee9eb --- /dev/null +++ b/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper @@ -0,0 +1 @@ +io.helidon.tests.integration.telemetry.mp.filterselectivity.FilterSelectorSuppressPersonalizedGreetingSpan \ No newline at end of file From ed1bdf092da9aaa2b105ff1b689295465c317f7d Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Thu, 5 Dec 2024 07:17:02 -0600 Subject: [PATCH 2/4] Use CDI instead of Java service loading to locate helpers --- .../telemetry/HelidonTelemetryClientFilter.java | 4 ---- .../telemetry/HelidonTelemetryContainerFilter.java | 6 ------ ...file.telemetry.spi.HelidonTelemetryContainerFilterHelper | 1 - 3 files changed, 11 deletions(-) delete mode 100644 tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java index 06246cf584b..96f77a2ee65 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java @@ -21,7 +21,6 @@ import java.util.Set; import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.LazyValue; import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper; import io.helidon.tracing.HeaderConsumer; import io.helidon.tracing.HeaderProvider; @@ -60,9 +59,6 @@ class HelidonTelemetryClientFilter implements ClientRequestFilter, ClientRespons Response.Status.Family.CLIENT_ERROR, Response.Status.Family.SERVER_ERROR); - private static final LazyValue> HELPERS = LazyValue.create( - HelidonTelemetryClientFilter::helpers); - private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryClientFilterHelper.class.getName() + ".startSpan"; private final io.helidon.tracing.Tracer helidonTracer; diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java index 32de129b0b5..6548f912211 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java @@ -19,11 +19,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; -import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicBoolean; -import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.LazyValue; import io.helidon.common.context.Contexts; import io.helidon.config.mp.MpConfig; import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; @@ -70,9 +67,6 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryContainerFilterHelper.class + ".startSpan"; - private static final LazyValue> HELPERS = LazyValue.create( - HelidonTelemetryContainerFilter::helpers); - @Deprecated(forRemoval = true, since = "4.1") static final String SPAN_NAME_INCLUDES_METHOD = "telemetry.span.name-includes-method"; diff --git a/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper b/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper deleted file mode 100644 index 233ff0ee9eb..00000000000 --- a/tests/integration/mp-telemetry/src/main/resources/META-INF/services/io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper +++ /dev/null @@ -1 +0,0 @@ -io.helidon.tests.integration.telemetry.mp.filterselectivity.FilterSelectorSuppressPersonalizedGreetingSpan \ No newline at end of file From a154474a97fc40d07fcb2a54af75803ebf57066b Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Wed, 18 Dec 2024 09:13:56 -0600 Subject: [PATCH 3/4] Inject either native OTel objects or wrappers that notify span listeners, based on configuration Signed-off-by: Tim Quinn --- .../telemetry/etc/spotbugs/exclude.xml | 5 + microprofile/telemetry/pom.xml | 36 +- .../microprofile/telemetry/InjectionType.java | 34 ++ .../NativeOpenTelemetryProducer.java | 102 ++++++ .../telemetry/OpenTelemetryProducer.java | 137 +++----- .../telemetry/TelemetryConfigBlueprint.java | 37 ++ .../telemetry/WrappedProducer.java | 319 ++++++++++++++++++ .../telemetry/TestListenersWithInjection.java | 299 ++++++++++++++++ .../opentelemetry/HelidonOpenTelemetry.java | 24 ++ .../opentelemetry/OpenTelemetryScope.java | 13 + .../opentelemetry/OpenTelemetrySpan.java | 21 ++ .../OpenTelemetryTracerProvider.java | 39 +++ .../main/java/io/helidon/tracing/Scope.java | 18 + .../main/java/io/helidon/tracing/Wrapper.java | 32 ++ 14 files changed, 1032 insertions(+), 84 deletions(-) create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/InjectionType.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/NativeOpenTelemetryProducer.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/WrappedProducer.java create mode 100644 microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/TestListenersWithInjection.java create mode 100644 tracing/tracing/src/main/java/io/helidon/tracing/Wrapper.java diff --git a/microprofile/telemetry/etc/spotbugs/exclude.xml b/microprofile/telemetry/etc/spotbugs/exclude.xml index 48198837112..69e66e67e43 100644 --- a/microprofile/telemetry/etc/spotbugs/exclude.xml +++ b/microprofile/telemetry/etc/spotbugs/exclude.xml @@ -28,4 +28,9 @@ + + + + + diff --git a/microprofile/telemetry/pom.xml b/microprofile/telemetry/pom.xml index cb75c632365..f584e2f966f 100644 --- a/microprofile/telemetry/pom.xml +++ b/microprofile/telemetry/pom.xml @@ -135,19 +135,49 @@ helidon-common-features-processor ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + org.apache.maven.plugins maven-surefire-plugin + + + ${otel.bsp.schedule.delay} + + default-test - - ${otel.bsp.schedule.delay} - + + **/TestListenersWithInjection.java + + + + + + test-listeners-with-injection + + test + + + + **/TestListenersWithInjection.java + diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/InjectionType.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/InjectionType.java new file mode 100644 index 00000000000..3517f0911dc --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/InjectionType.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.microprofile.telemetry; + +/** + * Choices for the telemetry injection type config value. + */ +public enum InjectionType { + + /** + * Inject native OpenTelemetry types. + */ + NATIVE, + + /** + * Inject Helidon neutral types which wrap the OpenTelemetry types. + */ + NEUTRAL; + + static final String DEFAULT = "NATIVE"; +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/NativeOpenTelemetryProducer.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/NativeOpenTelemetryProducer.java new file mode 100644 index 00000000000..ec039d6746d --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/NativeOpenTelemetryProducer.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.microprofile.telemetry; + +import java.util.concurrent.TimeUnit; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; + +/** + * Producer implementation which injects the native OTel objects (which do not participate in the Helidon-specific + * span listener feature). + */ +class NativeOpenTelemetryProducer implements OpenTelemetryProducer.Producer { + + static NativeOpenTelemetryProducer create(Tracer nativeTracer) { + return new NativeOpenTelemetryProducer(nativeTracer); + } + + private final Tracer nativeTracer; + + private NativeOpenTelemetryProducer(Tracer nativeTracer) { + this.nativeTracer = nativeTracer; + } + + @Override + public Tracer tracer() { + return nativeTracer; + } + + @Override + public Span span() { + return new Span() { + @Override + public Span setAttribute(AttributeKey key, T value) { + return Span.current().setAttribute(key, value); + } + + @Override + public Span addEvent(String name, Attributes attributes) { + return Span.current().addEvent(name, attributes); + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + return Span.current().addEvent(name, attributes, timestamp, unit); + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + return Span.current().setStatus(statusCode, description); + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + return Span.current().recordException(exception, additionalAttributes); + } + + @Override + public Span updateName(String name) { + return Span.current().updateName(name); + } + + @Override + public void end() { + Span.current().end(); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + Span.current().end(timestamp, unit); + } + + @Override + public SpanContext getSpanContext() { + return Span.current().getSpanContext(); + } + + @Override + public boolean isRecording() { + return Span.current().isRecording(); + } + }; + } +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java index a672f16d70b..70afffe44b1 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java @@ -18,10 +18,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; import io.helidon.tracing.providers.opentelemetry.HelidonOpenTelemetry; import io.helidon.tracing.providers.opentelemetry.OpenTelemetryTracerProvider; @@ -30,11 +30,7 @@ import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.baggage.BaggageBuilder; import io.opentelemetry.api.baggage.BaggageEntry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -51,6 +47,9 @@ */ @ApplicationScoped class OpenTelemetryProducer { + + static final String INJECTION_TYPE = "telemetry.injection-type"; + private static final System.Logger LOGGER = System.getLogger(OpenTelemetryProducer.class.getName()); private static final String HELIDON_SERVICE_NAME = "HELIDON_MICROPROFILE_TELEMETRY"; private static final String SERVICE_NAME = "service.name"; @@ -73,6 +72,8 @@ class OpenTelemetryProducer { private final org.eclipse.microprofile.config.Config mpConfig; + private Producer producer; + @Inject OpenTelemetryProducer(Config config, org.eclipse.microprofile.config.Config mpConfig) { this.config = config; @@ -125,6 +126,11 @@ private void init() { tracer = openTelemetry.getTracer(exporterName); helidonTracer = HelidonOpenTelemetry.create(openTelemetry, tracer, Map.of()); + TelemetryConfig telemetryConfig = + TelemetryConfig.create(MpConfig.toHelidonConfig(mpConfig).get(TelemetryConfig.TELEMETRY_CONFIG_KEY)); + producer = (telemetryConfig.injectionType() == InjectionType.NEUTRAL) + ? WrappedProducer.create(helidonTracer) + : NativeOpenTelemetryProducer.create(tracer); OpenTelemetryTracerProvider.globalTracer(helidonTracer); } @@ -146,7 +152,7 @@ OpenTelemetry openTelemetry() { */ @Produces Tracer tracer() { - return tracer; + return producer.tracer(); } /** @@ -166,57 +172,7 @@ io.helidon.tracing.Tracer helidonTracer() { */ @Produces Span span() { - return new Span() { - @Override - public Span setAttribute(AttributeKey key, T value) { - return Span.current().setAttribute(key, value); - } - - @Override - public Span addEvent(String name, Attributes attributes) { - return Span.current().addEvent(name, attributes); - } - - @Override - public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { - return Span.current().addEvent(name, attributes, timestamp, unit); - } - - @Override - public Span setStatus(StatusCode statusCode, String description) { - return Span.current().setStatus(statusCode, description); - } - - @Override - public Span recordException(Throwable exception, Attributes additionalAttributes) { - return Span.current().recordException(exception, additionalAttributes); - } - - @Override - public Span updateName(String name) { - return Span.current().updateName(name); - } - - @Override - public void end() { - Span.current().end(); - } - - @Override - public void end(long timestamp, TimeUnit unit) { - Span.current().end(timestamp, unit); - } - - @Override - public SpanContext getSpanContext() { - return Span.current().getSpanContext(); - } - - @Override - public boolean isRecording() { - return Span.current().isRecording(); - } - }; + return producer.span(); } /** @@ -225,36 +181,53 @@ public boolean isRecording() { * @return a {@link io.opentelemetry.api.baggage.Baggage}. */ @Produces - @ApplicationScoped Baggage baggage() { - return new Baggage() { - @Override - public int size() { - return Baggage.current().size(); - } + return producer.baggage(); + } - @Override - public void forEach(BiConsumer consumer) { - Baggage.current().forEach(consumer); - } + interface Producer { + + Tracer tracer(); + + Span span(); + + /** + * Produces a {@link io.opentelemetry.api.baggage.Baggage} instance for injection. + *

+ * Note that we do not need to return a wrapper around the baggage because the span listener interface has no + * callback method related to baggage. + * + * @return OTel baggage instance + */ + default Baggage baggage() { + return new Baggage() { + @Override + public int size() { + return Baggage.current().size(); + } - @Override - public Map asMap() { - return Baggage.current().asMap(); - } + @Override + public void forEach(BiConsumer consumer) { + Baggage.current().forEach(consumer); + } - @Override - public String getEntryValue(String entryKey) { - return Baggage.current().getEntryValue(entryKey); - } + @Override + public Map asMap() { + return Baggage.current().asMap(); + } - @Override - public BaggageBuilder toBuilder() { - return Baggage.current().toBuilder(); - } - }; - } + @Override + public String getEntryValue(String entryKey) { + return Baggage.current().getEntryValue(entryKey); + } + @Override + public BaggageBuilder toBuilder() { + return Baggage.current().toBuilder(); + } + }; + } + } // Process "otel." properties from microprofile config file. private Map getTelemetryProperties() { @@ -293,4 +266,6 @@ private String getServiceName(ConfigProperties c) { return c.getString(SERVICE_NAME_PROPERTY, HELIDON_SERVICE_NAME); } + + } diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java new file mode 100644 index 00000000000..44c79c3f941 --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.microprofile.telemetry; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * Configuration for telemetry. + */ +@Prototype.Blueprint +@Prototype.Configured(TelemetryConfigBlueprint.TELEMETRY_CONFIG_KEY) +interface TelemetryConfigBlueprint { + + /** + * The config key containing settings for all of metrics. + */ + String TELEMETRY_CONFIG_KEY = "telemetry"; + + @Option.Configured + @Option.Default(InjectionType.DEFAULT) + InjectionType injectionType(); + +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/WrappedProducer.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/WrappedProducer.java new file mode 100644 index 00000000000..45e599d6aa0 --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/WrappedProducer.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.microprofile.telemetry; + +import java.util.concurrent.TimeUnit; + +import io.helidon.tracing.Wrapper; +import io.helidon.tracing.providers.opentelemetry.HelidonOpenTelemetry; +import io.helidon.tracing.providers.opentelemetry.OpenTelemetryTracerProvider; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +/** + * Producer implementation that returns wrappers which implement the OTel interfaces but wrap Helidon OTel implementations, + * thus notifying any registered span listeners of state changes. + *

+ * Each wrapped instance records its Helidon counterpart which in turn wraps the OTel native object. In each wrapped type, + * the OTel methods which do not alter states delegate to the OTel native object, whereas for the most part those which do + * change state delegate to the Helidon object thereby notifying any registered span listeners. + */ +class WrappedProducer implements OpenTelemetryProducer.Producer { + + private static final System.Logger LOGGER = System.getLogger(WrappedProducer.class.getName()); + + private final io.helidon.tracing.Tracer helidonTracer; + + private WrappedProducer(io.helidon.tracing.Tracer helidonTracer) { + this.helidonTracer = helidonTracer; + } + + static WrappedProducer create(io.helidon.tracing.Tracer helidonTracer) { + return new WrappedProducer(helidonTracer); + } + + @Override + public Tracer tracer() { + return new WrappedTracer(helidonTracer); + } + + @Override + public Span span() { + return new WrappedSpan(helidonTracer); + } + + static class WrappedTracer implements Tracer { + + private final io.helidon.tracing.Tracer helidonTracer; + + WrappedTracer(io.helidon.tracing.Tracer helidonTracer) { + this.helidonTracer = helidonTracer; + } + + @Override + public SpanBuilder spanBuilder(String spanName) { + return new WrappedSpanBuilder(helidonTracer, spanName); + } + } + + static class WrappedSpanBuilder implements io.opentelemetry.api.trace.SpanBuilder, Wrapper { + + private final io.helidon.tracing.Span.Builder helidonSpanBuilder; + private final SpanBuilder nativeSpanBuilder; + + private WrappedSpanBuilder(io.helidon.tracing.Tracer helidonTracer, String spanName) { + nativeSpanBuilder = helidonTracer.unwrap(Tracer.class).spanBuilder(spanName); + helidonSpanBuilder = HelidonOpenTelemetry.create(nativeSpanBuilder, helidonTracer); + // To maintain the Helidon span lineage set the parent if one is available. + io.helidon.tracing.Span.current().map(io.helidon.tracing.Span::context).ifPresent(helidonSpanBuilder::parent); + } + + @Override + public SpanBuilder setParent(Context context) { + nativeSpanBuilder.setParent(context); + // Generally we don't also invoke the Helidon implementation's method because most of the methods simply delegate + // to the native OTel counterpart method. But we need to set the parent of the Helidon span builder + // explicitly because the parentage is not maintained via simple delegation to the native OTel span builder. + helidonSpanBuilder.parent(HelidonOpenTelemetry.create(context)); + return this; + } + + @Override + public SpanBuilder setNoParent() { + // There is no Helidon counterpart for clearing the parent once set. + nativeSpanBuilder.setNoParent(); + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext) { + nativeSpanBuilder.addLink(spanContext); + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + nativeSpanBuilder.addLink(spanContext, attributes); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + nativeSpanBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + nativeSpanBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + nativeSpanBuilder.setAttribute(key, value); + helidonSpanBuilder.tag(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + nativeSpanBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(AttributeKey key, T value) { + nativeSpanBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setSpanKind(SpanKind spanKind) { + nativeSpanBuilder.setSpanKind(spanKind); + return this; + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + nativeSpanBuilder.setStartTimestamp(startTimestamp, unit); + return this; + } + + @Override + public Span startSpan() { + return new WrappedSpan(helidonSpanBuilder.start()); + } + + @Override + public R unwrap(Class c) { + if (c.isInstance(helidonSpanBuilder)) { + return c.cast(helidonSpanBuilder); + } + if (c.isInstance(nativeSpanBuilder)) { + return c.cast(nativeSpanBuilder); + } + throw new IllegalArgumentException("Cannot provide an instance of " + c.getName() + + "; the wrapped telemetry span builder has type " + + nativeSpanBuilder.getClass().getName() + + " and the wrapped Helidon span builder has type " + + helidonSpanBuilder.getClass().getName()); + } + } + + static class WrappedSpan implements io.opentelemetry.api.trace.Span, Wrapper { + + private final io.helidon.tracing.Span helidonSpan; + private final Span nativeSpan; + + private WrappedSpan(io.helidon.tracing.Span helidonSpan) { + this.helidonSpan = helidonSpan; + nativeSpan = helidonSpan.unwrap(io.opentelemetry.api.trace.Span.class); + } + + /** + * Constructor for use by the injection producer method which does not have a Helidon span already. + * + * @param helidonTracer Helidon tracer + */ + private WrappedSpan(io.helidon.tracing.Tracer helidonTracer) { + Span nativeCurrentSpan = Span.fromContextOrNull(Context.current()); + + nativeSpan = Span.current(); + helidonSpan = OpenTelemetryTracerProvider.span(helidonTracer, nativeSpan, nativeCurrentSpan == null); + } + + @Override + public Span setAttribute(AttributeKey key, T value) { + nativeSpan.setAttribute(key, value); + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes) { + nativeSpan.addEvent(name, attributes); + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + nativeSpan.addEvent(name, attributes, timestamp, unit); + return this; + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + nativeSpan.setStatus(statusCode, description); + return this; + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + nativeSpan.recordException(exception, additionalAttributes); + return this; + } + + @Override + public Span updateName(String name) { + nativeSpan.updateName(name); + return this; + } + + @Override + public void end() { + // Invoking the Helidon end method will notify listeners as well as end its native delegate span. + helidonSpan.end(); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + // The Helidon API does not have an end(long, TimeUnit) method. So we have to invoke the native span directly + // with those arguments and then invoke the listeners explicitly. We don't want to also invoke the Helidon "end()" + // method to accomplish the notifications because that would end the native span again. + nativeSpan.end(timestamp, unit); + HelidonOpenTelemetry.invokeListeners(helidonSpan, LOGGER, listener -> listener.ended(helidonSpan)); + } + + @Override + public SpanContext getSpanContext() { + return nativeSpan.getSpanContext(); + } + + @Override + public boolean isRecording() { + return nativeSpan.isRecording(); + } + + @Override + public Scope makeCurrent() { + return new WrappedScope(helidonSpan.activate()); + } + + @Override + public R unwrap(Class c) { + if (c.isInstance(nativeSpan)) { + return c.cast(nativeSpan); + } + if (c.isInstance(helidonSpan)) { + return c.cast(helidonSpan); + } + throw new IllegalArgumentException("Cannot provide an instance of " + c.getName() + + "; the wrapped telemetry span has type " + + nativeSpan.getClass().getName() + + " and the wrapped Helidon span has type " + + helidonSpan.getClass().getName()); + } + } + + static class WrappedScope implements io.opentelemetry.context.Scope, Wrapper { + + private final io.helidon.tracing.Scope helidonScope; + + private WrappedScope(io.helidon.tracing.Scope helidonScope) { + this.helidonScope = helidonScope; + } + + @Override + public void close() { + helidonScope.close(); + } + + @Override + public R unwrap(Class c) { + Scope nativeScope = helidonScope.unwrap(Scope.class); + if (c.isInstance(nativeScope)) { + return c.cast(nativeScope); + } + if (c.isInstance(helidonScope)) { + return c.cast(helidonScope); + } + throw new IllegalArgumentException("Cannot provide an instance of " + c.getName() + + "; the wrapped telemetry scope has type " + + nativeScope.getClass().getName() + + " and the wrapped Helidon scope has type " + + helidonScope.getClass().getName()); + } + } +} diff --git a/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/TestListenersWithInjection.java b/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/TestListenersWithInjection.java new file mode 100644 index 00000000000..1467288e545 --- /dev/null +++ b/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/TestListenersWithInjection.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.microprofile.telemetry; + +import java.util.ArrayList; +import java.util.List; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.tracing.Scope; +import io.helidon.tracing.Span; +import io.helidon.tracing.SpanListener; +import io.helidon.tracing.Tracer; +import io.helidon.tracing.Wrapper; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(TestListenersWithInjection.TestResource.class) +@AddConfig(key = "otel.sdk.disabled", value = "false") +@AddConfig(key = "telemetry.injection-type", value = "neutral") +class TestListenersWithInjection { + + private static final TestSpanListener testSpanListener = new TestSpanListener(); + + @Inject + private WebTarget webTarget; + + @Inject + private TestResource testResource; + + @BeforeAll + static void prepareTracer() { + Tracer.global().register(testSpanListener); + } + + @BeforeEach + void clear() { + testSpanListener.clear(); + } + + @Test + void checkNotifications() throws InterruptedException { + int startingBefore = testSpanListener.starting(); + int startedBefore = testSpanListener.started(); + int closedBefore = testSpanListener.closed(); + int endedBefore = testSpanListener.ended(); + + testResource.go(); + + assertThat("Starting", testSpanListener.starting() - startingBefore, is(1)); + assertThat("Started", testSpanListener.started() - startedBefore, is(1)); + assertThat("Closed", testSpanListener.closed() - closedBefore, is(1)); + assertThat("Ended", testSpanListener.ended() - endedBefore, is(1)); + + } + + @Test + void checkTypesOfListenerParameters() throws InterruptedException { + + int startingBefore = testSpanListener.starting(); + int startedBefore = testSpanListener.started(); + int closedBefore = testSpanListener.closed(); + int endedBefore = testSpanListener.ended(); + + // Access the resource using HTTP so the filter will set a current span that the resource will inject. + Response response = webTarget.path("/test/work").request(MediaType.TEXT_PLAIN).get(); + + assertThat("Starting", testSpanListener.starting() - startingBefore, greaterThan(0)); + assertThat("Started", testSpanListener.started() - startedBefore, greaterThan(0)); + assertThat("Closed", testSpanListener.closed() - closedBefore, greaterThan(0)); + assertThat("Ended", testSpanListener.ended() - endedBefore, greaterThan(0)); + + // The listener should have recorded span lifecycles for these spans: + // 1. outgoing client request with name HTTP GET + // 2. incoming server request added by the Helidon MP filter with name /test/work + // 3. explicitly-created span in the service REST method with name explicitSpan + // + // Further, the injected span in the REST resource should match span 2 above (the span added by the Helidon MP filter) + + assertThat("Response from test resource", response.getStatus(), is(equalTo(200))); + + assertThat("Number of spans recorded by listener", testSpanListener.spansStarted, hasSize(3)); + + Span outgoingClientSpan = testSpanListener.spansStarted.getFirst(); + Span incomingServerSpanFromFilter = testSpanListener.spansStarted.get(1); + Span explicitlyCreatedSpan = testSpanListener.spansStarted.getLast(); + + io.opentelemetry.api.trace.SpanContext incomingServerRequestNativeSpanContextViaInjection = + testResource.copyOfInjectedOtelSpanContext(); + + io.opentelemetry.api.trace.Span incomingServerRequestNativeSpanFromFilterViaListener = + incomingServerSpanFromFilter.unwrap(io.opentelemetry.api.trace.Span.class); + assertThat("Base native span context via injection vs. via listener", + incomingServerRequestNativeSpanContextViaInjection, + is(equalTo(incomingServerRequestNativeSpanFromFilterViaListener.getSpanContext()))); + + assertThat("Parent of explicitly-created span", explicitlyCreatedSpan.unwrap(ReadableSpan.class).getParentSpanContext(), + equalTo(incomingServerRequestNativeSpanContextViaInjection)); + + } + + @Test + void checkTypesOfInjectedFields() { + + assertThat("Injected tracer", + testResource.injectedOtelTracer(), + allOf(instanceOf(io.opentelemetry.api.trace.Tracer.class), + instanceOf(WrappedProducer.WrappedTracer.class))); + + assertThat("Injected span", testResource.injectedOtelSpan(), allOf(instanceOf(io.opentelemetry.api.trace.Span.class), + instanceOf(Wrapper.class))); + } + + @Path("/test") + public static class TestResource { + + SpanContext copyOfInjectedOtelSpanContext; + + @Inject + private io.opentelemetry.api.trace.Tracer injectedOtelTracer; + + @Inject + private io.opentelemetry.api.trace.Span injectedOtelSpan; + + @WithSpan + void go() throws InterruptedException { + Thread.sleep(500); + } + + io.opentelemetry.api.trace.Tracer injectedOtelTracer() { + return injectedOtelTracer; + } + + io.opentelemetry.api.trace.Span injectedOtelSpan() { + return injectedOtelSpan; + } + + io.opentelemetry.api.trace.SpanContext copyOfInjectedOtelSpanContext() { + return copyOfInjectedOtelSpanContext; + } + + @Path("/work") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String workWithInjectedTracerAndSpan() throws InterruptedException { + copyOfInjectedOtelSpanContext = injectedOtelSpan.getSpanContext(); + io.opentelemetry.api.trace.SpanBuilder otelSpanBuilder = injectedOtelTracer.spanBuilder("explicitSpan"); + io.opentelemetry.api.trace.Span otelSpan = otelSpanBuilder.startSpan(); + + try (io.opentelemetry.context.Scope ignored = otelSpan.makeCurrent()) { + Thread.sleep(200); + } + otelSpan.end(); + + injectedOtelSpan.setAttribute("marker", true); + return "worked"; + } + } + + private static class TestSpanListener implements SpanListener { + + private final List> spanBuildersStarting = new ArrayList<>(); + + private final List spansStarted = new ArrayList<>(); + + private final List spansActivated = new ArrayList<>(); + + private final List scopesActivated = new ArrayList<>(); + + private final List spansClosed = new ArrayList<>(); + private final List scopesClosed = new ArrayList<>(); + + private final List spansEnded = new ArrayList<>(); + + void clear() { + spanBuildersStarting.clear(); + + spansStarted.clear(); + + spansActivated.clear(); + scopesActivated.clear(); + + spansClosed.clear(); + scopesClosed.clear(); + + spansEnded.clear(); + } + + @Override + public void starting(Span.Builder spanBuilder) { + spanBuildersStarting.add(spanBuilder); + } + + @Override + public void started(Span span) { + spansStarted.add(span); + } + + @Override + public void activated(Span span, Scope scope) { + spansActivated.add(span); + scopesActivated.add(scope); + } + + @Override + public void closed(Span span, Scope scope) { + spansClosed.add(span); + scopesClosed.add(scope); + } + + @Override + public void ended(Span span) { + spansEnded.add(span); + } + + int starting() { + return spanBuildersStarting.size(); + } + + List> spanBuilderStarting() { + return spanBuildersStarting; + } + + int started() { + return spansStarted.size(); + } + + List spanStarted() { + return spansStarted; + } + + int activated() { + return spansActivated.size(); + } + + List spanActivated() { + return spansActivated; + } + + List scopeActivated() { + return scopesActivated; + } + + int closed() { + return spansClosed.size(); + } + + List spanClosed() { + return spansClosed; + } + + List scopeClosed() { + return scopesClosed; + } + + int ended() { + return spansEnded.size(); + } + + List spanEnded() { + return spansEnded; + } + } +} diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/HelidonOpenTelemetry.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/HelidonOpenTelemetry.java index 23474c8f918..35fc8ae35d6 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/HelidonOpenTelemetry.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/HelidonOpenTelemetry.java @@ -32,6 +32,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import static io.opentelemetry.context.Context.current; @@ -106,6 +107,16 @@ public static io.helidon.tracing.Span.Builder create(SpanBuilder spanBuilder, return new OpenTelemetrySpanBuilder(spanBuilder, helidonTracer.unwrap(OpenTelemetryTracer.class).spanListeners()); } + /** + * Wrap an Open Telemetry context.. + * + * @param context Open Telemetry context + * @return Helidon {@link io.helidon.tracing.SpanContext} + */ + public static io.helidon.tracing.SpanContext create(Context context) { + return new OpenTelemetrySpanContext(context); + } + /** * Check if OpenTelemetry is present by indirect properties. * This class does best explicit check if OTEL_AGENT_PRESENT_PROPERTY config property is set and uses its @@ -169,6 +180,19 @@ private static boolean checkContext() { } + /** + * Invokes listeners known to the specified Helidon span using the provided operation. + * + * @param helidonSpan Helidon {@link io.helidon.tracing.Span} whose listeners are to be invoked + * @param logger logger for reporting exceptions during listener invocations + * @param operation operation to invoke on each listener + */ + public static void invokeListeners(io.helidon.tracing.Span helidonSpan, + System.Logger logger, + Consumer operation) { + invokeListeners(helidonSpan.unwrap(OpenTelemetrySpan.class).spanListeners(), logger, operation); + } + static void invokeListeners(List spanListeners, System.Logger logger, Consumer operation) { if (spanListeners.isEmpty()) { return; diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java index 6ff0f54eff4..42fd31325aa 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java @@ -37,6 +37,19 @@ class OpenTelemetryScope implements Scope { this.spanListeners = spanListeners; } + /** + * Creates a new Helidon {@link io.helidon.tracing.Scope} which wraps an existing OTel {@link io.opentelemetry.context.Scope}. + * + * @param helidonTracer Helidon tracer + * @param span Helidon span + * @param scope OTel scope + */ + OpenTelemetryScope(OpenTelemetryTracer helidonTracer, + OpenTelemetrySpan span, + io.opentelemetry.context.Scope scope) { + this(span, scope, helidonTracer.spanListeners()); + } + @Override public void close() { if (closed.compareAndSet(false, true) && delegate != null) { diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java index 6aea08718b6..b70d50afeb1 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java @@ -25,6 +25,7 @@ import io.helidon.tracing.Span; import io.helidon.tracing.SpanContext; import io.helidon.tracing.SpanListener; +import io.helidon.tracing.Tracer; import io.helidon.tracing.WritableBaggage; import io.opentelemetry.api.baggage.Baggage; @@ -44,6 +45,22 @@ class OpenTelemetrySpan implements Span { this(span, new MutableOpenTelemetryBaggage(), spanListeners); } + /** + * Creates a Helidon wrapper around an OTel span. + *

+ * This constructor can be used to create a Helidon span when producing an OTel span for injection of the current + * OTel span. Under OTel semantics if there is no current span then a request for it returns a no-op span. The Helidon + * semantics for span listeners is that they are not notified for no-op spans. So this constructor allows us to create + * a Helidon wrapper span with the correct listener semantics, accounting for whether the OTel span is no-op or not. + * + * @param helidonTracer the Helidon tracer + * @param span the native OTel span + * @param isNoop whether the OTel span is a no-op + */ + OpenTelemetrySpan(Tracer helidonTracer, io.opentelemetry.api.trace.Span span, boolean isNoop) { + this(span, isNoop ? List.of() : helidonTracer.unwrap(OpenTelemetryTracer.class).spanListeners()); + } + OpenTelemetrySpan(io.opentelemetry.api.trace.Span span, Baggage baggage, List spanListeners) { delegate = span; this.baggage = baggage; @@ -147,6 +164,10 @@ public T unwrap(Class spanClass) { + ", telemetry span is: " + delegate.getClass().getName()); } + List spanListeners() { + return List.copyOf(spanListeners); + } + Limited limited() { if (limited != null) { return limited; diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java index 67b4dfb1eb0..988e2bdd20f 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java @@ -25,6 +25,7 @@ import io.helidon.common.Weighted; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; +import io.helidon.tracing.Scope; import io.helidon.tracing.Span; import io.helidon.tracing.Tracer; import io.helidon.tracing.TracerBuilder; @@ -115,6 +116,44 @@ public static Optional activeSpan() { return Optional.of(HelidonOpenTelemetry.create(otelSpan, otelBaggage)); } + /** + * Returns a Helidon {@link io.helidon.tracing.Tracer} which wraps the provided OpenTelemetry + * {@link io.opentelemetry.api.trace.Tracer}. + * + * @param openTelemetryTracer native OTel tracer to wrap + * @return Helidon tracer wrapping the native OTel tracer + */ + public static Tracer tracer(io.opentelemetry.api.trace.Tracer openTelemetryTracer) { + return new OpenTelemetryTracer(GlobalOpenTelemetry.get(), openTelemetryTracer, Map.of()); + } + + /** + * Returns a Helidon {@link io.helidon.tracing.Span} which wraps the provided OpenTelemetry + * {@link io.opentelemetry.api.trace.Span}. + * + * @param openTelemetrySpan native OTel span to wrap + * @param isNoop whether the native span is a no-op span + * @return Helidon span wrapping the native OTel span + */ + public static Span span(Tracer helidonTracer, io.opentelemetry.api.trace.Span openTelemetrySpan, boolean isNoop) { + return new OpenTelemetrySpan(helidonTracer, openTelemetrySpan, isNoop); + } + + /** + * Returns a Helidon {@link io.helidon.tracing.Scope} which wraps the provided OpenTelemetry + * {@link io.opentelemetry.context.Scope}. + * + * @param helidonTracer Helidon {@link io.helidon.tracing.Tracer} + * @param helidonSpan Helidon {@link io.helidon.tracing.Span} associated with the scope + * @param openTelemetryScope OpenTelemetry {@code Scope} to be wrapped + * @return Helidon {@link Scope} wrapping the OpenTelemetry {@code Scope} + */ + public static Scope scope(Tracer helidonTracer, Span helidonSpan, io.opentelemetry.context.Scope openTelemetryScope) { + return new OpenTelemetryScope(helidonTracer.unwrap(OpenTelemetryTracer.class), + helidonSpan.unwrap(OpenTelemetrySpan.class), + openTelemetryScope); + } + @Override public TracerBuilder createBuilder() { return OpenTelemetryTracer.builder(); diff --git a/tracing/tracing/src/main/java/io/helidon/tracing/Scope.java b/tracing/tracing/src/main/java/io/helidon/tracing/Scope.java index b5011f6084a..870c0d5308c 100644 --- a/tracing/tracing/src/main/java/io/helidon/tracing/Scope.java +++ b/tracing/tracing/src/main/java/io/helidon/tracing/Scope.java @@ -27,4 +27,22 @@ public interface Scope extends AutoCloseable { * @return if this scope is closed */ boolean isClosed(); + + /** + * Access the underlying scope by specific type. + * This is a dangerous operation that will succeed only if the scope is of expected type. This practically + * removes abstraction capabilities of this API. + * + * @param scopeClass type to access + * @param type of the scope + * @return instance of the scope + * @throws java.lang.IllegalArgumentException in case the scope cannot provide the expected type + */ + default T unwrap(Class scopeClass) { + try { + return scopeClass.cast(this); + } catch (ClassCastException e) { + throw new IllegalArgumentException("This scope is not compatible with " + scopeClass.getName()); + } + } } diff --git a/tracing/tracing/src/main/java/io/helidon/tracing/Wrapper.java b/tracing/tracing/src/main/java/io/helidon/tracing/Wrapper.java new file mode 100644 index 00000000000..c39818064f5 --- /dev/null +++ b/tracing/tracing/src/main/java/io/helidon/tracing/Wrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 + * + * 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 io.helidon.tracing; + +/** + * Behavior of a type that wraps a related type, typically through delegation. + */ +public interface Wrapper { + + /** + * Unwraps the delegate as the specified type. + * + * @param c {@link Class} to which to cast the delegate + * @param type to cast to + * @return the delegate cast as the requested type + * @throws java.lang.ClassCastException if the delegate is not compatible with the requested type + */ + R unwrap(Class c); +} From 0fe2a361e9de681a94046c67afb0c32a3b4494af Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Wed, 18 Dec 2024 11:28:53 -0600 Subject: [PATCH 4/4] Add doc update --- docs/src/main/asciidoc/mp/telemetry.adoc | 15 +++++++++++++++ microprofile/telemetry/pom.xml | 6 ++++++ .../telemetry/TelemetryConfigBlueprint.java | 6 ++++++ .../OpenTelemetryTracerProvider.java | 1 + 4 files changed, 28 insertions(+) diff --git a/docs/src/main/asciidoc/mp/telemetry.adoc b/docs/src/main/asciidoc/mp/telemetry.adoc index 2be45e916b9..4e545a9abbd 100644 --- a/docs/src/main/asciidoc/mp/telemetry.adoc +++ b/docs/src/main/asciidoc/mp/telemetry.adoc @@ -204,6 +204,15 @@ include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_6, indent=0] include::{rootdir}/includes/tracing/common-callbacks.adoc[tags=defs;detailed,leveloffset=+1] +==== Using Span Listeners with Injection +By default, Helidon _does not_ notify registered span listeners of life cycle events that involve an injected OpenTelemetry `Tracer` or `Span`. + +To enable this notification for injected telemetry objects configure `telemetry.injection-type` to `neutral` (overriding the default of `native`). See the <> section for more information. Helidon then injects wrappers around the OpenTelemetry native objects instead of the OpenTelemetry native objects themselves. The wrappers notify registered listeners of life cycle changes. + +Note that although the injected neutral objects implement the corresponding OpenTelemetry interfaces, they _are not_ type-compatible with the underlying OpenTelemetry implementation types, so code that uses `instanceof` or other type-sensitive logic that refers to OpenTelemetry implementation types will not work. The injected neutral objects implement the +link:{tracing-javadoc-base-url}/io/helidon/tracing/Wrapper.html[`io.helidon.tracing.Wrapper` interface] +which declares the `unwrap` method. You can check and cast an injected neutral object to `Wrapper` and invokes its `unwrap` method to get access to the underlying OpenTelemetry object. + === Controlling Automatic Span Creation By default, Helidon MP Telemetry creates a new child span for each incoming REST request and for each outgoing REST client request. You can selectively control if Helidon creates these automatic spans on a request-by-request basis by adding a very small amount of code to your project. @@ -262,6 +271,12 @@ The OpenTelemetry Java Agent may influence the work of MicroProfile Telemetry, o This way, Helidon will explicitly get all the configuration and objects from the Agent, thus allowing correct span hierarchy settings. +=== Helidon Telemetry Configuration + +include::{rootdir}/config/io_helidon_microprofile_telemetry_TelemetryConfig.adoc[leveloffset=+1,tag=config] + +The <> section describes the effect of this configuration. + [[examples]] == Examples diff --git a/microprofile/telemetry/pom.xml b/microprofile/telemetry/pom.xml index f584e2f966f..7f08dc2df5c 100644 --- a/microprofile/telemetry/pom.xml +++ b/microprofile/telemetry/pom.xml @@ -146,6 +146,12 @@ helidon-builder-codegen ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java index 44c79c3f941..be27465b011 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryConfigBlueprint.java @@ -30,6 +30,12 @@ interface TelemetryConfigBlueprint { */ String TELEMETRY_CONFIG_KEY = "telemetry"; + /** + * Injection type for injected OpenTelemetry objects such as {@link io.opentelemetry.api.trace.Tracer} and + * {@link io.opentelemetry.api.trace.Span}. + * + * @return injection type + */ @Option.Configured @Option.Default(InjectionType.DEFAULT) InjectionType injectionType(); diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java index 988e2bdd20f..f638299385f 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java @@ -131,6 +131,7 @@ public static Tracer tracer(io.opentelemetry.api.trace.Tracer openTelemetryTrace * Returns a Helidon {@link io.helidon.tracing.Span} which wraps the provided OpenTelemetry * {@link io.opentelemetry.api.trace.Span}. * + * @param helidonTracer the Helidon tracer from which to create the span * @param openTelemetrySpan native OTel span to wrap * @param isNoop whether the native span is a no-op span * @return Helidon span wrapping the native OTel span