cjuexuan / mynote

236 stars 34 forks source link

web服务日志相关的需求 #58

Open cjuexuan opened 6 years ago

cjuexuan commented 6 years ago

平时工作中web服务日志的需求总结下大概有这么几种需求:

  1. 动态调整日志级别
  2. 随时查看某个类的日志级别
  3. web页面访问日志,其中包含head和tail两种用法

从这几个需求出发,我完善了下我们的日志类的工具,考虑到有些工程使用的是log4j,有些工程使用的是logback,所以这里选择用runtime的方式解决这两种需求的适配问题,另外由于log相关的配置实在是复杂,目前我涵盖的是我们常用的几种方式,比如指定了file输出的情况。另外,由于最外层的http server有很多,所以最后一步封装成http调用的代码我这里并没有给出,在我们内部,我们在akka-http-ext和spring-boot-ext中都实现了相关的逻辑,底层调用的是同一套代码。最后,由于file文件可能很大,所以目前给出的fulllog的实现其实不太好,最好还是用stream解决,不过由于平时看全部日志的需求并不是很多,所以找时间再优化下代码

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

效果图

tail

image

tail5

head

logLevel

debugLevel

setLevel

regetLevel