Skip to content

Commit 799f3c4

Browse files
authored
Line-level logging configuration (#2153)
* wip: basic line-level logging configuration, imperfect * wip: basic line-level logging configuration, better rules
1 parent 07eb4e0 commit 799f3c4

File tree

9 files changed

+147
-54
lines changed

9 files changed

+147
-54
lines changed

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

+22-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import izumi.distage.roles.launcher.LogConfigLoader.DeclarativeLoggerConfig
66
import izumi.logstage.api.Log
77
import izumi.logstage.api.Log.Level.Warn
88
import izumi.logstage.api.Log.Message
9+
import izumi.logstage.api.config.{LoggerPath, LoggerPathConfig, LoggerPathForLines}
910
import izumi.logstage.api.rendering.RenderingOptions
1011
import logstage.IzLogger
1112

@@ -27,20 +28,20 @@ object LogConfigLoader {
2728
case class DeclarativeLoggerConfig(
2829
format: LoggerFormat,
2930
rendering: RenderingOptions,
30-
levels: Map[String, Log.Level],
31+
levels: Map[LoggerPath, LoggerPathConfig],
3132
rootLevel: Log.Level,
3233
interceptJUL: Boolean,
3334
)
3435

35-
final case class SinksConfig(
36+
final case class HoconSinksSection(
3637
levels: Map[String, List[String]],
3738
options: Option[RenderingOptions],
3839
json: Option[Boolean],
3940
jul: Option[Boolean],
4041
)
4142

42-
object SinksConfig {
43-
implicit val configReader: DIConfigReader[SinksConfig] = DIConfigReader.derived
43+
object HoconSinksSection {
44+
implicit val configReader: DIConfigReader[HoconSinksSection] = DIConfigReader.derived
4445
}
4546

4647
class LogConfigLoaderImpl(cliOptions: CLILoggerOptions, earlyLogger: IzLogger @Id("early")) extends LogConfigLoader {
@@ -53,7 +54,19 @@ object LogConfigLoader {
5354
val levels = logconf.levels.flatMap {
5455
case (stringLevel, packageList) =>
5556
val level = Log.Level.parseLetter(stringLevel)
56-
packageList.map(pkg => (pkg, level))
57+
packageList.flatMap {
58+
pkg =>
59+
val p = LoggerPathForLines.parse(pkg)
60+
if (p.lines.nonEmpty) {
61+
p.lines.map {
62+
l =>
63+
(LoggerPath(p.id, Some(l)), LoggerPathConfig(level))
64+
}
65+
} else {
66+
Seq((LoggerPath(p.id, None), LoggerPathConfig(level)))
67+
}
68+
69+
}
5770
}
5871

5972
val format = if (isJson) {
@@ -65,19 +78,19 @@ object LogConfigLoader {
6578
DeclarativeLoggerConfig(format, options, levels, cliOptions.level, jul)
6679
}
6780

68-
private def readConfig(config: AppConfig): SinksConfig = {
81+
private def readConfig(config: AppConfig): HoconSinksSection = {
6982
Try(config.config.getConfig("logger")).toEither.left
7083
.map(_ => Message("No `logger` section in config. Using defaults."))
7184
.flatMap {
7285
config =>
73-
SinksConfig.configReader.decodeConfig(config).toEither.left.map {
86+
HoconSinksSection.configReader.decodeConfig(config).toEither.left.map {
7487
exception =>
75-
Message(s"Failed to parse `logger` config section into ${classOf[SinksConfig] -> "type"}. Using defaults. $exception")
88+
Message(s"Failed to parse `logger` config section into ${classOf[HoconSinksSection] -> "type"}. Using defaults. $exception")
7689
}
7790
} match {
7891
case Left(errMessage) =>
7992
earlyLogger.log(Warn)(errMessage)
80-
SinksConfig(Map.empty, None, None, None)
93+
HoconSinksSection(Map.empty, None, None, None)
8194

8295
case Right(value) =>
8396
value

distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/RouterFactory.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package izumi.distage.roles.launcher
22

33
import izumi.distage.roles.launcher.LogConfigLoader.{DeclarativeLoggerConfig, LoggerFormat}
4-
import izumi.logstage.api.config.{LoggerConfig, LoggerPathConfig}
4+
import izumi.logstage.api.config.{LoggerConfig, LoggerPathConfig, LoggerPathRule}
55
import izumi.logstage.api.logger.LogQueue
66
import izumi.logstage.api.rendering.StringRenderingPolicy
77
import izumi.logstage.api.routing.LogConfigServiceImpl
@@ -25,13 +25,13 @@ object RouterFactory {
2525
val sinks = List(sink)
2626
val levelConfigs = config.levels.map {
2727
case (pkg, level) =>
28-
(pkg, LoggerPathConfig(level, sinks))
28+
(pkg, LoggerPathRule(level, sinks))
2929
}
3030

3131
// TODO: here we may read log configuration from config file
3232
val router = new ConfigurableLogRouter(
3333
new LogConfigServiceImpl(
34-
LoggerConfig(LoggerPathConfig(config.rootLevel, sinks), levelConfigs)
34+
LoggerConfig(LoggerPathRule(LoggerPathConfig(config.rootLevel), sinks), levelConfigs)
3535
),
3636
buffer,
3737
)
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package izumi.logstage.api.config
22

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

logstage/logstage-core/src/main/scala/izumi/logstage/api/config/LoggerPathConfig.scala

-6
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package izumi.logstage.api.config
2+
3+
import izumi.logstage.api.Log
4+
import izumi.logstage.api.logger.LogSink
5+
6+
final case class LoggerPath(id: String, line: Option[Int])
7+
8+
final case class LoggerPathConfig(threshold: Log.Level)
9+
10+
final case class LoggerPathRule(config: LoggerPathConfig, sinks: Seq[LogSink])
11+
12+
final case class LoggerPathForLines(id: String, lines: Set[Int])
13+
14+
object LoggerPathForLines {
15+
def parse(cfg: String): LoggerPathForLines = {
16+
def toInt(s: String): Option[Int] = {
17+
try {
18+
Some(s.toInt)
19+
} catch {
20+
case _: NumberFormatException => None
21+
}
22+
}
23+
24+
val li = cfg.lastIndexOf(':')
25+
26+
if (li > 0 && li + 1 < cfg.length) {
27+
val spec = cfg.substring(li + 1, cfg.length)
28+
val elems = spec.split(',').map(toInt).collect {
29+
case Some(i) =>
30+
i
31+
}
32+
if (elems.nonEmpty) {
33+
LoggerPathForLines(cfg.substring(0, li), elems.toSet)
34+
} else {
35+
LoggerPathForLines(cfg, Set.empty)
36+
}
37+
38+
} else {
39+
LoggerPathForLines(cfg, Set.empty)
40+
}
41+
}
42+
}

logstage/logstage-core/src/main/scala/izumi/logstage/api/logger/LogRouter.scala

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import izumi.logstage.sink.ConsoleSink
99

1010
trait LogRouter extends AutoCloseable {
1111
def log(entry: Log.Entry): Unit
12+
1213
def acceptable(id: Log.LoggerId, logLevel: Log.Level): Boolean
1314

1415
override def close(): Unit = {}

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

+15-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import izumi.fundamentals.platform.console.TrivialLogger
44
import izumi.fundamentals.platform.console.TrivialLogger.Config
55
import izumi.logstage.DebugProperties
66
import izumi.logstage.api.Log
7-
import izumi.logstage.api.config.{LogConfigService, LoggerConfig, LoggerPathConfig}
7+
import izumi.logstage.api.config.{LogConfigService, LoggerConfig, LoggerPath, LoggerPathConfig, LoggerPathForLines, LoggerPathRule}
88
import izumi.logstage.api.logger.{LogQueue, LogRouter, LogSink}
99
import izumi.logstage.sink.{ConsoleSink, FallbackConsoleSink}
1010

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

60-
val rootConfig = LoggerPathConfig(threshold, sinks)
61-
val levelConfigs = levels.view.mapValues(lvl => LoggerPathConfig(lvl, sinks)).toMap
60+
val rootConfig = LoggerPathRule(LoggerPathConfig(threshold), sinks)
61+
val levelConfigs = levels.view.flatMap {
62+
case (id, lvl) =>
63+
val p = LoggerPathForLines.parse(id)
64+
if (p.lines.nonEmpty) {
65+
p.lines.map {
66+
line =>
67+
(LoggerPath(p.id, Some(line)), LoggerPathRule(LoggerPathConfig(lvl), sinks))
68+
}
69+
} else {
70+
Seq((LoggerPath(p.id, None), LoggerPathRule(LoggerPathConfig(lvl), sinks)))
71+
}
72+
73+
}.toMap
6274

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

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

+48-31
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,69 @@
11
package izumi.logstage.api.routing
22

33
import izumi.logstage.api.Log
4-
import izumi.logstage.api.config.{LogConfigService, LogEntryConfig, LoggerConfig, LoggerPathConfig}
4+
import izumi.logstage.api.config.{LogConfigService, LogEntryConfig, LoggerConfig, LoggerPath, LoggerPathRule}
55
import izumi.logstage.api.routing.LogConfigServiceImpl.{ConfiguredLogTreeNode, LogTreeNode}
66

77
import scala.annotation.tailrec
88

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

1414
override def config(e: Log.Entry): LogEntryConfig = {
15-
LogEntryConfig(configFor(e.context.static.id).sinks)
15+
val config = configFor(e.context.static.id, e.context.static.position.line)
16+
if (e.context.dynamic.level >= config.config.threshold) {
17+
LogEntryConfig(config.sinks)
18+
} else {
19+
LogEntryConfig(List.empty)
20+
}
1621
}
1722

1823
private val configTree = LogConfigServiceImpl.build(loggerConfig)
1924

20-
@inline private def configFor(e: Log.LoggerId): LoggerPathConfig = {
21-
val configPath = findConfig(e.id.split('.').toList, List.empty, configTree)
25+
@inline private def configFor(e: Log.LoggerId, line: Int): LoggerPathRule = {
26+
val configPath = findConfig(e.id.split('.').toList, line, List.empty, configTree)
27+
2228
configPath
2329
.collect {
2430
case c: ConfiguredLogTreeNode => c
2531
}.last.config
2632
}
2733

2834
@tailrec
29-
private final def findConfig(outPath: List[String], inPath: List[LogTreeNode], current: LogTreeNode): List[LogTreeNode] = {
35+
private final def findConfig(outPath: List[String], line: Int, inPath: List[LogTreeNode], current: LogTreeNode): List[LogTreeNode] = {
3036
outPath match {
3137
case head :: tail =>
32-
current.sub.get(head) match {
33-
case Some(value) =>
34-
findConfig(tail, inPath :+ current, value)
35-
case None =>
36-
inPath :+ current
38+
current match {
39+
case _ =>
40+
current.sub.get(LoggerPath(head, Some(line))).orElse(current.sub.get(LoggerPath(head, None))) match {
41+
case Some(value) =>
42+
findConfig(tail, line, inPath :+ current, value)
43+
case None =>
44+
inPath :+ current
45+
}
3746
}
47+
3848
case Nil =>
3949
inPath :+ current
4050
}
4151
}
4252

43-
// override def close(): Unit = {
44-
// (loggerConfig.root.sinks ++ loggerConfig.entries.values.flatMap(_.sinks)).foreach(_.close())
45-
// }
46-
4753
private def print(node: LogTreeNode, level: Int): String = {
4854
val sub = node.sub.values.map(s => print(s, level + 1))
4955

50-
def reprCfg(cfg: LoggerPathConfig) = {
51-
s"${cfg.threshold} -> ${cfg.sinks}"
56+
def reprCfg(cfg: LoggerPathRule) = {
57+
s"${cfg.config.threshold} -> ${cfg.sinks}"
5258
}
5359

5460
val repr = node match {
5561
case LogConfigServiceImpl.LogTreeRootNode(config, _) =>
5662
s"[${reprCfg(config)}]"
5763
case LogConfigServiceImpl.LogTreeEmptyNode(id, _) =>
5864
s"$id"
59-
case LogConfigServiceImpl.LogTreeMainNode(id, config, _) =>
60-
s"$id: ${reprCfg(config)}"
65+
case LogConfigServiceImpl.LogTreeMainNode(id, line, config, _) =>
66+
s"$id[L=$line]: ${reprCfg(config)}"
6167
}
6268

6369
val out = (List(repr) ++ sub).mkString("\n")
@@ -76,41 +82,52 @@ class LogConfigServiceImpl(loggerConfig: LoggerConfig) extends LogConfigService
7682

7783
object LogConfigServiceImpl {
7884
sealed trait LogTreeNode {
79-
def sub: Map[String, IdentifiedLogTreeNode]
85+
def sub: Map[LoggerPath, IdentifiedLogTreeNode]
8086
}
8187
sealed trait IdentifiedLogTreeNode extends LogTreeNode {
8288
def id: String
8389
}
8490

8591
sealed trait ConfiguredLogTreeNode extends LogTreeNode {
86-
def config: LoggerPathConfig
92+
def config: LoggerPathRule
8793
}
8894

89-
case class LogTreeRootNode(config: LoggerPathConfig, sub: Map[String, IdentifiedLogTreeNode]) extends LogTreeNode with ConfiguredLogTreeNode
90-
case class LogTreeEmptyNode(id: String, sub: Map[String, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode
91-
case class LogTreeMainNode(id: String, config: LoggerPathConfig, sub: Map[String, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode with ConfiguredLogTreeNode
95+
case class LogTreeRootNode(config: LoggerPathRule, sub: Map[LoggerPath, IdentifiedLogTreeNode]) extends LogTreeNode with ConfiguredLogTreeNode
96+
case class LogTreeEmptyNode(id: String, sub: Map[LoggerPath, IdentifiedLogTreeNode]) extends IdentifiedLogTreeNode
97+
case class LogTreeMainNode(id: String, line: Option[Int], config: LoggerPathRule, sub: Map[LoggerPath, IdentifiedLogTreeNode])
98+
extends IdentifiedLogTreeNode
99+
with ConfiguredLogTreeNode
92100

93101
def build(config: LoggerConfig): LogTreeRootNode = {
94-
val p = config.entries.iterator.map { case (k, v) => (k.split('.').toList, v) }.toList
102+
val p = config.entries.iterator.map {
103+
case (k, v) =>
104+
val parts = k.id.split('.').toList
105+
(parts.init.map(p => LoggerPath(p, None)) ++ List(LoggerPath(parts.last, k.line)), v)
106+
}.toList
95107
LogTreeRootNode(config.root, buildLookupSubtree(p))
96108
}
97109

98-
private def buildLookupSubtree(entries: List[(List[String], LoggerPathConfig)]): Map[String, IdentifiedLogTreeNode] = {
99-
buildSubtrees(entries).map(node => (node.id, node)).toMap
110+
private def buildLookupSubtree(entries: List[(List[LoggerPath], LoggerPathRule)]): Map[LoggerPath, IdentifiedLogTreeNode] = {
111+
buildSubtrees(entries).map {
112+
case m: LogTreeMainNode =>
113+
(LoggerPath(m.id, m.line), m)
114+
case o =>
115+
(LoggerPath(o.id, None), o)
116+
}.toMap
100117
}
101118

102-
private def buildSubtrees(entries: List[(List[String], LoggerPathConfig)]): List[IdentifiedLogTreeNode] = {
119+
private def buildSubtrees(entries: List[(List[LoggerPath], LoggerPathRule)]): List[IdentifiedLogTreeNode] = {
103120
entries
104121
.groupBy(_._1.head).map {
105122
case (cp, entries) =>
106123
val truncatedEntries = entries.map { case (p, c) => (p.tail, c) }
107124
val (current, sub) = truncatedEntries.partition(_._1.isEmpty)
108-
val subTree: Map[String, IdentifiedLogTreeNode] = if (sub.isEmpty) Map.empty else buildLookupSubtree(sub)
125+
val subTree: Map[LoggerPath, IdentifiedLogTreeNode] = if (sub.isEmpty) Map.empty else buildLookupSubtree(sub)
109126
current match {
110127
case Nil =>
111-
LogTreeEmptyNode(cp, subTree)
128+
LogTreeEmptyNode(cp.id, subTree)
112129
case head :: Nil =>
113-
LogTreeMainNode(cp, head._2, subTree)
130+
LogTreeMainNode(cp.id, cp.line, head._2, subTree)
114131
case list =>
115132
throw new RuntimeException(s"BUG: More than one logger config bound to one path at $cp: $list")
116133
}

logstage/logstage-core/src/test/scala/izumi/logstage/sink/LoggingConsoleSinkTest.scala

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package izumi.logstage.sink
22

33
import izumi.logstage.api.IzLogger
4+
import izumi.logstage.api.routing.ConfigurableLogRouter
45
import izumi.logstage.sink.ConsoleSink.ColoredConsoleSink
6+
import logstage.{Log, LogQueue}
57
import org.scalatest.wordspec.AnyWordSpec
68

79
class LoggingConsoleSinkTest extends AnyWordSpec {
@@ -17,7 +19,19 @@ class LoggingConsoleSinkTest extends AnyWordSpec {
1719
object LoggingConsoleSinkTest {
1820

1921
def setupConsoleLogger(): IzLogger = {
20-
IzLogger(IzLogger.Level.Trace, ColoredConsoleSink)
22+
IzLogger.apply(
23+
ConfigurableLogRouter.apply(
24+
Log.Level.Trace,
25+
Seq(ColoredConsoleSink),
26+
Map(
27+
"izumi.logstage.sink.ExampleService.start:26,27" -> Log.Level.Error,
28+
"izumi.logstage.sink.ExampleService.start:28" -> Log.Level.Error,
29+
),
30+
LogQueue.Immediate,
31+
)
32+
)
33+
34+
// IzLogger(IzLogger.Level.Trace, ColoredConsoleSink)
2135
}
2236

2337
}

0 commit comments

Comments
 (0)