-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from levigo/feature/opentelemetry
Feature/opentelemetry
- Loading branch information
Showing
14 changed files
with
855 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>com.neverpile.commons</groupId> | ||
<artifactId>neverpile-commons</artifactId> | ||
<version>1.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>neverpile-commons-opentelemetry</artifactId> | ||
|
||
<properties> | ||
<opentelemetry.version>1.33.0</opentelemetry.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-aop</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>io.opentelemetry</groupId> | ||
<artifactId>opentelemetry-api</artifactId> | ||
<version>${opentelemetry.version}</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
</project> |
56 changes: 56 additions & 0 deletions
56
...ile-commons-opentelemetry/src/main/java/com/neverpile/common/opentelemetry/Attribute.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.neverpile.common.opentelemetry; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
import java.util.function.BiConsumer; | ||
import java.util.function.Function; | ||
|
||
/** | ||
* Parameter annotation used on methods annotated with {@link TraceInvocation} used to indicate that the | ||
* given method parameter should be used as the value of a span attribute. | ||
*/ | ||
@Target({ | ||
ElementType.PARAMETER, ElementType.ANNOTATION_TYPE | ||
}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface Attribute { | ||
class NoopMapper implements Function<Object, Object> { | ||
@Override | ||
public Object apply(final Object t) { | ||
return t; // just a dummy | ||
} | ||
} | ||
|
||
/** | ||
* The name of the attribute for a traced parameter. | ||
* @return The name of the attribute. | ||
*/ | ||
String name() default ""; | ||
|
||
/** | ||
* An optional implementation of a {@link Function} used to map from the argument value to the attribute | ||
* value. | ||
* @return Function to map a non-standard value for tracing. | ||
*/ | ||
Class<? extends Function<? extends Object, ? extends Object>> valueAdapter() default NoopMapper.class; | ||
|
||
interface AttributeExtractor<V> { | ||
void extract(V value, BiConsumer<String, Object> attributeCreator); | ||
} | ||
|
||
public static class NoopExtractor implements AttributeExtractor<Object> { | ||
@Override | ||
public void extract(final Object value, final BiConsumer<String, Object> a) { | ||
// just a dummy | ||
} | ||
} | ||
|
||
/** | ||
* An optional implementation of a {@link Function} used to map from the argument value to the attribute | ||
* value. | ||
* @return Function to map a non-standard value for tracing. | ||
*/ | ||
Class<? extends AttributeExtractor<?>> attributeExtractor() default NoopExtractor.class; | ||
} |
18 changes: 18 additions & 0 deletions
18
...mmons-opentelemetry/src/main/java/com/neverpile/common/opentelemetry/TraceInvocation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.neverpile.common.opentelemetry; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Annotation used on methods to indicate that a new OpenTelemetry span should be created around the | ||
* method invocation. Span attributes can be assigned from method parameters using {@link Attribute} | ||
*/ | ||
@Target({ | ||
ElementType.METHOD, ElementType.ANNOTATION_TYPE | ||
}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface TraceInvocation { | ||
String operationName() default ""; | ||
} |
162 changes: 162 additions & 0 deletions
162
...elemetry/src/main/java/com/neverpile/common/opentelemetry/aspect/OpenTelemetryAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
package com.neverpile.common.opentelemetry.aspect; | ||
|
||
import java.lang.reflect.Parameter; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.function.Function; | ||
|
||
import org.aspectj.lang.ProceedingJoinPoint; | ||
import org.aspectj.lang.annotation.Around; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.aspectj.lang.reflect.MethodSignature; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.BeanUtils; | ||
import org.springframework.core.DefaultParameterNameDiscoverer; | ||
import org.springframework.core.ParameterNameDiscoverer; | ||
import org.springframework.util.ClassUtils; | ||
import org.springframework.util.StringUtils; | ||
|
||
import com.neverpile.common.opentelemetry.Attribute; | ||
import com.neverpile.common.opentelemetry.Attribute.AttributeExtractor; | ||
import com.neverpile.common.opentelemetry.TraceInvocation; | ||
|
||
import io.opentelemetry.api.trace.Span; | ||
import io.opentelemetry.api.trace.StatusCode; | ||
import io.opentelemetry.api.trace.Tracer; | ||
import io.opentelemetry.context.Scope; | ||
|
||
import jakarta.annotation.PostConstruct; | ||
|
||
/** | ||
* An aspect used to create OpenTelemetry spans for calls to methods annotated with | ||
* {@link TraceInvocation}. | ||
*/ | ||
@Aspect | ||
public class OpenTelemetryAspect { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenTelemetryAspect.class); | ||
|
||
private final Tracer tracer; | ||
|
||
public OpenTelemetryAspect(Tracer tracer) { | ||
this.tracer = tracer; | ||
} | ||
|
||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); | ||
|
||
private final Map<Class<? extends Function<Object, Object>>, Function<Object, Object>> valueAdapterCache = new ConcurrentHashMap<>(); | ||
|
||
private final Map<Class<? extends AttributeExtractor<Object>>, AttributeExtractor<Object>> attributeExtractorCache = new ConcurrentHashMap<>(); | ||
|
||
@PostConstruct | ||
public void logActivation() { | ||
LOGGER.info("OpenTelemetry Tracer found - tracing of methods annotated with @TraceInvocation enabled"); | ||
} | ||
|
||
@Around("execution (@com.neverpile.common.opentelemetry.TraceInvocation * *.*(..))") | ||
public Object newSpanAround(final ProceedingJoinPoint joinPoint) throws Throwable { | ||
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); | ||
Object[] args = joinPoint.getArgs(); | ||
|
||
Span span = startSpan(signature, joinPoint.getThis()); | ||
|
||
resolveParameters(signature, args, span); | ||
|
||
try (Scope scope = span.makeCurrent()) { | ||
return joinPoint.proceed(args); | ||
} catch (Throwable ex) { | ||
span.setStatus(StatusCode.ERROR, ex.getMessage()); | ||
span.recordException(ex); | ||
throw ex; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
|
||
private void resolveParameters(final MethodSignature signature, final Object[] args, final Span span) { | ||
Parameter[] parameters = signature.getMethod().getParameters(); | ||
for (int i = 0; i < parameters.length; i++) { | ||
if (parameters[i].getAnnotation(Attribute.class) != null) { | ||
setupAttribute(signature, i, parameters[i], args[i], span); | ||
} | ||
} | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private void setupAttribute(final MethodSignature signature, final int parameterIndex, final Parameter parameter, | ||
final Object arg, final Span span) { | ||
Attribute annotation = parameter.getAnnotation(Attribute.class); | ||
|
||
// if we have a value extractor, use it | ||
if (!annotation.attributeExtractor().equals(Attribute.NoopExtractor.class)) { | ||
if (!annotation.valueAdapter().equals(Attribute.NoopMapper.class)) | ||
LOGGER.warn("@Attribute.attributeExtractor and @Attribute.valueAdapter are mutually exclusive"); | ||
attributeExtractorCache // | ||
.computeIfAbsent((Class<? extends AttributeExtractor<Object>>) annotation.attributeExtractor(), | ||
c -> (AttributeExtractor<Object>) BeanUtils.instantiateClass(c)) // | ||
.extract(arg, (key, value) -> setAttribute(span, key, value)); | ||
|
||
} else { | ||
// otherwise build a single attribute | ||
String attributeKey = StringUtils.hasText(annotation.name()) | ||
? annotation.name() | ||
: findParameterName(signature, parameterIndex); | ||
Object value = arg; | ||
|
||
// if we have a value mapper, apply it to the value | ||
if (!annotation.valueAdapter().equals(Attribute.NoopMapper.class)) { | ||
value = valueAdapterCache // | ||
.computeIfAbsent((Class<? extends Function<Object, Object>>) annotation.valueAdapter(), | ||
c -> (Function<Object, Object>) BeanUtils.instantiateClass(c)) // | ||
.apply(value); | ||
} | ||
|
||
setAttribute(span, attributeKey, value); | ||
} | ||
} | ||
|
||
private String findParameterName(final MethodSignature signature, final int parameterIndex) { | ||
String[] parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod()); | ||
if (null == parameterNames || parameterNames.length <= parameterIndex) { | ||
LOGGER.warn("Can't determine a parameter name for {}", signature.getMethod()); | ||
return "<unknown>"; | ||
} | ||
|
||
return parameterNames[parameterIndex]; | ||
} | ||
|
||
private void setAttribute(final Span span, final String key, final Object value) { | ||
if (null == value) | ||
span.setAttribute(key, "<NULL>"); | ||
else if (value instanceof Long longValue) | ||
span.setAttribute(key, longValue); | ||
else if (value instanceof Double doubleValue) | ||
span.setAttribute(key, doubleValue); | ||
else if (value instanceof Integer integerValue) | ||
span.setAttribute(key, integerValue); | ||
else if (value instanceof Boolean booleanValue) | ||
span.setAttribute(key, booleanValue); | ||
else | ||
span.setAttribute(key, value.toString()); | ||
} | ||
|
||
private Span startSpan(final MethodSignature signature, final Object target) { | ||
String operationName = getOperationName(signature, target); | ||
// NOTE: setParent(...) is not required; `Span.current()` is automatically added as the parent | ||
return tracer.spanBuilder(operationName).startSpan(); | ||
} | ||
|
||
private String getOperationName(final MethodSignature signature, final Object target) { | ||
String operationName; | ||
TraceInvocation newSpanAnnotation = signature.getMethod().getAnnotation(TraceInvocation.class); | ||
if (!StringUtils.hasText(newSpanAnnotation.operationName())) { | ||
operationName = (target != null | ||
? ClassUtils.getUserClass(target).getSimpleName() | ||
: signature.getDeclaringType().getSimpleName()) + "." + signature.getName(); | ||
} else { | ||
operationName = newSpanAnnotation.operationName(); | ||
} | ||
|
||
return operationName; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
.../java/com/neverpile/common/opentelemetry/aspect/OpenTelemetryAspectAutoConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.neverpile.common.opentelemetry.aspect; | ||
|
||
import org.springframework.boot.autoconfigure.AutoConfigureOrder; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.core.Ordered; | ||
|
||
import io.opentelemetry.api.trace.Tracer; | ||
|
||
@Configuration | ||
// Configure as late as possible, after any tracer has been created | ||
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) | ||
@ConditionalOnBean(Tracer.class) | ||
public class OpenTelemetryAspectAutoConfiguration { | ||
@Bean | ||
public OpenTelemetryAspect openTelemetryAspect(Tracer tracer) { | ||
return new OpenTelemetryAspect(tracer); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
...esources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
com.neverpile.common.opentelemetry.aspect.OpenTelemetryAspectAutoConfiguration |
96 changes: 96 additions & 0 deletions
96
...entelemetry/src/test/java/com/neverpile/common/opentelemetry/OpenTelemetryAspectTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package com.neverpile.common.opentelemetry; | ||
|
||
import static org.assertj.core.api.Assertions.*; | ||
|
||
import java.util.HashMap; | ||
|
||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
|
||
@SpringBootTest | ||
class OpenTelemetryAspectTest { | ||
@Autowired | ||
TestTracer tt; | ||
|
||
@Autowired | ||
SomeTracedService service; | ||
|
||
@BeforeEach | ||
public void reset() { | ||
service.reset(); | ||
tt.getFinishedSpans().clear(); | ||
} | ||
|
||
@Test | ||
void testNoParams() { | ||
service.noParams(); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getOperationName()).isEqualTo("SomeTracedService.noParams"); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()).isEmpty(); | ||
} | ||
|
||
@Test | ||
void testRenamedOp() { | ||
service.weirdMethodNameReplacedByOperationName(); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getOperationName()).isEqualTo("foo"); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()).isEmpty(); | ||
} | ||
|
||
@Test | ||
void testParams() { | ||
service.someParams("hello", 4711, true); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()) // | ||
.containsEntry("foo", "hello") // | ||
.containsEntry("bar", 4711L) // | ||
.containsEntry("baz", true); | ||
} | ||
|
||
@Test | ||
void testSelfNamingParam() { | ||
service.selfNamingParam("hello"); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()) // | ||
.containsEntry("foo", "hello"); // | ||
} | ||
|
||
@Test | ||
void testPartialParams() { | ||
service.someMoreParams("hello", 4711, true); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()).containsEntry("foo", "hello").hasSize(1); | ||
} | ||
|
||
@Test | ||
void testAttributeExtractor() { | ||
HashMap<String, Object> m = new HashMap<>(); | ||
m.put("foo", "bar"); | ||
m.put("bar", 1234L); | ||
m.put("baz", 1234.56d); | ||
m.put("buz", false); | ||
|
||
service.parameterWithExtractor(m); | ||
|
||
assertThat(service.getInvocationCounter()).isEqualTo(1); | ||
assertThat(tt.getFinishedSpans()).hasSize(1); | ||
assertThat(tt.getFinishedSpans().get(0).getAttributes()) // | ||
.containsEntry("foo", "bar") // | ||
.containsEntry("bar", 1234L) // | ||
.containsEntry("baz", 1234.56d) // | ||
.containsEntry("buz", false); | ||
} | ||
} |
Oops, something went wrong.