Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mvv committed Nov 24, 2019
0 parents commit 097d5fc
Show file tree
Hide file tree
Showing 13 changed files with 515 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
.*
*.swp
*~
*.log
target
7 changes: 7 additions & 0 deletions .scalafmt.conf
@@ -0,0 +1,7 @@
version = 2.2.2
maxColumn = 120
align = most
align.openParenDefnSite = true
align.openParenCallSite = true
includeCurlyBraceInSelectChains = false
rewrite.rules = [SortImports, SortModifiers, RedundantBraces]
65 changes: 65 additions & 0 deletions build.sbt
@@ -0,0 +1,65 @@
import sbt._
import Keys._
import xerial.sbt.Sonatype._

inThisBuild(
Seq(
organization := "com.github.mvv.zilog",
version := "0.1-SNAPSHOT",
homepage := Some(url("https://github.com/mvv/zilog")),
scmInfo := Some(ScmInfo(url("https://github.com/mvv/zilog"), "scm:git@github.com:mvv/zilog.git")),
licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
developers := List(
Developer(id = "mvv",
name = "Mikhail Vorozhtsov",
email = "mikhail.vorozhtsov@gmail.com",
url = url("https://github.com/mvv"))
),
sonatypeProjectHosting := Some(GitHubHosting("mvv", "zilog", "mikhail.vorozhtsov@gmail.com"))
)
)

ThisBuild / publishTo := sonatypePublishToBundle.value
ThisBuild / publishMavenStyle := true

inThisBuild(
Seq(
crossScalaVersions := Seq("2.13.1", "2.12.10", "2.11.12"),
scalaVersion := crossScalaVersions.value.head,
scalacOptions ++= Seq("-feature", "-deprecation", "-unchecked", "-Xfatal-warnings")
)
)

def isPriorTo2_13(version: String): Boolean =
CrossVersion.partialVersion(version) match {
case Some((2, minor)) => minor < 13
case _ => false
}

lazy val zilog = (project in file("."))
.settings(
name := "zilog",
sonatypeProfileName := "com.github.mvv",
sonatypeSessionName := s"Zilog_${version.value}",
scalacOptions ++= {
if (isPriorTo2_13(scalaVersion.value)) {
Nil
} else {
Seq("-Ymacro-annotations")
}
},
libraryDependencies ++=
Seq(
"dev.zio" %% "zio" % "1.0.0-RC16" % Provided,
"org.slf4j" % "slf4j-api" % "1.7.29",
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
"org.specs2" %% "specs2-core" % "4.8.1" % Test
),
libraryDependencies ++= {
if (isPriorTo2_13(scalaVersion.value)) {
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
} else {
Nil
}
}
)
1 change: 1 addition & 0 deletions project/build.properties
@@ -0,0 +1 @@
sbt.version=1.3.3
3 changes: 3 additions & 0 deletions project/plugins.sbt
@@ -0,0 +1,3 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.2.1")
addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8")
106 changes: 106 additions & 0 deletions src/main/scala/com/github/mvv/zilog/ImplicitArgsLogger.scala
@@ -0,0 +1,106 @@
package com.github.mvv.zilog

import org.slf4j.MDC
import zio.{FiberRef, UIO, ZIO}

trait ImplicitArgsLogger extends Logger {
override def logger: ImplicitArgsLogger.Service[Any]
}

object ImplicitArgsLogger {
trait Service[-R] extends Logger.Service[R] {
def withImplicitLogArgs[R1 <: R, E, A](args: (String, Any)*)(zio: ZIO[R1, E, A]): ZIO[R1, E, A]
}

sealed abstract private class FiberRefService[-R](fiberRef: FiberRef[Map[String, Any]]) extends Service[R] {
final override def withImplicitLogArgs[R1 <: R, E, A](args: (String, Any)*)(zio: ZIO[R1, E, A]): ZIO[R1, E, A] =
fiberRef.get.flatMap { current =>
fiberRef.locally(current ++ args)(zio)
}
def log(level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any])(
implicit ctx: LoggerContext
): ZIO[R, Nothing, Unit]
final override def log(level: Level, format: String, args: Array[Any])(
implicit ctx: LoggerContext
): ZIO[R, Nothing, Unit] =
fiberRef.get.flatMap(log(level, format, args, _))
}

object Service {
def apply[R](
f: (LoggerContext, Level, String, Array[Any], Map[String, Any]) => ZIO[R, Nothing, Unit]
): UIO[Service[R]] =
FiberRef.make(Map.empty[String, Any], (first: Map[String, Any], _: Map[String, Any]) => first).map { fiberRef =>
new FiberRefService[R](fiberRef) {
override def log(level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any])(
implicit ctx: LoggerContext
): ZIO[R, Nothing, Unit] =
f(ctx, level, format, explicitArgs, implicitArgs)
}
}
}

