case class LogConfig(fullClass: String, logLevel: String)
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.ximalaya.scala.common.log
import java.io.{File, FileFilter, RandomAccessFile}
import java.util.regex.Pattern
import ch.qos.logback.classic.{LoggerContext, Level ⇒ LogbackLevel}
import ch.qos.logback.core.{FileAppender ⇒ LogbackAppender}
import org.apache.commons.io.input.ReversedLinesFileReader
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.reflect.FieldUtils
import org.apache.log4j.{FileAppender ⇒ Log4jFileAppender, Level ⇒ Log4jLevel, Logger ⇒ Log4jLogger}
import org.slf4j.LoggerFactory
import org.slf4j.impl.Log4jLoggerAdapter
import scala.collection.JavaConverters._
/**
* @author cjuexuan at 2018/5/7 15:59.
* email : cjuexuan@gmail.com
*/
object LogUtilFactory {
//runtime set log level
lazy val logUtil = {
LoggerFactory.getILoggerFactory.getClass.getName match {
case "org.slf4j.impl.Log4jLoggerFactory" ⇒
new Log4JUtil
case "ch.qos.logback.classic.LoggerContext" ⇒
new LogbackUtil
}
}
}
trait LogUtil {
def setLogLevel(logConfig: LogConfig): Unit
def getLogLevel(className: String): String
val logFile: File
lazy val userDirLogPath: String = {
val projectDir = System.getProperty("user.dir")
new File(projectDir).listFiles(new FileFilter {
private val pattern = Pattern.compile(".*\\.log")
override def accept(pathname: File) = pattern.matcher(pathname.getName).matches()
}).headOption.map(_.getAbsolutePath).getOrElse("/tmp/log")
}
def headLog(lines: Int = 100): String = {
val randomAccessFile = new RandomAccessFile(logFile, "r")
try {
val builder = StringBuilder.newBuilder
randomAccessFile.seek(0)
(1 to lines).takeWhile { _ ⇒
val line = randomAccessFile.readLine()
if (line != null) {
builder.append(line).append("\n")
true
} else false
}
builder.toString()
} finally {
randomAccessFile.close()
}
}
def tailLog(lines: Int = 100): String = {
val reversedLinesFileReader = new ReversedLinesFileReader(logFile)
try {
val builder = StringBuilder.newBuilder
(1 to lines).takeWhile { _ ⇒
Option(reversedLinesFileReader.readLine()).map { line ⇒
builder.append(line.reverse).append("\n")
}.isDefined
}
builder.reverse.mkString
} finally {
reversedLinesFileReader.close()
}
}
//TODO refine
def fullLog: String = FileUtils.readFileToString(logFile)
}
class LogbackUtil extends LogUtil {
override def setLogLevel(logConfig: LogConfig): Unit = {
getLogbackLogger(logConfig.fullClass)
.setLevel(LogbackLevel.valueOf(logConfig.logLevel))
}
override def getLogLevel(className: String) = {
getLogbackLogger(className)
.getEffectiveLevel.toString.toUpperCase()
}
private def getLogbackLogger(className: String) = {
LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext].getLogger(className)
}
override val logFile = {
val logFileName = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)
.iteratorForAppenders().asScala
.find(_.isInstanceOf[LogbackAppender[_]])
.map(_.asInstanceOf[LogbackAppender[_]].getFile).getOrElse(userDirLogPath)
new File(logFileName)
}
}
class Log4JUtil extends LogUtil {
override def setLogLevel(logConfig: LogConfig): Unit = {
getLog4jLogger(logConfig.fullClass)
.setLevel(Log4jLevel.toLevel(logConfig.logLevel))
}
override def getLogLevel(className: String) = {
getLog4jLogger(className).getEffectiveLevel.toString.toUpperCase()
}
private def getLog4jLogger(className: String) = {
val logger = LoggerFactory.getILoggerFactory.getLogger(className).asInstanceOf[Log4jLoggerAdapter]
FieldUtils.getAllFields(logger.getClass)
.filter(_.getName == "logger")
.map { field ⇒ field.setAccessible(true); field }
.head
.get(logger)
.asInstanceOf[Log4jLogger]
}
override val logFile = {
Log4jLogger.getRootLogger.getAllAppenders.asScala
.find(_.isInstanceOf[Log4jFileAppender]).map { appender ⇒
new File(appender.asInstanceOf[Log4jFileAppender].getFile)
}.getOrElse(new File(userDirLogPath))
}
}
平时工作中web服务日志的需求总结下大概有这么几种需求:
从这几个需求出发,我完善了下我们的日志类的工具,考虑到有些工程使用的是log4j,有些工程使用的是logback,所以这里选择用runtime的方式解决这两种需求的适配问题,另外由于log相关的配置实在是复杂,目前我涵盖的是我们常用的几种方式,比如指定了file输出的情况。另外,由于最外层的http server有很多,所以最后一步封装成http调用的代码我这里并没有给出,在我们内部,我们在akka-http-ext和spring-boot-ext中都实现了相关的逻辑,底层调用的是同一套代码。最后,由于file文件可能很大,所以目前给出的fulllog的实现其实不太好,最好还是用stream解决,不过由于平时看全部日志的需求并不是很多,所以找时间再优化下代码
效果图