Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,6 +5,9 @@

package io.opentelemetry.common;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;

/** A loader for components that are discovered via SPI. */
Expand All @@ -25,4 +28,10 @@ public interface ComponentLoader {
static ComponentLoader forClassLoader(ClassLoader classLoader) {
return new ServiceLoaderComponentLoader(classLoader);
}

static <T> List<T> loadList(ComponentLoader componentLoader, Class<T> spiClass) {
List<T> result = new ArrayList<>();
componentLoader.load(spiClass).forEach(result::add);
return Collections.unmodifiableList(result);
}
}
5 changes: 4 additions & 1 deletion docs/apidiffs/current_vs_latest/opentelemetry-common.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
Comparing source compatibility of opentelemetry-common-1.61.0-SNAPSHOT.jar against opentelemetry-common-1.60.1.jar
No changes.
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.common.ComponentLoader (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.List<T> loadList(io.opentelemetry.common.ComponentLoader, java.lang.Class<T>)
GENERIC TEMPLATES: +++ T:java.lang.Object
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
Comparing source compatibility of opentelemetry-sdk-extension-autoconfigure-spi-1.61.0-SNAPSHOT.jar against opentelemetry-sdk-extension-autoconfigure-spi-1.60.1.jar
No changes.
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.autoconfigure.spi.Ordered (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.List<T> loadOrderedList(io.opentelemetry.common.ComponentLoader, java.lang.Class<T>)
GENERIC TEMPLATES: +++ T:io.opentelemetry.sdk.autoconfigure.spi.Ordered
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

package io.opentelemetry.sdk.autoconfigure.spi;

import io.opentelemetry.common.ComponentLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
* Interface to be extended by SPIs that need to guarantee ordering during loading.
*
Expand All @@ -20,4 +26,11 @@ public interface Ordered {
default int order() {
return 0;
}

static <T extends Ordered> List<T> loadOrderedList(
ComponentLoader componentLoader, Class<T> spiClass) {
List<T> result = new ArrayList<>(ComponentLoader.loadList(componentLoader, spiClass));
result.sort(Comparator.comparing(Ordered::order));
return Collections.unmodifiableList(result);
}
}
1 change: 1 addition & 0 deletions sdk-extensions/autoconfigure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {
api(project(":sdk-extensions:autoconfigure-spi"))

compileOnly(project(":api:incubator"))
compileOnly(project(":sdk-extensions:incubator"))

annotationProcessor("com.google.auto.value:auto-value")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.autoconfigure;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.resources.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
* Creates an OpenTelemetry {@link Resource} from environment configuration.
*
* <p>This class is intentionally self-contained (no dependencies on other autoconfigure-internal
* classes) so that it can be copied wholesale into declarative configuration without pulling in
* additional dependencies. Do not add dependencies on non-API, non-SPI classes.
*/
final class EnvironmentResource {

private static final AttributeKey<String> SERVICE_NAME = AttributeKey.stringKey("service.name");

// Visible for testing
static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes";
static final String SERVICE_NAME_PROPERTY = "otel.service.name";

/**
* Create a {@link Resource} from the environment. The resource contains attributes parsed from
* environment variables and system property keys {@code otel.resource.attributes} and {@code
* otel.service.name}.
*
* @param config the {@link ConfigProperties} used to obtain resource properties
* @return the resource.
*/
@SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10
static Resource createEnvironmentResource(ConfigProperties config) {
AttributesBuilder resourceAttributes = Attributes.builder();
for (Map.Entry<String, String> entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) {
resourceAttributes.put(
entry.getKey(),
// Attributes specified via otel.resource.attributes follow the W3C Baggage spec and
// characters outside the baggage-octet range are percent encoded
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
decodeResourceAttributes(entry.getValue()));
}
String serviceName = config.getString(SERVICE_NAME_PROPERTY);
if (serviceName != null) {
resourceAttributes.put(SERVICE_NAME, serviceName);
}

return Resource.create(resourceAttributes.build());
}

/**
* Decodes percent-encoded characters in resource attribute values per W3C Baggage spec.
*
* <p>Unlike {@link java.net.URLDecoder}, this method:
*
* <ul>
* <li>Preserves '+' as a literal plus sign (URLDecoder decodes '+' as space)
* <li>Preserves invalid percent sequences as literals (e.g., "%2G", "%", "%2")
* <li>Supports multi-byte UTF-8 sequences (e.g., "%C3%A9" decodes to "é")
* </ul>
*
* @param value the percent-encoded string
* @return the decoded string
*/
private static String decodeResourceAttributes(String value) {
// no percent signs means nothing to decode
if (value.indexOf('%') < 0) {
return value;
}

int n = value.length();
// Use byte array to properly handle multi-byte UTF-8 sequences
byte[] bytes = new byte[n];
int pos = 0;

for (int i = 0; i < n; i++) {
char c = value.charAt(i);
// Check for percent-encoded sequence i.e. '%' followed by two hex digits
if (c == '%' && i + 2 < n) {
int d1 = Character.digit(value.charAt(i + 1), 16);
int d2 = Character.digit(value.charAt(i + 2), 16);
// Valid hex digits return 0-15, invalid returns -1
if (d1 != -1 && d2 != -1) {
// Combine two hex digits into a single byte (e.g., "2F" becomes 0x2F)
bytes[pos++] = (byte) ((d1 << 4) + d2);
// Skip the two hex digits (loop will also do i++)
i += 2;
continue;
}
}
// Keep '+' as '+' (unlike URLDecoder) and preserve invalid percent sequences which will be
// treated as literals
bytes[pos++] = (byte) c;
}
return new String(bytes, 0, pos, StandardCharsets.UTF_8);
}

private EnvironmentResource() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@

import io.opentelemetry.api.incubator.config.DeclarativeConfigException;
import io.opentelemetry.common.ComponentLoader;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration;
import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel;
import io.opentelemetry.sdk.internal.ExtendedOpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.resources.Resource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.logging.Logger;
import javax.annotation.Nullable;

Expand All @@ -32,30 +35,14 @@ final class IncubatingUtil {

private IncubatingUtil() {}

// Visible for testing
interface Factory {
@Nullable
AutoConfiguredOpenTelemetrySdk create()
throws ClassNotFoundException,
NoSuchMethodException,
IllegalAccessException,
InvocationTargetException;
}

static AutoConfiguredOpenTelemetrySdk configureFromFile(
Logger logger, String configurationFile, ComponentLoader componentLoader) {
logger.fine("Autoconfiguring from configuration file: " + configurationFile);
try (FileInputStream fis = new FileInputStream(configurationFile)) {
return requireNonNull(
createWithFactory(
"file",
() ->
getOpenTelemetrySdk(
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration")
.getMethod("parse", InputStream.class)
.invoke(null, fis),
componentLoader)));
OpenTelemetryConfigurationModel model = DeclarativeConfiguration.parse(fis);
return create(model, componentLoader);
} catch (DeclarativeConfigException e) {
throw toConfigurationException(e);
} catch (FileNotFoundException e) {
throw new ConfigurationException("Configuration file not found", e);
} catch (IOException e) {
Expand All @@ -67,75 +54,34 @@ static AutoConfiguredOpenTelemetrySdk configureFromFile(

@Nullable
public static AutoConfiguredOpenTelemetrySdk configureFromSpi(ComponentLoader componentLoader) {
return createWithFactory(
"SPI",
() -> {
Class<?> providerClass =
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider");
Method getConfigurationModel = providerClass.getMethod("getConfigurationModel");

for (Object configProvider : componentLoader.load(providerClass)) {
Object model = getConfigurationModel.invoke(configProvider);
if (model != null) {
return getOpenTelemetrySdk(model, componentLoader);
}
}
return null;
});
for (DeclarativeConfigurationProvider provider :
componentLoader.load(DeclarativeConfigurationProvider.class)) {
OpenTelemetryConfigurationModel model = provider.getConfigurationModel();
if (model != null) {
return create(model, componentLoader);
}
}
return null;
}

private static AutoConfiguredOpenTelemetrySdk getOpenTelemetrySdk(
Object model, ComponentLoader componentLoader)
throws IllegalAccessException,
InvocationTargetException,
ClassNotFoundException,
NoSuchMethodException {

Class<?> openTelemetryConfiguration =
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel");
Class<?> declarativeConfiguration =
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration");

Class<?> contextClass =
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigContext");
Method createContext = contextClass.getDeclaredMethod("create", ComponentLoader.class);
createContext.setAccessible(true);
Object context = createContext.invoke(null, componentLoader);

Method create =
declarativeConfiguration.getDeclaredMethod(
"create", openTelemetryConfiguration, contextClass);
create.setAccessible(true);
OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model, context);

Method getResource = contextClass.getDeclaredMethod("getResource");
getResource.setAccessible(true);
Resource resource = (Resource) getResource.invoke(context);

return AutoConfiguredOpenTelemetrySdk.create(sdk, resource, null);
}
private static AutoConfiguredOpenTelemetrySdk create(
OpenTelemetryConfigurationModel model, ComponentLoader componentLoader) {
ExtendedOpenTelemetrySdk sdk;
try {
sdk = DeclarativeConfiguration.create(model, componentLoader);
} catch (DeclarativeConfigException e) {
throw toConfigurationException(e);
}

// Visible for testing
@Nullable
static AutoConfiguredOpenTelemetrySdk createWithFactory(String name, Factory factory) {
try {
return factory.create();
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
throw new ConfigurationException(
String.format(
"Error configuring from %s. Is opentelemetry-sdk-extension-incubator on the classpath?",
name),
e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof DeclarativeConfigException) {
throw toConfigurationException((DeclarativeConfigException) cause);
}
throw new ConfigurationException("Unexpected error configuring from " + name, e);
Field sharedState = SdkMeterProvider.class.getDeclaredField("sharedState");
sharedState.setAccessible(true);
Resource resource =
((MeterProviderSharedState) sharedState.get(sdk.getSdkMeterProvider())).getResource();

return AutoConfiguredOpenTelemetrySdk.create(sdk, resource, null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new ConfigurationException("Error resolving resource from ExtendedOpenTelemetrySdk", e);
}
}

Expand Down
Loading
Loading