Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil;
import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties;
import io.opentelemetry.instrumentation.api.internal.InstrumentationCustomizer;
import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess;
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider;
import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil;
import io.opentelemetry.instrumentation.api.internal.SpanKey;
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -51,7 +56,7 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {

final OpenTelemetry openTelemetry;
final String instrumentationName;
final SpanNameExtractor<? super REQUEST> spanNameExtractor;
SpanNameExtractor<? super REQUEST> spanNameExtractor;

final List<SpanLinksExtractor<? super REQUEST>> spanLinksExtractors = new ArrayList<>();
final List<AttributesExtractor<? super REQUEST, ? super RESPONSE>> attributesExtractors =
Expand All @@ -69,6 +74,19 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
boolean propagateOperationListenersToOnEnd = false;
boolean enabled = true;

private static final Map<Predicate<String>, List<InstrumentationCustomizer>>
INSTRUMENTATION_CUSTOMIZER_MAP = new HashMap<>();

static {
List<InstrumentationCustomizer> customizers =
ServiceLoaderUtil.load(InstrumentationCustomizer.class);
for (InstrumentationCustomizer customizer : customizers) {
INSTRUMENTATION_CUSTOMIZER_MAP
.computeIfAbsent(customizer.instrumentationNamePredicate(), k -> new ArrayList<>())
.add(customizer);
}
}

InstrumenterBuilder(
OpenTelemetry openTelemetry,
String instrumentationName,
Expand Down Expand Up @@ -281,6 +299,30 @@ public Instrumenter<REQUEST, RESPONSE> buildInstrumenter(
private Instrumenter<REQUEST, RESPONSE> buildInstrumenter(
InstrumenterConstructor<REQUEST, RESPONSE> constructor,
SpanKindExtractor<? super REQUEST> spanKindExtractor) {
List<InstrumentationCustomizer> customizers =
INSTRUMENTATION_CUSTOMIZER_MAP.entrySet().stream()
.filter(entry -> entry.getKey().test(instrumentationName))
.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toList());
if (customizers != null) {
for (InstrumentationCustomizer customizer : customizers) {
if (customizer.getContextCustomizer() != null) {
addContextCustomizer(customizer.getContextCustomizer());
}
if (customizer.getAttributesExtractor() != null) {
addAttributesExtractor(customizer.getAttributesExtractor());
}
if (customizer.getAttributesExtractors() != null) {
addAttributesExtractors(customizer.getAttributesExtractors());
}
if (customizer.getOperationMetrics() != null) {
addOperationMetrics(customizer.getOperationMetrics());
}
if (customizer.getSpanNameExtractor() != null) {
this.spanNameExtractor = customizer.getSpanNameExtractor();
}
}
}
this.spanKindExtractor = spanKindExtractor;
return constructor.create(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.internal;

import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import java.util.List;
import java.util.function.Predicate;
import javax.annotation.Nullable;

/**
* A service provider interface (SPI) for providing customizations for instrumentation, including
* operation metrics, attributes extraction, and context customization.
*
* <p>This allows external modules or plugins to contribute custom logic for specific instrumented
* libraries, without modifying core instrumentation code. This class is internal and is hence not
* for public use. Its APIs are unstable and can change at any time.
*/
public interface InstrumentationCustomizer {

/**
* Returns a predicate that determines whether this customizer supports a given instrumentation
* name.
*
* <p>The customizer will only be applied if the current instrumentation name matches the
* predicate. For example, the predicate might match names like "io.opentelemetry.netty-3.8" or
* "io.opentelemetry.apache-httpclient-4.3".
*
* @return a predicate for supported instrumentation names
*/
Predicate<String> instrumentationNamePredicate();

/**
* Returns a new instance of an {@link OperationMetrics} that will record metrics for the
* instrumented operation.
*
* @return an operation metrics instance, or null if not applicable
*/
default OperationMetrics getOperationMetrics() {
return null;
}

/**
* Returns a new instance of an {@link AttributesExtractor} that will extract attributes from
* requests and responses during the instrumentation process.
*
* @param <REQUEST> the type of request object used by the instrumented library
* @param <RESPONSE> the type of response object used by the instrumented library
* @return an attributes extractor instance, or null if not applicable
*/
default <REQUEST, RESPONSE> AttributesExtractor<REQUEST, RESPONSE> getAttributesExtractor() {
return null;
}

/**
* Returns a list of {@link AttributesExtractor}s that will extract attributes from requests and
* responses during the instrumentation process.
*
* <p>This allows providing multiple extractors for a single instrumentation. The default
* implementation returns {@code null} for backward compatibility.
*
* @param <REQUEST> the type of request object used by the instrumented library
* @param <RESPONSE> the type of response object used by the instrumented library
* @return a list of attributes extractors, or null if not applicable
*/
@Nullable
default <REQUEST, RESPONSE>
List<AttributesExtractor<REQUEST, RESPONSE>> getAttributesExtractors() {
return null;
}

/**
* Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context
* during request processing.
*
* @param <REQUEST> the type of request object used by the instrumented library
* @return a context customizer instance, or null if not applicable
*/
default <REQUEST> ContextCustomizer<REQUEST> getContextCustomizer() {
return null;
}

/**
* Returns a new instance of a {@link SpanNameExtractor} that will customize the span name during
* request processing.
*
* @param <REQUEST> the type of request object used by the instrumented library
* @return a customized {@link SpanNameExtractor}, or null if not applicable
*/
default <REQUEST> SpanNameExtractor<REQUEST> getSpanNameExtractor() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.internal;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.function.Function;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class ServiceLoaderUtil {

private static Function<Class<?>, List<?>> loaderFunction =
clazz -> {
List<Object> instances = new ArrayList<>();
ServiceLoader<?> serviceLoader = ServiceLoader.load(clazz);
for (Object instance : serviceLoader) {
instances.add(instance);
}
return instances;
};

private ServiceLoaderUtil() {
// Utility class, no instantiation
}

@SuppressWarnings("unchecked")
public static <T> List<T> load(Class<T> clazz) {
return (List<T>) loaderFunction.apply(clazz);
}

public static void setLoaderFunction(Function<Class<?>, List<?>> customLoaderFunction) {
loaderFunction = customLoaderFunction;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.internal;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;

import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class InstrumentationCustomizerTest {

@Mock private OperationMetrics operationMetrics;
@Mock private AttributesExtractor<Object, Object> attributesExtractor;
@Mock private ContextCustomizer<Object> contextCustomizer;
@Mock private SpanNameExtractor<Object> spanNameExtractor;

private InstrumentationCustomizer customizer;

@BeforeEach
@SuppressWarnings("unchecked")
void setUp() {
customizer =
new InstrumentationCustomizer() {

@Override
public Predicate<String> instrumentationNamePredicate() {
return name -> name.startsWith("test.instrumentation");
}

@Override
public OperationMetrics getOperationMetrics() {
return operationMetrics;
}

@Override
public <REQUEST, RESPONSE>
AttributesExtractor<REQUEST, RESPONSE> getAttributesExtractor() {
return (AttributesExtractor<REQUEST, RESPONSE>) attributesExtractor;
}

@Override
public <REQUEST, RESPONSE>
List<AttributesExtractor<REQUEST, RESPONSE>> getAttributesExtractors() {
return Collections.singletonList(
(AttributesExtractor<REQUEST, RESPONSE>) attributesExtractor);
}

@Override
public <REQUEST> ContextCustomizer<REQUEST> getContextCustomizer() {
return (ContextCustomizer<REQUEST>) contextCustomizer;
}

@Override
public <REQUEST> SpanNameExtractor<REQUEST> getSpanNameExtractor() {
return (SpanNameExtractor<REQUEST>) spanNameExtractor;
}
};
}

@Test
void testInstrumentationNamePredicate() {
assertThat(customizer.instrumentationNamePredicate().test("test.instrumentation.example"))
.isTrue();
assertThat(customizer.instrumentationNamePredicate().test("other.instrumentation.example"))
.isFalse();
}

@Test
void testGetOperationMetrics() {
OperationMetrics metrics = customizer.getOperationMetrics();
assertThat(metrics).isSameAs(operationMetrics);
}

@Test
void testGetAttributesExtractor() {
AttributesExtractor<Object, Object> extractor = customizer.getAttributesExtractor();
assertThat(extractor).isSameAs(attributesExtractor);
}

@Test
void testGetAttributesExtractors() {
List<AttributesExtractor<Object, Object>> extractors = customizer.getAttributesExtractors();
assertThat(extractors).containsExactly(attributesExtractor);
}

@Test
void testGetContextCustomizer() {
assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer);
}

@Test
void testGetSpanNameExtractor() {
SpanNameExtractor<Object> extractor = customizer.getSpanNameExtractor();
assertThat(extractor).isSameAs(spanNameExtractor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package io.opentelemetry.javaagent.tooling;

import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil;
import io.opentelemetry.instrumentation.api.internal.cache.weaklockfree.WeakConcurrentMapCleaner;
import io.opentelemetry.javaagent.bootstrap.AgentInitializer;
import io.opentelemetry.javaagent.bootstrap.AgentStarter;
Expand All @@ -16,6 +17,8 @@
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
Expand Down Expand Up @@ -72,6 +75,7 @@ public void start() {

EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create();
extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig);
setLoaderFunction();

String loggerImplementationName = earlyConfig.getString("otel.javaagent.logging");
// default to the built-in stderr slf4j-simple logger
Expand Down Expand Up @@ -246,4 +250,17 @@ public void visitMethodInsn(
return hookInserted ? cw.toByteArray() : null;
}
}

/** Sets a custom loader function for the ServiceLoaderUtil to use the agentClassLoader. */
private void setLoaderFunction() {
ServiceLoaderUtil.setLoaderFunction(
clazz -> {
List<Object> instances = new ArrayList<>();
ServiceLoader<?> serviceLoader = ServiceLoader.load(clazz, extensionClassLoader);
for (Object instance : serviceLoader) {
instances.add(instance);
}
return instances;
});
}
}
Loading