Выборочное логирование трафика для сервисов SOAP
Практически в любом нашем проекте, который интегрируется с сервисами заказчика, используются SOAP-сервисы. И в каждом таком проекте есть задача логирования информации. Мы особенно заинтересованы в логировании запросов, которые связаны с бизнес-операциями решения. Например, сотрудник ввел неправильные данные и сервис вернул ошибку. Мы хотим знать детали этой ошибки и как можно быстрее её поправить или вынести на обсуждение с заказчиком.
Мы хотели бы видеть запросы нашего клиента и ответы сервиса в неизменном виде, поскольку это помогает разбирать возникающие проблемы.
В этой статье расскажем, как мы настроили выборочное логирование трафика для сервисов SOAP.

Проблема
Начнём с того, что Spring имеет встроенное логирование запросов и ответов, которое включается через конфигурацию
logging.level.org.springframework.ws.client.MessageTracing.sent=TRACE logging.level.org.springframework.ws.client.MessageTracing.received=TRACE
Проблема в том, что эти настройки включают логирование всего SOAP трафика. Нам же нужны только несколько методов и то не полностью. Например, мы не хотим видеть запросы по загрузке файлов в логах, потому что это большой объем данных.
В Spring Framework стандартом де-факто для построения soap клиента является использование WebServiceGatewaySupport, в котором можно также добавить обработку запросов и ответов через ClientInterceptor. Но в его методах сложно понять, какой метод нашего клиента инициировал вызов. И непонятно, надо ли логировать только запрос, ответ или всё сразу.
Решение
Мы будем использовать стандартный ClientInterceptor, но снабдим его недостающей информацией о сигнатуре метода и флажками "input", "output", по которым можно понять, нужно ли логировать запрос и ответ.
Оговоримся сразу, что такое решение не будет работать с «потоковой» сериализацией/десериализацией. Но мы используем Axiom, у которого по умолчанию включена опция “payload caching” и такое решение работает.
Структура для хранения информации:
import lombok.Data; @Data public class SoapLoggingInfo { private String method; private boolean input = false; private boolean output = false; }
Контекст для хранения информации:
public class SoapLoggingContext { private static final ThreadLocalholder = new ThreadLocal<>(); public static void set(SoapLoggingInfo value) { holder.set(value); } public static SoapLoggingInfo get() { return holder.get(); } }
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Component public @interface SoapLoggable { boolean value() default true; boolean input() default true; boolean output() default true; }
@Aspect @Component @Slf4j public class SoapLoggingAspect { @Value("${logging.soap.request.enabled:false}") private Boolean enabled; @Pointcut("execution(@ru.trueengineering.agentapi.logging.SoapLoggable * *(..))") public void soapLoggableMethod() {} @Before("soapLoggableMethod()") public void beforeSoapLoggable(JoinPoint joinPoint) { if (!enabled) { return; } SoapLoggable soapRequestLogger = getAnnotation(joinPoint); if (soapRequestLogger.value()) { SoapLoggingInfo loggingInfo = new SoapLoggingInfo(); loggingInfo.setInput(soapRequestLogger.input()); loggingInfo.setOutput(soapRequestLogger.output()); final Class aClass = joinPoint.getTarget().getClass(); final Signature signature = joinPoint.getSignature(); final String name = signature.getName(); loggingInfo.setMethod(aClass.getSimpleName() + "." + name); SoapLoggingContext.set(loggingInfo); } } @After("soapLoggableMethod()") public void afterSoapLoggable(JoinPoint joinPoint) { SoapLoggingContext.set(null); } private SoapLoggable getAnnotation(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); return method.getAnnotation(SoapLoggable.class); } }
Разберём по частям
Срез представляет собой выражение, означающее "все методы, аннотированные SoapLogable". Мы используем возможности AspectJ:
@Pointcut("execution(@ru.trueengineering.agentapi.logging.SoapLoggable * *(..))") public void soapLoggableMethod() {}
Есть совет, вызываемый до метода, попавшего под срез:
@Before("soapLoggableMethod()") public void beforeSoapLoggable(JoinPoint joinPoint) {}
В этом методе мы берём аннотацию, извлекаем сигнатуру метода и метаинформацию из аннотации, формируем объект для ClientInterceptor и помещаем его в контекст.
Есть совет, вызываемый после вызова метода, попавшего под срез:
@After("soapLoggableMethod()") public void afterSoapLoggable(JoinPoint joinPoint) {}
Он просто чистит контекст. Собственно, логирование под катом:
@Component @Slf4j public class SoapLoggingInterceptor extends ClientInterceptorAdapter { @Override public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException { SoapLoggingInfo info = SoapLoggingContext.get(); if (info != null && info.isInput()) { ByteArrayOutputStream xml = new ByteArrayOutputStream(); try { messageContext.getRequest().writeTo(xml); log.debug("Метод: " + info.getMethod() + ", запрос:" + xml.toString(StandardCharsets.UTF_8)); } catch (IOException e) { log.error("Ошибка получения SOAP request", e); } } return true; } @Override public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException { return handleResponseOrFault(messageContext); } @Override public boolean handleFault(MessageContext messageContext) throws WebServiceClientException { return handleResponseOrFault(messageContext); } private boolean handleResponseOrFault(MessageContext messageContext) { SoapLoggingInfo info = SoapLoggingContext.get(); if (info != null && info.isOutput()) { ByteArrayOutputStream xml = new ByteArrayOutputStream(); try { messageContext.getResponse().writeTo(xml); log.debug("Метод: " + info.getMethod() + ", ответ:" + xml.toString(StandardCharsets.UTF_8)); } catch (IOException e) { log.error("Ошибка получения SOAP response", e); } } return true; } }
Используем стандартный способ «перехвата» и обработки SOAP запросов, но используем информацию из контекста, чтобы выборочно логировать только нужные нам методы.
Profit!
Пользоваться этим подходом очень просто.
Не желаем видеть запрос с приаттаченным файлом? Оk!
@SoapLoggable(input = false) public Optional> attachFile( final AttachFileRequest attachFileRequest) { return send(new WsAttachFileRequest(attachFileRequest)); }
Хотим видеть всё? Так ещё проще.
@SoapLoggable public Optional> calculate(final CalcRequest calcRequest) { }
Заключение
Оригинал опубликован на Habr.ru