A multi-AI agent platform that helps you level up your development skills and ace your interview preparation to secure your dream job.
Launch Xperto-AILogging is an essential aspect of any software application, providing valuable insights into its behavior, performance, and potential issues. A well-designed logging system can significantly improve debugging, troubleshooting, and monitoring processes. In this blog post, we'll dive deep into creating a robust logging system using Java, exploring best practices and implementation details along the way.
Before we jump into the implementation, let's take a moment to understand why logging is crucial:
A well-designed logging system typically consists of the following components:
Now, let's dive into implementing our custom logging system in Java.
We'll start by creating a simple Logger
class that will serve as the core of our logging system:
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class Logger { private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public enum Level { DEBUG, INFO, WARN, ERROR } private Level level; public Logger(Level level) { this.level = level; } public void log(Level messageLevel, String message) { if (messageLevel.ordinal() >= this.level.ordinal()) { String timestamp = LocalDateTime.now().format(formatter); System.out.println(String.format("[%s] %s: %s", timestamp, messageLevel, message)); } } // Convenience methods for different log levels public void debug(String message) { log(Level.DEBUG, message); } public void info(String message) { log(Level.INFO, message); } public void warn(String message) { log(Level.WARN, message); } public void error(String message) { log(Level.ERROR, message); } }
This basic implementation includes:
To use this logger, you can create an instance and start logging:
public class Main { public static void main(String[] args) { Logger logger = new Logger(Logger.Level.INFO); logger.debug("This is a debug message"); // Won't be logged logger.info("Application started"); logger.warn("Low memory warning"); logger.error("Database connection failed"); } }
Our current implementation only logs to the console. Let's add support for multiple appenders:
import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; public class Logger { // ... (previous code) private List<Appender> appenders = new ArrayList<>(); public void addAppender(Appender appender) { appenders.add(appender); } public void log(Level messageLevel, String message) { if (messageLevel.ordinal() >= this.level.ordinal()) { String timestamp = LocalDateTime.now().format(formatter); String formattedMessage = String.format("[%s] %s: %s", timestamp, messageLevel, message); for (Appender appender : appenders) { appender.append(formattedMessage); } } } // ... (convenience methods) } interface Appender { void append(String message); } class ConsoleAppender implements Appender { @Override public void append(String message) { System.out.println(message); } } class FileAppender implements Appender { private String filename; public FileAppender(String filename) { this.filename = filename; } @Override public void append(String message) { try (PrintWriter out = new PrintWriter(new FileWriter(filename, true))) { out.println(message); } catch (IOException e) { e.printStackTrace(); } } }
Now you can use multiple appenders:
public class Main { public static void main(String[] args) { Logger logger = new Logger(Logger.Level.INFO); logger.addAppender(new ConsoleAppender()); logger.addAppender(new FileAppender("application.log")); logger.info("Application started"); logger.warn("Low memory warning"); logger.error("Database connection failed"); } }
To provide more flexibility in log message formatting, let's add support for layouts:
public class Logger { // ... (previous code) private Layout layout; public void setLayout(Layout layout) { this.layout = layout; } public void log(Level messageLevel, String message) { if (messageLevel.ordinal() >= this.level.ordinal()) { String formattedMessage = layout.format(messageLevel, message); for (Appender appender : appenders) { appender.append(formattedMessage); } } } // ... (convenience methods) } interface Layout { String format(Logger.Level level, String message); } class SimpleLayout implements Layout { @Override public String format(Logger.Level level, String message) { String timestamp = LocalDateTime.now().format(Logger.formatter); return String.format("[%s] %s: %s", timestamp, level, message); } } class JSONLayout implements Layout { @Override public String format(Logger.Level level, String message) { String timestamp = LocalDateTime.now().format(Logger.formatter); return String.format("{\"timestamp\":\"%s\",\"level\":\"%s\",\"message\":\"%s\"}", timestamp, level, message); } }
Usage example:
public class Main { public static void main(String[] args) { Logger logger = new Logger(Logger.Level.INFO); logger.addAppender(new ConsoleAppender()); logger.addAppender(new FileAppender("application.log")); logger.setLayout(new JSONLayout()); logger.info("Application started"); logger.warn("Low memory warning"); logger.error("Database connection failed"); } }
Finally, let's implement filters to provide more granular control over which messages are logged:
public class Logger { // ... (previous code) private List<Filter> filters = new ArrayList<>(); public void addFilter(Filter filter) { filters.add(filter); } public void log(Level messageLevel, String message) { if (messageLevel.ordinal() >= this.level.ordinal()) { for (Filter filter : filters) { if (!filter.isLoggable(messageLevel, message)) { return; } } String formattedMessage = layout.format(messageLevel, message); for (Appender appender : appenders) { appender.append(formattedMessage); } } } // ... (convenience methods) } interface Filter { boolean isLoggable(Logger.Level level, String message); } class LevelRangeFilter implements Filter { private Logger.Level minLevel; private Logger.Level maxLevel; public LevelRangeFilter(Logger.Level minLevel, Logger.Level maxLevel) { this.minLevel = minLevel; this.maxLevel = maxLevel; } @Override public boolean isLoggable(Logger.Level level, String message) { return level.ordinal() >= minLevel.ordinal() && level.ordinal() <= maxLevel.ordinal(); } } class RegexFilter implements Filter { private String regex; public RegexFilter(String regex) { this.regex = regex; } @Override public boolean isLoggable(Logger.Level level, String message) { return message.matches(regex); } }
Now you can use filters to control which messages are logged:
public class Main { public static void main(String[] args) { Logger logger = new Logger(Logger.Level.DEBUG); logger.addAppender(new ConsoleAppender()); logger.setLayout(new SimpleLayout()); logger.addFilter(new LevelRangeFilter(Logger.Level.INFO, Logger.Level.ERROR)); logger.addFilter(new RegexFilter(".*error.*")); logger.debug("This is a debug message"); // Won't be logged due to level filter logger.info("Application started"); // Won't be logged due to regex filter logger.warn("Low memory warning"); // Won't be logged due to regex filter logger.error("Database connection error"); // Will be logged } }
With these components in place, we now have a flexible and extensible logging system that can be easily customized to suit various application needs. This implementation demonstrates the core concepts of a logging system, including log levels, appenders, layouts, and filters.
As you continue to develop your logging system, consider adding more features such as:
Remember that logging is a critical part of any application, and investing time in creating a robust logging system will pay off in improved debugging, monitoring, and maintenance capabilities.
06/11/2024 | System Design
15/11/2024 | System Design
03/11/2024 | System Design
02/10/2024 | System Design
15/09/2024 | System Design
02/10/2024 | System Design
03/11/2024 | System Design
15/11/2024 | System Design
06/11/2024 | System Design
03/11/2024 | System Design
03/11/2024 | System Design
15/11/2024 | System Design