All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy