Reputation: 9464
How can I use log4j2 to log messages into a Jenkins pipeline job console output (while the job is running)?
By console output, I mean the log of text outputted from a job typically found:
http://localhost:8080/job/<Job Name>/<Job Run Number>/console
C:\Program Files (x86)\Jenkins\jobs\<Job Name>\builds\<Job Run Number>\log
For example, from a Jenkins pipeline job using shared libraries, a class calling log4j2's Logger.info()
:
package mypackage
import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.LogManager
@Grab(group = "org.apache.logging.log4j", module = "log4j-api", version = "2.8.2")
public class MyJobClass {
// Logger
private static final Logger logger = LogManager.getLogger(MyJobClass.class)
public void execute(def script) { // 'script' here is 'this' from within the pipeline script such as in the shared libraries example.
// This will appear in the job console output.
script.println("foo")
// This will appear in files and stdout as defined in the log4j2 configuration file, but not the job console output.
logger.info("bar")
}
}
Ideally I'd like to be able to, during runtime, set additional log4j2 configuration to add an 'appender' targetting the current running job's console output stream.
One thing I plan on trying is to append to the C:\Program Files (x86)\Jenkins\jobs\<Job Name>\builds\<Job Run Number>\log
file directly from log4j2, which is a configuration I would have to set during runtime. However, I don't know how compatible this will be with Jenkin's console output view, or if Jenkins locks the file during the job execution, or if unknown issues will arise from writing to the file at the same time Jenkins does.
Upvotes: 1
Views: 4496
Reputation: 9464
To get log4j2 to log into the Jenkins job console output I created a Logger wrapper which calls script.echo()
.
Note: this code is in Groovy.
LogManager:
package myApplication.logging
import com.cloudbees.groovy.cps.NonCPS
import my.util.PathUtils
import my.util.StringUtils
import org.apache.logging.log4j.core.Appender
import org.apache.logging.log4j.core.LoggerContext
import org.apache.logging.log4j.core.appender.FileAppender
import org.apache.logging.log4j.core.config.Configuration
import org.apache.logging.log4j.core.impl.Log4jLogEvent
import org.apache.logging.log4j.core.layout.PatternLayout
import org.apache.logging.log4j.message.SimpleMessage
/**
* Log manager.
*/
@Grapes([
@Grab(group = "org.apache.logging.log4j", module = "log4j-api", version = "2.8.2", initClass = true),
@Grab(group = "org.apache.logging.log4j", module = "log4j-core", version = "2.8.2", initClass = true),
@Grab(group = "org.apache.logging.log4j", module = "log4j-web", version = "2.8.2", initClass = true)
])
public class LogManager {
/** The script object. */
private static def script
/** Initialised flag. */
private static boolean initialised = false
/** Layout object containing the log format string. */
private static PatternLayout layout
/** Jenkins job console output log level. */
private static Level logLevel = Level.ALL
/**
* Initialise the logger with the script object.
* This allows loading of the log4j settings file and adds the Jenkins job console output appender.
* Called in JeevesJobTemplate.vm and BuildMyJobsJeeves.
*
* @param script The script object.
* @param logLevel Jenkins job console output log level.
*/
@NonCPS
public static void initialise(def script, Level logLevel) {
if (!script) throw new IllegalArgumentException("script object cannot be null.")
if (initialised) throw new IllegalStateException("LogManager.initialise() was called more than once.")
this.script = script
this.logLevel = logLevel
// Deal with the 'WARN Unable to instantiate org.fusesource.jansi.WindowsAnsiOutputStream' message.
System.setProperty("log4j.skipJansi", "true")
final LoggerContext context = LoggerContext.getContext(false)
// Set the configuration file.
context.setConfigLocation(new File("${PathUtils.getResourcePath(script)}/log4j2.json").toURI())
final Configuration configuration = context.configuration
// Get 'logFormat' property from the log4j2.json configuration file.
final String logFormat = configuration.getStrSubstitutor().getVariableResolver().lookup("logFormat")
layout = PatternLayout.newBuilder().withPattern(logFormat).build()
// Add job file appender.
final Appender jobFileAppender = FileAppender.newBuilder()
.withName("Job File")
.withFileName("${PathUtils.getJobPath(script)}/Jeeves.log")
.withLayout(layout)
.build()
addAppender(configuration, jobFileAppender)
// Remove 'Console' appender because Logger will log to the Jenkins job console.
configuration.rootLogger.removeAppender("Console")
initialised = true
}
/**
* Helper method to get a Logger without having to import or grab grapes.
*
* @param clazz Class to log data from.
* @return Log4j2 Logger object.
*/
@NonCPS
public static Logger getLogger(Class<?> clazz) {
if (!clazz) throw new IllegalArgumentException("clazz cannot be null.")
return new Logger(clazz)
}
/**
* Log a copy of a log4j message to the Jenkins job console.
*
* @param loggerName Name of the logger, typically the class from which the logger was initialised.
* @param level Log level.
* @param message Message to log.
*/
@NonCPS
public static void log(String loggerName, Level level, String message) {
if (!initialised) throw new IllegalStateException("LogManager is not initialised.")
if (level <= logLevel) {
final Log4jLogEvent event = Log4jLogEvent.newBuilder().setLoggerName(loggerName).setLevel(level.toLog4jLevel()).setMessage(new SimpleMessage(message)).build()
final String logMessage = layout.toSerializable(event)
script.echo(logMessage.substring(0, logMessage.length() - StringUtils.LINE_SEPARATOR.length()))
}
}
/**
* Add appender to log4j2 configuration.
*
* @param configuration Log4j2 configuration object.
* @param appender Log4j2 appender to add to the configuration.
*/
@NonCPS
private static void addAppender(Configuration configuration, Appender appender) {
if (!configuration) throw new IllegalArgumentException("configuration cannot be null.")
if (!appender) throw new IllegalArgumentException("appender cannot be null.")
appender.start()
configuration.addAppender(appender)
configuration.rootLogger.addAppender(appender, null, null)
}
}
Logger:
package myApplication.logging
import com.cloudbees.groovy.cps.NonCPS
/**
* Logger wrapper for log4j2's Logger class.
* Needed to populate the Jenkins job console output.
*/
public class Logger implements Serializable {
/** Log4j2 Logger object. */
private org.apache.logging.log4j.Logger logger
/** Logger constructor. */
public Logger(Class<?> clazz) {
logger = org.apache.logging.log4j.LogManager.getLogger(clazz)
}
/**
* Log debug level message.
* @param message Message to log.
*/
@NonCPS
public void debug(String message) {
logger.debug(message)
LogManager.log(logger.name, Level.DEBUG, message)
}
/**
* Log error level message.
* @param message Message to log.
*/
@NonCPS
public void error(String message) {
logger.error(message)
LogManager.log(logger.name, Level.ERROR, message)
}
/**
* Log fatal level message.
* @param message Message to log.
*/
@NonCPS
public void fatal(String message) {
logger.fatal(message)
LogManager.log(logger.name, Level.FATAL, message)
}
/**
* Log info level message.
* @param message Message to log.
*/
@NonCPS
public void info(String message) {
logger.info(message)
LogManager.log(logger.name, Level.INFO, message)
}
/**
* Log a message at the supplied level.
* @param level Level to log the message with.
* @param message Message to log.
*/
@NonCPS
public void log(Level level, String message) {
logger.log(level.toLog4jLevel(), message)
LogManager.log(logger.name, level, message)
}
/**
* Log trace level message.
* @param message Message to log.
*/
@NonCPS
public void trace(String message) {
logger.trace(message)
LogManager.log(logger.name, Level.TRACE, message)
}
/**
* Log warn level message.
* @param message Message to log.
*/
@NonCPS
public void warn(String message) {
logger.warn(message)
LogManager.log(logger.name, Level.WARN, message)
}
}
Level:
package my.logging
import com.cloudbees.groovy.cps.NonCPS
import org.apache.logging.log4j.Level as Log4jLevel
/**
* Log levels.
* Do not change the order of the enumeration elements.
*/
public enum Level implements Serializable {
OFF(Log4jLevel.OFF),
FATAL(Log4jLevel.FATAL),
ERROR(Log4jLevel.ERROR),
WARN(Log4jLevel.WARN),
INFO(Log4jLevel.INFO),
DEBUG(Log4jLevel.DEBUG),
TRACE(Log4jLevel.TRACE),
ALL(Log4jLevel.ALL)
private final Log4jLevel level
/**
* Level constructor.
* @param level Log4j level.
*/
Level(Log4jLevel level) {
this.level = level
}
/**
* Get equivalent Log4j level.
* @return Equivalent Log4j level.
*/
@NonCPS
public Log4jLevel toLog4jLevel() {
return level
}
}
Then, during initialisation, call LogManager.initialisation(script, Level.DEBUG)
or whatever your Jenkins output log level should be.
Upvotes: 1