Java in a variety of log framework, I believe we are not unfamiliar. Log4j/Log4j2 / Logback/jboss logging, and so on, actually these logging framework core structure makes no difference, just on the detail implementation and its performance is different. This article takes you through a step-by-step design of a logging framework from scratch
Output content –LoggingEvent
When it comes to logging frameworks, the most obvious core function that comes to mind is logging. A line of log content should contain at least the following information:
- Log timestamp
- Thread information
- Log name (generally full class name)
- The level of logging
- Log body (what needs to be output, such as info (STR))
To easily manage the output, we now need to create an output class to encapsulate this information:
public class LoggingEvent { public long timestamp; Private int level; // Log level Private Object message; Private String threadName; Private long threadId; // Thread ID private String loggerName; //getter and setters... @Override public String toString() { return "LoggingEvent{" + "timestamp=" + timestamp + ", level=" + level + ", message=" + message + ", thread + threadName + '\'' + ", threadId=" + threadId + ", logger + loggerName + '\'' + '}'; }}Copy the code
Each log is an event-Event, so it is named LoggingEvent
Output component – Appender
Now that you have the output, you need to think about the output. There are many ways to Output: Standard Output/Console, files, mail, even message queues (MQ) and databases.
An Appender module is an Appender module that produces output. The following is an Appender module that produces output.
public interface Appender {
void append(LoggingEvent event);
}
Copy the code
Different methods of output can be implemented by implementing the Appender interface, such as ConsoleAppender – output to the console
public class ConsoleAppender implements Appender { private OutputStream out = System.out; private OutputStream out_err = System.err; @Override public void append(LoggingEvent event) { try { out.write(event.toString().getBytes(encoding)); } catch (IOException e) { e.printStackTrace(); }}}Copy the code
Log Level design-level
The logging framework should also provide the function of logging level. Programs can print logs of different levels and adjust the log level to display logs. Generally, log levels are defined as follows, sorted from left to right
ERROR > WARN > INFO > DEBUG > TRACE
Copy the code
Now create a log level enumeration with only two attributes, a level name and a level value (for comparison purposes)
public enum Level { ERROR(40000, "ERROR"), WARN(30000, "WARN"), INFO(20000, "INFO"), DEBUG(10000, "DEBUG"), TRACE(5000, "TRACE"); private int levelInt; private String levelStr; Level(int i, String s) { levelInt = i; levelStr = s; } public static Level parse(String level) { return valueOf(level.toUpperCase()); } public int toInt() { return levelInt; } public String toString() { return levelStr; } public boolean isGreaterOrEqual(Level level) { return levelInt>=level.toInt(); }}Copy the code
After the log Level definition is complete, replace the log Level in LoggingEvent with this Level enumeration
public class LoggingEvent { public long timestamp; // Log timestamp private Level Level; // New log level private Object message; Private String threadName; Private long threadId; // Thread ID private String loggerName; //getter and setters... }Copy the code
Now the basic output mode and output content have been basically completed, the next step needs to design the entrance of log printing, after all, there is an entrance to print
Log printing entry – Logger
Now consider how to design the log printing entrance. As a log printing entrance, it needs to contain the following core functions:
- Provide the error/warn/info/debug/trace several print method
- Has a name attribute to distinguish between different loggers
- Call the appender to output the log
- Has its own proprietary level (for example, if its level is INFO, only INFO/WARN/ERROR can be output)
Start by simply creating a Logger interface for easy extension
public interface Logger{
void trace(String msg);
void info(String msg);
void debug(String msg);
void warn(String msg);
void error(String msg);
String getName();
}
Copy the code
Create another default Logger implementation class:
public class LogcLogger implements Logger{ private String name; private Appender appender; private Level level = Level.TRACE; // The current Logger level, the default private int effectiveLevelInt; @override public void trace(String MSG) {filterAndLog(level.trace, MSG); } @Override public void info(String msg) { filterAndLog(Level.INFO,msg); } @Override public void debug(String msg) { filterAndLog(Level.DEBUG,msg); } @Override public void warn(String msg) { filterAndLog(Level.WARN,msg); } @Override public void error(String msg) { filterAndLog(Level.ERROR,msg); } /** * filter and output, * @param level Log level * @param MSG Output content */ private void filterAndLog(level level,String MSG){LoggingEvent e = new LoggingEvent(level, msg,getName()); If (level.toint () >= effectiveLevelInt){appender.append(e); appender.append(e); } } @Override public String getName() { return name; } //getters and setters... }Copy the code
Ok, so now you have a very basic logging model that allows you to create loggers and output different levels of logging. Not quite enough, though, and still missing some core features
Log Hierarchy – Hierarchy
When using a logging framework, there is a basic requirement: ** Logs with different package names should be output in different ways, or logs with different package names and classes should be output at different levels. ** For example, I want framework-based DEBUG logs to be output for debugging purposes, while others use INFO levels by default.
And you don’t want to use an Appender every time you create a Logger, which would be unfriendly. It is best to use a global Logger configuration directly, but also to support special loggers that are not sensitive when creating loggers in the program (such as LoggerFactory.getLogger(xxx.class)).
But the existing design above can not meet this demand, need a little modification
Now design a hierarchy, each oneLoggerHas a **Parent Logger, ** infilterAndLogIf you don’t have an Appender of your own, call the parent’s Appender upappnder“, sort of a reverse parent delegate
The **Root Logger in the figure above is the global default Logger, which is the **Parent Logger of all loggers (newly created) by default. ** By default, Root Logger appender and level are used for output when filterAndLog is generated
Now adjust the filterAndLog method to add the logic to call up:
private LogcLogger parent; Private void filterAndLog(Level Level,String MSG){LoggingEvent e = new LoggingEvent(Level,String MSG) msg,getName()); // Loop up to find available loggers and print for (LogcLogger l = this; l ! = null; l = l.parent){ if(l.appender == null){ continue; } if(level.toInt()>effectiveLevelInt){ l.appender.append(e); } break; }}Copy the code
Now, the log level design is complete, but it mentioned above that different package names use different logger configuration. How do package names and logger correspond?
It’s as simple as defining a global Logger for each package name configuration and resolving the package name configuration directly for a different package name
Log context – LoggerContext
Considering that there are some global and Root loggers that need to be referenced by various loggers, we need to design a Logger container to store these loggers
Public class LoggerContext {/** * root logger */ private logger root; Private Map<String, logger > loggerCache = new HashMap<>(); private Map<String, loggerCache = new HashMap<>(); public void addLogger(String name,Logger logger){ loggerCache.put(name,logger); } public void addLogger(Logger logger){ loggerCache.put(logger.getName(),logger); } //getters and setters... }Copy the code
With a container to hold Logger objects, the next step is to create a Logger
Log creation – LoggerFactory
To make it easier to build Logger hierarchies, each time new is not very friendly, now create a LoggerFactory interface
Public interface ILoggerFactory {// Get/create logger by class. > clazz); Logger logger getLogger(String name); // Create logger logger newLogger(String name); }Copy the code
Let’s have another default implementation class
public class StaticLoggerFactory implements ILoggerFactory { private LoggerContext loggerContext; Override public Logger getLogger(Class<? > clazz) { return getLogger(clazz.getName()); } @Override public Logger getLogger(String name) { Logger logger = loggerContext.getLoggerCache().get(name); if(logger == null){ logger = newLogger(name); } return logger; } / create Logger object * * * * matching Logger name, demolition and created after classification of matching (including configuration) Logger * such as the name for the com. Aaa, BBB, CCC. XXService, So the name of the com/com. Aaa/com. The aaa. BBB/com. Aaa. BBB. CCC * logger can be as the parent logger, but need order here, In this case, the com.aaa.bbb. CCC logger will be matched first as its parent. Logger newLogger(String name) {Override public logger newLogger(String name) {Override public logger newLogger(String name) {Override public logger newLogger(String name) {Override public logger newLogger(String name) {Override public logger newLogger(String name) logger = new LogcLogger(); logger.setName(name); Logger parent = null; Parent logger for (int I = name.lastIndexof ("."); i >= 0; i = name.lastIndexOf(".",i-1)) { String parentName = name.substring(0,i); parent = loggerContext.getLoggerCache().get(parentName); if(parent ! = null){ break; } } if(parent == null){ parent = loggerContext.getRoot(); } logger.setParent(parent); logger.setLoggerContext(loggerContext); return logger; }}Copy the code
Here’s another static factory class for ease of use:
public class LoggerFactory {
private static ILoggerFactory loggerFactory = new StaticLoggerFactory();
public static ILoggerFactory getLoggerFactory(){
return loggerFactory;
}
public static Logger getLogger(Class<?> clazz){
return getLoggerFactory().getLogger(clazz);
}
public static Logger getLogger(String name){
return getLoggerFactory().getLogger(name);
}
}
Copy the code
At this point, all the basic components are complete, and all that remains is assembly
Configuration file Design
The configuration file must provide at least the following configuration functions:
- Configuration Appender
- To configure the Logger
- Configure the Root Logger
Here is an example of a minimum configuration
<configuration>
<appender >
</appender>
<logger >
<appender-ref ref="std_plain"/>
</logger>
<root level="trace">
<appender-ref ref="std_pattern"/>
</root>
</configuration>
Copy the code
In addition to XML configuration, you can also consider adding configuration files in the form of YAML/Properties. Therefore, we need to abstract the function of parsing configuration files and design a configuration interface for parsing configuration files.
public interface Configurator {
void doConfigure();
}
Copy the code
Create a default configuration parser in XML form:
public class XMLConfigurator implements Configurator{ private final LoggerContext loggerContext; public XMLConfigurator(URL url, LoggerContext loggerContext) { this.url = url; // File url this.loggerContext = loggerContext; } @Override public void doConfigure() { try{ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = factory.newDocumentBuilder(); Document document = documentBuilder.parse(url.openStream()); parse(document.getDocumentElement()); . }catch (Exception e){ ... } } private void parse(Element document) throws IllegalAccessException, ClassNotFoundException, InstantiationException { //do parse... }}Copy the code
During parsing, LoggerContext is assembled, and Logger/Root Logger/Appender information is constructed and filled into the LoggerContext
Now you also need an initialized entry to load/parse the configuration file and provide the loaded/parsed global LoggerContext
public class ContextInitializer { final public static String AUTOCONFIG_FILE = "logc.xml"; // The default XML configuration file is used. Final public static String YAML_FILE = "logc.yml"; private static final LoggerContext DEFAULT_LOGGER_CONTEXT = new LoggerContext(); Public static void autoConfig () {URL URL = getConfigURL(); if(url == null){ System.err.println("config[logc.xml or logc.yml] file not found!" ); return ; } String urlString = url.toString(); Configurator configurator = null; if(urlString.endsWith("xml")){ configurator = new XMLConfigurator(url,DEFAULT_LOGGER_CONTEXT); } if(urlString.endsWith("yml")){ configurator = new YAMLConfigurator(url,DEFAULT_LOGGER_CONTEXT); } configurator.doConfigure(); } private static URL getConfigURL(){ URL url = null; ClassLoader classLoader = ContextInitializer.class.getClassLoader(); url = classLoader.getResource(AUTOCONFIG_FILE); if(url ! = null){ return url; } url = classLoader.getResource(YAML_FILE); if(url ! = null){ return url; } return null; } /** * Public static LoggerContext getDefautLoggerContext(){return DEFAULT_LOGGER_CONTEXT; }}Copy the code
LoggerFactory is initialized automatically when loggerFactory. getLogger is initialized automatically to StaticLoggerFactory:
public class StaticLoggerFactory implements ILoggerFactory { private LoggerContext loggerContext; Public StaticLoggerFactory () {/ / construct StaticLoggerFactory, direct call configuration analytical method, and obtain loggerContext ContextInitializer. Autoconfig (); loggerContext = ContextInitializer.getDefautLoggerContext(); }}Copy the code
A logging framework is now almost complete. Although there are still many details not perfect, but the main functions have been included, sparrow is small five organs
The complete code
For ease of reading, some of the code is not posted in this article, you can refer to the complete code: github.com/kongwu-/log…
Original is not easy to reprint, please at the beginning of the famous article source and author. If my article is helpful to you, please click “like” to collect encouragement and support.