太长不读版
我开发了一个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
现在我们来试着解决类型问题
首先我们看看打印一条日志需要哪些步骤:
- 针对当前场景给出一条描述
- 将附加数据都转换到一个目标类型,然后将目标类型转换成字符串,这里目标类型通常直接就是字符串
- 把附加数据的字符串拼接起来然后打印
我们可以在第二步做一些事情,告诉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()
}
复制代码