visit
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.
Why do we need a logging library? Isn’t logging something I can just handle with a few print statements or built-in functions?
Logging, if done directly on an application’s main thread, can cause bottlenecks, which in turn slow down your application.
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());
}
}
}
}
}
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 log
method 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!
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");
}
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.
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();
}
}
}
}
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();
}
}