paint-brush
Let Us Design A Logging libraryby@infinity
165 reads

Let Us Design A Logging library

by Rishabh AgarwalOctober 11th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Monitoring the health and performance of a live system is one of the most challenging task for its maintainers. Teams need a way to monitor everything that is going on and fix issues as soon as they arrive. This is exactly where a Logging Service comes to play. It acts as the eyes and ears of your system, recording crucial information about its operations.
featured image - Let Us Design A Logging library
Rishabh Agarwal HackerNoon profile picture


Monitoring the health and performance of a live system is one of the most challenging task for its maintainers. Teams need a way to monitor everything that is going on and fix issues as soon as they arrive. This is exactly where a Logging Service comes to play. It acts as the eyes and ears of your system, recording crucial information about its operations.


Developers can scan through the generated logs and potentially find cause for bugs and failures. In today’s era, it is not possible to operate any large scale system without proper logging!


In this article, we’ll explore how to design a logging library tailored for high-volume, concurrent applications. We’ll take an iterative approach, gradually building and refining our solutions to make them more effective with each step. Ready to dive in? Let’s get started!

Do we even need a library for logging?

Let us first address the elephant of the room i.e.,


Why do we need a logging library? Isn’t logging something I can just handle with a few print statements or built-in functions?


While this indeed is the case for small projects, it really gets complex when developing for large, high-traffic applications. Here’s why a dedicated logging library really does pay off ~


  • Uniformity and Structure: A logging library will help you in having a consistent format of your logs within the whole application. Added as an advantage to this consistency is better readability, search, and analysis of logs in case there are problems or when reviewing the performance of a system.
  • Advanced feature: A number of advanced features are also available in most libraries, including log levels such as INFO, WARN, ERROR, and more, and log rotation and asynchronous logging. These enhance better management and organization of log data and might help your application’s performance.
  • Centralized Configuration: Unlike disseminating logging configuration throughout the code, a library allows configuration of all logging settings in one place. That makes central configuration easier to change log levels, formats, and outputs without changing code segments individually.
  • Integration and Extensibility: Most logging libraries provide support for integrability with other tools and services, such as monitoring systems or alerting services. They also provide for customizations and extensions in many cases so that one may establish logging behavior tailored to specific needs.
  • Performance Considerations: It helps avoid the performance bottlenecks created by handling logging manually and, hence, the high-volume scenarios are particularly impacted. Basically, a good logging library is performance-optimized; it reduces the effect of logging operations on the general speed and responsiveness of your application.
  • Error Handling and Recovery: Logging libraries are often designed with error handling mechanisms so that problems with the logging do not bring down an application. In fact, such reliability will turn out to be of great importance in keeping up with the general stability of your system.


In other words, while the basic logging may suffice for small projects or simple prototypes, there are much more compelling reasons to use a logging library in terms of functionality, performance, and manageability. This, then, makes it very worth your time for an application that needs a reliable and scalable logging solution.

Logging Library — 1st Version

In this first implementation of our logging library, we will consider one of the critical performance considerations ~


Logging, if done directly on an application’s main thread, can cause bottlenecks, which in turn slow down your application.


To resolve this, we would design the logging library so that it keeps a separate message queue for log messages. Then, we will have a different thread within the logging library process these messages and write them to the log, all while keeping the main thread of the application responsive.


Here is how such a library can be implemented ~


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class AsyncLogger {
    private static final long CAPACITY = 1000;
    private final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(CAPACITY);
    private final LoggerThread loggerThread = new LoggerThread();

    public AsyncLogger() {
        loggerThread.start();
    }

    public void log(String message) {
        try {
            logQueue.put(message);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Failed to log message: " + e.getMessage());
        }
    }

    private class LoggerThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    String message = logQueue.take();
                    System.out.println(message); // Replace with actual logging to file or other destinations
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.err.println("Logger thread interrupted: " + e.getMessage());
                }
            }
        }
    }
}


Voila! With this we have created the first versions of our logging library. But we are still not done, there are a number of improvements we can make on top of it.