val mdc: UIO[Service[Any]] = Service {
(ctx: LoggerContext, level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any]) =>
ZIO.effectTotal {
val oldValues = implicitArgs.iterator.map {
case (key, value) =>
val oldValue = Option(MDC.get(key))
MDC.put(key, value.toString)
key -> oldValue
}.toSeq
ctx.log(level, format, explicitArgs)
oldValues.foreach {
case (key, oldValue) =>
oldValue match {
case Some(value) =>
MDC.put(key, value)
case None =>
MDC.remove(key)
}
}
}
}

private def countPlaceholdersIn(format: String): Int = {
var i = format.indexOf("{}")
var counter = 0
if (i == 0) {
counter += 1
i = format.indexOf("{}", 2)
}
while (i > 0) {
if (format.charAt(i - 1) != '\\') {
counter += 1
}
i = format.indexOf("{}", i + 2)
}
counter
}

val appendToMessage: UIO[Service[Any]] = Service {
(ctx: LoggerContext, level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any]) =>
ZIO.effectTotal {
val (fullFormat, fullArgs) = if (implicitArgs.isEmpty) {
(format, explicitArgs)
} else {
val formatSuffix = implicitArgs.keysIterator.map { key =>
s"$key = {}"
}.mkString("; ")
val numFormatArgs = countPlaceholdersIn(format)
val numExplicitArgs = explicitArgs.length
val fullArgs = numFormatArgs.compare(numExplicitArgs) match {
case -1 =>
val (formatArgs, extraArgs) = explicitArgs.splitAt(numFormatArgs)
formatArgs ++ implicitArgs.values ++ extraArgs
case 1 =>
explicitArgs ++ Seq.fill(numFormatArgs - numExplicitArgs)(null) ++ implicitArgs.values
case _ =>
explicitArgs ++ implicitArgs.values
}
(s"$format; $formatSuffix", fullArgs)
}
ctx.log(level, fullFormat, fullArgs)
}
}
}
27 changes: 27 additions & 0 deletions src/main/scala/com/github/mvv/zilog/Level.scala
@@ -0,0 +1,27 @@
package com.github.mvv.zilog

sealed trait Level {
protected val code: Int
}

object Level {
case object Error extends Level {
override protected val code = 4
}
case object Warn extends Level {
override protected val code = 3
}
case object Info extends Level {
override protected val code = 2
}
case object Debug extends Level {
override protected val code = 1
}
case object Trace extends Level {
override protected val code = 0
}

implicit val levelOrdering: Ordering[Level] = new Ordering[Level] {
override def compare(x: Level, y: Level): Int = x.code.compare(y.code)
}
}
34 changes: 34 additions & 0 deletions src/main/scala/com/github/mvv/zilog/Logger.scala
@@ -0,0 +1,34 @@
package com.github.mvv.zilog

import zio.ZIO

import scala.language.experimental.macros

trait Logger {
def logger: Logger.Service[Any]
}

