Skip to content

Commit

Permalink
Added support for opentelemetry WithSpan annotation (elastic#3406)
Browse files Browse the repository at this point in the history

---------

Co-authored-by: SylvainJuge <[email protected]>
  • Loading branch information
videnkz and SylvainJuge authored Nov 27, 2023
1 parent fd60ac8 commit 59da6ee
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
=== Unreleased
[float]
===== Features
* Added support for OpenTelemetry annotations - `WithSpan` and `SpanAttribute` - {pull}3406[#3406]
[[release-notes-1.x]]
=== Java Agent version 1.x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,8 @@ public String toSafeString(List<WildcardMatcher> value) {
.tags("added[1.25.0]")
.configurationCategory(CORE_CATEGORY)
.tags("performance")
.description("A boolean specifying if the agent should search the class hierarchy for public api annotations (`@CaptureTransaction`, `@CaptureSpan`, `@Traced`).\n " +
.description("A boolean specifying if the agent should search the class hierarchy for public api annotations (`@CaptureTransaction`, `@CaptureSpan`, `@Traced` and from 1.45.0 `@WithSpan`" +
").\n " +
"When set to `false`, a method is instrumented if it is annotated with a public api annotation.\n " +
"When set to `true` methods overriding annotated methods will be instrumented as well.\n " +
"Either way, methods will only be instrumented if they are included in the configured <<config-application-packages>>.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>${version.opentelemetry}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
import co.elastic.apm.agent.sdk.ElasticApmInstrumentation;
import net.bytebuddy.matcher.ElementMatcher;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass;

public abstract class AbstractOpenTelemetryInstrumentation extends ElasticApmInstrumentation {

@Override
public final ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
public ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
return classLoaderCanLoadClass("io.opentelemetry.context.propagation.TextMapSetter");
}

Expand All @@ -40,7 +40,7 @@ public final boolean includeWhenInstrumentationIsDisabled() {


@Override
public final Collection<String> getInstrumentationGroupNames() {
return Arrays.asList("opentelemetry");
public Collection<String> getInstrumentationGroupNames() {
return Collections.singletonList("opentelemetry");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent.opentelemetry;

import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration;
import co.elastic.apm.agent.opentelemetry.tracing.OTelHelper;
import co.elastic.apm.agent.sdk.bytebuddy.AnnotationValueOffsetMappingFactory;
import co.elastic.apm.agent.sdk.bytebuddy.SimpleMethodSignatureOffsetMappingFactory;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import co.elastic.apm.agent.tracer.AbstractSpan;
import co.elastic.apm.agent.tracer.ElasticContext;
import co.elastic.apm.agent.tracer.GlobalTracer;
import co.elastic.apm.agent.tracer.Outcome;
import co.elastic.apm.agent.tracer.Span;
import co.elastic.apm.agent.tracer.Tracer;
import co.elastic.apm.agent.tracer.configuration.CoreConfiguration;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass;
import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.isInAnyPackage;
import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.isProxy;
import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.overridesOrImplementsMethodThat;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;

public class WithSpanInstrumentation extends AbstractOpenTelemetryInstrumentation {
public static final Logger logger = LoggerFactory.getLogger(WithSpanInstrumentation.class);

protected static final Tracer tracer = GlobalTracer.get();

private final CoreConfiguration coreConfig;
private final StacktraceConfiguration stacktraceConfig;

public WithSpanInstrumentation(ElasticApmTracer tracer) {
coreConfig = tracer.getConfig(CoreConfiguration.class);
stacktraceConfig = tracer.getConfig(StacktraceConfiguration.class);
}

@Override
public final ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
return classLoaderCanLoadClass("io.opentelemetry.instrumentation.annotations.WithSpan");
}

@Override
public Collection<String> getInstrumentationGroupNames() {
List<String> ret = new ArrayList<>(super.getInstrumentationGroupNames());
ret.add("opentelemetry-annotations");
return ret;
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return isInAnyPackage(stacktraceConfig.getApplicationPackages(), ElementMatchers.<NamedElement>none())
.and(not(isProxy()))
.and(declaresMethod(getMethodMatcher()));
}

@Override
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
if (coreConfig.isEnablePublicApiAnnotationInheritance()) {
return overridesOrImplementsMethodThat(isAnnotatedWith(named("io.opentelemetry.instrumentation.annotations.WithSpan")));
}
return isAnnotatedWith(named("io.opentelemetry.instrumentation.annotations.WithSpan"));
}

@Override
public String getAdviceClassName() {
return "co.elastic.apm.agent.opentelemetry.WithSpanInstrumentation$AdviceClass";
}

public static class AdviceClass {
@Nullable
@Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
public static Object onMethodEnter(
@SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature,
@AnnotationValueOffsetMappingFactory.AnnotationValueExtractor(annotationClassName = "io.opentelemetry.instrumentation.annotations.WithSpan", method = "value") String spanName,
@AnnotationValueOffsetMappingFactory.AnnotationValueExtractor(annotationClassName = "io.opentelemetry.instrumentation.annotations.WithSpan", method = "kind") SpanKind otelKind,
@Advice.Origin Method method,
@Advice.AllArguments Object[] methodArguments) {

ElasticContext<?> activeContext = tracer.currentContext();
final AbstractSpan<?> parentSpan = activeContext.getSpan();
if (parentSpan == null) {
logger.debug("Not creating span for {} because there is no currently active span.", signature);
return null;
}
if (activeContext.shouldSkipChildSpanCreation()) {
// span limit reached means span will not be reported, thus we can optimize and skip creating one
logger.debug("Not creating span for {} because span limit is reached.", signature);
return null;
}

Span<?> span = activeContext.createSpan();
if (span == null) {
return null;
}

// process parameters that annotated with `io.opentelemetry.instrumentation.annotations.SpanAttribute` annotation
int argsLength = methodArguments.length;
if (argsLength > 0) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < argsLength; i++) {
Annotation[] parameterAnnotation = parameterAnnotations[i];
int parameterAnnotationLength = parameterAnnotation.length;
for (int j = 0; j < parameterAnnotationLength; j++) {
if (parameterAnnotation[j] instanceof SpanAttribute) {
SpanAttribute spanAttribute = (SpanAttribute) parameterAnnotation[j];
String attributeName = spanAttribute.value();
if (!attributeName.isEmpty()) {
span.withOtelAttribute(attributeName, methodArguments[i]);
}
break;
}
}
}
}

span.withName(spanName.isEmpty() ? signature : spanName)
.activate();

((co.elastic.apm.agent.impl.transaction.Span) span).withOtelKind(OTelHelper.map(otelKind));

return span;
}

@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
public static void onMethodExit(@Advice.Enter @Nullable Object span,
@Advice.Thrown @Nullable Throwable t) {
if (span instanceof Span<?>) {
((Span<?>) span)
.captureException(t)
.withOutcome(t != null ? Outcome.FAILURE : Outcome.SUCCESS)
.deactivate()
.end();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent.opentelemetry.tracing;

import co.elastic.apm.agent.impl.transaction.OTelSpanKind;
import io.opentelemetry.api.trace.SpanKind;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public final class OTelHelper {
private OTelHelper() {}

@Nonnull
public static OTelSpanKind map(@Nullable SpanKind kind) {
if (kind == null) {
return OTelSpanKind.INTERNAL;
} else {
return OTelSpanKind.valueOf(kind.name());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,7 @@ public Span startSpan() {
}
}

if (kind == null) {
span.withOtelKind(OTelSpanKind.INTERNAL);
} else {
span.withOtelKind(OTelSpanKind.valueOf(kind.name()));
}

span.withOtelKind(OTelHelper.map(kind));

// With OTel API, the status (bridged to outcome) should only be explicitly set, thus we have to set and use
// user outcome to provide higher priority and avoid inferring outcome from any reported exception
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
co.elastic.apm.agent.opentelemetry.GlobalOpenTelemetryInstrumentation
co.elastic.apm.agent.opentelemetry.ContextStorageInstrumentation
co.elastic.apm.agent.opentelemetry.ArrayBasedContextInstrumentation
co.elastic.apm.agent.opentelemetry.WithSpanInstrumentation
Loading

0 comments on commit 59da6ee

Please sign in to comment.