├── .gitignore ├── src ├── main │ ├── resources │ │ └── META-INF │ │ │ ├── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ └── java │ │ └── com │ │ └── github │ │ └── throwable │ │ └── mdc4spring │ │ ├── loggers │ │ ├── DummyLoggerMDCAdapter.java │ │ ├── LoggerMDCAdapter.java │ │ ├── Slf4JLoggerMDCAdapter.java │ │ ├── Log4JLoggerMDCAdapter.java │ │ ├── Log4J2LoggerMDCAdapter.java │ │ └── LoggingSubsystemResolver.java │ │ ├── anno │ │ ├── MDCOutParams.java │ │ ├── MDCParams.java │ │ ├── MDCOutParam.java │ │ ├── WithMDC.java │ │ └── MDCParam.java │ │ ├── spring │ │ ├── MDCAutoConfiguration.java │ │ ├── MDCConfiguration.java │ │ ├── spel │ │ │ ├── SpelExpressionEvaluator.java │ │ │ └── PrivateFieldPropertyAccessor.java │ │ └── WithMDCAspect.java │ │ ├── util │ │ ├── ExpressionEvaluator.java │ │ ├── MethodInvocationMDCParametersValues.java │ │ └── AnnotatedMethodMDCParamsEvaluator.java │ │ ├── MDCInvocationBuilder.java │ │ ├── CloseableMDC.java │ │ └── MDC.java └── test │ ├── java │ └── com │ │ └── github │ │ └── throwable │ │ └── mdc4spring │ │ ├── spring │ │ ├── cmp │ │ │ ├── ExternalParameterBean.java │ │ │ ├── BeanMDCComponent.java │ │ │ ├── NestedMDCComponent.java │ │ │ └── SampleMDCComponent.java │ │ ├── SpringBootTestConfiguration.java │ │ └── TestAnnotatedMDCSpring.java │ │ ├── MapBasedLoggerMDCAdapter.java │ │ ├── InMemoryLoggingEventsAppender.java │ │ └── TestMDCCore.java │ └── resources │ └── logback-test.xml ├── CHANGELOG.md ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── TODO.txt ├── pom.xml ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /target/ -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.github.throwable.mdc4spring.spring.MDCAutoConfiguration -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.throwable.mdc4spring.spring.MDCConfiguration 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.1 2 | 3 | * Spring Boot 3 integration (see [issue](https://github.com/throwable/mdc4spring/issues/3)) 4 | * Tolerate NPEs during path evaluation. 5 | * `@MDCOutParam`: name is optional now. 6 | * README.md fixes. 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/DummyLoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | public class DummyLoggerMDCAdapter implements LoggerMDCAdapter { 4 | @Override 5 | public void put(String key, String value) { 6 | } 7 | 8 | @Override 9 | public void remove(String key) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/cmp/ExternalParameterBean.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.cmp; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | @Service("externalParameterBean") 6 | public class ExternalParameterBean { 7 | public String getExternalBeanValue() { 8 | return "Sample external bean value"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/anno/MDCOutParams.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.anno; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Repeatable wrapper for {@literal @}MDCParamOut 7 | * @see MDCOutParam 8 | */ 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ElementType.METHOD}) 12 | public @interface MDCOutParams { 13 | MDCOutParam[] value(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/anno/MDCParams.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.anno; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Repeatable wrapper for {@literal @}MDCParam 7 | * @see MDCParam 8 | */ 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ElementType.TYPE, ElementType.METHOD}) 12 | public @interface MDCParams { 13 | MDCParam[] value(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/spring/MDCAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring; 2 | 3 | import org.springframework.boot.autoconfigure.AutoConfiguration; 4 | import org.springframework.context.annotation.*; 5 | 6 | /** 7 | * MDC4Spring autoconfiguration for Spring Boot 3.x 8 | */ 9 | @AutoConfiguration 10 | @Import(MDCConfiguration.class) 11 | public class MDCAutoConfiguration { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/LoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | /** 4 | * Bridge with the underlying logging system MDC implementation 5 | */ 6 | public interface LoggerMDCAdapter { 7 | String MDC_ADAPTER_SYSTEM_PROPERTY = "com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter"; 8 | 9 | void put(String key, String value); 10 | void remove(String key); 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/SpringBootTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring; 2 | 3 | import org.springframework.boot.SpringBootConfiguration; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @SpringBootConfiguration 8 | @EnableAutoConfiguration 9 | @ComponentScan 10 | public class SpringBootTestConfiguration { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/Slf4JLoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | import org.slf4j.MDC; 4 | 5 | public class Slf4JLoggerMDCAdapter implements LoggerMDCAdapter { 6 | 7 | public Slf4JLoggerMDCAdapter() { 8 | org.slf4j.LoggerFactory.getLogger(LoggingSubsystemResolver.class) 9 | .debug("MDC4Spring is configured to use with Slf4J"); 10 | } 11 | 12 | @Override 13 | public void put(String key, String value) { 14 | MDC.put(key, value); 15 | } 16 | 17 | @Override 18 | public void remove(String key) { 19 | MDC.remove(key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/Log4JLoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | import org.apache.log4j.MDC; 4 | 5 | public class Log4JLoggerMDCAdapter implements LoggerMDCAdapter { 6 | 7 | public Log4JLoggerMDCAdapter() { 8 | org.apache.log4j.LogManager.getLogger(LoggingSubsystemResolver.class) 9 | .debug("MDC4Spring is configured to use with Log4J"); 10 | } 11 | 12 | @Override 13 | public void put(String key, String value) { 14 | MDC.put(key, value); 15 | } 16 | 17 | @Override 18 | public void remove(String key) { 19 | MDC.remove(key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/MapBasedLoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class MapBasedLoggerMDCAdapter implements LoggerMDCAdapter { 9 | private HashMap map = new HashMap<>(); 10 | 11 | @Override 12 | public void put(String key, String value) { 13 | map.put(key, value); 14 | } 15 | @Override 16 | public void remove(String key) { 17 | map.remove(key); 18 | } 19 | 20 | public Map getMap() { 21 | return map; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/Log4J2LoggerMDCAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | import org.apache.logging.log4j.ThreadContext; 4 | 5 | public class Log4J2LoggerMDCAdapter implements LoggerMDCAdapter { 6 | 7 | public Log4J2LoggerMDCAdapter() { 8 | org.apache.logging.log4j.LogManager.getLogger(LoggingSubsystemResolver.class) 9 | .debug("MDC4Spring is configured to use with Log4J2"); 10 | } 11 | 12 | @Override 13 | public void put(String key, String value) { 14 | ThreadContext.put(key, value); 15 | } 16 | 17 | @Override 18 | public void remove(String key) { 19 | ThreadContext.remove(key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/InMemoryLoggingEventsAppender.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.AppenderBase; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | 10 | public class InMemoryLoggingEventsAppender extends AppenderBase { 11 | @Override 12 | protected void append(Object event) { 13 | events.add((ILoggingEvent) event); 14 | } 15 | 16 | private final static List events = new ArrayList<>(); 17 | 18 | public static List getLoggingEvents() { 19 | return new ArrayList<>(events); 20 | } 21 | 22 | public static void clearLoggingEvents() { 23 | events.clear(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Library 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | workflow_dispatch: 9 | inputs: 10 | BRANCH: 11 | description: Branch/Tag to build 12 | required: false 13 | default: 'master' 14 | type: string 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | ref: ${{ inputs.BRANCH || github.ref }} 24 | 25 | - name: Set up JDK 17 26 | uses: actions/setup-java@v3 27 | with: 28 | java-version: '17' 29 | distribution: 'temurin' 30 | cache: maven 31 | gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 32 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 33 | 34 | - name: Build Library 35 | run: export GPG_TTY=$(tty) && mvn clean verify 36 | env: 37 | MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/spring/MDCConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring; 2 | 3 | import com.github.throwable.mdc4spring.spring.spel.SpelExpressionEvaluator; 4 | import com.github.throwable.mdc4spring.util.ExpressionEvaluator; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 10 | import org.springframework.core.env.Environment; 11 | 12 | /** 13 | * MDC4Spring configuration. 14 | * When using Spring Boot it is added to your ApplicationContext automatically. 15 | * If you are using Spring Framework you should import it manually. 16 | */ 17 | @Configuration 18 | @ComponentScan 19 | @EnableAspectJAutoProxy 20 | public class MDCConfiguration { 21 | @Bean 22 | ExpressionEvaluator spelExpressionEvaluator(Environment environment, ApplicationContext applicationContext) { 23 | return new SpelExpressionEvaluator(environment, applicationContext); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Publish Library 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | server-id: ossrh 21 | server-username: MAVEN_USERNAME 22 | server-password: MAVEN_PASSWORD 23 | gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 24 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 25 | cache: maven 26 | - name: Build & Publish with Maven 27 | run: export GPG_TTY=$(tty) && mvn --batch-mode clean deploy 28 | env: 29 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 30 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 31 | MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/util/ExpressionEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.util; 2 | 3 | import org.springframework.lang.Nullable; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Abstract expression evaluator 9 | */ 10 | @FunctionalInterface 11 | public interface ExpressionEvaluator { 12 | /** 13 | * When set to true (default value) the evaluation will tolerate NPEs and return null instead. 14 | */ 15 | String TOLERATE_NPE_SYSTEM_PROPERTY = "com.github.throwable.mdc4spring.util.ExpressionEvaluator"; 16 | 17 | /** 18 | * Evaluate expression for an MDC parameter 19 | * 20 | * @param expression expression string to evaluate 21 | * @param rootObject in case of annotated method argument a value of that argument, in case of annotated method 22 | * a local bean instance (unproxied) 23 | * @param argumentValues null when evaluating an annotated method argument or all argument values when evaluating annotated 24 | * method 25 | * @param expressionVariables additional variables available during expression evaluation 26 | * @return expression evaluation result 27 | */ 28 | Object evaluate(String expression, Object rootObject, @Nullable Map argumentValues, Map expressionVariables); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/anno/MDCOutParam.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.anno; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * Defines a parameter that will be added to current MDC after the method returns. 9 | * The parameter will be present in all log messages occurred after the method invocation. 10 | * @see WithMDC 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ElementType.METHOD}) 15 | @Repeatable(MDCOutParams.class) 16 | public @interface MDCOutParam { 17 | /** 18 | * Output parameter name. If omitted a method name is used. 19 | * @return Output parameter name. 20 | */ 21 | @AliasFor("value") 22 | String name() default ""; 23 | 24 | /** 25 | * Output parameter name. Alias for name attribute. 26 | * @return Output parameter name. 27 | */ 28 | @AliasFor("name") 29 | String value() default ""; 30 | 31 | /** 32 | * Expression to evaluate (optional). The expression is evaluated using method's return value as #root object. 33 | * If omitted a return value is returned as is. 34 | * For more information please refer to Spring Expression Language documentation. 35 | * @return Expression to evaluate. 36 | */ 37 | String eval() default ""; 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/cmp/BeanMDCComponent.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.cmp; 2 | 3 | import com.github.throwable.mdc4spring.anno.MDCParam; 4 | import com.github.throwable.mdc4spring.anno.WithMDC; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.Map; 11 | 12 | @Service 13 | @MDCParam(name = "environmentProperty", eval = "#environment['sample.property']") 14 | @MDCParam(name = "staticParam", eval = "'Static Value'") 15 | public class BeanMDCComponent { 16 | private final static Logger log = LoggerFactory.getLogger(NestedMDCComponent.class); 17 | 18 | 19 | public void execWithBeanMDCParams() { 20 | log.info("Bean MDC method"); 21 | } 22 | 23 | public void execWithBeanMDCAndArgumentsAsParams(@MDCParam String param1, String sample) { 24 | log.info("Bean MDC method with params"); 25 | } 26 | 27 | @MDCParam(name = "anotherProperty", eval = "'Fixed value'") 28 | public void execWithBeanParamsAndMethodParamsCombined(@MDCParam String param1, String sample) { 29 | log.info("Bean MDC with method MDC mix"); 30 | } 31 | 32 | @WithMDC(name = "nestedScope") 33 | @MDCParam(name = "anotherProperty", eval = "'Fixed value'") 34 | public void execWithBeanMDCAndNestedMethodMDCCombined(@MDCParam String param1, String sample) { 35 | log.info("Bean MDC with method MDC on different namespace"); 36 | } 37 | 38 | @WithMDC 39 | public void extractParameterValue(@MDCParam(name = "id", eval = "['id']") @Nullable Map map) { 40 | log.info("Parameter extracted"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %-4r [%t] %5p %c{1} - %m%n 5 | 6 | 7 | 8 | 9 | 11 | true 12 | 13 | yyyy-MM-dd' 'HH:mm:ss.SSS 14 | 15 | 16 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/util/MethodInvocationMDCParametersValues.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.util; 2 | 3 | import org.springframework.lang.Nullable; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Evaluated MDC parameters for method invocation 9 | */ 10 | public class MethodInvocationMDCParametersValues { 11 | @Nullable private final String beanMDCNamespace; 12 | private final Map beanMDCParamValues; 13 | @Nullable private final String methodMDCNamespace; 14 | private final Map methodMDCParamValues; 15 | private final boolean hasMDCParamOut; 16 | 17 | public MethodInvocationMDCParametersValues(@Nullable String beanMDCNamespace, 18 | Map beanMDCParamValues, 19 | @Nullable String methodMDCNamespace, 20 | Map methodMDCParamValues, 21 | boolean hasMDCParamOut) 22 | { 23 | this.beanMDCNamespace = beanMDCNamespace; 24 | this.beanMDCParamValues = beanMDCParamValues; 25 | this.methodMDCNamespace = methodMDCNamespace; 26 | this.methodMDCParamValues = methodMDCParamValues; 27 | this.hasMDCParamOut = hasMDCParamOut; 28 | } 29 | 30 | @Nullable 31 | public String getMethodMDCNamespace() { 32 | return methodMDCNamespace; 33 | } 34 | 35 | @Nullable 36 | public String getBeanMDCNamespace() { 37 | return beanMDCNamespace; 38 | } 39 | 40 | public Map getBeanMDCParamValues() { 41 | return beanMDCParamValues; 42 | } 43 | 44 | public Map getMethodMDCParamValues() { 45 | return methodMDCParamValues; 46 | } 47 | 48 | public boolean isHasMDCParamOut() { 49 | return hasMDCParamOut; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/cmp/NestedMDCComponent.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.cmp; 2 | 3 | import com.github.throwable.mdc4spring.MDC; 4 | import com.github.throwable.mdc4spring.anno.MDCParam; 5 | import com.github.throwable.mdc4spring.anno.MDCOutParam; 6 | import com.github.throwable.mdc4spring.anno.WithMDC; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | import static com.github.throwable.mdc4spring.MDC.current; 11 | 12 | @SuppressWarnings({"UnusedReturnValue", "unused"}) 13 | @Service 14 | public class NestedMDCComponent { 15 | private final static Logger log = LoggerFactory.getLogger(NestedMDCComponent.class); 16 | 17 | @WithMDC(name = "component2") 18 | public void execWithNewMDC() { 19 | current().put("nestedKey", "NestedKeyValue"); 20 | log.info("nested component"); 21 | } 22 | 23 | @WithMDC 24 | public void execWithSameNameMDC() { 25 | current().put("nestedKey", "NestedKeyValue"); 26 | log.info("nested component"); 27 | } 28 | 29 | public void execWithCurrentMDC(@MDCParam String param1) { 30 | MDC.param("param2", "value2"); 31 | log.info("Inside method call"); 32 | } 33 | 34 | @WithMDC 35 | void samplePackagePrivateMethod(@MDCParam String scope) { 36 | log.info("Package-private method"); 37 | } 38 | 39 | @WithMDC 40 | protected void sampleProtectedMethod(@MDCParam String scope) { 41 | log.info("Protected method"); 42 | } 43 | 44 | 45 | @MDCOutParam(name = "message1", eval = "'Hello, ' + #this") 46 | public String returnOutputParameterWithoutNestedMDC() { 47 | return "Pete"; 48 | } 49 | 50 | @WithMDC 51 | @MDCOutParam(name = "message2", eval = "'Hello, ' + #this") 52 | public String returnOutputParameterWithNestedMDC() { 53 | return "Mike"; 54 | } 55 | 56 | @MDCOutParam 57 | @MDCOutParam(eval = "#this + '-1'") 58 | @MDCOutParam(name = "named", eval = "#this + '-2'") 59 | public String returnUnnamedOutParams() { 60 | return "NoName"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/anno/WithMDC.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.anno; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * Creates a new MDC for method invocation. 9 | *

10 | * The annotation is applicable to container managed beans, and indicates that the annotated method must be executed inside new MDC. 11 | * All parameters defined with {@literal @}MDCParam annotation or set explicitly with MDC.param() 12 | * inside the method's body will belong to this MDC and will automatically be removed after the method returns. 13 | *

14 | * If any method annotated with ```WithMDC``` calls another method that has ```@WithMDC``` annotation too, 15 | * it will create a new 'nested' MDC that will be closed after the method returns removing only parameters defined inside it. 16 | * Any parameter defined in outer MDC will be also included in log messages. 17 | *

18 | * When the annotation is placed at class level it will create a new MDC for all its methods invocations. 19 | *

Limitations

20 | * The library uses Spring AOP to intercept annotated method invocations so these considerations must be token into account: 21 | *
    22 | *
  • The method must be invoked from outside the bean scope. Local calls are not intercepted by Spring AOP, thus any method annotation will be ignored in this case.
  • 23 | *
  • Spring AOP does not intercept private methods, so if you invoke an inner bean private method, it will have no effect on it.
  • 24 | *
25 | * @see MDCParam 26 | */ 27 | @Documented 28 | @Retention(RetentionPolicy.RUNTIME) 29 | @Target({ElementType.TYPE, ElementType.METHOD}) 30 | public @interface WithMDC { 31 | /** 32 | * MDC name (optional). 33 | * The name will be used as a prefix for all parameters defined inside the MDC scope. 34 | * @return MDC name 35 | */ 36 | @AliasFor("value") 37 | String name() default ""; 38 | 39 | /** 40 | * MDC name (optional). Alias for name. 41 | * @return MDC name 42 | */ 43 | @AliasFor("name") 44 | String value() default ""; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/loggers/LoggingSubsystemResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.loggers; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | 5 | public class LoggingSubsystemResolver { 6 | 7 | /** 8 | * Resolve logging system using classpath or set up custom implementation specified in LoggerMDCAdapter.MDC_ADAPTER_SYSTEM_PROPERTY 9 | * system property. 10 | * @return logging system MDC adapter 11 | */ 12 | public static LoggerMDCAdapter resolveMDCAdapter() { 13 | if (System.getProperty(LoggerMDCAdapter.MDC_ADAPTER_SYSTEM_PROPERTY) != null) { 14 | String adapterClazz = System.getProperty(LoggerMDCAdapter.MDC_ADAPTER_SYSTEM_PROPERTY); 15 | try { 16 | Class aClass = Thread.currentThread().getContextClassLoader().loadClass(adapterClazz); 17 | return (LoggerMDCAdapter) aClass.getDeclaredConstructor().newInstance(); 18 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { 19 | throw new RuntimeException("Can not instantiate logger MDC adapter class: " + adapterClazz, e); 20 | } catch (InvocationTargetException | NoSuchMethodException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | 25 | if (classExistsInClasspath("org.slf4j.Logger")) { 26 | return new Slf4JLoggerMDCAdapter(); 27 | } 28 | else if (classExistsInClasspath("org.apache.logging.log4j.Logger")) { 29 | return new Log4J2LoggerMDCAdapter(); 30 | } 31 | else if (classExistsInClasspath("org.apache.log4j.Logger")) { 32 | return new Log4JLoggerMDCAdapter(); 33 | } 34 | else { 35 | System.err.println("MDC4Spring: no any logging subsystem was detected in classpath."); 36 | return new DummyLoggerMDCAdapter(); 37 | } 38 | } 39 | 40 | private static boolean classExistsInClasspath(String className) { 41 | try { 42 | Thread.currentThread().getContextClassLoader().loadClass(className); 43 | return true; 44 | } catch (ClassNotFoundException e) { 45 | return false; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/MDCInvocationBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.Callable; 6 | import java.util.function.Supplier; 7 | 8 | /** 9 | * Builds new MDC and invokes task inside it 10 | */ 11 | public class MDCInvocationBuilder { 12 | private final String namespace; 13 | private Map parameters; 14 | 15 | MDCInvocationBuilder(String namespace) { 16 | this.namespace = namespace; 17 | } 18 | 19 | /** 20 | * Add new parameter to newly created MDC 21 | * @param name parameter's name 22 | * @param value parameter's value 23 | * @throws IllegalArgumentException if parameter name is null 24 | * @return this builder instance 25 | */ 26 | public MDCInvocationBuilder param(String name, Object value) { 27 | if (name == null) throw new IllegalArgumentException("Name must not be null"); 28 | if (parameters == null) 29 | parameters = new HashMap<>(); 30 | parameters.put(name, value); 31 | return this; 32 | } 33 | 34 | /** 35 | * Creates new MDC and runs a task inside it. 36 | * @param task task to run 37 | */ 38 | public void run(Runnable task) { 39 | try (CloseableMDC mdc = MDC.create(namespace)) { 40 | if (parameters != null) 41 | parameters.forEach(mdc::put); 42 | task.run(); 43 | } 44 | } 45 | 46 | /** 47 | * Creates new MDC and runs inside it a task that returns some value. 48 | * @param task task to run 49 | * @param return type 50 | * @return a value returned by task 51 | */ 52 | public T run(Supplier task) { 53 | try (CloseableMDC mdc = MDC.create(namespace)) { 54 | if (parameters != null) 55 | parameters.forEach(mdc::put); 56 | return task.get(); 57 | } 58 | } 59 | 60 | /** 61 | * Creates new MDC and runs a callable task inside it. 62 | * @param task task to run 63 | * @param return type 64 | * @return a value returned by callable task 65 | * @throws Exception exception thrown by callable task 66 | */ 67 | public T call(Callable task) throws Exception { 68 | try (CloseableMDC mdc = MDC.create(namespace)) { 69 | if (parameters != null) 70 | parameters.forEach(mdc::put); 71 | return task.call(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Ideas and roadmap 2 | ----------------- 3 | [x] Add #systemProperties variable 4 | 5 | [-] ELK adds an automatic correlation to MDC using agent instrumentation: 6 | https://www.elastic.co/guide/en/apm/agent/java/current/log-correlation.html. 7 | Adds these MDC parameters are automatically added: 8 | - transaction.id 9 | - trace.id 10 | - error.id 11 | How to convert Java logs to ELK format: https://www.elastic.co/guide/en/ecs-logging/java/1.x/setup.html 12 | 13 | [x] Access to private fields in local bean? 14 | 15 | [ ] Add support for jboss-log-manager? 16 | 17 | [?] Use Agent or Annotation Processor instead of AOP? 18 | https://blog.jcore.com/2016/12/modify-java-8-final-behaviour-with-annotations/ 19 | Java TreeTranslator is a compiler's internal API! 20 | 21 | [ ] Copy MDC state to another MDC context. 22 | [ ] Add support for Spring @Async annotation 23 | 24 | [x] Parent-child parameters overwrite: when the child MDC is closed restore parent's params values that were overwritten by child MDC 25 | 26 | [x] Document all public API 27 | 28 | [x] Readme.md 29 | 30 | [x] How to maintain method arguments' names after compilation (debug information) 31 | Add -parameters argument to javac. 32 | Already resolved in Spring Boot Plugin 33 | https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/ 34 | 35 | [x] Class MDC: add lambda-based MDC definitions: 36 | MDC.with() 37 | .param("param1", "value1") 38 | .param("param2", "value2") 39 | .run(() -> { 40 | ... 41 | }); 42 | MDC.runWith(() -> { 43 | ... 44 | }) 45 | 46 | [x] Add MDC.rootParam() to put param into root MDC. 47 | 48 | [x] Refactor annotations: allow multiple @MDCParam annotations on method or bean. 49 | @MDCParam(name = "param1", eval = "..."); 50 | @MDCParam(name = "param2", eval = "..."); 51 | public void myMethod(@MDCParam param3) 52 | 53 | [x] Behavioral changes: create new MDC context only with explicit definition @WithMDC. 54 | Any call to a method annotated with @MDCParam and without @WithMDC will set parameters on current MDC. 55 | After the method exits all parameters must be removed explicitly. 56 | 57 | [x] Refactor detection of logging subsystem 58 | 59 | [x] Support for local calls. 60 | - Using AspectJ? 61 | AspectJ uses compile-time or runtime weaving. In first case it requires a maven plugin to enhance classes. 62 | In second case it requires an agent specified at JVM start. Both of them significally complicate the usage. 63 | [x] Try: Spring AOP also intercepts protected and package-private methods? 64 | 65 | [x] Add "#className" and "#methodName" to the evaluation context variable 66 | 67 | [x] Provide output parameter propagation for 'parent' MDC: 68 | public @MDCParam(name = "customerId", eval = "id") Customer findCustomerByClientId() 69 | 70 | [ ] Discover subclasses & interfaces for annotations 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/spring/spel/SpelExpressionEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.spel; 2 | 3 | import com.github.throwable.mdc4spring.util.ExpressionEvaluator; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.expression.*; 7 | import org.springframework.expression.spel.SpelCompilerMode; 8 | import org.springframework.expression.spel.SpelEvaluationException; 9 | import org.springframework.expression.spel.SpelParserConfiguration; 10 | import org.springframework.expression.spel.standard.SpelExpressionParser; 11 | import org.springframework.expression.spel.support.StandardEvaluationContext; 12 | import org.springframework.lang.NonNull; 13 | import org.springframework.lang.Nullable; 14 | 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | public class SpelExpressionEvaluator implements ExpressionEvaluator { 19 | private static final ConcurrentHashMap expressionCache = new ConcurrentHashMap<>(); 20 | 21 | // SpEL parser is thead-safe 22 | private final ExpressionParser expressionParser; 23 | private final Environment environment; 24 | private final ApplicationContext applicationContext; 25 | private final boolean tolerateNPEs; 26 | 27 | 28 | private final BeanResolver applicationContextBeanResolver = new BeanResolver() { 29 | @Override 30 | public @NonNull Object resolve(@NonNull EvaluationContext context, @NonNull String beanName) { 31 | return applicationContext.getBean(beanName); 32 | } 33 | }; 34 | 35 | private final PropertyAccessor environmentPropertyAccessor = new PropertyAccessor() { 36 | @Override 37 | public Class[] getSpecificTargetClasses() { 38 | return new Class[] {Environment.class}; 39 | } 40 | 41 | @Override 42 | public boolean canRead(@NonNull EvaluationContext context, Object target, @NonNull String name) { 43 | return true; 44 | } 45 | 46 | @NonNull 47 | @Override 48 | public TypedValue read(@NonNull EvaluationContext context, Object target, @NonNull String name) { 49 | return new TypedValue(environment.getProperty(name)); 50 | } 51 | 52 | @Override 53 | public boolean canWrite(@NonNull EvaluationContext context, Object target, @NonNull String name) { 54 | return false; 55 | } 56 | 57 | @Override 58 | public void write(@NonNull EvaluationContext context, Object target, @NonNull String name, Object newValue) { 59 | } 60 | }; 61 | 62 | public SpelExpressionEvaluator(Environment environment, ApplicationContext applicationContext) { 63 | this.environment = environment; 64 | this.applicationContext = applicationContext; 65 | this.expressionParser = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null)); 66 | this.tolerateNPEs = "true".equalsIgnoreCase(System.getProperty(ExpressionEvaluator.TOLERATE_NPE_SYSTEM_PROPERTY, "true")); 67 | } 68 | 69 | @Override 70 | public Object evaluate(String expression, Object rootObject, 71 | @Nullable Map argumentValues, 72 | Map expressionVariables) 73 | { 74 | Expression parsedExpression = expressionCache.get(expression); 75 | if (parsedExpression == null) { 76 | parsedExpression = expressionParser.parseExpression(expression); 77 | final Expression expressionUpdated = expressionCache.putIfAbsent(expression, parsedExpression); 78 | if (expressionUpdated != null) 79 | parsedExpression = expressionUpdated; 80 | } 81 | 82 | StandardEvaluationContext context = new StandardEvaluationContext(rootObject); 83 | context.addPropertyAccessor(environmentPropertyAccessor); 84 | // Ugly: detect if we are evaluating expression on root=localBean give full access to its private properties 85 | if (argumentValues != null) 86 | context.addPropertyAccessor(new PrivateFieldPropertyAccessor(rootObject.getClass())); 87 | context.setBeanResolver(applicationContextBeanResolver); 88 | context.setVariable("environment", environment); 89 | context.setVariable("systemProperties", System.getProperties()); 90 | 91 | if (argumentValues != null) { 92 | //context.setVariable("params", argumentValues); 93 | argumentValues.forEach(context::setVariable); 94 | } 95 | 96 | expressionVariables.forEach(context::setVariable); 97 | 98 | try { 99 | return parsedExpression.getValue(context); 100 | } catch (SpelEvaluationException e) { 101 | // EL1012E: Cannot index into a null value 102 | if (tolerateNPEs && e.getMessage().startsWith("EL1012E:")) 103 | return null; 104 | throw e; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/anno/MDCParam.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.anno; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * Defines a parameter that will be added to current MDC. The parameter will be present in all log messages 9 | * occurred during the method invocation. 10 | *

11 | * The annotation may be set at method level, at bean level or for any of method arguments. 12 | *

Annotating method arguments

13 | * When {@literal @}MDCParam annotates method argument, a new parameter will be added to current MDC 14 | * during the method invocation. By default, the parameter will have the argument name and the value the method invoked with. 15 | * Alternatively you can specify a custom name for the parameter or define an expression that performs a transformation of its original value. 16 | *

17 | *

Annotating methods

18 | * When annotating a method with {@literal @}MDCParam you can define additional parameters that will be 19 | * included to MDC during the method invocation. For each parameter you need to specify a unique name and an expression 20 | * that will be evaluated during every method call. 21 | * The evaluation context may contain references to: 22 | *
    23 | *
  • Local bean as #root object. You have access to bean's private fields and properties.
  • 24 | *
  • All argument values of the invocation referenced by #argumentName variables.
  • 25 | *
  • Spring configuration properties available in #environment map.
  • 26 | *
  • System properties referenced by #systemProperties variable.
  • 27 | *
  • Current class and method names with #className and #methodName variables.
  • 28 | *
29 | *

30 | * For more information about Spring expressions please refer to Spring Expression Language documentation. 31 | *

32 | *

Annotating beans

33 | * The {@literal @}MDCParam annotation set at bean level has the same effect as if it is applied to each method of the bean. 34 | *

35 | *

Limitations

36 | * The library uses Spring AOP to intercept annotated method invocations so these considerations must be token into account: 37 | *
    38 | *
  • The method must be invoked from outside the bean scope. Local calls are not intercepted by Spring AOP, thus any method annotation will be ignored in this case.
  • 39 | *
  • Spring AOP does not intercept private methods, so if you invoke an inner bean private method, it will have no effect on it.
  • 40 | *
  • By default Java compiler does not keep method argument names in generated bytecode, so it may cause 41 | * possible problems with parameter name resolutions when using {@literal @}MDCParam. There are three ways to avoid this problem: 42 | *
      43 | *
    • If you are using Spring Boot and Spring Boot Maven or Gradle plugin all method arguments will already be saved 44 | * with their names in generated bytecode, and no additional action is required.
    • 45 | *
    • If you are not using Spring Boot plugin you may tell to compiler to preserve method argument names 46 | * by adding -parameters argument to javac invocation.
    • 47 | *
    • You may also provide parameter names explicitly: 48 | *
      public User findUserById({@literal @}MDCParam("userId") String userId)
    • 49 | *
    50 | *
  • 51 | *
52 | *

Sample usage

53 | *
54 |  * {@literal @}MDCParam(name = "transaction.id", eval = "#order.transactionId"),
55 |  * {@literal @}MDCParam(name = "client.id", eval = "#order.clientId")
56 |  *  public void createOrder(Order order,
57 |  *                         {@literal @}MDCParam(eval = "name") Queue queue,
58 |  *                         {@literal @}MDCParam Priority priority,
59 |  *                         {@literal @}MDCParam("user.id") String userId)
60 |  * 
61 | * @see WithMDC 62 | */ 63 | @Documented 64 | @Retention(RetentionPolicy.RUNTIME) 65 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) 66 | @Repeatable(MDCParams.class) 67 | public @interface MDCParam { 68 | /** 69 | * Parameter name. Optional for method argument annotation, required for method and bean-level annotations. 70 | * @return Parameter name 71 | */ 72 | @AliasFor("value") 73 | String name() default ""; 74 | 75 | /** 76 | * Parameter name. Alias for name attribute. 77 | * @return Parameter name 78 | */ 79 | @AliasFor("name") 80 | String value() default ""; 81 | 82 | /** 83 | * Expression to evaluate. 84 | * For more information please refer to Spring Expression Language documentation. 85 | * @return Expression to evaluate 86 | */ 87 | String eval() default ""; 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/CloseableMDC.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter; 4 | import com.github.throwable.mdc4spring.loggers.LoggingSubsystemResolver; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | 9 | /** 10 | * An MDC implementation. Additionally, it implements an AutoCloseable interface to use in try-with-resources block. 11 | */ 12 | public class CloseableMDC implements AutoCloseable, MDC { 13 | 14 | private static final ThreadLocal currentMdc = new ThreadLocal<>(); 15 | private static LoggerMDCAdapter loggerMDCAdapter = LoggingSubsystemResolver.resolveMDCAdapter(); 16 | 17 | private final CloseableMDC parent; 18 | private final String namePrefix; 19 | private HashMap mdcData; 20 | 21 | 22 | private CloseableMDC(CloseableMDC parent, String namePrefix) { 23 | this.parent = parent; 24 | this.namePrefix = namePrefix; 25 | mdcData = new HashMap<>(); 26 | } 27 | 28 | static CloseableMDC current() throws IllegalStateException { 29 | CloseableMDC mdc = currentMdc.get(); 30 | if (mdc == null) 31 | throw new IllegalStateException("No MDC was set for current execution scope"); 32 | return mdc; 33 | } 34 | 35 | static boolean hasCurrent() { 36 | return currentMdc.get() != null; 37 | } 38 | 39 | static CloseableMDC root() throws IllegalStateException { 40 | CloseableMDC mdc = current(); 41 | while (mdc.getParent() != null) 42 | mdc = mdc.getParent(); 43 | return mdc; 44 | } 45 | 46 | static void setLoggerMDCAdapter(LoggerMDCAdapter mdcAdapter) { 47 | loggerMDCAdapter = mdcAdapter; 48 | } 49 | 50 | static LoggerMDCAdapter getLoggerMDCAdapter() { 51 | return loggerMDCAdapter; 52 | } 53 | 54 | static CloseableMDC create() { 55 | return create(""); 56 | } 57 | 58 | static CloseableMDC create(String namespace) { 59 | CloseableMDC current = currentMdc.get(); 60 | String keyPrefix = current != null ? current.namePrefix : ""; 61 | String newKeyPrefix; 62 | if (namespace != null && !namespace.isEmpty()) 63 | newKeyPrefix = keyPrefix + namespace + "."; 64 | else 65 | newKeyPrefix = keyPrefix; 66 | CloseableMDC mdc = new CloseableMDC(current, newKeyPrefix); 67 | currentMdc.set(mdc); 68 | return mdc; 69 | } 70 | 71 | @SuppressWarnings("resource") 72 | @Override 73 | public void close() { 74 | if (mdcData == null) throw new IllegalStateException("MDC is closed"); 75 | for (String name : new ArrayList<>(mdcData.keySet())) { 76 | remove(name); 77 | } 78 | if (this.getParent() != null) 79 | currentMdc.set(this.getParent()); 80 | else 81 | currentMdc.remove(); 82 | mdcData = null; 83 | } 84 | 85 | @Override 86 | public CloseableMDC getParent() { 87 | return parent; 88 | } 89 | 90 | @Override 91 | public CloseableMDC put(String name, Object value) { 92 | if (mdcData == null) throw new IllegalStateException("MDC is closed"); 93 | if (name == null) throw new IllegalArgumentException("Name must not be null"); 94 | mdcData.put(name, value); 95 | loggerMDCAdapter.put(namePrefix + name, value != null ? value.toString() : null); 96 | return this; 97 | } 98 | 99 | @Override 100 | public Object get(String name) { 101 | if (mdcData == null) throw new IllegalStateException("MDC is closed"); 102 | if (name == null) throw new IllegalArgumentException("Name must not be null"); 103 | return mdcData.get(name); 104 | } 105 | 106 | @Override 107 | public CloseableMDC remove(String name) { 108 | if (mdcData == null) throw new IllegalStateException("MDC is closed"); 109 | if (name == null) throw new IllegalArgumentException("Name must not be null"); 110 | mdcData.remove(name); 111 | loggerMDCAdapter.remove(namePrefix + name); 112 | if (getParent() != null) 113 | getParent().restore(namePrefix + name); 114 | return this; 115 | } 116 | 117 | /** 118 | * Child MDC may overwrite a parent's MDC parameter. So when it removes any of its params we must ensure that 119 | * the original value will be restored. 120 | * @param nameWithPrefix full parameter's name with prefix 121 | */ 122 | private void restore(String nameWithPrefix) { 123 | if (nameWithPrefix.startsWith(this.namePrefix)) { 124 | String name = nameWithPrefix.substring(this.namePrefix.length()); 125 | if (mdcData.containsKey(name)) { 126 | final Object value = mdcData.get(name); 127 | loggerMDCAdapter.put(nameWithPrefix, value != null ? value.toString() : null); 128 | } else { 129 | if (getParent() != null) 130 | getParent().restore(nameWithPrefix); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/spring/spel/PrivateFieldPropertyAccessor.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.spel; 2 | 3 | import org.springframework.expression.AccessException; 4 | import org.springframework.expression.EvaluationContext; 5 | import org.springframework.expression.PropertyAccessor; 6 | import org.springframework.expression.TypedValue; 7 | import org.springframework.lang.NonNull; 8 | 9 | import java.lang.reflect.Field; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.function.Function; 14 | import java.util.function.Supplier; 15 | 16 | class PrivateFieldPropertyAccessor implements PropertyAccessor { 17 | private final static ConcurrentHashMap resolvedAccessors = new ConcurrentHashMap<>(); 18 | 19 | private final Class clazz; 20 | 21 | interface Accessor { 22 | Object getValue(Object target) throws AccessException; 23 | } 24 | 25 | PrivateFieldPropertyAccessor(Class clazz) { 26 | this.clazz = clazz; 27 | } 28 | 29 | @Override 30 | public Class[] getSpecificTargetClasses() { 31 | return new Class[] {clazz}; 32 | } 33 | 34 | @Override 35 | public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { 36 | if (target == null) return false; 37 | resolveAccessor(target.getClass(), name); 38 | return true; 39 | } 40 | 41 | @Override 42 | public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { 43 | if (target == null) 44 | throw new AccessException("Unable to read a property of null target"); 45 | return new TypedValue(resolveAccessor(target.getClass(), name).getValue(target)); 46 | } 47 | 48 | @Override 49 | public boolean canWrite(EvaluationContext context, Object target, String name) { 50 | return false; 51 | } 52 | 53 | @Override 54 | public void write(EvaluationContext context, Object target, String name, Object newValue) { 55 | } 56 | 57 | @NonNull 58 | private static Accessor resolveAccessor(Class clazz, String name) throws AccessException { 59 | String accessorId = clazz.getName() + "/" + name; 60 | Accessor accessor = resolvedAccessors.get(accessorId); 61 | 62 | if (accessor == null) { 63 | for (Class aClass = clazz; !Object.class.equals(aClass); aClass = aClass.getSuperclass()) { 64 | // Try to find accessor field 65 | try { 66 | Field propertyField = aClass.getDeclaredField(name); 67 | propertyField.setAccessible(true); 68 | accessor = target -> { 69 | try { 70 | return propertyField.get(target); 71 | } catch (IllegalAccessException ex) { 72 | throw new AccessException(ex.getMessage()); 73 | } 74 | }; 75 | break; 76 | } catch (NoSuchFieldException ignore) { 77 | } 78 | 79 | // Try to find accessor method 80 | String nameCapitalized = Character.toUpperCase(name.charAt(0)) + name.substring(1); 81 | Method accessorMethod = null; 82 | try { 83 | accessorMethod = aClass.getDeclaredMethod("get" + nameCapitalized); 84 | accessorMethod.setAccessible(true); 85 | } catch (NoSuchMethodException ignore) { 86 | try { 87 | accessorMethod = aClass.getDeclaredMethod("is" + nameCapitalized); 88 | if (!Boolean.class.isAssignableFrom(accessorMethod.getReturnType())) { 89 | accessorMethod = null; 90 | } else { 91 | accessorMethod.setAccessible(true); 92 | } 93 | } catch (NoSuchMethodException ignore1) { 94 | } 95 | } 96 | 97 | if (accessorMethod == null) 98 | continue; 99 | 100 | Method finalAccessorMethod = accessorMethod; 101 | accessor = target -> { 102 | try { 103 | return finalAccessorMethod.invoke(target); 104 | } catch (InvocationTargetException | IllegalAccessException ex) { 105 | throw new AccessException(ex.getMessage()); 106 | } 107 | }; 108 | } 109 | } 110 | else 111 | return accessor; 112 | 113 | if (accessor == null) 114 | throw new AccessException("Property accessor or field was not found for property '" + 115 | name + " in class " + clazz.getName()); 116 | 117 | final Accessor accessorUpdated = resolvedAccessors.putIfAbsent(accessorId, accessor); 118 | return accessorUpdated != null ? accessorUpdated : accessor; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/cmp/SampleMDCComponent.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring.cmp; 2 | 3 | import com.github.throwable.mdc4spring.anno.MDCParam; 4 | import com.github.throwable.mdc4spring.anno.MDCOutParam; 5 | import com.github.throwable.mdc4spring.anno.WithMDC; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.math.BigDecimal; 12 | 13 | import static com.github.throwable.mdc4spring.MDC.current; 14 | 15 | @Service 16 | @SuppressWarnings({"unused", "SameParameterValue"}) 17 | public class SampleMDCComponent { 18 | private final static Logger log = LoggerFactory.getLogger(SampleMDCComponent.class); 19 | 20 | @Autowired 21 | NestedMDCComponent nestedMDCComponent; 22 | @Autowired InnerMDCComponent innerMDCComponent; 23 | 24 | private final String sampleFieldValue = "Sample local field value"; 25 | 26 | private String getSampleAccessorValue() { 27 | return "Sample accessor value"; 28 | } 29 | 30 | public String sampleMethodValue(String argument) { 31 | return argument.toUpperCase(); 32 | } 33 | 34 | @WithMDC 35 | public void execWithSimpleMDC() { 36 | current().put("sampleKey", "Some Value"); 37 | log.info("Simple MDC Scope trace"); 38 | } 39 | 40 | @WithMDC(name = "component1") 41 | public void execWithNamedMDC() { 42 | current().put("sampleKey", "Some Value"); 43 | log.info("Prefixed MDC keys"); 44 | } 45 | 46 | @WithMDC(name = "component1") 47 | public void execWithNestedMDCs() { 48 | current().put("sampleKey", "Some Value"); 49 | log.info("Before nested MDC"); 50 | nestedMDCComponent.execWithNewMDC(); 51 | log.info("After nested MDC"); 52 | } 53 | 54 | @WithMDC(name = "component1") 55 | public void execWithNestedSameNameMDC() { 56 | current().put("sampleKey", "Some Value"); 57 | log.info("Before nested MDC"); 58 | nestedMDCComponent.execWithSameNameMDC(); 59 | log.info("After nested MDC"); 60 | } 61 | 62 | @WithMDC 63 | public void execInvocationInCurrentMDC() { 64 | nestedMDCComponent.execWithCurrentMDC("value1"); 65 | log.info("After invocation"); 66 | } 67 | 68 | @MDCParam(name = "keyParam1", eval = "'Sample string'") 69 | public void execParamMethodWithoutMDC() { 70 | log.info("MDC must be created implicitly"); 71 | } 72 | 73 | 74 | @WithMDC 75 | @MDCParam(name = "keyParam1", eval = "'Sample string'") 76 | public void execWithMethodOnlyMDCParameter() { 77 | log.info("Only one parameter"); 78 | } 79 | 80 | @WithMDC 81 | @MDCParam(name = "keyParam1", eval = "'Sample string'") 82 | @MDCParam(name = "keyParam2", eval = "'Number ' + 5") 83 | public void execWithFixedMDCParameters() { 84 | log.info("Fixed parameters"); 85 | } 86 | 87 | @WithMDC 88 | @MDCParam(name = "localFieldParam", eval = "sampleFieldValue") 89 | @MDCParam(name = "localAccessorParam", eval = "sampleAccessorValue") 90 | @MDCParam(name = "localMethodParam", eval = "'Transformed: ' + sampleMethodValue(sampleFieldValue)") 91 | @MDCParam(name = "environmentProperty", eval = "#environment['sample.property']") 92 | @MDCParam(name = "systemProperty", eval = "#systemProperties['user.home']") 93 | @MDCParam(name = "externalParameterBeanValue", eval = "@externalParameterBean.externalBeanValue") 94 | @MDCParam(name = "method", eval = "#className + '/' + #methodName") 95 | public void execWithMDCParametersReferencingContext() { 96 | log.info("Parameters referencing local bean and environment"); 97 | } 98 | 99 | @WithMDC 100 | public void execWithMethodArgumentAsMDCParameter(@MDCParam String param1, String param2) 101 | { 102 | log.info("Argument as MDC parameter"); 103 | } 104 | 105 | @WithMDC 106 | @MDCParam(name = "concatAllArgumentsParam", eval = "#param1 + #param2 + #param3 + #clazz + #notIncluded") 107 | public void execWithMethodArgumentsAsMDCParameters(@MDCParam String param1, 108 | @MDCParam("param2") int myArg, 109 | @MDCParam BigDecimal param3, 110 | @MDCParam(eval = "name") Class clazz, 111 | String notIncluded) 112 | { 113 | log.info("Arguments as MDC parameters"); 114 | } 115 | 116 | @WithMDC 117 | public void execLocalNonPublicMethods() { 118 | samplePackagePrivateMethod("package-private"); 119 | sampleProtectedMethod("protected"); 120 | samplePrivateMethod("private"); 121 | } 122 | 123 | @WithMDC 124 | public void execLocalPublicMethod() { 125 | samplePublicMethod("public"); 126 | } 127 | 128 | 129 | @WithMDC 130 | public void samplePublicMethod(@MDCParam String scope) { 131 | log.info("Public method"); 132 | } 133 | 134 | @WithMDC 135 | void samplePackagePrivateMethod(@MDCParam String scope) { 136 | log.info("Package-private method"); 137 | } 138 | 139 | @WithMDC 140 | protected void sampleProtectedMethod(@MDCParam String scope) { 141 | log.info("Protected method"); 142 | } 143 | 144 | @WithMDC 145 | private void samplePrivateMethod(@MDCParam String scope) { 146 | log.info("Private method"); 147 | } 148 | 149 | @WithMDC 150 | public void execRemoteNonPublicMethods() { 151 | innerMDCComponent.samplePackagePrivateMethod("package-private"); 152 | innerMDCComponent.sampleProtectedMethod("protected"); 153 | innerMDCComponent.samplePrivateMethod("private"); 154 | } 155 | 156 | 157 | @Service 158 | public static class InnerMDCComponent { 159 | @WithMDC 160 | void samplePackagePrivateMethod(@MDCParam String scope) { 161 | log.info("Package-private method"); 162 | } 163 | 164 | @WithMDC 165 | protected void sampleProtectedMethod(@MDCParam String scope) { 166 | log.info("Protected method"); 167 | } 168 | 169 | @WithMDC 170 | private void samplePrivateMethod(@MDCParam String scope) { 171 | log.info("Private method"); 172 | } 173 | } 174 | 175 | @WithMDC 176 | public void returnOutputParameters() { 177 | nestedMDCComponent.returnOutputParameterWithoutNestedMDC(); 178 | log.info("message1 param must be present here"); 179 | nestedMDCComponent.returnOutputParameterWithNestedMDC(); 180 | log.info("message2 param must also be present here"); 181 | } 182 | 183 | @MDCOutParam(name = "test", eval = "'rest'") 184 | public void returnOutputParameterWithoutScope() { 185 | } 186 | 187 | @WithMDC 188 | public void returnOutputUnnamedParameters() { 189 | nestedMDCComponent.returnUnnamedOutParams(); 190 | log.info("unnamed out params"); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/TestMDCCore.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.assertj.core.api.Assertions.*; 10 | 11 | 12 | public class TestMDCCore { 13 | static MapBasedLoggerMDCAdapter mdcAdapter = new MapBasedLoggerMDCAdapter(); 14 | static LoggerMDCAdapter originalMDCLoggerAdapter; 15 | 16 | @BeforeAll 17 | public static void setupMDCAdapter() { 18 | originalMDCLoggerAdapter = MDC.getLoggerMDCAdapter(); 19 | MDC.setLoggerMDCAdapter(mdcAdapter); 20 | } 21 | 22 | @AfterAll 23 | public static void restoreMDCAdapter() { 24 | MDC.setLoggerMDCAdapter(originalMDCLoggerAdapter); 25 | } 26 | 27 | @BeforeEach 28 | public void clearMdc() { 29 | mdcAdapter.getMap().clear(); 30 | } 31 | 32 | @Test 33 | public void testManualMDCOperation() { 34 | assertThat(MDC.hasCurrent()).isFalse(); 35 | 36 | try (CloseableMDC mdc = MDC.create() 37 | .put("property1", 1) 38 | .put("property2", "test")) { 39 | assertThat(MDC.hasCurrent()).isTrue(); 40 | assertThat(mdc.get("property1")).isEqualTo(1); 41 | 42 | MDC.current().put("property3", "rest"); 43 | MDC.param("property4", "property4value"); 44 | 45 | try (CloseableMDC ignored = MDC.create("pfx") 46 | .put("property4", "nested")) { 47 | assertThat(mdcAdapter.getMap()) 48 | .containsEntry("property1", "1") 49 | .containsEntry("pfx.property4", "nested"); 50 | assertThat(MDC.root()).isSameAs(mdc); 51 | } 52 | 53 | mdc.put("property5", "property5value"); 54 | 55 | assertThat(mdcAdapter.getMap()) 56 | .containsEntry("property1", "1") 57 | .containsEntry("property2", "test") 58 | .containsEntry("property3", "rest") 59 | .containsEntry("property4", "property4value") 60 | .containsEntry("property5", "property5value") 61 | .hasSize(5); 62 | 63 | mdc.remove("property5").remove("property4"); 64 | assertThat(mdcAdapter.getMap()) 65 | .doesNotContainKey("property5") 66 | .doesNotContainKey("property4") 67 | .containsKey("property3"); 68 | } 69 | 70 | assertThatThrownBy(() -> MDC.current().put("param", "test")) 71 | .as("Must be no active MDC") 72 | .isInstanceOf(IllegalStateException.class); 73 | assertThat(mdcAdapter.getMap()).isEmpty(); 74 | } 75 | 76 | @Test 77 | public void parameterOverwrites() { 78 | try (CloseableMDC ignored = MDC.create()) { 79 | MDC.param("some.prefix.param1", "value1"); 80 | try (CloseableMDC ignored1 = MDC.create("some")) { 81 | MDC.param("prefix.param1", "value2"); 82 | try (CloseableMDC ignored2 = MDC.create("prefix")) { 83 | MDC.param("param1", "value3"); 84 | assertThat(mdcAdapter.getMap()) 85 | .containsEntry("some.prefix.param1", "value3"); 86 | } 87 | assertThat(mdcAdapter.getMap()) 88 | .containsEntry("some.prefix.param1", "value2"); 89 | } 90 | assertThat(mdcAdapter.getMap()) 91 | .containsEntry("some.prefix.param1", "value1"); 92 | } 93 | } 94 | 95 | @Test 96 | public void noActiveMDCShouldFail() { 97 | assertThatThrownBy(() -> MDC.current().put("param", "test")).isInstanceOf(IllegalStateException.class); 98 | } 99 | 100 | @Test 101 | public void nullKeyMustNotBeAccepted() { 102 | assertThatThrownBy(() -> { 103 | try (CloseableMDC mdc = MDC.create()) { 104 | mdc.put(null, 1); 105 | } 106 | }) 107 | .as("Must not accept null keys") 108 | .isInstanceOf(IllegalArgumentException.class); 109 | } 110 | 111 | @Test 112 | public void doubleCloseMDCShouldFail() { 113 | assertThatThrownBy(() -> { 114 | try (CloseableMDC mdc = MDC.create()) { 115 | //noinspection RedundantExplicitClose 116 | mdc.close(); 117 | } 118 | }) 119 | .as("Must fail when closing already closed MDC") 120 | .isInstanceOf(IllegalStateException.class); 121 | } 122 | 123 | @Test 124 | public void rootParamTest() { 125 | try (CloseableMDC ignored = MDC.create()) { 126 | try (CloseableMDC ignored1 = MDC.create()) { 127 | try (CloseableMDC ignored2 = MDC.create()) { 128 | MDC.param("localParam", "localValue"); 129 | MDC.rootParam("rootParam", "rootParamValue"); 130 | } 131 | } 132 | assertThat(mdcAdapter.getMap()) 133 | .containsEntry("rootParam", "rootParamValue") 134 | .doesNotContainKey("localParam"); 135 | } 136 | } 137 | 138 | @Test 139 | public void mdcInvocationBuilder() throws Exception { 140 | MDC.with("component") 141 | .param("param1", "value1") 142 | .param("param2", "value2") 143 | .run(() -> { 144 | assertThat(mdcAdapter.getMap()) 145 | .containsEntry("component.param1", "value1") 146 | .containsEntry("component.param2", "value2"); 147 | }); 148 | 149 | final String result = MDC.with() 150 | .param("param1", "value1") 151 | .run(() -> { 152 | assertThat(mdcAdapter.getMap()) 153 | .hasSize(1) 154 | .containsEntry("param1", "value1"); 155 | return "Result"; 156 | }); 157 | assertThat(result).isEqualTo("Result"); 158 | 159 | final String callResult = MDC.with() 160 | .param("param1", "value1") 161 | .call(() -> { 162 | assertThat(mdcAdapter.getMap()) 163 | .hasSize(1) 164 | .containsEntry("param1", "value1"); 165 | return "Result"; 166 | }); 167 | assertThat(callResult).isEqualTo("Result"); 168 | 169 | assertThatThrownBy(() -> MDC.with() 170 | .param("param1", "value1") 171 | .call(() -> { 172 | assertThat(mdcAdapter.getMap()) 173 | .hasSize(1) 174 | .containsEntry("param1", "value1"); 175 | throw new UnsupportedOperationException(); 176 | }) 177 | ).isInstanceOf(UnsupportedOperationException.class); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/spring/WithMDCAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring; 2 | 3 | import com.github.throwable.mdc4spring.CloseableMDC; 4 | import com.github.throwable.mdc4spring.MDC; 5 | import com.github.throwable.mdc4spring.util.AnnotatedMethodMDCParamsEvaluator; 6 | import com.github.throwable.mdc4spring.util.ExpressionEvaluator; 7 | import com.github.throwable.mdc4spring.util.MethodInvocationMDCParametersValues; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.annotation.Around; 10 | import org.aspectj.lang.annotation.Aspect; 11 | import org.aspectj.lang.reflect.MethodSignature; 12 | import org.springframework.aop.framework.Advised; 13 | import org.springframework.aop.support.AopUtils; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.core.DefaultParameterNameDiscoverer; 16 | import org.springframework.stereotype.Component; 17 | 18 | import java.util.Map; 19 | import java.util.Objects; 20 | 21 | @Aspect 22 | @Component 23 | @SuppressWarnings("unused") 24 | public class WithMDCAspect { 25 | private final AnnotatedMethodMDCParamsEvaluator annotatedMethodMDCParamsEvaluator; 26 | 27 | @Autowired 28 | public WithMDCAspect(ExpressionEvaluator expressionEvaluator) { 29 | DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 30 | this.annotatedMethodMDCParamsEvaluator = new AnnotatedMethodMDCParamsEvaluator( 31 | parameterNameDiscoverer::getParameterNames, expressionEvaluator); 32 | } 33 | 34 | // https://www.faqcode4u.com/faq/214039/aspectj-pointcut-expression-match-parameter-annotations-at-any-position 35 | @Around("@annotation(com.github.throwable.mdc4spring.anno.WithMDC) || " + 36 | "@annotation(com.github.throwable.mdc4spring.anno.MDCParam) || " + 37 | "@annotation(com.github.throwable.mdc4spring.anno.MDCParams) || " + 38 | "@annotation(com.github.throwable.mdc4spring.anno.MDCOutParam) || " + 39 | "@annotation(com.github.throwable.mdc4spring.anno.MDCOutParams) || " + 40 | "@within(com.github.throwable.mdc4spring.anno.WithMDC) || " + 41 | "@within(com.github.throwable.mdc4spring.anno.MDCParam) || " + 42 | "@within(com.github.throwable.mdc4spring.anno.MDCParams) ||" + 43 | "execution(* *(.., @com.github.throwable.mdc4spring.anno.MDCParam (*), ..))" 44 | ) 45 | public Object invokeWithMDC(ProceedingJoinPoint joinPoint) throws Throwable { 46 | Object unproxiedTarget = joinPoint.getTarget(); 47 | while (AopUtils.isAopProxy(unproxiedTarget) && unproxiedTarget instanceof Advised) { 48 | unproxiedTarget = ((Advised) unproxiedTarget).getTargetSource().getTarget(); 49 | } 50 | 51 | MethodSignature signature = (MethodSignature) joinPoint.getSignature(); 52 | MethodInvocationMDCParametersValues methodInvocationMdcParamValues = 53 | annotatedMethodMDCParamsEvaluator.evalMethodInvocationMDCParamValues( 54 | signature.getMethod(), unproxiedTarget, joinPoint.getArgs()); 55 | 56 | if (methodInvocationMdcParamValues == null) 57 | // Not supposed to be here: wrong PointCut configuration? 58 | return joinPoint.proceed(); 59 | 60 | final Object result; 61 | 62 | if (methodInvocationMdcParamValues.getBeanMDCNamespace() == null && methodInvocationMdcParamValues.getMethodMDCNamespace() == null) { 63 | if (MDC.hasCurrent()) { 64 | result = invokeInCurrentMDC(joinPoint, methodInvocationMdcParamValues); 65 | } else { 66 | result = invokeInNewMDC(joinPoint, methodInvocationMdcParamValues); 67 | } 68 | } 69 | else if (!Objects.equals( 70 | methodInvocationMdcParamValues.getBeanMDCNamespace(), 71 | methodInvocationMdcParamValues.getMethodMDCNamespace() 72 | )) 73 | { 74 | // Bean and method scope namespaces are different. 75 | // Create two separate MDCs: one for bean-level and another one for method-level, 76 | // for each one add their corresponding parameters. 77 | result = invokeInSeparateMDCs(joinPoint, methodInvocationMdcParamValues); 78 | } else { 79 | // Bean and method scope namespaces are the same. 80 | // Create a unique MDCs containing both bean and method parameters. 81 | result = invokeInNewMDC(joinPoint, methodInvocationMdcParamValues); 82 | } 83 | 84 | // Setting up output parameters to current MDC (if any) 85 | if (methodInvocationMdcParamValues.isHasMDCParamOut() && MDC.hasCurrent()) { 86 | Map outputParams = annotatedMethodMDCParamsEvaluator.evaluateMethodInvocationOutputParams( 87 | signature.getMethod(), result); 88 | if (outputParams != null) { 89 | MDC mdc = MDC.current(); 90 | outputParams.forEach(mdc::put); 91 | } 92 | } 93 | return result; 94 | } 95 | 96 | private Object invokeInNewMDC(ProceedingJoinPoint joinPoint, MethodInvocationMDCParametersValues methodInvocationMdcParamValues) throws Throwable { 97 | String namespace = methodInvocationMdcParamValues.getBeanMDCNamespace() != null ? 98 | methodInvocationMdcParamValues.getBeanMDCNamespace() : 99 | (methodInvocationMdcParamValues.getMethodMDCNamespace() != null ? 100 | methodInvocationMdcParamValues.getMethodMDCNamespace() : ""); 101 | 102 | try (CloseableMDC mdc = MDC.create(namespace)) { 103 | methodInvocationMdcParamValues.getBeanMDCParamValues() 104 | .forEach(mdc::put); 105 | methodInvocationMdcParamValues.getMethodMDCParamValues() 106 | .forEach(mdc::put); 107 | 108 | return joinPoint.proceed(); 109 | } 110 | } 111 | 112 | private Object invokeInSeparateMDCs(ProceedingJoinPoint joinPoint, MethodInvocationMDCParametersValues methodInvocationMdcParamValues) throws Throwable { 113 | try (CloseableMDC beanMdc = MDC.create(methodInvocationMdcParamValues.getBeanMDCNamespace())) { 114 | methodInvocationMdcParamValues.getBeanMDCParamValues() 115 | .forEach(beanMdc::put); 116 | try (CloseableMDC methodMdc = MDC.create(methodInvocationMdcParamValues.getMethodMDCNamespace())) { 117 | methodInvocationMdcParamValues.getMethodMDCParamValues() 118 | .forEach(methodMdc::put); 119 | 120 | return joinPoint.proceed(); 121 | } 122 | } 123 | } 124 | 125 | private Object invokeInCurrentMDC(ProceedingJoinPoint joinPoint, MethodInvocationMDCParametersValues methodInvocationMdcParamValues) throws Throwable { 126 | // ??? remove parameters after method returns 127 | methodInvocationMdcParamValues.getBeanMDCParamValues() 128 | .forEach(MDC::param); 129 | methodInvocationMdcParamValues.getMethodMDCParamValues() 130 | .forEach(MDC::param); 131 | return joinPoint.proceed(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/MDC.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring; 2 | 3 | import com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter; 4 | 5 | /** 6 | * A basic class to manage MDC programmatically. 7 | * Initially an execution flow must open a new MDC using MDC.create() method in try-with-resources statement. 8 | * Inside the MDC scope you can set MDC parameters with their values that will be transmitted to an underlying logging system. 9 | * After leaving scope all the defined parameters will be cleared automatically. 10 | * You can also define multiple nested MDC scopes. In this case the logging trace inside nested scope will include all 11 | * parameters defined in all parent scopes. When leaving nested scope only parameters defined inside it will be cleared. 12 | *

13 | * Example: 14 | *

 15 |  * try (CloseableMDC mdc = MDC.create()) {
 16 |  *     mdc.put("param1", "value1");
 17 |  *     log.info("Logging trace has param1");
 18 |  *     try (CloseableMDC mdcNested = MDC.create() {
 19 |  *         mdcNested.put("param2", "value2");
 20 |  *         log.info("Included param1 and param2);
 21 |  *     }
 22 |  *     log.info("param1 is still here while param2 is already out of scope");
 23 |  * }
 24 |  * 
25 | */ 26 | public interface MDC { 27 | /** 28 | * Get a "current" MDC instance: the closest one to current execution scope. 29 | * If MDC is not defined at current execution scope a method will throw IllegalStateException. 30 | * @return current MDC instance 31 | * @throws IllegalStateException if no MDC defined at current execution scope 32 | */ 33 | static MDC current() { 34 | return CloseableMDC.current(); 35 | } 36 | 37 | /** 38 | * Get a root MDC instance: the first MDC opened by current execution flow. 39 | * @return root MDC instance 40 | * @throws IllegalStateException if no MDC defined at current execution scope 41 | */ 42 | static MDC root() { 43 | return CloseableMDC.root(); 44 | } 45 | 46 | /** 47 | * Set LoggerMDCAdapter implementation. 48 | * @param loggerMDCAdapter new logger MDC adapter implementation 49 | */ 50 | static void setLoggerMDCAdapter(LoggerMDCAdapter loggerMDCAdapter) { 51 | CloseableMDC.setLoggerMDCAdapter(loggerMDCAdapter); 52 | } 53 | 54 | /** 55 | * Get current LoggerMDCAdapter implementation. 56 | * @return current loggerMDCAdapter implementation. 57 | */ 58 | static LoggerMDCAdapter getLoggerMDCAdapter() { 59 | return CloseableMDC.getLoggerMDCAdapter(); 60 | } 61 | 62 | /** 63 | * Check if MDC was defined at current execution scope. 64 | * @return true if MDC was defined, false otherwise 65 | */ 66 | static boolean hasCurrent() { 67 | return CloseableMDC.hasCurrent(); 68 | } 69 | 70 | /** 71 | * Define new MDC (root or nested). This method must be used with try-with-resources statement to ensure its correct cleanup. 72 | *
 73 |      * try (CloseableMDC mdc = MDC.create()) {
 74 |      *      ...
 75 |      * }
 76 |      * 
77 | * @return closeable MDC resource 78 | */ 79 | static CloseableMDC create() { 80 | return CloseableMDC.create(); 81 | } 82 | 83 | /** 84 | * Define new MDC (root or nested) using namespace prefix. All parameters defined inside this MDC will have 85 | * prefix specified in namespace. 86 | * This method must be used with try-with-resources statement to ensure its correct cleanup. 87 | *
 88 |      * try (CloseableMDC mdc = MDC.create("myComponent")) {
 89 |      *      mdc.param("param1", "value1");
 90 |      *      log.info("The final name of parameter in logging trace will be 'myComponent.param1'");
 91 |      * }
 92 |      * 
93 | * @param namespace namespace prefix 94 | * @return closeable MDC resource 95 | */ 96 | static CloseableMDC create(String namespace) { 97 | return CloseableMDC.create(namespace); 98 | } 99 | 100 | /** 101 | * Set parameter value in closest MDC. The method is equivalent to MDC.current().put(name, value). 102 | * @param name parameter's name 103 | * @param value parameter's value 104 | * @throws IllegalArgumentException if parameter name is null 105 | * @throws IllegalStateException if no MDC defined at current execution scope. 106 | */ 107 | static void param(String name, Object value) throws IllegalArgumentException, IllegalStateException { 108 | current().put(name, value); 109 | } 110 | 111 | /** 112 | * Set parameter value in root MDC. The method is equivalent to MDC.root().put(name, value). 113 | * @param name parameter's name 114 | * @param value parameter's value 115 | * @throws IllegalArgumentException if parameter name is null 116 | * @throws IllegalStateException if no MDC defined at current execution scope. 117 | */ 118 | static void rootParam(String name, Object value) throws IllegalArgumentException, IllegalStateException { 119 | root().put(name, value); 120 | } 121 | 122 | /** 123 | * Build new MDC and pass lambda that will be invoked inside it. 124 | *

125 | * Sample usage: 126 | *
127 |      * MDC.with("component")
128 |      *         .param("param1", "value1")
129 |      *         .param("param2", "value2")
130 |      *         .run(() -> {
131 |      *             ...
132 |      *         });
133 |      * 
134 | * @param namespace namespace for new MDC 135 | * @return invocation builder for new MDC 136 | */ 137 | static MDCInvocationBuilder with(String namespace) { 138 | return new MDCInvocationBuilder(namespace); 139 | } 140 | 141 | /** 142 | * Build new MDC and pass lambda that will be invoked inside it. 143 | *

144 | * Sample usage: 145 | *

146 |      * MDC.with("component")
147 |      *         .param("param1", "value1")
148 |      *         .param("param2", "value2")
149 |      *         .run(() -> {
150 |      *             ...
151 |      *         });
152 |      * 
153 | * @return invocation builder for new MDC 154 | */ 155 | static MDCInvocationBuilder with() { 156 | return new MDCInvocationBuilder(null); 157 | } 158 | 159 | 160 | /** 161 | * If current MDC is a nested one return its direct parent. 162 | * @return parent MDC or null if current MDC is a root 163 | */ 164 | MDC getParent(); 165 | 166 | /** 167 | * Set parameter's value. 168 | * @param name parameter's name 169 | * @param value parameter's value 170 | * @throws IllegalArgumentException if parameter's name is null 171 | * @return current MDC 172 | */ 173 | MDC put(String name, Object value) throws IllegalArgumentException; 174 | 175 | /** 176 | * Get parameter's value. 177 | * @param name parameter's name 178 | * @throws IllegalArgumentException if parameter's name is null 179 | * @return parameter value 180 | */ 181 | Object get(String name) throws IllegalArgumentException; 182 | 183 | /** 184 | * Remove parameter from MDC. 185 | * @param name parameter's name 186 | * @throws IllegalArgumentException if parameter's name is null 187 | * @return current MDC 188 | */ 189 | MDC remove(String name) throws IllegalArgumentException; 190 | } 191 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.throwable.mdc4spring 8 | mdc4spring 9 | 1.1 10 | mdc4spring 11 | Declarative Logging Mapping Diagnostic Context for Spring 12 | https://github.com/throwable/mdc4spring 13 | jar 14 | 15 | 16 | scm:git:git@github.com:throwable/mdc4spring.git 17 | scm:git:git@github.com:throwable/mdc4spring.git 18 | https://github.com/throwable/mdc4spring 19 | HEAD 20 | 21 | 22 | 23 | 24 | owner 25 | Anton Kuranov 26 | ant.kuranov@gmail.com 27 | UTC+2 28 | 29 | 30 | 31 | 32 | 33 | The Apache Software License, Version 2.0 34 | https://www.apache.org/licenses/LICENSE-2.0.txt 35 | 36 | 37 | 38 | 39 | UTF-8 40 | UTF-8 41 | 8 42 | 8 43 | 44 | 3.1.2 45 | 46 | 47 | 48 | 49 | log4j 50 | log4j 51 | 1.2.17 52 | provided 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter 58 | ${spring-boot-version} 59 | provided 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-aop 64 | ${spring-boot-version} 65 | provided 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | ${spring-boot-version} 71 | test 72 | 73 | 74 | 75 | 76 | org.assertj 77 | assertj-core 78 | 3.24.2 79 | test 80 | 81 | 82 | 83 | 84 | ch.qos.logback.contrib 85 | logback-json-classic 86 | 0.1.5 87 | test 88 | 89 | 90 | ch.qos.logback.contrib 91 | logback-jackson 92 | 0.1.5 93 | test 94 | 95 | 96 | com.fasterxml.jackson.core 97 | jackson-databind 98 | 2.15.1 99 | test 100 | 101 | 102 | 103 | ch.qos.logback 104 | logback-classic 105 | 1.4.8 106 | test 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-surefire-plugin 116 | 3.1.2 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-failsafe-plugin 121 | 3.1.2 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-source-plugin 127 | 3.3.0 128 | 129 | 130 | attach-sources 131 | 132 | jar 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-javadoc-plugin 140 | 3.5.0 141 | 142 | 143 | attach-javadocs 144 | 145 | jar 146 | 147 | 148 | none 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-gpg-plugin 156 | 1.6 157 | 158 | 159 | sign-artifacts 160 | verify 161 | 162 | sign 163 | 164 | 165 | 166 | --pinentry-mode 167 | loopback 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | ossrh 179 | https://s01.oss.sonatype.org/content/repositories/snapshots 180 | 181 | 182 | ossrh 183 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/test/java/com/github/throwable/mdc4spring/spring/TestAnnotatedMDCSpring.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.spring; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import com.github.throwable.mdc4spring.InMemoryLoggingEventsAppender; 5 | import com.github.throwable.mdc4spring.MDC; 6 | import com.github.throwable.mdc4spring.spring.cmp.BeanMDCComponent; 7 | import com.github.throwable.mdc4spring.spring.cmp.SampleMDCComponent; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | 13 | import java.math.BigDecimal; 14 | import java.util.*; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.assertj.core.api.Assertions.assertThatCode; 18 | 19 | @SpringBootTest( 20 | properties = "sample.property=Environment property value" 21 | ) 22 | class TestAnnotatedMDCSpring { 23 | @Autowired 24 | SampleMDCComponent sampleMDCComponent; 25 | @Autowired 26 | BeanMDCComponent beanMDCComponent; 27 | 28 | @BeforeEach 29 | public void clearMdc() { 30 | InMemoryLoggingEventsAppender.clearLoggingEvents(); 31 | } 32 | 33 | @Test 34 | void simpleMDC() { 35 | sampleMDCComponent.execWithSimpleMDC(); 36 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 37 | assertThat(traces).hasSize(1); 38 | assertThat(traces.get(0).getMDCPropertyMap()) 39 | .hasSize(1) 40 | .containsEntry("sampleKey", "Some Value"); 41 | } 42 | 43 | @Test 44 | void namedMDC() { 45 | sampleMDCComponent.execWithNamedMDC(); 46 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 47 | assertThat(traces).hasSize(1); 48 | assertThat(traces.get(0).getMDCPropertyMap()) 49 | .hasSize(1) 50 | .containsEntry("component1.sampleKey", "Some Value"); 51 | } 52 | 53 | @Test 54 | void nestedMDCs() { 55 | sampleMDCComponent.execWithNestedMDCs(); 56 | 57 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 58 | assertThat(traces).hasSize(3); 59 | assertThat(traces.get(0).getMDCPropertyMap()) 60 | .hasSize(1) 61 | .containsEntry("component1.sampleKey", "Some Value"); 62 | assertThat(traces.get(1).getMDCPropertyMap()) 63 | .as("Nested MDC must contain keys from all parent scopes") 64 | .hasSize(2) 65 | .containsEntry("component1.sampleKey", "Some Value") 66 | .containsEntry("component1.component2.nestedKey", "NestedKeyValue"); 67 | assertThat(traces.get(2).getMDCPropertyMap()) 68 | .as("By exiting nested MDC all inner keys must be removed") 69 | .hasSize(1) 70 | .containsEntry("component1.sampleKey", "Some Value"); 71 | } 72 | 73 | @Test 74 | void nestedMDCSameName() { 75 | sampleMDCComponent.execWithNestedSameNameMDC(); 76 | 77 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 78 | assertThat(traces).hasSize(3); 79 | assertThat(traces.get(0).getMDCPropertyMap()) 80 | .hasSize(1) 81 | .containsEntry("component1.sampleKey", "Some Value"); 82 | assertThat(traces.get(1).getMDCPropertyMap()) 83 | .as("Nested MDC must contain keys from all parent scopes") 84 | .hasSize(2) 85 | .containsEntry("component1.sampleKey", "Some Value") 86 | .containsEntry("component1.nestedKey", "NestedKeyValue"); 87 | assertThat(traces.get(2).getMDCPropertyMap()) 88 | .as("By exiting nested all inner keys must be removed") 89 | .hasSize(1) 90 | .containsEntry("component1.sampleKey", "Some Value"); 91 | } 92 | 93 | @Test 94 | void nestedBeanWithInvocationInCurrentMDC() { 95 | sampleMDCComponent.execInvocationInCurrentMDC(); 96 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 97 | assertThat(traces).hasSize(2); 98 | assertThat(traces.get(0).getMDCPropertyMap()) 99 | .hasSize(2) 100 | .containsEntry("param1", "value1") 101 | .containsEntry("param2", "value2"); 102 | assertThat(traces.get(1).getMDCPropertyMap()) 103 | .as("Current MDC parameters should not be cleared after the method call") 104 | .hasSize(2) 105 | .containsEntry("param1", "value1") 106 | .containsEntry("param2", "value2"); 107 | } 108 | 109 | @Test 110 | void paramMethodCallWithoutMDCDefined() { 111 | sampleMDCComponent.execParamMethodWithoutMDC(); 112 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 113 | assertThat(traces).hasSize(1); 114 | assertThat(traces.get(0).getMDCPropertyMap()) 115 | .as("MDC must implicitly be created") 116 | .hasSize(1) 117 | .containsEntry("keyParam1", "Sample string"); 118 | assertThat(MDC.hasCurrent()).isFalse(); 119 | } 120 | 121 | @Test 122 | void mdcWithMethodOnlyMDCParameter() { 123 | sampleMDCComponent.execWithMethodOnlyMDCParameter(); 124 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 125 | assertThat(traces).hasSize(1); 126 | assertThat(traces.get(0).getMDCPropertyMap()) 127 | .hasSize(1) 128 | .containsEntry("keyParam1", "Sample string"); 129 | } 130 | 131 | @Test 132 | void mdcWithFixedParameters() { 133 | sampleMDCComponent.execWithFixedMDCParameters(); 134 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 135 | assertThat(traces).hasSize(1); 136 | assertThat(traces.get(0).getMDCPropertyMap()) 137 | .hasSize(2) 138 | .containsEntry("keyParam1", "Sample string") 139 | .containsEntry("keyParam2", "Number 5"); 140 | } 141 | 142 | @Test 143 | void mdcWithParametersReferencingContext() { 144 | sampleMDCComponent.execWithMDCParametersReferencingContext(); 145 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 146 | assertThat(traces).hasSize(1); 147 | assertThat(traces.get(0).getMDCPropertyMap()) 148 | .hasSize(7) 149 | .containsEntry("localFieldParam", "Sample local field value") 150 | .containsEntry("localAccessorParam", "Sample accessor value") 151 | .containsEntry("localMethodParam", "Transformed: SAMPLE LOCAL FIELD VALUE") 152 | .containsEntry("environmentProperty", "Environment property value") 153 | .containsKey("systemProperty") 154 | .containsEntry("externalParameterBeanValue", "Sample external bean value") 155 | .containsEntry("method", "com.github.throwable.mdc4spring.spring.cmp.SampleMDCComponent/execWithMDCParametersReferencingContext"); 156 | } 157 | 158 | 159 | @Test 160 | void mdcMethodArgumentAsAParameter() { 161 | sampleMDCComponent.execWithMethodArgumentAsMDCParameter( 162 | "Param1 value", "Param2 value"); 163 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 164 | assertThat(traces).hasSize(1); 165 | assertThat(traces.get(0).getMDCPropertyMap()) 166 | .hasSize(1) 167 | .containsEntry("param1", "Param1 value") 168 | .doesNotContainKey("param2"); 169 | } 170 | 171 | @Test 172 | void mdcMethodArgumentsAsParameters() { 173 | sampleMDCComponent.execWithMethodArgumentsAsMDCParameters( 174 | "Param1 value", 42, new BigDecimal(65536), BigDecimal.class, "ParamNotIncluded"); 175 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 176 | assertThat(traces).hasSize(1); 177 | assertThat(traces.get(0).getMDCPropertyMap()) 178 | .hasSize(5) 179 | .containsEntry("param1", "Param1 value") 180 | .containsEntry("param2", "42") 181 | .containsEntry("param3", "65536") 182 | .containsEntry("clazz", "java.math.BigDecimal") 183 | .containsEntry("concatAllArgumentsParam", "Param1 value4265536java.math.BigDecimalParamNotIncluded") 184 | .doesNotContainKey("notIncluded"); 185 | } 186 | 187 | 188 | @Test 189 | void beanMDCParamsMethodCall() { 190 | beanMDCComponent.execWithBeanMDCParams(); 191 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 192 | assertThat(traces).hasSize(1); 193 | assertThat(traces.get(0).getMDCPropertyMap()) 194 | .hasSize(2) 195 | .containsEntry("environmentProperty", "Environment property value") 196 | .containsEntry("staticParam", "Static Value"); 197 | } 198 | 199 | 200 | @Test 201 | void beanMDCWithArgumentsAsParams() { 202 | beanMDCComponent.execWithBeanMDCAndArgumentsAsParams("Value 1", "Value 2"); 203 | 204 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 205 | assertThat(traces).hasSize(1); 206 | assertThat(traces.get(0).getMDCPropertyMap()) 207 | .hasSize(3) 208 | .containsEntry("environmentProperty", "Environment property value") 209 | .containsEntry("staticParam", "Static Value") 210 | .containsEntry("param1", "Value 1"); 211 | } 212 | 213 | @Test 214 | void beanParamsAndMethodParamsCombined() { 215 | beanMDCComponent.execWithBeanParamsAndMethodParamsCombined("Value 1", "Value 2"); 216 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 217 | assertThat(traces).hasSize(1); 218 | assertThat(traces.get(0).getMDCPropertyMap()) 219 | .hasSize(4) 220 | .containsEntry("environmentProperty", "Environment property value") 221 | .containsEntry("staticParam", "Static Value") 222 | .containsEntry("param1", "Value 1") 223 | .containsEntry("anotherProperty", "Fixed value"); 224 | } 225 | 226 | @Test 227 | void beanMDCAndNestedMethodMDCCombined() { 228 | beanMDCComponent.execWithBeanMDCAndNestedMethodMDCCombined("Value 1", "Value 2"); 229 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 230 | assertThat(traces).hasSize(1); 231 | assertThat(traces.get(0).getMDCPropertyMap()) 232 | .hasSize(4) 233 | .containsEntry("environmentProperty", "Environment property value") 234 | .containsEntry("staticParam", "Static Value") 235 | .containsEntry("nestedScope.param1", "Value 1") 236 | .containsEntry("nestedScope.anotherProperty", "Fixed value"); 237 | } 238 | 239 | @Test 240 | void beanMDCNullablePathExpression() { 241 | beanMDCComponent.extractParameterValue(null); 242 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 243 | assertThat(traces).hasSize(1); 244 | assertThat(traces.get(0).getMDCPropertyMap()) 245 | .hasSize(3) 246 | .as("SpEL should tolerate null values and never throw NPEs") 247 | .containsEntry("id", null); 248 | } 249 | 250 | 251 | @Test 252 | void callsToLocalNonPublicMethods() { 253 | sampleMDCComponent.execLocalNonPublicMethods(); 254 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 255 | assertThat(traces).hasSize(3); 256 | assertThat(traces.get(0).getMDCPropertyMap()) 257 | .as("Local calls are not instrumented by Spring AOP") 258 | .isEmpty(); 259 | assertThat(traces.get(1).getMDCPropertyMap()) 260 | .as("Local calls are not instrumented by Spring AOP") 261 | .isEmpty(); 262 | assertThat(traces.get(2).getMDCPropertyMap()) 263 | .as("Local calls are not instrumented by Spring AOP") 264 | .isEmpty(); 265 | } 266 | 267 | @Test 268 | void callsToLocalPublicMethod() { 269 | sampleMDCComponent.execLocalPublicMethod(); 270 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 271 | assertThat(traces).hasSize(1); 272 | assertThat(traces.get(0).getMDCPropertyMap()) 273 | .as("Local calls are not instrumented by Spring AOP") 274 | .isEmpty(); 275 | } 276 | 277 | @Test 278 | void callsToRemoteNonPublicMethods() { 279 | sampleMDCComponent.execRemoteNonPublicMethods(); 280 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 281 | assertThat(traces).hasSize(3); 282 | assertThat(traces.get(0).getMDCPropertyMap()) 283 | .hasSize(1) 284 | .containsEntry("scope", "package-private"); 285 | assertThat(traces.get(1).getMDCPropertyMap()) 286 | .hasSize(1) 287 | .containsEntry("scope", "protected"); 288 | assertThat(traces.get(2).getMDCPropertyMap()) 289 | .as("Spring AOP does not instrument private methods") 290 | .isEmpty(); 291 | } 292 | 293 | @Test 294 | void returnOutputParameter() { 295 | sampleMDCComponent.returnOutputParameters(); 296 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 297 | assertThat(traces).hasSize(2); 298 | assertThat(traces.get(0).getMDCPropertyMap()) 299 | .hasSize(1) 300 | .containsEntry("message1", "Hello, Pete"); 301 | assertThat(traces.get(1).getMDCPropertyMap()) 302 | .hasSize(2) 303 | .containsEntry("message2", "Hello, Mike"); 304 | } 305 | 306 | @Test 307 | void returnOutputParameterWithoutScope() { 308 | assertThatCode(() -> 309 | sampleMDCComponent.returnOutputParameterWithoutScope() 310 | ).doesNotThrowAnyException(); 311 | } 312 | 313 | @Test 314 | void returnUnnamedOutParams() { 315 | sampleMDCComponent.returnOutputUnnamedParameters(); 316 | List traces = InMemoryLoggingEventsAppender.getLoggingEvents(); 317 | assertThat(traces).hasSize(1); 318 | assertThat(traces.get(0).getMDCPropertyMap()) 319 | .hasSize(3) 320 | .containsEntry("returnUnnamedOutParams.0", "NoName") 321 | .containsEntry("returnUnnamedOutParams.1", "NoName-1") 322 | .containsEntry("named", "NoName-2"); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/main/java/com/github/throwable/mdc4spring/util/AnnotatedMethodMDCParamsEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.github.throwable.mdc4spring.util; 2 | 3 | import com.github.throwable.mdc4spring.anno.*; 4 | import org.springframework.lang.Nullable; 5 | 6 | import java.lang.annotation.Annotation; 7 | import java.lang.reflect.Method; 8 | import java.util.*; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.function.Function; 11 | 12 | /** 13 | * Class that resolves a method MDC configuration and evaluates MDC parameters for a method invocation. 14 | */ 15 | public class AnnotatedMethodMDCParamsEvaluator { 16 | private static final ConcurrentHashMap annotatedMethodConfigCache = new ConcurrentHashMap<>(); 17 | 18 | private final Function argumentsNamesDiscoverer; // = new DefaultParameterNameDiscoverer(); 19 | private final ExpressionEvaluator expressionEvaluator; 20 | 21 | public AnnotatedMethodMDCParamsEvaluator(Function argumentsNamesDiscoverer, 22 | ExpressionEvaluator expressionEvaluator) 23 | { 24 | this.argumentsNamesDiscoverer = argumentsNamesDiscoverer; 25 | this.expressionEvaluator = expressionEvaluator; 26 | } 27 | 28 | /** 29 | * Evaluate method MDC parameters for a particular method invocation. 30 | * @param method method to invoke 31 | * @param target target object instance 32 | * @param args method arguments values 33 | * @return method's MDC configuration and evaluated parameters with their values 34 | */ 35 | @Nullable 36 | public MethodInvocationMDCParametersValues evalMethodInvocationMDCParamValues( 37 | Method method, Object target, Object[] args) 38 | { 39 | AnnotatedMethodConfig annotatedMethodConfig = resolveAnnotatedMethodConfig(method); 40 | if (annotatedMethodConfig == null) 41 | return null; 42 | 43 | Map beanMDCParamValues = Collections.emptyMap(); 44 | if (!annotatedMethodConfig.getBeanMDCParamAnnotations().isEmpty()) { 45 | beanMDCParamValues = new HashMap<>(annotatedMethodConfig.getBeanMDCParamAnnotations().size() * 4 / 3 + 1); 46 | 47 | for (MDCParam parameter : annotatedMethodConfig.getBeanMDCParamAnnotations()) { 48 | String paramName = !parameter.name().isEmpty() ? parameter.name() : parameter.value(); 49 | if (paramName.isEmpty()) 50 | continue; 51 | if (!parameter.eval().isEmpty()) { 52 | Object expressionResult = evaluateExpression(parameter.eval(), target, null, 53 | annotatedMethodConfig.getExpressionStaticVariables()); 54 | beanMDCParamValues.put(paramName, expressionResult); 55 | } else { 56 | beanMDCParamValues.put(paramName, null); 57 | } 58 | } 59 | } 60 | 61 | Map methodMDCParamValues = null; 62 | 63 | if (!annotatedMethodConfig.getMdcParamByArgumentName().isEmpty()) { 64 | int estimatedCapacity = annotatedMethodConfig.getMdcParamByArgumentName().size() + 65 | (annotatedMethodConfig.getMethodMDCParamAnnotations().size()); 66 | methodMDCParamValues = new HashMap<>(estimatedCapacity * 4 / 3 + 1); 67 | 68 | for (Map.Entry argumentParam : annotatedMethodConfig.getMdcParamByArgumentName().entrySet()) { 69 | String paramName = argumentParam.getKey(); 70 | MDCParam parameter = argumentParam.getValue(); 71 | Object argumentValue = args[annotatedMethodConfig.getArgumentParamIndex(paramName)]; 72 | Object expressionResult; 73 | if (parameter.eval().isEmpty()) 74 | expressionResult = argumentValue; 75 | else 76 | expressionResult = evaluateExpression(parameter.eval(), argumentValue, null, 77 | annotatedMethodConfig.getExpressionStaticVariables()); 78 | methodMDCParamValues.put(paramName, expressionResult); 79 | } 80 | } 81 | 82 | if (!annotatedMethodConfig.getMethodMDCParamAnnotations().isEmpty()) { 83 | // In @WithMDC expression may access method arguments 84 | HashMap argumentValues = new HashMap<>(annotatedMethodConfig.getMethodMDCParamAnnotations().size() * 4 / 3 + 1); 85 | 86 | if (methodMDCParamValues == null) { 87 | methodMDCParamValues = new HashMap<>(annotatedMethodConfig.getMethodMDCParamAnnotations().size() * 4 / 3 + 1); 88 | } 89 | for (int i = 0; i < annotatedMethodConfig.getArgumentNames().size(); i++) { 90 | String paramName = annotatedMethodConfig.getArgumentNames().get(i); 91 | Object argumentValue; 92 | if (methodMDCParamValues.containsKey(paramName)) 93 | argumentValue = methodMDCParamValues.get(paramName); 94 | else argumentValue = args[i]; 95 | argumentValues.put(paramName, argumentValue); 96 | } 97 | 98 | for (MDCParam parameter : annotatedMethodConfig.getMethodMDCParamAnnotations()) { 99 | String paramName = !parameter.name().isEmpty() ? parameter.name() : parameter.value(); 100 | if (paramName.isEmpty()) 101 | continue; 102 | if (!parameter.eval().isEmpty()) { 103 | Object expressionResult = evaluateExpression(parameter.eval(), target, argumentValues, 104 | annotatedMethodConfig.getExpressionStaticVariables()); 105 | methodMDCParamValues.put(paramName, expressionResult); 106 | } 107 | else 108 | methodMDCParamValues.put(paramName, null); 109 | } 110 | } 111 | 112 | if (methodMDCParamValues == null) 113 | methodMDCParamValues = Collections.emptyMap(); 114 | 115 | //methodMDCParamValues.keySet().removeAll(excludeKeys); 116 | 117 | return new MethodInvocationMDCParametersValues( 118 | annotatedMethodConfig.getBeanMDCAnno() != null ? annotatedMethodConfig.getBeanMDCAnno().name() : null, 119 | beanMDCParamValues, 120 | annotatedMethodConfig.getMethodMDCAnno() != null ? annotatedMethodConfig.getMethodMDCAnno().name() : null, 121 | methodMDCParamValues, 122 | !annotatedMethodConfig.getMethodMDCParamOutAnnotations().isEmpty()); 123 | } 124 | 125 | @Nullable 126 | public Map evaluateMethodInvocationOutputParams(Method method, Object result) { 127 | AnnotatedMethodConfig annotatedMethodConfig = resolveAnnotatedMethodConfig(method); 128 | if (annotatedMethodConfig == null) 129 | return null; 130 | 131 | Map methodMDCParamOutValues = null; 132 | 133 | if (!annotatedMethodConfig.getMethodMDCParamOutAnnotations().isEmpty()) { 134 | methodMDCParamOutValues = new HashMap<>(annotatedMethodConfig.getMethodMDCParamOutAnnotations().size() * 4 / 3 + 1); 135 | int i = 0; 136 | for (MDCOutParam parameter : annotatedMethodConfig.getMethodMDCParamOutAnnotations()) { 137 | String paramName = !parameter.name().isEmpty() ? parameter.name() : parameter.value(); 138 | if (paramName.isEmpty()) { 139 | if (annotatedMethodConfig.getMethodMDCParamAnnotations().size() == 1) 140 | paramName = method.getName(); 141 | else 142 | paramName = method.getName() + "." + i++; 143 | } 144 | 145 | if (parameter.eval().isEmpty()) { 146 | methodMDCParamOutValues.put(paramName, result); 147 | } else { 148 | Object expressionResult = evaluateExpression(parameter.eval(), result, null, 149 | annotatedMethodConfig.getExpressionStaticVariables()); 150 | methodMDCParamOutValues.put(paramName, expressionResult); 151 | } 152 | } 153 | } 154 | return methodMDCParamOutValues; 155 | } 156 | 157 | private Object evaluateExpression(String expression, Object root, 158 | @Nullable Map argumentValues, 159 | Map environmentVariables) { 160 | try { 161 | return expressionEvaluator.evaluate(expression, root, argumentValues, environmentVariables); 162 | } catch (Exception e) { 163 | return "#EVALUATION ERROR#: " + e.getMessage(); 164 | } 165 | } 166 | 167 | 168 | private AnnotatedMethodConfig resolveAnnotatedMethodConfig(Method method) { 169 | String methodId = method.getDeclaringClass().getName() + "/" + method.getName(); 170 | AnnotatedMethodConfig config = annotatedMethodConfigCache.get(methodId); 171 | 172 | if (config == null) { 173 | WithMDC methodMDCAnno = method.getAnnotation(WithMDC.class); 174 | WithMDC beanMDCAnno = method.getDeclaringClass().getAnnotation(WithMDC.class); 175 | final MDCParam methodMDCParamAnno = method.getAnnotation(MDCParam.class); 176 | final MDCParam beanMDCParamAnno = method.getDeclaringClass().getAnnotation(MDCParam.class); 177 | final MDCParams methodMDCParamsAnno = method.getAnnotation(MDCParams.class); 178 | final MDCParams beanMDCParamsAnno = method.getDeclaringClass().getAnnotation(MDCParams.class); 179 | final MDCOutParam methodMDCOutParamAnno = method.getAnnotation(MDCOutParam.class); 180 | final MDCOutParams methodMDCOutParamsAnno = method.getAnnotation(MDCOutParams.class); 181 | 182 | 183 | final ArrayList beanMDCParamAnnotations = new ArrayList<>(); 184 | if (beanMDCParamAnno != null) 185 | beanMDCParamAnnotations.add(beanMDCParamAnno); 186 | if (beanMDCParamsAnno != null) 187 | beanMDCParamAnnotations.addAll(Arrays.asList(beanMDCParamsAnno.value())); 188 | 189 | final ArrayList methodMDCParamAnnotations = new ArrayList<>(); 190 | if (methodMDCParamAnno != null) 191 | methodMDCParamAnnotations.add(methodMDCParamAnno); 192 | if (methodMDCParamsAnno != null) 193 | methodMDCParamAnnotations.addAll(Arrays.asList(methodMDCParamsAnno.value())); 194 | 195 | final ArrayList methodMDCOutParamAnnotations = new ArrayList<>(); 196 | if (methodMDCOutParamAnno != null) 197 | methodMDCOutParamAnnotations.add(methodMDCOutParamAnno); 198 | if (methodMDCOutParamsAnno != null) 199 | methodMDCOutParamAnnotations.addAll(Arrays.asList(methodMDCOutParamsAnno.value())); 200 | 201 | Annotation[][] argumentsAnnotations = method.getParameterAnnotations(); 202 | ArrayList argumentsNames = new ArrayList<>(); 203 | Map mdcParamMap = new HashMap<>(); 204 | 205 | // Please note that for successful argument names resolution project must be compiled with 206 | // javac -parameters or using Spring Boot plugin 207 | String[] argumentsNamesAsDeclared = argumentsNamesDiscoverer.apply(method); 208 | 209 | for (int i = 0; i < argumentsAnnotations.length; i++) { 210 | Annotation[] annotations = argumentsAnnotations[i]; 211 | String parameterName = argumentsNamesAsDeclared[i]; 212 | MDCParam mdcParam = null; 213 | 214 | for (Annotation annotation : annotations) { 215 | if (MDCParam.class.equals(annotation.annotationType())) { 216 | mdcParam = (MDCParam) annotation; 217 | String paramName = !mdcParam.name().isEmpty() ? mdcParam.name() : mdcParam.value(); 218 | if (!paramName.isEmpty()) { 219 | parameterName = paramName; 220 | } 221 | } 222 | } 223 | argumentsNames.add(parameterName); 224 | if (mdcParam != null) 225 | mdcParamMap.put(parameterName, mdcParam); 226 | } 227 | 228 | final HashMap expressionStaticVariables = new HashMap<>(); 229 | expressionStaticVariables.put("methodName", method.getName()); 230 | expressionStaticVariables.put("className", method.getDeclaringClass().getName()); 231 | 232 | config = new AnnotatedMethodConfig(beanMDCAnno, methodMDCAnno, beanMDCParamAnnotations, 233 | methodMDCParamAnnotations, methodMDCOutParamAnnotations, argumentsNames, mdcParamMap, expressionStaticVariables); 234 | final AnnotatedMethodConfig configUpdated = annotatedMethodConfigCache.putIfAbsent(methodId, config); 235 | if (configUpdated != null) 236 | config = configUpdated; 237 | } 238 | return config; 239 | } 240 | 241 | 242 | private static class AnnotatedMethodConfig { 243 | @Nullable 244 | private final WithMDC beanMDCAnno; 245 | @Nullable 246 | private final WithMDC methodMDCAnno; 247 | 248 | private final List beanMDCParamAnnotations; 249 | private final List methodMDCParamAnnotations; 250 | private final List methodMDCOutParamAnnotations; 251 | private final List argumentNames; 252 | private final Map mdcParamByArgumentName; 253 | private final Map argumentIndexByParamName; 254 | private final Map expressionStaticVariables; 255 | 256 | private AnnotatedMethodConfig(@Nullable WithMDC beanMDCAnno, @Nullable WithMDC methodMDCAnno, 257 | List beanMDCParamAnnotations, List methodMDCParamAnnotations, 258 | List methodMDCOutParamAnnotations, 259 | List argumentNames, Map mdcParamByArgumentName, 260 | Map expressionStaticVariables) { 261 | this.beanMDCAnno = beanMDCAnno; 262 | this.methodMDCAnno = methodMDCAnno; 263 | this.beanMDCParamAnnotations = Collections.unmodifiableList(beanMDCParamAnnotations); 264 | this.methodMDCParamAnnotations = Collections.unmodifiableList(methodMDCParamAnnotations); 265 | this.methodMDCOutParamAnnotations = Collections.unmodifiableList(methodMDCOutParamAnnotations); 266 | this.argumentNames = Collections.unmodifiableList(argumentNames); 267 | this.mdcParamByArgumentName = Collections.unmodifiableMap(mdcParamByArgumentName); 268 | this.expressionStaticVariables = Collections.unmodifiableMap(expressionStaticVariables); 269 | argumentIndexByParamName = new HashMap<>(); 270 | for (int i = 0; i < argumentNames.size(); i++) { 271 | argumentIndexByParamName.put(argumentNames.get(i), i); 272 | } 273 | } 274 | 275 | @Nullable 276 | public WithMDC getBeanMDCAnno() { 277 | return beanMDCAnno; 278 | } 279 | 280 | @Nullable 281 | public WithMDC getMethodMDCAnno() { 282 | return methodMDCAnno; 283 | } 284 | 285 | public List getBeanMDCParamAnnotations() { 286 | return beanMDCParamAnnotations; 287 | } 288 | 289 | public List getMethodMDCParamAnnotations() { 290 | return methodMDCParamAnnotations; 291 | } 292 | 293 | public List getMethodMDCParamOutAnnotations() { 294 | return methodMDCOutParamAnnotations; 295 | } 296 | 297 | public List getArgumentNames() { 298 | return argumentNames; 299 | } 300 | 301 | public Map getMdcParamByArgumentName() { 302 | return mdcParamByArgumentName; 303 | } 304 | 305 | public int getArgumentParamIndex(String paramName) { 306 | Integer idx = argumentIndexByParamName.get(paramName); 307 | if (idx == null) 308 | throw new IllegalArgumentException("Wrong parameter name: " + paramName); 309 | return idx; 310 | } 311 | 312 | public Map getExpressionStaticVariables() { 313 | return expressionStaticVariables; 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![build status](https://github.com/throwable/mdc4spring/actions/workflows/publish.yml/badge.svg) 3 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.throwable.mdc4spring/mdc4spring/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.throwable.mdc4spring/mdc4spring/) 5 | 6 | 7 | ## MDC4Spring 8 | 9 | Simple and secure management of Mapped Diagnostic Contexts (MDCs) for multiple logging systems with 10 | Spring AOP. Supported logging systems: Slf4J/Logback, Log4J, Log4J2. 11 | 12 | ### Reasoning 13 | 14 | _Mapped Diagnostic Context_ (MDC) provides a way to enrich log traces with contextual information about the execution scope. 15 | Instead of including business parameters manually in each log message, we may consider as a good practice 16 | to provide them inside the MDC associated with the trace. 17 | This could be especially useful when you are using ELK stack, DataDog, or any other 18 | log aggregation system to track the execution of your business flows. 19 | 20 | Example of Logback logging trace that contains MDC with business data formatted by [JSON-based appender](#configuring-logback-for-json-output): 21 | 22 | ```json 23 | { 24 | "timestamp": "2022-07-26 09:01:51.482", 25 | "level": "INFO", 26 | "thread": "main", 27 | "mdc": { 28 | "requestUuid": "b8e0cd40-0cc6-11ed-861d-0242ac120002", 29 | "sourceIp": "127.0.0.1", 30 | "instance": "instance1.mybusinessdomain:8080", 31 | "action" : "createOrder", 32 | "order.transactionId": "184325928574329523", 33 | "order.clientId": "web-57961e5e0242ac120002", 34 | "order.customerId": "A123456789", 35 | "order.assigneeUserId": "192385", 36 | "order.id": "2349682" 37 | }, 38 | "logger": "com.github.throwable.mdc4springdemo.ShippingOrderController", 39 | "message": "Order created", 40 | "context": "default" 41 | } 42 | ``` 43 | 44 | Different logging libraries provide a way of setting MDC parameters using thread-local context that 45 | require a user to manually control the proper cleanup of them when the execution flow leaves the scope. 46 | So, if one forgets to clean them properly, it may later pollute log messages with outdated information or even provoke memory 47 | leaks in thread-pooled environments. 48 | 49 | The idea is to use automatic MDC management by intercepting method calls specifying via annotations 50 | the data from method arguments to include in the context. 51 | 52 | ##### Example 53 | 54 | ```java 55 | class OrderProcessor { 56 | // define a new MDC that will be cleared automatically when the method returns 57 | @WithMDC 58 | public void assignToUser( 59 | // add method arguments with their values to the current MDC 60 | @MDCParam String orderId, @MDCParam String userId 61 | ) { 62 | // programmatically include additional parameter to the current MDC 63 | MDC.param("transactionId", this.getCurrentTransactionId()); 64 | 65 | // business logic... 66 | 67 | // The MDC of the log message contains parameters: orderId, userId, transactionId 68 | log.info("User was successfully assigned to order"); 69 | // All these parameters defined within the scope of the method 70 | // are automatically removed when the method returns. 71 | } 72 | } 73 | ``` 74 | 75 |

(back to top)

76 | 77 | 78 | 79 | 80 | ## Getting Started 81 | 82 | ### Prerequisites 83 | 84 | The library works with Java 8+ and Spring Boot (2.x, 3.x) or Spring Framework (5.x, 6.x) environments. 85 | Currently, it supports Slf4J/Logback, Log4j2, and Log4j logging systems. 86 | By default, the logging system is detected using classpath library resolution, but you can change this behavior setting 87 | `com.github.throwable.mdc4spring.loggers.LoggerMDCAdapter` 88 | system property to a desired `LoggerMDCAdapter` implementation class: 89 | `Log4J2LoggerMDCAdapter`, `Log4JLoggerMDCAdapter`, `Slf4JLoggerMDCAdapter`. 90 | 91 | ### Installation 92 | 93 | Add the following dependencies to your project's build file. 94 | 95 | #### Maven 96 | 97 | ```xml 98 | 99 | io.github.throwable.mdc4spring 100 | mdc4spring 101 | 1.1 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-starter-aop 106 | 107 | ``` 108 | 109 | #### Gradle 110 | 111 | ```groovy 112 | dependencies { 113 | // ... 114 | implementation("io.github.throwable.mdc4spring:mdc4spring:1.0") 115 | implementation("org.springframework.boot:spring-boot-starter-aop") 116 | } 117 | ``` 118 | 119 | If you are using Spring Boot the configuration will be applied automatically by autoconfiguration mechanism. 120 | In case you are not using Spring Boot you have to import `com.github.throwable.mdc4spring.spring.MDCConfiguration` 121 | manually. 122 | 123 | In case you're not using Spring Boot, replace the `spring-boot-starter-aop` dependency by the Spring Framework AOP library: 124 | 125 | ```xml 126 | 127 | org.springframework 128 | spring-aspects 129 | ${springframework-version} 130 | 131 | ``` 132 | ```groovy 133 | dependencies { 134 | // ... 135 | implementation("org.springframework:spring-aspects:${springframework-version}") 136 | } 137 | ``` 138 | You also need to configure your logging subsystem to see MDC parameters in your log traces. 139 | [See an example of JSON-based appender](#configuring-logback-for-json-output). 140 | 141 |

(back to top)

142 | 143 | 144 | 145 | ## Usage 146 | 147 | 148 | #### Method argument parameters 149 | 150 | Simple usage: log messages inside the method will contain MDC with `orderId` and `userId` parameters that will automatically be removed after the method returns. 151 | 152 | ```java 153 | class OrderProcessor { 154 | @WithMDC 155 | public void assignToUser(@MDCParam String orderId, @MDCParam String userId) { 156 | // business logic... 157 | log.info("User assigned to order"); 158 | } 159 | } 160 | ``` 161 | 162 | By default, method argument names will be used as parameter names, [see considerations](#method-argument-names), but you can also define custom names for them. 163 | 164 | ```java 165 | class OrderProcessor { 166 | @WithMDC 167 | public void assignToUser(@MDCParam("order.id") String orderId, @MDCParam("user.id") String userId) { 168 | log.info("User assigned to order"); 169 | } 170 | } 171 | ``` 172 | 173 | The value of any argument can be converted inline using [Spring expression language](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions). 174 | In this case `order.id` and `user.id` parameters will be set to `order.getId()` and `user.getId()` respectively. 175 | If any error occurs during the expression evaluation, the execution will not be interrupted, and a parameter value will 176 | contain a value like _#EVALUATION ERROR#: exception message_. 177 | By default, NPE errors are excluded from this case, and the `null` value is returned if any referenced object in the path is null. 178 | 179 | ```java 180 | class OrderProcessor { 181 | @WithMDC 182 | public void assignToUser( 183 | @MDCParam(name = "order.id", eval = "id") Order order, 184 | @MDCParam(name = "user.id", eval = "id") User user) 185 | { 186 | log.info("User assigned to order"); 187 | } 188 | } 189 | ``` 190 | 191 | #### Additional parameters 192 | 193 | Also, additional MDC parameters can be defined for any method. 194 | In this case, the `eval` attribute should define a SpEL expression that may contain references to the current evaluation context. 195 | 196 | ```java 197 | class RequestProcessor { 198 | @WithMDC 199 | @MDCParam(name = "request.uid", eval = "T(java.util.UUID).randomUUID()") // (1) 200 | @MDCParam(name = "app.id", eval = "#environment['spring.application.name']") // (2) 201 | @MDCParam(name = "jvm.rt", eval = "#systemProperties['java.runtime.version']") // (3) 202 | @MDCParam(name = "tx.id", eval = "transactionId") // (4) 203 | @MDCParam(name = "source.ip", eval = "#request.remoteIpAddr") // (5) 204 | @MDCParam(name = "operation", eval = "#className + '/' + #methodName") // (6) 205 | @MDCParam(name = "client.id", eval = "@authenticationService.getCurrentClientId()") // (7) 206 | public void processRequest(Request request) { 207 | // ... 208 | } 209 | 210 | private String getTransactionId() { 211 | // ... 212 | } 213 | } 214 | ``` 215 | The example above contains: 216 | 217 | 1. Providing a parameter value by calling a static method of some class. 218 | 2. Accessing Spring configuration property using `#environment` variable. 219 | 3. Obtaining JVM system property using `#systemProperties` variable. 220 | 4. Accessing a property of the local bean. The whole bean object is available in expression evaluation as `#root` variable. 221 | The property may be a getter or a local field of any visibility level. 222 | 5. Each method argument is available within the expression using `#argumentName` variable. 223 | 6. Variables `#className` and `#methodName` contain the fully-qualified class name and the method name respectively. 224 | 7. Using `@beanName` notation, you can reference any named bean within the Spring Application Context. 225 | 226 | #### MDC and the method scope 227 | 228 | `@WithMDC` and `@MDCParam` annotations may also be defined at class level. 229 | In this case, they provoke the same effect as being applied to each method. 230 | 231 | ```java 232 | @WithMDC 233 | @MDCParam(name = "request.uid", eval = "T(java.util.UUID).randomUUID()") 234 | @MDCParam(name = "app.id", eval = "#environment['spring.application.name']") 235 | @MDCParam(name = "operation", eval = "#className + '/' + #methodName") 236 | class OrderProcessor { 237 | public void createOrder(@MDCParam(name = "order.id", eval = "id") Order order) {} 238 | public void updateOrder(@MDCParam(name = "order.id", eval = "id") Order order) {} 239 | public void removeOrder(@MDCParam(name = "order.id") String orderId) {} 240 | public Order getOrder(@MDCParam(name = "order.id") String orderId) {} 241 | } 242 | ``` 243 | 244 | MDC may also be named. This adds the name as a prefix to any parameter defined within its scope. 245 | 246 | ```java 247 | @WithMDC("order") 248 | class OrderProcessor { 249 | // The name of the MDC parameter will be "order.id" 250 | public void createOrder(@MDCParam(name = "id", eval = "id") Order order) { 251 | } 252 | } 253 | ``` 254 | 255 | In a cascade invocations of two methods when both annotated with `WithMDC`, 256 | a new 'nested' MDC is created for the nested method invocation. 257 | This MDC will contain parameters defined inside the nested method, and it will be closed after the method returns 258 | ([see considerations](#method-invocations)). 259 | All parameters created in the 'outer' MDC will remain as is in log messages. 260 | 261 | ```java 262 | class OrderProcessor { 263 | @WithMDC 264 | public void createOrder(@MDCParam(name = "orderId", eval = "id") Order order) { 265 | // Call a method that defines a 'nested' MDC 266 | Customer customer = customerRepository.findCustomerByName(order.getCustomerId()); 267 | log.info("after the 'nested' MDC call returns the customerId parameter will no longer exist in log messages"); 268 | } 269 | } 270 | class CustomerRepository { 271 | // Defines a 'nested' MDC. The parameter customerId belongs to this new MDC 272 | // and will be cleared after the method returns. 273 | @WithMDC 274 | public Customer findCustomerById(@MDCParam String customerId) { 275 | log.info("this log message will have orderId and customerId parameters defined"); 276 | } 277 | } 278 | ``` 279 | 280 | Any call to a 'nested' method that defines `@MDCParam` but is not annotated with `@WithMDC` annotation 281 | will simply add a new parameter to the current MDC, and it remains there after the method returns. 282 | 283 | ```java 284 | class OrderProcessor { 285 | @WithMDC 286 | public void createOrder(@MDCParam(name = "orderId", eval = "id") Order order) { 287 | // Call a method current MDC 288 | Customer customer = customerRepository.findCustomerByName(order.getCustomerId()); 289 | log.info("after the method call we still have a customerId parameter in our MDC"); 290 | } 291 | } 292 | class CustomerRepository { 293 | // The parameter customerId will be added to current MDC, and remains there after the method returns. 294 | public Customer findCustomerById(@MDCParam String customerId) { 295 | log.info("this log message will have orderId and customerId parameters defined"); 296 | } 297 | } 298 | ``` 299 | 300 | #### Output parameters 301 | 302 | With `@MDCOutParam` annotation you can define an output parameter that will be added to the current MDC after the method returns. 303 | Its value is evaluated using the value returned by this method. 304 | 305 | ```java 306 | class OrderProcessor { 307 | @WithMDC 308 | public void createOrder(Order order) { 309 | User user = userRepository.findUserById(order.getUserId()); 310 | log.info("this log message will have userId, userName and userGroup MDC parameters"); 311 | } 312 | } 313 | class UserRepository { 314 | @MDCOutParam(name = "userName", eval = "name") 315 | @MDCOutParam(name = "userGroup", eval = "group.name") 316 | public User findUserById(@MDCParam String userId) {} 317 | } 318 | ``` 319 | 320 | #### Defining MDC programmatically 321 | 322 | That gives you a full control over MDC scopes and parameter definitions. 323 | Use try-with-resources block to ensure a proper cleanup of all defined parameters. 324 | 325 | ```java 326 | class OrderProcessor { 327 | public void createOrder(Order order) { 328 | try (CloseableMDC rootMdc = MDC.create()) { 329 | // Add a param to current MDC 330 | MDC.param("order.id", order.getId()); 331 | log.info("order.id is added to MDC"); 332 | 333 | try (CloseableMDC nestedMdc = MDC.create()) { 334 | // Add a param to nested MDC (nearest for current execution scope) 335 | MDC.param("customer.id", order.getCustomerId()); 336 | log.info("Both order.id and customer.id appear in log messages"); 337 | } 338 | log.info("order.id is still remains in messages but customer.id is removed with its MDC"); 339 | } 340 | } 341 | } 342 | ``` 343 | 344 | Alternatively you may use a lambda-based API to define MDC scopes. 345 | 346 | ```java 347 | class OrderProcessor { 348 | public void createOrder(Order order) { 349 | MDC.with().param("order.id", order.getId()).run(() -> { 350 | log.info("order.id is added to MDC"); 351 | Customer customer = MDC.with().param("customer.id", order.getCustomerId()).apply(() -> { 352 | log.info("Both order.id and customer.id appear in log messages"); 353 | return customerRepository.findCustomerById(order.getCustomerId()); 354 | }); 355 | log.info("order.id is still remains in messages but customer.id is removes with its MDC"); 356 | }); 357 | } 358 | } 359 | ``` 360 | 361 |

(back to top)

362 | 363 | 364 | ## Considerations and limitations 365 | 366 | ### Method invocations 367 | 368 | The library uses Spring AOP to intercept annotated method invocations, so these considerations must be taken into account: 369 | 370 | * The annotated method must be invoked from outside the bean scope. Local calls are not intercepted by Spring AOP, thus any method annotation will be ignored in this case. 371 | ```java 372 | class MyBean { 373 | @Lazy @Autowired MyBean self; 374 | 375 | public void publicMethod(@MDCParam String someParam) { 376 | anotherPublicMethod("this call is local, so 'anotherParam' will not be included in MDC"); 377 | self.anotherPublicMethod("this call is proxied, so 'anotherParam' will be included hin MDC"); 378 | } 379 | public void anotherPublicMethod(@MDCParam String anotherParam) { 380 | log.info("Some log trace"); 381 | } 382 | } 383 | ``` 384 | * Spring AOP does not intercept private methods, so if you invoke an inner bean's private method, it will have no effect on it. 385 | 386 | In both of the cases above you should define your parameters in an imperative way using `MDC.param()`. 387 | 388 | ### Method argument names 389 | 390 | By default, Java compiler does not keep method argument names in generated bytecode, and it may cause 391 | possible problems with parameter name resolutions when using `@MDCParam` for method's arguments. 392 | There are three ways to avoid this problem: 393 | 394 | * If you are using Spring Boot and Spring Boot Maven or Gradle plugin, the generated bytecode will already contain 395 | all method arguments with their names, and no additional action is required. 396 | * If you are not using Spring Boot plugin you may tell your compiler to preserve method argument names 397 | by adding `-parameters` argument to `javac` invocation. 398 | * You may also provide parameter names explicitly in your code: 399 | ``` 400 | public User findUserById(@MDCParam("userId") String userId) 401 | ``` 402 | 403 | ## Acknowledgements 404 | 405 | 406 | ### Configuring Logback for JSON output 407 | 408 | Add these dependencies to your pom.xml: 409 | ```xml 410 | 411 | 412 | ch.qos.logback.contrib 413 | logback-json-classic 414 | 0.1.5 415 | 416 | 417 | ch.qos.logback.contrib 418 | logback-jackson 419 | 0.1.5 420 | 421 | 422 | com.fasterxml.jackson.core 423 | jackson-databind 424 | 2.15.1 425 | 426 | 427 | ``` 428 | 429 | Create a new appender or modify an existing one setting JsonLayout: 430 | 431 | ```xml 432 | 433 | 434 | 435 | 437 | true 438 | 439 | yyyy-MM-dd' 'HH:mm:ss.SSS 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | ``` 448 | Now log messages will be written in JSON format that will include all MDC parameters. 449 | 450 | ### Configuring output for Elasticsearch 451 | 452 | Please refer to these resources: 453 | 454 | * [Configuring output for ELK](https://www.elastic.co/guide/en/ecs-logging/java/1.x/setup.html) 455 | * [Adding APM Agent for log correlation](https://www.elastic.co/guide/en/apm/agent/java/master/log-correlation.html) 456 | 457 | 458 | 459 | 460 | ## Roadmap 461 | 462 | - [ ] Use low-level AspectJ load-time weaving instead of Spring AOP 463 | - [ ] Make the library working with annotated interfaces 464 | - [ ] Save and restore current MDC parameters to raw Map 465 | - [ ] Intercept @Async calls maintaining the same MDC 466 | - [ ] Spring WebFlux support? 467 | - [ ] CDI & JakartaEE support? 468 | - [ ] Add jboss-log-manager support. 469 | - [ ] Future research: 470 | - [ ] Annotation-processor based compile-time code enhancement 471 | - [ ] Agent-based runtime class transformations 472 | 473 |

(back to top)

474 | 475 | 476 | 477 | ## License 478 | 479 | Distributed under the Apache Version 2.0 License. See the license file `LICENSE.md` for more information. 480 | 481 |

(back to top)

482 | --------------------------------------------------------------------------------