Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line-level logging configuration #2153

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import izumi.logstage.api.Log
import izumi.logstage.api.Log.Level.Warn
import izumi.logstage.api.Log.Message
import izumi.logstage.api.config.{LoggerPath, LoggerPathConfig, LoggerPathForLines}
import izumi.logstage.api.rendering.RenderingOptions
import logstage.IzLogger

Expand All @@ -27,20 +28,20 @@
case class DeclarativeLoggerConfig(
format: LoggerFormat,
rendering: RenderingOptions,
levels: Map[String, Log.Level],
levels: Map[LoggerPath, LoggerPathConfig],
rootLevel: Log.Level,
interceptJUL: Boolean,
)

final case class SinksConfig(
final case class HoconSinksSection(
levels: Map[String, List[String]],
options: Option[RenderingOptions],
json: Option[Boolean],
jul: Option[Boolean],
)

object SinksConfig {
implicit val configReader: DIConfigReader[SinksConfig] = DIConfigReader.derived
object HoconSinksSection {
implicit val configReader: DIConfigReader[HoconSinksSection] = DIConfigReader.derived
}

class LogConfigLoaderImpl(cliOptions: CLILoggerOptions, earlyLogger: IzLogger @Id("early")) extends LogConfigLoader {
Expand All @@ -53,7 +54,19 @@
val levels = logconf.levels.flatMap {
case (stringLevel, packageList) =>
val level = Log.Level.parseLetter(stringLevel)
packageList.map(pkg => (pkg, level))
packageList.flatMap {
pkg =>
val p = LoggerPathForLines.parse(pkg)
if (p.lines.nonEmpty) {
p.lines.map {

Check warning on line 61 in distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala

View check run for this annotation

Codecov / codecov/patch

distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala#L61

Added line #L61 was not covered by tests
l =>
(LoggerPath(p.id, Some(l)), LoggerPathConfig(level))

Check warning on line 63 in distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala

View check run for this annotation

Codecov / codecov/patch

distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala#L63

Added line #L63 was not covered by tests
}
} else {
Seq((LoggerPath(p.id, None), LoggerPathConfig(level)))
}

}
}

val format = if (isJson) {
Expand All @@ -65,19 +78,19 @@
DeclarativeLoggerConfig(format, options, levels, cliOptions.level, jul)
}

private def readConfig(config: AppConfig): SinksConfig = {
private def readConfig(config: AppConfig): HoconSinksSection = {
Try(config.config.getConfig("logger")).toEither.left
.map(_ => Message("No `logger` section in config. Using defaults."))
.flatMap {
config =>
SinksConfig.configReader.decodeConfig(config).toEither.left.map {
HoconSinksSection.configReader.decodeConfig(config).toEither.left.map {
exception =>
Message(s"Failed to parse `logger` config section into ${classOf[SinksConfig] -> "type"}. Using defaults. $exception")
Message(s"Failed to parse `logger` config section into ${classOf[HoconSinksSection] -> "type"}. Using defaults. $exception")
}
} match {
case Left(errMessage) =>
earlyLogger.log(Warn)(errMessage)
SinksConfig(Map.empty, None, None, None)
HoconSinksSection(Map.empty, None, None, None)

case Right(value) =>
value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package izumi.distage.roles.launcher

import izumi.distage.roles.launcher.LogConfigLoader.{DeclarativeLoggerConfig, LoggerFormat}
import izumi.logstage.api.config.{LoggerConfig, LoggerPathConfig}
import izumi.logstage.api.config.{LoggerConfig, LoggerPathConfig, LoggerPathRule}
import izumi.logstage.api.logger.LogQueue
import izumi.logstage.api.rendering.StringRenderingPolicy
import izumi.logstage.api.routing.LogConfigServiceImpl
Expand All @@ -25,13 +25,13 @@ object RouterFactory {
val sinks = List(sink)
val levelConfigs = config.levels.map {
case (pkg, level) =>
(pkg, LoggerPathConfig(level, sinks))
(pkg, LoggerPathRule(level, sinks))
}

// TODO: here we may read log configuration from config file
val router = new ConfigurableLogRouter(
new LogConfigServiceImpl(
LoggerConfig(LoggerPathConfig(config.rootLevel, sinks), levelConfigs)
LoggerConfig(LoggerPathRule(LoggerPathConfig(config.rootLevel), sinks), levelConfigs)
),
buffer,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package izumi.logstage.api.config

final case class LoggerConfig(root: LoggerPathConfig, entries: Map[String, LoggerPathConfig])
final case class LoggerConfig(root: LoggerPathRule, entries: Map[LoggerPath, LoggerPathRule])

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package izumi.logstage.api.config

import izumi.logstage.api.Log
import izumi.logstage.api.logger.LogSink

final case class LoggerPath(id: String, line: Option[Int])

final case class LoggerPathConfig(threshold: Log.Level)

final case class LoggerPathRule(config: LoggerPathConfig, sinks: Seq[LogSink])

final case class LoggerPathForLines(id: String, lines: Set[Int])

object LoggerPathForLines {
def parse(cfg: String): LoggerPathForLines = {
def toInt(s: String): Option[Int] = {
try {
Some(s.toInt)
} catch {
case _: NumberFormatException => None

Check warning on line 20 in logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala

View check run for this annotation

Codecov / codecov/patch

logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala#L20

Added line #L20 was not covered by tests
}
}

val li = cfg.lastIndexOf(':')

if (li > 0 && li + 1 < cfg.length) {
val spec = cfg.substring(li + 1, cfg.length)
val elems = spec.split(',').map(toInt).collect {
case Some(i) =>
i
}
if (elems.nonEmpty) {
LoggerPathForLines(cfg.substring(0, li), elems.toSet)
} else {
LoggerPathForLines(cfg, Set.empty)

Check warning on line 35 in logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala

View check run for this annotation

Codecov / codecov/patch

logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala#L35

Added line #L35 was not covered by tests
}

} else {
LoggerPathForLines(cfg, Set.empty)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import izumi.logstage.sink.ConsoleSink

trait LogRouter extends AutoCloseable {
def log(entry: Log.Entry): Unit

def acceptable(id: Log.LoggerId, logLevel: Log.Level): Boolean

override def close(): Unit = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import izumi.fundamentals.platform.console.TrivialLogger.Config
import izumi.logstage.DebugProperties
import izumi.logstage.api.Log
import izumi.logstage.api.config.{LogConfigService, LoggerConfig, LoggerPathConfig}
import izumi.logstage.api.config.{LogConfigService, LoggerConfig, LoggerPath, LoggerPathConfig, LoggerPathForLines, LoggerPathRule}
import izumi.logstage.api.logger.{LogQueue, LogRouter, LogSink}
import izumi.logstage.sink.{ConsoleSink, FallbackConsoleSink}

Expand Down Expand Up @@ -57,8 +57,20 @@
def apply(threshold: Log.Level, sinks: Seq[LogSink], levels: Map[String, Log.Level], buffer: LogQueue): ConfigurableLogRouter = {
import scala.collection.compat._

val rootConfig = LoggerPathConfig(threshold, sinks)
val levelConfigs = levels.view.mapValues(lvl => LoggerPathConfig(lvl, sinks)).toMap
val rootConfig = LoggerPathRule(LoggerPathConfig(threshold), sinks)
val levelConfigs = levels.view.flatMap {
case (id, lvl) =>
val p = LoggerPathForLines.parse(id)
if (p.lines.nonEmpty) {
p.lines.map {
line =>
(LoggerPath(p.id, Some(line)), LoggerPathRule(LoggerPathConfig(lvl), sinks))
}
} else {
Seq((LoggerPath(p.id, None), LoggerPathRule(LoggerPathConfig(lvl), sinks)))

Check warning on line 70 in logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala

View check run for this annotation

Codecov / codecov/patch

logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala#L70

Added line #L70 was not covered by tests
}

}.toMap

val configService = new LogConfigServiceImpl(LoggerConfig(rootConfig, levelConfigs))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,69 @@
package izumi.logstage.api.routing

import izumi.logstage.api.Log
import izumi.logstage.api.config.{LogConfigService, LogEntryConfig, LoggerConfig, LoggerPathConfig}
import izumi.logstage.api.config.{LogConfigService, LogEntryConfig, LoggerConfig, LoggerPath, LoggerPathRule}
import izumi.logstage.api.routing.LogConfigServiceImpl.{ConfiguredLogTreeNode, LogTreeNode}

import scala.annotation.tailrec

class LogConfigServiceImpl(loggerConfig: LoggerConfig) extends LogConfigService {
override def threshold(e: Log.LoggerId): Log.Level = {
configFor(e).threshold
configFor(e, -1).config.threshold
}

override def config(e: Log.Entry): LogEntryConfig = {
LogEntryConfig(configFor(e.context.static.id).sinks)
val config = configFor(e.context.static.id, e.context.static.position.line)
if (e.context.dynamic.level >= config.config.threshold) {
LogEntryConfig(config.sinks)
} else {
LogEntryConfig(List.empty)
}
}

private val configTree = LogConfigServiceImpl.build(loggerConfig)

@inline private def configFor(e: Log.LoggerId): LoggerPathConfig = {
val configPath = findConfig(e.id.split('.').toList, List.empty, configTree)
@inline private def configFor(e: Log.LoggerId, line: Int): LoggerPathRule = {
val configPath = findConfig(e.id.split('.').toList, line, List.empty, configTree)

configPath
.collect {
case c: ConfiguredLogTreeNode => c
}.last.config
}

@tailrec
private final def findConfig(outPath: List[String], inPath: List[LogTreeNode], current: LogTreeNode): List[LogTreeNode] = {
private final def findConfig(outPath: List[String], line: Int, inPath: List[LogTreeNode], current: LogTreeNode): List[LogTreeNode] = {
outPath match {
case head :: tail =>
current.sub.get(head) match {
case Some(value) =>
findConfig(tail, inPath :+ current, value)
case None =>
inPath :+ current
current match {
case _ =>
current.sub.get(LoggerPath(head, Some(line))).orElse(current.sub.get(LoggerPath(head, None))) match {
case Some(value) =>
findConfig(tail, line, inPath :+ current, value)
case None =>
inPath :+ current
}
}

case Nil =>
inPath :+ current
}
}

// override def close(): Unit = {
// (loggerConfig.root.sinks ++ loggerConfig.entries.values.flatMap(_.sinks)).foreach(_.close())
// }

private def print(node: LogTreeNode, level: Int): String = {
val sub = node.sub.values.map(s => print(s, level + 1))

def reprCfg(cfg: LoggerPathConfig) = {
s"${cfg.threshold} -> ${cfg.sinks}"
def reprCfg(cfg: LoggerPathRule) = {
s"${cfg.config.threshold} -> ${cfg.sinks}"

Check warning on line 57 in logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala

View check run for this annotation

Codecov / codecov/patch

logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala#L57

Added line #L57 was not covered by tests
}

val repr = node match {
case LogConfigServiceImpl.LogTreeRootNode(config, _) =>
s"[${reprCfg(config)}]"
case LogConfigServiceImpl.LogTreeEmptyNode(id, _) =>
s"$id"
case LogConfigServiceImpl.LogTreeMainNode(id, config, _) =>
s"$id: ${reprCfg(config)}"
case LogConfigServiceImpl.LogTreeMainNode(id, line, config, _) =>
s"$id[L=$line]: ${reprCfg(config)}"

Check warning on line 66 in logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala

View check run for this annotation

Codecov / codecov/patch

logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala#L66

Added line #L66 was not covered by tests
}

val out = (List(repr) ++ sub).mkString("\n")
Expand All @@ -76,41 +82,52 @@

object LogConfigServiceImpl {
sealed trait LogTreeNode {
def sub: Map[String, IdentifiedLogTreeNode]
def sub: Map[LoggerPath, IdentifiedLogTreeNode]
}
sealed trait IdentifiedLogTreeNode extends LogTreeNode {
def id: String
}

sealed trait ConfiguredLogTreeNode extends LogTreeNode {
def config: LoggerPathConfig
def config: LoggerPathRule
}

case class LogTreeRootNode(config: LoggerPathConfig, sub: Map[String, IdentifiedLogTreeNode]) extends LogTreeNode with ConfiguredLogTreeNode
case class LogTreeEmptyNode(id: String, sub: Map[String, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode
case class LogTreeMainNode(id: String, config: LoggerPathConfig, sub: Map[String, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode with ConfiguredLogTreeNode
case class LogTreeRootNode(config: LoggerPathRule, sub: Map[LoggerPath, IdentifiedLogTreeNode]) extends LogTreeNode with ConfiguredLogTreeNode
case class LogTreeEmptyNode(id: String, sub: Map[LoggerPath, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode
case class LogTreeMainNode(id: String, line: Option[Int], config: LoggerPathRule, sub: Map[LoggerPath, IdentifiedLogTreeNode])
extends IdentifiedLogTreeNode
with ConfiguredLogTreeNode

def build(config: LoggerConfig): LogTreeRootNode = {
val p = config.entries.iterator.map { case (k, v) => (k.split('.').toList, v) }.toList
val p = config.entries.iterator.map {
case (k, v) =>
val parts = k.id.split('.').toList
(parts.init.map(p => LoggerPath(p, None)) ++ List(LoggerPath(parts.last, k.line)), v)
}.toList
LogTreeRootNode(config.root, buildLookupSubtree(p))
}

private def buildLookupSubtree(entries: List[(List[String], LoggerPathConfig)]): Map[String, IdentifiedLogTreeNode] = {
buildSubtrees(entries).map(node => (node.id, node)).toMap
private def buildLookupSubtree(entries: List[(List[LoggerPath], LoggerPathRule)]): Map[LoggerPath, IdentifiedLogTreeNode] = {
buildSubtrees(entries).map {
case m: LogTreeMainNode =>
(LoggerPath(m.id, m.line), m)
case o =>
(LoggerPath(o.id, None), o)
}.toMap
}

private def buildSubtrees(entries: List[(List[String], LoggerPathConfig)]): List[IdentifiedLogTreeNode] = {
private def buildSubtrees(entries: List[(List[LoggerPath], LoggerPathRule)]): List[IdentifiedLogTreeNode] = {
entries
.groupBy(_._1.head).map {
case (cp, entries) =>
val truncatedEntries = entries.map { case (p, c) => (p.tail, c) }
val (current, sub) = truncatedEntries.partition(_._1.isEmpty)
val subTree: Map[String, IdentifiedLogTreeNode] = if (sub.isEmpty) Map.empty else buildLookupSubtree(sub)
val subTree: Map[LoggerPath, IdentifiedLogTreeNode] = if (sub.isEmpty) Map.empty else buildLookupSubtree(sub)
current match {
case Nil =>
LogTreeEmptyNode(cp, subTree)
LogTreeEmptyNode(cp.id, subTree)
case head :: Nil =>
LogTreeMainNode(cp, head._2, subTree)
LogTreeMainNode(cp.id, cp.line, head._2, subTree)
case list =>
throw new RuntimeException(s"BUG: More than one logger config bound to one path at $cp: $list")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package izumi.logstage.sink

import izumi.logstage.api.IzLogger
import izumi.logstage.api.routing.ConfigurableLogRouter
import izumi.logstage.sink.ConsoleSink.ColoredConsoleSink
import logstage.{Log, LogQueue}
import org.scalatest.wordspec.AnyWordSpec

class LoggingConsoleSinkTest extends AnyWordSpec {
Expand All @@ -17,7 +19,19 @@ class LoggingConsoleSinkTest extends AnyWordSpec {
object LoggingConsoleSinkTest {

def setupConsoleLogger(): IzLogger = {
IzLogger(IzLogger.Level.Trace, ColoredConsoleSink)
IzLogger.apply(
ConfigurableLogRouter.apply(
Log.Level.Trace,
Seq(ColoredConsoleSink),
Map(
"izumi.logstage.sink.ExampleService.start:26,27" -> Log.Level.Error,
"izumi.logstage.sink.ExampleService.start:28" -> Log.Level.Error,
),
LogQueue.Immediate,
)
)

// IzLogger(IzLogger.Level.Trace, ColoredConsoleSink)
}

}
Loading