Skip to content

Add Observation implementation #1438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 18, 2024
Merged
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
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
<bouncycastle.version>1.78</bouncycastle.version>
<byte-buddy.version>1.14.13</byte-buddy.version>
<spring-asciidoctor-backends.version>0.0.5</spring-asciidoctor-backends.version>
<micrometer-observation.version>1.13.5</micrometer-observation.version>
</properties>

<dependencyManagement>
Expand Down
12 changes: 12 additions & 0 deletions spring-ws-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>${micrometer-observation.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
Expand Down Expand Up @@ -244,6 +250,12 @@
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<version>${micrometer-observation.version}</version>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ws.client.core.observation;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.ws.client.core.observation.WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames;

/**
* ObservationConvention that describes how a WebServiceTemplate is observed.
* @author Johan Kindgren
*/
public class DefaultWebServiceTemplateConvention implements WebServiceTemplateConvention {

private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION,
KeyValue.NONE_VALUE);
private static final String NAME = "webservice.client";

@Override
public KeyValues getHighCardinalityKeyValues(WebServiceTemplateObservationContext context) {
if (context.getPath() != null) {
return KeyValues.of(path(context));
}
return KeyValues.empty();
}

@Override
public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext context) {
return KeyValues.of(
exception(context),
host(context),
localname(context),
namespace(context),
outcome(context),
soapAction(context));
}

private KeyValue path(WebServiceTemplateObservationContext context) {

return WebServiceTemplateObservationDocumentation.HighCardinalityKeyNames
.PATH
.withValue(context.getPath());
}

private KeyValue localname(WebServiceTemplateObservationContext context) {
return LowCardinalityKeyNames
.LOCALPART
.withValue(context.getLocalPart());
}

private KeyValue namespace(WebServiceTemplateObservationContext context) {
return LowCardinalityKeyNames
.NAMESPACE
.withValue(context.getNamespace());
}
private KeyValue host(WebServiceTemplateObservationContext context) {
return LowCardinalityKeyNames
.HOST
.withValue(context.getHost());
}


private KeyValue outcome(WebServiceTemplateObservationContext context) {
return LowCardinalityKeyNames
.OUTCOME
.withValue(context.getOutcome());
}

private KeyValue soapAction(WebServiceTemplateObservationContext context) {
return LowCardinalityKeyNames
.SOAPACTION
.withValue(context.getSoapAction());
}

private KeyValue exception(WebServiceTemplateObservationContext context) {
if (context.getError() != null) {
return LowCardinalityKeyNames
.EXCEPTION
.withValue(context.getError().getClass().getSimpleName());
}
return EXCEPTION_NONE;
}

@Override
public String getName() {
return NAME;
}

@Override
public String getContextualName(WebServiceTemplateObservationContext context) {
return context.getContextualName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ws.client.core.observation;

import io.micrometer.common.util.internal.logging.WarnThenDebugLogger;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.ws.FaultAwareWebServiceMessage;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptorAdapter;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.support.ObservationHelper;
import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection;
import org.springframework.ws.transport.TransportConstants;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.context.TransportContext;
import org.springframework.ws.transport.context.TransportContextHolder;

import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import java.net.URI;
import java.net.URISyntaxException;

/**
* Interceptor that creates an Observation for each operation.
*
* @author Johan Kindgren
* @see Observation
* @see io.micrometer.observation.ObservationConvention
*/
public class WebServiceObservationInterceptor extends ClientInterceptorAdapter {

private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceObservationInterceptor.class);
private static final String OBSERVATION_KEY = "observation";
private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention();

private final ObservationRegistry observationRegistry;

private final WebServiceTemplateConvention customConvention;
private final ObservationHelper observationHelper;

public WebServiceObservationInterceptor(
@NonNull
ObservationRegistry observationRegistry,
@NonNull
ObservationHelper observationHelper,
@Nullable
WebServiceTemplateConvention customConvention) {

this.observationRegistry = observationRegistry;
this.observationHelper = observationHelper;
this.customConvention = customConvention;
}


@Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {

TransportContext transportContext = TransportContextHolder.getTransportContext();
HeadersAwareSenderWebServiceConnection connection =
(HeadersAwareSenderWebServiceConnection) transportContext.getConnection();

Observation observation = WebServiceTemplateObservationDocumentation.WEB_SERVICE_TEMPLATE.start(
customConvention,
DEFAULT_CONVENTION,
() -> new WebServiceTemplateObservationContext(connection),
observationRegistry);

messageContext.setProperty(OBSERVATION_KEY, observation);

return true;
}

@Override
public void afterCompletion(MessageContext messageContext, Exception ex) {

Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY);
if (observation == null) {
WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed.");
return;
}

WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext();

WebServiceMessage request = messageContext.getRequest();
WebServiceMessage response = messageContext.getResponse();

if (request instanceof SoapMessage soapMessage) {

Source source = soapMessage.getSoapBody().getPayloadSource();
QName root = observationHelper.getRootElement(source);
if (root != null) {
context.setLocalPart(root.getLocalPart());
context.setNamespace(root.getNamespaceURI());
}
if (soapMessage.getSoapAction() != null && !soapMessage.getSoapAction().equals(TransportConstants.EMPTY_SOAP_ACTION)) {
context.setSoapAction(soapMessage.getSoapAction());
}
}

if (ex == null) {
context.setOutcome("success");
} else {
context.setError(ex);
context.setOutcome("fault");
}

if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) {
if (faultAwareResponse.hasFault()) {
context.setOutcome("fault");
}
}

URI uri = getUriFromConnection();
if (uri != null) {
context.setHost(uri.getHost());
context.setPath(uri.getPath());
}

context.setContextualName("POST");

observation.stop();
}

URI getUriFromConnection() {
TransportContext transportContext = TransportContextHolder.getTransportContext();
WebServiceConnection connection = transportContext.getConnection();
try {
return connection.getUri();
} catch (URISyntaxException e) {
return null;
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ws.client.core.observation;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;

/**
* ObservationConvention that can be implemented to create a custom observation.
* @author Johan Kindgren
*/
public interface WebServiceTemplateConvention extends ObservationConvention<WebServiceTemplateObservationContext> {

@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof WebServiceTemplateObservationContext;
}
}
Loading