Linkage Error Loader Constraint Violation - JUnit test case development issue
Its good to write unit tests cases, and this part is mostly forgotten by…
September 28, 2022
Java log4j has many ways to initialize and append the desired layout. In this post, I will create a custom logger, with following functionalities:
There are two ways to use JSON logging with log4j:
We will see both of these in this post.
This is the preferred way of using json logs. Lets look at the code:
Below code is complete java code for customized logger with all mentioned features.
package com.gyanblog.logger;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.message.ObjectMessage;
public class MyLogger {
public enum LogLevel {
DEBUG, INFO, WARN, ERROR, FATAL;
}
public final static String LOG_LEVEL = "log.level";
public final static String LOG4J_XML_PATH = "log4j.xml.path";
private static Logger rawLogger;
// for setting global key-value pairs
private static HashMap<String, String> keyValMap;
public static void logInfo(String message) {
logHelper(LogLevel.INFO, message, null, null);
}
public static void logInfo(String message, Map<String, String> additionKeyValuePairs) {
logHelper(LogLevel.INFO, message, additionKeyValuePairs, null);
}
public static void logError(String message) {
logHelper(LogLevel.ERROR, message, null, null);
}
public static void logError(String message, Map<String, String> additionKeyValuePairs) {
logHelper(LogLevel.ERROR, message, additionKeyValuePairs, null);
}
public static void logError(String message, Map<String, String> additionKeyValuePairs, Exception exc) {
logHelper(LogLevel.ERROR, message, additionKeyValuePairs, exc);
}
public static void init(String loggerName) {
if (MyLogger.isLoggerInitialized()) {
return;
}
String log4jxml = getPathLog4j();
if (log4jxml == null) {
MyLogger.rawLogger = LogManager.getLogger(loggerName);
//set default appender
setConsoleAppender();
}
else {
//setting standard log4j property so that logger can read from this xml
System.setProperty("log4j2.configurationFile", log4jxml);
MyLogger.rawLogger = LogManager.getLogger(loggerName);
}
}
public static void setGlobalKeyValueMap(Map<String, String> keyValueMap) {
if (MyLogger.keyValMap == null) {
MyLogger.keyValMap = new HashMap<>();
}
MyLogger.keyValMap.putAll(keyValueMap);
}
public static void addGlobalKeyValue(String key, String value) {
if (MyLogger.keyValMap == null) {
MyLogger.keyValMap = new HashMap<>();
}
MyLogger.keyValMap.put(key, value);
}
private static void logHelper(
LogLevel logLevel, String message,
Map<String, String> additionKeyValuePairs,
Exception exc) {
if (!MyLogger.isLoggerInitialized()) {
MyLogger.init("MyLogger");
}
Map<String, String> map = new HashMap<>();
if (MyLogger.keyValMap != null) {
map.putAll(MyLogger.keyValMap);
}
if (additionKeyValuePairs != null) {
map.putAll(additionKeyValuePairs);
}
if (exc != null) {
map.put("Stacktrace", ExceptionUtils.getFullStackTrace(exc));
}
// now put actual log message
map.put("message", message);
switch (logLevel) {
case DEBUG:
MyLogger.rawLogger.debug(new ObjectMessage(map));
break;
case ERROR:
MyLogger.rawLogger.error(new ObjectMessage(map));
break;
case FATAL:
MyLogger.rawLogger.fatal(new ObjectMessage(map));
break;
case INFO:
MyLogger.rawLogger.info(new ObjectMessage(map));
break;
case WARN:
MyLogger.rawLogger.warn(new ObjectMessage(map));
break;
default:
MyLogger.rawLogger.info(new ObjectMessage(map));
}
}
private static void setConsoleAppender(){
if (MyLogger.rawLogger == null) {
return;
}
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(getEnvLogLevel());
// naming the logger configuration
builder.setConfigurationName("DefaultLogger");
// create a console appender
AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE")
.addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
LayoutComponentBuilder jsonLayout = builder.newLayout("JsonTemplateLayout");
appenderBuilder.add(jsonLayout);
RootLoggerComponentBuilder rootLogger = builder.newRootLogger(getEnvLogLevel());
rootLogger.add(builder.newAppenderRef("Console"));
builder.add(appenderBuilder);
builder.add(rootLogger);
Configurator.reconfigure(builder.build());
}
private static Level getEnvLogLevel() {
String logLevel = getPropertyValue(LOG_LEVEL);
if (logLevel != null) {
if (logLevel.toLowerCase().contains("info")) {
return Level.INFO;
}
else if (logLevel.toLowerCase().contains("debug")) {
return Level.DEBUG;
}
else if (logLevel.toLowerCase().contains("warn")) {
return Level.WARN;
}
else if (logLevel.toLowerCase().contains("error")) {
return Level.ERROR;
}
else if (logLevel.toLowerCase().contains("fatal")) {
return Level.FATAL;
}
}
return Level.INFO;
}
private static String getPropertyValue(String key) {
String value = System.getProperty(key);
if (value != null) {
return value;
}
return System.getenv(key);
}
private static boolean isLoggerInitialized() {
return MyLogger.rawLogger != null;
}
private static String getPathLog4j() {
String path = getPropertyValue(LOG4J_XML_PATH);
if (path != null && !"".equals(path.trim())) {
return path.trim();
}
return null;
}
}
The code is very simple and self-explanatory to use. I can include usage for putting custom json keys:
MyLogger.rawLogger.info(
new ObjectMessage(map)
);
The idea is to send a map to logger, not string.
MyLogger.init("mylogger");
MyLogger.logInfo("This is a test log");
MyLogger.addGlobalKeyValue("instanceId", "myContainer-123");
MyLogger.logInfo("This is another test log");
MyLogger.logInfo("This is also another test log", getKeyValueMap("jiraId", "ABC-123", "objId", "xyz-124"));
// code to prepare map from key-value params
public static Map<String, String> getKeyValueMap(String ... keyValuePairs){
Map<String, String> map = new JCLoggerConcurrentHashMap<String, String>();
if (keyValuePairs != null){
int l = keyValuePairs.length;
for(int i=0; i<l; i++){
String key = keyValuePairs[i];
String value = "";
i++;
if (i < l){
value = keyValuePairs[i];
//Will not allow a null key
map.put(key, value);
}
}
}
return map;
}
This will produce following logs:
{"@timestamp":"2022-09-28T13:25:40.358Z","ecs.version":"1.2.0","log.level":"INFO","message":"{message=This is a test log}","process.thread.name":"main","log.logger":"mylogger"}
{"@timestamp":"2022-09-28T13:25:40.360Z","ecs.version":"1.2.0","log.level":"INFO","message":"{message=This is another test log, instanceId=myContainer-123}","process.thread.name":"main","log.logger":"mylogger"}
{"@timestamp":"2022-09-28T13:25:40.361Z","ecs.version":"1.2.0","log.level":"INFO","message":"{objId=xyz-124, instanceId=myContainer-123, message=This is also another test log, jiraId=ABC-123}","process.thread.name":"main","log.logger":"mylogger"}
This is default log format, if you need customization. See next block.
You need to put a json file in classpath, and configure logger to load it from that file. You can write various layout options.
See various options at https://logging.apache.org/log4j/2.x/manual/json-template-layout.html
In setConsoleAppender()
method above, you need to modify it like:
private static void setConsoleAppender(){
if (MyLogger.rawLogger == null) {
return;
}
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(getEnvLogLevel());
// naming the logger configuration
builder.setConfigurationName("DefaultLogger");
// create a console appender
AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE")
.addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
LayoutComponentBuilder jsonLayout = builder.newLayout("JsonTemplateLayout")
.addAttribute("eventTemplateUri", "classpath:layout.json");
appenderBuilder.add(jsonLayout);
RootLoggerComponentBuilder rootLogger = builder.newRootLogger(getEnvLogLevel());
rootLogger.add(builder.newAppenderRef("Console"));
builder.add(appenderBuilder);
builder.add(rootLogger);
Configurator.reconfigure(builder.build());
}
Note the .addAttribute("eventTemplateUri", "classpath:layout.json");
.
And, you need to put this layout.json
in classpath. I have put in src/main/resources
and added this path in classpath.
layout.json
{
"timestamp": {
"$resolver": "timestamp",
"pattern": {
"format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"timeZone": "UTC"
}
},
"level": {
"$resolver": "level",
"field": "name"
},
"message": {
"$resolver": "message",
"stringified": false
},
"labels": {
"$resolver": "mdc",
"flatten": true,
"stringified": true
},
"tags": {
"$resolver": "ndc"
},
"error.type": {
"$resolver": "exception",
"field": "className"
},
"error.message": {
"$resolver": "exception",
"field": "message"
},
"error.stack_trace": {
"$resolver": "exception",
"field": "stackTrace",
"stackTrace": {
"stringified": true
}
}
}
Now, if I run above sample log code. I will get following logs:
{"timestamp":"2022-09-28T13:33:32.927Z","level":"INFO","message":{"message":"This is a test log"}}
{"timestamp":"2022-09-28T13:33:32.930Z","level":"INFO","message":{"message":"This is another test log","instanceId":"myContainer-123"}}
{"timestamp":"2022-09-28T13:33:32.932Z","level":"INFO","message":{"objId":"xyz-124","instanceId":"myContainer-123","message":"This is also another test log","jiraId":"ABC-123"}}
I will include the initialization code from above method, you just need to change this.
private static void setConsoleAppender(){
if (MyLogger.rawLogger == null) {
return;
}
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(getEnvLogLevel());
// naming the logger configuration
builder.setConfigurationName("DefaultLogger");
// create a console appender
AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE")
.addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
LayoutComponentBuilder jsonLayout = builder.newLayout("JsonLayout")
.addAttribute("complete", false)
.addAttribute("compact", false)
.addAttribute("includeTimeMillis", true)
.addAttribute("objectMessageAsJsonObject", true)
.addComponent(appenderBuilder);
appenderBuilder.add(jsonLayout);
RootLoggerComponentBuilder rootLogger = builder.newRootLogger(getEnvLogLevel());
rootLogger.add(builder.newAppenderRef("Console"));
builder.add(appenderBuilder);
builder.add(rootLogger);
Configurator.reconfigure(builder.build());
}
For more options, see https://logging.apache.org/log4j/2.x/manual/layouts.html
This logger is less customizable, and it adds annoying unnecessary fields to logs.
Its very important to add objectMessageAsJsonObject
property. Otherwise, the log message will come as string, not json.
Now, if I run above sample log code. I will get following logs:
2022-09-28 19:05:09,790 main ERROR layout JsonLayout has no parameter that matches element CONSOLE
{
"timeMillis" : 1664372109886,
"thread" : "main",
"level" : "INFO",
"loggerName" : "mylogger",
"message" : {
"message" : "This is a test log"
},
"endOfBatch" : false,
"loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",
"threadId" : 1,
"threadPriority" : 5
}
{
"timeMillis" : 1664372109931,
"thread" : "main",
"level" : "INFO",
"loggerName" : "mylogger",
"message" : {
"message" : "This is another test log",
"instanceId" : "myContainer-123"
},
"endOfBatch" : false,
"loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",
"threadId" : 1,
"threadPriority" : 5
}
{
"timeMillis" : 1664372109932,
"thread" : "main",
"level" : "INFO",
"loggerName" : "mylogger",
"message" : {
"objId" : "xyz-124",
"instanceId" : "myContainer-123",
"message" : "This is also another test log",
"jiraId" : "ABC-123"
},
"endOfBatch" : false,
"loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",
"threadId" : 1,
"threadPriority" : 5
}
Let me know if you face any issue with this.
Its good to write unit tests cases, and this part is mostly forgotten by…
I have a Java project and dependencies are being managed through maven. I have…
Introduction I was trying to integrate Okta with Spring, and when I deploy the…
Introduction On 9th December 2021, an industry-wide vulnerability was discovered…
Suppose you have two lists, and you want Union and Intersection of those two…
Introduction In this post, we will see multiple ways to use annotation…
Introduction In this post we will see following: How to schedule a job on cron…
Introduction There are some cases, where I need another git repository while…
Introduction In this post, we will see how to fetch multiple credentials and…
Introduction I have an automation script, that I want to run on different…
Introduction I had to write a CICD system for one of our project. I had to…
Introduction You have a running kubernetes setup, and have a webservice (exposed…