Skip to content

Log4j2 ThreadContext

Created on May 14, ’23 ・ Updated on May 14, ’23

Source repository: git.sr.ht/~nyg/example-java-log4j

This proof-of-concept shows how to use a Log4j Routing appender to route log events of a service to different files, based on who initially called the service methods, i.e. a JMX call or a Spring scheduler in our case.

The service Service has two methods: doOne and doTwo, which represent two distinct business actions in an application. Each method is called by a scheduler every 15 seconds in SchedulerOne and SchedulerTwo, respectively. Each method can also be executed via a JMX call through the OperationOne and OperationTwo managed resources (e.g. by using VisualVM).

As the methods could be executed asynchronously and/or may take considerable time to execute (in a real application), the logs produced by each method would overlap each other if logged into the same file. This makes it hard to distinguish the execution flow of each method invocation and debug any problems that might arise in a production environment.

ThreadContext

By using the log4j ThreadContext, we can associate multiple key-value pairs to a thread in order to better identify each method invocation. For example:

@ManagedOperation
public void executeOne(String data) {
    ThreadContext.put("caller", "jmx");
    ThreadContext.put("operation", "one");
    ThreadContext.put("uuid", UUID.randomUUID().toString().split("-")[0]);

    log.info("Starting Operation One with: {}", data);
    service.doOne(data);

    ThreadContext.clearMap();
}
@Scheduled(fixedRate = 15_000)
public void schedulerOne() {
    ThreadContext.put("caller", "sched");
    ThreadContext.put("operation", "one");
    ThreadContext.put("uuid", UUID.randomUUID().toString().split("-")[0]);

    log.info("Executing Scheduler One");
    service.doOne("no-data");

    ThreadContext.clearMap();
}

Dynamic filenames

The key-value pairs can be used in the used in the log4j configuration file in the following way:

<Routes pattern="$${ctx:caller}-$${ctx:operation}">
    <Route>
        <File name="File-${ctx:caller}-${ctx:operation}" fileName="logs/example-log4j-${ctx:caller}-${ctx:operation}.log">
            <PatternLayout pattern="${extendedPattern}"/>
        </File>
    </Route>
</Routes>

Using this configuration, when the doOne method is invoked via JMX, the logs are written to a file named example-log4j-jmx-one.log. When it is invoked by the scheduler, the filename is example-log4j-sched-one.log. The same is done for the doTwo method.

See below for the value of the extendedPattern property.

Log format

The key-value pairs can also be used in the PatternLayout, so as to be able to distinguish each log event easily, even if they were written to a single log file:

<Property name="extendedPattern">
    %d{ISO8601} [%c{1.1.1.2.}] [%t] [%X{caller}-%X{operation}-%X{uuid}] [%p] %m%n
</Property>

Example of example-log4j-jmx-one.log:

2023-03-19T20:15:14,605 [e.s.n.ex.lo.ap.jm.OperationOne] [RMI TCP Connection(4)-192.168.1.213] [jmx-one-d5458d68] [INFO] Starting Operation One with: some data
2023-03-19T20:15:14,618 [e.s.n.ex.lo.ap.co.Service] [RMI TCP Connection(4)-192.168.1.213] [jmx-one-d5458d68] [INFO] Executed One with: some data
2023-03-19T20:15:18,618 [e.s.n.ex.lo.ap.co.Service] [Thread-26] [jmx-one-d5458d68] [INFO] This is logged from a separate thread but inherits from the ThreadContext map
2023-03-19T20:15:53,934 [e.s.n.ex.lo.ap.jm.OperationOne] [RMI TCP Connection(4)-192.168.1.213] [jmx-one-e6a8d238] [INFO] Starting Operation One with: some other data
2023-03-19T20:15:53,940 [e.s.n.ex.lo.ap.co.Service] [RMI TCP Connection(4)-192.168.1.213] [jmx-one-e6a8d238] [INFO] Executed One with: some other data
2023-03-19T20:15:57,941 [e.s.n.ex.lo.ap.co.Service] [Thread-31] [jmx-one-e6a8d238] [INFO] This is logged from a separate thread but inherits from the ThreadContext map

Example of example-log4j-sched-two.log:

2023-03-19T20:12:57,851 [e.s.n.ex.lo.ap.sc.SchedulerTwo] [sched-task-2] [sched-two-09f481d1] [INFO] Executing Scheduler Two
2023-03-19T20:12:57,852 [e.s.n.ex.lo.ap.co.Service] [sched-task-2] [sched-two-09f481d1] [INFO] Executed Two with: no-data
2023-03-19T20:13:01,858 [e.s.n.ex.lo.ap.co.Service] [Thread-6] [sched-two-09f481d1] [INFO] This is logged from a separate thread but inherits from the ThreadContext map
2023-03-19T20:13:12,850 [e.s.n.ex.lo.ap.sc.SchedulerTwo] [sched-task-7] [sched-two-bcbd80d6] [INFO] Executing Scheduler Two
2023-03-19T20:13:12,852 [e.s.n.ex.lo.ap.co.Service] [sched-task-7] [sched-two-bcbd80d6] [INFO] Executed Two with: no-data
2023-03-19T20:13:16,856 [e.s.n.ex.lo.ap.co.Service] [Thread-8] [sched-two-bcbd80d6] [INFO] This is logged from a separate thread but inherits from the ThreadContext map

As we can see, each log event contains a string of the following format: [<caller>-<operation-name>-<operation-uuid>], e.g. sched-two-00be3fa0.