Spring MDC Transaction Logging - Vlad Mihalcea (2024)

Last modified:

Follow @vlad_mihalcea

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see how to configure a Spring application to use MDC (Mapped Diagnostic Context) for transaction logging.

This technique is going to help us inject the Persistence Context information as well as the associated database transaction identifier in all the log entries emitted from within a @Transactional service method.

MDC (Mapped Diagnostic Context)

As I explained in this article, MDC is for application logging what the ThreadLocal context is to Java threads.

In short, MDC allows us to register key/value sets that are bound to a given thread and can be referenced by the logging framework when printing a given log message.

To use MDC, the logging framework needs to reference the key of the MDC key/value set in the log message pattern, as illustrated by the following example:

<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>TRACE</level> </filter> <encoder> <Pattern>%-5p [%t]:%X{txId} %c{1} - %m%n</Pattern> <charset>UTF-8</charset> </encoder></appender>

Once we have the txId MDC variable reference by the Logback message pattern, when running the following code snippet:

try(MDC.MDCCloseable mdc = MDC .putCloseable( "txId", String.format( " Persistence Context Id: [%d], DB Transaction Id: [%s]", 123456, 7890 ) )) { LOGGER.info("Fetch Post by title");}

Logback is going to print the following message:

2022-12-21 14:51:38,823 INFO [main]: Persistence Context Id: [123456], DB Transaction Id: [7890] c.v.b.h.s.t.m.SpringMdcTest - Update Post entity

That’s great, but we need to automate how the Persistence Context identifier and the actual database transaction id are bound to the txId MDC variable.

Spring MDC Transaction Logging

So, to achieve our goal, we need to intercept the following JPA actions:

  • when the JPA Persistence Context is open, we need to assign a Persistence Context identifier
  • after a statement is executed, we check whether the relational database has assigned a transaction identifier
  • after the JPA transaction ends, we remove the transaction identifier
  • after the JPA Persistence Context is closed, we remove the txId variable from the MDC logging context

Luckily, both Spring and Hibernate are extremely customizable, so we can intercept all those actions and apply the MDC transition logging.

First, we need to configure the Spring LocalContainerEntityManagerFactoryBean to use some custom Hibernate settings:

@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); ... entityManagerFactoryBean.setJpaVendorAdapter( new HibernateJpaVendorAdapter() ); entityManagerFactoryBean.setJpaProperties(properties()); return entityManagerFactoryBean;}protected Properties properties() { Properties properties = new Properties(); properties.setProperty( AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, Boolean.TRUE.toString() ); properties.put( AvailableSettings.AUTO_SESSION_EVENTS_LISTENER, TransactionInfoSessionEventListener.class.getName() ); return properties;}

The CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT property allows Hibernate to acquire the database connection lazily, as opposed to when you enter the @Transactional method. For more details about this setting, check out this article.

The AUTO_SESSION_EVENTS_LISTENER setting allows us to provide a SessionEventListener that can intercept various Hibernate actions.

Next, we are going to see how we can intercept Hibernate actions using the awesome SessionEventListener mechanism.

Intercepting Hibernate actions using the SessionEventListener

The TransactionInfoSessionEventListener looks as follows:

public class TransactionInfoSessionEventListener extends BaseSessionEventListener { private final TransactionInfo transactionInfo; private EntityManager entityManager; /** * Executes when a JPA EntityManager is created. */ public TransactionInfoSessionEventListener() { transactionInfo = new TransactionInfo(); } /** * Executes after a JDBC statement was executed. */ @Override public void jdbcExecuteStatementEnd() { if (!transactionInfo.hasTransactionId()) { resolveTransactionId(); } } /** * Executes after the commit or rollback was called * on the JPA EntityTransaction. */ @Override public void transactionCompletion(boolean successful) { transactionInfo.setTransactionId(null); } /** * Executes after JPA EntityManager is closed. */ @Override public void end() { transactionInfo.close(); } private EntityManager getEntityManager() { if (entityManager == null) { entityManager = SpringTransactionUtils.currentEntityManager(); } return entityManager; } private void resolveTransactionId() { EntityManager entityManager = getEntityManager(); SessionFactoryImplementor sessionFactory = entityManager .getEntityManagerFactory() .unwrap(SessionFactoryImplementor.class); entityManager.unwrap(Session.class).doWork(connection -> { try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery( Database.of(sessionFactory.getJdbcServices().getDialect()) .dataSourceProvider() .queries() .transactionId() )) { if (resultSet.next()) { transactionInfo.setTransactionId(resultSet.getString(1)); } } }); }}

A new TransactionInfoSessionEventListener object is created whenever a new JPA EntityManager is instantiated, so we use this opportunity to create a new TransactionInfo object, which we are going to use to hold the current transaction information.

In the jdbcExecuteStatementEnd callback, we check whether the database transaction identifier was previously resolved, and if it was not, we try to resolve it.

