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

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

/*
 * 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, FileReader}
import java.util.Properties
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.{AtomicLong, AtomicReference}

import wvlet.log.LogLevelScanner.ScannerState
import wvlet.log.io.{IOUtil, Resource}
import wvlet.log.io.IOUtil._

import scala.concurrent.duration.Duration

object LogLevelScanner {
  private val logger = Logger("wvlet.log.LogLevelScanner")

  /**
    * Set log levels using a given Properties file
    *
    * @param file Properties file
    */
  def setLogLevels(file: File) {
    val logLevels = new Properties()
    withResource(new FileReader(file)) { in =>
      logLevels.load(in)
    }
    Logger.setLogLevels(logLevels)
  }

  val DEFAULT_LOGLEVEL_FILE_CANDIDATES = {
    Seq("log-test.properties", "log.properties")
  }

  /**
    * Scan the default log level file only once. To periodically scan, use scheduleLogLevelScan
    */
  def scanLogLevels {
    scanLogLevels(DEFAULT_LOGLEVEL_FILE_CANDIDATES)
  }

  /**
    * Scan the specified log level file
    *
    * @param loglevelFileCandidates
    */
  def scanLogLevels(loglevelFileCandidates: Seq[String]) {
    LogLevelScanner.scan(loglevelFileCandidates, None)
  }

  /**
    * Run the default LogLevelScanner every 1 minute
    */
  def scheduleLogLevelScan {
    scheduleLogLevelScan(LogLevelScannerConfig(DEFAULT_LOGLEVEL_FILE_CANDIDATES, Duration(1, TimeUnit.MINUTES)))
  }

  private[log] lazy val logLevelScanner: LogLevelScanner = new LogLevelScanner

  /**
    * Schedule the log level scanner with the given configuration.
    */
  def scheduleLogLevelScan(config: LogLevelScannerConfig) {
    logLevelScanner.setConfig(config)
    logLevelScanner.start
  }

  /**
    * Schedule the log level scanner with the given interval
    */
  def scheduleLogLevelScan(duration: Duration) {
    scheduleLogLevelScan(LogLevelScannerConfig(DEFAULT_LOGLEVEL_FILE_CANDIDATES, duration))
  }

  /**
    * Terminate the log-level scanner thread. The thread will remain in the system until
    * the next log scan schedule. This is for reusing the thread if scheduleLogLevelScan is called again in a short duration, and
    * reduce the overhead of creating a new thread.
    */
  def stopScheduledLogLevelScan {
    logLevelScanner.stop
  }

  /**
    * @param logLevelFileCandidates
    * @param lastScannedMillis
    * @return updated last scanned millis
    */
  private[log] def scan(logLevelFileCandidates: Seq[String], lastScannedMillis: Option[Long]): Option[Long] = {
    try {
      val logFileURL = logLevelFileCandidates.toStream.flatMap(f => Resource.find(f)).headOption
      logFileURL
        .map { url =>
          url.getProtocol match {
            case "file" =>
              val f            = new File(url.toURI)
              val lastModified = f.lastModified()
              if (lastScannedMillis.isEmpty || lastScannedMillis.get < lastModified) {
                LogLevelScanner.setLogLevels(f)
                Some(System.currentTimeMillis())
              } else {
                lastScannedMillis
              }
            case other if lastScannedMillis.isEmpty =>
              // non file resources found in the class path is stable, so we only need to read it once
              IOUtil.withResource(url.openStream()) { in =>
                val p = new Properties
                p.load(in)
                Logger.setLogLevels(p)
                Some(System.currentTimeMillis())
              }
            case _ =>
              None
          }
        }
        .getOrElse {
          lastScannedMillis
        }
    } catch {
      case e: Throwable =>
        // We need to use the native java.util.logging.Logger since the logger macro cannot be used within the same project
        logger.wrapped.log(LogLevel.WARN.jlLevel, s"Error occurred while scanning log properties: ${e.getMessage}", e)
        lastScannedMillis
    }
  }

  private[log] sealed trait ScannerState
  private[log] object RUNNING  extends ScannerState
  private[log] object STOPPING extends ScannerState
  private[log] object STOPPED  extends ScannerState

}

case class LogLevelScannerConfig(logLevelFileCandidates: Seq[String], scanInterval: Duration = Duration(1, TimeUnit.MINUTES))

import wvlet.log.LogLevelScanner._

private[log] class LogLevelScanner extends Guard { scanner =>
  private val config: AtomicReference[LogLevelScannerConfig] = new AtomicReference(LogLevelScannerConfig(Seq.empty))
  private val configChanged                                  = newCondition
  private[log] val scanCount                                 = new AtomicLong(0)

  def getConfig: LogLevelScannerConfig = config.get()
  def setConfig(config: LogLevelScannerConfig) {
    guard {
      val prev = this.config.get()
      if (prev.logLevelFileCandidates != config.logLevelFileCandidates) {
        lastScannedMillis = None
      }
      this.config.set(config)
      configChanged.signalAll()
    }
  }

  private val state = new AtomicReference[ScannerState](STOPPED)

  def start {
    guard {
      state.compareAndSet(STOPPING, RUNNING)
      if (state.compareAndSet(STOPPED, RUNNING)) {
        // Create a new thread if the previous thread is terminated
        new LogLevelScannerThread().start
      }
    }
  }

  def stop {
    guard {
      state.set(STOPPING)
    }
  }

  private var lastScheduledMillis: Option[Long] = None
  private var lastScannedMillis: Option[Long]   = None

  private def run {
    // We need to exit here so that the thread can be automatically discarded after the scan interval has passed
    // Otherwise, the thread remains in the classloader(s) if used for running test cases
    while (!state.compareAndSet(STOPPING, STOPPED)) {
      // Periodically run
      val currentTimeMillis  = System.currentTimeMillis()
      val scanIntervalMillis = getConfig.scanInterval.toMillis
      if (lastScheduledMillis.isEmpty || currentTimeMillis - lastScheduledMillis.get > scanIntervalMillis) {
        val updatedLastScannedMillis = scan(getConfig.logLevelFileCandidates, lastScannedMillis)
        scanCount.incrementAndGet()
        guard {
          lastScannedMillis = updatedLastScannedMillis
        }
        lastScheduledMillis = Some(currentTimeMillis)
      }
      // wait until next scheduled time
      val sleepTime = scanIntervalMillis - math.max(0, math.min(scanIntervalMillis, currentTimeMillis - lastScheduledMillis.get))
      guard {
        if (configChanged.await(sleepTime, TimeUnit.MILLISECONDS)) {
          // awaken due to config change
        }
      }
    }
  }

  private class LogLevelScannerThread extends Thread {
    setName("WvletLogLevelScanner")
    // Enable terminating JVM without shutting down this executor
    setDaemon(true)

    override def run(): Unit = {
      scanner.run
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy