Logging is one of the most important features that is used in any application. Working across corporates, I have noticed one of the least used feature is Mapping Diagnostic Context (MDC). MDC is provided by all logging frameworks for a long time. It provides a very easy way of logging extra information for each session for the application. In this blog we will look at logback, but the same applies to log4j as well.
One of the key advantage of MDC is that it will maintain the context information on a per thread basis. What that means is if you want to log some example for each session (like IP address, logged in user etc.), you will only set it on entry of the session and it will be maintained throughout the lifetime of the session.
For this blog we will create a simple spring boot application, add couple of REST controllers, one of them unsecure and the other one secure. On the secure endpoint, we will also log the logged in user. We can additionally log any other feature that we want – but for this test, this single parameter is sufficient.
We will create a Java project using Maven this time as we are working with logback.
Maven Dependencies
We will start with a parent POM definition.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.10</version> <relativePath /> </parent>
Here are the rest of the dependencies.
<!-- Spring --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- Logger --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> <!-- Swagger --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.7.0</version> </dependency>
Spring Boot Application – Security Chain
We build a REST application using spring boot to test out MDC. We will add security filter chain and use a in-memory user details manager to manage access. To handle access denied errors, we will first define a controller advice for exception handling. Code snippet for this component is provided below,
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AuthenticationException.class }) @ResponseBody public ResponseEntity<String> handleAuthenticationException(Exception ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"status\": \"Access Denied\"}"); } }
Next we define a component that will serve as an entry point for authentication.
@Component("authEntryPoint") public class AuthEntryPoint implements AuthenticationEntryPoint { @Autowired @Qualifier("handlerExceptionResolver") private HandlerExceptionResolver resolver; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { resolver.resolveException(request, response, null, authException); } }
With these out of the way, we can now start creating the web security config definition. We will create a class as @Configuration and @EnableWebSecurity and start defining the required beans (including the filter chain).
We start by loading the custom users. I have kept users as an array for now that looks as follows.
private String[][] allUsers = { {"captain", "Password", "ADMIN"}, {"matey", "Password", "MONITOR"}, {"gunner", "Password", "USER"} };
Now we load them in the following beans,
@Bean public PasswordEncoder passwordEncoder() { final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); return encoder; } @Bean public InMemoryUserDetailsManager customUsersService(final PasswordEncoder passwordEncoder) { final InMemoryUserDetailsManager iudm = new InMemoryUserDetailsManager(); for (final String[] aUser : allUsers) { log.info("Adding user:"); final UserDetails user = User.withUsername(aUser[0]) .password(passwordEncoder.encode(aUser[1])) .roles(aUser[2]) .build(); iudm.createUser(user); } return iudm; }
We defined the user loading part above. Now we will start with security chain.
@Bean public SecurityFilterChain securityFilter(final HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/public/**") .permitAll() .antMatchers("/restricted/**") .authenticated() .and() .httpBasic() .and() .exceptionHandling() .authenticationEntryPoint(authEntryPoint); return http.build(); }
The rule states that /public endpoints will not need authentication. However, if we send /restricted as endpoint, it needs to be authenticated.
Spring Boot Application – Controllers
We will add two different REST controllers to test the two scenarios. The first one defines an open endpoint.
@Slf4j @RestController @RequestMapping(path="/public", produces="application/json") public class HealthController { @RequestMapping(value = "/health", method = RequestMethod.GET) public ResponseEntity<String> getHealth() { MDC.put(StringConstants.LOG_USER, StringConstants.DFLT_USER); log.info("HealthController::getHealth called..."); return ResponseEntity.ok().body("{ \"status\": \"At your Service\"}"); } }
The second one is a restricted endpoint. We will need to send a token for this to succeed.
@Slf4j @RestController @RequestMapping(path="/restricted", produces="application/json") public class SecureController { @RequestMapping(value = "/health", method = RequestMethod.GET) public ResponseEntity<String> getHealth(final Principal principal) { MDC.put(StringConstants.LOG_USER, principal.getName()); log.info("SecureController::getHealth called..."); return ResponseEntity.ok().body("{ \"status\": \"Secure at your Service\"}"); } }
Implement Mapped Diagnostic Context
Let’s start with what changes we need in logback.xml. We will add a session parameter for every MDC value we want to log. In this case, I just added an user value.
<encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{session.user} - %msg%n</pattern> </encoder>
That is the only change. From Java code, we will have to set the MDC session values once for each thread. so, following line adds a mapped diagnostic context to the logger.
MDC.put(StringConstants.LOG_USER, principal.getName());
That’s all to MDC. Now, whenever we log, MDC information will be added to log.
Conclusion
We wanted to have this blog for MDC, but we spent more time in defining the application. That just proves the simplicity of implementing MDC for logging. It is a powerful feature and starting to use it will add additional value for all logging. Ciao for now!