In the transactionCompletion callback, we reset the database transaction identifier since the associated database transaction was ended.

And, in the end callback, which is called when the JPA EntityManager is closed, we also close the associated TransactionInfo object.

The TransactionInfo object looks as follows:

class TransactionInfo { private final Long persistenceContextId; private String transactionId; private MDC.MDCCloseable mdc; public TransactionInfo() { this.persistenceContextId = TsidUtils.randomTsid().toLong(); setMdc(); } public boolean hasTransactionId() { return transactionId != null; } public TransactionInfo setTransactionId(String transactionId) { this.transactionId = transactionId; setMdc(); return this; } private void setMdc() { this.mdc = MDC.putCloseable( "txId", String.format( " Persistence Context Id: [%d], DB Transaction Id: [%s]", persistenceContextId, transactionId ) ); } public void close() { if(mdc != null) { mdc.close(); } }}

The persistenceContextId stores the Persistence Context identifier. While we could use the Hibernate Session UUID identifier, I prefer using a time-sorted Tsid instead.

The transactionId stores the database transaction identifier.

The mdc variable references the MDC.MDCCloseable object that we can use to reset the MDC logging variable we bound for the currently executing Thread.

That’s it!

Testing time

Assuming we have the following addComment service method:

@Transactionalpublic void addComment(Long postId, String review) { Post post = postRepository.lockById( postId, LockModeType.OPTIMISTIC ); postCommentRepository.persist( new PostComment() .setReview(review) .setPost(post) ); postRepository.lockById( postId, LockModeType.PESSIMISTIC_READ );}

When calling the addComment method on PostgreSQL:

forumService.addComment(postId, "Awesome");

The following log messages are printed:

Persistence Context Id: [393424039441824927], DB Transaction Id: [null] o.h.e.t.i.TransactionImpl - beginPersistence Context Id: [393424039441824927], DB Transaction Id: [null] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select p1_0.id,p1_0.slug,p1_0.title,p1_0.version from post p1_0 where p1_0.id=?"], Params:[(1)]Persistence Context Id: [393424039441824927], DB Transaction Id: [null] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["SELECT CAST(pg_current_xact_id_if_assigned() AS text)"], Params:[]Persistence Context Id: [393424039441824927], DB Transaction Id: [null] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select nextval('post_comment_SEQ')"], Params:[()]Persistence Context Id: [393424039441824927], DB Transaction Id: [null] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["SELECT CAST(pg_current_xact_id_if_assigned() AS text)"], Params:[]Persistence Context Id: [393424039441824927], DB Transaction Id: [431894] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select id from post where id =? and version =? for share"], Params:[(1, 0)]Persistence Context Id: [393424039441824927], DB Transaction Id: [431894] o.h.e.t.i.TransactionImpl - committingPersistence Context Id: [393424039441824927], DB Transaction Id: [431894] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:2, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1, Awesome, 1)]Persistence Context Id: [393424039441824927], DB Transaction Id: [431894] n.t.d.l.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select version as version_ from post where id =?"], Params:[(1)]Persistence Context Id: [393424039441824927], DB Transaction Id: [431894] o.h.r.j.i.AbstractLogicalConnectionImplementor - LogicalConnection#beforeTransactionCompletionPersistence Context Id: [393424039441824927], DB Transaction Id: [431894] o.h.r.j.i.AbstractLogicalConnectionImplementor - Preparing to commit transaction via JDBC Connection.commit()Persistence Context Id: [393424039441824927], DB Transaction Id: [431894] o.h.r.j.i.AbstractLogicalConnectionImplementor - Transaction committed via JDBC Connection.commit()

While the Persistence Context identifier is available right from the start, the database transaction id is only assigned after PostgreSQL executes an INSERT, UPDATE or DELETE statement.

Awesome, right?

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Spring MDC Transaction Logging - Vlad Mihalcea (1)Spring MDC Transaction Logging - Vlad Mihalcea (2)Spring MDC Transaction Logging - Vlad Mihalcea (3)

Conclusion

MDC logging is a very useful feature as it allows you to customize your log messages using context-specific information.

While the article shows you how you can add the transaction information to your logs using a Hibernate SessionEventListener, this solution is not limited to Spring. You can also use it with Jakarta EE or Java EE. The only thing you need to do is change the way you are passing the AUTO_SESSION_EVENTS_LISTENER Hibernate property.

Follow @vlad_mihalcea

Spring MDC Transaction Logging - Vlad Mihalcea (4)

Related

Spring MDC Transaction Logging - Vlad Mihalcea (2024)

FAQs

What is MDC logging in Spring Boot? ›

Mapped Diagnostic Context (MDC) is a feature in the Logback framework (which is the default logging framework for Spring Boot) that allows you to store contextual information in a logging message.

Which logger is best for Spring Boot? ›

Spring Boot uses Commons Logging for all internal logging but leaves the underlying log implementation open. Default configurations are provided for Java Util Logging, Log4J2, and Logback. In each case, loggers are pre-configured to use console output with optional file output also available.

