The mapped diagnostic context is an under-appreciated feature of logging
libraries. It's essentially a thread-local Map
, to which you
add key-value pairs that provide context about what your program is doing.
Here's a trivial example:
logger.info("program starting"); File file = new File(argv[0]); MDC.put("outputFile", file.getAbsolutePath()); try (FileOutputStream fos = new FileOutputStream(file); OutputStreamWriter osr = new OutputStreamWriter(fos, StandardCharsets.UTF_8); BufferedWriter out = new BufferedWriter(osr)) { logger.info("starting to write"); out.write("hello, world"); out.newLine(); logger.info("finished writing"); } MDC.remove("outputFile"); logger.info("program finished");
When you run this program, using a logging config that outputs the MDC, you'll see something like this:
2022-02-23 07:28:01,673 INFO [main] c.kdgregory.example.Main - - program starting 2022-02-23 07:28:01,677 INFO [main] c.kdgregory.example.Main - outputFile=/tmp/test.txt - starting to write 2022-02-23 07:28:01,677 INFO [main] c.kdgregory.example.Main - outputFile=/tmp/test.txt - finished writing 2022-02-23 07:28:01,677 INFO [main] c.kdgregory.example.Main - - program finished
Web-apps can benefit enormously from the MDC: before processing the request, you extract things like the invoking user, request path, session token, or a unique request ID. This makes it easier to track down problems, because you can filter the log by any of those parameters.
But I've recently been doing a lot of data engineering work: reading files or database tables, performing some transformation on them, and sending them to some destination. These tasks tend to be complex, with multiple steps, and they tend to be performed for multiple files or tables at a time. As with a web-app, it's really nice to add contextual information such as table name and selection criteria to the MDC, and let the logging framework write it with every message.
The problem with the MDC is that every put()
has to be paired with a
remove()
. If you forget to remove a key it will “pollute”
every subsequent log message from that thread, giving you potentially misleading
information.
However, one thing that I noticed is that I tend to set the MDC just before a
try-with-resources block. Or inside a loop that calls one or more functions that could
be wrapped in such a block. This led me to the AutoMDC
class:
logger.info("program starting"); File file = new File(argv[0]); try (AutoMDC mdc = new AutoMDC("outputFile", file.getAbsolutePath()); FileOutputStream fos = new FileOutputStream(file); OutputStreamWriter osr = new OutputStreamWriter(fos, StandardCharsets.UTF_8); BufferedWriter out = new BufferedWriter(osr)) { logger.info("starting to write"); out.write("hello, world"); out.newLine(); logger.info("finished writing"); } logger.info("program finished");
Try-with-resources works with classes that implement java.lang.AutoCloseable
:
an interface that declares the close()
method. My AutoMDC
class
implements this interface, and uses a Set<String>
to keep track of the
keys. It provides two methods (and the “convenience” constructor shown above):
public AutoMDC put(String key, Object value) { keys.add(key); MDC.put(key, String.valueOf(value)); return this; } @Override public void close() throws Exception { for (String key : keys) { MDC.remove(key); } }
That's it: you can add items to the MDC in the try-with-resources initializer, or anywhere
inside the try
block itself, and they'll be removed at the end. One downside
is that the close()
method is called before any catch
or
finally
blocks; if you need to log contextual information in those places,
you'll need to do it manually.
If you'd like to use this in your own programs, I've provided an implementation for Logback as a GitHub gist; the same basic code can be adapted to whichever logging framework you use.
I think the idea of leveraging try-with-resources is a useful one for other situations, especially those where you might otherwise turn to aspect-oriented programming. Plus, it encourages the good habit of wrapping your code in try/catch blocks.
No comments:
Post a Comment