Skip to content

Commit f8ba714

Browse files
authored
Add Observation implementation (#1438)
* Add Observation implementation * Move versions to parent pom properties * Set micrometer dependency as optional * Add license header and documentation * Add Javadoc Changed name of key "localname" to "localpart" Removed superfluos method implementations Modified implementation of ContextualName * Updated ContextualName to match Span name conventions * Replaced Assert with WarnThenDebugLogger * Modified log-message * Added nullability annotations * Improved exeptionhandling and logging * Extract common logic to ObservationHelper * Fix for DomSource-handling See #1094
1 parent 13de5b3 commit f8ba714

18 files changed

+2048
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
<bouncycastle.version>1.78</bouncycastle.version>
155155
<byte-buddy.version>1.14.13</byte-buddy.version>
156156
<spring-asciidoctor-backends.version>0.0.5</spring-asciidoctor-backends.version>
157+
<micrometer-observation.version>1.13.5</micrometer-observation.version>
157158
</properties>
158159

159160
<dependencyManagement>

spring-ws-core/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
</exclusion>
6767
</exclusions>
6868
</dependency>
69+
<dependency>
70+
<groupId>io.micrometer</groupId>
71+
<artifactId>micrometer-observation</artifactId>
72+
<version>${micrometer-observation.version}</version>
73+
<optional>true</optional>
74+
</dependency>
6975
<dependency>
7076
<groupId>org.springframework</groupId>
7177
<artifactId>spring-test</artifactId>
@@ -250,6 +256,12 @@
250256
<artifactId>spring-webflux</artifactId>
251257
<scope>test</scope>
252258
</dependency>
259+
<dependency>
260+
<groupId>io.micrometer</groupId>
261+
<artifactId>micrometer-observation-test</artifactId>
262+
<version>${micrometer-observation.version}</version>
263+
<scope>test</scope>
264+
</dependency>
253265

254266
</dependencies>
255267

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2005-2024 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+
* http://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+
package org.springframework.ws.client.core.observation;
17+
18+
import io.micrometer.common.KeyValue;
19+
import io.micrometer.common.KeyValues;
20+
import org.springframework.ws.client.core.observation.WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames;
21+
22+
/**
23+
* ObservationConvention that describes how a WebServiceTemplate is observed.
24+
* @author Johan Kindgren
25+
*/
26+
public class DefaultWebServiceTemplateConvention implements WebServiceTemplateConvention {
27+
28+
private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION,
29+
KeyValue.NONE_VALUE);
30+
private static final String NAME = "webservice.client";
31+
32+
@Override
33+
public KeyValues getHighCardinalityKeyValues(WebServiceTemplateObservationContext context) {
34+
if (context.getPath() != null) {
35+
return KeyValues.of(path(context));
36+
}
37+
return KeyValues.empty();
38+
}
39+
40+
@Override
41+
public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext context) {
42+
return KeyValues.of(
43+
exception(context),
44+
host(context),
45+
localname(context),
46+
namespace(context),
47+
outcome(context),
48+
soapAction(context));
49+
}
50+
51+
private KeyValue path(WebServiceTemplateObservationContext context) {
52+
53+
return WebServiceTemplateObservationDocumentation.HighCardinalityKeyNames
54+
.PATH
55+
.withValue(context.getPath());
56+
}
57+
58+
private KeyValue localname(WebServiceTemplateObservationContext context) {
59+
return LowCardinalityKeyNames
60+
.LOCALPART
61+
.withValue(context.getLocalPart());
62+
}
63+
64+
private KeyValue namespace(WebServiceTemplateObservationContext context) {
65+
return LowCardinalityKeyNames
66+
.NAMESPACE
67+
.withValue(context.getNamespace());
68+
}
69+
private KeyValue host(WebServiceTemplateObservationContext context) {
70+
return LowCardinalityKeyNames
71+
.HOST
72+
.withValue(context.getHost());
73+
}
74+
75+
76+
private KeyValue outcome(WebServiceTemplateObservationContext context) {
77+
return LowCardinalityKeyNames
78+
.OUTCOME
79+
.withValue(context.getOutcome());
80+
}
81+
82+
private KeyValue soapAction(WebServiceTemplateObservationContext context) {
83+
return LowCardinalityKeyNames
84+
.SOAPACTION
85+
.withValue(context.getSoapAction());
86+
}
87+
88+
private KeyValue exception(WebServiceTemplateObservationContext context) {
89+
if (context.getError() != null) {
90+
return LowCardinalityKeyNames
91+
.EXCEPTION
92+
.withValue(context.getError().getClass().getSimpleName());
93+
}
94+
return EXCEPTION_NONE;
95+
}
96+
97+
@Override
98+
public String getName() {
99+
return NAME;
100+
}
101+
102+
@Override
103+
public String getContextualName(WebServiceTemplateObservationContext context) {
104+
return context.getContextualName();
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2005-2024 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+
* http://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+
package org.springframework.ws.client.core.observation;
17+
18+
import io.micrometer.common.util.internal.logging.WarnThenDebugLogger;
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationRegistry;
21+
import org.springframework.lang.NonNull;
22+
import org.springframework.lang.Nullable;
23+
import org.springframework.ws.FaultAwareWebServiceMessage;
24+
import org.springframework.ws.WebServiceMessage;
25+
import org.springframework.ws.client.WebServiceClientException;
26+
import org.springframework.ws.client.support.interceptor.ClientInterceptorAdapter;
27+
import org.springframework.ws.context.MessageContext;
28+
import org.springframework.ws.soap.SoapMessage;
29+
import org.springframework.ws.support.ObservationHelper;
30+
import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection;
31+
import org.springframework.ws.transport.TransportConstants;
32+
import org.springframework.ws.transport.WebServiceConnection;
33+
import org.springframework.ws.transport.context.TransportContext;
34+
import org.springframework.ws.transport.context.TransportContextHolder;
35+
36+
import javax.xml.namespace.QName;
37+
import javax.xml.transform.Source;
38+
import java.net.URI;
39+
import java.net.URISyntaxException;
40+
41+
/**
42+
* Interceptor that creates an Observation for each operation.
43+
*
44+
* @author Johan Kindgren
45+
* @see Observation
46+
* @see io.micrometer.observation.ObservationConvention
47+
*/
48+
public class WebServiceObservationInterceptor extends ClientInterceptorAdapter {
49+
50+
private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceObservationInterceptor.class);
51+
private static final String OBSERVATION_KEY = "observation";
52+
private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention();
53+
54+
private final ObservationRegistry observationRegistry;
55+
56+
private final WebServiceTemplateConvention customConvention;
57+
private final ObservationHelper observationHelper;
58+
59+
public WebServiceObservationInterceptor(
60+
@NonNull
61+
ObservationRegistry observationRegistry,
62+
@NonNull
63+
ObservationHelper observationHelper,
64+
@Nullable
65+
WebServiceTemplateConvention customConvention) {
66+
67+
this.observationRegistry = observationRegistry;
68+
this.observationHelper = observationHelper;
69+
this.customConvention = customConvention;
70+
}
71+
72+
73+
@Override
74+
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
75+
76+
TransportContext transportContext = TransportContextHolder.getTransportContext();
77+
HeadersAwareSenderWebServiceConnection connection =
78+
(HeadersAwareSenderWebServiceConnection) transportContext.getConnection();
79+
80+
Observation observation = WebServiceTemplateObservationDocumentation.WEB_SERVICE_TEMPLATE.start(
81+
customConvention,
82+
DEFAULT_CONVENTION,
83+
() -> new WebServiceTemplateObservationContext(connection),
84+
observationRegistry);
85+
86+
messageContext.setProperty(OBSERVATION_KEY, observation);
87+
88+
return true;
89+
}
90+
91+
@Override
92+
public void afterCompletion(MessageContext messageContext, Exception ex) {
93+
94+
Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY);
95+
if (observation == null) {
96+
WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed.");
97+
return;
98+
}
99+
100+
WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext();
101+
102+
WebServiceMessage request = messageContext.getRequest();
103+
WebServiceMessage response = messageContext.getResponse();
104+
105+
if (request instanceof SoapMessage soapMessage) {
106+
107+
Source source = soapMessage.getSoapBody().getPayloadSource();
108+
QName root = observationHelper.getRootElement(source);
109+
if (root != null) {
110+
context.setLocalPart(root.getLocalPart());
111+
context.setNamespace(root.getNamespaceURI());
112+
}
113+
if (soapMessage.getSoapAction() != null && !soapMessage.getSoapAction().equals(TransportConstants.EMPTY_SOAP_ACTION)) {
114+
context.setSoapAction(soapMessage.getSoapAction());
115+
}
116+
}
117+
118+
if (ex == null) {
119+
context.setOutcome("success");
120+
} else {
121+
context.setError(ex);
122+
context.setOutcome("fault");
123+
}
124+
125+
if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) {
126+
if (faultAwareResponse.hasFault()) {
127+
context.setOutcome("fault");
128+
}
129+
}
130+
131+
URI uri = getUriFromConnection();
132+
if (uri != null) {
133+
context.setHost(uri.getHost());
134+
context.setPath(uri.getPath());
135+
}
136+
137+
context.setContextualName("POST");
138+
139+
observation.stop();
140+
}
141+
142+
URI getUriFromConnection() {
143+
TransportContext transportContext = TransportContextHolder.getTransportContext();
144+
WebServiceConnection connection = transportContext.getConnection();
145+
try {
146+
return connection.getUri();
147+
} catch (URISyntaxException e) {
148+
return null;
149+
}
150+
}
151+
}
152+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2005-2024 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+
* http://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+
package org.springframework.ws.client.core.observation;
17+
18+
import io.micrometer.observation.Observation;
19+
import io.micrometer.observation.ObservationConvention;
20+
21+
/**
22+
* ObservationConvention that can be implemented to create a custom observation.
23+
* @author Johan Kindgren
24+
*/
25+
public interface WebServiceTemplateConvention extends ObservationConvention<WebServiceTemplateObservationContext> {
26+
27+
@Override
28+
default boolean supportsContext(Observation.Context context) {
29+
return context instanceof WebServiceTemplateObservationContext;
30+
}
31+
}

0 commit comments

Comments
 (0)