First of all, we need a way to terminate the logger so that it does not prevent the JVM from shutting down in production. Stopping the logger thread is easy since our library makes use of BlockingQueue which is responsive to interruptions. So, whenever an interruption is received, our logger will exit on the very next take call.


Not familiar with interruptions? Here is a blog that can help you get going!


There is, however, a problem with the way our logger currently handles interruption. There could still be messages inside the logQueue when an interruption is received, and an abrupt shutdown would cause all of those messages to be lost even when the client would expect those messages to be already added to the log!


Additionally, it is possible that the client threads are blocked on the logmethod call because there being CAPACITY number of messages in the logQueue. In such a case, interruption sent to logger would not cause the blocked client threads to resume!


Think of shutting down the logger like ending a typical Producer-Consumer process. You need to stop both the consumer and the producer. In our case, the logger thread acts as the consumer, while the clients that send log messages are the producers. Interrupting the logger thread stops the consumer, but because clients are not separate threads, managing their cancellation is more complex.

Logging Library — 2nd Version

Another way of implementing shutdown is to keep a flag inside the logger library. Whenever a shutdown is needed, the flag would be set to true and clients should check the flag before trying to submit message to the queue. This is how the log method might look if we modify it to use the flag.


public void log(String msg) throws InterruptedException {
    if (!shutdownRequested)
        queue.put(msg);
    else
        throw new IllegalStateException("logger is shut down");
}


Unfortunately, this approach introduces a potential race condition in our library. A client might check that a shutdown has not been requested, but by the time it attempts to submit a message to the queue, the logger thread could have already exited.


A way to fix the race condition is to make submission of log messages atomic. Note that we don’t want to hold a lock when enqueuing a message to logQueue since put can block. Instead, we can atomically check for shutdown and conditionally increment a counter to “reserve” the right to submit a message.


This is how the logger library would now look like ~


public class AsyncLogger {
    private static final long CAPACITY = 1000;
    private final BlockingQueue <String> logQueue = new LinkedBlockingQueue<>(CAPACITY);
    private final LoggerThread loggerThread = new LoggerThread();
    private final PrintWriter writer;
    @GuardedBy("this") private boolean isShutdown;
    @GuardedBy("this") private int reservations;

    public AsyncLogger(final PrintWriter printWriter) {
      this.printWriter = printWriter;
      loggerThread.start();
    }  

    // Method to be called when logger service need to be turned off
    public void stop() {
        synchronized(this) {
            isShutdown = true;
        }
        loggerThread.interrupt();
    }

    public void log(String msg) throws InterruptedException {
        synchronized(this) {
            if (isShutdown)
                throw new IllegalStateException(...);
            ++reservations; // Increasing reservations
        }
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        public void run() {
            try {
                while (true) {
                    try {
                        synchronized(this) {
                            if (isShutdown && reservations == 0)
                                break;
                        }
                        String msg = queue.take();
                        synchronized(this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) { /*  retry  */ }
                }
            } finally {
                writer.close();
            }
        }
    }
}


In this way, most of the issues relating to logger shutdown are resolved.


Also, note that we have replaced System.out.println calls with a call to PrintWriter#println method. The PrintWriter class abstracts out the actual logging logic. There could be several ways to implement this class and one of them could be to print it to standard output!


interface PrintWriter {
  void println(String msg);
  void close();
}

class StandardOutputWriter implements PrintWriter {
  void println(String msg) {
    System.out.println(msg);
  }
  void close() {}
}

public class FileWriter implements PrintWriter {
    private final BufferedWriter writer;

    public FileWriter(String filePath) throws IOException {
        this.writer = new BufferedWriter(new ::java.io.FileWriter(filePath));
    }

    @Override
    public void println(String msg) {
        try {
            writer.write(msg);
            writer.newLine();
            writer.flush(); // Ensure the message is written to the file immediately
        } catch (IOException e) {
            System.err.println("Failed to write message to file: " + e.getMessage());
        }
    }

    public void close() throws IOException {
        writer.close();
    }
}

With this we reach the end of this blog. If you enjoy this read, subscribe and checkout my profile for more such interesting articles!