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

wvlet.log.LogRotationHandler.scala Maven / Gradle / Ivy

There is a newer version: 2025.1.8
Show newest version
/*
 * 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 wvlet.log

import java.io.{File, Flushable}
import java.nio.charset.StandardCharsets
import java.util.logging.ErrorManager
import java.util.{logging => jl}
import ch.qos.logback.core.ContextBase
import ch.qos.logback.core.encoder.EncoderBase
import ch.qos.logback.core.rolling.{
  RollingFileAppender,
  SizeAndTimeBasedFileNamingAndTriggeringPolicy,
  TimeBasedRollingPolicy
}
import ch.qos.logback.core.util.FileSize
import wvlet.log.LogFormatter.AppLogFormatter

import scala.util.{Failure, Success, Try}

object LogRotationHandler {

  /**
    * Encoding log string as UTF-8
    */
  private[LogRotationHandler] class StringEncoder extends EncoderBase[String] {
    override def encode(event: String): Array[Byte] = {
      event.getBytes(StandardCharsets.UTF_8)
    }
    override def headerBytes(): Array[Byte] = Array.emptyByteArray
    override def footerBytes(): Array[Byte] = Array.emptyByteArray
  }
}

/**
  * Writing logs to a file without rotation. This is just an wrapper of LogRotationHandler
  * @param fileName
  * @param formatter
  * @param logFileExt
  */
class FileHandler(fileName: String, formatter: LogFormatter = AppLogFormatter, logFileExt: String = ".log")
    extends LogRotationHandler(
      fileName,
      maxNumberOfFiles = Integer.MAX_VALUE,
      maxSizeInBytes = Long.MaxValue,
      formatter = formatter,
      logFileExt = logFileExt
    )

/**
  * Log rotation handler
  */
class LogRotationHandler(
    fileName: String,
    maxNumberOfFiles: Int = 100,      // Rotate up to 100 files
    maxSizeInBytes: Long = 104857600, // 100 MB
    formatter: LogFormatter = AppLogFormatter,
    logFileExt: String = ".log",
    tempFileExt: String = ".tmp"
) extends jl.Handler
    with AutoCloseable
    with Flushable {
  import LogRotationHandler.*

  recoverTempFiles(fileName)
  setFormatter(formatter)

  private val fileAppender = {
    val context = new ContextBase

    val fileAppender     = new RollingFileAppender[String]()
    val rollingPolicy    = new TimeBasedRollingPolicy[String]
    val triggeringPolicy = new SizeAndTimeBasedFileNamingAndTriggeringPolicy[String]

    rollingPolicy.setContext(context)
    val fileNameStem =
      if (fileName.endsWith(logFileExt)) fileName.substring(0, fileName.length - logFileExt.length) else fileName
    rollingPolicy.setFileNamePattern(s"${fileNameStem}-%d{yyyy-MM-dd}.%i${logFileExt}.gz")
    rollingPolicy.setMaxHistory(maxNumberOfFiles)
    rollingPolicy.setTimeBasedFileNamingAndTriggeringPolicy(triggeringPolicy)
    rollingPolicy.setParent(fileAppender)

    triggeringPolicy.setContext(context)
    triggeringPolicy.setTimeBasedRollingPolicy(rollingPolicy)
    triggeringPolicy.setMaxFileSize(new FileSize(maxSizeInBytes))

    fileAppender.setContext(context)
    fileAppender.setFile(fileName)
    fileAppender.setAppend(true)
    fileAppender.setEncoder(new StringEncoder)
    fileAppender.setRollingPolicy(rollingPolicy)

    // RollingPolicy and TriggeringPolicy must be started after configuring the FileAppender
    rollingPolicy.start()
    triggeringPolicy.start()
    fileAppender.start()

    fileAppender
  }

  override def flush(): Unit = {}

  private def toException(t: Throwable) = new Exception(t.getMessage, t)

  override def publish(record: jl.LogRecord): Unit = {
    if (isLoggable(record)) {
      Try(formatter.format(record)) match {
        case Success(message) =>
          Try(fileAppender.doAppend(s"${message}\n")) match {
            case Success(x) =>
            // do nothing
            case Failure(e) =>
              reportError(null, toException(e), ErrorManager.WRITE_FAILURE)
          }
        case Failure(e) =>
          reportError(null, toException(e), ErrorManager.FORMAT_FAILURE)
      }
    }
  }

  override def close(): Unit = {
    Try(fileAppender.stop) match {
      case Success(x) =>
      // do nothing
      case Failure(e) =>
        reportError(null, toException(e), ErrorManager.CLOSE_FAILURE)
    }
  }

  private def recoverTempFiles(logPath: String): Unit = {
    // Recover orphaned temp files
    for {
      logPathFile <- Option(new File(logPath).getParentFile)
      fileList    <- Option(logPathFile.listFiles)
      tempFile    <- fileList.filter(_.getName.endsWith(tempFileExt))
    } {
      val newName = tempFile.getName().substring(0, tempFile.getName().length() - tempFileExt.size)
      val newFile = new File(tempFile.getParent, newName + logFileExt)

      if (!tempFile.renameTo(newFile)) {
        reportError(s"Failed to rename temp file ${tempFile} to ${newFile}", null, ErrorManager.OPEN_FAILURE)
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy