jlogger - 一个以JSON格式打印日志的Scala库

太长不读版

我开发了一个Scala日志库 jlogger, 可以在 build.sbt 添加下面的依赖进行安装

  libraryDependencies += "io.github.sjmyuan" %% "jlogger" % "0.0.2",
复制代码

功能

  • 类型安全,基于 cats进行开发
  • 支持多个日志级别,如info, warning, error等
  • 支持最多5个任意类型的附加信息
  • 支持JSON格式
  • 支持 logback 和 self4j

动机

我们团队一直在使用 Splunk 进行日志收集和分析, 它对JSON的支持非常好, 以JSON格式来打印日志可以极大提高分析效率.

思路

一图胜千言

重载函数

打印日志时,除了描述,级别和时间戳,我们通常都会添加一些额外的信息来帮助我们分析。最常见的做法就是把这些信息直接拼接到字符串里。例如

val name = "Tom"
val age = 10

logger.info(s"Got request from ${name} who is ${age} years old.") // Got request from Tom who is 10 years old.
复制代码

但是我们觉得这样做太麻烦了,能不能让logger帮我们做呢?就像这样

val name = "Tom"
val age = 10

logger.info("Got request", "name" -> name, "age" -> age) // Got request: name=Tome, age=10.(for example)
复制代码

上面的例子中只有两个数据,如果有更多呢?最简单的方法就是传一个Map或者List

logger.info("Got request", Map("name" -> name, "age" -> age))
复制代码

但是这样做会有两个问题

  • 不够简练,需要构造Map
  • 不能支持多个类型不一样的数据

对于类型的问题,我们会在下一节讨论。

对于Map的构造,通常我们不会传很多的数据(我们团队的情况是最多有5个),可以尝试用重载函数来替换它。

final def info[A](description: String, data: (String, A)): M[Unit]
final def info[A1, A2](description: String, data1: (String, A1), data2: (String, A2)): M[Unit]
final def info[A1, A2, A3](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3)): M[Unit]
final def info[A1, A2, A3, A4](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3), data4: (String, A4)): M[Unit]
final def info[A1, A2, A3, A4, A5](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3), data4: (String, A4), data5: (String, A5)): M[Unit]
复制代码

Formatter

现在我们来试着解决类型问题

首先我们看看打印一条日志需要哪些步骤:

  1. 针对当前场景给出一条描述
  2. 将附加数据都转换到一个目标类型,然后将目标类型转换成字符串,这里目标类型通常直接就是字符串
  3. 把附加数据的字符串拼接起来然后打印

我们可以在第二步做一些事情,告诉logger怎么把一个数据转换到目标类型,那么当我们传多个类型不同的数据时,只要把每个类型的 Formatter也一起传进去就好了。 这也就是Formatter设计的初衷

trait Formatter[A, B] {
  def format(key: String, value: A): B
}
复制代码

因为我们想打印的是JSON格式,所以可以用circe来定义一个以Json为目标类型的Formatter

implicit def generateJsonFormatter[A: Encoder]: Formatter[A, Json] =
  new Formatter[A, Json] {
    def format(key: String, value: A): Json = Json.obj(key -> value.asJson)
  }
复制代码

然后我们可以将Formatter传给logger,以info为例

final def info[A: Formatter[*, B]](description: String, data: (String, A)): M[Unit]
final def info[A1: Formatter[*, B], A2: Formatter[*, B]](description: String, data1: (String, A1), data2: (String, A2)): M[Unit]
final def info[A1: Formatter[*, B], A2: Formatter[*, B], A3: Formatter[*, B]](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3)): M[Unit]
final def info[A1: Formatter[*, B], A2: Formatter[*, B], A3: Formatter[*, B], A4: Formatter[*, B]](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3), data4: (String, A4)): M[Unit]
final def info[A1: Formatter[*, B], A2: Formatter[*, B], A3: Formatter[*, B], A4: Formatter[*, B], A5: Formatter[*, B]](description: String, data1: (String, A1), data2: (String, A2), data3: (String, A3), data4: (String, A4), data5: (String, A5)): M[Unit]
复制代码

JLogger

现在我们得到了一系列类型相同的数据,该怎么把他们拼接成一条日志呢?

JLogger里,我们要求目标类型实现Monoid,这样我们就能够把任意多个数据合并成一个数据

abstract class JLogger[M[_]: Monad, B: Monoid](implicit
    clock: Clock[M],
    stringFormatter: Formatter[String, B],
    instantFormatter: Formatter[Instant, B]
) { ... }
复制代码

我们还定义了一个抽象函数,子类可以使用它将目标类型转换成字符串然后打印

def log(logLevel: LogLevel, attrs: B): M[Unit]
复制代码

用法

使用 self4j 打印日志

import io.github.sjmyuan.jlogger.SimpleJsonLogger
import cats.effect.IO
import cats.effect.IOApp
import org.slf4j.LoggerFactory

object App extends IOApp {
    val logger = new Self4jJsonLogger[IO](LoggerFactory.getLogger(getClass( "IO")))

    val program = for {
      _ <-logger.warn("This is a json logger")
      _ <-logger.error("This is a json logger")
      _ <-logger.info("This is a json logger")
    } yield()

    program.unsafeRunSync()
}
复制代码

使用原生的 println 打印日志

import io.github.sjmyuan.jlogger.SimpleJsonLogger
import cats.effect.IO
import cats.effect.IOApp

object App extends IOApp {
    val logger = new SimpleJsonLogger[IO]( "IO")

    val program = for {
      _ <-logger.warn("This is a json logger")
      _ <-logger.error("This is a json logger")
      _ <-logger.info("This is a json logger")
    } yield()

    program.unsafeRunSync()
}
复制代码

猜你喜欢

转载自juejin.im/post/7048237609100771359