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

Feature #612 - implement LogFilter glob-like wildcard support #613

Merged
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
ThijsBroersen marked this conversation as resolved.
Show resolved Hide resolved
} 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