Skip to content

Commit 8fcba19

Browse files
Break sdk incubator dependency on autoconfigure (#8242)
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com> Co-authored-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 7daa1f3 commit 8fcba19

37 files changed

Lines changed: 459 additions & 426 deletions

File tree

common/src/main/java/io/opentelemetry/common/ComponentLoader.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
package io.opentelemetry.common;
77

8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.List;
811
import java.util.ServiceLoader;
912

1013
/** A loader for components that are discovered via SPI. */
@@ -25,4 +28,14 @@ public interface ComponentLoader {
2528
static ComponentLoader forClassLoader(ClassLoader classLoader) {
2629
return new ServiceLoaderComponentLoader(classLoader);
2730
}
31+
32+
/**
33+
* Convenience method to load a list of SPI implementations rather than the iterable returned by
34+
* {@link #load(Class)}.
35+
*/
36+
static <T> List<T> loadList(ComponentLoader componentLoader, Class<T> spiClass) {
37+
List<T> result = new ArrayList<>();
38+
componentLoader.load(spiClass).forEach(result::add);
39+
return Collections.unmodifiableList(result);
40+
}
2841
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
Comparing source compatibility of opentelemetry-common-1.61.0-SNAPSHOT.jar against opentelemetry-common-1.60.1.jar
2-
No changes.
2+
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.common.ComponentLoader (not serializable)
3+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
4+
+++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.List<T> loadList(io.opentelemetry.common.ComponentLoader, java.lang.Class<T>)
5+
GENERIC TEMPLATES: +++ T:java.lang.Object
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
Comparing source compatibility of opentelemetry-sdk-extension-autoconfigure-spi-1.61.0-SNAPSHOT.jar against opentelemetry-sdk-extension-autoconfigure-spi-1.60.1.jar
2-
No changes.
2+
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.autoconfigure.spi.Ordered (not serializable)
3+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
4+
+++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.List<T> loadOrderedList(io.opentelemetry.common.ComponentLoader, java.lang.Class<T>)
5+
GENERIC TEMPLATES: +++ T:io.opentelemetry.sdk.autoconfigure.spi.Ordered

sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/Ordered.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
package io.opentelemetry.sdk.autoconfigure.spi;
77

8+
import io.opentelemetry.common.ComponentLoader;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.Comparator;
12+
import java.util.List;
13+
814
/**
915
* Interface to be extended by SPIs that need to guarantee ordering during loading.
1016
*
@@ -20,4 +26,12 @@ public interface Ordered {
2026
default int order() {
2127
return 0;
2228
}
29+
30+
/** Convenience method to load an ordered list of SPIs implementing {@link Ordered}. */
31+
static <T extends Ordered> List<T> loadOrderedList(
32+
ComponentLoader componentLoader, Class<T> spiClass) {
33+
List<T> result = new ArrayList<>(ComponentLoader.loadList(componentLoader, spiClass));
34+
result.sort(Comparator.comparing(Ordered::order));
35+
return Collections.unmodifiableList(result);
36+
}
2337
}

sdk-extensions/autoconfigure/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
api(project(":sdk-extensions:autoconfigure-spi"))
1212

1313
compileOnly(project(":api:incubator"))
14+
compileOnly(project(":sdk-extensions:incubator"))
1415

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.autoconfigure;
7+
8+
import io.opentelemetry.api.common.AttributeKey;
9+
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.common.AttributesBuilder;
11+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
12+
import io.opentelemetry.sdk.resources.Resource;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.Map;
15+
16+
/**
17+
* Creates an OpenTelemetry {@link Resource} from environment configuration.
18+
*
19+
* <p>This class is intentionally self-contained (no dependencies on other autoconfigure-internal
20+
* classes) so that it can be copied wholesale into declarative configuration without pulling in
21+
* additional dependencies. Do not add dependencies on non-API, non-SPI classes.
22+
*/
23+
final class EnvironmentResource {
24+
25+
private static final AttributeKey<String> SERVICE_NAME = AttributeKey.stringKey("service.name");
26+
27+
// Visible for testing
28+
static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes";
29+
static final String SERVICE_NAME_PROPERTY = "otel.service.name";
30+
31+
/**
32+
* Create a {@link Resource} from the environment. The resource contains attributes parsed from
33+
* environment variables and system property keys {@code otel.resource.attributes} and {@code
34+
* otel.service.name}.
35+
*
36+
* @param config the {@link ConfigProperties} used to obtain resource properties
37+
* @return the resource.
38+
*/
39+
@SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10
40+
static Resource createEnvironmentResource(ConfigProperties config) {
41+
AttributesBuilder resourceAttributes = Attributes.builder();
42+
for (Map.Entry<String, String> entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) {
43+
resourceAttributes.put(
44+
entry.getKey(),
45+
// Attributes specified via otel.resource.attributes follow the W3C Baggage spec and
46+
// characters outside the baggage-octet range are percent encoded
47+
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
48+
decodeResourceAttributes(entry.getValue()));
49+
}
50+
String serviceName = config.getString(SERVICE_NAME_PROPERTY);
51+
if (serviceName != null) {
52+
resourceAttributes.put(SERVICE_NAME, serviceName);
53+
}
54+
55+
return Resource.create(resourceAttributes.build());
56+
}
57+
58+
/**
59+
* Decodes percent-encoded characters in resource attribute values per W3C Baggage spec.
60+
*
61+
* <p>Unlike {@link java.net.URLDecoder}, this method:
62+
*
63+
* <ul>
64+
* <li>Preserves '+' as a literal plus sign (URLDecoder decodes '+' as space)
65+
* <li>Preserves invalid percent sequences as literals (e.g., "%2G", "%", "%2")
66+
* <li>Supports multi-byte UTF-8 sequences (e.g., "%C3%A9" decodes to "é")
67+
* </ul>
68+
*
69+
* @param value the percent-encoded string
70+
* @return the decoded string
71+
*/
72+
private static String decodeResourceAttributes(String value) {
73+
// no percent signs means nothing to decode
74+
if (value.indexOf('%') < 0) {
75+
return value;
76+
}
77+
78+
int n = value.length();
79+
// Use byte array to properly handle multi-byte UTF-8 sequences
80+
byte[] bytes = new byte[n];
81+
int pos = 0;
82+
83+
for (int i = 0; i < n; i++) {
84+
char c = value.charAt(i);
85+
// Check for percent-encoded sequence i.e. '%' followed by two hex digits
86+
if (c == '%' && i + 2 < n) {
87+
int d1 = Character.digit(value.charAt(i + 1), 16);
88+
int d2 = Character.digit(value.charAt(i + 2), 16);
89+
// Valid hex digits return 0-15, invalid returns -1
90+
if (d1 != -1 && d2 != -1) {
91+
// Combine two hex digits into a single byte (e.g., "2F" becomes 0x2F)
92+
bytes[pos++] = (byte) ((d1 << 4) + d2);
93+
// Skip the two hex digits (loop will also do i++)
94+
i += 2;
95+
continue;
96+
}
97+
}
98+
// Keep '+' as '+' (unlike URLDecoder) and preserve invalid percent sequences which will be
99+
// treated as literals
100+
bytes[pos++] = (byte) c;
101+
}
102+
return new String(bytes, 0, pos, StandardCharsets.UTF_8);
103+
}
104+
105+
private EnvironmentResource() {}
106+
}

sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtil.java

Lines changed: 22 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99

1010
import io.opentelemetry.api.incubator.config.DeclarativeConfigException;
1111
import io.opentelemetry.common.ComponentLoader;
12-
import io.opentelemetry.sdk.OpenTelemetrySdk;
1312
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
14-
import io.opentelemetry.sdk.resources.Resource;
13+
import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigResult;
14+
import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration;
15+
import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider;
16+
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel;
1517
import java.io.FileInputStream;
1618
import java.io.FileNotFoundException;
1719
import java.io.IOException;
18-
import java.io.InputStream;
19-
import java.lang.reflect.InvocationTargetException;
20-
import java.lang.reflect.Method;
2120
import java.util.logging.Logger;
2221
import javax.annotation.Nullable;
2322

@@ -32,30 +31,14 @@ final class IncubatingUtil {
3231

3332
private IncubatingUtil() {}
3433

35-
// Visible for testing
36-
interface Factory {
37-
@Nullable
38-
AutoConfiguredOpenTelemetrySdk create()
39-
throws ClassNotFoundException,
40-
NoSuchMethodException,
41-
IllegalAccessException,
42-
InvocationTargetException;
43-
}
44-
4534
static AutoConfiguredOpenTelemetrySdk configureFromFile(
4635
Logger logger, String configurationFile, ComponentLoader componentLoader) {
4736
logger.fine("Autoconfiguring from configuration file: " + configurationFile);
4837
try (FileInputStream fis = new FileInputStream(configurationFile)) {
49-
return requireNonNull(
50-
createWithFactory(
51-
"file",
52-
() ->
53-
getOpenTelemetrySdk(
54-
Class.forName(
55-
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration")
56-
.getMethod("parse", InputStream.class)
57-
.invoke(null, fis),
58-
componentLoader)));
38+
OpenTelemetryConfigurationModel model = DeclarativeConfiguration.parse(fis);
39+
return create(model, componentLoader);
40+
} catch (DeclarativeConfigException e) {
41+
throw toConfigurationException(e);
5942
} catch (FileNotFoundException e) {
6043
throw new ConfigurationException("Configuration file not found", e);
6144
} catch (IOException e) {
@@ -67,75 +50,23 @@ static AutoConfiguredOpenTelemetrySdk configureFromFile(
6750

6851
@Nullable
6952
public static AutoConfiguredOpenTelemetrySdk configureFromSpi(ComponentLoader componentLoader) {
70-
return createWithFactory(
71-
"SPI",
72-
() -> {
73-
Class<?> providerClass =
74-
Class.forName(
75-
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider");
76-
Method getConfigurationModel = providerClass.getMethod("getConfigurationModel");
77-
78-
for (Object configProvider : componentLoader.load(providerClass)) {
79-
Object model = getConfigurationModel.invoke(configProvider);
80-
if (model != null) {
81-
return getOpenTelemetrySdk(model, componentLoader);
82-
}
83-
}
84-
return null;
85-
});
86-
}
87-
88-
private static AutoConfiguredOpenTelemetrySdk getOpenTelemetrySdk(
89-
Object model, ComponentLoader componentLoader)
90-
throws IllegalAccessException,
91-
InvocationTargetException,
92-
ClassNotFoundException,
93-
NoSuchMethodException {
94-
95-
Class<?> openTelemetryConfiguration =
96-
Class.forName(
97-
"io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel");
98-
Class<?> declarativeConfiguration =
99-
Class.forName(
100-
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration");
101-
102-
Class<?> contextClass =
103-
Class.forName(
104-
"io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigContext");
105-
Method createContext = contextClass.getDeclaredMethod("create", ComponentLoader.class);
106-
createContext.setAccessible(true);
107-
Object context = createContext.invoke(null, componentLoader);
108-
109-
Method create =
110-
declarativeConfiguration.getDeclaredMethod(
111-
"create", openTelemetryConfiguration, contextClass);
112-
create.setAccessible(true);
113-
OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model, context);
114-
115-
Method getResource = contextClass.getDeclaredMethod("getResource");
116-
getResource.setAccessible(true);
117-
Resource resource = (Resource) getResource.invoke(context);
118-
119-
return AutoConfiguredOpenTelemetrySdk.create(sdk, resource, null);
53+
for (DeclarativeConfigurationProvider provider :
54+
componentLoader.load(DeclarativeConfigurationProvider.class)) {
55+
OpenTelemetryConfigurationModel model = provider.getConfigurationModel();
56+
if (model != null) {
57+
return create(model, componentLoader);
58+
}
59+
}
60+
return null;
12061
}
12162

122-
// Visible for testing
123-
@Nullable
124-
static AutoConfiguredOpenTelemetrySdk createWithFactory(String name, Factory factory) {
63+
private static AutoConfiguredOpenTelemetrySdk create(
64+
OpenTelemetryConfigurationModel model, ComponentLoader componentLoader) {
12565
try {
126-
return factory.create();
127-
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
128-
throw new ConfigurationException(
129-
String.format(
130-
"Error configuring from %s. Is opentelemetry-sdk-extension-incubator on the classpath?",
131-
name),
132-
e);
133-
} catch (InvocationTargetException e) {
134-
Throwable cause = e.getCause();
135-
if (cause instanceof DeclarativeConfigException) {
136-
throw toConfigurationException((DeclarativeConfigException) cause);
137-
}
138-
throw new ConfigurationException("Unexpected error configuring from " + name, e);
66+
DeclarativeConfigResult result = DeclarativeConfiguration.create(model, componentLoader);
67+
return AutoConfiguredOpenTelemetrySdk.create(result.getSdk(), result.getResource(), null);
68+
} catch (DeclarativeConfigException e) {
69+
throw toConfigurationException(e);
13970
}
14071
}
14172

0 commit comments

Comments
 (0)