From 904785e25f52dbd7a80deccc89e654a5377cbc86 Mon Sep 17 00:00:00 2001 From: johkin Date: Sat, 9 Nov 2024 22:58:35 +0100 Subject: [PATCH 01/15] Add Observation implementation #1094 --- spring-ws-core/pom.xml | 6 + .../DefaultWebServiceTemplateConvention.java | 93 ++++ .../observation/RootElementSAXHandler.java | 23 + .../WebServiceObservationInterceptor.java | 154 ++++++ .../WebServiceTemplateConvention.java | 16 + .../WebServiceTemplateObservationContext.java | 74 +++ ...rviceTemplateObservationDocumentation.java | 84 ++++ .../DefaultWebServiceEndpointConvention.java | 88 ++++ .../EndpointObservationDocumentation.java | 77 +++ .../observation/ObservationInterceptor.java | 140 ++++++ .../WebServiceEndpointContext.java | 67 +++ .../WebServiceEndpointConvention.java | 17 + ...iceTemplateObservationIntegrationTest.java | 461 ++++++++++++++++++ .../endpoint/observation/MyEndpoint.java | 53 ++ ...ObservationInterceptorIntegrationTest.java | 116 +++++ .../observation/WebServiceConfig.java | 59 +++ 16 files changed, 1528 insertions(+) create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 9b2d0f93f..b119349f7 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -244,6 +244,12 @@ spring-webflux test + + io.micrometer + micrometer-observation-test + 1.13.3 + test + diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java new file mode 100644 index 000000000..6de70429a --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java @@ -0,0 +1,93 @@ +package org.springframework.ws.client.core.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; + +/** + * ObservationConvention that describes how a WebServiceTemplate is observed. + * @author Johan Kindgren + */ +public class DefaultWebServiceTemplateConvention implements WebServiceTemplateConvention { + + private static final KeyValue EXCEPTION_NONE = KeyValue.of(WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); + private String name = "webservice.client"; + + + @Override + public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext context) { + return KeyValues.of( + exception(context), + host(context), + localname(context), + namespace(context), + outcome(context), + soapAction(context)); + } + + private KeyValue localname(WebServiceTemplateObservationContext context) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .LOCALNAME + .withValue(context.getLocalname()); + } + + private KeyValue namespace(WebServiceTemplateObservationContext context) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .NAMESPACE + .withValue(context.getNamespace()); + } + private KeyValue host(WebServiceTemplateObservationContext context) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .HOST + .withValue(context.getHost()); + } + + + private KeyValue outcome(WebServiceTemplateObservationContext context) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .OUTCOME + .withValue(context.getOutcome()); + } + + private KeyValue soapAction(WebServiceTemplateObservationContext context) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .SOAPACTION + .withValue(context.getSoapAction()); + } + + private KeyValue exception(WebServiceTemplateObservationContext context) { + if (context.getError() != null) { + return WebServiceTemplateObservationDocumentation + .LowCardinalityKeyNames + .EXCEPTION + .withValue(context.getError().getClass().getSimpleName()); + } else { + return EXCEPTION_NONE; + } + } + + @Override + public KeyValues getHighCardinalityKeyValues(WebServiceTemplateObservationContext context) { + return KeyValues.empty(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getContextualName(WebServiceTemplateObservationContext context) { + return "WebServiceTemplate " + context.getHost(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceTemplateObservationContext; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java new file mode 100644 index 000000000..770021b81 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java @@ -0,0 +1,23 @@ +package org.springframework.ws.client.core.observation; + +import org.xml.sax.Attributes; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.namespace.QName; + +public class RootElementSAXHandler extends DefaultHandler { + + private QName rootElementName = null; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if (rootElementName == null) { + // Spara rotnodens localName och namespaceURI + rootElementName = new QName(uri, localName); + } + } + + public QName getRootElementName() { + return rootElementName; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java new file mode 100644 index 000000000..d404acffc --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -0,0 +1,154 @@ +package org.springframework.ws.client.core.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +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.transport.HeadersAwareSenderWebServiceConnection; +import org.springframework.ws.transport.WebServiceConnection; +import org.springframework.ws.transport.context.TransportContext; +import org.springframework.ws.transport.context.TransportContextHolder; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import javax.xml.namespace.QName; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; +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 ObservationRegistry observationRegistry; + + private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention(); + + private SAXParserFactory parserFactory; + private SAXParser saxParser; + + private WebServiceTemplateConvention customConvention; + + public WebServiceObservationInterceptor(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + parserFactory = SAXParserFactory.newNSInstance(); + try { + saxParser = parserFactory.newSAXParser(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + + @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", observation); + + return true; + } + + @Override + public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException { + + Observation observation = (Observation) messageContext.getProperty("observation"); + WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext(); + + context.setHost(getHostFromConnection()); + context.setError(ex); + + WebServiceMessage request = messageContext.getRequest(); + WebServiceMessage response = messageContext.getResponse(); + + if (request instanceof SoapMessage soapMessage) { + + Source source = soapMessage.getSoapBody().getPayloadSource(); + QName root = getRootElement(source); + if (root != null) { + context.setLocalname(root.getLocalPart()); + context.setNamespace(root.getNamespaceURI()); + } + context.setSoapAction((soapMessage).getSoapAction()); + } + + if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { + if (!faultAwareResponse.hasFault() && ex == null) { + context.setOutcome("success"); + } else { + context.setOutcome("fault"); + } + } + + observation.stop(); + } + + public void setCustomConvention(WebServiceTemplateConvention customConvention) { + this.customConvention = customConvention; + } + + String getHostFromConnection() { + TransportContext transportContext = TransportContextHolder.getTransportContext(); + WebServiceConnection connection = transportContext.getConnection(); + try { + return connection.getUri().getHost(); + } catch (URISyntaxException e) { + return WebServiceTemplateObservationContext.UNKNOWN; + } + } + + QName getRootElement(Source source) { + if (source instanceof DOMSource) { + Node root = ((DOMSource) source).getNode(); + return new QName(root.getNamespaceURI(), root.getLocalName()); + } + if (source instanceof StreamSource) { + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(((StreamSource) source).getInputStream(), handler); + return handler.getRootElementName(); + } catch (Exception e) { + return new QName("unknown", "unknow"); + } + } + if (source instanceof SAXSource) { + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(((SAXSource) source).getInputSource(), handler); + return handler.getRootElementName(); + } catch (Exception e) { + return new QName("unknown", "unknow"); + } + } + return new QName("unknown", "unknow"); + } + +} + diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java new file mode 100644 index 000000000..d26e15597 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java @@ -0,0 +1,16 @@ +package org.springframework.ws.client.core.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * ObservationConvention that describes how a WebServiceTemplate is observed. + * @author Johan Kindgren + */ +public interface WebServiceTemplateConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceTemplateObservationContext; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java new file mode 100644 index 000000000..dee1f3a2f --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -0,0 +1,74 @@ +package org.springframework.ws.client.core.observation; + +import io.micrometer.observation.transport.RequestReplySenderContext; +import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; +import org.springframework.ws.transport.TransportInputStream; + +import java.io.IOException; + +class WebServiceTemplateObservationContext extends RequestReplySenderContext { + + + public static final String UNKNOWN = "unknown"; + private String outcome = UNKNOWN; + private String localname = UNKNOWN; + private String namespace = UNKNOWN; + private String host = UNKNOWN; + private String soapAction = UNKNOWN; + + public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnection connection) { + super((carrier, key, value) -> { + + if (carrier != null) { + try { + carrier.addRequestHeader(key, value); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + setCarrier(connection); + } + + public String getOutcome() { + return outcome; + } + + public void setOutcome(String outcome) { + this.outcome = outcome; + } + + public String getLocalname() { + return localname; + } + + public void setLocalname(String localname) { + this.localname = localname; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + + public String getSoapAction() { + return soapAction; + } + + public void setSoapAction(String soapAction) { + this.soapAction = soapAction; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java new file mode 100644 index 000000000..29104a610 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -0,0 +1,84 @@ +package org.springframework.ws.client.core.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * ObservationDocumentation for WebSeviceTemplate. + * + * @author Johan Kindgren + */ +public enum WebServiceTemplateObservationDocumentation implements ObservationDocumentation { + + WEB_SERVICE_TEMPLATE { + + @Override + public Class> getDefaultConvention() { + return DefaultWebServiceTemplateConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return super.getHighCardinalityKeyNames(); + } + }; + + enum LowCardinalityKeyNames implements KeyName { + + /** + * Name of the exception thrown during the exchange, + * or {@value KeyValue#NONE_VALUE} if no exception happened. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + /** + * Outcome of the WebService exchange. + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + NAMESPACE { + @Override + public String asString() { + return "namespace"; + } + }, + + LOCALNAME { + @Override + public String asString() { + return "localname"; + } + }, + HOST { + @Override + public String asString() { + return "host"; + } + }, + + SOAPACTION { + @Override + public String asString() { + return "soapaction"; + } + } + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java new file mode 100644 index 000000000..e4c31fb8a --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java @@ -0,0 +1,88 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; + +/** + * ObservationConvention that describes how a WebServiceTemplate is observed. + * @author Johan Kindgren + */ +public class DefaultWebServiceEndpointConvention implements WebServiceEndpointConvention { + + private static final KeyValue EXCEPTION_NONE = KeyValue.of(EndpointObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); + private String name = "webservice.server"; + + + @Override + public KeyValues getLowCardinalityKeyValues(WebServiceEndpointContext context) { + return KeyValues.of( + exception(context), + localname(context), + namespace(context), + outcome(context), + soapAction(context)); + + } + + private KeyValue localname(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .LOCALNAME + .withValue(context.getLocalname()); + } + + private KeyValue namespace(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .NAMESPACE + .withValue(context.getNamespace()); + } + + + private KeyValue outcome(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .OUTCOME + .withValue(context.getOutcome()); + } + + private KeyValue soapAction(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .SOAPACTION + .withValue(context.getSoapAction()); + } + + private KeyValue exception(WebServiceEndpointContext context) { + if (context.getError() != null) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .EXCEPTION + .withValue(context.getError().getClass().getSimpleName()); + } else { + return EXCEPTION_NONE; + } + } + + @Override + public KeyValues getHighCardinalityKeyValues(WebServiceEndpointContext context) { + return KeyValues.empty(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getContextualName(WebServiceEndpointContext context) { + return "WebServiceEndpoint " + context.getNamespace() + ':' + context.getLocalname(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceEndpointContext; + } + +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java new file mode 100644 index 000000000..d0c94cc8f --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -0,0 +1,77 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * ObservationDocumentation for EndpointAdapter. + * + * @author Johan Kindgren + */ +public enum EndpointObservationDocumentation implements ObservationDocumentation { + + WEB_SERVICE_ENDPOINT { + @Override + public Class> getDefaultConvention() { + return DefaultWebServiceEndpointConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return super.getHighCardinalityKeyNames(); + } + }; + + enum LowCardinalityKeyNames implements KeyName { + + /** + * Name of the exception thrown during the exchange, + * or {@value KeyValue#NONE_VALUE} if no exception happened. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + /** + * Outcome of the WebService exchange. + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + NAMESPACE { + @Override + public String asString() { + return "namespace"; + } + }, + + LOCALNAME { + @Override + public String asString() { + return "localname"; + } + }, + + SOAPACTION { + @Override + public String asString() { + return "soapaction"; + } + } + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java new file mode 100644 index 000000000..b45903fb2 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -0,0 +1,140 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.ws.FaultAwareWebServiceMessage; +import org.springframework.ws.WebServiceMessage; +import org.springframework.ws.client.core.observation.RootElementSAXHandler; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.server.endpoint.interceptor.EndpointInterceptorAdapter; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; +import org.springframework.ws.transport.TransportConstants; +import org.springframework.ws.transport.context.TransportContext; +import org.springframework.ws.transport.context.TransportContextHolder; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import javax.xml.namespace.QName; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +public class ObservationInterceptor extends EndpointInterceptorAdapter { + + private ObservationRegistry observationRegistry; + + private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); + + private SAXParserFactory parserFactory; + private SAXParser saxParser; + + private WebServiceEndpointConvention customConvention; + + public ObservationInterceptor(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + + parserFactory = SAXParserFactory.newNSInstance(); + try { + saxParser = parserFactory.newSAXParser(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception { + + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareReceiverWebServiceConnection connection = + (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); + + + Observation observation = EndpointObservationDocumentation.WEB_SERVICE_ENDPOINT.start( + customConvention, + DEFAULT_CONVENTION, + () -> new WebServiceEndpointContext(connection), + observationRegistry); + + messageContext.setProperty("observation", observation); + + return true; + } + + + + @Override + public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) throws Exception { + + Observation observation = (Observation) messageContext.getProperty("observation"); + WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); + + context.setError(ex); + + WebServiceMessage request = messageContext.getRequest(); + WebServiceMessage response = messageContext.getResponse(); + + if (request instanceof SoapMessage soapMessage) { + + Source source = soapMessage.getSoapBody().getPayloadSource(); + QName root = getRootElement(source); + if (root != null) { + context.setLocalname(root.getLocalPart()); + context.setNamespace(root.getNamespaceURI()); + } + String action = soapMessage.getSoapAction(); + if (!TransportConstants.EMPTY_SOAP_ACTION.equals(action)) { + context.setSoapAction(soapMessage.getSoapAction()); + } else { + context.setSoapAction("none"); + } + + } + + if (response instanceof FaultAwareWebServiceMessage) { + if (!((FaultAwareWebServiceMessage) response).hasFault() && ex == null) { + context.setOutcome("success"); + } else { + context.setOutcome("fault"); + } + } + + observation.stop(); + } + + QName getRootElement(Source source) { + if (source instanceof DOMSource) { + Node root = ((DOMSource) source).getNode(); + return new QName(root.getNamespaceURI(), root.getLocalName()); + } + if (source instanceof StreamSource) { + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(((StreamSource) source).getInputStream(), handler); + return handler.getRootElementName(); + } catch (Exception e) { + return new QName("unknown", "unknow"); + } + } + if (source instanceof SAXSource) { + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(((SAXSource) source).getInputSource(), handler); + return handler.getRootElementName(); + } catch (Exception e) { + return new QName("unknown", "unknow"); + } + } + return new QName("unknown", "unknow"); + } + + public void setCustomConvention(WebServiceEndpointConvention customConvention) { + this.customConvention = customConvention; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java new file mode 100644 index 000000000..230a4895f --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -0,0 +1,67 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.observation.transport.Propagator; +import io.micrometer.observation.transport.RequestReplyReceiverContext; +import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; +import org.springframework.ws.transport.TransportInputStream; + +import java.io.IOException; +import java.util.Iterator; + +public class WebServiceEndpointContext extends RequestReplyReceiverContext { + + + public static final String UNKNOWN = "unknown"; + private String outcome = UNKNOWN; + private String localname = UNKNOWN; + private String namespace = UNKNOWN; + private String soapAction = UNKNOWN; + + public WebServiceEndpointContext(HeadersAwareReceiverWebServiceConnection connection) { + super((carrier, key) -> { + try { + Iterator headers = carrier.getRequestHeaders(key); + if (headers.hasNext()) { + return headers.next(); + } else { + return null; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + setCarrier(connection); + } + + public String getOutcome() { + return outcome; + } + + public void setOutcome(String outcome) { + this.outcome = outcome; + } + + public String getLocalname() { + return localname; + } + + public void setLocalname(String localname) { + this.localname = localname; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getSoapAction() { + return soapAction; + } + + public void setSoapAction(String soapAction) { + this.soapAction = soapAction; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java new file mode 100644 index 000000000..81452ba8b --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java @@ -0,0 +1,17 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * ObservationConvention that describes how a WebServiceTemplate is observed. + * @author Johan Kindgren + */ +public interface WebServiceEndpointConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceEndpointContext; + } + +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java new file mode 100644 index 000000000..637b3ac2d --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -0,0 +1,461 @@ +/* + * Copyright 2005-2022 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.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import jakarta.activation.CommandMap; +import jakarta.activation.DataHandler; +import jakarta.activation.MailcapCommandMap; +import jakarta.mail.util.ByteArrayDataSource; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.xml.soap.*; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.ws.client.WebServiceTransportException; +import org.springframework.ws.client.core.AbstractSoap12WebServiceTemplateIntegrationTestCase; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.client.support.interceptor.ClientInterceptor; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.soap.client.SoapFaultClientException; +import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; +import org.springframework.ws.transport.http.HttpComponentsMessageSender; +import org.springframework.ws.transport.support.FreePortScanner; +import org.springframework.xml.transform.StringResult; +import org.springframework.xml.transform.StringSource; +import org.springframework.xml.transform.TransformerFactoryUtils; +import org.xmlunit.assertj.XmlAssert; + +import javax.xml.transform.*; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.StringTokenizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Johan Kindgren + */ +public class WebServiceTemplateObservationIntegrationTest { + + private TestObservationRegistry observationRegistry; + + private static Server jettyServer; + + private static String baseUrl; + + private WebServiceTemplate template; + + private String messagePayload = ""; + + @BeforeAll + public static void startJetty() throws Exception { + + int port = FreePortScanner.getFreePort(); + baseUrl = "http://localhost:" + port; + + jettyServer = new Server(port); + Connector connector = new ServerConnector(jettyServer); + jettyServer.addConnector(connector); + + ServletContextHandler jettyContext = new ServletContextHandler(); + jettyContext.setContextPath("/"); + + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.EchoSoapServlet.class, "/soap/echo"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.SoapReceiverFaultServlet.class, "/soap/receiverFault"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.SoapSenderFaultServlet.class, "/soap/senderFault"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.NoResponseSoapServlet.class, "/soap/noResponse"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.AttachmentsServlet.class, "/soap/attachment"); + + ServletHolder notfound = jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.ErrorServlet.class, "/errors/notfound"); + notfound.setInitParameter("sc", "404"); + + ServletHolder errors = jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.ErrorServlet.class, "/errors/server"); + errors.setInitParameter("sc", "500"); + + jettyServer.setHandler(jettyContext); + jettyServer.start(); + } + + @AfterAll + public static void stopJetty() throws Exception { + + if (jettyServer.isRunning()) { + jettyServer.stop(); + } + } + + /** + * A workaround for the faulty XmlDataContentHandler in the SAAJ RI, which cannot handle mime types such as "text/xml; + * charset=UTF-8", causing issues with Axiom. We basically reset the command map + */ + @BeforeEach + public void removeXmlDataContentHandler() throws SOAPException { + + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage message = messageFactory.createMessage(); + message.createAttachmentPart(); + CommandMap.setDefaultCommandMap(new MailcapCommandMap()); + } + + @BeforeEach + public void createWebServiceTemplate() throws Exception { + observationRegistry = TestObservationRegistry.create(); + + template = new WebServiceTemplate(new SaajSoapMessageFactory(MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL))); + template.setMessageSender(new HttpComponentsMessageSender()); + template.setInterceptors(new ClientInterceptor[]{ + new WebServiceObservationInterceptor(observationRegistry) + }); + } + + + @Test + public void sendSourceAndReceiveToResult() { + + StringResult result = new StringResult(); + boolean b = template.sendSourceAndReceiveToResult(baseUrl + "/soap/echo", new StringSource(messagePayload), result); + + assertThat(b).isTrue(); + XmlAssert.assertThat(result.toString()).and(messagePayload).ignoreWhitespace().areIdentical(); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localname", "root") + .hasContextualNameEqualTo("WebServiceTemplate localhost") + ); + } + + @Test + public void sendSourceAndReceiveToResultNoResponse() { + + boolean b = template.sendSourceAndReceiveToResult(baseUrl + "/soap/noResponse", new StringSource(messagePayload), + new StringResult()); + assertThat(b).isFalse(); + } + + @Test + public void marshalSendAndReceiveResponse() throws TransformerConfigurationException { + + final Transformer transformer = TransformerFactoryUtils.newInstance().newTransformer(); + final Object requestObject = new Object(); + + Marshaller marshaller = new Marshaller() { + + @Override + public void marshal(Object graph, Result result) throws XmlMappingException { + + assertThat(requestObject).isEqualTo(graph); + try { + transformer.transform(new StringSource(messagePayload), result); + } catch (TransformerException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean supports(Class clazz) { + + assertThat(clazz).isEqualTo(Object.class); + return true; + } + }; + + final Object responseObject = new Object(); + + Unmarshaller unmarshaller = new Unmarshaller() { + + @Override + public Object unmarshal(Source source) throws XmlMappingException { + return responseObject; + } + + @Override + public boolean supports(Class clazz) { + + assertThat(clazz).isEqualTo(Object.class); + return true; + } + }; + + template.setMarshaller(marshaller); + template.setUnmarshaller(unmarshaller); + Object result = template.marshalSendAndReceive(baseUrl + "/soap/echo", requestObject); + + assertThat(result).isEqualTo(responseObject); + } + + @Test + public void marshalSendAndReceiveNoResponse() throws TransformerConfigurationException { + + final Transformer transformer = TransformerFactoryUtils.newInstance().newTransformer(); + final Object requestObject = new Object(); + Marshaller marshaller = new Marshaller() { + + @Override + public void marshal(Object graph, Result result) throws XmlMappingException, IOException { + + assertThat(requestObject).isEqualTo(graph); + + try { + transformer.transform(new StringSource(messagePayload), result); + } catch (TransformerException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean supports(Class clazz) { + + assertThat(clazz).isEqualTo(Object.class); + return true; + } + }; + + template.setMarshaller(marshaller); + Object result = template.marshalSendAndReceive(baseUrl + "/soap/noResponse", requestObject); + + assertThat(result).isNull(); + } + + @Test + public void notFound() { + + assertThatExceptionOfType(WebServiceTransportException.class) + .isThrownBy(() -> template.sendSourceAndReceiveToResult(baseUrl + "/errors/notfound", + new StringSource(messagePayload), new StringResult())); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "WebServiceTransportException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localname", "root") + .hasContextualNameEqualTo("WebServiceTemplate localhost") + ); + + } + + @Test + public void receiverFault() { + + Result result = new StringResult(); + + assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template + .sendSourceAndReceiveToResult(baseUrl + "/soap/receiverFault", new StringSource(messagePayload), result)); + } + + @Test + public void senderFault() { + + Result result = new StringResult(); + + assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template + .sendSourceAndReceiveToResult(baseUrl + "/soap/senderFault", new StringSource(messagePayload), result)); + } + + @Test + public void attachment() { + + template.sendSourceAndReceiveToResult(baseUrl + "/soap/attachment", new StringSource(messagePayload), message -> { + + SoapMessage soapMessage = (SoapMessage) message; + final String attachmentContent = "content"; + soapMessage.addAttachment("attachment-1", + new DataHandler(new ByteArrayDataSource(attachmentContent, "text/plain"))); + }, new StringResult()); + } + + /** + * Servlet that returns and error message for a given status code. + */ + @SuppressWarnings("serial") + public static class ErrorServlet extends HttpServlet { + + private int sc; + + private ErrorServlet(int sc) { + this.sc = sc; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.sendError(sc); + } + } + + /** + * Abstract SOAP Servlet + */ + @SuppressWarnings("serial") + public abstract static class AbstractSoapServlet extends HttpServlet { + + protected MessageFactory messageFactory = null; + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + + super.init(servletConfig); + + try { + messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL); + } catch (SOAPException ex) { + throw new ServletException("Unable to create message factory" + ex.getMessage()); + } + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + + try { + MimeHeaders headers = getHeaders(req); + SOAPMessage request = messageFactory.createMessage(headers, req.getInputStream()); + SOAPMessage reply = onMessage(request); + + if (reply != null) { + reply.saveChanges(); + SOAPBody replyBody = reply.getSOAPBody(); + if (!replyBody.hasFault()) { + resp.setStatus(HttpServletResponse.SC_OK); + } else { + if (replyBody.getFault().getFaultCodeAsQName().equals(SOAPConstants.SOAP_SENDER_FAULT)) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + + } else { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + putHeaders(reply.getMimeHeaders(), resp); + reply.writeTo(resp.getOutputStream()); + } else { + resp.setStatus(HttpServletResponse.SC_ACCEPTED); + } + } catch (Exception ex) { + throw new ServletException("SAAJ POST failed " + ex.getMessage(), ex); + } + } + + private MimeHeaders getHeaders(HttpServletRequest httpServletRequest) { + + Enumeration enumeration = httpServletRequest.getHeaderNames(); + MimeHeaders headers = new MimeHeaders(); + + while (enumeration.hasMoreElements()) { + String headerName = (String) enumeration.nextElement(); + String headerValue = httpServletRequest.getHeader(headerName); + StringTokenizer values = new StringTokenizer(headerValue, ","); + while (values.hasMoreTokens()) { + headers.addHeader(headerName, values.nextToken().trim()); + } + } + + return headers; + } + + private void putHeaders(MimeHeaders headers, HttpServletResponse res) { + + Iterator it = headers.getAllHeaders(); + + while (it.hasNext()) { + MimeHeader header = (MimeHeader) it.next(); + String[] values = headers.getHeader(header.getName()); + for (String value : values) { + res.addHeader(header.getName(), value); + } + } + } + + protected abstract SOAPMessage onMessage(SOAPMessage message) throws SOAPException; + } + + @SuppressWarnings("serial") + public static class EchoSoapServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + return message; + } + } + + @SuppressWarnings("serial") + public static class NoResponseSoapServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + return null; + } + } + + @SuppressWarnings("serial") + public static class SoapReceiverFaultServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) throws SOAPException { + + SOAPMessage response = messageFactory.createMessage(); + SOAPBody body = response.getSOAPBody(); + body.addFault(SOAPConstants.SOAP_RECEIVER_FAULT, "Receiver Fault"); + return response; + } + } + + @SuppressWarnings("serial") + public static class SoapSenderFaultServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) throws SOAPException { + + SOAPMessage response = messageFactory.createMessage(); + SOAPBody body = response.getSOAPBody(); + body.addFault(SOAPConstants.SOAP_SENDER_FAULT, "Sender Fault"); + return response; + } + } + + @SuppressWarnings("serial") + public static class AttachmentsServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + + assertThat(message.countAttachments()).isEqualTo(1); + return null; + } + } + +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java new file mode 100644 index 000000000..ea898e449 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java @@ -0,0 +1,53 @@ +package org.springframework.ws.server.endpoint.observation; + +import jakarta.xml.bind.annotation.XmlRootElement; +import org.springframework.ws.server.endpoint.annotation.Endpoint; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; +import org.springframework.ws.server.endpoint.annotation.RequestPayload; +import org.springframework.ws.server.endpoint.annotation.ResponsePayload; + +@Endpoint +public class MyEndpoint { + + private static final String NAMESPACE_URI = "http://springframework.org/spring-ws"; + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "request") + @ResponsePayload + public MyResponse handleRequest(@RequestPayload MyRequest request) { + MyResponse myResponse = new MyResponse(); + myResponse.setMessage("Hello " + request.getName()); + return myResponse; + } + + + + @XmlRootElement(namespace = NAMESPACE_URI, name = "request") + static class MyRequest { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @XmlRootElement(namespace = NAMESPACE_URI, name = "response") + static class MyResponse { + + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} + + diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java new file mode 100644 index 000000000..63eab4969 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -0,0 +1,116 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.observation.tck.TestObservationRegistry; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.*; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.ws.WebServiceMessage; +import org.springframework.ws.client.core.WebServiceMessageCallback; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; +import org.springframework.ws.transport.context.TransportContext; +import org.springframework.ws.transport.context.TransportContextHolder; +import org.springframework.ws.transport.http.MessageDispatcherServlet; +import org.springframework.ws.transport.support.FreePortScanner; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import java.io.IOException; + +import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ObservationInterceptorIntegrationTest { + + private static AnnotationConfigWebApplicationContext applicationContext; + private WebServiceTemplate webServiceTemplate; + private TestObservationRegistry registry; + private static Server server; + + private final String requestPayload = ""; + + private TransformerFactory transformerFactory = TransformerFactory.newInstance(); + private Transformer transformer; + + private static String baseUrl; + + @BeforeAll + public static void startServer() throws Exception { + + int port = FreePortScanner.getFreePort(); + + baseUrl = "http://localhost:" + port + "/ws"; + + server = new Server(port); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + + applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.scan(WebServiceConfig.class.getPackage().getName()); + + MessageDispatcherServlet dispatcherServlet = new MessageDispatcherServlet(applicationContext); + dispatcherServlet.setTransformWsdlLocations(true); + + + + ServletHolder servletHolder = new ServletHolder(dispatcherServlet); + context.addServlet(servletHolder, "/ws/*"); + + server.setHandler(context); + server.start(); + } + + @AfterAll + public static void stopServer() throws Exception { + server.stop(); + } + + @BeforeEach + void setUp() throws TransformerConfigurationException { + + + webServiceTemplate = applicationContext.getBean(WebServiceTemplate.class); + registry = applicationContext.getBean(TestObservationRegistry.class); + + transformer = transformerFactory.newTransformer(); + } + + @Test + void testObservationInterceptorBehavior() { + + MyEndpoint.MyRequest request = new MyEndpoint.MyRequest(); + request.setName("John"); + MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl, request, new WebServiceMessageCallback() { + @Override + public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException { + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareSenderWebServiceConnection connection = + (HeadersAwareSenderWebServiceConnection) transportContext.getConnection(); + connection.addRequestHeader("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); + } + }); + + // Assertions based on expected behavior of ObservationInterceptor + assertNotNull(response); + + assertThat(registry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localname", "request") + .hasLowCardinalityKeyValue("soapaction", "none") + .hasContextualNameEqualTo("WebServiceEndpoint http://springframework.org/spring-ws:request") + .hasNameEqualTo("webservice.server") + ); + } + + @AfterEach + void tearDown() { + applicationContext.close(); + } +} \ No newline at end of file diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java new file mode 100644 index 000000000..ac32b46a5 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java @@ -0,0 +1,59 @@ +package org.springframework.ws.server.endpoint.observation; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.test.context.TestPropertySource; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.config.annotation.EnableWs; +import org.springframework.ws.config.annotation.WsConfigurerAdapter; +import org.springframework.ws.server.EndpointInterceptor; + +import java.util.List; + +@EnableWs +@Configuration +@TestPropertySource(properties = "management.tracing.sampling.probability=1") +public class WebServiceConfig extends WsConfigurerAdapter { + + @Autowired + private ObservationRegistry observationRegistry; + + @Bean + public Jaxb2Marshaller marshaller() { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(MyEndpoint.MyRequest.class, MyEndpoint.MyResponse.class); + return marshaller; + } + + @Bean + public ObservationRegistry observationRegistry() { + TestObservationRegistry registry = TestObservationRegistry.create(); + + registry.observationConfig(); + return registry; + } + + @Bean + public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller marshaller) { + WebServiceTemplate webServiceTemplate = new WebServiceTemplate(); + webServiceTemplate.setMarshaller(marshaller); + webServiceTemplate.setUnmarshaller(marshaller); + return webServiceTemplate; + } + + @Bean + public EndpointInterceptor observationInterceptor() { + return new ObservationInterceptor(observationRegistry); // Replace with your actual interceptor + } + + + @Override + public void addInterceptors(List interceptors) { + interceptors.add(observationInterceptor()); + } + +} \ No newline at end of file From 50b65c8f17d488b2c21ef0d207ea20543f34e2ac Mon Sep 17 00:00:00 2001 From: johkin Date: Sat, 9 Nov 2024 23:19:27 +0100 Subject: [PATCH 02/15] Move versions to parent pom properties #1094 --- pom.xml | 1 + spring-ws-core/pom.xml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cbc803fe7..728d28bf1 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,7 @@ 1.78 1.14.13 0.0.5 + 1.13.5 diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index b119349f7..5ff7eceb3 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -60,6 +60,11 @@ + + io.micrometer + micrometer-observation + ${micrometer-observation.version} + org.springframework spring-test @@ -247,7 +252,7 @@ io.micrometer micrometer-observation-test - 1.13.3 + ${micrometer-observation.version} test From cf3ce5e4963b948e6ca2873b022244fa32ebb667 Mon Sep 17 00:00:00 2001 From: johkin Date: Sun, 10 Nov 2024 08:56:14 +0100 Subject: [PATCH 03/15] Set micrometer dependency as optional #1094 --- spring-ws-core/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 5ff7eceb3..da544ab66 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -64,6 +64,7 @@ io.micrometer micrometer-observation ${micrometer-observation.version} + true org.springframework From 260e1fb2ba3612973a2a561bd8d5f569e63227b2 Mon Sep 17 00:00:00 2001 From: johkin Date: Sun, 10 Nov 2024 09:51:46 +0100 Subject: [PATCH 04/15] Add license header and documentation #1094 --- .../DefaultWebServiceTemplateConvention.java | 15 +++++++++++++ .../observation/RootElementSAXHandler.java | 21 +++++++++++++++++-- .../WebServiceObservationInterceptor.java | 15 +++++++++++++ .../WebServiceTemplateConvention.java | 17 ++++++++++++++- .../WebServiceTemplateObservationContext.java | 20 +++++++++++++++++- ...rviceTemplateObservationDocumentation.java | 15 +++++++++++++ .../DefaultWebServiceEndpointConvention.java | 17 ++++++++++++++- .../EndpointObservationDocumentation.java | 17 ++++++++++++++- .../observation/ObservationInterceptor.java | 20 +++++++++++++++++- .../WebServiceEndpointContext.java | 20 +++++++++++++++++- .../WebServiceEndpointConvention.java | 17 ++++++++++++++- ...iceTemplateObservationIntegrationTest.java | 1 + .../endpoint/observation/MyEndpoint.java | 19 +++++++++++++++++ ...ObservationInterceptorIntegrationTest.java | 19 +++++++++++++++++ .../observation/WebServiceConfig.java | 21 +++++++++++++++++-- 15 files changed, 243 insertions(+), 11 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java index 6de70429a..c37ef6ae6 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java index 770021b81..5dac81392 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java @@ -1,10 +1,28 @@ +/* + * 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 org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; import javax.xml.namespace.QName; - +/** + * DefaultHandler that extracts the root elements namespace and name. + * @author Johan Kindgren + */ public class RootElementSAXHandler extends DefaultHandler { private QName rootElementName = null; @@ -12,7 +30,6 @@ public class RootElementSAXHandler extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if (rootElementName == null) { - // Spara rotnodens localName och namespaceURI rootElementName = new QName(uri, localName); } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index d404acffc..320922628 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java index d26e15597..fe2d96daa 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java @@ -1,10 +1,25 @@ +/* + * 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 describes how a WebServiceTemplate is observed. + * ObservationConvention that can be implemented to create a custom observation. * @author Johan Kindgren */ public interface WebServiceTemplateConvention extends ObservationConvention { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java index dee1f3a2f..b04ce27e6 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -1,3 +1,18 @@ +/* + * 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.transport.RequestReplySenderContext; @@ -5,7 +20,10 @@ import org.springframework.ws.transport.TransportInputStream; import java.io.IOException; - +/** + * ObservationContext used to instrument a WebServiceTemplate operation. + * @author Johan Kindgren + */ class WebServiceTemplateObservationContext extends RequestReplySenderContext { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java index 29104a610..e39c53b52 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java index e4c31fb8a..646deb5d8 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.common.KeyValue; @@ -5,7 +20,7 @@ import io.micrometer.observation.Observation; /** - * ObservationConvention that describes how a WebServiceTemplate is observed. + * Default ObservationConvention for a WebService Endpoint. * @author Johan Kindgren */ public class DefaultWebServiceEndpointConvention implements WebServiceEndpointConvention { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java index d0c94cc8f..98d683cd5 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.common.KeyValue; @@ -7,7 +22,7 @@ import io.micrometer.observation.docs.ObservationDocumentation; /** - * ObservationDocumentation for EndpointAdapter. + * ObservationDocumentation for a WebService Endpoint. * * @author Johan Kindgren */ diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index b45903fb2..83f0458b0 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.observation.Observation; @@ -23,7 +38,10 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; - +/** + * Interceptor implementation that creates an observation for a WebService Endpoint. + * @author Johan Kindgren + */ public class ObservationInterceptor extends EndpointInterceptorAdapter { private ObservationRegistry observationRegistry; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java index 230a4895f..206bff8c8 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -1,6 +1,20 @@ +/* + * 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.server.endpoint.observation; -import io.micrometer.observation.transport.Propagator; import io.micrometer.observation.transport.RequestReplyReceiverContext; import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; import org.springframework.ws.transport.TransportInputStream; @@ -8,6 +22,10 @@ import java.io.IOException; import java.util.Iterator; +/** + * ObservationContext that describes how a WebService Endpoint is observed. + * @author Johan Kindgren + */ public class WebServiceEndpointContext extends RequestReplyReceiverContext { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java index 81452ba8b..4b5dd65ef 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java @@ -1,10 +1,25 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; /** - * ObservationConvention that describes how a WebServiceTemplate is observed. + * ObservationConvention that describes how a WebService Endpoint is observed. * @author Johan Kindgren */ public interface WebServiceEndpointConvention extends ObservationConvention { diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java index 637b3ac2d..538399715 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -64,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** + * Verifies observation for a WebServiceTemplate * @author Johan Kindgren */ public class WebServiceTemplateObservationIntegrationTest { diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java index ea898e449..1540791ac 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import jakarta.xml.bind.annotation.XmlRootElement; @@ -6,6 +21,10 @@ import org.springframework.ws.server.endpoint.annotation.RequestPayload; import org.springframework.ws.server.endpoint.annotation.ResponsePayload; +/** + * Testing endpoint. + * @author Johan Kindgren + */ @Endpoint public class MyEndpoint { diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java index 63eab4969..06ca53eeb 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.observation.tck.TestObservationRegistry; @@ -24,6 +39,10 @@ import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; +/** + * Verifies observation for a WebService Endpoint. + * @author Johan Kindgren + */ public class ObservationInterceptorIntegrationTest { private static AnnotationConfigWebApplicationContext applicationContext; diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java index ac32b46a5..b93f20114 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java @@ -1,3 +1,18 @@ +/* + * 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.server.endpoint.observation; import io.micrometer.observation.ObservationRegistry; @@ -6,7 +21,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.oxm.jaxb.Jaxb2Marshaller; -import org.springframework.test.context.TestPropertySource; import org.springframework.ws.client.core.WebServiceTemplate; import org.springframework.ws.config.annotation.EnableWs; import org.springframework.ws.config.annotation.WsConfigurerAdapter; @@ -14,9 +28,12 @@ import java.util.List; +/** + * Verifies observation for a WebService Endpoint. + * @author Johan Kindgren + */ @EnableWs @Configuration -@TestPropertySource(properties = "management.tracing.sampling.probability=1") public class WebServiceConfig extends WsConfigurerAdapter { @Autowired From d98b61a9bac67d9d034973b4296738b09584686e Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 11 Nov 2024 23:19:26 +0100 Subject: [PATCH 05/15] Add Javadoc #1094 --- ...rviceTemplateObservationDocumentation.java | 22 ++++++++++++------- .../EndpointObservationDocumentation.java | 19 +++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java index e39c53b52..ce318568b 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -40,10 +40,6 @@ public KeyName[] getLowCardinalityKeyNames() { return LowCardinalityKeyNames.values(); } - @Override - public KeyName[] getHighCardinalityKeyNames() { - return super.getHighCardinalityKeyNames(); - } }; enum LowCardinalityKeyNames implements KeyName { @@ -68,7 +64,9 @@ public String asString() { return "outcome"; } }, - + /** + * Namespace of the WebService payload. + */ NAMESPACE { @Override public String asString() { @@ -76,19 +74,27 @@ public String asString() { } }, - LOCALNAME { + /** + * Localpart of the WebService payload. + */ + LOCALPART { @Override public String asString() { - return "localname"; + return "localpart"; } }, + /** + * Host for the WebService call. + */ HOST { @Override public String asString() { return "host"; } }, - + /** + * Value from the SoapAction header. + */ SOAPACTION { @Override public String asString() { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java index 98d683cd5..ca7fa6bf6 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -39,10 +39,6 @@ public KeyName[] getLowCardinalityKeyNames() { return LowCardinalityKeyNames.values(); } - @Override - public KeyName[] getHighCardinalityKeyNames() { - return super.getHighCardinalityKeyNames(); - } }; enum LowCardinalityKeyNames implements KeyName { @@ -67,21 +63,28 @@ public String asString() { return "outcome"; } }, - + /** + * Namespace of the WebService payload. + */ NAMESPACE { @Override public String asString() { return "namespace"; } }, - - LOCALNAME { + /** + * Localpart of the WebService payload. + */ + LOCALPART { @Override public String asString() { - return "localname"; + return "localpart"; } }, + /** + * Value from the SoapAction header. + */ SOAPACTION { @Override public String asString() { From f115593f2ff920faa7e260cf4d80d35d55cb3496 Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 11 Nov 2024 23:27:15 +0100 Subject: [PATCH 06/15] Minor modifications after review Changed name of key "localname" to "localpart" Removed superfluos method implementations Modified implementation of ContextualName #1094 --- .../DefaultWebServiceTemplateConvention.java | 46 ++++++----------- .../WebServiceObservationInterceptor.java | 51 +++++++++++++++---- .../WebServiceTemplateObservationContext.java | 15 +++--- .../DefaultWebServiceEndpointConvention.java | 19 +++---- .../observation/ObservationInterceptor.java | 16 ++++-- .../WebServiceEndpointContext.java | 11 ++-- ...iceTemplateObservationIntegrationTest.java | 6 +-- ...ObservationInterceptorIntegrationTest.java | 2 +- 8 files changed, 90 insertions(+), 76 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java index c37ef6ae6..dd818e6be 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java @@ -17,7 +17,7 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; -import io.micrometer.observation.Observation; +import org.springframework.ws.client.core.observation.WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames; /** * ObservationConvention that describes how a WebServiceTemplate is observed. @@ -25,8 +25,9 @@ */ public class DefaultWebServiceTemplateConvention implements WebServiceTemplateConvention { - private static final KeyValue EXCEPTION_NONE = KeyValue.of(WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); - private String name = "webservice.client"; + private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, + KeyValue.NONE_VALUE); + private static final String NAME = "webservice.client"; @Override @@ -41,68 +42,51 @@ public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext } private KeyValue localname(WebServiceTemplateObservationContext context) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames - .LOCALNAME - .withValue(context.getLocalname()); + return LowCardinalityKeyNames + .LOCALPART + .withValue(context.getLocalPart()); } private KeyValue namespace(WebServiceTemplateObservationContext context) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames + return LowCardinalityKeyNames .NAMESPACE .withValue(context.getNamespace()); } private KeyValue host(WebServiceTemplateObservationContext context) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames + return LowCardinalityKeyNames .HOST .withValue(context.getHost()); } private KeyValue outcome(WebServiceTemplateObservationContext context) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames + return LowCardinalityKeyNames .OUTCOME .withValue(context.getOutcome()); } private KeyValue soapAction(WebServiceTemplateObservationContext context) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames + return LowCardinalityKeyNames .SOAPACTION .withValue(context.getSoapAction()); } private KeyValue exception(WebServiceTemplateObservationContext context) { if (context.getError() != null) { - return WebServiceTemplateObservationDocumentation - .LowCardinalityKeyNames + return LowCardinalityKeyNames .EXCEPTION .withValue(context.getError().getClass().getSimpleName()); - } else { - return EXCEPTION_NONE; } - } - - @Override - public KeyValues getHighCardinalityKeyValues(WebServiceTemplateObservationContext context) { - return KeyValues.empty(); + return EXCEPTION_NONE; } @Override public String getName() { - return name; + return NAME; } @Override public String getContextualName(WebServiceTemplateObservationContext context) { - return "WebServiceTemplate " + context.getHost(); - } - - @Override - public boolean supportsContext(Observation.Context context) { - return context instanceof WebServiceTemplateObservationContext; + return context.getContextualName(); } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index 320922628..71b0d2e7e 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -15,8 +15,10 @@ */ package org.springframework.ws.client.core.observation; +import io.micrometer.common.KeyValue; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.springframework.util.Assert; import org.springframework.ws.FaultAwareWebServiceMessage; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.client.WebServiceClientException; @@ -24,6 +26,7 @@ import org.springframework.ws.context.MessageContext; import org.springframework.ws.soap.SoapMessage; 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; @@ -38,6 +41,7 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; +import java.net.URI; import java.net.URISyntaxException; /** @@ -50,6 +54,7 @@ public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { + public static final String OBSERVATION_KEY = "observation"; private ObservationRegistry observationRegistry; private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention(); @@ -86,7 +91,7 @@ public boolean handleRequest(MessageContext messageContext) throws WebServiceCli () -> new WebServiceTemplateObservationContext(connection), observationRegistry); - messageContext.setProperty("observation", observation); + messageContext.setProperty(OBSERVATION_KEY, observation); return true; } @@ -94,11 +99,10 @@ public boolean handleRequest(MessageContext messageContext) throws WebServiceCli @Override public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException { - Observation observation = (Observation) messageContext.getProperty("observation"); - WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext(); + Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); + Assert.notNull(observation, "Expected observation in messageContext"); - context.setHost(getHostFromConnection()); - context.setError(ex); + WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext(); WebServiceMessage request = messageContext.getRequest(); WebServiceMessage response = messageContext.getResponse(); @@ -108,10 +112,12 @@ public void afterCompletion(MessageContext messageContext, Exception ex) throws Source source = soapMessage.getSoapBody().getPayloadSource(); QName root = getRootElement(source); if (root != null) { - context.setLocalname(root.getLocalPart()); + context.setLocalPart(root.getLocalPart()); context.setNamespace(root.getNamespaceURI()); } - context.setSoapAction((soapMessage).getSoapAction()); + if (soapMessage.getSoapAction() != null && !soapMessage.getSoapAction().equals(TransportConstants.EMPTY_SOAP_ACTION)) { + context.setSoapAction(soapMessage.getSoapAction()); + } } if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { @@ -122,6 +128,31 @@ public void afterCompletion(MessageContext messageContext, Exception ex) throws } } + URI uri = getUriFromConnection(); + if (uri != null) { + context.setHost(uri.getHost()); + + StringBuilder contextualName = new StringBuilder("WebService "); + contextualName.append(uri.getHost()); + if (uri.getPort() != -1) { + contextualName.append(":").append(uri.getPort()); + } + contextualName.append(uri.getPath()); + + if (!context.getSoapAction().equals(KeyValue.NONE_VALUE)) { + contextualName.append(" Action=").append(context.getSoapAction()); + } + if (!context.getNamespace().equals(WebServiceTemplateObservationContext.UNKNOWN)) { + contextualName + .append(" QName=").append(context.getNamespace()) + .append(":").append(context.getLocalPart()); + } + + context.setContextualName(contextualName.toString()); + } + + context.setError(ex); + observation.stop(); } @@ -129,13 +160,13 @@ public void setCustomConvention(WebServiceTemplateConvention customConvention) { this.customConvention = customConvention; } - String getHostFromConnection() { + URI getUriFromConnection() { TransportContext transportContext = TransportContextHolder.getTransportContext(); WebServiceConnection connection = transportContext.getConnection(); try { - return connection.getUri().getHost(); + return connection.getUri(); } catch (URISyntaxException e) { - return WebServiceTemplateObservationContext.UNKNOWN; + return null; } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java index b04ce27e6..95ab31ec5 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -15,6 +15,7 @@ */ package org.springframework.ws.client.core.observation; +import io.micrometer.common.KeyValue; import io.micrometer.observation.transport.RequestReplySenderContext; import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; import org.springframework.ws.transport.TransportInputStream; @@ -24,15 +25,15 @@ * ObservationContext used to instrument a WebServiceTemplate operation. * @author Johan Kindgren */ -class WebServiceTemplateObservationContext extends RequestReplySenderContext { +public class WebServiceTemplateObservationContext extends RequestReplySenderContext { public static final String UNKNOWN = "unknown"; private String outcome = UNKNOWN; - private String localname = UNKNOWN; + private String localPart = UNKNOWN; private String namespace = UNKNOWN; private String host = UNKNOWN; - private String soapAction = UNKNOWN; + private String soapAction = KeyValue.NONE_VALUE; public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnection connection) { super((carrier, key, value) -> { @@ -56,12 +57,12 @@ public void setOutcome(String outcome) { this.outcome = outcome; } - public String getLocalname() { - return localname; + public String getLocalPart() { + return localPart; } - public void setLocalname(String localname) { - this.localname = localname; + public void setLocalPart(String localPart) { + this.localPart = localPart; } public String getNamespace() { diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java index 646deb5d8..767ae75fe 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java @@ -26,25 +26,25 @@ public class DefaultWebServiceEndpointConvention implements WebServiceEndpointConvention { private static final KeyValue EXCEPTION_NONE = KeyValue.of(EndpointObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); - private String name = "webservice.server"; + private static final String NAME = "webservice.server"; @Override public KeyValues getLowCardinalityKeyValues(WebServiceEndpointContext context) { return KeyValues.of( exception(context), - localname(context), + localPart(context), namespace(context), outcome(context), soapAction(context)); } - private KeyValue localname(WebServiceEndpointContext context) { + private KeyValue localPart(WebServiceEndpointContext context) { return EndpointObservationDocumentation .LowCardinalityKeyNames - .LOCALNAME - .withValue(context.getLocalname()); + .LOCALPART + .withValue(context.getLocalPart()); } private KeyValue namespace(WebServiceEndpointContext context) { @@ -80,19 +80,14 @@ private KeyValue exception(WebServiceEndpointContext context) { } } - @Override - public KeyValues getHighCardinalityKeyValues(WebServiceEndpointContext context) { - return KeyValues.empty(); - } - @Override public String getName() { - return name; + return NAME; } @Override public String getContextualName(WebServiceEndpointContext context) { - return "WebServiceEndpoint " + context.getNamespace() + ':' + context.getLocalname(); + return context.getContextualName(); } @Override diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index 83f0458b0..f112bcff0 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -17,6 +17,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.springframework.util.Assert; import org.springframework.ws.FaultAwareWebServiceMessage; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.client.core.observation.RootElementSAXHandler; @@ -44,6 +45,7 @@ */ public class ObservationInterceptor extends EndpointInterceptorAdapter { + public static final String OBSERVATION_KEY = "observation"; private ObservationRegistry observationRegistry; private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); @@ -80,17 +82,17 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr () -> new WebServiceEndpointContext(connection), observationRegistry); - messageContext.setProperty("observation", observation); + messageContext.setProperty(OBSERVATION_KEY, observation); return true; } - - @Override public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) throws Exception { - Observation observation = (Observation) messageContext.getProperty("observation"); + Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); + Assert.notNull(observation, "Expected observation in messageContext"); + WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); context.setError(ex); @@ -103,7 +105,7 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce Source source = soapMessage.getSoapBody().getPayloadSource(); QName root = getRootElement(source); if (root != null) { - context.setLocalname(root.getLocalPart()); + context.setLocalPart(root.getLocalPart()); context.setNamespace(root.getNamespaceURI()); } String action = soapMessage.getSoapAction(); @@ -122,6 +124,10 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce context.setOutcome("fault"); } } + StringBuilder contextualName = new StringBuilder("WebServiceEndpoint ") + .append(context.getNamespace()).append(":") + .append(context.getLocalPart()); + context.setContextualName(contextualName.toString()); observation.stop(); } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java index 206bff8c8..c87e36fd9 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -28,10 +28,9 @@ */ public class WebServiceEndpointContext extends RequestReplyReceiverContext { - public static final String UNKNOWN = "unknown"; private String outcome = UNKNOWN; - private String localname = UNKNOWN; + private String localPart = UNKNOWN; private String namespace = UNKNOWN; private String soapAction = UNKNOWN; @@ -59,12 +58,12 @@ public void setOutcome(String outcome) { this.outcome = outcome; } - public String getLocalname() { - return localname; + public String getLocalPart() { + return localPart; } - public void setLocalname(String localname) { - this.localname = localname; + public void setLocalPart(String localPart) { + this.localPart = localPart; } public String getNamespace() { diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java index 538399715..14f558a8e 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -156,8 +156,7 @@ public void sendSourceAndReceiveToResult() { .hasLowCardinalityKeyValue("exception", "none") .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") - .hasLowCardinalityKeyValue("localname", "root") - .hasContextualNameEqualTo("WebServiceTemplate localhost") + .hasLowCardinalityKeyValue("localpart", "root") ); } @@ -266,8 +265,7 @@ public void notFound() { .hasLowCardinalityKeyValue("exception", "WebServiceTransportException") .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") - .hasLowCardinalityKeyValue("localname", "root") - .hasContextualNameEqualTo("WebServiceTemplate localhost") + .hasLowCardinalityKeyValue("localpart", "root") ); } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java index 06ca53eeb..c3e231ff1 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -121,7 +121,7 @@ public void doWithMessage(WebServiceMessage message) throws IOException, Transfo .hasLowCardinalityKeyValue("outcome", "success") .hasLowCardinalityKeyValue("exception", "none") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") - .hasLowCardinalityKeyValue("localname", "request") + .hasLowCardinalityKeyValue("localpart", "request") .hasLowCardinalityKeyValue("soapaction", "none") .hasContextualNameEqualTo("WebServiceEndpoint http://springframework.org/spring-ws:request") .hasNameEqualTo("webservice.server") From 8ced9ddee0ecfb95025143f8b95fd2a3668815b3 Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 11 Nov 2024 23:37:40 +0100 Subject: [PATCH 07/15] Add Javadoc #1094 --- .../WebServiceTemplateObservationDocumentation.java | 9 ++++++++- .../EndpointObservationDocumentation.java | 13 ++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java index ce318568b..276c22fe4 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -27,7 +27,11 @@ * @author Johan Kindgren */ public enum WebServiceTemplateObservationDocumentation implements ObservationDocumentation { - + /** + * This enum constant defines observation documentation for the WebServiceTemplate. + * It provides the default observation convention and low cardinality key names + * relevant to WebService operations. + */ WEB_SERVICE_TEMPLATE { @Override @@ -42,6 +46,9 @@ public KeyName[] getLowCardinalityKeyNames() { }; + /** + * Enum representing low cardinality key names for observing a WebServiceTemplate. + */ enum LowCardinalityKeyNames implements KeyName { /** diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java index ca7fa6bf6..619239c16 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -27,7 +27,15 @@ * @author Johan Kindgren */ public enum EndpointObservationDocumentation implements ObservationDocumentation { - + /** + * An enumeration for ObservationDocumentation related to WebService Endpoint. + * + * The {@code WEB_SERVICE_ENDPOINT} provides default conventions and low cardinality key names for + * observing a WebService endpoint. + * + * This implementation returns the {@link DefaultWebServiceEndpointConvention} class as the default convention, + * and an array of {@link LowCardinalityKeyNames} for low cardinality key names. + */ WEB_SERVICE_ENDPOINT { @Override public Class> getDefaultConvention() { @@ -41,6 +49,9 @@ public KeyName[] getLowCardinalityKeyNames() { }; + /** + * Enum representing low cardinality key names for observing a WebService endpoint. + */ enum LowCardinalityKeyNames implements KeyName { /** From f6bd2301acc12f921e16ed2223571a5d5a284a87 Mon Sep 17 00:00:00 2001 From: johkin Date: Tue, 12 Nov 2024 14:47:39 +0100 Subject: [PATCH 08/15] Updated ContextualName to match Span name conventions #1094 --- .../WebServiceObservationInterceptor.java | 21 ++----------- .../observation/ObservationInterceptor.java | 31 ++++++++++++++----- ...iceTemplateObservationIntegrationTest.java | 2 ++ ...ObservationInterceptorIntegrationTest.java | 19 ++---------- 4 files changed, 31 insertions(+), 42 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index 71b0d2e7e..b4279c1fb 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -15,7 +15,6 @@ */ package org.springframework.ws.client.core.observation; -import io.micrometer.common.KeyValue; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.springframework.util.Assert; @@ -132,23 +131,9 @@ public void afterCompletion(MessageContext messageContext, Exception ex) throws if (uri != null) { context.setHost(uri.getHost()); - StringBuilder contextualName = new StringBuilder("WebService "); - contextualName.append(uri.getHost()); - if (uri.getPort() != -1) { - contextualName.append(":").append(uri.getPort()); - } - contextualName.append(uri.getPath()); - - if (!context.getSoapAction().equals(KeyValue.NONE_VALUE)) { - contextualName.append(" Action=").append(context.getSoapAction()); - } - if (!context.getNamespace().equals(WebServiceTemplateObservationContext.UNKNOWN)) { - contextualName - .append(" QName=").append(context.getNamespace()) - .append(":").append(context.getLocalPart()); - } - - context.setContextualName(contextualName.toString()); + context.setContextualName("POST " + uri.getPath()); + } else { + context.setContextualName("POST"); } context.setError(ex); diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index f112bcff0..d421cdd6f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -26,6 +26,7 @@ import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; 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 org.w3c.dom.Node; @@ -39,6 +40,9 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; +import java.net.URI; +import java.net.URISyntaxException; + /** * Interceptor implementation that creates an observation for a WebService Endpoint. * @author Johan Kindgren @@ -46,6 +50,7 @@ public class ObservationInterceptor extends EndpointInterceptorAdapter { public static final String OBSERVATION_KEY = "observation"; + public static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private ObservationRegistry observationRegistry; private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); @@ -124,14 +129,26 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce context.setOutcome("fault"); } } - StringBuilder contextualName = new StringBuilder("WebServiceEndpoint ") - .append(context.getNamespace()).append(":") - .append(context.getLocalPart()); - context.setContextualName(contextualName.toString()); + URI requestUri = getUriFromConnection(); + if (requestUri != null) { + context.setContextualName("POST " + requestUri.getPath()); + } else { + context.setContextualName("POST"); + } observation.stop(); } + URI getUriFromConnection() { + TransportContext transportContext = TransportContextHolder.getTransportContext(); + WebServiceConnection connection = transportContext.getConnection(); + try { + return connection.getUri(); + } catch (URISyntaxException e) { + return null; + } + } + QName getRootElement(Source source) { if (source instanceof DOMSource) { Node root = ((DOMSource) source).getNode(); @@ -143,7 +160,7 @@ QName getRootElement(Source source) { saxParser.parse(((StreamSource) source).getInputStream(), handler); return handler.getRootElementName(); } catch (Exception e) { - return new QName("unknown", "unknow"); + return UNKNOWN_Q_NAME; } } if (source instanceof SAXSource) { @@ -152,10 +169,10 @@ QName getRootElement(Source source) { saxParser.parse(((SAXSource) source).getInputSource(), handler); return handler.getRootElementName(); } catch (Exception e) { - return new QName("unknown", "unknow"); + return UNKNOWN_Q_NAME; } } - return new QName("unknown", "unknow"); + return UNKNOWN_Q_NAME; } public void setCustomConvention(WebServiceEndpointConvention customConvention) { diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java index 14f558a8e..1eaaacad0 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -157,6 +157,7 @@ public void sendSourceAndReceiveToResult() { .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "root") + .hasContextualNameEqualTo("POST /soap/echo") ); } @@ -266,6 +267,7 @@ public void notFound() { .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "root") + .hasContextualNameEqualTo("POST /errors/notfound") ); } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java index c3e231ff1..7bfa93e13 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -21,20 +21,13 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.jupiter.api.*; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.ws.WebServiceMessage; -import org.springframework.ws.client.core.WebServiceMessageCallback; import org.springframework.ws.client.core.WebServiceTemplate; -import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; -import org.springframework.ws.transport.context.TransportContext; -import org.springframework.ws.transport.context.TransportContextHolder; import org.springframework.ws.transport.http.MessageDispatcherServlet; import org.springframework.ws.transport.support.FreePortScanner; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; -import java.io.IOException; import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -103,15 +96,7 @@ void testObservationInterceptorBehavior() { MyEndpoint.MyRequest request = new MyEndpoint.MyRequest(); request.setName("John"); - MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl, request, new WebServiceMessageCallback() { - @Override - public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException { - TransportContext transportContext = TransportContextHolder.getTransportContext(); - HeadersAwareSenderWebServiceConnection connection = - (HeadersAwareSenderWebServiceConnection) transportContext.getConnection(); - connection.addRequestHeader("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); - } - }); + MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl, request); // Assertions based on expected behavior of ObservationInterceptor assertNotNull(response); @@ -123,7 +108,7 @@ public void doWithMessage(WebServiceMessage message) throws IOException, Transfo .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "request") .hasLowCardinalityKeyValue("soapaction", "none") - .hasContextualNameEqualTo("WebServiceEndpoint http://springframework.org/spring-ws:request") + .hasContextualNameEqualTo("POST /ws") .hasNameEqualTo("webservice.server") ); } From a7df58586abca9b7a5606dd5dac58a5ca3a3f93f Mon Sep 17 00:00:00 2001 From: johkin Date: Thu, 14 Nov 2024 22:00:30 +0100 Subject: [PATCH 09/15] Minor changes after review #1094 --- .../DefaultWebServiceTemplateConvention.java | 14 +++ .../WebServiceObservationInterceptor.java | 33 +++-- .../WebServiceTemplateObservationContext.java | 15 ++- ...rviceTemplateObservationDocumentation.java | 24 +++- .../DefaultWebServiceEndpointConvention.java | 25 ++++ .../EndpointObservationDocumentation.java | 33 ++++- .../observation/ObservationInterceptor.java | 59 ++++++--- .../WebServiceEndpointContext.java | 28 ++++- ...iceTemplateObservationIntegrationTest.java | 119 ++++++++++-------- ...ObservationInterceptorIntegrationTest.java | 31 +++-- .../observation/WebServiceConfig.java | 8 +- 11 files changed, 274 insertions(+), 115 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java index dd818e6be..baf5452ed 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java @@ -29,6 +29,13 @@ public class DefaultWebServiceTemplateConvention implements WebServiceTemplateCo 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) { @@ -41,6 +48,13 @@ public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext soapAction(context)); } + private KeyValue path(WebServiceTemplateObservationContext context) { + + return WebServiceTemplateObservationDocumentation.HighCardinalityKeyNames + .PATH + .withValue(context.getPath()); + } + private KeyValue localname(WebServiceTemplateObservationContext context) { return LowCardinalityKeyNames .LOCALPART diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index b4279c1fb..5d796390f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -53,24 +53,24 @@ public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { - public static final String OBSERVATION_KEY = "observation"; - private ObservationRegistry observationRegistry; - + private static final String OBSERVATION_KEY = "observation"; private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention(); - private SAXParserFactory parserFactory; - private SAXParser saxParser; + private final ObservationRegistry observationRegistry; + private final SAXParserFactory parserFactory; + private final SAXParser saxParser; - private WebServiceTemplateConvention customConvention; + private final WebServiceTemplateConvention customConvention; - public WebServiceObservationInterceptor(ObservationRegistry observationRegistry) { + public WebServiceObservationInterceptor( + ObservationRegistry observationRegistry, + WebServiceTemplateConvention customConvention) { this.observationRegistry = observationRegistry; + this.customConvention = customConvention; parserFactory = SAXParserFactory.newNSInstance(); try { saxParser = parserFactory.newSAXParser(); - } catch (ParserConfigurationException e) { - throw new RuntimeException(e); - } catch (SAXException e) { + } catch (ParserConfigurationException | SAXException e) { throw new RuntimeException(e); } } @@ -96,7 +96,7 @@ public boolean handleRequest(MessageContext messageContext) throws WebServiceCli } @Override - public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException { + public void afterCompletion(MessageContext messageContext, Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); Assert.notNull(observation, "Expected observation in messageContext"); @@ -130,21 +130,16 @@ public void afterCompletion(MessageContext messageContext, Exception ex) throws URI uri = getUriFromConnection(); if (uri != null) { context.setHost(uri.getHost()); - - context.setContextualName("POST " + uri.getPath()); - } else { - context.setContextualName("POST"); + context.setPath(uri.getPath()); } + context.setContextualName("POST"); + context.setError(ex); observation.stop(); } - public void setCustomConvention(WebServiceTemplateConvention customConvention) { - this.customConvention = customConvention; - } - URI getUriFromConnection() { TransportContext transportContext = TransportContextHolder.getTransportContext(); WebServiceConnection connection = transportContext.getConnection(); diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java index 95ab31ec5..6a913c4f8 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -16,6 +16,7 @@ package org.springframework.ws.client.core.observation; import io.micrometer.common.KeyValue; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.transport.RequestReplySenderContext; import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; import org.springframework.ws.transport.TransportInputStream; @@ -27,6 +28,7 @@ */ public class WebServiceTemplateObservationContext extends RequestReplySenderContext { + private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(WebServiceTemplateObservationContext.class); public static final String UNKNOWN = "unknown"; private String outcome = UNKNOWN; @@ -34,6 +36,7 @@ public class WebServiceTemplateObservationContext extends RequestReplySenderCont private String namespace = UNKNOWN; private String host = UNKNOWN; private String soapAction = KeyValue.NONE_VALUE; + private String path = null; public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnection connection) { super((carrier, key, value) -> { @@ -42,7 +45,7 @@ public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnecti try { carrier.addRequestHeader(key, value); } catch (IOException e) { - throw new RuntimeException(e); + logger.log("Could not add key to carrier", e); } } }); @@ -73,7 +76,6 @@ public void setNamespace(String namespace) { this.namespace = namespace; } - public String getHost() { return host; } @@ -82,7 +84,6 @@ public void setHost(String host) { this.host = host; } - public String getSoapAction() { return soapAction; } @@ -90,4 +91,12 @@ public String getSoapAction() { public void setSoapAction(String soapAction) { this.soapAction = soapAction; } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java index 276c22fe4..9c4ff4758 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -26,7 +26,7 @@ * * @author Johan Kindgren */ -public enum WebServiceTemplateObservationDocumentation implements ObservationDocumentation { +enum WebServiceTemplateObservationDocumentation implements ObservationDocumentation { /** * This enum constant defines observation documentation for the WebServiceTemplate. * It provides the default observation convention and low cardinality key names @@ -44,8 +44,30 @@ public KeyName[] getLowCardinalityKeyNames() { return LowCardinalityKeyNames.values(); } + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } }; + enum HighCardinalityKeyNames implements KeyName { + /** + * Path for the client request. + * Optional value. + */ + PATH { + @Override + public String asString() { + return "path"; + } + + @Override + public boolean isRequired() { + return false; + } + } + } + /** * Enum representing low cardinality key names for observing a WebServiceTemplate. */ diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java index 767ae75fe..21e453425 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java @@ -36,8 +36,16 @@ public KeyValues getLowCardinalityKeyValues(WebServiceEndpointContext context) { localPart(context), namespace(context), outcome(context), + path(context), soapAction(context)); + } + @Override + public KeyValues getHighCardinalityKeyValues(WebServiceEndpointContext context) { + if (context.getPathInfo() != null) { + return KeyValues.of(pathInfo(context)); + } + return KeyValues.empty(); } private KeyValue localPart(WebServiceEndpointContext context) { @@ -80,6 +88,23 @@ private KeyValue exception(WebServiceEndpointContext context) { } } + private KeyValue path(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .PATH + .withValue(context.getPath()); + } + + private KeyValue pathInfo(WebServiceEndpointContext context) { + if (context.getPathInfo() != null) { + return EndpointObservationDocumentation + .HighCardinalityKeyNames + .PATH_INFO + .withValue(context.getPathInfo()); + } + return null; + } + @Override public String getName() { return NAME; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java index 619239c16..37d782657 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -26,7 +26,7 @@ * * @author Johan Kindgren */ -public enum EndpointObservationDocumentation implements ObservationDocumentation { +enum EndpointObservationDocumentation implements ObservationDocumentation { /** * An enumeration for ObservationDocumentation related to WebService Endpoint. * @@ -47,8 +47,30 @@ public KeyName[] getLowCardinalityKeyNames() { return LowCardinalityKeyNames.values(); } + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } }; + enum HighCardinalityKeyNames implements KeyName { + + /** + * Possible + */ + PATH_INFO { + @Override + public String asString() { + return "pathinfo"; + } + + @Override + public boolean isRequired() { + return false; + } + } + } + /** * Enum representing low cardinality key names for observing a WebService endpoint. */ @@ -101,6 +123,15 @@ public String asString() { public String asString() { return "soapaction"; } + }, + /** + * Path for the current request. + */ + PATH { + @Override + public String asString() { + return "path"; + } } } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index d421cdd6f..63fcfa501 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -17,6 +17,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.Assert; import org.springframework.ws.FaultAwareWebServiceMessage; import org.springframework.ws.WebServiceMessage; @@ -29,6 +30,7 @@ import org.springframework.ws.transport.WebServiceConnection; import org.springframework.ws.transport.context.TransportContext; import org.springframework.ws.transport.context.TransportContextHolder; +import org.springframework.ws.transport.http.HttpServletConnection; import org.w3c.dom.Node; import org.xml.sax.SAXException; @@ -49,26 +51,24 @@ */ public class ObservationInterceptor extends EndpointInterceptorAdapter { - public static final String OBSERVATION_KEY = "observation"; - public static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); - private ObservationRegistry observationRegistry; - + private static final String OBSERVATION_KEY = "observation"; + private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); - private SAXParserFactory parserFactory; - private SAXParser saxParser; + private final ObservationRegistry observationRegistry; + private final WebServiceEndpointConvention customConvention; + private final SAXParserFactory parserFactory; + private final SAXParser saxParser; - private WebServiceEndpointConvention customConvention; - public ObservationInterceptor(ObservationRegistry observationRegistry) { + public ObservationInterceptor(ObservationRegistry observationRegistry, WebServiceEndpointConvention customConvention) { this.observationRegistry = observationRegistry; + this.customConvention = customConvention; parserFactory = SAXParserFactory.newNSInstance(); try { saxParser = parserFactory.newSAXParser(); - } catch (ParserConfigurationException e) { - throw new RuntimeException(e); - } catch (SAXException e) { + } catch (ParserConfigurationException | SAXException e) { throw new RuntimeException(e); } } @@ -80,6 +80,16 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr HeadersAwareReceiverWebServiceConnection connection = (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); + if (connection instanceof HttpServletConnection) { + HttpServletConnection servletConnection = (HttpServletConnection) connection; + String servletPath = servletConnection.getHttpServletRequest().getServletPath(); + String pathInfo = servletConnection.getHttpServletRequest().getPathInfo(); + + + if (servletPath != null) { + + } + } Observation observation = EndpointObservationDocumentation.WEB_SERVICE_ENDPOINT.start( customConvention, @@ -93,7 +103,7 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr } @Override - public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) throws Exception { + public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); Assert.notNull(observation, "Expected observation in messageContext"); @@ -129,9 +139,24 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce context.setOutcome("fault"); } } - URI requestUri = getUriFromConnection(); - if (requestUri != null) { - context.setContextualName("POST " + requestUri.getPath()); + + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareReceiverWebServiceConnection connection = + (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); + + if (connection instanceof HttpServletConnection) { + HttpServletConnection servletConnection = (HttpServletConnection) connection; + HttpServletRequest servletRequest = servletConnection.getHttpServletRequest(); + String servletPath = servletRequest.getServletPath(); + String pathInfo = servletRequest.getPathInfo(); + + if (pathInfo != null) { + context.setContextualName("POST " + servletPath + "/{pathInfo}"); + context.setPath(servletPath + "/{pathInfo}"); + context.setPathInfo(pathInfo); + } else { + context.setContextualName("POST " + servletPath); + } } else { context.setContextualName("POST"); } @@ -174,8 +199,4 @@ QName getRootElement(Source source) { } return UNKNOWN_Q_NAME; } - - public void setCustomConvention(WebServiceEndpointConvention customConvention) { - this.customConvention = customConvention; - } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java index c87e36fd9..5c0ecf944 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -15,6 +15,7 @@ */ package org.springframework.ws.server.endpoint.observation; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.transport.RequestReplyReceiverContext; import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; import org.springframework.ws.transport.TransportInputStream; @@ -28,11 +29,15 @@ */ public class WebServiceEndpointContext extends RequestReplyReceiverContext { - public static final String UNKNOWN = "unknown"; + private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(WebServiceEndpointContext.class); + private static final String UNKNOWN = "unknown"; + private String outcome = UNKNOWN; private String localPart = UNKNOWN; private String namespace = UNKNOWN; private String soapAction = UNKNOWN; + private String path = UNKNOWN; + private String pathInfo = null; public WebServiceEndpointContext(HeadersAwareReceiverWebServiceConnection connection) { super((carrier, key) -> { @@ -40,12 +45,11 @@ public WebServiceEndpointContext(HeadersAwareReceiverWebServiceConnection connec Iterator headers = carrier.getRequestHeaders(key); if (headers.hasNext()) { return headers.next(); - } else { - return null; } } catch (IOException e) { - throw new RuntimeException(e); + logger.log("Could not read key from carrier", e); } + return null; }); setCarrier(connection); } @@ -81,4 +85,20 @@ public String getSoapAction() { public void setSoapAction(String soapAction) { this.soapAction = soapAction; } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + public String getPathInfo() { + return pathInfo; + } } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java index 1eaaacad0..81cf82a73 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -38,7 +38,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.oxm.Marshaller; -import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; import org.springframework.ws.client.WebServiceTransportException; import org.springframework.ws.client.core.AbstractSoap12WebServiceTemplateIntegrationTestCase; @@ -54,7 +53,10 @@ import org.springframework.xml.transform.TransformerFactoryUtils; import org.xmlunit.assertj.XmlAssert; -import javax.xml.transform.*; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; import java.io.IOException; import java.util.Enumeration; import java.util.Iterator; @@ -136,7 +138,7 @@ public void createWebServiceTemplate() throws Exception { template = new WebServiceTemplate(new SaajSoapMessageFactory(MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL))); template.setMessageSender(new HttpComponentsMessageSender()); template.setInterceptors(new ClientInterceptor[]{ - new WebServiceObservationInterceptor(observationRegistry) + new WebServiceObservationInterceptor(observationRegistry, null) }); } @@ -157,7 +159,8 @@ public void sendSourceAndReceiveToResult() { .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "root") - .hasContextualNameEqualTo("POST /soap/echo") + .hasHighCardinalityKeyValue("path", "/soap/echo") + .hasContextualNameEqualTo("POST") ); } @@ -167,59 +170,20 @@ public void sendSourceAndReceiveToResultNoResponse() { boolean b = template.sendSourceAndReceiveToResult(baseUrl + "/soap/noResponse", new StringSource(messagePayload), new StringResult()); assertThat(b).isFalse(); - } - - @Test - public void marshalSendAndReceiveResponse() throws TransformerConfigurationException { - - final Transformer transformer = TransformerFactoryUtils.newInstance().newTransformer(); - final Object requestObject = new Object(); - - Marshaller marshaller = new Marshaller() { - - @Override - public void marshal(Object graph, Result result) throws XmlMappingException { - - assertThat(requestObject).isEqualTo(graph); - try { - transformer.transform(new StringSource(messagePayload), result); - } catch (TransformerException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean supports(Class clazz) { - assertThat(clazz).isEqualTo(Object.class); - return true; - } - }; - - final Object responseObject = new Object(); - - Unmarshaller unmarshaller = new Unmarshaller() { - - @Override - public Object unmarshal(Source source) throws XmlMappingException { - return responseObject; - } - - @Override - public boolean supports(Class clazz) { - - assertThat(clazz).isEqualTo(Object.class); - return true; - } - }; - - template.setMarshaller(marshaller); - template.setUnmarshaller(unmarshaller); - Object result = template.marshalSendAndReceive(baseUrl + "/soap/echo", requestObject); - - assertThat(result).isEqualTo(responseObject); + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/noResponse") + .hasContextualNameEqualTo("POST") + ); } + @Test public void marshalSendAndReceiveNoResponse() throws TransformerConfigurationException { @@ -251,6 +215,17 @@ public boolean supports(Class clazz) { Object result = template.marshalSendAndReceive(baseUrl + "/soap/noResponse", requestObject); assertThat(result).isNull(); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/noResponse") + .hasContextualNameEqualTo("POST") + ); } @Test @@ -267,7 +242,8 @@ public void notFound() { .hasLowCardinalityKeyValue("host", "localhost") .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "root") - .hasContextualNameEqualTo("POST /errors/notfound") + .hasHighCardinalityKeyValue("path", "/errors/notfound") + .hasContextualNameEqualTo("POST") ); } @@ -279,6 +255,17 @@ public void receiverFault() { assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template .sendSourceAndReceiveToResult(baseUrl + "/soap/receiverFault", new StringSource(messagePayload), result)); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "SoapFaultClientException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/receiverFault") + .hasContextualNameEqualTo("POST") + ); } @Test @@ -288,6 +275,17 @@ public void senderFault() { assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template .sendSourceAndReceiveToResult(baseUrl + "/soap/senderFault", new StringSource(messagePayload), result)); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "SoapFaultClientException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/senderFault") + .hasContextualNameEqualTo("POST") + ); } @Test @@ -300,6 +298,17 @@ public void attachment() { soapMessage.addAttachment("attachment-1", new DataHandler(new ByteArrayDataSource(attachmentContent, "text/plain"))); }, new StringResult()); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/attachment") + .hasContextualNameEqualTo("POST") + ); } /** diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java index 7bfa93e13..ecad87ede 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -67,8 +67,6 @@ public static void startServer() throws Exception { MessageDispatcherServlet dispatcherServlet = new MessageDispatcherServlet(applicationContext); dispatcherServlet.setTransformWsdlLocations(true); - - ServletHolder servletHolder = new ServletHolder(dispatcherServlet); context.addServlet(servletHolder, "/ws/*"); @@ -77,14 +75,14 @@ public static void startServer() throws Exception { } @AfterAll - public static void stopServer() throws Exception { + static void tearDown() throws Exception { + applicationContext.close(); server.stop(); } @BeforeEach void setUp() throws TransformerConfigurationException { - webServiceTemplate = applicationContext.getBean(WebServiceTemplate.class); registry = applicationContext.getBean(TestObservationRegistry.class); @@ -113,8 +111,27 @@ void testObservationInterceptorBehavior() { ); } - @AfterEach - void tearDown() { - applicationContext.close(); + @Test + void testPathWithVariable() { + + MyEndpoint.MyRequest request = new MyEndpoint.MyRequest(); + request.setName("John"); + MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl + "/1234", request); + + // Assertions based on expected behavior of ObservationInterceptor + assertNotNull(response); + + assertThat(registry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "request") + .hasLowCardinalityKeyValue("soapaction", "none") + .hasLowCardinalityKeyValue("path", "/ws/{pathInfo}") + .hasContextualNameEqualTo("POST /ws/{pathInfo}") + .hasHighCardinalityKeyValue("pathinfo", "/1234") + .hasNameEqualTo("webservice.server") + ); } } \ No newline at end of file diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java index b93f20114..9df61c38d 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java @@ -48,10 +48,7 @@ public Jaxb2Marshaller marshaller() { @Bean public ObservationRegistry observationRegistry() { - TestObservationRegistry registry = TestObservationRegistry.create(); - - registry.observationConfig(); - return registry; + return TestObservationRegistry.create(); } @Bean @@ -64,10 +61,9 @@ public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller marshaller) { @Bean public EndpointInterceptor observationInterceptor() { - return new ObservationInterceptor(observationRegistry); // Replace with your actual interceptor + return new ObservationInterceptor(observationRegistry, null); // Replace with your actual interceptor } - @Override public void addInterceptors(List interceptors) { interceptors.add(observationInterceptor()); From 8de1a8f372d8810c650ea89cecaac633470164f4 Mon Sep 17 00:00:00 2001 From: johkin Date: Fri, 15 Nov 2024 12:31:54 +0100 Subject: [PATCH 10/15] Replaced Assert with WarnThenDebugLogger #1094 --- .../observation/WebServiceObservationInterceptor.java | 9 ++++++--- .../WebServiceTemplateObservationContext.java | 4 ++-- .../endpoint/observation/ObservationInterceptor.java | 6 ++++++ .../endpoint/observation/WebServiceEndpointContext.java | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index 5d796390f..bcbeaea7f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -15,9 +15,9 @@ */ 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.util.Assert; import org.springframework.ws.FaultAwareWebServiceMessage; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.client.WebServiceClientException; @@ -52,7 +52,7 @@ */ 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(); @@ -99,7 +99,10 @@ public boolean handleRequest(MessageContext messageContext) throws WebServiceCli public void afterCompletion(MessageContext messageContext, Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); - Assert.notNull(observation, "Expected observation in messageContext"); + if (observation == null) { + WARN_THEN_DEBUG_LOGGER.log("Expected observation in messageContext, cancelling observation."); + return; + } WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext(); diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java index 6a913c4f8..d58e4f2f3 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -28,7 +28,7 @@ */ public class WebServiceTemplateObservationContext extends RequestReplySenderContext { - private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(WebServiceTemplateObservationContext.class); + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceTemplateObservationContext.class); public static final String UNKNOWN = "unknown"; private String outcome = UNKNOWN; @@ -45,7 +45,7 @@ public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnecti try { carrier.addRequestHeader(key, value); } catch (IOException e) { - logger.log("Could not add key to carrier", e); + WARN_THEN_DEBUG_LOGGER.log("Could not add key to carrier", e); } } }); diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index 63fcfa501..01ce3d77f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -15,6 +15,7 @@ */ package org.springframework.ws.server.endpoint.observation; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; @@ -51,6 +52,7 @@ */ public class ObservationInterceptor extends EndpointInterceptorAdapter { + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); private static final String OBSERVATION_KEY = "observation"; private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); @@ -106,6 +108,10 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); + if (observation == null) { + WARN_THEN_DEBUG_LOGGER.log("Expected observation in messageContext, cancelling observation."); + return; + } Assert.notNull(observation, "Expected observation in messageContext"); WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java index 5c0ecf944..c4d9d3d0f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -29,7 +29,7 @@ */ public class WebServiceEndpointContext extends RequestReplyReceiverContext { - private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(WebServiceEndpointContext.class); + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceEndpointContext.class); private static final String UNKNOWN = "unknown"; private String outcome = UNKNOWN; @@ -47,7 +47,7 @@ public WebServiceEndpointContext(HeadersAwareReceiverWebServiceConnection connec return headers.next(); } } catch (IOException e) { - logger.log("Could not read key from carrier", e); + WARN_THEN_DEBUG_LOGGER.log("Could not read key from carrier", e); } return null; }); From a2910e102539dcc0586cf1f94bc4ad7d53ab0eb0 Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 18 Nov 2024 10:06:43 +0100 Subject: [PATCH 11/15] Modified log-message #1094 --- .../core/observation/WebServiceObservationInterceptor.java | 2 +- .../ws/server/endpoint/observation/ObservationInterceptor.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index bcbeaea7f..e7f56547c 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -100,7 +100,7 @@ public void afterCompletion(MessageContext messageContext, Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); if (observation == null) { - WARN_THEN_DEBUG_LOGGER.log("Expected observation in messageContext, cancelling observation."); + WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed."); return; } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index 01ce3d77f..c9a3bc36f 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -109,10 +109,9 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); if (observation == null) { - WARN_THEN_DEBUG_LOGGER.log("Expected observation in messageContext, cancelling observation."); + WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed."); return; } - Assert.notNull(observation, "Expected observation in messageContext"); WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); From dbdec8a3403190780f3416c7aa2386ebe7b9daa6 Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 18 Nov 2024 10:08:57 +0100 Subject: [PATCH 12/15] Added nullability annotations #1094 --- .../WebServiceObservationInterceptor.java | 6 ++++++ .../endpoint/observation/ObservationInterceptor.java | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index e7f56547c..8eac839c9 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -18,6 +18,10 @@ import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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; @@ -63,7 +67,9 @@ public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { private final WebServiceTemplateConvention customConvention; public WebServiceObservationInterceptor( + @NonNull ObservationRegistry observationRegistry, + @Nullable WebServiceTemplateConvention customConvention) { this.observationRegistry = observationRegistry; this.customConvention = customConvention; diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index c9a3bc36f..408d26c34 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -19,7 +19,10 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; -import org.springframework.util.Assert; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.core.observation.RootElementSAXHandler; @@ -62,8 +65,11 @@ public class ObservationInterceptor extends EndpointInterceptorAdapter { private final SAXParserFactory parserFactory; private final SAXParser saxParser; - - public ObservationInterceptor(ObservationRegistry observationRegistry, WebServiceEndpointConvention customConvention) { + public ObservationInterceptor( + @NonNull + ObservationRegistry observationRegistry, + @Nullable + WebServiceEndpointConvention customConvention) { this.observationRegistry = observationRegistry; this.customConvention = customConvention; From f486fdc8c3414c83020e76090fe719887ea03975 Mon Sep 17 00:00:00 2001 From: johkin Date: Mon, 18 Nov 2024 10:09:42 +0100 Subject: [PATCH 13/15] Improved exeptionhandling and logging #1094 --- .../WebServiceObservationInterceptor.java | 51 +++++++++----- .../observation/ObservationInterceptor.java | 69 ++++++++----------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index 8eac839c9..ee94b6953 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -44,6 +44,7 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -56,12 +57,14 @@ */ public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { + private final Log logger = LogFactory.getLog(getClass()); + 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 static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private final ObservationRegistry observationRegistry; - private final SAXParserFactory parserFactory; private final SAXParser saxParser; private final WebServiceTemplateConvention customConvention; @@ -71,14 +74,18 @@ public WebServiceObservationInterceptor( ObservationRegistry observationRegistry, @Nullable WebServiceTemplateConvention customConvention) { + this.observationRegistry = observationRegistry; this.customConvention = customConvention; - parserFactory = SAXParserFactory.newNSInstance(); + + SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); + SAXParser parser = null; try { - saxParser = parserFactory.newSAXParser(); + parser = parserFactory.newSAXParser(); } catch (ParserConfigurationException | SAXException e) { - throw new RuntimeException(e); + logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); } + saxParser = parser; } @@ -89,7 +96,6 @@ public boolean handleRequest(MessageContext messageContext) throws WebServiceCli HeadersAwareSenderWebServiceConnection connection = (HeadersAwareSenderWebServiceConnection) transportContext.getConnection(); - Observation observation = WebServiceTemplateObservationDocumentation.WEB_SERVICE_TEMPLATE.start( customConvention, DEFAULT_CONVENTION, @@ -128,10 +134,15 @@ public void afterCompletion(MessageContext messageContext, Exception ex) { } } + if (ex == null) { + context.setOutcome("success"); + } else { + context.setError(ex); + context.setOutcome("fault"); + } + if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { - if (!faultAwareResponse.hasFault() && ex == null) { - context.setOutcome("success"); - } else { + if (faultAwareResponse.hasFault()) { context.setOutcome("fault"); } } @@ -144,12 +155,10 @@ public void afterCompletion(MessageContext messageContext, Exception ex) { context.setContextualName("POST"); - context.setError(ex); - observation.stop(); } - URI getUriFromConnection() { + URI getUriFromConnection() { TransportContext transportContext = TransportContextHolder.getTransportContext(); WebServiceConnection connection = transportContext.getConnection(); try { @@ -165,24 +174,34 @@ QName getRootElement(Source source) { return new QName(root.getNamespaceURI(), root.getLocalName()); } if (source instanceof StreamSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } RootElementSAXHandler handler = new RootElementSAXHandler(); try { saxParser.parse(((StreamSource) source).getInputStream(), handler); return handler.getRootElementName(); - } catch (Exception e) { - return new QName("unknown", "unknow"); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; } } if (source instanceof SAXSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } RootElementSAXHandler handler = new RootElementSAXHandler(); try { saxParser.parse(((SAXSource) source).getInputSource(), handler); return handler.getRootElementName(); - } catch (Exception e) { - return new QName("unknown", "unknow"); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; } } - return new QName("unknown", "unknow"); + return UNKNOWN_Q_NAME; } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index 408d26c34..dc41fbcd2 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -31,7 +31,6 @@ import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; 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 org.springframework.ws.transport.http.HttpServletConnection; @@ -46,8 +45,7 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; -import java.net.URI; -import java.net.URISyntaxException; +import java.io.IOException; /** * Interceptor implementation that creates an observation for a WebService Endpoint. @@ -55,6 +53,8 @@ */ public class ObservationInterceptor extends EndpointInterceptorAdapter { + private final Log logger = LogFactory.getLog(getClass()); + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); private static final String OBSERVATION_KEY = "observation"; private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); @@ -62,7 +62,6 @@ public class ObservationInterceptor extends EndpointInterceptorAdapter { private final ObservationRegistry observationRegistry; private final WebServiceEndpointConvention customConvention; - private final SAXParserFactory parserFactory; private final SAXParser saxParser; public ObservationInterceptor( @@ -73,12 +72,14 @@ public ObservationInterceptor( this.observationRegistry = observationRegistry; this.customConvention = customConvention; - parserFactory = SAXParserFactory.newNSInstance(); + SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); + SAXParser parser = null; try { - saxParser = parserFactory.newSAXParser(); + parser = parserFactory.newSAXParser(); } catch (ParserConfigurationException | SAXException e) { - throw new RuntimeException(e); + logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); } + saxParser = parser; } @Override @@ -88,17 +89,6 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr HeadersAwareReceiverWebServiceConnection connection = (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); - if (connection instanceof HttpServletConnection) { - HttpServletConnection servletConnection = (HttpServletConnection) connection; - String servletPath = servletConnection.getHttpServletRequest().getServletPath(); - String pathInfo = servletConnection.getHttpServletRequest().getPathInfo(); - - - if (servletPath != null) { - - } - } - Observation observation = EndpointObservationDocumentation.WEB_SERVICE_ENDPOINT.start( customConvention, DEFAULT_CONVENTION, @@ -111,7 +101,7 @@ public boolean handleRequest(MessageContext messageContext, Object endpoint) thr } @Override - public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) { + public void afterCompletion(MessageContext messageContext, Object endpoint, @Nullable Exception ex) { Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); if (observation == null) { @@ -121,8 +111,6 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); - context.setError(ex); - WebServiceMessage request = messageContext.getRequest(); WebServiceMessage response = messageContext.getResponse(); @@ -140,13 +128,17 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce } else { context.setSoapAction("none"); } + } + if (ex == null) { + context.setOutcome("success"); + } else { + context.setError(ex); + context.setOutcome("fault"); } - if (response instanceof FaultAwareWebServiceMessage) { - if (!((FaultAwareWebServiceMessage) response).hasFault() && ex == null) { - context.setOutcome("success"); - } else { + if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { + if (faultAwareResponse.hasFault()) { context.setOutcome("fault"); } } @@ -155,8 +147,7 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce HeadersAwareReceiverWebServiceConnection connection = (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); - if (connection instanceof HttpServletConnection) { - HttpServletConnection servletConnection = (HttpServletConnection) connection; + if (connection instanceof HttpServletConnection servletConnection) { HttpServletRequest servletRequest = servletConnection.getHttpServletRequest(); String servletPath = servletRequest.getServletPath(); String pathInfo = servletRequest.getPathInfo(); @@ -175,36 +166,36 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, Exce observation.stop(); } - URI getUriFromConnection() { - TransportContext transportContext = TransportContextHolder.getTransportContext(); - WebServiceConnection connection = transportContext.getConnection(); - try { - return connection.getUri(); - } catch (URISyntaxException e) { - return null; - } - } - QName getRootElement(Source source) { if (source instanceof DOMSource) { Node root = ((DOMSource) source).getNode(); return new QName(root.getNamespaceURI(), root.getLocalName()); } if (source instanceof StreamSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } RootElementSAXHandler handler = new RootElementSAXHandler(); try { saxParser.parse(((StreamSource) source).getInputStream(), handler); return handler.getRootElementName(); - } catch (Exception e) { + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); return UNKNOWN_Q_NAME; } } if (source instanceof SAXSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } RootElementSAXHandler handler = new RootElementSAXHandler(); try { saxParser.parse(((SAXSource) source).getInputSource(), handler); return handler.getRootElementName(); - } catch (Exception e) { + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); return UNKNOWN_Q_NAME; } } From 4cd42549e566e445777ef50cb48fea2fbe9c8b34 Mon Sep 17 00:00:00 2001 From: johkin Date: Tue, 19 Nov 2024 16:30:33 +0100 Subject: [PATCH 14/15] Extract common logic to ObservationHelper #1094 --- .../observation/RootElementSAXHandler.java | 40 ----- .../WebServiceObservationInterceptor.java | 68 +-------- .../observation/ObservationInterceptor.java | 69 +-------- .../ws/support/ObservationHelper.java | 144 ++++++++++++++++++ ...iceTemplateObservationIntegrationTest.java | 5 +- ...ObservationInterceptorIntegrationTest.java | 1 + .../observation/WebServiceConfig.java | 7 +- .../ws/support/ObservationHelperTest.java | 67 ++++++++ 8 files changed, 235 insertions(+), 166 deletions(-) delete mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java deleted file mode 100644 index 5dac81392..000000000 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/RootElementSAXHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 org.xml.sax.Attributes; -import org.xml.sax.helpers.DefaultHandler; - -import javax.xml.namespace.QName; -/** - * DefaultHandler that extracts the root elements namespace and name. - * @author Johan Kindgren - */ -public class RootElementSAXHandler extends DefaultHandler { - - private QName rootElementName = null; - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) { - if (rootElementName == null) { - rootElementName = new QName(uri, localName); - } - } - - public QName getRootElementName() { - return rootElementName; - } -} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java index ee94b6953..f545c44d2 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -18,8 +18,6 @@ import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.ws.FaultAwareWebServiceMessage; @@ -28,23 +26,15 @@ 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 org.w3c.dom.Node; -import org.xml.sax.SAXException; import javax.xml.namespace.QName; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Source; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -57,35 +47,26 @@ */ public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { - private final Log logger = LogFactory.getLog(getClass()); - 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 static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private final ObservationRegistry observationRegistry; - private final SAXParser saxParser; 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; - - SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); - SAXParser parser = null; - try { - parser = parserFactory.newSAXParser(); - } catch (ParserConfigurationException | SAXException e) { - logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); - } - saxParser = parser; } @@ -124,7 +105,7 @@ public void afterCompletion(MessageContext messageContext, Exception ex) { if (request instanceof SoapMessage soapMessage) { Source source = soapMessage.getSoapBody().getPayloadSource(); - QName root = getRootElement(source); + QName root = observationHelper.getRootElement(source); if (root != null) { context.setLocalPart(root.getLocalPart()); context.setNamespace(root.getNamespaceURI()); @@ -167,42 +148,5 @@ URI getUriFromConnection() { return null; } } - - QName getRootElement(Source source) { - if (source instanceof DOMSource) { - Node root = ((DOMSource) source).getNode(); - return new QName(root.getNamespaceURI(), root.getLocalName()); - } - if (source instanceof StreamSource) { - if (saxParser == null) { - WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); - return UNKNOWN_Q_NAME; - } - RootElementSAXHandler handler = new RootElementSAXHandler(); - try { - saxParser.parse(((StreamSource) source).getInputStream(), handler); - return handler.getRootElementName(); - } catch (SAXException | IOException e) { - WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); - return UNKNOWN_Q_NAME; - } - } - if (source instanceof SAXSource) { - if (saxParser == null) { - WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); - return UNKNOWN_Q_NAME; - } - RootElementSAXHandler handler = new RootElementSAXHandler(); - try { - saxParser.parse(((SAXSource) source).getInputSource(), handler); - return handler.getRootElementName(); - } catch (SAXException | IOException e) { - WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); - return UNKNOWN_Q_NAME; - } - } - return UNKNOWN_Q_NAME; - } - } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java index dc41fbcd2..7f11f0974 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -19,33 +19,22 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; 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.core.observation.RootElementSAXHandler; import org.springframework.ws.context.MessageContext; import org.springframework.ws.server.endpoint.interceptor.EndpointInterceptorAdapter; import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.support.ObservationHelper; import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; import org.springframework.ws.transport.TransportConstants; import org.springframework.ws.transport.context.TransportContext; import org.springframework.ws.transport.context.TransportContextHolder; import org.springframework.ws.transport.http.HttpServletConnection; -import org.w3c.dom.Node; -import org.xml.sax.SAXException; import javax.xml.namespace.QName; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Source; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; -import java.io.IOException; /** * Interceptor implementation that creates an observation for a WebService Endpoint. @@ -53,33 +42,24 @@ */ public class ObservationInterceptor extends EndpointInterceptorAdapter { - private final Log logger = LogFactory.getLog(getClass()); - private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); private static final String OBSERVATION_KEY = "observation"; - private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); private final ObservationRegistry observationRegistry; + private final ObservationHelper observationHelper; private final WebServiceEndpointConvention customConvention; - private final SAXParser saxParser; public ObservationInterceptor( @NonNull ObservationRegistry observationRegistry, + @NonNull + ObservationHelper observationHelper, @Nullable WebServiceEndpointConvention customConvention) { this.observationRegistry = observationRegistry; + this.observationHelper = observationHelper; this.customConvention = customConvention; - - SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); - SAXParser parser = null; - try { - parser = parserFactory.newSAXParser(); - } catch (ParserConfigurationException | SAXException e) { - logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); - } - saxParser = parser; } @Override @@ -117,7 +97,7 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, @Nul if (request instanceof SoapMessage soapMessage) { Source source = soapMessage.getSoapBody().getPayloadSource(); - QName root = getRootElement(source); + QName root = observationHelper.getRootElement(source); if (root != null) { context.setLocalPart(root.getLocalPart()); context.setNamespace(root.getNamespaceURI()); @@ -157,6 +137,7 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, @Nul context.setPath(servletPath + "/{pathInfo}"); context.setPathInfo(pathInfo); } else { + context.setPath(servletPath); context.setContextualName("POST " + servletPath); } } else { @@ -165,40 +146,4 @@ public void afterCompletion(MessageContext messageContext, Object endpoint, @Nul observation.stop(); } - - QName getRootElement(Source source) { - if (source instanceof DOMSource) { - Node root = ((DOMSource) source).getNode(); - return new QName(root.getNamespaceURI(), root.getLocalName()); - } - if (source instanceof StreamSource) { - if (saxParser == null) { - WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); - return UNKNOWN_Q_NAME; - } - RootElementSAXHandler handler = new RootElementSAXHandler(); - try { - saxParser.parse(((StreamSource) source).getInputStream(), handler); - return handler.getRootElementName(); - } catch (SAXException | IOException e) { - WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); - return UNKNOWN_Q_NAME; - } - } - if (source instanceof SAXSource) { - if (saxParser == null) { - WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); - return UNKNOWN_Q_NAME; - } - RootElementSAXHandler handler = new RootElementSAXHandler(); - try { - saxParser.parse(((SAXSource) source).getInputSource(), handler); - return handler.getRootElementName(); - } catch (SAXException | IOException e) { - WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); - return UNKNOWN_Q_NAME; - } - } - return UNKNOWN_Q_NAME; - } } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java new file mode 100644 index 000000000..5ae4f37f9 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java @@ -0,0 +1,144 @@ +/* + * 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.support; + +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ws.server.endpoint.observation.ObservationInterceptor; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.namespace.QName; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; +import java.io.IOException; + +/** + * Helper class for observation tasks. + * @author Johan Kindgren + */ +public class ObservationHelper { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); + private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); + + private final SAXParser saxParser; + + public ObservationHelper() { + SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); + SAXParser parser = null; + try { + parser = parserFactory.newSAXParser(); + } catch (ParserConfigurationException | SAXException e) { + logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); + } + saxParser = parser; + } + + + /** + * Try to find the root element QName for the given source. + * If it isn't possible to extract the QName, a QName with the values 'unknown:unknown' is returned. + */ + public QName getRootElement(Source source) { + if (source instanceof DOMSource) { + Node document = ((DOMSource) source).getNode(); + if (document.getNodeType() == Node.DOCUMENT_NODE) { + Document doc = (Document) document; + Node root = doc.getDocumentElement(); + if (root != null) { + return new QName(root.getNamespaceURI(), root.getLocalName()); + } + } + return UNKNOWN_Q_NAME; + } + if (source instanceof StreamSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(getInputSource((StreamSource) source), handler); + return handler.getRootElementName(); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; + } + } + if (source instanceof SAXSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(getInputSource((SAXSource) source), handler); + return handler.getRootElementName(); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; + } + } + return UNKNOWN_Q_NAME; + } + + InputSource getInputSource(StreamSource source) { + + if (source.getInputStream() != null) { + return new InputSource(source.getInputStream()); + } + return new InputSource(source.getReader()); + } + + InputSource getInputSource(SAXSource source) { + return source.getInputSource(); + } + + + + /** + * DefaultHandler that extracts the root elements namespace and name. + * @author Johan Kindgren + */ + static class RootElementSAXHandler extends DefaultHandler { + + private QName rootElementName = null; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if (rootElementName == null) { + rootElementName = new QName(uri, localName); + } + } + + public QName getRootElementName() { + return rootElementName; + } + } +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java index 81cf82a73..3ca5e06ff 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -46,6 +46,7 @@ import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.soap.client.SoapFaultClientException; import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; +import org.springframework.ws.support.ObservationHelper; import org.springframework.ws.transport.http.HttpComponentsMessageSender; import org.springframework.ws.transport.support.FreePortScanner; import org.springframework.xml.transform.StringResult; @@ -72,6 +73,7 @@ public class WebServiceTemplateObservationIntegrationTest { private TestObservationRegistry observationRegistry; + private ObservationHelper observationHelper; private static Server jettyServer; @@ -134,11 +136,12 @@ public void removeXmlDataContentHandler() throws SOAPException { @BeforeEach public void createWebServiceTemplate() throws Exception { observationRegistry = TestObservationRegistry.create(); + observationHelper = new ObservationHelper(); template = new WebServiceTemplate(new SaajSoapMessageFactory(MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL))); template.setMessageSender(new HttpComponentsMessageSender()); template.setInterceptors(new ClientInterceptor[]{ - new WebServiceObservationInterceptor(observationRegistry, null) + new WebServiceObservationInterceptor(observationRegistry, observationHelper, null) }); } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java index ecad87ede..47aa838c3 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -106,6 +106,7 @@ void testObservationInterceptorBehavior() { .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") .hasLowCardinalityKeyValue("localpart", "request") .hasLowCardinalityKeyValue("soapaction", "none") + .hasLowCardinalityKeyValue("path", "/ws") .hasContextualNameEqualTo("POST /ws") .hasNameEqualTo("webservice.server") ); diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java index 9df61c38d..38026f7fa 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java @@ -25,6 +25,7 @@ import org.springframework.ws.config.annotation.EnableWs; import org.springframework.ws.config.annotation.WsConfigurerAdapter; import org.springframework.ws.server.EndpointInterceptor; +import org.springframework.ws.support.ObservationHelper; import java.util.List; @@ -61,7 +62,11 @@ public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller marshaller) { @Bean public EndpointInterceptor observationInterceptor() { - return new ObservationInterceptor(observationRegistry, null); // Replace with your actual interceptor + return new ObservationInterceptor(observationRegistry, observationHelper(),null); // Replace with your actual interceptor + } + @Bean + public ObservationHelper observationHelper() { + return new ObservationHelper(); } @Override diff --git a/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java new file mode 100644 index 000000000..508c7fa4f --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java @@ -0,0 +1,67 @@ +package org.springframework.ws.support; + +import org.dom4j.Namespace; +import org.dom4j.dom.DOMDocument; +import org.dom4j.dom.DOMElement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.xml.transform.StringSource; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xmlunit.builder.Input; + +import javax.xml.namespace.QName; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.sax.SAXSource; +import java.io.StringReader; + +import static org.assertj.core.api.Assertions.assertThat; + +class ObservationHelperTest { + + private ObservationHelper helper; + + @BeforeEach + void setUp() { + helper = new ObservationHelper(); + } + + @Test + void getRootElementStreamSource() { + + StringSource source = new StringSource(""); + + QName name = helper.getRootElement(source); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } + + @Test + void getRootElementDomSource() { + + DOMDocument document = new DOMDocument( + new DOMElement( + new org.dom4j.QName("root", + new Namespace(null, "http://springframework.org/spring-ws")))); + document.getRootElement().addElement("child"); + + QName name = helper.getRootElement(Input.from(document).build()); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } + + @Test + void getRootElementSaxSource() throws Exception { + StringReader reader = new StringReader(""); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + XMLReader xmlReader = saxParser.getXMLReader(); + + SAXSource saxSource = new SAXSource(xmlReader, new InputSource(reader)); + QName name = helper.getRootElement(saxSource); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } +} \ No newline at end of file From 7163362cfe71d3a19562a2da5634308babf9a8ed Mon Sep 17 00:00:00 2001 From: johkin Date: Wed, 27 Nov 2024 09:43:08 +0100 Subject: [PATCH 15/15] Fix for DomSource-handling #1094 --- .../ws/support/ObservationHelper.java | 11 +++-------- .../ws/support/ObservationHelperTest.java | 12 +++++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java index 5ae4f37f9..39f77716e 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java @@ -19,7 +19,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.ws.server.endpoint.observation.ObservationInterceptor; -import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.InputSource; @@ -67,13 +66,9 @@ public ObservationHelper() { */ public QName getRootElement(Source source) { if (source instanceof DOMSource) { - Node document = ((DOMSource) source).getNode(); - if (document.getNodeType() == Node.DOCUMENT_NODE) { - Document doc = (Document) document; - Node root = doc.getDocumentElement(); - if (root != null) { - return new QName(root.getNamespaceURI(), root.getLocalName()); - } + Node payload = ((DOMSource) source).getNode(); + if (payload.getNodeType() == Node.ELEMENT_NODE) { + return new QName(payload.getNamespaceURI(), payload.getLocalName()); } return UNKNOWN_Q_NAME; } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java index 508c7fa4f..0a1b756e4 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java @@ -1,7 +1,6 @@ package org.springframework.ws.support; import org.dom4j.Namespace; -import org.dom4j.dom.DOMDocument; import org.dom4j.dom.DOMElement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,13 +39,12 @@ void getRootElementStreamSource() { @Test void getRootElementDomSource() { - DOMDocument document = new DOMDocument( - new DOMElement( - new org.dom4j.QName("root", - new Namespace(null, "http://springframework.org/spring-ws")))); - document.getRootElement().addElement("child"); + DOMElement payloadElement = new DOMElement( + new org.dom4j.QName("root", + new Namespace(null, "http://springframework.org/spring-ws"))); + payloadElement.addElement("child"); - QName name = helper.getRootElement(Input.from(document).build()); + QName name = helper.getRootElement(Input.from(payloadElement).build()); assertThat(name.getLocalPart()).isEqualTo("root"); assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); }