Skip to content

Commit 39671a7

Browse files
authored
Permits Actuator endpoints to depend on HttpTracing (#1680)
* Permits Actuator endpoints to depend on HttpTracing It is unusual to inject HttpTracing into an actuator endpoint vs an end-user api such as `Tracer` or `SpanCustomizer`. However, we decided to allow this and so need a test to prove this continues to work. Fixes #1679
1 parent 18c27d9 commit 39671a7

File tree

4 files changed

+159
-25
lines changed

4 files changed

+159
-25
lines changed

spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebAutoConfiguration.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.cloud.sleuth.instrument.web;
1818

19-
import java.util.ArrayList;
2019
import java.util.Collection;
2120
import java.util.List;
2221
import java.util.Optional;
@@ -26,7 +25,7 @@
2625

2726
import brave.Tracing;
2827

29-
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
3029
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
3130
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
3231
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
@@ -43,6 +42,7 @@
4342
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
4443
import org.springframework.context.annotation.Bean;
4544
import org.springframework.context.annotation.Configuration;
45+
import org.springframework.lang.Nullable;
4646
import org.springframework.util.StringUtils;
4747

4848
/**
@@ -64,32 +64,56 @@
6464
@EnableConfigurationProperties(SleuthWebProperties.class)
6565
public class TraceWebAutoConfiguration {
6666

67-
@Autowired(required = false)
68-
List<SingleSkipPattern> patterns = new ArrayList<>();
69-
7067
@Bean
7168
@ConditionalOnMissingBean
72-
SkipPatternProvider sleuthSkipPatternProvider() {
73-
if (this.patterns == null) {
69+
SkipPatternProvider sleuthSkipPatternProvider(
70+
@Nullable List<SingleSkipPattern> patterns) {
71+
if (patterns == null || patterns.isEmpty()) {
7472
return null;
7573
}
76-
List<Pattern> presentPatterns = this.patterns.stream()
74+
75+
// Actuator endpoints are queried to make the default skip pattern. There's an
76+
// edge case where actuator endpoints indirectly reference the still constructing
77+
// HttpTracing bean. Ex: an instrumented client could cause a cyclic dep.
78+
//
79+
// Below optimizes for the opposite: that custom actuator endpoints are not in
80+
// use. This allows configuration to be eagerly parsed, allowing any errors to
81+
// surface earlier. In the case there is a cyclic dep, this parsing becomes lazy,
82+
// deferring any errors creating the skip pattern.
83+
//
84+
// See #1679
85+
try {
86+
Pattern result = consolidateSkipPatterns(patterns);
87+
if (result == null) {
88+
return null;
89+
}
90+
return () -> result;
91+
}
92+
catch (BeanCurrentlyInCreationException e) {
93+
// Most likely, there is an actuator endpoint that indirectly references an
94+
// instrumented HTTP client.
95+
return () -> consolidateSkipPatterns(patterns);
96+
}
97+
}
98+
99+
@Nullable
100+
static Pattern consolidateSkipPatterns(List<SingleSkipPattern> patterns) {
101+
List<Pattern> presentPatterns = patterns.stream()
77102
.map(SingleSkipPattern::skipPattern).filter(Optional::isPresent)
78103
.map(Optional::get).collect(Collectors.toList());
79104
if (presentPatterns.isEmpty()) {
80105
return null;
81106
}
82107
if (presentPatterns.size() == 1) {
83-
Pattern pattern = presentPatterns.get(0);
84-
return () -> pattern;
108+
return presentPatterns.get(0);
85109
}
110+
86111
StringJoiner joiner = new StringJoiner("|");
87112
for (Pattern pattern : presentPatterns) {
88113
String s = pattern.pattern();
89114
joiner.add(s);
90115
}
91-
Pattern pattern = Pattern.compile(joiner.toString());
92-
return () -> pattern;
116+
return Pattern.compile(joiner.toString());
93117
}
94118

95119
@Configuration(proxyBeanMethods = false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2013-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.sleuth.instrument.web;
18+
19+
import brave.http.HttpTracing;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
24+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
25+
import org.springframework.boot.test.context.SpringBootTest;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.stereotype.Service;
29+
30+
/**
31+
* This tests that actuator components can have instrumented HTTP clients inside of them.
32+
*
33+
* @author Marcin Grzejszczak
34+
*/
35+
@SpringBootTest(classes = { EndpointWithCyclicDependenciesTests.ClientConfig.class })
36+
public class EndpointWithCyclicDependenciesTests {
37+
38+
@Test
39+
void should_load_context() {
40+
}
41+
42+
static class Client {
43+
44+
}
45+
46+
@EnableAutoConfiguration
47+
@Configuration
48+
static class ClientConfig {
49+
50+
@Bean
51+
public Client client(HttpTracing httpTracing) {
52+
// imagine this instruments the client.
53+
return new Client();
54+
}
55+
56+
}
57+
58+
@Service
59+
static class MyService {
60+
61+
@Autowired
62+
Client client;
63+
64+
}
65+
66+
@RestControllerEndpoint(id = "admin-endpoint")
67+
static class MyRestEndpoint {
68+
69+
@Autowired
70+
MyService myService;
71+
72+
}
73+
74+
}

spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/SkipPatternProviderConfigTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.Collection;
2121
import java.util.Collections;
22+
import java.util.List;
2223
import java.util.Optional;
2324
import java.util.regex.Pattern;
2425
import java.util.stream.Collectors;
@@ -42,6 +43,7 @@
4243
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
4344
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
4445
import org.springframework.context.ApplicationContext;
46+
import org.springframework.context.annotation.Bean;
4547
import org.springframework.context.annotation.Configuration;
4648

4749
import static org.assertj.core.api.BDDAssertions.then;
@@ -113,10 +115,9 @@ public void should_return_management_context_with_context_path() throws Exceptio
113115

114116
@Test
115117
public void should_return_empty_when_no_endpoints() {
116-
EndpointsSupplier<ExposableWebEndpoint> endpointsSupplier = Collections::emptyList;
117118
Optional<Pattern> pattern = new TraceWebAutoConfiguration.ActuatorSkipPatternProviderConfig()
118119
.skipPatternForActuatorEndpointsSamePort(new ServerProperties(),
119-
new WebEndpointProperties(), endpointsSupplier)
120+
new WebEndpointProperties(), Collections::emptyList)
120121
.skipPattern();
121122

122123
then(pattern).isEmpty();
@@ -237,9 +238,9 @@ public void should_return_endpoints_with_context_path_different_port() {
237238
@Test
238239
public void should_combine_skip_patterns_from_list() throws Exception {
239240
TraceWebAutoConfiguration configuration = new TraceWebAutoConfiguration();
240-
configuration.patterns.addAll(Arrays.asList(foo(), bar()));
241+
List<SingleSkipPattern> patterns = Arrays.asList(foo(), bar());
241242

242-
Pattern pattern = configuration.sleuthSkipPatternProvider().skipPattern();
243+
Pattern pattern = configuration.sleuthSkipPatternProvider(patterns).skipPattern();
243244

244245
then(pattern.pattern()).isEqualTo("foo|bar");
245246
}
@@ -276,4 +277,14 @@ static class ServerPropertiesConfig {
276277

277278
}
278279

280+
@Configuration
281+
static class EmptyEndpoints {
282+
283+
@Bean
284+
EndpointsSupplier<ExposableWebEndpoint> endpointsSupplier() {
285+
return Collections::emptyList;
286+
}
287+
288+
}
289+
279290
}

spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/internal/LazyBeanTests.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,53 @@
1717
package org.springframework.cloud.sleuth.internal;
1818

1919
import brave.propagation.CurrentTraceContext;
20+
import org.junit.After;
2021
import org.junit.Test;
2122

22-
import org.springframework.context.ConfigurableApplicationContext;
23+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
2326

2427
import static org.assertj.core.api.BDDAssertions.then;
25-
import static org.mockito.Mockito.mock;
26-
import static org.mockito.Mockito.when;
2728

2829
public class LazyBeanTests {
2930

31+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
32+
33+
@After
34+
public void close() {
35+
context.close();
36+
}
37+
3038
@Test
31-
public void should_return_null_when_exception_thrown_upon_bean_retrieval() {
32-
ConfigurableApplicationContext springContext = mock(
33-
ConfigurableApplicationContext.class);
39+
public void should_work_with_basic_type() {
40+
context.register(BasicConfig.class);
41+
context.refresh();
42+
43+
LazyBean<CurrentTraceContext> provider = LazyBean.create(context,
44+
CurrentTraceContext.class);
45+
46+
then(provider.get()).isNotNull();
47+
}
3448

35-
when(springContext.getBean(CurrentTraceContext.class))
36-
.thenThrow(new IllegalStateException());
49+
@Test
50+
public void should_return_null_when_no_basic_type() {
51+
context.refresh();
3752

38-
LazyBean<CurrentTraceContext> provider = new LazyBean<>(springContext,
53+
LazyBean<CurrentTraceContext> provider = LazyBean.create(context,
3954
CurrentTraceContext.class);
4055

4156
then(provider.get()).isNull();
4257
}
4358

59+
@Configuration
60+
static class BasicConfig {
61+
62+
@Bean
63+
CurrentTraceContext currentTraceContext() {
64+
return CurrentTraceContext.Default.create();
65+
}
66+
67+
}
68+
4469
}

0 commit comments

Comments
 (0)