object Logger {
trait Service[-R] {
def log(level: Level, format: String, args: Array[Any])(implicit ctx: LoggerContext): ZIO[R, Nothing, Unit]
final def error(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logError
final def error(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logErrorWithThrowable
final def warn(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logWarn
final def warn(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logWarnWithThrowable
final def info(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logInfo
final def info(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logInfoWithThrowable
final def debug(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logDebug
final def debug(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logDebugWithThrowable
final def trace(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logTrace
final def trace(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logTraceWithThrowable
}

object Default extends Logger {
override val logger: Service[Any] = new Service[Any] {
override def log(level: Level, format: String, args: Array[Any])(
implicit ctx: LoggerContext
): ZIO[Any, Nothing, Unit] =
ZIO.effectTotal(ctx.log(level, format, args))
}
}
}
29 changes: 29 additions & 0 deletions src/main/scala/com/github/mvv/zilog/LoggerContext.scala
@@ -0,0 +1,29 @@
package com.github.mvv.zilog

import org.slf4j.LoggerFactory

import scala.reflect.{classTag, ClassTag}

final class LoggerContext(val underlying: org.slf4j.Logger) extends AnyVal {
def isLevelEnabled(level: Level): Boolean =
level match {
case Level.Error => underlying.isErrorEnabled
case Level.Warn => underlying.isWarnEnabled
case Level.Info => underlying.isInfoEnabled
case Level.Debug => underlying.isDebugEnabled
case Level.Trace => underlying.isTraceEnabled
}
def log(level: Level, format: String, args: Array[Any]): Unit =
level match {
case Level.Error => underlying.error(format, args.asInstanceOf[Array[AnyRef]]: _*)
case Level.Warn => underlying.warn(format, args.asInstanceOf[Array[AnyRef]]: _*)
case Level.Info => underlying.info(format, args.asInstanceOf[Array[AnyRef]]: _*)
case Level.Debug => underlying.debug(format, args.asInstanceOf[Array[AnyRef]]: _*)
case Level.Trace => underlying.trace(format, args.asInstanceOf[Array[AnyRef]]: _*)
}
}

object LoggerContext {
def apply(name: String): LoggerContext = new LoggerContext(LoggerFactory.getLogger(name))
def apply[A: ClassTag]: LoggerContext = new LoggerContext(LoggerFactory.getLogger(classTag[A].runtimeClass))
}
100 changes: 100 additions & 0 deletions src/main/scala/com/github/mvv/zilog/LoggerMacro.scala
@@ -0,0 +1,100 @@
package com.github.mvv.zilog

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

class LoggerMacro(val c: blackbox.Context) {
import c.universe._

private val Error = c.Expr(q"_root_.com.github.mvv.zilog.Level.Error")
private val Warn = c.Expr(q"_root_.com.github.mvv.zilog.Level.Warn")
private val Info = c.Expr(q"_root_.com.github.mvv.zilog.Level.Info")
private val Debug = c.Expr(q"_root_.com.github.mvv.zilog.Level.Debug")
private val Trace = c.Expr(q"_root_.com.github.mvv.zilog.Level.Trace")

def logError(message: c.Expr[String]): Tree =
log(Error, None, message)
def logErrorWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree =
log(Error, Some(e), message)
def logWarn(message: c.Expr[String]): Tree =
log(Warn, None, message)
def logWarnWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree =
log(Warn, Some(e), message)
def logInfo(message: c.Expr[String]): Tree =
log(Info, None, message)
def logInfoWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree =
log(Info, Some(e), message)
def logDebug(message: c.Expr[String]): Tree =
log(Debug, None, message)
def logDebugWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree =
log(Debug, Some(e), message)
def logTrace(message: c.Expr[String]): Tree =
log(Trace, None, message)
def logTraceWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree =
log(Trace, Some(e), message)

private object ListOfStrings {
def unapply(list: List[Tree]): Option[List[String]] = {
@tailrec
def loop(acc: List[String], left: List[Tree]): Option[List[String]] =
left.headOption match {
case Some(Literal(Constant(s: String))) =>
loop(s :: acc, left.tail)
case Some(_) =>
None
case None =>
Some(acc.reverse)
}
loop(Nil, list)
}
}

def log(level: c.Expr[Level], error: Option[c.Expr[Throwable]], message: c.Expr[String]): Tree = {
val (format, args) = message.tree match {
// 2.11 and 2.12
case Apply(Select(Apply(Select(prefix, TermName("apply")), ListOfStrings(formatPieces)), TermName("s")), args)
if prefix.tpe.typeSymbol == typeOf[StringContext.type].typeSymbol && formatPieces.size == args.size + 1 =>
(Literal(Constant(formatPieces.mkString("{}"))), args)
// 2.13+
case Typed(expr, _) =>
@tailrec
def linearize(acc: List[Tree], lhs: Tree): List[Tree] =
lhs match {
case Apply(Select(next, TermName("$plus")), List(arg))
if next.tpe.typeSymbol == typeOf[String].typeSymbol =>
linearize(arg :: acc, next)
case other =>
other :: acc
}
@tailrec
def loop(format: String, args: Seq[Tree], rhs: Seq[Tree], isArg: Boolean): (Tree, Seq[Tree]) =
rhs.headOption match {
case Some(next) if isArg =>
loop(s"$format{}", args :+ next, rhs.tail, false)
case Some(Literal(Constant(s: String))) =>
loop(s"$format$s", args, rhs.tail, true)
case Some(next) =>
loop(s"$format{}", args :+ next, rhs.tail, false)
case None =>
(Literal(Constant(format)), args)
}
loop("", Vector.empty, linearize(Nil, expr), false)
case _ =>
(message.tree, Seq.empty)
}
val argsWithError = error.fold(args)(e => args :+ e.tree)
val ctx = TermName(c.freshName("ctx"))
q"""{
val $ctx = implicitly[_root_.com.github.mvv.zilog.LoggerContext]
if ($ctx.isLevelEnabled($level)) {
${c.prefix}.log($level, $format, ${if (argsWithError.isEmpty) {
q"_root_.scala.collection.immutable.Nil"
} else {
q"_root_.scala.Array[_root_.scala.Any](..$argsWithError)"
}})
} else {
_root_.zio.ZIO.unit
}
}"""
}
}

0 comments on commit 097d5fc

Please sign in to comment.