Skip to content

Commit

Permalink
LogFilter glob-like wildcard support (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
ThijsBroersen authored Mar 28, 2023
1 parent 071a1bf commit 8469b9f
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 21 deletions.
130 changes: 113 additions & 17 deletions core/jvm/src/test/scala/zio/logging/LogFilterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,89 @@ object LogFilterSpec extends ZIOSpecDefault {
}

val spec: Spec[Environment, Any] = suite("LogFilterSpec")(
test("log filtering by log level and name") {
suite("log filtering by log level and name")(
test("simple paths") {
val filter: LogFilter[String] = LogFilter.logLevelByName(
LogLevel.Debug,
"a" -> LogLevel.Info,
"a.b.c" -> LogLevel.Warning,
"e.f" -> LogLevel.Error
)

val filter: LogFilter[String] = LogFilter.logLevelByName(
LogLevel.Debug,
"a" -> LogLevel.Info,
"a.b.c" -> LogLevel.Warning,
"e.f" -> LogLevel.Error
)
testFilter(filter, "x.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "a.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "e.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "e.f.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "e.f.Exec.exec", LogLevel.Error, Assertion.isTrue)
},
test("any string pattern") {
val filter: LogFilter[String] = LogFilter.logLevelByName(
LogLevel.Debug,
"a" -> LogLevel.Info,
"a.*.c" -> LogLevel.Warning,
"e.f.*" -> LogLevel.Error
)

testFilter(filter, "x.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "a.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "e.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "e.f.Exec.exec", LogLevel.Debug, Assertion.isFalse)
},
testFilter(filter, "x.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "a.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b2.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b2.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b2.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "e.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "e.f.g.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "e.f.g.Exec.exec", LogLevel.Error, Assertion.isTrue)
},
test("any string and globstar patterns") {
val filter: LogFilter[String] = LogFilter.logLevelByName(
LogLevel.Debug,
"a" -> LogLevel.Info,
"a.**.*y" -> LogLevel.Warning
)

testFilter(filter, "a.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.y.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.y.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b.y.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b.xy.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b.xyz.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b.xyz.Exec.exec", LogLevel.Info, Assertion.isTrue)
},
test("globstar pattern") {
val filter: LogFilter[String] = LogFilter.logLevelByName(
LogLevel.Debug,
"a" -> LogLevel.Info,
"a.**.c" -> LogLevel.Warning,
"e.f.**" -> LogLevel.Error
)

testFilter(filter, "x.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "a.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b2.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "a.b.Exec.exec", LogLevel.Info, Assertion.isTrue) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b2.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b.b2.c.Exec.exec", LogLevel.Info, Assertion.isFalse) &&
testFilter(filter, "a.b.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b2.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "a.b.b2.c.Exec.exec", LogLevel.Warning, Assertion.isTrue) &&
testFilter(filter, "e.Exec.exec", LogLevel.Debug, Assertion.isTrue) &&
testFilter(filter, "e.f.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "e.f.g.Exec.exec", LogLevel.Debug, Assertion.isFalse) &&
testFilter(filter, "e.f.g.Exec.exec", LogLevel.Error, Assertion.isTrue)
}
),
test("log filtering by log level and name with annotation") {

val loggerName: LogGroup[Any, String] = LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogGroup()
Expand Down Expand Up @@ -302,6 +366,38 @@ object LogFilterSpec extends ZIOSpecDefault {
"a" -> LogLevel.Info,
"a" -> LogLevel.Warning
)
) &&
check(
Seq(
"a" -> LogLevel.Warning,
"a" -> LogLevel.Info,
"**" -> LogLevel.Info,
"*" -> LogLevel.Info,
"a.**.c.Service1" -> LogLevel.Warning,
"a.b.c.Service1" -> LogLevel.Warning,
"a.*.c.Service1" -> LogLevel.Warning,
"a.b.c" -> LogLevel.Error,
"a.b.d" -> LogLevel.Debug,
"e.f" -> LogLevel.Error,
"e.*.g.*.i" -> LogLevel.Error,
"e.*.g.h.*" -> LogLevel.Error,
"e.f.*.h" -> LogLevel.Error
),
Seq(
"e.f.*.h" -> LogLevel.Error,
"e.f" -> LogLevel.Error,
"e.*.g.h.*" -> LogLevel.Error,
"e.*.g.*.i" -> LogLevel.Error,
"a.b.d" -> LogLevel.Debug,
"a.b.c.Service1" -> LogLevel.Warning,
"a.b.c" -> LogLevel.Error,
"a.*.c.Service1" -> LogLevel.Warning,
"a.**.c.Service1" -> LogLevel.Warning,
"a" -> LogLevel.Info,
"a" -> LogLevel.Warning,
"*" -> LogLevel.Info,
"**" -> LogLevel.Info
)
)
}
)
Expand Down
43 changes: 41 additions & 2 deletions core/shared/src/main/scala/zio/logging/LogFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,42 @@ object LogFilter {
val mappingsSorted = mappings.map(splitNameByDotAndLevel.tupled).sorted(nameLevelOrdering)
val nameGroup = group.map(splitNameByDot)

@tailrec
def globStarCompare(l: List[String], m: List[String]): Boolean =
(l, m) match {
case (_, Nil) => true
case (Nil, _) => false
case (l @ (_ :: ls), m) =>
// try a regular, routesCompare or check if skipping paths (globstar pattern) results in a matching path
l.startsWith(m) || compareRoutes(l, m) || globStarCompare(ls, m)
}

@tailrec
def anystringCompare(l: String, m: List[String]): Boolean = m match {
case mh :: ms =>
val startOfMh = l.indexOfSlice(mh)
if (startOfMh >= 0) anystringCompare(l.drop(startOfMh + mh.size), ms)
else false
case Nil => l.isEmpty()
}

@tailrec
def compareRoutes(l: List[String], m: List[String]): Boolean =
(l, m) match {
case (_, Nil) => true
case (Nil, _) => false
case (_ :: ls, "*" :: ms) => compareRoutes(ls, ms)
case (l, "**" :: ms) => globStarCompare(l, ms)
case (lh :: ls, mh :: ms) if !mh.contains("*") =>
lh == mh && compareRoutes(ls, ms)
case (l @ (lh :: ls), m @ (mh :: ms)) =>
anystringCompare(lh, mh.split('*').toList) && compareRoutes(ls, ms)
}

logLevelByGroup[M, List[String]](
rootLevel,
nameGroup,
(l, m) => l.startsWith(m),
(l, m) => l.startsWith(m) || compareRoutes(l, m),
mappingsSorted: _*
)
}
Expand Down Expand Up @@ -371,7 +403,14 @@ object LogFilter {
case (xFirst :: xTail, yFirst :: yTail) =>
val r = yFirst.compareTo(xFirst)
if (r != 0) {
r
if (xFirst.contains('*') || yFirst.contains('*')) {
if (xFirst == "**") 1
else if (yFirst == "**") -1
else if (xFirst == "*") 1
else if (yFirst == "*") -1
else
compareNames(xFirst.split('*').toList.filter(_.nonEmpty), yFirst.split('*').toList.filter(_.nonEmpty))
} else r
} else compareNames(xTail, yTail)

case _ => 0
Expand Down
6 changes: 4 additions & 2 deletions docs/log-filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import zio.logging.LogFilter
val filter = LogFilter.logLevelByName(
LogLevel.Debug,
"io.netty" -> LogLevel.Info,
"io.grpc.netty" -> LogLevel.Info
"io.grpc.netty" -> LogLevel.Info,
"org.my.**.ServiceX" -> LogLevel.Trace, // glob-like (any paths) filter
"org.my.X*Layers" -> LogLevel.Info // glob-like (single or partial path) filter
)
```

will use the `Debug` log level for everything except for log events with the logger name
prefixed by either `List("io", "netty")` or `List("io", "grpc", "netty")`.
prefixed by either `List("io", "netty")` or `List("io", "grpc", "netty")` or `List("org", "my", "**", "ServiceX")` or `List("org", "my", "X*Layers")`.
Logger name is extracted from log annotation or `zio.Trace`.

`LogFilter.filter` returns a version of `zio.ZLogger` that only logs messages when this filter is satisfied.
Expand Down

0 comments on commit 8469b9f

Please sign in to comment.