com.reactific.helpers.LoggingHelper.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015-2017 Reactific Software LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.reactific.helpers
import scala.collection.mutable
import com.typesafe.scalalogging.{Logger => ScalaLogger}
import ch.qos.logback.classic.{Level, LoggerContext, Logger => LogbackLogger}
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.core.{Appender, ConsoleAppender, FileAppender}
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.CyclicBufferAppender
import ch.qos.logback.core.rolling.{
FixedWindowRollingPolicy,
RollingFileAppender,
SizeBasedTriggeringPolicy
}
import ch.qos.logback.classic.html.HTMLLayout
import ch.qos.logback.core.util.FileSize
import java.io.File
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import scala.collection.JavaConverters._
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.matching.Regex
import scala.util.{Failure, Success, Try}
trait LoggingHelper {
import LoggingHelper.ScalaLoggerExtension
protected lazy val log: ScalaLogger = {
val logger = LoggerFactory.getLogger(createLoggerName)
LoggingHelper.configureLogger(logger)
ScalaLogger(logger)
}
protected def createLoggerName: String = {
this.getClass.getName.replace('$', '_')
}
def loggerName: String = log.underlying.getName
def level: Level = {
log.withActual(Level.OFF) { (actual) ⇒
actual.getLevel
}
}
}
/** Log File Related Helpers
*
* This object just provides a variety of utilities for manipulating
* LogBack programatically.
*/
object LoggingHelper extends LoggingHelper {
implicit class ScalaLoggerExtension(scalaLogger: ScalaLogger) {
def actualLogger: Option[LogbackLogger] = {
scalaLogger.underlying match {
case result: LogbackLogger ⇒
Some(result)
case _ ⇒
None
}
}
def withActual[T](default: T)(f: (LogbackLogger) ⇒ T): T = {
scalaLogger.underlying match {
case logger: LogbackLogger ⇒
f(logger)
case _ ⇒
default
}
}
def loggerContext: Option[LoggerContext] = {
withActual(LoggingHelper.rootLoggerContext) { (l) ⇒
Some(l.getLoggerContext)
}
}
/** Determine if a logger has an appender or not
*
* @return true iff the logger has an appender
*/
def hasAppenders: Boolean = {
actualLogger.map(_.iteratorForAppenders().hasNext).orElse(Some(false)).get
}
def setAppender(appender: Appender[ILoggingEvent]): Unit = {
withActual(()) {
case l: LogbackLogger ⇒
l.detachAndStopAllAppenders()
l.addAppender(appender)
l.setLevel(Level.INFO)
l.setAdditive(false);
case _ ⇒
()
}
}
def setToError(): Unit = {
val _ = LoggingHelper
.setLoggingLevel(scalaLogger.underlying.getName, Level.ERROR)
}
def setToWarn(): Unit = {
val _ = LoggingHelper
.setLoggingLevel(scalaLogger.underlying.getName, Level.WARN)
}
def setToInfo(): Unit = {
val _ = LoggingHelper
.setLoggingLevel(scalaLogger.underlying.getName, Level.INFO)
}
def setToDebug(): Unit = {
val _ = LoggingHelper
.setLoggingLevel(scalaLogger.underlying.getName, Level.DEBUG)
}
def setToTrace(): Unit = {
val _ = LoggingHelper
.setLoggingLevel(scalaLogger.underlying.getName, Level.TRACE)
}
}
/** Easy access to the root logger */
val rootLogger: ScalaLogger = {
ScalaLogger(LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME))
}
/** Easy access to the logger context */
def rootLoggerContext: Option[LoggerContext] = {
rootLogger.underlying match {
case result: LogbackLogger ⇒
Some(result.getLoggerContext)
case _ =>
None
}
}
def waitForRootLoggerContext(
attempts: Long = 40L,
sleepTime: FiniteDuration = 50.milliseconds
)(implicit ec: ExecutionContext
): Future[LoggerContext] = Future {
for { _ ← 1L to attempts if rootLoggerContext.isEmpty } {
Thread.sleep(sleepTime.toMillis)
}
rootLoggerContext.getOrElse {
throw new IllegalStateException(
"LogBack Root LoggerContext failed to activate in " +
DateTimeHelpers.makeReadable(sleepTime * attempts)
)
}
}
val levelPatterns = mutable.Map.empty[Regex, Level]
/** Set the logger's level per requested patterns */
def configureLogger(logger: Logger): Unit = {
val name = logger.getName
for {
(regex, level) ← levelPatterns if regex.findFirstMatchIn(name).isDefined
} {
setLoggingLevel(name, level)
()
}
}
/** Set a component to ERROR logging level */
def setToError(component: LoggingHelper): Seq[String] =
setLoggingLevel(component.loggerName, Level.ERROR)
def setToError(pkg: String): Seq[String] =
setLoggingLevel(pkg, Level.ERROR)
/** Set a component to WARN logging level */
def setToWarn(component: LoggingHelper): Seq[String] =
setLoggingLevel(component.loggerName, Level.WARN)
def setToWarn(pkg: String): Seq[String] =
setLoggingLevel(pkg, Level.WARN)
/** Set a component to WARN logging level */
def setToInfo(component: LoggingHelper): Seq[String] =
setLoggingLevel(component.loggerName, Level.INFO)
def setToInfo(pkg: String): Seq[String] =
setLoggingLevel(pkg, Level.INFO)
/** Set a component to WARN logging level */
def setToDebug(component: LoggingHelper): Seq[String] =
setLoggingLevel(component.loggerName, Level.DEBUG)
def setToDebug(pkg: String): Seq[String] =
setLoggingLevel(pkg, Level.DEBUG)
/** Set a component to WARN logging level */
def setToTrace(component: LoggingHelper): Seq[String] =
setLoggingLevel(component.loggerName, Level.TRACE)
def setToTrace(pkg: String): Seq[String] =
setLoggingLevel(pkg, Level.TRACE)
/** Set Logging Level Generically.
* This function sets the logging level for any pkg that matches a regular
* expression. This allows a variety of loggers to be set without knowing
* their full names explicitly.
*
* @param regex A Scala regular expression string for the names of the
* loggers to match
* @param level The level you want any matching loggers to be set to.
* @param forFuture When true, the regex is saved and applied to future
* instantiations of loggers, too.
* @return A list of the names of the loggers whose levels were set
*/
def setLoggingLevel(
regex: String,
level: Level,
forFuture: Boolean = false
): Seq[String] = {
if (forFuture) {
for {
(r, _) <- levelPatterns.filter {
case (r, _) ⇒ r.pattern.pattern == regex
}
} {
levelPatterns.remove(r)
}
levelPatterns.put(new Regex(regex), level)
}
for { logger ← findLoggers(regex) } yield {
val previousLevel: Level = logger.getLevel
logger.setLevel(level)
log.trace(
"Switched Logging Level For '" + logger.getName + "' from " +
previousLevel + " to " + level
)
logger.getName
}
}
/** Clear the remembered patterns
* Patterns set with the forFuture flag set to true are remembers for
* subsequent loggers. This call causes all such remembered patterns to be
* forgotten. No existing loggers are affected.
*/
def forgetLoggingLevels(): Unit = {
levelPatterns.clear
}
def getLoggingLevel(name: String): Level = {
findLogger(name) match {
case Some(lggr) ⇒
lggr.getEffectiveLevel
case None ⇒
Level.OFF
}
}
/** Find loggers matching a pattern
*
* @param pattern A Scala regular expression string for the names of
* the loggers to match
* @return A sequence of the matching loggers
*/
def findLoggers(pattern: String): Seq[LogbackLogger] = {
rootLoggerContext match {
case Some(rlc) =>
val regex = new Regex(pattern)
for {
log ← rlc.getLoggerList.asScala
if regex.findFirstIn(log.getName).isDefined
} yield {
log
}
case None =>
Seq.empty[LogbackLogger]
}
}
def findLogger(name: String): Option[LogbackLogger] = {
rootLoggerContext.flatMap { rlc: LoggerContext =>
rlc.getLoggerList.asScala.find { log =>
log.getName == name
}
}
}
/** Determine if a logger has an appender or not
*
* @param logger The logger to check
* @return true iff the logger has an appender
*/
def hasAppenders(logger: LogbackLogger): Boolean = {
logger.iteratorForAppenders().hasNext
}
def getLoggingTableData: (Iterable[String], Iterable[Iterable[String]]) = {
rootLoggerContext match {
case Some(rlc: LoggerContext) =>
val data = for {
log ← rlc.getLoggerList.asScala
} yield {
List(
log.getName,
log.getEffectiveLevel.toString,
log.getLoggerContext.getName
)
}
List("Name", "Level", "Context") -> data
case None =>
List.empty[String] -> List.empty[List[String]]
}
}
def getLoggingConfig: List[(String, String)] = {
rootLoggerContext match {
case Some(rlc: LoggerContext) => {
for {
log ← rlc.getLoggerList.asScala
} yield {
log.getName -> log.getEffectiveLevel.toString
}
}.toList
case None =>
List.empty[(String, String)]
}
}
def removeAppender(name: String): Unit = {
rootLogger.actualLogger match {
case Some(logger) ⇒
val existingAppender = logger.getAppender(name)
if (existingAppender != null) {
existingAppender.stop()
val _ = logger.detachAppender(existingAppender)
}
case None ⇒
()
}
}
private val FILE_PATTERN =
"%d %-7relative %-5level [%thread:%logger{30}] - %msg%n%xException"
private val CONSOLE_PATTERN =
"%date %-5level %logger{30} - %message%n%xException"
private def makeEncoder(pattern: String, lc: LoggerContext) = {
val ple = new PatternLayoutEncoder()
ple.setPattern(pattern)
ple.setOutputPatternAsHeader(false)
ple.setContext(lc)
ple.start()
ple
}
private def setRollingPolicy(
fwrp: FixedWindowRollingPolicy,
maxFiles: Int,
fName: String
): Unit = {
fwrp.setMaxIndex(maxFiles)
fwrp.setMinIndex(1)
fwrp.setFileNamePattern(fName + ".%i.zip")
}
private def makeRollingPolicy(
lc: LoggerContext,
maxFiles: Int,
fName: String,
appender: FileAppender[_]
): FixedWindowRollingPolicy = {
val fwrp = new FixedWindowRollingPolicy
setRollingPolicy(fwrp, maxFiles, fName)
fwrp.setContext(lc)
fwrp.setParent(appender)
fwrp.start()
fwrp
}
private def setTriggeringPolicy(
sbtp: SizeBasedTriggeringPolicy[ILoggingEvent],
maxSize: Int
): Unit = {
sbtp.setMaxFileSize(FileSize.valueOf(maxSize + "MB"))
}
private def makeTriggeringPolicy(
lc: LoggerContext,
maxSize: Int
): SizeBasedTriggeringPolicy[ILoggingEvent] = {
val sbtp = new SizeBasedTriggeringPolicy[ILoggingEvent]
setTriggeringPolicy(sbtp, maxSize)
sbtp.setContext(lc)
sbtp.start()
sbtp
}
val FILE_APPENDER_NAME = "FILE"
val PAGE_APPENDER_NAME = "PAGE"
val STDOUT_APPENDER_NAME = "STDOUT"
def makeRollingFileAppender(
file: File,
maxFiles: Int,
maxFileSizeInMB: Int,
immediateFlush: Boolean,
name: String
): RollingFileAppender[ILoggingEvent] = {
val lc = rootLoggerContext.get
val fName = file.getCanonicalPath
val rfa = new RollingFileAppender[ILoggingEvent]
rfa.setContext(lc)
rfa.setAppend(true)
rfa.setName(name)
rfa.setFile(fName)
rfa.setImmediateFlush(immediateFlush)
rfa.setEncoder(makeEncoder(FILE_PATTERN, lc))
rfa.setRollingPolicy(makeRollingPolicy(lc, maxFiles, fName, rfa))
rfa.setTriggeringPolicy(makeTriggeringPolicy(lc, maxFileSizeInMB))
rfa.start()
rfa
}
val nullValue: Null = None.orNull
def setFileAppender(
file: File,
maxFiles: Int,
maxFileSizeInMB: Int,
immediateFlush: Boolean,
name: String = FILE_APPENDER_NAME
): Option[FileAppender[_]] =
Try {
val lc = rootLoggerContext.get
val fName = file.getCanonicalPath
rootLogger.withActual[RollingFileAppender[ILoggingEvent]](nullValue) {
(logger) ⇒
logger.getAppender(name) match {
case rfa: RollingFileAppender[ILoggingEvent] ⇒
rfa.getRollingPolicy match {
case fwrp: FixedWindowRollingPolicy ⇒
setRollingPolicy(fwrp, maxFiles, fName)
case _ ⇒
rfa.setRollingPolicy(
makeRollingPolicy(lc, maxFiles, fName, rfa)
)
}
rfa.getTriggeringPolicy match {
case sbtp: SizeBasedTriggeringPolicy[ILoggingEvent] ⇒
setTriggeringPolicy(sbtp, maxFileSizeInMB)
case _ ⇒
rfa.setTriggeringPolicy(
makeTriggeringPolicy(lc, maxFileSizeInMB)
)
}
rfa.setImmediateFlush(immediateFlush)
rfa.getEncoder match {
case _: PatternLayoutEncoder ⇒
// Already set
case _ ⇒
rfa.setEncoder(makeEncoder(FILE_PATTERN, lc))
}
rfa
case _ ⇒
val rfa = makeRollingFileAppender(
file,
maxFiles,
maxFileSizeInMB,
immediateFlush,
name
)
logger.addAppender(rfa)
rfa
}
}
} match {
case Success(fa) ⇒
Some(fa)
case Failure(xcptn) ⇒
log.error("Failed to set RollingFileAppender: ", xcptn)
None
}
var pageAppender: // scalastyle:ignore
Option[CyclicBufferAppender[ILoggingEvent]] = None
def setPageAppender(maxSize: Int, name: String = PAGE_APPENDER_NAME): Unit = {
Try {
rootLogger.withActual[CyclicBufferAppender[ILoggingEvent]](nullValue) {
(logger) ⇒
logger.getAppender(name) match {
case cba: CyclicBufferAppender[ILoggingEvent] ⇒
cba.setMaxSize(maxSize)
cba
case _ ⇒
val cba = new CyclicBufferAppender[ILoggingEvent]()
cba.setMaxSize(maxSize)
cba.setName(name)
cba.setContext(rootLoggerContext.get)
cba.start()
logger.addAppender(cba)
cba
}
}
} match {
case Success(cb) ⇒
pageAppender = Some(cb)
case Failure(xcptn) ⇒
log.warn("Failed to set PageAppender: ", xcptn)
pageAppender = None
}
}
def convertRecentEventsToHtml(): String = {
Try {
pageAppender match {
case None ⇒
"No log content available."
case Some(pa) ⇒
val startingBufLen = 4096
val layout = new HTMLLayout()
val buffer = new StringBuilder(startingBufLen)
layout.setContext(rootLoggerContext.get)
layout.setPattern("%date%relative%level%logger%msg%ex")
layout.setTitle("")
layout.start()
for { i ← 0 until pa.getLength } {
buffer.append(layout.doLayout(pa.get(i)))
}
s"${buffer.toString()}"
}
} match {
case Success(result) ⇒
result
case Failure(xcptn) ⇒
log.warn("Error while converting log events to html: ", xcptn)
s"""
|Error while converting log events to html:
|${xcptn.getClass.getCanonicalName}: ${xcptn.getMessage}
|""".stripMargin
}
}
def setStdOutAppender(
name: String = STDOUT_APPENDER_NAME
): Try[ConsoleAppender[ILoggingEvent]] = Try {
rootLogger.withActual[ConsoleAppender[ILoggingEvent]](nullValue) {
(logger) ⇒
logger.getAppender(name) match {
case ca: ConsoleAppender[ILoggingEvent] ⇒
ca.setWithJansi(true)
ca
case _ ⇒
val ca = new ConsoleAppender[ILoggingEvent]
val rlc = rootLoggerContext.get
ca.setImmediateFlush(true)
ca.setContext(rlc)
ca.setEncoder(makeEncoder(CONSOLE_PATTERN, rlc))
ca.setWithJansi(true)
ca.start()
logger.addAppender(ca)
ca
}
}
}
}