How to check logs in Spring Boot application? ›

  1. Now we simply need to run the application and hit http://localhost:8080/log to see the log messages.
  2. Spring boot allows us to see the logs in the console even if we do not provide any specific configuration for it. ...
  3. Alternatively, This can also be done by adding the following properties in the application.
Jan 4, 2024

How to set logging level to debug in Spring Boot? ›

By default ERROR , WARN and INFO level messages are logged. You can also enable a “debug” mode by starting your application with a --debug flag. you can also specify debug=true in your application. properties .

What is an example of MDC? ›

Examples of Major Diagnostic Categories (MDC)

- Primary diagnoses: Stroke, epilepsy, multiple sclerosis, Parkinson's disease.

What is the difference between MDC and marker? ›

Use Logback Markers to add structured data to a log-statement. Just like MDC, Markers is Logbacks way of adding additional data to a log statement. But this only applies to that specific statement, where you usually have more specific data than at places in your code where you do MDC.

Which is the fastest logger in Java? ›

Log4j, Logback, and Log4j2 are good logging frameworks that are broadly used. So which one should you use? I recommend using Log4j2 because it's the fastest and most advanced of the three frameworks. However, bear in mind to use the latest version of this library.

What is Java most popular logger? ›

Because of this convenience and the many advanced features, SLF4j is currently the most popular Java logging framework. Both of these frameworks are easy to use. For logging with SLF4j, you just have to instantiate a new Logger object by calling the getLogger() method.

How to improve logging in Spring Boot? ›

Logback provides several built-in layouts, but you can also create custom layouts if you need a specific format for your logs.
  1. Integrating Logback with Spring Boot. ...
  2. Basic Configuration with logback-spring. ...
  3. File Appender for File-Based Logging. ...
  4. Advanced Configuration. ...
  5. Environment-Specific Configuration. ...
  6. Conditional Logging.
Apr 21, 2024

Why do we use loggers in Spring Boot? ›

Logging in spring boot is basically an API that provides tracing out of information along with a recording of any critical failure that might occur in the application during its run. Spring boot uses a common logging framework to implement all internal logging and leaves the log implementation open.

What are the different logging levels in Spring Boot? ›

Log Level: ERROR , WARN , INFO , DEBUG , or TRACE .

Which is the default logging file in Spring Boot? ›

By default, Spring Boot logs only to the console and does not write log files. If you want to write log files in addition to the console output, you need to set a logging. file or logging. path property (for example, in your application.

What is the difference between log INFO and log debug? ›

The following log levels are common to most programming languages and frameworks: Debug: Used in the development process to show diagnostic information about the application. Info: Provides information about what is part of an application's regular operation, such as a successful system startup or shutdown.

What is SLF4J logger in Spring Boot? ›

Spring Boot, by default, includes the Simple Logging Facade for Java (SLF4J) coupled with Logback. SLF4J acts as an abstraction layer, meaning you can plug in your preferred logging framework at deployment time, while Logback serves as the default implementation.

How to change log level in Spring Boot without restarting? ›

To update the log level for a Spring Boot app running in Cloud Run without restarting the service, even with multiple instances:
  1. Enable the Actuator endpoints in your app.
  2. Use a tool to invoke the loggers endpoint and change the log level.
  3. Propagate the log level change to all instances of the service.

What is the use of MDC? ›

MDC in Log4j allows us to fill a map-like structure with pieces of information that are accessible to the appender when the log message is actually written. The MDC structure is internally attached to the executing thread in the same way a ThreadLocal variable would be.

What is the full form of MDC in Log4j? ›

A Mapped Diagnostic Context, or MDC in short, is an instrument for distinguishing interleaved log output from different sources. Log output is typically interleaved when a server handles multiple clients near-simultaneously. The MDC is managed on a per thread basis.

What is mdc in programming? ›

10.3.

Multiple description coding (MDC) was introduced more than 20 years ago by the information theory communit (Ozarow, 1980). It consists in representing the source by M independent streams, named descriptions.

What is the difference between MDC clear and MDC remove? ›

Only difference in MDC. clear and MDC. remove is: clear will remove all entries from ThreadContextMap (Map per thread MDC keep) remove will remove only one key from ThreadContextMap if it exist.

References

Top Articles
Latest Posts
Article information

Author: Frankie Dare

Last Updated:

Views: 6075

Rating: 4.2 / 5 (73 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Frankie Dare

Birthday: 2000-01-27

Address: Suite 313 45115 Caridad Freeway, Port Barabaraville, MS 66713

Phone: +3769542039359

Job: Sales Manager

Hobby: Baton twirling, Stand-up comedy, Leather crafting, Rugby, tabletop games, Jigsaw puzzles, Air sports

Introduction: My name is Frankie Dare, I am a funny, beautiful, proud, fair, pleasant, cheerful, enthusiastic person who loves writing and wants to share my knowledge and understanding with you.