diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala index 73d161c83d..89f4c279f8 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/LogConfigLoader.scala @@ -6,6 +6,7 @@ import izumi.distage.roles.launcher.LogConfigLoader.DeclarativeLoggerConfig 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 @@ -27,20 +28,20 @@ object LogConfigLoader { 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 { @@ -53,7 +54,19 @@ object LogConfigLoader { 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 { + l => + (LoggerPath(p.id, Some(l)), LoggerPathConfig(level)) + } + } else { + Seq((LoggerPath(p.id, None), LoggerPathConfig(level))) + } + + } } val format = if (isJson) { @@ -65,19 +78,19 @@ object LogConfigLoader { 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 diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/RouterFactory.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/RouterFactory.scala index d6e6293e4d..608d1ce343 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/RouterFactory.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/RouterFactory.scala @@ -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 @@ -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, ) diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerConfig.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerConfig.scala index dcc7d7d2bb..899b998e70 100644 --- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerConfig.scala +++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerConfig.scala @@ -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]) diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathConfig.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathConfig.scala deleted file mode 100644 index 93a0405d11..0000000000 --- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathConfig.scala +++ /dev/null @@ -1,6 +0,0 @@ -package izumi.logstage.api.config - -import izumi.logstage.api.Log -import izumi.logstage.api.logger.LogSink - -final case class LoggerPathConfig(threshold: Log.Level, sinks: Seq[LogSink]) diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala new file mode 100644 index 0000000000..393e4b4bb9 --- /dev/null +++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathRule.scala @@ -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 + } + } + + 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) + } + + } else { + LoggerPathForLines(cfg, Set.empty) + } + } +} diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/logger/LogRouter.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/logger/LogRouter.scala index e8ae05518b..3492cca585 100644 --- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/logger/LogRouter.scala +++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/logger/LogRouter.scala @@ -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 = {} diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala index a614133d87..79b4d23fbd 100644 --- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala +++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/ConfigurableLogRouter.scala @@ -4,7 +4,7 @@ import izumi.fundamentals.platform.console.TrivialLogger 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} @@ -57,8 +57,20 @@ object ConfigurableLogRouter { 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))) + } + + }.toMap val configService = new LogConfigServiceImpl(LoggerConfig(rootConfig, levelConfigs)) diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala index cfdfb2eb0f..275ec77283 100644 --- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala +++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/routing/LogConfigServiceImpl.scala @@ -1,24 +1,30 @@ 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 @@ -26,29 +32,29 @@ class LogConfigServiceImpl(loggerConfig: LoggerConfig) extends LogConfigService } @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}" } val repr = node match { @@ -56,8 +62,8 @@ class LogConfigServiceImpl(loggerConfig: LoggerConfig) extends LogConfigService 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)}" } val out = (List(repr) ++ sub).mkString("\n") @@ -76,41 +82,52 @@ class LogConfigServiceImpl(loggerConfig: LoggerConfig) extends LogConfigService 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") } diff --git a/logstage/logstage-core/src/test/scala/izumi/logstage/sink/LoggingConsoleSinkTest.scala b/logstage/logstage-core/src/test/scala/izumi/logstage/sink/LoggingConsoleSinkTest.scala index 87e4af65e2..a6e6cf6c76 100644 --- a/logstage/logstage-core/src/test/scala/izumi/logstage/sink/LoggingConsoleSinkTest.scala +++ b/logstage/logstage-core/src/test/scala/izumi/logstage/sink/LoggingConsoleSinkTest.scala @@ -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 { @@ -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) } }