Skip to content

Commit b94cf7d

Browse files
authored
[Spring starter] Make sdk bean overridable and cache env calls (#15132)
1 parent 425a383 commit b94cf7d

File tree

3 files changed

+236
-14
lines changed

3 files changed

+236
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties;
7+
8+
import java.util.Objects;
9+
import java.util.Optional;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import javax.annotation.Nullable;
12+
import org.springframework.core.env.Environment;
13+
14+
/**
15+
* A caching wrapper around Spring's {@link Environment} that caches property lookups to avoid
16+
* repeated expensive operations and property source traversal.
17+
*
18+
* <p>Thread-safe for concurrent access. Cached values persist indefinitely and are assumed to be
19+
* immutable after the first access. If properties can change at runtime, use {@link #clear()} to
20+
* invalidate the cache.
21+
*
22+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
23+
* at any time.
24+
*/
25+
final class CachedPropertyResolver {
26+
private final Environment environment;
27+
private final ConcurrentHashMap<CacheKey, Optional<?>> cache = new ConcurrentHashMap<>();
28+
29+
CachedPropertyResolver(Environment environment) {
30+
this.environment = Objects.requireNonNull(environment, "environment");
31+
}
32+
33+
/**
34+
* Gets a property value from the environment, using a cache to avoid repeated lookups.
35+
*
36+
* @param name the property name
37+
* @param targetType the target type to convert to
38+
* @return the property value, or null if not found
39+
*/
40+
@Nullable
41+
<T> T getProperty(String name, Class<T> targetType) {
42+
CacheKey key = new CacheKey(name, targetType);
43+
return targetType.cast(
44+
cache
45+
.computeIfAbsent(
46+
key, k -> Optional.ofNullable(environment.getProperty(name, targetType)))
47+
.orElse(null));
48+
}
49+
50+
/**
51+
* Gets a string property value from the environment.
52+
*
53+
* @param name the property name
54+
* @return the property value, or null if not found
55+
*/
56+
@Nullable
57+
String getProperty(String name) {
58+
return getProperty(name, String.class);
59+
}
60+
61+
/** Clears all cached property values, forcing fresh lookups on subsequent calls. */
62+
void clear() {
63+
cache.clear();
64+
}
65+
66+
/** Cache key combining property name and target type. */
67+
private static final class CacheKey {
68+
private final String name;
69+
private final Class<?> targetType;
70+
private final int cachedHashCode;
71+
72+
CacheKey(String name, Class<?> targetType) {
73+
this.name = Objects.requireNonNull(name, "name");
74+
this.targetType = Objects.requireNonNull(targetType, "targetType");
75+
this.cachedHashCode = Objects.hash(name, targetType);
76+
}
77+
78+
@Override
79+
public boolean equals(Object obj) {
80+
if (this == obj) {
81+
return true;
82+
}
83+
if (!(obj instanceof CacheKey)) {
84+
return false;
85+
}
86+
CacheKey other = (CacheKey) obj;
87+
return name.equals(other.name) && targetType.equals(other.targetType);
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
return cachedHashCode;
93+
}
94+
}
95+
}

instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigProperties.java

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* any time.
2626
*/
2727
public class SpringConfigProperties implements ConfigProperties {
28-
private final Environment environment;
28+
private final CachedPropertyResolver environment;
2929

3030
private final ExpressionParser parser;
3131
private final OtlpExporterProperties otlpExporterProperties;
@@ -44,7 +44,7 @@ public SpringConfigProperties(
4444
OtelResourceProperties resourceProperties,
4545
OtelSpringProperties otelSpringProperties,
4646
ConfigProperties otelSdkProperties) {
47-
this.environment = environment;
47+
this.environment = new CachedPropertyResolver(environment);
4848
this.parser = parser;
4949
this.otlpExporterProperties = otlpExporterProperties;
5050
this.resourceProperties = resourceProperties;
@@ -154,8 +154,8 @@ public String getString(String name) {
154154
String normalizedName = ConfigUtil.normalizeEnvironmentVariableKey(name);
155155
String value = environment.getProperty(normalizedName, String.class);
156156
if (value == null && normalizedName.equals("otel.exporter.otlp.protocol")) {
157-
// SDK autoconfigure module defaults to `grpc`, but this module aligns with recommendation
158-
// in specification to default to `http/protobuf`
157+
// SDK autoconfigure module defaults to `grpc`, but this module aligns with
158+
// recommendation in specification to default to `http/protobuf`
159159
return OtlpConfigUtil.PROTOCOL_HTTP_PROTOBUF;
160160
}
161161
return or(value, otelSdkProperties.getString(name));
@@ -196,7 +196,6 @@ public Double getDouble(String name) {
196196
@SuppressWarnings("unchecked")
197197
@Override
198198
public List<String> getList(String name) {
199-
200199
String normalizedName = ConfigUtil.normalizeEnvironmentVariableKey(name);
201200

202201
List<String> list = listPropertyValues.get(normalizedName);
@@ -210,7 +209,8 @@ public List<String> getList(String name) {
210209
}
211210
}
212211

213-
return or(environment.getProperty(normalizedName, List.class), otelSdkProperties.getList(name));
212+
List<String> envValue = (List<String>) environment.getProperty(normalizedName, List.class);
213+
return or(envValue, otelSdkProperties.getList(name));
214214
}
215215

216216
@Nullable
@@ -231,6 +231,21 @@ public Map<String, String> getMap(String name) {
231231

232232
String normalizedName = ConfigUtil.normalizeEnvironmentVariableKey(name);
233233
// maps from config properties are not supported by Environment, so we have to fake it
234+
Map<String, String> specialMap = getSpecialMapProperty(normalizedName, otelSdkMap);
235+
if (specialMap != null) {
236+
return specialMap;
237+
}
238+
239+
String value = environment.getProperty(normalizedName);
240+
if (value == null) {
241+
return otelSdkMap;
242+
}
243+
return (Map<String, String>) parser.parseExpression(value).getValue();
244+
}
245+
246+
@Nullable
247+
private Map<String, String> getSpecialMapProperty(
248+
String normalizedName, Map<String, String> otelSdkMap) {
234249
switch (normalizedName) {
235250
case "otel.resource.attributes":
236251
return mergeWithOtel(resourceProperties.getAttributes(), otelSdkMap);
@@ -243,14 +258,8 @@ public Map<String, String> getMap(String name) {
243258
case "otel.exporter.otlp.traces.headers":
244259
return mergeWithOtel(otlpExporterProperties.getTraces().getHeaders(), otelSdkMap);
245260
default:
246-
break;
247-
}
248-
249-
String value = environment.getProperty(normalizedName);
250-
if (value == null) {
251-
return otelSdkMap;
261+
return null;
252262
}
253-
return (Map<String, String>) parser.parseExpression(value).getValue();
254263
}
255264

256265
/**

instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigPropertiesTest.java

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55

66
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties;
77

8+
import static java.util.Collections.emptyMap;
89
import static org.assertj.core.api.Assertions.assertThat;
910
import static org.assertj.core.api.Assertions.entry;
11+
import static org.mockito.ArgumentMatchers.eq;
12+
import static org.mockito.Mockito.spy;
13+
import static org.mockito.Mockito.times;
14+
import static org.mockito.Mockito.verify;
1015

1116
import io.opentelemetry.api.OpenTelemetry;
1217
import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration;
@@ -17,6 +22,7 @@
1722
import java.util.HashMap;
1823
import java.util.List;
1924
import java.util.Map;
25+
import java.util.function.Consumer;
2026
import java.util.stream.Stream;
2127
import org.junit.jupiter.api.DisplayName;
2228
import org.junit.jupiter.api.Test;
@@ -162,8 +168,120 @@ void shouldInitializeAttributesByMap() {
162168
});
163169
}
164170

171+
public static Stream<Arguments> propertyCachingTestCases() {
172+
return Stream.of(
173+
// property, typeClass, assertion
174+
Arguments.of(
175+
"otel.service.name=test-service",
176+
String.class,
177+
(Consumer<SpringConfigProperties>)
178+
config ->
179+
assertThat(config.getString("otel.service.name")).isEqualTo("test-service")),
180+
Arguments.of(
181+
"otel.exporter.otlp.enabled=true",
182+
Boolean.class,
183+
(Consumer<SpringConfigProperties>)
184+
config -> assertThat(config.getBoolean("otel.exporter.otlp.enabled")).isTrue()),
185+
Arguments.of(
186+
"otel.metric.export.interval=10",
187+
Integer.class,
188+
(Consumer<SpringConfigProperties>)
189+
config -> assertThat(config.getInt("otel.metric.export.interval")).isEqualTo(10)),
190+
Arguments.of(
191+
"otel.bsp.schedule.delay=5000",
192+
Long.class,
193+
(Consumer<SpringConfigProperties>)
194+
config -> assertThat(config.getLong("otel.bsp.schedule.delay")).isEqualTo(5000L)),
195+
Arguments.of(
196+
"otel.traces.sampler.arg=0.5",
197+
Double.class,
198+
(Consumer<SpringConfigProperties>)
199+
config -> assertThat(config.getDouble("otel.traces.sampler.arg")).isEqualTo(0.5)),
200+
Arguments.of(
201+
"otel.bsp.export.timeout=30s",
202+
String.class,
203+
(Consumer<SpringConfigProperties>)
204+
config ->
205+
assertThat(config.getDuration("otel.bsp.export.timeout"))
206+
.isEqualByComparingTo(java.time.Duration.ofSeconds(30))),
207+
Arguments.of(
208+
"otel.attribute.value.length.limit=256",
209+
List.class,
210+
(Consumer<SpringConfigProperties>)
211+
config ->
212+
assertThat(config.getList("otel.attribute.value.length.limit"))
213+
.containsExactly("256")));
214+
}
215+
216+
@ParameterizedTest
217+
@MethodSource("propertyCachingTestCases")
218+
@DisplayName("should cache property lookups and call Environment.getProperty() only once")
219+
void propertyCaching(
220+
String property, Class<?> typeClass, Consumer<SpringConfigProperties> assertion) {
221+
String propertyName = property.split("=")[0];
222+
223+
this.contextRunner
224+
.withPropertyValues(property)
225+
.run(
226+
context -> {
227+
Environment realEnvironment = context.getBean("environment", Environment.class);
228+
Environment spyEnvironment = spy(realEnvironment);
229+
230+
SpringConfigProperties config =
231+
new SpringConfigProperties(
232+
spyEnvironment,
233+
new SpelExpressionParser(),
234+
context.getBean(OtlpExporterProperties.class),
235+
context.getBean(OtelResourceProperties.class),
236+
context.getBean(OtelSpringProperties.class),
237+
DefaultConfigProperties.createFromMap(emptyMap()));
238+
239+
for (int i = 0; i < 100; i++) {
240+
assertion.accept(config);
241+
}
242+
243+
verify(spyEnvironment, times(1)).getProperty(eq(propertyName), eq(typeClass));
244+
});
245+
}
246+
247+
@Test
248+
@DisplayName("should cache map property lookups and call Environment.getProperty() only once")
249+
void mapPropertyCaching() {
250+
this.contextRunner
251+
.withSystemProperties(
252+
"otel.instrumentation.common.peer-service-mapping={'host1':'serviceA','host2':'serviceB'}")
253+
.run(
254+
context -> {
255+
Environment realEnvironment = context.getBean("environment", Environment.class);
256+
Environment spyEnvironment = spy(realEnvironment);
257+
258+
SpringConfigProperties config =
259+
new SpringConfigProperties(
260+
spyEnvironment,
261+
new SpelExpressionParser(),
262+
context.getBean(OtlpExporterProperties.class),
263+
context.getBean(OtelResourceProperties.class),
264+
context.getBean(OtelSpringProperties.class),
265+
DefaultConfigProperties.createFromMap(emptyMap()));
266+
267+
for (int i = 0; i < 100; i++) {
268+
Map<String, String> mapping =
269+
config.getMap("otel.instrumentation.common.peer-service-mapping");
270+
assertThat(mapping)
271+
.containsEntry("host1", "serviceA")
272+
.containsEntry("host2", "serviceB");
273+
}
274+
275+
// Map properties call getProperty(name) which delegates to getProperty(name,
276+
// String.class)
277+
verify(spyEnvironment, times(1))
278+
.getProperty(
279+
eq("otel.instrumentation.common.peer-service-mapping"), eq(String.class));
280+
});
281+
}
282+
165283
private static ConfigProperties getConfig(AssertableApplicationContext context) {
166-
return getConfig(context, Collections.emptyMap());
284+
return getConfig(context, emptyMap());
167285
}
168286

169287
private static SpringConfigProperties getConfig(

0 commit comments

Comments
 (0)