Too long do not read version

I developed a Scala logging library, JLogger, which can be installed by adding the following dependencies in build.sbt

  libraryDependencies += "io.github.sjmyuan"% %"jlogger" % "Hundreds".Copy the code

function

  • Type safe, developed based on CATS
  • Supports multiple log levels, such as INFO, Warning, and error
  • Supports a maximum of five additional information of any type
  • Support JSON format
  • Support logBack and self4j

motivation

Our team has been using Splunk for log collection and analysis, and it supports JSON very well, so printing logs in JSON format can greatly improve analysis efficiency.

Train of thought

A picture is worth a thousand words

Overloaded function

When printing logs, in addition to description, level, and timestamp, we usually add some additional information to help us analyze them. The most common way to do this is to concatenate this information directly into a string. For example,

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.
Copy the code

But we think it is too much trouble to do so. Can we ask Logger to do it for us? Like this

val name = "Tom"
val age = 10

logger.info("Got request"."name" -> name, "age" -> age) // Got request: name=Tome, age=10.(for example)
Copy the code

There are only two pieces of data in the above example. What if there are more? The easiest way to do this is to pass a Map or List

logger.info("Got request".Map("name" -> name, "age" -> age))
Copy the code

But there are two problems with this

  • Not concise enough, need to construct Map
  • Multiple types of data cannot be supported

We will discuss the question of types in the next section.

For Map constructs, we usually don’t pass a lot of data (in our case, up to five) and can try to replace it with overloaded functions.

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]
Copy the code

Formatter

Now let’s try to solve the type problem

First let’s look at the steps required to print a log:

  1. Give a description of the current scenario
  2. Convert all the additional data to a target type, and then convert the target type to a string, where the target type is usually just a string
  3. Concatenate strings of additional data and print them

We can do something in step 2 that tells logger how to convert a data type to the target type, so that when we pass multiple different types of data, we can just pass in the Formatter of each type as well. That’s what Formatter was designed for

trait Formatter[A.B] {
  def format(key: String, value: A) :B
}
Copy the code

Since we want to print in JSON format, we can use Circe to define a Formatter that targets JSON

implicit def generateJsonFormatter[A: Encoder] :Formatter[A.Json] =
  new Formatter[A.Json] {
    def format(key: String, value: A) :Json = Json.obj(key -> value.asJson)
  }
Copy the code

We can then pass the Formatter to logger, using info as an example

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]
Copy the code

JLogger

Now that we have a set of data of the same type, how do we concatenate them into a log?

In JLogger, we require the target type to implement Monoid so that we can combine any number of data into one

abstract class JLogger[M[_]: Monad.B: Monoid] (implicit
    clock: Clock[M],
    stringFormatter: Formatter[String.B],
    instantFormatter: Formatter[Instant.B]) {... }Copy the code

We also define an abstract function that subclasses can use to convert the target type to a string and then print it

def log(logLevel: LogLevel, attrs: B) :M[Unit]
Copy the code

usage

Run the self4j command to print logs

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()
}
Copy the code

Use native println to print logs

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()
}
Copy the code