diff --git a/benchmarks/src/main/scala/zio/logging/FilterBenchmarks.scala b/benchmarks/src/main/scala/zio/logging/FilterBenchmarks.scala index 038530d36..cd4edbcac 100644 --- a/benchmarks/src/main/scala/zio/logging/FilterBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/logging/FilterBenchmarks.scala @@ -14,7 +14,7 @@ class FilterBenchmarks { val runtime = Runtime.default val unfilteredLogging: ZLayer[Any, Nothing, Unit] = - Runtime.removeDefaultLoggers >>> consoleLogger(ConsoleLoggerConfig(LogFormat.default, LogFilter.acceptAll)) + Runtime.removeDefaultLoggers >>> makeSystemOutLogger(LogFormat.default.toLogger).install val handWrittenFilteredLogging: ZLayer[Any, Nothing, Unit] = { val loggerNameGroup: LogGroup[Any, String] = LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogGroup() @@ -37,40 +37,45 @@ class FilterBenchmarks { } } ) - Runtime.removeDefaultLoggers >>> consoleLogger(ConsoleLoggerConfig(LogFormat.default, filter)) + Runtime.removeDefaultLoggers >>> makeSystemOutLogger(LogFormat.default.toLogger) + .filter(filter) + .install } + val filterConfig: LogFilter.LogLevelByNameConfig = LogFilter.LogLevelByNameConfig( + LogLevel.Debug, + "a.b.c" -> LogLevel.Info, + "a.b.d" -> LogLevel.Warning, + "e" -> LogLevel.Info, + "f.g" -> LogLevel.Error, + "f" -> LogLevel.Info + ) + val filterByLogLevelAndNameLogging: ZLayer[Any, Nothing, Unit] = - Runtime.removeDefaultLoggers >>> consoleLogger( - ConsoleLoggerConfig( - LogFormat.default, - LogFilter.logLevelByName( - LogLevel.Debug, - "a.b.c" -> LogLevel.Info, - "a.b.d" -> LogLevel.Warning, - "e" -> LogLevel.Info, - "f.g" -> LogLevel.Error, - "f" -> LogLevel.Info - ) - ) - ) + Runtime.removeDefaultLoggers >>> makeSystemOutLogger(LogFormat.default.toLogger) + .filter(filterConfig.toFilter) + .install val cachedFilterByLogLevelAndNameLogging: ZLayer[Any, Nothing, Unit] = - Runtime.removeDefaultLoggers >>> consoleLogger( - ConsoleLoggerConfig( - LogFormat.default, - LogFilter - .logLevelByName( - LogLevel.Debug, - "a.b.c" -> LogLevel.Info, - "a.b.d" -> LogLevel.Warning, - "e" -> LogLevel.Info, - "f.g" -> LogLevel.Error, - "f" -> LogLevel.Info - ) - .cached + Runtime.removeDefaultLoggers >>> makeSystemOutLogger(LogFormat.default.toLogger) + .filter(filterConfig.toFilter.cached) + .install + + val reconfigurableFilterByLogLevelAndNameLogging: ZLayer[Any, Nothing, Unit] = + Runtime.removeDefaultLoggers >>> ReconfigurableLogger + .make[Any, Nothing, String, Any, ConsoleLoggerConfig]( + ZIO.succeed(ConsoleLoggerConfig(LogFormat.default, filterConfig)), + (config, _) => makeSystemOutLogger(config.format.toLogger).filter(config.toFilter) ) - ) + .installUnscoped[Any] + + val reconfigurableCachedFilterByLogLevelAndNameLogging: ZLayer[Any, Nothing, Unit] = + Runtime.removeDefaultLoggers >>> ReconfigurableLogger + .make[Any, Nothing, String, Any, ConsoleLoggerConfig]( + ZIO.succeed(ConsoleLoggerConfig(LogFormat.default, filterConfig)), + (config, _) => makeSystemOutLogger(config.format.toLogger).filter(config.toFilter.cached) + ) + .installUnscoped[Any] val names: List[String] = List( "a", @@ -108,15 +113,17 @@ class FilterBenchmarks { } /** - * 2022/10/28 Initial results + * 2023/12/26 Initial results * * jmh:run -i 3 -wi 3 -f1 -t1 .*FilterBenchmarks.* * - * Benchmark Mode Cnt Score Error Units - * FilterBenchmarks.cachedFilterByLogLevelAndNameLog thrpt 3 16623.054 ± 15855.331 ops/s - * FilterBenchmarks.filterByLogLevelAndNameLog thrpt 3 18048.598 ± 3868.976 ops/s - * FilterBenchmarks.handWrittenFilterLog thrpt 3 16352.488 ± 2316.372 ops/s - * FilterBenchmarks.noFilteringLog thrpt 3 15104.002 ± 3857.108 ops/s + * Benchmark Mode Cnt Score Error Units + * FilterBenchmarks.cachedFilterByLogLevelAndNameLog thrpt 3 14795.547 ± 1372.850 ops/s + * FilterBenchmarks.filterByLogLevelAndNameLog thrpt 3 15093.994 ± 1230.494 ops/s + * FilterBenchmarks.handWrittenFilterLog thrpt 3 13157.888 ± 10193.287 ops/s + * FilterBenchmarks.noFilteringLog thrpt 3 11043.746 ± 230.514 ops/s + * FilterBenchmarks.reconfigurableCachedFilterByLogLevelAndNameLog thrpt 3 7532.412 ± 415.760 ops/s + * FilterBenchmarks.reconfigurableFilterByLogLevelAndNameLog thrpt 3 7482.096 ± 628.534 ops/s */ @Benchmark @@ -135,4 +142,12 @@ class FilterBenchmarks { def cachedFilterByLogLevelAndNameLog(): Unit = testLoggingWith(cachedFilterByLogLevelAndNameLogging) + @Benchmark + def reconfigurableFilterByLogLevelAndNameLog(): Unit = + testLoggingWith(reconfigurableFilterByLogLevelAndNameLogging) + + @Benchmark + def reconfigurableCachedFilterByLogLevelAndNameLog(): Unit = + testLoggingWith(reconfigurableCachedFilterByLogLevelAndNameLogging) + } diff --git a/build.sbt b/build.sbt index 2b9c168de..7d3682475 100644 --- a/build.sbt +++ b/build.sbt @@ -94,7 +94,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .settings(enableZIO(enableStreaming = true)) .settings( libraryDependencies ++= Seq( - "dev.zio" %%% "zio-parser" % zioParser + "dev.zio" %%% "zio-parser" % zioParser, + "dev.zio" %%% "zio-prelude" % zioPrelude ) ) .jvmSettings( @@ -198,8 +199,9 @@ lazy val examplesCore = project .settings( publish / skip := true, libraryDependencies ++= Seq( - "dev.zio" %% "zio-metrics-connectors" % zioMetricsConnectorsVersion, - "dev.zio" %% "zio-config-typesafe" % zioConfig + "dev.zio" %% "zio-metrics-connectors-prometheus" % zioMetricsConnectorsVersion, + "dev.zio" %% "zio-http" % zioHttp, + "dev.zio" %% "zio-config-typesafe" % zioConfig ) ) diff --git a/core/jvm/src/test/scala/zio/logging/ConsoleLoggerConfigSpec.scala b/core/jvm/src/test/scala/zio/logging/ConsoleLoggerConfigSpec.scala index 4431a38d2..3602103da 100644 --- a/core/jvm/src/test/scala/zio/logging/ConsoleLoggerConfigSpec.scala +++ b/core/jvm/src/test/scala/zio/logging/ConsoleLoggerConfigSpec.scala @@ -34,6 +34,31 @@ object ConsoleLoggerConfigSpec extends ZIOSpecDefault { assertTrue(true) } }, + test("equals config with same sources") { + + // "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" //FIXME + + val logFormat = + "%highlight{%timestamp %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" + + val configProvider: ConfigProvider = ConfigProvider.fromMap( + Map( + "logger/format" -> logFormat, + "logger/filter/rootLevel" -> LogLevel.Info.label, + "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label + ), + "/" + ) + + import zio.prelude._ + for { + c1 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) + c2 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) + } yield assertTrue( + c1.format == c2.format, + c1 === c2 + ) + }, test("fail on invalid filter config") { val logFormat = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" diff --git a/core/jvm/src/test/scala/zio/logging/FileLoggerConfigSpec.scala b/core/jvm/src/test/scala/zio/logging/FileLoggerConfigSpec.scala index c1f499775..774a8b2c1 100644 --- a/core/jvm/src/test/scala/zio/logging/FileLoggerConfigSpec.scala +++ b/core/jvm/src/test/scala/zio/logging/FileLoggerConfigSpec.scala @@ -52,6 +52,33 @@ object FileLoggerConfigSpec extends ZIOSpecDefault { assertTrue(loadedConfig.bufferedIOSize.isEmpty) } }, + test("equals config with same sources") { + + val logFormat = + "%highlight{%timestamp %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" + + val configProvider: ConfigProvider = ConfigProvider.fromMap( + Map( + "logger/format" -> logFormat, + "logger/path" -> "file:///tmp/test.log", + "logger/autoFlushBatchSize" -> "2", + "logger/bufferedIOSize" -> "4096", + "logger/rollingPolicy/type" -> "TimeBasedRollingPolicy", + "logger/filter/rootLevel" -> LogLevel.Info.label, + "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label + ), + "/" + ) + + import zio.prelude._ + for { + c1 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) + c2 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) + } yield assertTrue( + c1.format == c2.format, + c1 === c2 + ) + }, test("fail on invalid charset and filter config") { val logFormat = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" diff --git a/core/jvm/src/test/scala/zio/logging/LogFilterSpec.scala b/core/jvm/src/test/scala/zio/logging/LogFilterSpec.scala index fc1fd26df..59ae8a579 100644 --- a/core/jvm/src/test/scala/zio/logging/LogFilterSpec.scala +++ b/core/jvm/src/test/scala/zio/logging/LogFilterSpec.scala @@ -3,7 +3,21 @@ package zio.logging import zio.logging.test.TestService import zio.test.ZTestLogger.LogEntry import zio.test._ -import zio.{ Cause, Chunk, Config, ConfigProvider, FiberId, FiberRefs, LogLevel, LogSpan, Runtime, Trace, ZIO, ZLogger } +import zio.{ + Cause, + Chunk, + Config, + ConfigProvider, + FiberId, + FiberRefs, + LogLevel, + LogSpan, + Runtime, + Trace, + ZIO, + ZIOAspect, + ZLogger +} object LogFilterSpec extends ZIOSpecDefault { @@ -399,6 +413,129 @@ object LogFilterSpec extends ZIOSpecDefault { "**" -> LogLevel.Info ) ) + }, + test("log filters by log level and name from same configuration should be equal") { + + val configProvider = ConfigProvider.fromMap( + Map( + "logger/rootLevel" -> LogLevel.Debug.label, + "logger/mappings/a" -> LogLevel.Info.label, + "logger/mappings/a.b.c" -> LogLevel.Warning.label, + "logger/mappings/e.f" -> LogLevel.Error.label + ), + "/" + ) + + import zio.prelude._ + + for { + f1 <- configProvider + .load(LogFilter.LogLevelByNameConfig.config.nested("logger")) + .map(LogFilter.logLevelByName) + f2 <- configProvider + .load(LogFilter.LogLevelByNameConfig.config.nested("logger")) + .map(LogFilter.logLevelByName) + f3 <- configProvider + .load(LogFilter.LogLevelByNameConfig.config.nested("logger")) + .map(LogFilter.logLevelByName) + .map(_.cached) + f4 <- configProvider + .load(LogFilter.LogLevelByNameConfig.config.nested("logger")) + .map(LogFilter.logLevelByName) + .map(_.cached) + } yield assertTrue(f1 === f2, f3 === f4) + }, + test("and") { + + val filter = LogFilter.causeNonEmpty.and(LogFilter.logLevel(LogLevel.Info)) + + def testFilter(level: LogLevel, cause: Cause[_], expected: Boolean) = + assertTrue( + filter( + Trace.empty, + FiberId.None, + level, + () => "", + cause, + FiberRefs.empty, + List.empty, + Map.empty + ) == expected + ) + + testFilter(LogLevel.Info, Cause.fail("fail"), true) && testFilter( + LogLevel.Info, + Cause.empty, + false + ) && testFilter(LogLevel.Debug, Cause.fail("fail"), false) + }, + test("or") { + + val filter = LogFilter.causeNonEmpty.or(LogFilter.logLevel(LogLevel.Info)) + + def testFilter(level: LogLevel, cause: Cause[_], expected: Boolean) = + assertTrue( + filter( + Trace.empty, + FiberId.None, + level, + () => "", + cause, + FiberRefs.empty, + List.empty, + Map.empty + ) == expected + ) + + testFilter(LogLevel.Info, Cause.fail("fail"), true) && testFilter(LogLevel.Info, Cause.empty, true) && testFilter( + LogLevel.Debug, + Cause.fail("fail"), + true + ) && testFilter(LogLevel.Debug, Cause.empty, false) + }, + test("not") { + + val filter = LogFilter.causeNonEmpty.and(LogFilter.logLevel(LogLevel.Info)).not + + def testFilter(level: LogLevel, cause: Cause[_], expected: Boolean) = + assertTrue( + filter( + Trace.empty, + FiberId.None, + level, + () => "", + cause, + FiberRefs.empty, + List.empty, + Map.empty + ) == expected + ) + + testFilter(LogLevel.Info, Cause.fail("fail"), false) && testFilter( + LogLevel.Info, + Cause.empty, + true + ) && testFilter(LogLevel.Debug, Cause.fail("fail"), true) + }, + test("cached") { + val logOutputRef = new java.util.concurrent.atomic.AtomicReference[Chunk[LogEntry]](Chunk.empty) + + val filter = LogFilter + .logLevelByGroup( + LogLevel.Info, + LogGroup.loggerName, + "zio.logger1" -> LogLevel.Debug, + "zio.logging.test" -> LogLevel.Warning + ) + .cached + .asInstanceOf[LogFilter.CachedFilter[String]] + + (for { + _ <- ZIO.logDebug("debug") @@ ZIOAspect.annotated(loggerNameAnnotationKey, "zio.logger1") + res1 = filter.cache.get(List("zio", "logger1") -> LogLevel.Debug) + _ <- ZIO.logDebug("debug") @@ ZIOAspect.annotated(loggerNameAnnotationKey, "zio.logger2") + res2 = filter.cache.get(List("zio", "logger2") -> LogLevel.Debug) + } yield assertTrue(res1 == true, res2 == false)).provideLayer(testLogger(logOutputRef, filter)) } ) } diff --git a/core/jvm/src/test/scala/zio/logging/ReconfigurableLoggerSpec.scala b/core/jvm/src/test/scala/zio/logging/ReconfigurableLoggerSpec.scala new file mode 100644 index 000000000..5ca3eee8e --- /dev/null +++ b/core/jvm/src/test/scala/zio/logging/ReconfigurableLoggerSpec.scala @@ -0,0 +1,76 @@ +package zio.logging + +import zio.test._ +import zio.{ Chunk, Config, ConfigProvider, LogLevel, Queue, Runtime, Schedule, ZIO, ZLayer, _ } + +object ReconfigurableLoggerSpec extends ZIOSpecDefault { + + def configuredLogger( + queue: zio.Queue[String] + ): ZLayer[Any, Config.Error, Unit] = + ZLayer.scoped { + for { + logger <- ReconfigurableLogger + .make[Any, Config.Error, String, Any, ConsoleLoggerConfig]( + ConsoleLoggerConfig.load(), + (config, _) => + ZIO.succeed { + config.format.toLogger.map { line => + zio.Unsafe.unsafe { implicit u => + Runtime.default.unsafe.run(queue.offer(line)) + } + }.filter(config.toFilter) + }, + Schedule.fixed(200.millis) + ) + _ <- ZIO.withLoggerScoped(logger) + } yield () + } + + val spec: Spec[Environment, Any] = suite("ReconfigurableLogger2")( + test("log with changed config") { + + val initialProperties = Map( + "logger/format" -> "%message", + "logger/filter/rootLevel" -> LogLevel.Info.label, + "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label + ) + + for { + _ <- ZIO.foreach(initialProperties) { case (k, v) => + TestSystem.putProperty(k, v).as(k -> v) + } + + queue <- Queue.unbounded[String] + + runTest = + for { + _ <- ZIO.logInfo("info") + _ <- ZIO.logDebug("debug") + elements1 <- queue.takeAll + _ <- TestSystem.putProperty("logger/format", "%level %message") + _ <- ZIO.sleep(500.millis) + _ <- ZIO.logWarning("warn") + _ <- ZIO.logDebug("debug") + elements2 <- queue.takeAll + _ <- TestSystem.putProperty("logger/format", "L: %level M: %message") + _ <- TestSystem.putProperty("logger/filter/rootLevel", LogLevel.Debug.label) + _ <- ZIO.sleep(500.millis) + _ <- ZIO.logDebug("debug") + elements3 <- queue.takeAll + } yield assertTrue( + elements1 == Chunk("info") && elements2 == Chunk("WARN warn") && elements3 == Chunk( + "L: DEBUG M: debug" + ) + ) + + result <- + runTest.provide( + Runtime.removeDefaultLoggers >>> Runtime + .setConfigProvider(ConfigProvider.fromProps("/")) >>> configuredLogger(queue) + ) + } yield result + + } + ) @@ TestAspect.withLiveClock +} diff --git a/core/shared/src/main/scala/zio/logging/ConsoleLoggerConfig.scala b/core/shared/src/main/scala/zio/logging/ConsoleLoggerConfig.scala index 1cf6545a1..50489a5e4 100644 --- a/core/shared/src/main/scala/zio/logging/ConsoleLoggerConfig.scala +++ b/core/shared/src/main/scala/zio/logging/ConsoleLoggerConfig.scala @@ -15,23 +15,41 @@ */ package zio.logging -import zio.{ Config, LogLevel } +import zio.prelude._ +import zio.{ Config, NonEmptyChunk, ZIO, ZLayer } -final case class ConsoleLoggerConfig(format: LogFormat, filter: LogFilter[String]) +final case class ConsoleLoggerConfig( + format: LogFormat, + filter: LogFilter.LogLevelByNameConfig +) { + def toFilter[M]: LogFilter[M] = filter.toFilter +} object ConsoleLoggerConfig { - val default: ConsoleLoggerConfig = ConsoleLoggerConfig(LogFormat.default, LogFilter.logLevel(LogLevel.Info)) + val default: ConsoleLoggerConfig = ConsoleLoggerConfig(LogFormat.default, LogFilter.LogLevelByNameConfig.default) val config: Config[ConsoleLoggerConfig] = { val formatConfig = LogFormat.config.nested("format").withDefault(LogFormat.default) val filterConfig = LogFilter.LogLevelByNameConfig.config.nested("filter") - (formatConfig ++ filterConfig).map { case (format, filterConfig) => + (formatConfig ++ filterConfig).map { case (format, filter) => ConsoleLoggerConfig( format, - LogFilter.logLevelByName(filterConfig) + filter ) } } + implicit val equal: Equal[ConsoleLoggerConfig] = Equal.make { (l, r) => + l.format == r.format && l.filter === r.filter + } + + def load(configPath: NonEmptyChunk[String] = loggerConfigPath): ZIO[Any, Config.Error, ConsoleLoggerConfig] = + ZIO.config(ConsoleLoggerConfig.config.nested(configPath.head, configPath.tail: _*)) + + def make( + configPath: NonEmptyChunk[String] = loggerConfigPath + ): ZLayer[Any, Config.Error, ConsoleLoggerConfig] = + ZLayer.fromZIO(load(configPath)) + } diff --git a/core/shared/src/main/scala/zio/logging/FileLoggerConfig.scala b/core/shared/src/main/scala/zio/logging/FileLoggerConfig.scala index 580c67851..ab2c91d02 100644 --- a/core/shared/src/main/scala/zio/logging/FileLoggerConfig.scala +++ b/core/shared/src/main/scala/zio/logging/FileLoggerConfig.scala @@ -16,6 +16,7 @@ package zio.logging import zio._ +import zio.prelude._ import java.net.URI import java.nio.charset.{ Charset, StandardCharsets } @@ -25,12 +26,14 @@ import scala.util.{ Failure, Success, Try } final case class FileLoggerConfig( destination: Path, format: LogFormat, - filter: LogFilter[String], + filter: LogFilter.LogLevelByNameConfig, charset: Charset = StandardCharsets.UTF_8, autoFlushBatchSize: Int = 1, bufferedIOSize: Option[Int] = None, rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None -) +) { + def toFilter[M]: LogFilter[M] = filter.toFilter +} object FileLoggerConfig { @@ -73,11 +76,11 @@ object FileLoggerConfig { val rollingPolicyConfig = FileRollingPolicy.config.nested("rollingPolicy").optional (pathConfig ++ formatConfig ++ filterConfig ++ charsetConfig ++ autoFlushBatchSizeConfig ++ bufferedIOSizeConfig ++ rollingPolicyConfig).map { - case (path, format, filterConfig, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) => + case (path, format, filter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) => FileLoggerConfig( path, format, - LogFilter.logLevelByName(filterConfig), + filter, charset, autoFlushBatchSize, bufferedIOSize, @@ -86,4 +89,22 @@ object FileLoggerConfig { } } + implicit val equal: Equal[FileLoggerConfig] = Equal.make { (l, r) => + l.destination == r.destination && + l.charset == r.charset && + l.autoFlushBatchSize == r.autoFlushBatchSize && + l.bufferedIOSize == r.bufferedIOSize && + l.rollingPolicy == r.rollingPolicy && + l.format == r.format && + l.filter === r.filter + } + + def load(configPath: NonEmptyChunk[String] = loggerConfigPath): ZIO[Any, Config.Error, FileLoggerConfig] = + ZIO.config(FileLoggerConfig.config.nested(configPath.head, configPath.tail: _*)) + + def make( + configPath: NonEmptyChunk[String] = loggerConfigPath + ): ZLayer[Any, Config.Error, FileLoggerConfig] = + ZLayer.fromZIO(load(configPath)) + } diff --git a/core/shared/src/main/scala/zio/logging/FilteredLogger.scala b/core/shared/src/main/scala/zio/logging/FilteredLogger.scala new file mode 100644 index 000000000..112dea5b4 --- /dev/null +++ b/core/shared/src/main/scala/zio/logging/FilteredLogger.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging + +import zio.{ Cause, FiberId, FiberRefs, LogLevel, LogSpan, Trace, ZLogger } + +final case class FilteredLogger[Message, Output](logger: zio.ZLogger[Message, Output], filter: LogFilter[Message]) + extends ZLogger[Message, Option[Output]] { + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Message, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Option[Output] = + if (filter(trace, fiberId, logLevel, message, cause, context, spans, annotations)) { + Some(logger(trace, fiberId, logLevel, message, cause, context, spans, annotations)) + } else None + + def withFilter(newFilter: LogFilter[Message]): FilteredLogger[Message, Output] = FilteredLogger(logger, newFilter) +} diff --git a/core/shared/src/main/scala/zio/logging/LogFilter.scala b/core/shared/src/main/scala/zio/logging/LogFilter.scala index d8e9593a3..607d796b4 100644 --- a/core/shared/src/main/scala/zio/logging/LogFilter.scala +++ b/core/shared/src/main/scala/zio/logging/LogFilter.scala @@ -15,7 +15,8 @@ */ package zio.logging -import zio.{ Cause, Config, FiberId, FiberRefs, LogLevel, LogSpan, Trace, ZLogger } +import zio.prelude.Equal +import zio.{ Cause, Config, FiberId, FiberRefs, LogLevel, LogSpan, Trace } import scala.annotation.tailrec @@ -59,29 +60,16 @@ sealed trait LogFilter[-Message] { self => * The alphanumeric version of the `&&` operator. */ final def and[M <: Message](other: LogFilter[M]): LogFilter[M] = - LogFilter[M, (self.Value, other.Value)]( - self.group ++ other.group, - v => { - val (v1, v2) = v - - self.predicate(v1) && other.predicate(v2) - } - ) + LogFilter.AndFilter(self, other) /** * Returns a new log filter with cached results */ - final def cached: LogFilter[Message] = { - val cache = new java.util.concurrent.ConcurrentHashMap[Value, Boolean]() - LogFilter[Message, self.Value]( - self.group, - v => - cache.computeIfAbsent( - v, - _ => self.predicate(v) - ) - ) - } + final def cached: LogFilter[Message] = + self match { + case LogFilter.CachedFilter(filter) => LogFilter.CachedFilter(filter) + case _ => LogFilter.CachedFilter(self) + } final def contramap[M](f: M => Message): LogFilter[M] = LogFilter[M, self.Value]( group.contramap(f), @@ -91,41 +79,19 @@ sealed trait LogFilter[-Message] { self => /** * Returns a version of logger that only logs messages when this filter is satisfied */ - def filter[M <: Message, O](logger: zio.ZLogger[M, O]): zio.ZLogger[M, Option[O]] = - new ZLogger[M, Option[O]] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => M, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Option[O] = - if (self(trace, fiberId, logLevel, message, cause, context, spans, annotations)) { - Some(logger(trace, fiberId, logLevel, message, cause, context, spans, annotations)) - } else None - } + final def filter[M <: Message, O](logger: zio.ZLogger[M, O]): zio.ZLogger[M, Option[O]] = FilteredLogger(logger, self) /** * The alphanumeric version of the `!` operator. */ final def not: LogFilter[Message] = - LogFilter[Message, self.Value](self.group, v => !self.predicate(v)) + LogFilter.NotFilter(self) /** * The alphanumeric version of the `||` operator. */ final def or[M <: Message](other: LogFilter[M]): LogFilter[M] = - LogFilter[M, (self.Value, other.Value)]( - self.group ++ other.group, - v => { - val (v1, v2) = v - - self.predicate(v1) || other.predicate(v2) - } - ) + LogFilter.OrFilter(self, other) /** * Returns a new log filter with negated result @@ -136,6 +102,130 @@ sealed trait LogFilter[-Message] { self => object LogFilter { + implicit val equal: Equal[LogFilter[_]] = Equal.make { (l, r) => + (l, r) match { + case (l: GroupPredicateFilter[_, _], r: GroupPredicateFilter[_, _]) => GroupPredicateFilter.equal.equal(l, r) + case (l: CachedFilter[_], r: CachedFilter[_]) => CachedFilter.equal.equal(l, r) + case (l: AndFilter[_], r: AndFilter[_]) => AndFilter.equal.equal(l, r) + case (l: OrFilter[_], r: OrFilter[_]) => OrFilter.equal.equal(l, r) + case (l: NotFilter[_], r: NotFilter[_]) => NotFilter.equal.equal(l, r) + case (l: ConfiguredFilter[_, _], r: ConfiguredFilter[_, _]) => ConfiguredFilter.equal.equal(l, r) + case (l, r) => l == r + } + } + + private[logging] final case class GroupPredicateFilter[M, V]( + logGroup: LogGroup[M, V], + valuePredicate: V => Boolean + ) extends LogFilter[M] { + override type Value = V + + override def group: LogGroup[M, Value] = logGroup + + override def predicate(value: Value): Boolean = valuePredicate(value) + } + + private[logging] object GroupPredicateFilter { + implicit val equal: Equal[GroupPredicateFilter[_, _]] = Equal.default + } + + private[logging] final case class AndFilter[M]( + first: LogFilter[M], + second: LogFilter[M] + ) extends LogFilter[M] { + override type Value = (first.Value, second.Value) + + override val group: LogGroup[M, Value] = first.group ++ second.group + + override def predicate(value: Value): Boolean = { + val (v1, v2) = value + first.predicate(v1) && second.predicate(v2) + } + } + + private[logging] object AndFilter { + implicit val equal: Equal[AndFilter[_]] = Equal.make { (f, s) => + LogFilter.equal.equal(f.first, s.first) && LogFilter.equal.equal(f.second, s.second) + } + } + + private[logging] final case class OrFilter[M]( + first: LogFilter[M], + second: LogFilter[M] + ) extends LogFilter[M] { + override type Value = (first.Value, second.Value) + + override val group: LogGroup[M, Value] = first.group ++ second.group + + override def predicate(value: Value): Boolean = { + val (v1, v2) = value + first.predicate(v1) || second.predicate(v2) + } + } + + private[logging] object OrFilter { + implicit val equal: Equal[OrFilter[_]] = Equal.make { (f, s) => + LogFilter.equal.equal(f.first, s.first) && LogFilter.equal.equal(f.second, s.second) + } + } + + private[logging] final case class NotFilter[M]( + filter: LogFilter[M] + ) extends LogFilter[M] { + override type Value = filter.Value + + override def group: LogGroup[M, Value] = filter.group + + override def predicate(value: Value): Boolean = + !filter.predicate(value) + } + + private[logging] object NotFilter { + implicit val equal: Equal[NotFilter[_]] = Equal.make { (f, s) => + LogFilter.equal.equal(f.filter, s.filter) + } + } + + private[logging] final case class CachedFilter[M](filter: LogFilter[M]) extends LogFilter[M] { + + private[logging] val cache = new java.util.concurrent.ConcurrentHashMap[filter.Value, Boolean]() + + override type Value = filter.Value + + override def group: LogGroup[M, Value] = filter.group + + override def predicate(value: Value): Boolean = + cache.computeIfAbsent( + value, + _ => filter.predicate(value) + ) + + def clear(): Unit = + cache.clear() + } + + private[logging] object CachedFilter { + implicit val equal: Equal[CachedFilter[_]] = Equal.make { (f, s) => + LogFilter.equal.equal(f.filter, s.filter) + } + } + + private[logging] final case class ConfiguredFilter[M, C](config: C, make: C => LogFilter[M]) extends LogFilter[M] { + val filter: LogFilter[M] = make(config) + + override type Value = filter.Value + + override def group: LogGroup[M, Value] = filter.group + + override def predicate(value: Value): Boolean = filter.predicate(value) + + def witConfig(newConfig: C): ConfiguredFilter[M, C] = ConfiguredFilter(newConfig, make) + } + + private[logging] object ConfiguredFilter { + implicit val equal: Equal[ConfiguredFilter[_, _]] = Equal.default.contramap(_.config) + } + /** * Defines a filter from a list of log-levels specified per tree node * @@ -157,10 +247,27 @@ object LogFilter { * @param rootLevel Minimum log level for the root node * @param mappings List of mappings, nesting defined by dot-separated strings */ - final case class LogLevelByNameConfig(rootLevel: LogLevel, mappings: Map[String, LogLevel]) + final case class LogLevelByNameConfig(rootLevel: LogLevel, mappings: Map[String, LogLevel]) { + + def withRootLevel(rootLevel: LogLevel): LogLevelByNameConfig = + LogLevelByNameConfig(rootLevel, mappings) + + def withMapping(name: String, level: LogLevel): LogLevelByNameConfig = + LogLevelByNameConfig(rootLevel, mappings + (name -> level)) + + def withMappings(mappings: Map[String, LogLevel]): LogLevelByNameConfig = + LogLevelByNameConfig(rootLevel, mappings) + + def toFilter[M]: LogFilter[M] = LogFilter.logLevelByName(this) + } object LogLevelByNameConfig { + def apply(rootLevel: LogLevel, mappings: (String, LogLevel)*): LogLevelByNameConfig = + LogLevelByNameConfig(rootLevel, mappings.toMap) + + val default: LogLevelByNameConfig = LogLevelByNameConfig(LogLevel.Info, Map.empty[String, LogLevel]) + val config: Config[LogLevelByNameConfig] = { val rootLevelConfig = Config.logLevel.nested("rootLevel").withDefault(LogLevel.Info) val mappingsConfig = Config.table("mappings", Config.logLevel).withDefault(Map.empty) @@ -169,16 +276,14 @@ object LogFilter { LogLevelByNameConfig(rootLevel, mappings) } } + + implicit val equal: Equal[LogLevelByNameConfig] = Equal.default } def apply[M, V]( group0: LogGroup[M, V], predicate0: V => Boolean - ): LogFilter[M] = new LogFilter[M] { - override type Value = V - override def group: LogGroup[M, V] = group0 - override def predicate(value: Value): Boolean = predicate0(value) - } + ): LogFilter[M] = GroupPredicateFilter(group0, predicate0) def apply[M]( group0: LogGroup[M, Boolean] @@ -267,42 +372,10 @@ 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) || compareRoutes(l, m), + nameMatcher, mappingsSorted: _* ) } @@ -333,11 +406,51 @@ object LogFilter { logLevelByGroup[M](rootLevel, LogGroup.loggerName, mappings: _*) def logLevelByGroup[M](group: LogGroup[M, String], config: LogLevelByNameConfig): LogFilter[M] = - logLevelByGroup[M](config.rootLevel, group, config.mappings.toList: _*) + ConfiguredFilter[M, LogLevelByNameConfig]( + config, + config => logLevelByGroup[M](config.rootLevel, group, config.mappings.toList: _*) + ) def logLevelByName[M](config: LogLevelByNameConfig): LogFilter[M] = logLevelByGroup[M](LogGroup.loggerName, config) + private[logging] val nameMatcher: (List[String], List[String]) => Boolean = (l, m) => { + + @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 (lh :: ls, mh :: ms) => + anystringCompare(lh, mh.split('*').toList) && compareRoutes(ls, ms) + } + + l.startsWith(m) || compareRoutes(l, m) + } + private[logging] val splitNameByDotAndLevel: (String, LogLevel) => (List[String], LogLevel) = (name, level) => splitNameByDot(name) -> level diff --git a/core/shared/src/main/scala/zio/logging/LogFormat.scala b/core/shared/src/main/scala/zio/logging/LogFormat.scala index e41fad254..bd07af2a7 100644 --- a/core/shared/src/main/scala/zio/logging/LogFormat.scala +++ b/core/shared/src/main/scala/zio/logging/LogFormat.scala @@ -31,7 +31,7 @@ import scala.util.Try * timestamp.fixed(32) |-| level |-| label("message", quoted(line)) * }}} */ -trait LogFormat { self => +sealed trait LogFormat { self => import zio.logging.LogFormat.text /** @@ -47,10 +47,7 @@ trait LogFormat { self => * separator between them. */ final def +(other: LogFormat): LogFormat = - LogFormat.make { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => - self.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) - other.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) - } + LogFormat.ConcatFormat(self, other) /** * Returns a new log format which concats both formats together with a space @@ -76,51 +73,27 @@ trait LogFormat { self => * Returns a new log format that produces the same as this one, if filter is satisfied */ final def filter[M >: String](filter: LogFilter[M]): LogFormat = - LogFormat.make { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => - if (filter(trace, fiberId, level, line, cause, context, spans, annotations)) { - self.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) - } - } + LogFormat.FilteredFormat(self, filter) /** * Returns a new log format that produces the same as this one, but with a * space-padded, fixed-width output. Be careful using this operator, as it * destroys all structure, resulting in purely textual log output. */ - final def fixed(size: Int): LogFormat = - LogFormat.make { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => - val tempBuilder = new StringBuilder - val append = LogAppender.unstructured { (line: String) => - tempBuilder.append(line) - () - } - self.unsafeFormat(append)(trace, fiberId, level, line, cause, context, spans, annotations) - - val messageSize = tempBuilder.size - if (messageSize < size) { - builder.appendText(tempBuilder.take(size).appendAll(Array.fill(size - messageSize)(' ')).toString()) - } else { - builder.appendText(tempBuilder.take(size).toString()) - } - } + final def fixed(size: Int): LogFormat = LogFormat.FixedFormat(self, size) /** * Returns a new log format that produces the same as this one, except that * log levels are colored according to the specified mapping. */ - final def highlight(fn: LogLevel => LogColor): LogFormat = - LogFormat.make { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => - builder.appendText(fn(level).ansi) - try self.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) - finally builder.appendText(LogColor.RESET.ansi) - } + final def highlight(fn: LogLevel => LogColor): LogFormat = LogFormat.HighlightFormat(self, fn) /** * Returns a new log format that produces the same as this one, except that * the log output is highlighted. */ final def highlight: LogFormat = - highlight(defaultHighlighter(_)) + highlight(defaultHighlighter) /** * The alphanumeric version of the `|-|` operator. @@ -192,7 +165,7 @@ trait LogFormat { self => builder.toString() } - private def defaultHighlighter(level: LogLevel) = level match { + private val defaultHighlighter: LogLevel => LogColor = { case LogLevel.Error => LogColor.RED case LogLevel.Warning => LogColor.YELLOW case LogLevel.Info => LogColor.CYAN @@ -203,6 +176,223 @@ trait LogFormat { self => object LogFormat { + private[logging] def makeLogger( + fn: ( + LogAppender, + Trace, + FiberId, + LogLevel, + () => String, + Cause[Any], + FiberRefs, + List[LogSpan], + Map[String, String] + ) => Any + )( + builder: LogAppender + ): ZLogger[String, Unit] = new ZLogger[String, Unit] { + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => String, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Unit = { + fn(builder, trace, fiberId, logLevel, message, cause, context, spans, annotations) + () + } + } + + private[logging] final case class FnFormat( + fn: ( + LogAppender, + Trace, + FiberId, + LogLevel, + () => String, + Cause[Any], + FiberRefs, + List[LogSpan], + Map[String, String] + ) => Any + ) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger(fn)(builder) + } + + private[logging] final case class ConcatFormat(first: LogFormat, second: LogFormat) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => + first.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) + second.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) + () + }(builder) + } + + private[logging] final case class FilteredFormat(format: LogFormat, filter: LogFilter[String]) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => + if (filter(trace, fiberId, level, line, cause, context, spans, annotations)) { + format.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) + } + () + }(builder) + } + + private[logging] final case class HighlightFormat(format: LogFormat, fn: LogLevel => LogColor) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => + builder.appendText(fn(level).ansi) + try format.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) + finally builder.appendText(LogColor.RESET.ansi) + () + }(builder) + } + + private[logging] final case class TextFormat(value: String) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, _, _, _) => + builder.appendText(value) + }(builder) + } + + private[logging] final case class TimestampFormat(formatter: DateTimeFormatter) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, _, _, _) => + val now = ZonedDateTime.now() + val value = formatter.format(now) + + builder.appendText(value) + }(builder) + } + + private[logging] final case class LabelFormat(label: String, format: LogFormat) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => + builder.openKey() + try builder.appendText(label) + finally builder.closeKeyOpenValue() + + try format.unsafeFormat(builder)(trace, fiberId, level, line, cause, context, spans, annotations) + finally builder.closeValue() + }(builder) + } + + private[logging] final case class LoggerNameFormat( + loggerNameExtractor: LoggerNameExtractor, + loggerNameDefault: String + ) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, _, _, _, _, context, _, annotations) => + val loggerName = loggerNameExtractor(trace, context, annotations).getOrElse(loggerNameDefault) + builder.appendText(loggerName) + }(builder) + } + + private[logging] final case class FixedFormat(format: LogFormat, size: Int) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, trace, fiberId, level, line, cause, context, spans, annotations) => + val tempBuilder = new StringBuilder + val append = LogAppender.unstructured { (line: String) => + tempBuilder.append(line) + () + } + format.unsafeFormat(append)(trace, fiberId, level, line, cause, context, spans, annotations) + + val messageSize = tempBuilder.size + if (messageSize < size) { + builder.appendText(tempBuilder.take(size).appendAll(Array.fill(size - messageSize)(' ')).toString()) + } else { + builder.appendText(tempBuilder.take(size).toString()) + } + }(builder) + } + + private[logging] final case class AnnotationFormat(name: String) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, _, _, annotations) => + annotations.get(name).foreach { value => + builder.appendKeyValue(name, value) + } + }(builder) + } + + private[logging] final case class AnnotationsFormat(excludeKeys: Set[String]) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, _, _, annotations) => + builder.appendKeyValues(annotations.filterNot(kv => excludeKeys.contains(kv._1))) + }(builder) + } + + private[logging] final case class LogAnnotationFormat[A](annotation: LogAnnotation[A]) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, fiberRefs, _, _) => + fiberRefs + .get(logContext) + .foreach { context => + context.get(annotation).foreach { value => + builder.appendKeyValue(annotation.name, annotation.render(value)) + } + } + }(builder) + } + + private[logging] final case class LogAnnotationsFormat(excludeKeys: Set[String]) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, fiberRefs, _, _) => + fiberRefs + .get(logContext) + .foreach { context => + builder.appendKeyValues(context.asMap.filterNot(kv => excludeKeys.contains(kv._1))) + } + () + }(builder) + } + + private[logging] final case class AllAnnotationsFormat(excludeKeys: Set[String]) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, fiberRefs, _, annotations) => + val keyValues = annotations.filterNot(kv => excludeKeys.contains(kv._1)).toList ++ fiberRefs + .get(logContext) + .map { context => + context.asMap.filterNot(kv => excludeKeys.contains(kv._1)).toList + } + .getOrElse(Nil) + builder.appendKeyValues(keyValues) + () + }(builder) + } + + private[logging] final case class AnyAnnotationFormat(name: String) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, fiberRefs, _, annotations) => + annotations + .get(name) + .orElse( + fiberRefs + .get(logContext) + .flatMap(_.get(name)) + ) + .foreach { value => + builder.appendKeyValue(name, value) + } + }(builder) + } + + private[logging] final case class SpanFormat(name: String) extends LogFormat { + override private[logging] def unsafeFormat(builder: LogAppender): ZLogger[String, Unit] = + makeLogger { (builder, _, _, _, _, _, _, spans, _) => + spans.find(_.label == name).foreach { span => + val duration = (java.lang.System.currentTimeMillis() - span.startTime).toString + builder.appendKeyValue(name, s"${duration}ms") + } + }(builder) + } + /** * A `Pattern` is string representation of `LogFormat` */ @@ -574,6 +764,13 @@ object LogFormat { def parse(pattern: String): Either[Parser.ParserError[String], Pattern] = syntax.parseString(pattern) + + val config: Config[Pattern] = Config.string.mapOrFail { value => + Pattern.parse(value) match { + case Right(p) => Right(p) + case Left(_) => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a Pattern, but found ${value}")) + } + } } private val NL = System.lineSeparator() @@ -597,99 +794,39 @@ object LogFormat { List[LogSpan], Map[String, String] ) => Any - ): LogFormat = { (builder: LogAppender) => - new ZLogger[String, Unit] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => String, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Unit = { - format(builder, trace, fiberId, logLevel, message, cause, context, spans, annotations) - () - } - } - } + ): LogFormat = FnFormat(format) def loggerName(loggerNameExtractor: LoggerNameExtractor, loggerNameDefault: String = "zio-logger"): LogFormat = - LogFormat.make { (builder, trace, _, _, _, _, context, _, annotations) => - val loggerName = loggerNameExtractor(trace, context, annotations).getOrElse(loggerNameDefault) - builder.appendText(loggerName) - } + LoggerNameFormat(loggerNameExtractor, loggerNameDefault) def annotation(name: String): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, _, _, annotations) => - annotations.get(name).foreach { value => - builder.appendKeyValue(name, value) - } - } + AnnotationFormat(name) def logAnnotation[A](ann: LogAnnotation[A]): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, fiberRefs, _, _) => - fiberRefs - .get(logContext) - .foreach { context => - context.get(ann).foreach { value => - builder.appendKeyValue(ann.name, ann.render(value)) - } - } - } + LogAnnotationFormat(ann) def annotation[A](ann: LogAnnotation[A]): LogFormat = logAnnotation(ann) def anyAnnotation(name: String): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, fiberRefs, _, annotations) => - annotations - .get(name) - .orElse( - fiberRefs - .get(logContext) - .flatMap(_.get(name)) - ) - .foreach { value => - builder.appendKeyValue(name, value) - } - } + AnyAnnotationFormat(name) /** * Returns a new log format that appends all annotations to the log output. */ - def annotations: LogFormat = annotations(Set.empty) + val annotations: LogFormat = annotations(Set.empty) def annotations(excludeKeys: Set[String]): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, _, _, annotations) => - builder.appendKeyValues(annotations.filterNot(kv => excludeKeys.contains(kv._1))) - } + AnnotationsFormat(excludeKeys) - def logAnnotations: LogFormat = logAnnotations(Set.empty) + val logAnnotations: LogFormat = logAnnotations(Set.empty) def logAnnotations(excludeKeys: Set[String]): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, fiberRefs, _, _) => - fiberRefs - .get(logContext) - .foreach { context => - builder.appendKeyValues(context.asMap.filterNot(kv => excludeKeys.contains(kv._1))) - } - () - } + LogAnnotationsFormat(excludeKeys) - def allAnnotations: LogFormat = allAnnotations(Set.empty) + val allAnnotations: LogFormat = allAnnotations(Set.empty) - def allAnnotations(excludeKeys: Set[String]): LogFormat = LogFormat.make { - (builder, _, _, _, _, _, fiberRefs, _, annotations) => - val keyValues = annotations.filterNot(kv => excludeKeys.contains(kv._1)).toList ++ fiberRefs - .get(logContext) - .map { context => - context.asMap.filterNot(kv => excludeKeys.contains(kv._1)).toList - } - .getOrElse(Nil) - - builder.appendKeyValues(keyValues) - } + def allAnnotations(excludeKeys: Set[String]): LogFormat = + AllAnnotationsFormat(excludeKeys) def bracketed(inner: LogFormat): LogFormat = bracketStart + inner + bracketEnd @@ -744,19 +881,7 @@ object LogFormat { } } - @deprecated("use LogFormat.filter", "2.1.2") - def ifCauseNonEmpty(format: LogFormat): LogFormat = - format.filter(LogFilter.causeNonEmpty) - - def label(label: => String, value: LogFormat): LogFormat = - LogFormat.make { (builder, trace, fiberId, logLevel, message, cause, context, spans, annotations) => - builder.openKey() - try builder.appendText(label) - finally builder.closeKeyOpenValue() - - try value.unsafeFormat(builder)(trace, fiberId, logLevel, message, cause, context, spans, annotations) - finally builder.closeValue() - } + def label(label: => String, value: LogFormat): LogFormat = LabelFormat(label, value) val newLine: LogFormat = text(NL) @@ -770,17 +895,12 @@ object LogFormat { * Returns a new log format that appends the specified span to the log output. */ def span(name: String): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, _, spans, _) => - spans.find(_.label == name).foreach { span => - val duration = (java.lang.System.currentTimeMillis() - span.startTime).toString - builder.appendKeyValue(name, s"${duration}ms") - } - } + SpanFormat(name) /** * Returns a new log format that appends all spans to the log output. */ - def spans: LogFormat = + val spans: LogFormat = LogFormat.make { (builder, _, _, _, _, _, _, spans, _) => builder.appendKeyValues(spans.map { span => val duration = (java.lang.System.currentTimeMillis() - span.startTime).toString @@ -788,18 +908,11 @@ object LogFormat { }) } - def text(value: => String): LogFormat = - LogFormat.make { (builder, _, _, _, _, _, _, _, _) => - builder.appendText(value) - } + def text(value: => String): LogFormat = TextFormat(value) val timestamp: LogFormat = timestamp(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - def timestamp(formatter: => DateTimeFormatter): LogFormat = - text { - val now = ZonedDateTime.now() - formatter.format(now) - } + def timestamp(formatter: => DateTimeFormatter): LogFormat = TimestampFormat(formatter) val default: LogFormat = label("timestamp", timestamp.fixed(32)) |-| diff --git a/core/shared/src/main/scala/zio/logging/LogGroup.scala b/core/shared/src/main/scala/zio/logging/LogGroup.scala index 8a20612a1..eff4e472f 100644 --- a/core/shared/src/main/scala/zio/logging/LogGroup.scala +++ b/core/shared/src/main/scala/zio/logging/LogGroup.scala @@ -17,7 +17,7 @@ package zio.logging import zio.{ Cause, FiberId, FiberRefs, LogLevel, LogSpan, Trace, Zippable } -trait LogGroup[-Message, Out] { self => +sealed trait LogGroup[-Message, Out] { self => def apply( trace: Trace, @@ -39,24 +39,95 @@ trait LogGroup[-Message, Out] { self => other ) - final def contramap[M](f: M => Message): LogGroup[M, Out] = new LogGroup[M, Out] { + final def contramap[M](f: M => Message): LogGroup[M, Out] = LogGroup.ContramapGroup(self, f) + + /** + * Returns new log group whose result is mapped by the specified f function. + */ + final def map[O](f: Out => O): LogGroup[Message, O] = LogGroup.MapGroup(self, f) + + /** + * Combine this log group with specified log group + */ + final def zip[M <: Message, O, Out2]( + other: LogGroup[M, O] + )(implicit zippable: Zippable.Out[Out, O, Out2]): LogGroup[M, Out2] = LogGroup.ZipGroup(self, other) + + /** + * Zips this log group together with the specified log group using the combination functions. + */ + final def zipWith[M <: Message, O, Out2]( + other: LogGroup[M, O] + )(f: (Out, O) => Out2): LogGroup[M, Out2] = LogGroup.ZipWithGroup(self, other, f) + +} + +object LogGroup { + + private[logging] final case class FnGroup[-Message, Out]( + fn: ( + Trace, + FiberId, + LogLevel, + () => Message, + Cause[Any], + FiberRefs, + List[LogSpan], + Map[String, String] + ) => Out + ) extends LogGroup[Message, Out] { override def apply( trace: Trace, fiberId: FiberId, logLevel: LogLevel, - message: () => M, + message: () => Message, cause: Cause[Any], context: FiberRefs, spans: List[LogSpan], annotations: Map[String, String] ): Out = - self(trace, fiberId, logLevel, () => f(message()), cause, context, spans, annotations) + fn(trace, fiberId, logLevel, message, cause, context, spans, annotations) } - /** - * Returns new log group whose result is mapped by the specified f function. - */ - final def map[O](f: Out => O): LogGroup[Message, O] = new LogGroup[Message, O] { + private[logging] final case class LoggerNameExtractorGroup( + loggerNameExtractor: LoggerNameExtractor, + loggerNameDefault: String + ) extends LogGroup[Any, String] { + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Any, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): String = + loggerNameExtractor(trace, context, annotations).getOrElse(loggerNameDefault) + } + + private[logging] final case class ConstantGroup[Output]( + constant: Output + ) extends LogGroup[Any, Output] { + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Any, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Output = + constant + } + + private[logging] final case class ZipGroup[Message, Out1, Out2, Out]( + first: LogGroup[Message, Out1], + second: LogGroup[Message, Out2] + )(implicit zippable: Zippable.Out[Out1, Out2, Out]) + extends LogGroup[Message, Out] { + override def apply( trace: Trace, fiberId: FiberId, @@ -66,75 +137,78 @@ trait LogGroup[-Message, Out] { self => context: FiberRefs, spans: List[LogSpan], annotations: Map[String, String] - ): O = - f(self(trace, fiberId, logLevel, message, cause, context, spans, annotations)) + ): Out = + zippable.zip( + first(trace, fiberId, logLevel, message, cause, context, spans, annotations), + second(trace, fiberId, logLevel, message, cause, context, spans, annotations) + ) } - /** - * Combine this log group with specified log group - */ - final def zip[M <: Message, O, Out2]( - other: LogGroup[M, O] - )(implicit zippable: Zippable.Out[Out, O, Out2]): LogGroup[M, Out2] = - new LogGroup[M, Out2] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => M, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Out2 = - zippable.zip( - self(trace, fiberId, logLevel, message, cause, context, spans, annotations), - other(trace, fiberId, logLevel, message, cause, context, spans, annotations) - ) - } + private[logging] final case class ZipWithGroup[Message, Out1, Out2, Out]( + first: LogGroup[Message, Out1], + second: LogGroup[Message, Out2], + fn: (Out1, Out2) => Out + ) extends LogGroup[Message, Out] { - /** - * Zips this log group together with the specified log group using the combination functions. - */ - final def zipWith[M <: Message, O, Out2]( - other: LogGroup[M, O] - )(f: (Out, O) => Out2): LogGroup[M, Out2] = new LogGroup[M, Out2] { override def apply( trace: Trace, fiberId: FiberId, logLevel: LogLevel, - message: () => M, + message: () => Message, cause: Cause[Any], context: FiberRefs, spans: List[LogSpan], annotations: Map[String, String] - ): Out2 = - f( - self(trace, fiberId, logLevel, message, cause, context, spans, annotations), - other(trace, fiberId, logLevel, message, cause, context, spans, annotations) + ): Out = + fn( + first(trace, fiberId, logLevel, message, cause, context, spans, annotations), + second(trace, fiberId, logLevel, message, cause, context, spans, annotations) ) } -} + private[logging] final case class MapGroup[Message, Out1, Out2]( + group: LogGroup[Message, Out1], + fn: Out1 => Out2 + ) extends LogGroup[Message, Out2] { -object LogGroup { + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Message, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Out2 = + fn( + group(trace, fiberId, logLevel, message, cause, context, spans, annotations) + ) + } + + private[logging] final case class ContramapGroup[Message1, Message2, Out]( + group: LogGroup[Message1, Out], + fn: Message2 => Message1 + ) extends LogGroup[Message2, Out] { - def apply[M, O]( - f: (Trace, FiberId, LogLevel, () => M, Cause[Any], FiberRefs, List[LogSpan], Map[String, String]) => O - ): LogGroup[M, O] = new LogGroup[M, O] { override def apply( trace: Trace, fiberId: FiberId, logLevel: LogLevel, - message: () => M, + message: () => Message2, cause: Cause[Any], context: FiberRefs, spans: List[LogSpan], annotations: Map[String, String] - ): O = - f(trace, fiberId, logLevel, message, cause, context, spans, annotations) + ): Out = + group(trace, fiberId, logLevel, () => fn(message()), cause, context, spans, annotations) + } + def apply[M, O]( + fn: (Trace, FiberId, LogLevel, () => M, Cause[Any], FiberRefs, List[LogSpan], Map[String, String]) => O + ): LogGroup[M, O] = FnGroup(fn) + /** * Log group by cause */ @@ -143,15 +217,12 @@ object LogGroup { /** * Log group with given constant value */ - def constant[O](value: O): LogGroup[Any, O] = apply((_, _, _, _, _, _, _, _) => value) + def constant[O](value: O): LogGroup[Any, O] = ConstantGroup(value) def fromLoggerNameExtractor( loggerNameExtractor: LoggerNameExtractor, loggerNameDefault: String = "zio-logger" - ): LogGroup[Any, String] = - apply((trace, _, _, _, _, context, _, annotations) => - loggerNameExtractor(trace, context, annotations).getOrElse(loggerNameDefault) - ) + ): LogGroup[Any, String] = LoggerNameExtractorGroup(loggerNameExtractor, loggerNameDefault) /** * Log group by level diff --git a/core/shared/src/main/scala/zio/logging/LoggerLayers.scala b/core/shared/src/main/scala/zio/logging/LoggerLayers.scala new file mode 100644 index 000000000..f00ad2ab8 --- /dev/null +++ b/core/shared/src/main/scala/zio/logging/LoggerLayers.scala @@ -0,0 +1,276 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging + +import zio.metrics.Metric +import zio.{ Config, NonEmptyChunk, Queue, Runtime, Scope, Tag, UIO, ZIO, ZLayer, ZLogger } + +import java.io.PrintStream +import java.nio.charset.Charset +import java.nio.file.Path + +private[logging] trait LoggerLayers { + + private[logging] val logLevelMetricLabel = "level" + + private[logging] val loggedTotalMetric = Metric.counter(name = "zio_log_total") + + val logMetrics: ZLayer[Any, Nothing, Unit] = + makeMetricLogger(loggedTotalMetric, logLevelMetricLabel).install + + def logMetricsWith(name: String, logLevelLabel: String): ZLayer[Any, Nothing, Unit] = + makeMetricLogger(Metric.counter(name), logLevelLabel).install + + def consoleErrLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeConsoleErrLogger(config).install + + def consoleErrJsonLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeConsoleErrJsonLogger(config).install + + def consoleErrJsonLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + ConsoleLoggerConfig.load(configPath).flatMap(makeConsoleErrJsonLogger).install + + def consoleErrLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + ConsoleLoggerConfig.load(configPath).flatMap(makeConsoleErrLogger).install + + def consoleJsonLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeConsoleJsonLogger(config).install + + def consoleJsonLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + ConsoleLoggerConfig.load(configPath).flatMap(makeConsoleJsonLogger).install + + def consoleLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeConsoleLogger(config).install + + def consoleLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + ConsoleLoggerConfig.load(configPath).flatMap(makeConsoleLogger).install + + def fileAsyncJsonLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeFileAsyncJsonLogger(config).installUnscoped + + def fileAsyncJsonLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + FileLoggerConfig.load(configPath).flatMap(makeFileAsyncJsonLogger).installUnscoped + + def fileAsyncLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeFileAsyncLogger(config).installUnscoped + + def fileAsyncLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + FileLoggerConfig.load(configPath).flatMap(makeFileAsyncLogger).installUnscoped + + def fileJsonLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeFileJsonLogger(config).install + + def fileJsonLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + FileLoggerConfig.load(configPath).flatMap(makeFileJsonLogger).install + + def fileLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = + makeFileLogger(config).install + + def fileLogger(configPath: NonEmptyChunk[String] = loggerConfigPath): ZLayer[Any, Config.Error, Unit] = + FileLoggerConfig.load(configPath).flatMap(makeFileLogger).install + + def makeConsoleErrLogger(config: ConsoleLoggerConfig): ZIO[Any, Nothing, ZLogger[String, Any]] = + makeSystemErrLogger(config.format.toLogger).filter(config.toFilter) + + def makeConsoleErrJsonLogger(config: ConsoleLoggerConfig): ZIO[Any, Nothing, ZLogger[String, Any]] = + makeSystemErrLogger(config.format.toJsonLogger).filter(config.toFilter) + + def makeConsoleLogger(config: ConsoleLoggerConfig): ZIO[Any, Nothing, ZLogger[String, Any]] = + makeSystemOutLogger(config.format.toLogger).filter(config.toFilter) + + def makeConsoleJsonLogger(config: ConsoleLoggerConfig): ZIO[Any, Nothing, ZLogger[String, Any]] = + makeSystemOutLogger(config.format.toJsonLogger).filter(config.toFilter) + + def makeSystemOutLogger( + logger: ZLogger[String, String] + ): ZIO[Any, Nothing, ZLogger[String, Any]] = makePrintStreamLogger(logger, java.lang.System.out) + + def makeSystemErrLogger( + logger: ZLogger[String, String] + ): ZIO[Any, Nothing, ZLogger[String, Any]] = makePrintStreamLogger(logger, java.lang.System.err) + + def makePrintStreamLogger( + logger: ZLogger[String, String], + stream: PrintStream + ): ZIO[Any, Nothing, ZLogger[String, Any]] = ZIO.succeed(printStreamLogger(logger, stream)) + + private def printStreamLogger( + logger: ZLogger[String, String], + stream: PrintStream + ): ZLogger[String, Any] = { + val stringLogger = logger.map { line => + try stream.println(line) + catch { + case t: VirtualMachineError => throw t + case _: Throwable => () + } + } + stringLogger + } + + def makeFileAsyncJsonLogger(config: FileLoggerConfig): ZIO[Scope, Nothing, FilteredLogger[String, Any]] = + makeFileAsyncLogger( + config.destination, + config.format.toJsonLogger, + config.charset, + config.autoFlushBatchSize, + config.bufferedIOSize, + config.rollingPolicy + ).filter(config.toFilter) + + def makeFileAsyncLogger(config: FileLoggerConfig): ZIO[Scope, Nothing, FilteredLogger[String, Any]] = + makeFileAsyncLogger( + config.destination, + config.format.toLogger, + config.charset, + config.autoFlushBatchSize, + config.bufferedIOSize, + config.rollingPolicy + ).filter(config.toFilter) + + def makeFileAsyncLogger( + destination: Path, + logger: ZLogger[String, String], + charset: Charset, + autoFlushBatchSize: Int, + bufferedIOSize: Option[Int], + rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] + ): ZIO[Scope, Nothing, ZLogger[String, Any]] = + for { + queue <- Queue.bounded[UIO[Any]](1000) + _ <- queue.take.flatMap(task => task.ignore).forever.forkScoped + } yield fileWriterAsyncLogger( + destination, + logger, + charset, + autoFlushBatchSize, + bufferedIOSize, + queue, + rollingPolicy + ) + + private def fileWriterAsyncLogger( + destination: Path, + logger: ZLogger[String, String], + charset: Charset, + autoFlushBatchSize: Int, + bufferedIOSize: Option[Int], + queue: Queue[UIO[Any]], + rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] + ): ZLogger[String, Any] = { + val logWriter = + new zio.logging.internal.FileWriter(destination, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) + + val stringLogger: ZLogger[String, Any] = logger.map { (line: String) => + zio.Unsafe.unsafe { implicit u => + Runtime.default.unsafe.run(queue.offer(ZIO.succeed { + try logWriter.writeln(line) + catch { + case t: VirtualMachineError => throw t + case _: Throwable => () + } + })) + } + } + stringLogger + } + + def makeFileJsonLogger(config: FileLoggerConfig): ZIO[Any, Nothing, FilteredLogger[String, Any]] = + makeFileLogger( + config.destination, + config.format.toJsonLogger, + config.charset, + config.autoFlushBatchSize, + config.bufferedIOSize, + config.rollingPolicy + ).filter(config.toFilter) + + def makeFileLogger(config: FileLoggerConfig): ZIO[Any, Nothing, FilteredLogger[String, Any]] = + makeFileLogger( + config.destination, + config.format.toLogger, + config.charset, + config.autoFlushBatchSize, + config.bufferedIOSize, + config.rollingPolicy + ).filter(config.toFilter) + + def makeFileLogger( + destination: Path, + logger: ZLogger[String, String], + charset: Charset, + autoFlushBatchSize: Int, + bufferedIOSize: Option[Int], + rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] + ): ZIO[Any, Nothing, ZLogger[String, Any]] = + ZIO.succeed( + fileWriterLogger(destination, logger, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) + ) + + private def fileWriterLogger( + destination: Path, + logger: ZLogger[String, String], + charset: Charset, + autoFlushBatchSize: Int, + bufferedIOSize: Option[Int], + rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] + ): ZLogger[String, Any] = { + val logWriter = + new zio.logging.internal.FileWriter(destination, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) + + val stringLogger: ZLogger[String, Any] = logger.map { (line: String) => + try logWriter.writeln(line) + catch { + case t: VirtualMachineError => throw t + case _: Throwable => () + } + } + + stringLogger + } + + def makeMetricLogger(counter: Metric.Counter[Long], logLevelLabel: String): ZIO[Any, Nothing, MetricLogger] = + ZIO.succeed(MetricLogger(counter, logLevelLabel)) + + implicit final class ZLoggerZIOLayerOps[RIn, +E, ROut <: ZLogger[String, Any]: Tag]( + private val self: ZIO[RIn, E, ROut] + ) { + + def filter(filter: LogFilter[String]): ZIO[RIn, E, FilteredLogger[String, Any]] = + self.map(logger => FilteredLogger(logger, filter)) + + def install: ZLayer[RIn, E, Unit] = + ZLayer.scoped[RIn] { + self.flatMap { logger => + ZIO.withLoggerScoped(logger) + } + } + + def installUnscoped[RIn2](implicit ev: RIn2 with Scope =:= RIn): ZLayer[RIn2, E, Unit] = + ZLayer.scoped[RIn2] { + self.asInstanceOf[ZIO[RIn2 with Scope, E, ROut]].flatMap { logger => + ZIO.withLoggerScoped(logger) + } + } + + def installScoped: ZLayer[Scope with RIn, E, Unit] = + ZLayer.fromZIO(self.flatMap { logger => + ZIO.withLoggerScoped(logger) + }) + + } + +} diff --git a/core/shared/src/main/scala/zio/logging/LoggerNameExtractor.scala b/core/shared/src/main/scala/zio/logging/LoggerNameExtractor.scala index 31fd4b461..017fea25c 100644 --- a/core/shared/src/main/scala/zio/logging/LoggerNameExtractor.scala +++ b/core/shared/src/main/scala/zio/logging/LoggerNameExtractor.scala @@ -17,7 +17,7 @@ package zio.logging import zio.{ FiberRefs, Trace } -trait LoggerNameExtractor { self => +sealed trait LoggerNameExtractor { self => def apply( trace: Trace, @@ -35,7 +35,7 @@ trait LoggerNameExtractor { self => * The alphanumeric version of the `||` operator. */ final def or(other: LoggerNameExtractor): LoggerNameExtractor = - (trace, context, annotations) => self(trace, context, annotations).orElse(other(trace, context, annotations)) + LoggerNameExtractor.OrExtractor(self, other) /** * Converts this extractor into a log format @@ -52,13 +52,33 @@ trait LoggerNameExtractor { self => object LoggerNameExtractor { + private[logging] final case class FnExtractor(fn: (Trace, FiberRefs, Map[String, String]) => Option[String]) + extends LoggerNameExtractor { + override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = + fn(trace, context, annotations) + } + + private[logging] final case class AnnotationExtractor(name: String) extends LoggerNameExtractor { + override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = + annotations.get(name) + } + + private[logging] final case class OrExtractor(first: LoggerNameExtractor, second: LoggerNameExtractor) + extends LoggerNameExtractor { + + override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = + first(trace, context, annotations).orElse(second(trace, context, annotations)) + } + + def make(fn: (Trace, FiberRefs, Map[String, String]) => Option[String]): LoggerNameExtractor = FnExtractor(fn) + /** * Extractor which take logger name from [[Trace]] * * trace with value ''example.LivePingService.ping(PingService.scala:22)'' * will have ''example.LivePingService'' as logger name */ - val trace: LoggerNameExtractor = (trace, _, _) => + val trace: LoggerNameExtractor = FnExtractor((trace, _, _) => trace match { case Trace(location, _, _) => val last = location.lastIndexOf(".") @@ -68,13 +88,14 @@ object LoggerNameExtractor { Some(name) case _ => None } + ) /** * Extractor which take logger name from annotation * * @param name name of annotation */ - def annotation(name: String): LoggerNameExtractor = (_, _, annotations) => annotations.get(name) + def annotation(name: String): LoggerNameExtractor = AnnotationExtractor(name) /** * Extractor which take logger name from annotation or [[Trace]] if specified annotation is not present diff --git a/core/shared/src/main/scala/zio/logging/MetricLogger.scala b/core/shared/src/main/scala/zio/logging/MetricLogger.scala new file mode 100644 index 000000000..10ee509a0 --- /dev/null +++ b/core/shared/src/main/scala/zio/logging/MetricLogger.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging + +import zio.metrics.{ Metric, MetricLabel } +import zio.{ Cause, FiberId, FiberRef, FiberRefs, LogLevel, LogSpan, Trace, Unsafe, ZLogger } + +final case class MetricLogger(counter: Metric.Counter[Long], logLevelLabel: String) extends ZLogger[String, Unit] { + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => String, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Unit = { + val tags = context.get(FiberRef.currentTags).getOrElse(Set.empty) + counter.unsafe.update(1, tags + MetricLabel(logLevelLabel, logLevel.label.toLowerCase))(Unsafe.unsafe) + () + } + +} diff --git a/core/shared/src/main/scala/zio/logging/ReconfigurableLogger.scala b/core/shared/src/main/scala/zio/logging/ReconfigurableLogger.scala new file mode 100644 index 000000000..920ec06ba --- /dev/null +++ b/core/shared/src/main/scala/zio/logging/ReconfigurableLogger.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging + +import zio._ +import zio.prelude._ + +import java.util.concurrent.atomic.AtomicReference + +sealed trait ReconfigurableLogger[-Message, +Output, Config] extends ZLogger[Message, Output] { + + def get: (Config, ZLogger[Message, Output]) + + def set[M <: Message, O >: Output](config: Config, logger: ZLogger[M, O]): Unit +} + +object ReconfigurableLogger { + + def apply[Message, Output, Config]( + config: Config, + logger: ZLogger[Message, Output] + ): ReconfigurableLogger[Message, Output, Config] = { + val configuredLogger: AtomicReference[(Config, ZLogger[Message, Output])] = + new AtomicReference[(Config, ZLogger[Message, Output])]((config, logger)) + + new ReconfigurableLogger[Message, Output, Config] { + + override def get: (Config, ZLogger[Message, Output]) = configuredLogger.get() + + override def set[M <: Message, O >: Output](config: Config, logger: ZLogger[M, O]): Unit = + configuredLogger.set((config, logger.asInstanceOf[ZLogger[Message, Output]])) + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Message, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Output = + configuredLogger.get()._2.apply(trace, fiberId, logLevel, message, cause, context, spans, annotations) + } + } + + def make[R, E, M, O, C: Equal]( + loadConfig: => ZIO[R, E, C], + makeLogger: (C, Option[ZLogger[M, O]]) => ZIO[R, E, ZLogger[M, O]], + updateLogger: Schedule[R, Any, Any] = Schedule.fixed(10.seconds) + ): ZIO[R with Scope, E, ReconfigurableLogger[M, O, C]] = + for { + initialConfig <- loadConfig + initialLogger <- makeLogger(initialConfig, None) + reconfigurableLogger = ReconfigurableLogger[M, O, C](initialConfig, initialLogger) + _ <- loadConfig.flatMap { newConfig => + val (currentConfig, currentLogger) = reconfigurableLogger.get + if (currentConfig !== newConfig) { + makeLogger(newConfig, Some(currentLogger)).map { newLogger => + reconfigurableLogger.set(newConfig, newLogger) + }.unit + } else ZIO.unit + }.schedule(updateLogger).forkScoped + } yield reconfigurableLogger + +} diff --git a/core/shared/src/main/scala/zio/logging/internal/JsonValidator.scala b/core/shared/src/main/scala/zio/logging/internal/JsonValidator.scala index 8f7ac6f8f..a5b6b679a 100644 --- a/core/shared/src/main/scala/zio/logging/internal/JsonValidator.scala +++ b/core/shared/src/main/scala/zio/logging/internal/JsonValidator.scala @@ -1,3 +1,18 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package zio.logging.internal import scala.annotation.tailrec diff --git a/core/shared/src/main/scala/zio/logging/internal/WriterProvider.scala b/core/shared/src/main/scala/zio/logging/internal/WriterProvider.scala index 0b60ce2f0..d07e22cac 100644 --- a/core/shared/src/main/scala/zio/logging/internal/WriterProvider.scala +++ b/core/shared/src/main/scala/zio/logging/internal/WriterProvider.scala @@ -1,3 +1,18 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package zio.logging.internal import java.io.{ BufferedWriter, FileOutputStream, OutputStreamWriter, Writer } diff --git a/core/shared/src/main/scala/zio/logging/package.scala b/core/shared/src/main/scala/zio/logging/package.scala index 08c578685..ccc03f802 100644 --- a/core/shared/src/main/scala/zio/logging/package.scala +++ b/core/shared/src/main/scala/zio/logging/package.scala @@ -15,13 +15,7 @@ */ package zio -import zio.metrics.{ Metric, MetricLabel } - -import java.io.PrintStream -import java.nio.charset.{ Charset, StandardCharsets } -import java.nio.file.Path - -package object logging { +package object logging extends LoggerLayers { /** * The [[logContext]] fiber reference is used to store typed, structured log @@ -50,9 +44,7 @@ package object logging { */ val loggerNameAnnotationKey = "logger_name" - private[logging] val logLevelMetricLabel = "level" - - private[logging] val loggedTotalMetric = Metric.counter(name = "zio_log_total") + val loggerConfigPath: NonEmptyChunk[String] = NonEmptyChunk("logger") /** * Logger name aspect, by this aspect is possible to set logger name (in general, logger name is extracted from [[Trace]]) @@ -62,456 +54,16 @@ package object logging { def loggerName(value: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = ZIOAspect.annotated(loggerNameAnnotationKey, value) - @deprecated("use zio.logging.consoleLogger", "2.1.10") - def console( - format: LogFormat = LogFormat.colored, - logLevel: LogLevel = LogLevel.Info - ): ZLayer[Any, Nothing, Unit] = - console(format, LogFilter.logLevel(logLevel)) - - @deprecated("use zio.logging.consoleLogger", "2.1.10") - def console( - format: LogFormat, - logFilter: LogFilter[String] - ): ZLayer[Any, Nothing, Unit] = - consoleLogger(ConsoleLoggerConfig(format, logFilter)) - - @deprecated("use zio.logging.consoleErrLogger", "2.1.10") - def consoleErr( - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info - ): ZLayer[Any, Nothing, Unit] = - consoleErr(format, LogFilter.logLevel(logLevel)) - - @deprecated("use zio.logging.consoleErrLogger", "2.1.10") - def consoleErr( - format: LogFormat, - logFilter: LogFilter[String] - ): ZLayer[Any, Nothing, Unit] = - consoleErrLogger(ConsoleLoggerConfig(format, logFilter)) - - @deprecated("use zio.logging.consoleErrJsonLogger", "2.1.10") - def consoleErrJson( - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info - ): ZLayer[Any, Nothing, Unit] = - consoleErrJson(format, LogFilter.logLevel(logLevel)) - - @deprecated("use zio.logging.consoleErrJsonLogger", "2.1.10") - def consoleErrJson( - format: LogFormat, - logFilter: LogFilter[String] - ): ZLayer[Any, Nothing, Unit] = - consoleErrJsonLogger(ConsoleLoggerConfig(format, logFilter)) - - def consoleErrLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeConsoleErrLogger(config)) - - def consoleErrJsonLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeConsoleErrJsonLogger(config)) - - def consoleErrJsonLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(ConsoleLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeConsoleErrJsonLogger(config)) - } yield () - } - - def consoleErrLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(ConsoleLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeConsoleErrLogger(config)) - } yield () - } - - @deprecated("use zio.logging.consoleJsonLogger", "2.1.10") - def consoleJson( - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info - ): ZLayer[Any, Nothing, Unit] = - consoleJson(format, LogFilter.logLevel(logLevel)) - - @deprecated("use zio.logging.consoleJsonLogger", "2.1.10") - def consoleJson( - format: LogFormat, - logFilter: LogFilter[String] - ): ZLayer[Any, Nothing, Unit] = - consoleJsonLogger(ConsoleLoggerConfig(format, logFilter)) - - def consoleJsonLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeConsoleJsonLogger(config)) - - def consoleJsonLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(ConsoleLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeConsoleJsonLogger(config)) - } yield () - } - - def consoleLogger(config: ConsoleLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeConsoleLogger(config)) - - def consoleLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(ConsoleLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeConsoleLogger(config)) - } yield () - } - - @deprecated("use zio.logging.fileLogger", "2.1.10") - def file( - destination: Path, - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info, - charset: Charset = StandardCharsets.UTF_8, - autoFlushBatchSize: Int = 1, - bufferedIOSize: Option[Int] = None, - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None - ): ZLayer[Any, Nothing, Unit] = - file(destination, format, LogFilter.logLevel(logLevel), charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - - @deprecated("use zio.logging.fileLogger", "2.1.10") - def file( - destination: Path, - format: LogFormat, - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLayer[Any, Nothing, Unit] = - fileLogger( - FileLoggerConfig(destination, format, logFilter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - ) - - @deprecated("use zio.logging.fileAsyncLogger", "2.1.10") - def fileAsync( - destination: Path, - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info, - charset: Charset = StandardCharsets.UTF_8, - autoFlushBatchSize: Int = 1, - bufferedIOSize: Option[Int] = None, - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None - ): ZLayer[Any, Nothing, Unit] = - fileAsync( - destination, - format, - LogFilter.logLevel(logLevel), - charset, - autoFlushBatchSize, - bufferedIOSize, - rollingPolicy - ) - - @deprecated("use zio.logging.fileAsyncLogger", "2.1.10") - def fileAsync( - destination: Path, - format: LogFormat, - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLayer[Any, Nothing, Unit] = - fileAsyncLogger( - FileLoggerConfig(destination, format, logFilter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - ) - - @deprecated("use zio.logging.fileJsonLogger", "2.1.10") - def fileJson( - destination: Path, - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info, - charset: Charset = StandardCharsets.UTF_8, - autoFlushBatchSize: Int = 1, - bufferedIOSize: Option[Int] = None, - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None - ): ZLayer[Any, Nothing, Unit] = - fileJson( - destination, - format, - LogFilter.logLevel(logLevel), - charset, - autoFlushBatchSize, - bufferedIOSize, - rollingPolicy - ) - - @deprecated("use zio.logging.fileJsonLogger", "2.1.10") - def fileJson( - destination: Path, - format: LogFormat, - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLayer[Any, Nothing, Unit] = - fileJsonLogger( - FileLoggerConfig(destination, format, logFilter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - ) - - @deprecated("use zio.logging.fileAsyncJsonLogger", "2.1.10") - def fileAsyncJson( - destination: Path, - format: LogFormat = LogFormat.default, - logLevel: LogLevel = LogLevel.Info, - charset: Charset = StandardCharsets.UTF_8, - autoFlushBatchSize: Int = 1, - bufferedIOSize: Option[Int] = None, - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None - ): ZLayer[Any, Nothing, Unit] = - fileAsyncJson( - destination, - format, - LogFilter.logLevel(logLevel), - charset, - autoFlushBatchSize, - bufferedIOSize, - rollingPolicy - ) - - @deprecated("use zio.logging.fileAsyncJsonLogger", "2.1.10") - def fileAsyncJson( - destination: Path, - format: LogFormat, - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLayer[Any, Nothing, Unit] = - fileAsyncJsonLogger( - FileLoggerConfig(destination, format, logFilter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - ) - - def fileAsyncJsonLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = - ZLayer.scoped(makeFileAsyncJsonLogger(config)) - - def fileAsyncJsonLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(FileLoggerConfig.config.nested(configPath)) - _ <- makeFileAsyncJsonLogger(config) - } yield () - } - - def fileAsyncLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = - ZLayer.scoped(makeFileAsyncLogger(config)) - - def fileAsyncLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(FileLoggerConfig.config.nested(configPath)) - _ <- makeFileAsyncLogger(config) - } yield () - } - - def fileJsonLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeFileJsonLogger(config)) - - def fileJsonLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(FileLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeFileJsonLogger(config)) - } yield () - } - - def fileLogger(config: FileLoggerConfig): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeFileLogger(config)) - - def fileLogger(configPath: String = "logger"): ZLayer[Any, Config.Error, Unit] = - ZLayer.scoped { - for { - config <- ZIO.config(FileLoggerConfig.config.nested(configPath)) - _ <- ZIO.withLoggerScoped(makeFileLogger(config)) - } yield () - } - - val logMetrics: ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeMetricLogger(loggedTotalMetric, logLevelMetricLabel)) - - def logMetricsWith(name: String, logLevelLabel: String): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(makeMetricLogger(Metric.counter(name), logLevelLabel)) - - private def makeConsoleErrLogger(config: ConsoleLoggerConfig): ZLogger[String, Any] = - makeConsoleLogger(config.format.toLogger, java.lang.System.err, config.filter) - - private def makeConsoleErrJsonLogger(config: ConsoleLoggerConfig): ZLogger[String, Any] = - makeConsoleLogger(config.format.toJsonLogger, java.lang.System.err, config.filter) - - private def makeConsoleLogger(config: ConsoleLoggerConfig): ZLogger[String, Any] = - makeConsoleLogger(config.format.toLogger, java.lang.System.out, config.filter) - - private def makeConsoleJsonLogger(config: ConsoleLoggerConfig): ZLogger[String, Any] = - makeConsoleLogger(config.format.toJsonLogger, java.lang.System.out, config.filter) - - private def makeConsoleLogger( - logger: ZLogger[String, String], - stream: PrintStream, - logFilter: LogFilter[String] - ): ZLogger[String, Any] = { - - val stringLogger = logFilter.filter(logger.map { line => - try stream.println(line) - catch { - case t: VirtualMachineError => throw t - case _: Throwable => () - } - }) - stringLogger - } - - private def makeFileAsyncJsonLogger( - config: FileLoggerConfig - ): ZIO[Scope, Nothing, Unit] = makeFileAsyncLogger( - config.destination, - config.format.toJsonLogger, - config.filter, - config.charset, - config.autoFlushBatchSize, - config.bufferedIOSize, - config.rollingPolicy - ) - - private def makeFileAsyncLogger( - config: FileLoggerConfig - ): ZIO[Scope, Nothing, Unit] = makeFileAsyncLogger( - config.destination, - config.format.toLogger, - config.filter, - config.charset, - config.autoFlushBatchSize, - config.bufferedIOSize, - config.rollingPolicy - ) - - private def makeFileAsyncLogger( - destination: Path, - logger: ZLogger[String, String], - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZIO[Scope, Nothing, Unit] = - for { - queue <- Queue.bounded[UIO[Any]](1000) - stringLogger = - makeFileAsyncLogger( - destination, - logger, - logFilter, - charset, - autoFlushBatchSize, - bufferedIOSize, - queue, - rollingPolicy - ) - _ <- ZIO.withLoggerScoped(stringLogger) - _ <- queue.take.flatMap(task => task.ignore).forever.forkScoped - } yield () - - private def makeFileAsyncLogger( - destination: Path, - logger: ZLogger[String, String], - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - queue: Queue[UIO[Any]], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLogger[String, Any] = { - val logWriter = - new zio.logging.internal.FileWriter(destination, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - - val stringLogger: ZLogger[String, Any] = logFilter.filter(logger.map { (line: String) => - zio.Unsafe.unsafe { implicit u => - Runtime.default.unsafe.run(queue.offer(ZIO.succeed { - try logWriter.writeln(line) - catch { - case t: VirtualMachineError => throw t - case _: Throwable => () - } - })) - } - }) - stringLogger - } - - private def makeFileJsonLogger(config: FileLoggerConfig): ZLogger[String, Any] = - makeFileLogger( - config.destination, - config.format.toJsonLogger, - config.filter, - config.charset, - config.autoFlushBatchSize, - config.bufferedIOSize, - config.rollingPolicy - ) - - private def makeFileLogger(config: FileLoggerConfig): ZLogger[String, Any] = - makeFileLogger( - config.destination, - config.format.toLogger, - config.filter, - config.charset, - config.autoFlushBatchSize, - config.bufferedIOSize, - config.rollingPolicy - ) - - private def makeFileLogger( - destination: Path, - logger: ZLogger[String, String], - logFilter: LogFilter[String], - charset: Charset, - autoFlushBatchSize: Int, - bufferedIOSize: Option[Int], - rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] - ): ZLogger[String, Any] = { - val logWriter = - new zio.logging.internal.FileWriter(destination, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) - - val stringLogger: ZLogger[String, Any] = logFilter.filter(logger.map { (line: String) => - try logWriter.writeln(line) - catch { - case t: VirtualMachineError => throw t - case _: Throwable => () - } - }) - - stringLogger - } - - private def makeMetricLogger(counter: Metric.Counter[Long], logLevelLabel: String): ZLogger[String, Unit] = - new ZLogger[String, Unit] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => String, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Unit = { - val tags = context.get(FiberRef.currentTags).getOrElse(Set.empty) - counter.unsafe.update(1, tags + MetricLabel(logLevelLabel, logLevel.label.toLowerCase))(Unsafe.unsafe) - () - } - } - - val removeDefaultLoggers: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers - implicit final class LogAnnotationZIOSyntax[R, E, A](private val self: ZIO[R, E, A]) { def logAnnotate[V: Tag](key: LogAnnotation[V], value: V): ZIO[R, E, A] = self @@ key(value) } + + implicit final class ZLoggerOps[-Message, +Output](private val self: ZLogger[Message, Output]) { + + /** + * Returns a version of logger that only logs messages when this filter is satisfied + */ + def filter[M <: Message](filter: LogFilter[M]): ZLogger[M, Option[Output]] = FilteredLogger(self, filter) + } } diff --git a/docs/console-logger.md b/docs/console-logger.md index 310310ab2..8debda568 100644 --- a/docs/console-logger.md +++ b/docs/console-logger.md @@ -11,7 +11,7 @@ import zio.{ ConfigProvider, Runtime } val configProvider: ConfigProvider = ??? -val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> consoleLogger(configPath = "logger") +val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> consoleLogger() ``` logger layer with given configuration: diff --git a/docs/file-logger.md b/docs/file-logger.md index 7e51274f5..867ea5d52 100644 --- a/docs/file-logger.md +++ b/docs/file-logger.md @@ -11,7 +11,7 @@ import zio.{ ConfigProvider, Runtime } val configProvider: ConfigProvider = ??? -val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> fileLogger(configPath = "logger") +val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> fileLogger() ``` logger layer with given configuration: diff --git a/docs/jpl.md b/docs/jpl.md index eb116fbe1..0b5ee3047 100644 --- a/docs/jpl.md +++ b/docs/jpl.md @@ -91,4 +91,11 @@ Oct 28, 2022 1:47:02 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 INFO: user=59c114fd-676d-4df9-a5a0-b8e132468fbf trace_id=7d3e3b84-dd3b-44ff-915a-04fb2d135e28 Stopping user operation Oct 28, 2022 1:47:02 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 INFO: Done -``` \ No newline at end of file +``` + +## Feature changes + +### Version 2.2.0 + +Deprecated log annotation with key `jpl_logger_name` (`JPL.loggerNameAnnotationKey`) removed, +only common log annotation with key `logger_name` (`zio.logging.loggerNameAnnotationKey`) for logger name is supported now. diff --git a/docs/reconfigurable-logger.md b/docs/reconfigurable-logger.md new file mode 100644 index 000000000..d78da4cf7 --- /dev/null +++ b/docs/reconfigurable-logger.md @@ -0,0 +1,258 @@ +--- +id: reconfigurable-logger +title: "Reconfigurable Logger" +--- + +`ReconfigurableLogger` is adding support for updating logger configuration in application runtime. + +logger layer with configuration from `ConfigProvider` (example with [Console Logger)](console-logger.md)): + + +```scala +import zio.logging.{ consoleLogger, ConsoleLoggerConfig, ReconfigurableLogger } +import zio._ + +val configProvider: ConfigProvider = ??? + +val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> ReconfigurableLogger + .make[Any, Nothing, String, Any, ConsoleLoggerConfig]( + ConsoleLoggerConfig.load().orDie, // loading current configuration + (config, _) => makeConsoleLogger(config), // make logger from loaded configuration + Schedule.fixed(5.second) // default is 10 seconds + ) + .installUnscoped[Any] +``` + +`ReconfigurableLogger`, based on given `Schedule` and load configuration function, will recreate logger if configuration changed. + +**NOTE:** consider if you need this feature in your application, as there may be some performance impacts (see [benchmarks](https://github.com/zio/zio-logging/blob/master/benchmarks/src/main/scala/zio/logging/FilterBenchmarks.scala)). + +## Examples + +You can find the source code [here](https://github.com/zio/zio-logging/tree/master/examples) + + +### Console Logger With Re-configuration From Configuration File In Runtime + +[//]: # (TODO: make snippet type-checked using mdoc) + +Example of application where logger configuration is updated at runtime when logger configuration file is changed. + +Configuration: + +``` +logger { + format = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}" + filter { + rootLevel = "INFO" + mappings { + "zio.logging.example" = "DEBUG" + } + } +} +``` + +Application: + +```scala +package zio.logging.example + +import com.typesafe.config.ConfigFactory +import zio.config.typesafe.TypesafeConfigProvider +import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, ReconfigurableLogger, _ } +import zio.{ Config, ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } + +import java.util.UUID + +object LoggerReconfigureApp extends ZIOAppDefault { + + def configuredLogger( + loadConfig: => ZIO[Any, Config.Error, ConsoleLoggerConfig] + ): ZLayer[Any, Config.Error, Unit] = + ReconfigurableLogger + .make[Any, Config.Error, String, Any, ConsoleLoggerConfig]( + loadConfig, + (config, _) => makeConsoleLogger(config), + Schedule.fixed(500.millis) + ) + .installUnscoped + + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = + Runtime.removeDefaultLoggers >>> configuredLogger( + for { + config <- ZIO.succeed(ConfigFactory.load("logger.conf")) + _ <- Console.printLine(config.getConfig("logger")).orDie + loggerConfig <- ConsoleLoggerConfig.load().withConfigProvider(TypesafeConfigProvider.fromTypesafeConfig(config)) + } yield loggerConfig + ) + + def exec(): ZIO[Any, Nothing, Unit] = + for { + ok <- Random.nextBoolean + traceId <- ZIO.succeed(UUID.randomUUID()) + _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) + userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) + _ <- ZIO.foreachPar(userIds) { userId => + { + ZIO.logDebug("Starting operation") *> + ZIO.logInfo("OK operation").when(ok) *> + ZIO.logError("Error operation").when(!ok) *> + ZIO.logDebug("Stopping operation") + } @@ LogAnnotation.UserId(userId) + } @@ LogAnnotation.TraceId(traceId) + _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) + } yield () + + override def run: ZIO[Scope, Any, ExitCode] = + for { + _ <- exec().repeat(Schedule.fixed(500.millis)) + } yield ExitCode.success + +} +``` + +When configuration for `logger/filter/mappings/zio.logging.example` change from `DEBUG` to `WARN`: + +``` +Config(SimpleConfigObject({"filter":{"mappings":{"zio.logging.example":"DEBUG"},"rootLevel":"INFO"},"format":"%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}"})) +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-5] zio.logging.example.LoggerReconfigureApp:51 Start trace_id=87ead38c-8b42-43ea-9905-039d0263026d +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-36] zio.logging.example.LoggerReconfigureApp:55 Starting operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=dfa05247-ec27-46f7-a4e0-bb86f2d501e9 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-37] zio.logging.example.LoggerReconfigureApp:55 Starting operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=19693c77-0896-4fae-a830-67d5fe370b05 +2023-12-26T10:10:26+0100 ERROR [zio-fiber-36] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=dfa05247-ec27-46f7-a4e0-bb86f2d501e9 +2023-12-26T10:10:26+0100 ERROR [zio-fiber-37] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=19693c77-0896-4fae-a830-67d5fe370b05 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-36] zio.logging.example.LoggerReconfigureApp:58 Stopping operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=dfa05247-ec27-46f7-a4e0-bb86f2d501e9 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-37] zio.logging.example.LoggerReconfigureApp:58 Stopping operation trace_id=87ead38c-8b42-43ea-9905-039d0263026d user_id=19693c77-0896-4fae-a830-67d5fe370b05 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-5] zio.logging.example.LoggerReconfigureApp:61 Done trace_id=87ead38c-8b42-43ea-9905-039d0263026d +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-5] zio.logging.example.LoggerReconfigureApp:51 Start trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-39] zio.logging.example.LoggerReconfigureApp:55 Starting operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=a62a153f-6a91-491e-8c97-bab94186f0a2 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-38] zio.logging.example.LoggerReconfigureApp:55 Starting operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=8eeb6442-80a9-40e5-b97d-a12876702a65 +2023-12-26T10:10:26+0100 ERROR [zio-fiber-39] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=a62a153f-6a91-491e-8c97-bab94186f0a2 +2023-12-26T10:10:26+0100 ERROR [zio-fiber-38] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=8eeb6442-80a9-40e5-b97d-a12876702a65 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-39] zio.logging.example.LoggerReconfigureApp:58 Stopping operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=a62a153f-6a91-491e-8c97-bab94186f0a2 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-38] zio.logging.example.LoggerReconfigureApp:58 Stopping operation trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c user_id=8eeb6442-80a9-40e5-b97d-a12876702a65 +2023-12-26T10:10:26+0100 DEBUG [zio-fiber-5] zio.logging.example.LoggerReconfigureApp:61 Done trace_id=c6d4c770-8db1-4ea8-91eb-548c6a99a90c +Config(SimpleConfigObject({"filter":{"mappings":{"zio.logging.example":"WARN"},"rootLevel":"INFO"},"format":"%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}"})) +2023-12-26T10:10:27+0100 ERROR [zio-fiber-40] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=e2d8bbb4-5ad0-4952-b035-03da7689ab56 user_id=0f6452da-3f7e-40ff-b8b4-6b4c731903fb +2023-12-26T10:10:27+0100 ERROR [zio-fiber-41] zio.logging.example.LoggerReconfigureApp:57 Error operation trace_id=e2d8bbb4-5ad0-4952-b035-03da7689ab56 user_id=c4a86b38-90d7-4bb6-9f49-73bc5701e1ef +``` + +### Console Logger With Configuration By Http APIs + +[//]: # (TODO: make snippet type-checked using mdoc) + +Example of application where logger configuration can be changed by Http APIs. + +Logger configurations APIs: +* get logger configurations + ```bash + curl -u "admin:admin" 'http://localhost:8080/example/logger' + ``` +* get `root` logger configuration + ```bash + curl -u "admin:admin" 'http://localhost:8080/example/logger/root' + ``` +* set `root` logger configuration + ```bash + curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/root' --header 'Content-Type: application/json' --data-raw '"WARN"' + ``` +* get `zio.logging.example` logger configuration + ```bash + curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/zio.logging.example' --header 'Content-Type: application/json' --data-raw '"WARN"' + ``` + +Configuration: + +``` +logger { + format = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}" + filter { + rootLevel = "INFO" + mappings { + "zio.logging.example" = "DEBUG" + } + } +} +``` + +Application: + +```scala +package zio.logging.example + +import com.typesafe.config.ConfigFactory +import zio.config.typesafe.TypesafeConfigProvider +import zio.http._ +import zio.logging.api.http.ApiHandlers +import zio.logging.{ ConfigurableLogger, ConsoleLoggerConfig, LogAnnotation, LoggerConfigurer, makeConsoleLogger } +import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } + +import java.util.UUID + +object ConfigurableLoggerApp extends ZIOAppDefault { + + def configurableLogger() = + ConsoleLoggerConfig + .load() + .flatMap { config => + makeConsoleLogger(config).map { logger => + ConfigurableLogger.make(logger, config.filter) + } + } + .install + + val configProvider: ConfigProvider = TypesafeConfigProvider.fromTypesafeConfig(ConfigFactory.load("logger.conf")) + + override val bootstrap: ZLayer[Any, Config.Error, Unit] = + Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> configurableLogger() + + def exec(): ZIO[Any, Nothing, Unit] = + for { + ok <- Random.nextBoolean + traceId <- ZIO.succeed(UUID.randomUUID()) + _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) + userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) + _ <- ZIO.foreachPar(userIds) { userId => + { + ZIO.logDebug("Starting operation") *> + ZIO.logInfo("OK operation").when(ok) *> + ZIO.logError("Error operation").when(!ok) *> + ZIO.logDebug("Stopping operation") + } @@ LogAnnotation.UserId(userId) + } @@ LogAnnotation.TraceId(traceId) + _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) + } yield () + + val httpApp: HttpApp[LoggerConfigurer] = + ApiHandlers.routes("example" :: Nil).toHttpApp @@ Middleware.basicAuth("admin", "admin") + + override def run: ZIO[Scope, Any, ExitCode] = + (for { + _ <- Server.serve(httpApp).fork + _ <- exec().repeat(Schedule.fixed(500.millis)) + } yield ExitCode.success).provide(LoggerConfigurer.layer ++ Server.default) + +} +``` + +**NOTE:** `ConfigurableLogger` and `ApiHandlers` are currently implemented in examples, +it will be considered in the future, if they will be moved to official `zio-logging` implementation +(once there will be official stable `zio-http` release). +If you like to use them in your app, you can copy them. + +When configuration for `logger/filter/mappings/zio.logging.example` change from `DEBUG` to `WARN`: + +```bash +curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/zio.logging.example' --header 'Content-Type: application/json' --data-raw '"WARN"' +``` + +``` +2023-12-26T10:49:27+0100 DEBUG [zio-fiber-73] zio.logging.example.ConfigurableLoggerApp:62 Starting operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=5d35778f-78ff-48a8-aa6a-73114ec719b5 +2023-12-26T10:49:27+0100 DEBUG [zio-fiber-72] zio.logging.example.ConfigurableLoggerApp:62 Starting operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=9e17bd97-aa18-4b09-a423-9de28241a20b +2023-12-26T10:49:27+0100 INFO [zio-fiber-73] zio.logging.example.ConfigurableLoggerApp:63 OK operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=5d35778f-78ff-48a8-aa6a-73114ec719b5 +2023-12-26T10:49:27+0100 INFO [zio-fiber-72] zio.logging.example.ConfigurableLoggerApp:63 OK operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=9e17bd97-aa18-4b09-a423-9de28241a20b +2023-12-26T10:49:27+0100 DEBUG [zio-fiber-73] zio.logging.example.ConfigurableLoggerApp:65 Stopping operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=5d35778f-78ff-48a8-aa6a-73114ec719b5 +2023-12-26T10:49:27+0100 DEBUG [zio-fiber-72] zio.logging.example.ConfigurableLoggerApp:65 Stopping operation trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 user_id=9e17bd97-aa18-4b09-a423-9de28241a20b +2023-12-26T10:49:27+0100 DEBUG [zio-fiber-4] zio.logging.example.ConfigurableLoggerApp:68 Done trace_id=dcf30228-dc00-4c1f-ab94-20c9f8116045 +2023-12-26T10:49:28+0100 ERROR [zio-fiber-77] zio.logging.example.ConfigurableLoggerApp:64 Error operation trace_id=7da8765e-2e27-42c6-8834-16d15d21c72c user_id=4395d188-5971-4839-a721-278d07a2881b +2023-12-26T10:49:28+0100 ERROR [zio-fiber-78] zio.logging.example.ConfigurableLoggerApp:64 Error operation trace_id=7da8765e-2e27-42c6-8834-16d15d21c72c user_id=27af6873-2f7b-4b9a-ad2b-6bd12479cace +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 3fde2ec5e..cf12c2599 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,6 +10,7 @@ const sidebars = { 'log-filter', 'console-logger', 'file-logger', + 'reconfigurable-logger', 'jpl', 'slf4j2', 'slf4j1', diff --git a/docs/slf4j1-bridge.md b/docs/slf4j1-bridge.md index 90fc80e59..df921d357 100644 --- a/docs/slf4j1-bridge.md +++ b/docs/slf4j1-bridge.md @@ -42,7 +42,7 @@ val logger = Runtime.removeDefaultLoggers >>> consoleJsonLogger() >+> Slf4jBridg ```
-**NOTE** You should either use `zio-logging-slf4j` to send all ZIO logs to an SLF4j provider (such as logback, log4j etc) OR `zio-logging-slf4j-bridge` to send all SLF4j logs to +**NOTE:** You should either use `zio-logging-slf4j` to send all ZIO logs to an SLF4j provider (such as logback, log4j etc) OR `zio-logging-slf4j-bridge` to send all SLF4j logs to ZIO logging. Enabling both causes circular logging and makes no sense. @@ -53,9 +53,10 @@ ZIO logging. Enabling both causes circular logging and makes no sense. [//]: # (TODO: make snippet type-checked using mdoc) ```scala -package zio.logging.slf4j.bridge +package zio.logging.example -import zio.logging._ +import zio.logging.slf4j.bridge.Slf4jBridge +import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } import java.util.UUID @@ -64,23 +65,16 @@ object Slf4jBridgeExampleApp extends ZIOAppDefault { private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") - private val logFilter: LogFilter[String] = LogFilter.logLevelByName( - LogLevel.Info, - "zio.logging.slf4j" -> LogLevel.Debug, - "SLF4J-LOGGER" -> LogLevel.Warning - ) + private val logFormat = LogFormat.label( + "name", + LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() + ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( + LogAnnotation.TraceId + ) + LogFormat.default override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger( - ConsoleLoggerConfig( - LogFormat.label( - "name", - LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() - ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( - LogAnnotation.TraceId - ) + LogFormat.default, - logFilter - ) + ConsoleLoggerConfig(logFormat, logFilterConfig) ) >+> Slf4jBridge.initialize private val uuids = List.fill(2)(UUID.randomUUID()) @@ -145,3 +139,10 @@ val logFilter: LogFilter[String] = LogFilter.logLevelByGroup( "SLF4J-LOGGER" -> LogLevel.Warning ) ``` + +### Version 2.2.0 + +Deprecated log annotation with key `slf4j_logger_name` (`Slf4jBridge.loggerNameAnnotationKey`) removed, +only common log annotation with key `logger_name` (`zio.logging.loggerNameAnnotationKey`) for logger name is supported now. + + diff --git a/docs/slf4j1.md b/docs/slf4j1.md index 6a2712641..8db60dd60 100644 --- a/docs/slf4j1.md +++ b/docs/slf4j1.md @@ -185,3 +185,11 @@ Expected Console Output: 15:53:20.688 [ZScheduler-Worker-11] [user=878689e0-da30-49f8-8923-ed915c00db9c, trace_id=71436dd4-22d5-4e06-aaa7-f3ff7b108037] INFO z.l.e.CustomTracingAnnotationApp Stopping operation 15:53:20.691 [ZScheduler-Worker-15] [] INFO z.l.e.CustomTracingAnnotationApp Done ``` + +## Feature changes + +### Version 2.2.0 + +Deprecated log annotation with key `slf4j_logger_name` (`SLF4J.loggerNameAnnotationKey`) removed, +only common log annotation with key `logger_name` (`zio.logging.loggerNameAnnotationKey`) for logger name is supported now. + diff --git a/docs/slf4j2-bridge.md b/docs/slf4j2-bridge.md index 5b4f08b96..7ddddf5ce 100644 --- a/docs/slf4j2-bridge.md +++ b/docs/slf4j2-bridge.md @@ -33,11 +33,13 @@ may be used to get logger name from log annotation or ZIO Trace. This logger name extractor is used by default in log filter, which applying log filtering by defined logger name and level: ```scala -val logFilter: LogFilter[String] = LogFilter.logLevelByName( +val logFilterConfig = LogFilter.LogLevelByNameConfig( LogLevel.Info, "zio.logging.slf4j" -> LogLevel.Debug, "SLF4J-LOGGER" -> LogLevel.Warning ) + +val logFilter: LogFilter[String] = logFilterConfig.toFilter ```
@@ -52,7 +54,7 @@ val logger = Runtime.removeDefaultLoggers >>> consoleJsonLogger() >+> Slf4jBridg
-**NOTE** You should either use `zio-logging-slf4j` to send all ZIO logs to an SLF4j provider (such as logback, log4j etc) OR `zio-logging-slf4j-bridge` to send all SLF4j logs to +**NOTE:** You should either use `zio-logging-slf4j` to send all ZIO logs to an SLF4j provider (such as logback, log4j etc) OR `zio-logging-slf4j-bridge` to send all SLF4j logs to ZIO logging. Enabling both causes circular logging and makes no sense. @@ -65,9 +67,10 @@ You can find the source code [here](https://github.com/zio/zio-logging/tree/mast [//]: # (TODO: make snippet type-checked using mdoc) ```scala -package zio.logging.slf4j.bridge +package zio.logging.example -import zio.logging._ +import zio.logging.slf4j.bridge.Slf4jBridge +import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } import java.util.UUID @@ -76,23 +79,22 @@ object Slf4jBridgeExampleApp extends ZIOAppDefault { private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") - private val logFilter: LogFilter[String] = LogFilter.logLevelByName( + private val logFilterConfig = LogFilter.LogLevelByNameConfig( LogLevel.Info, "zio.logging.slf4j" -> LogLevel.Debug, "SLF4J-LOGGER" -> LogLevel.Warning ) + private val logFormat = LogFormat.label( + "name", + LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() + ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( + LogAnnotation.TraceId + ) + LogFormat.default + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger( - ConsoleLoggerConfig( - LogFormat.label( - "name", - LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() - ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( - LogAnnotation.TraceId - ) + LogFormat.default, - logFilter - ) + ConsoleLoggerConfig(logFormat, logFilterConfig) ) >+> Slf4jBridge.initialize private val uuids = List.fill(2)(UUID.randomUUID()) diff --git a/examples/core/src/main/resources/logger.conf b/examples/core/src/main/resources/logger.conf new file mode 100644 index 000000000..0db7b21fe --- /dev/null +++ b/examples/core/src/main/resources/logger.conf @@ -0,0 +1,9 @@ +logger { + format = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}" + filter { + rootLevel = "INFO" + mappings { + "zio.logging.example" = "DEBUG" + } + } +} \ No newline at end of file diff --git a/examples/core/src/main/scala/zio/logging/ConfigurableLogger.scala b/examples/core/src/main/scala/zio/logging/ConfigurableLogger.scala new file mode 100644 index 000000000..6ff7d85df --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/ConfigurableLogger.scala @@ -0,0 +1,145 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging + +import zio.{ Cause, FiberId, FiberRefs, LogLevel, LogSpan, Trace, ZIO, ZLayer, ZLogger } + +trait LoggerConfigurer { + + def getLoggerConfigs(): ZIO[Any, Throwable, List[LoggerConfigurer.LoggerConfig]] + + def getLoggerConfig(name: String): ZIO[Any, Throwable, Option[LoggerConfigurer.LoggerConfig]] + + def setLoggerConfig(name: String, level: LogLevel): ZIO[Any, Throwable, LoggerConfigurer.LoggerConfig] +} + +object LoggerConfigurer { + + final case class LoggerConfig(name: String, level: LogLevel) + + def getLoggerConfigs(): ZIO[LoggerConfigurer, Throwable, List[LoggerConfigurer.LoggerConfig]] = + ZIO.serviceWithZIO[LoggerConfigurer](_.getLoggerConfigs()) + + def getLoggerConfig(name: String): ZIO[LoggerConfigurer, Throwable, Option[LoggerConfigurer.LoggerConfig]] = + ZIO.serviceWithZIO[LoggerConfigurer](_.getLoggerConfig(name)) + + def setLoggerConfig( + name: String, + logLevel: LogLevel + ): ZIO[LoggerConfigurer, Throwable, LoggerConfigurer.LoggerConfig] = + ZIO.serviceWithZIO[LoggerConfigurer](_.setLoggerConfig(name, logLevel)) + + val layer: ZLayer[Any, Throwable, LoggerConfigurer] = + ZLayer.fromZIO { + ZIO.loggers.flatMap { loggers => + loggers.collectFirst { case logger: ConfigurableLogger[_, _] => + logger.configurer + } match { + case Some(value) => ZIO.succeed(value) + case None => ZIO.fail(new RuntimeException("LoggerConfigurer not found")) + } + } + } +} + +trait ConfigurableLogger[-Message, +Output] extends ZLogger[Message, Output] { + + def configurer: LoggerConfigurer +} + +object ConfigurableLogger { + + def make[Message, Output]( + logger: ZLogger[Message, Output], + filterConfig: LogFilter.LogLevelByNameConfig + ): ConfigurableLogger[Message, Option[Output]] = { + + val initialLogger = LogFilter.logLevelByName(filterConfig).filter(logger) + + val reconfigurableLogger = ReconfigurableLogger[Message, Option[Output], LogFilter.LogLevelByNameConfig]( + filterConfig, + initialLogger + ) + + new ConfigurableLogger[Message, Option[Output]] { + + override val configurer: LoggerConfigurer = + Configurer(filterConfig => LogFilter.logLevelByName(filterConfig).filter(logger), reconfigurableLogger) + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => Message, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Option[Output] = + reconfigurableLogger.apply(trace, fiberId, logLevel, message, cause, context, spans, annotations) + } + } + + private case class Configurer[M, O]( + makeLogger: LogFilter.LogLevelByNameConfig => ZLogger[M, O], + logger: ReconfigurableLogger[M, Option[O], LogFilter.LogLevelByNameConfig] + ) extends LoggerConfigurer { + import zio.prelude._ + + private val rootName = "root" + + override def getLoggerConfigs(): ZIO[Any, Throwable, List[LoggerConfigurer.LoggerConfig]] = + ZIO.attempt { + val currentConfig = logger.get._1 + + LoggerConfigurer.LoggerConfig(rootName, currentConfig.rootLevel) :: currentConfig.mappings.map { case (n, l) => + LoggerConfigurer.LoggerConfig(n, l) + }.toList + } + + override def getLoggerConfig(name: String): ZIO[Any, Throwable, Option[LoggerConfigurer.LoggerConfig]] = + ZIO.attempt { + val currentConfig = logger.get._1 + + if (name == rootName) { + Some(LoggerConfigurer.LoggerConfig(rootName, currentConfig.rootLevel)) + } else { + currentConfig.mappings.collectFirst { + case (n, l) if n == name => LoggerConfigurer.LoggerConfig(n, l) + } + } + } + + override def setLoggerConfig(name: String, level: LogLevel): ZIO[Any, Throwable, LoggerConfigurer.LoggerConfig] = + ZIO.attempt { + val currentConfig = logger.get._1 + + val newConfig = if (name == rootName) { + currentConfig.withRootLevel(level) + } else { + currentConfig.withMapping(name, level) + } + + if (currentConfig !== newConfig) { + val newLogger = makeLogger(newConfig) + logger.set(newConfig, newLogger) + } + + LoggerConfigurer.LoggerConfig(name, level) + } + } + +} diff --git a/examples/core/src/main/scala/zio/logging/api/http/ApiDomain.scala b/examples/core/src/main/scala/zio/logging/api/http/ApiDomain.scala new file mode 100644 index 000000000..0f611eacb --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/api/http/ApiDomain.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging.api.http + +import zio.LogLevel +import zio.logging.LoggerConfigurer +import zio.schema.{ DeriveSchema, Schema } + +object ApiDomain { + + sealed trait Error { + def message: String + } + + object Error { + final case class NotFound(message: String = "Not Found") extends Error + + final case class Internal(message: String = "Internal") extends Error + + implicit val notFoundSchema: Schema[Error.NotFound] = DeriveSchema.gen[Error.NotFound] + implicit val internalSchema: Schema[Error.Internal] = DeriveSchema.gen[Error.Internal] + implicit val schema: Schema[Error] = DeriveSchema.gen[Error] + } + + implicit val logLevelSchema: Schema[LogLevel] = { + val levelToLabel: Map[LogLevel, String] = LogLevel.levels.map(level => (level, level.label)).toMap + val labelToLevel: Map[String, LogLevel] = levelToLabel.map(_.swap) + + Schema[String] + .transformOrFail[LogLevel](v => labelToLevel.get(v).toRight("Failed"), v => levelToLabel.get(v).toRight("Failed")) + } + + final case class LoggerConfig(name: String, level: LogLevel) + + object LoggerConfig { + implicit val schema: Schema[LoggerConfig] = DeriveSchema.gen[LoggerConfig] + + def from(value: LoggerConfigurer.LoggerConfig): LoggerConfig = + LoggerConfig(value.name, value.level) + } + +} diff --git a/examples/core/src/main/scala/zio/logging/api/http/ApiEndpoints.scala b/examples/core/src/main/scala/zio/logging/api/http/ApiEndpoints.scala new file mode 100644 index 000000000..cc22f605f --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/api/http/ApiEndpoints.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging.api.http + +import zio._ +import zio.http._ +import zio.http.codec.PathCodec.{ literal, string } +import zio.http.codec.{ HttpCodec, PathCodec } +import zio.http.endpoint.EndpointMiddleware.None +import zio.http.endpoint._ +import zio.http.endpoint.openapi.{ OpenAPI, OpenAPIGen } +import zio.logging.api.http.ApiDomain.Error + +object ApiEndpoints { + import ApiDomain.logLevelSchema + + def rootPathCodec(rootPath: Seq[String]): PathCodec[Unit] = + if (rootPath.isEmpty) { + PathCodec.empty + } else { + rootPath.map(literal).reduce(_ / _) + } + + def getLoggerConfigs( + rootPath: Seq[String] = Seq.empty + ): Endpoint[Unit, Unit, Error.Internal, List[ApiDomain.LoggerConfig], None] = + Endpoint(Method.GET / rootPathCodec(rootPath) / literal("logger")) + .out[List[ApiDomain.LoggerConfig]] + .outError[ApiDomain.Error.Internal](Status.InternalServerError) + + def getLoggerConfig( + rootPath: Seq[String] = Seq.empty + ): Endpoint[String, String, Error, ApiDomain.LoggerConfig, None] = + Endpoint(Method.GET / rootPathCodec(rootPath) / literal("logger") / string("name")) + .out[ApiDomain.LoggerConfig] + .outErrors[ApiDomain.Error]( + HttpCodec.error[ApiDomain.Error.Internal](Status.InternalServerError), + HttpCodec.error[ApiDomain.Error.NotFound](Status.NotFound) + ) + + def setLoggerConfig( + rootPath: Seq[String] = Seq.empty + ): Endpoint[String, (String, LogLevel), Error.Internal, ApiDomain.LoggerConfig, None] = + Endpoint(Method.PUT / rootPathCodec(rootPath) / literal("logger") / string("name")) + .in[LogLevel] + .out[ApiDomain.LoggerConfig] + .outError[ApiDomain.Error.Internal](Status.InternalServerError) + + def openAPI(rootPath: Seq[String] = Seq.empty): OpenAPI = + OpenAPIGen.fromEndpoints( + title = "Logger Configurations API", + version = "1.0", + getLoggerConfigs(rootPath), + getLoggerConfig(rootPath), + setLoggerConfig(rootPath) + ) +} diff --git a/examples/core/src/main/scala/zio/logging/api/http/ApiHandlers.scala b/examples/core/src/main/scala/zio/logging/api/http/ApiHandlers.scala new file mode 100644 index 000000000..4784936bf --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/api/http/ApiHandlers.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging.api.http + +import zio.http.{ Handler, Route, Routes } +import zio.logging.LoggerConfigurer +import zio.logging.api.http.ApiDomain.Error +import zio.{ LogLevel, ZIO } + +object ApiHandlers { + + def getLoggerConfigs(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = + ApiEndpoints + .getLoggerConfigs(rootPath) + .implement { + Handler.fromFunctionZIO[Unit] { _ => + LoggerConfigurer + .getLoggerConfigs() + .map(_.map(ApiDomain.LoggerConfig.from)) + .mapError(_ => Error.Internal()) + } + } + + def getLoggerConfig(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = + ApiEndpoints + .getLoggerConfig(rootPath) + .implement { + Handler.fromFunctionZIO[String] { name => + LoggerConfigurer.getLoggerConfig(name).mapError(_ => Error.Internal()).flatMap { + case Some(r) => ZIO.succeed(ApiDomain.LoggerConfig.from(r)) + case _ => ZIO.fail(Error.NotFound()) + } + } + } + + def setLoggerConfigs(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = + ApiEndpoints + .setLoggerConfig(rootPath) + .implement { + Handler.fromFunctionZIO[(String, LogLevel)] { case (name, logLevel) => + LoggerConfigurer + .setLoggerConfig(name, logLevel) + .map(ApiDomain.LoggerConfig.from) + .mapError(_ => Error.Internal()) + } + } + + def routes(rootPath: Seq[String] = Seq.empty): Routes[LoggerConfigurer, Nothing] = + Routes(getLoggerConfigs(rootPath), getLoggerConfig(rootPath), setLoggerConfigs(rootPath)) +} diff --git a/examples/core/src/main/scala/zio/logging/example/ConfigurableLoggerApp.scala b/examples/core/src/main/scala/zio/logging/example/ConfigurableLoggerApp.scala new file mode 100644 index 000000000..0fb86fb17 --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/example/ConfigurableLoggerApp.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging.example + +import com.typesafe.config.ConfigFactory +import zio.config.typesafe.TypesafeConfigProvider +import zio.http._ +import zio.logging.api.http.ApiHandlers +import zio.logging.{ ConfigurableLogger, ConsoleLoggerConfig, LogAnnotation, LoggerConfigurer, makeConsoleLogger } +import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } + +import java.util.UUID + +/* + curl -u "admin:admin" 'http://localhost:8080/example/logger' + + curl -u "admin:admin" 'http://localhost:8080/example/logger/root' + + curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/root' --header 'Content-Type: application/json' --data-raw '"WARN"' + + curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/zio.logging.example' --header 'Content-Type: application/json' --data-raw '"WARN"' + + */ +object ConfigurableLoggerApp extends ZIOAppDefault { + + def configurableLogger() = + ConsoleLoggerConfig + .load() + .flatMap { config => + makeConsoleLogger(config).map { logger => + ConfigurableLogger.make(logger, config.filter) + } + } + .install + + val configProvider: ConfigProvider = TypesafeConfigProvider.fromTypesafeConfig(ConfigFactory.load("logger.conf")) + + override val bootstrap: ZLayer[Any, Config.Error, Unit] = + Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> configurableLogger() + + def exec(): ZIO[Any, Nothing, Unit] = + for { + ok <- Random.nextBoolean + traceId <- ZIO.succeed(UUID.randomUUID()) + _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) + userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) + _ <- ZIO.foreachPar(userIds) { userId => + { + ZIO.logDebug("Starting operation") *> + ZIO.logInfo("OK operation").when(ok) *> + ZIO.logError("Error operation").when(!ok) *> + ZIO.logDebug("Stopping operation") + } @@ LogAnnotation.UserId(userId) + } @@ LogAnnotation.TraceId(traceId) + _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) + } yield () + + val httpApp: HttpApp[LoggerConfigurer] = + ApiHandlers.routes("example" :: Nil).toHttpApp @@ Middleware.basicAuth("admin", "admin") + + override def run: ZIO[Scope, Any, ExitCode] = + (for { + _ <- Server.serve(httpApp).fork + _ <- exec().repeat(Schedule.fixed(500.millis)) + } yield ExitCode.success).provide(LoggerConfigurer.layer ++ Server.default) + +} diff --git a/examples/core/src/main/scala/zio/logging/example/LoggerReconfigureApp.scala b/examples/core/src/main/scala/zio/logging/example/LoggerReconfigureApp.scala new file mode 100644 index 000000000..2652ae84a --- /dev/null +++ b/examples/core/src/main/scala/zio/logging/example/LoggerReconfigureApp.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package zio.logging.example + +import com.typesafe.config.ConfigFactory +import zio.config.typesafe.TypesafeConfigProvider +import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, ReconfigurableLogger, _ } +import zio.{ Config, ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } + +import java.util.UUID + +object LoggerReconfigureApp extends ZIOAppDefault { + + def configuredLogger( + loadConfig: => ZIO[Any, Config.Error, ConsoleLoggerConfig] + ): ZLayer[Any, Config.Error, Unit] = + ReconfigurableLogger + .make[Any, Config.Error, String, Any, ConsoleLoggerConfig]( + loadConfig, + (config, _) => makeConsoleLogger(config), + Schedule.fixed(500.millis) + ) + .installUnscoped + + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = + Runtime.removeDefaultLoggers >>> configuredLogger( + for { + config <- ZIO.succeed(ConfigFactory.load("logger.conf")) + _ <- Console.printLine(config.getConfig("logger")).orDie + loggerConfig <- ConsoleLoggerConfig.load().withConfigProvider(TypesafeConfigProvider.fromTypesafeConfig(config)) + } yield loggerConfig + ) + + def exec(): ZIO[Any, Nothing, Unit] = + for { + ok <- Random.nextBoolean + traceId <- ZIO.succeed(UUID.randomUUID()) + _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) + userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) + _ <- ZIO.foreachPar(userIds) { userId => + { + ZIO.logDebug("Starting operation") *> + ZIO.logInfo("OK operation").when(ok) *> + ZIO.logError("Error operation").when(!ok) *> + ZIO.logDebug("Stopping operation") + } @@ LogAnnotation.UserId(userId) + } @@ LogAnnotation.TraceId(traceId) + _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) + } yield () + + override def run: ZIO[Scope, Any, ExitCode] = + for { + _ <- exec().repeat(Schedule.fixed(500.millis)) + } yield ExitCode.success + +} diff --git a/examples/core/src/test/scala/zio/logging/api/http/ApiEndpointsSpec.scala b/examples/core/src/test/scala/zio/logging/api/http/ApiEndpointsSpec.scala new file mode 100644 index 000000000..8aa5c949a --- /dev/null +++ b/examples/core/src/test/scala/zio/logging/api/http/ApiEndpointsSpec.scala @@ -0,0 +1,72 @@ +package zio.logging.api.http + +import zio.http.codec.PathCodec.literal +import zio.http.codec._ +import zio.test._ +import zio.{ LogLevel, Scope } + +object ApiEndpointsSpec extends ZIOSpecDefault { + + def spec: Spec[Environment with TestEnvironment with Scope, Any] = suite("ApiEndpointsSpec")( + test("rootPathCodec") { + def testRootPathCodec(rootPath: Seq[String], expected: PathCodec[Unit]) = + assertTrue( + ApiEndpoints.rootPathCodec(rootPath).encode(()).getOrElse(zio.http.Path.empty) == expected + .encode(()) + .getOrElse(zio.http.Path.empty) + ) + + testRootPathCodec(Nil, PathCodec.empty) && testRootPathCodec( + "example" :: Nil, + literal("example") + ) && testRootPathCodec("v1" :: "example" :: Nil, literal("v1") / literal("example")) + }, + test("getLoggerConfigs") { + + def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = + assertTrue( + ApiEndpoints.getLoggerConfigs(rootPath).input.encodeRequest(()).path == expected + .encode(()) + .getOrElse(zio.http.Path.empty) + ) + + testPath(Nil, literal("logger")) && testPath( + "example" :: Nil, + literal("example") / literal("logger") + ) + }, + test("getLoggerConfig") { + + def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = + assertTrue( + ApiEndpoints.getLoggerConfig(rootPath).input.encodeRequest("my-logger").path == expected + .encode(()) + .getOrElse(zio.http.Path.empty) + ) + + testPath(Nil, literal("logger") / literal("my-logger")) && testPath( + "example" :: Nil, + literal("example") / literal("logger") / literal("my-logger") + ) + }, + test("setLoggerConfigs") { + + def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = + assertTrue( + ApiEndpoints + .setLoggerConfig(rootPath) + .input + .encodeRequest(("my-logger", LogLevel.Info)) + .path == expected + .encode(()) + .getOrElse(zio.http.Path.empty) + ) + + testPath(Nil, literal("logger") / literal("my-logger")) && testPath( + "example" :: Nil, + literal("example") / literal("logger") / literal("my-logger") + ) + } + ) + +} diff --git a/examples/core/src/test/scala/zio/logging/api/http/ApiHandlersSpec.scala b/examples/core/src/test/scala/zio/logging/api/http/ApiHandlersSpec.scala new file mode 100644 index 000000000..275ed35bf --- /dev/null +++ b/examples/core/src/test/scala/zio/logging/api/http/ApiHandlersSpec.scala @@ -0,0 +1,70 @@ +package zio.logging.api.http + +import zio.http._ +import zio.http.codec._ +import zio.logging.LoggerConfigurer +import zio.test._ +import zio.{ LogLevel, ULayer, ZIO, ZLayer } + +object ApiHandlersSpec extends ZIOSpecDefault { + + val loggerConfigurer: ULayer[LoggerConfigurer] = ZLayer.succeed { + new LoggerConfigurer { + override def getLoggerConfigs(): ZIO[Any, Throwable, List[LoggerConfigurer.LoggerConfig]] = + ZIO.succeed(LoggerConfigurer.LoggerConfig("root", LogLevel.Info) :: Nil) + + override def getLoggerConfig( + name: String + ): ZIO[Any, Throwable, Option[LoggerConfigurer.LoggerConfig]] = + ZIO.succeed(Some(LoggerConfigurer.LoggerConfig(name, LogLevel.Info))) + + override def setLoggerConfig( + name: String, + level: LogLevel + ): ZIO[Any, Throwable, LoggerConfigurer.LoggerConfig] = + ZIO.succeed(LoggerConfigurer.LoggerConfig(name, level)) + } + } + + def spec: Spec[Any, Serializable] = suite("ApiHandlersSpec")( + test("get all") { + val routes = ApiHandlers.routes("example" :: Nil) + + for { + request <- ZIO.attempt(Request.get(URL.decode("/example/logger").toOption.get)) + response <- routes.toHttpApp.runZIO(request) + content <- HttpCodec.content[List[ApiDomain.LoggerConfig]].decodeResponse(response) + } yield assertTrue(response.status.isSuccess) && assertTrue( + content == List(ApiDomain.LoggerConfig("root", LogLevel.Info)) + ) + }, + test("get") { + val routes = ApiHandlers.routes("example" :: Nil) + for { + request <- ZIO.attempt(Request.get(URL.decode("/example/logger/example.Service").toOption.get)) + response <- routes.toHttpApp.runZIO(request) + content <- HttpCodec.content[ApiDomain.LoggerConfig].decodeResponse(response) + } yield assertTrue(response.status.isSuccess) && assertTrue( + content == ApiDomain.LoggerConfig("example.Service", LogLevel.Info) + ) + }, + test("set") { + import ApiDomain.logLevelSchema + val routes = ApiHandlers.routes("example" :: Nil) + for { + request <- ZIO.attempt( + Request + .put( + URL.decode("/example/logger/example.Service").toOption.get, + HttpCodec.content[LogLevel].encodeRequest(LogLevel.Warning).body + ) + ) + response <- routes.toHttpApp.runZIO(request) + content <- HttpCodec.content[ApiDomain.LoggerConfig].decodeResponse(response) + } yield assertTrue(response.status.isSuccess) && assertTrue( + content == ApiDomain.LoggerConfig("example.Service", LogLevel.Warning) + ) + } + ).provideLayer(loggerConfigurer) + +} diff --git a/examples/slf4j2-bridge/src/main/scala/zio/logging/example/Slf4jBridgeExampleApp.scala b/examples/slf4j2-bridge/src/main/scala/zio/logging/example/Slf4jBridgeExampleApp.scala index d5bebd11b..69de5ad26 100644 --- a/examples/slf4j2-bridge/src/main/scala/zio/logging/example/Slf4jBridgeExampleApp.scala +++ b/examples/slf4j2-bridge/src/main/scala/zio/logging/example/Slf4jBridgeExampleApp.scala @@ -25,23 +25,22 @@ object Slf4jBridgeExampleApp extends ZIOAppDefault { private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") - private val logFilter: LogFilter[String] = LogFilter.logLevelByName( + private val logFilterConfig = LogFilter.LogLevelByNameConfig( LogLevel.Info, "zio.logging.slf4j" -> LogLevel.Debug, "SLF4J-LOGGER" -> LogLevel.Warning ) + private val logFormat = LogFormat.label( + "name", + LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() + ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( + LogAnnotation.TraceId + ) + LogFormat.default + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger( - ConsoleLoggerConfig( - LogFormat.label( - "name", - LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() - ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( - LogAnnotation.TraceId - ) + LogFormat.default, - logFilter - ) + ConsoleLoggerConfig(logFormat, logFilterConfig) ) >+> Slf4jBridge.initialize private val uuids = List.fill(2)(UUID.randomUUID()) diff --git a/jpl/src/main/scala/zio/logging/backend/JPL.scala b/jpl/src/main/scala/zio/logging/backend/JPL.scala index e2ced3e11..a7730c7ad 100644 --- a/jpl/src/main/scala/zio/logging/backend/JPL.scala +++ b/jpl/src/main/scala/zio/logging/backend/JPL.scala @@ -17,20 +17,7 @@ package zio.logging.backend import zio.logging.internal.LogAppender import zio.logging.{ LogFormat, LoggerNameExtractor } -import zio.{ - Cause, - FiberFailure, - FiberId, - FiberRefs, - LogLevel, - LogSpan, - Runtime, - Trace, - ZIOAspect, - ZLayer, - ZLogger, - logging -} +import zio.{ Cause, FiberFailure, FiberId, FiberRefs, LogLevel, LogSpan, Runtime, Trace, ZLayer, ZLogger, logging } object JPL { @@ -45,28 +32,11 @@ object JPL { LogLevel.None -> System.Logger.Level.OFF ) - /** - * log aspect annotation key for JPL logger name - */ - @deprecated("use zio.logging.loggerNameAnnotationKey", "2.1.8") - val loggerNameAnnotationKey = "jpl_logger_name" - /** * default log format for JPL logger */ val logFormatDefault: LogFormat = - LogFormat.allAnnotations(excludeKeys = - Set(JPL.loggerNameAnnotationKey, logging.loggerNameAnnotationKey) - ) + LogFormat.line + LogFormat.cause - - /** - * JPL logger name aspect, by this aspect is possible to change default logger name (default logger name is extracted from [[Trace]]) - * - * annotation key: [[JPL.loggerNameAnnotationKey]] - */ - @deprecated("use zio.logging.loggerName", "2.1.8") - def loggerName(value: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = - ZIOAspect.annotated(loggerNameAnnotationKey, value) + LogFormat.allAnnotations(excludeKeys = Set(logging.loggerNameAnnotationKey)) + LogFormat.line + LogFormat.cause private[backend] def getLoggerName(default: String = "zio-jpl-logger"): Trace => String = trace => LoggerNameExtractor.trace(trace, FiberRefs.empty, Map.empty).getOrElse(default) @@ -147,30 +117,34 @@ object JPL { loggerName: Trace => String, getJPLogger: String => System.Logger ): ZLogger[String, Unit] = - new ZLogger[String, Unit] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => String, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Unit = { - val jpLoggerName = annotations.getOrElse( - JPL.loggerNameAnnotationKey, - annotations.getOrElse(zio.logging.loggerNameAnnotationKey, loggerName(trace)) - ) - val jpLogger = getJPLogger(jpLoggerName) - if (isLogLevelEnabled(jpLogger, logLevel)) { - val appender = logAppender(jpLogger, logLevel) - - format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) - appender.closeLogEntry() - } - () + JplLogger(format, loggerName, getJPLogger) + + private[logging] case class JplLogger( + format: LogFormat, + loggerName: Trace => String, + getJPLogger: String => System.Logger + ) extends ZLogger[String, Unit] { + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => String, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Unit = { + val jpLoggerName = annotations.getOrElse(zio.logging.loggerNameAnnotationKey, loggerName(trace)) + val jpLogger = getJPLogger(jpLoggerName) + if (isLogLevelEnabled(jpLogger, logLevel)) { + val appender = logAppender(jpLogger, logLevel) + + format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) + appender.closeLogEntry() } + () } + } } diff --git a/jpl/src/test/scala/zio/logging/backend/JPLSpec.scala b/jpl/src/test/scala/zio/logging/backend/JPLSpec.scala index 6673fea81..6061c4eaa 100644 --- a/jpl/src/test/scala/zio/logging/backend/JPLSpec.scala +++ b/jpl/src/test/scala/zio/logging/backend/JPLSpec.scala @@ -96,25 +96,6 @@ object JPLSpec extends ZIOSpecDefault { ) } }.provide(loggerDefault), - test("log with custom logger name - legacy") { - val loggerName = "my-logger" - (startStop() @@ JPL.loggerName(loggerName)).map { case (traceId, users) => - val loggerOutput = TestAppender.logOutput - assertTrue(loggerOutput.size == 5) && assertTrue( - loggerOutput.forall(_.loggerName == loggerName) - ) && assertTrue(loggerOutput.forall(_.logLevel == LogLevel.Info)) && assert(loggerOutput.map(_.message))( - equalTo( - Chunk( - s"user=${users(0)} trace_id=$traceId Starting operation", - s"user=${users(0)} trace_id=$traceId Stopping operation", - s"user=${users(1)} trace_id=$traceId Starting operation", - s"user=${users(1)} trace_id=$traceId Stopping operation", - s"Done" - ) - ) - ) - } - }.provide(loggerDefault), test("log with custom logger name") { val loggerName = "my-logger" (startStop() @@ zio.logging.loggerName(loggerName)).map { case (traceId, users) => @@ -152,36 +133,6 @@ object JPLSpec extends ZIOSpecDefault { ) } }.provide(loggerTraceAnnotation), - test("logger name changes - legacy logger name annotation") { - val users = Chunk.fill(2)(UUID.randomUUID()) - for { - traceId <- ZIO.succeed(UUID.randomUUID()) - _ = TestAppender.reset() - _ <- ZIO.logInfo("Start") @@ JPL.loggerName("root-logger") - _ <- ZIO.foreach(users) { uId => - { - ZIO.logInfo("Starting user operation") *> ZIO.sleep(500.millis) *> ZIO.logInfo( - "Stopping user operation" - ) - } @@ ZIOAspect.annotated("user", uId.toString) @@ JPL.loggerName("user-logger") - } @@ LogAnnotation.TraceId(traceId) @@ JPL.loggerName("user-root-logger") - _ <- ZIO.logInfo("Done") @@ JPL.loggerName("root-logger") - } yield { - val loggerOutput = TestAppender.logOutput - assertTrue(loggerOutput.forall(_.logLevel == LogLevel.Info)) && assert(loggerOutput.map(_.loggerName))( - equalTo( - Chunk( - "root-logger", - "user-logger", - "user-logger", - "user-logger", - "user-logger", - "root-logger" - ) - ) - ) - } - }.provide(loggerDefault), test("logger name changes") { val users = Chunk.fill(2)(UUID.randomUUID()) for { @@ -218,14 +169,6 @@ object JPLSpec extends ZIOSpecDefault { someErrorAssert(loggerOutput) && assertTrue(loggerOutput(0).cause.exists(_.getMessage.contains("input < 1"))) } }.provide(loggerLineCause), - test("log error with cause with custom logger name - legacy") { - (someError() @@ JPL.loggerName("my-logger")).map { _ => - val loggerOutput = TestAppender.logOutput - someErrorAssert(loggerOutput, "my-logger") && assertTrue( - loggerOutput(0).cause.exists(_.getMessage.contains("input < 1")) - ) - } - }.provide(loggerLineCause), test("log error with cause with custom logger name") { (someError() @@ logging.loggerName("my-logger")).map { _ => val loggerOutput = TestAppender.logOutput diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index 677286ca6..d09b659e3 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -5,7 +5,7 @@ import sbt.Keys.{ name, organization } import sbt._ object MimaSettings { - lazy val bincompatVersionToCompare = "2.1.12" + lazy val bincompatVersionToCompare = "2.1.13" def mimaSettings(failOnProblem: Boolean) = Seq( diff --git a/project/Versions.scala b/project/Versions.scala index 297cdf60b..a10afb07d 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -6,8 +6,10 @@ object Versions { val scalaCollectionCompatVersion = "2.11.0" val logstashLogbackEncoderVersion = "6.6" val scalaJavaTimeVersion = "2.5.0" - val zioMetricsConnectorsVersion = "2.0.8" + val zioMetricsConnectorsVersion = "2.3.1" val zioConfig = "4.0.0" val zioParser = "0.1.9" + val zioPrelude = "1.0.0-RC21" + val zioHttp = "3.0.0-RC4" val log4jVersion = "2.19.0" } diff --git a/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala b/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala index 686782578..463dde8bb 100644 --- a/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala +++ b/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala @@ -20,40 +20,25 @@ import zio.{ Runtime, Semaphore, Unsafe, ZIO, ZLayer } object Slf4jBridge { - /** - * log annotation key for slf4j logger name - */ - @deprecated("use zio.logging.loggerNameAnnotationKey", "2.1.8") - val loggerNameAnnotationKey: String = "slf4j_logger_name" - /** * initialize SLF4J bridge */ def initialize: ZLayer[Any, Nothing, Unit] = - Runtime.enableCurrentFiber ++ layer(zio.logging.loggerNameAnnotationKey) + Runtime.enableCurrentFiber ++ layer /** * initialize SLF4J bridge without `FiberRef` propagation */ - def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = layer(zio.logging.loggerNameAnnotationKey) - - /** - * initialize SLF4J bridge, where custom annotation key for logger name may be provided - * this is to achieve backward compatibility where [[Slf4jBridge.loggerNameAnnotationKey]] was used - * - * NOTE: this feature may be removed in future releases - */ - def initialize(nameAnnotationKey: String): ZLayer[Any, Nothing, Unit] = - Runtime.enableCurrentFiber ++ layer(nameAnnotationKey) + def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = layer private val initLock = Semaphore.unsafe.make(1)(Unsafe.unsafe) - private def layer(nameAnnotationKey: String): ZLayer[Any, Nothing, Unit] = + private def layer: ZLayer[Any, Nothing, Unit] = ZLayer { for { runtime <- ZIO.runtime[Any] _ <- initLock.withPermit { - ZIO.succeed(ZioLoggerFactory.initialize(new ZioLoggerRuntime(runtime, nameAnnotationKey))) + ZIO.succeed(ZioLoggerFactory.initialize(new ZioLoggerRuntime(runtime))) } } yield () } diff --git a/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala b/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala index 44d4941d6..895e851bd 100644 --- a/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala +++ b/slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala @@ -21,7 +21,7 @@ import org.slf4j.helpers.MessageFormatter import org.slf4j.impl.LoggerRuntime import zio.{ Cause, Fiber, FiberId, FiberRef, FiberRefs, LogLevel, Runtime, Trace, Unsafe } -final class ZioLoggerRuntime(runtime: Runtime[Any], loggerNameAnnotationKey: String) extends LoggerRuntime { +final class ZioLoggerRuntime(runtime: Runtime[Any]) extends LoggerRuntime { override def log( name: String, @@ -44,7 +44,7 @@ final class ZioLoggerRuntime(runtime: Runtime[Any], loggerNameAnnotationKey: Str } val logSpan = zio.LogSpan(name, java.lang.System.currentTimeMillis()) - val loggerName = (loggerNameAnnotationKey -> name) + val loggerName = (zio.logging.loggerNameAnnotationKey -> name) val fiberRefs = currentFiberRefs .updatedAs(fiberId)(FiberRef.currentLogSpan, logSpan :: currentFiberRefs.getOrDefault(FiberRef.currentLogSpan)) diff --git a/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala b/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala index dd503c7bf..489d8b447 100644 --- a/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala +++ b/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala @@ -9,23 +9,22 @@ object Slf4jBridgeExampleApp extends ZIOAppDefault { private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") - private val logFilter: LogFilter[String] = LogFilter.logLevelByName( + private val logFilterConfig = LogFilter.LogLevelByNameConfig( LogLevel.Info, "zio.logging.slf4j" -> LogLevel.Debug, "SLF4J-LOGGER" -> LogLevel.Warning ) + private val logFormat = LogFormat.label( + "name", + LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() + ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( + LogAnnotation.TraceId + ) + LogFormat.default + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger( - ConsoleLoggerConfig( - LogFormat.label( - "name", - LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() - ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( - LogAnnotation.TraceId - ) + LogFormat.default, - logFilter - ) + ConsoleLoggerConfig(logFormat, logFilterConfig) ) >+> Slf4jBridge.initialize private val uuids = List.fill(2)(UUID.randomUUID()) diff --git a/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeSpec.scala b/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeSpec.scala index 86bcb756e..d03247d84 100644 --- a/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeSpec.scala +++ b/slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeSpec.scala @@ -27,77 +27,6 @@ object Slf4jBridgeSpec extends ZIOSpecDefault { } } yield assertCompletes }, - test("logs through slf4j - legacy logger name annotation key") { - val testFailure = new RuntimeException("test error") - for { - _ <- - (for { - logger <- ZIO.attempt(org.slf4j.LoggerFactory.getLogger("test.logger")) - _ <- ZIO.logSpan("span")(ZIO.attempt(logger.debug("test debug message"))) @@ ZIOAspect - .annotated("trace_id", "tId") - _ <- ZIO.attempt(logger.warn("hello {}", "world")) @@ ZIOAspect.annotated("user_id", "uId") - _ <- ZIO.attempt(logger.warn("{}..{}..{} ... go!", "3", "2", "1")) - _ <- ZIO.attempt(logger.warn("warn cause", testFailure)) - _ <- ZIO.attempt(logger.error("error", testFailure)) - _ <- ZIO.attempt(logger.error("error", null)) - } yield ()).exit - output <- ZTestLogger.logOutput - lines = output.map { logEntry => - LogEntry( - logEntry.spans.map(_.label), - logEntry.logLevel, - logEntry.annotations, - logEntry.message(), - logEntry.cause - ) - } - } yield assertTrue( - lines == Chunk( - LogEntry( - List("test.logger", "span"), - LogLevel.Debug, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger", "trace_id" -> "tId"), - "test debug message", - Cause.empty - ), - LogEntry( - List("test.logger"), - LogLevel.Warning, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger", "user_id" -> "uId"), - "hello world", - Cause.empty - ), - LogEntry( - List("test.logger"), - LogLevel.Warning, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger"), - "3..2..1 ... go!", - Cause.empty - ), - LogEntry( - List("test.logger"), - LogLevel.Warning, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger"), - "warn cause", - Cause.die(testFailure) - ), - LogEntry( - List("test.logger"), - LogLevel.Error, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger"), - "error", - Cause.die(testFailure) - ), - LogEntry( - List("test.logger"), - LogLevel.Error, - Map(Slf4jBridge.loggerNameAnnotationKey -> "test.logger"), - "error", - Cause.empty - ) - ) - ) - }.provide(Slf4jBridge.initialize(Slf4jBridge.loggerNameAnnotationKey)), test("Implements Logger#getName") { for { logger <- ZIO.attempt(org.slf4j.LoggerFactory.getLogger("zio.test.logger")) diff --git a/slf4j/src/main/scala/zio/logging/slf4j/SLF4J.scala b/slf4j/src/main/scala/zio/logging/slf4j/SLF4J.scala index e11d56444..03d74da06 100644 --- a/slf4j/src/main/scala/zio/logging/slf4j/SLF4J.scala +++ b/slf4j/src/main/scala/zio/logging/slf4j/SLF4J.scala @@ -37,12 +37,6 @@ import java.util object SLF4J { - /** - * log annotation key for slf4j logger name - */ - @deprecated("use zio.logging.loggerNameAnnotationKey", "2.1.8") - val loggerNameAnnotationKey = "slf4j_logger_name" - /** * log annotation key for slf4j marker name */ @@ -53,18 +47,9 @@ object SLF4J { */ val logFormatDefault: LogFormat = LogFormat.allAnnotations(excludeKeys = - Set(SLF4J.loggerNameAnnotationKey, SLF4J.logMarkerNameAnnotationKey, logging.loggerNameAnnotationKey) + Set(SLF4J.logMarkerNameAnnotationKey, logging.loggerNameAnnotationKey) ) + LogFormat.line + LogFormat.cause - /** - * slf4j logger name aspect, by this aspect is possible to change default logger name (default logger name is extracted from [[Trace]]) - * - * annotation key: [[SLF4J.loggerNameAnnotationKey]] - */ - @deprecated("use zio.logging.loggerName", "2.1.8") - def loggerName(value: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = - ZIOAspect.annotated(loggerNameAnnotationKey, value) - /** * slf4j marker name aspect * @@ -201,27 +186,6 @@ object SLF4J { } } - @deprecated("use layer without logLevel", "2.0.1") - def slf4j( - logLevel: zio.LogLevel, - format: LogFormat, - loggerName: Trace => String - ): ZLayer[Any, Nothing, Unit] = - Runtime.addLogger(slf4jLogger(format, loggerName).filterLogLevel(_ >= logLevel)) - - @deprecated("use layer without logLevel", "2.0.1") - def slf4j( - logLevel: zio.LogLevel, - format: LogFormat - ): ZLayer[Any, Nothing, Unit] = - slf4j(logLevel, format, getLoggerName()) - - @deprecated("use layer without logLevel", "2.0.1") - def slf4j( - logLevel: zio.LogLevel - ): ZLayer[Any, Nothing, Unit] = - slf4j(logLevel, logFormatDefault, getLoggerName()) - /** * Use this layer to register an use an Slf4j logger in your app. * To avoid double logging, you should create this layer only once in your application @@ -256,32 +220,33 @@ object SLF4J { // as in some program failure cases it may happen, that program exit sooner then log message will be logged (#616) LoggerFactory.getLogger("zio-slf4j-logger") - new ZLogger[String, Unit] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => String, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Unit = { - val slf4jLoggerName = annotations.getOrElse( - SLF4J.loggerNameAnnotationKey, - annotations.getOrElse(logging.loggerNameAnnotationKey, loggerName(trace)) - ) - val slf4jLogger = LoggerFactory.getLogger(slf4jLoggerName) - val slf4jMarkerName = annotations.get(SLF4J.logMarkerNameAnnotationKey) - val slf4jMarker = slf4jMarkerName.map(n => MarkerFactory.getMarker(n)) - if (isLogLevelEnabled(slf4jLogger, slf4jMarker, logLevel)) { - val appender = logAppender(slf4jLogger, slf4jMarker, logLevel) + Slf4jLogger(format, loggerName) + } - format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) - appender.closeLogEntry() - } - () + private[logging] case class Slf4jLogger(format: LogFormat, loggerName: Trace => String) + extends ZLogger[String, Unit] { + + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => String, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Unit = { + val slf4jLoggerName = annotations.getOrElse(logging.loggerNameAnnotationKey, loggerName(trace)) + val slf4jLogger = LoggerFactory.getLogger(slf4jLoggerName) + val slf4jMarkerName = annotations.get(SLF4J.logMarkerNameAnnotationKey) + val slf4jMarker = slf4jMarkerName.map(n => MarkerFactory.getMarker(n)) + if (isLogLevelEnabled(slf4jLogger, slf4jMarker, logLevel)) { + val appender = logAppender(slf4jLogger, slf4jMarker, logLevel) + + format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) + appender.closeLogEntry() } + () } } diff --git a/slf4j/src/test/scala/zio/logging/backend/SLF4JSpec.scala b/slf4j/src/test/scala/zio/logging/backend/SLF4JSpec.scala index 4d82cc898..5dacbd322 100644 --- a/slf4j/src/test/scala/zio/logging/backend/SLF4JSpec.scala +++ b/slf4j/src/test/scala/zio/logging/backend/SLF4JSpec.scala @@ -114,20 +114,6 @@ object SLF4JSpec extends ZIOSpecDefault { ) } }.provide(loggerTraceAnnotation), - test("log all annotations into MDC with custom logger name - legacy") { - (startStop() @@ SLF4J.loggerName("my-logger")).map { case (traceId, users) => - val loggerOutput = TestAppender.logOutput - startStopAssert(loggerOutput, "my-logger") && assert(loggerOutput.map(_.mdc.get(LogAnnotation.TraceId.name)))( - equalTo( - Chunk.fill(4)(Some(traceId.toString)) :+ None - ) - ) && assert(loggerOutput.map(_.mdc.get("user")))( - equalTo(users.flatMap(u => Chunk.fill(2)(Some(u.toString))) :+ None) - ) && assert(loggerOutput.map(_.mdc.contains(SLF4J.loggerNameAnnotationKey)))( - equalTo(Chunk.fill(5)(false)) - ) - } - }.provide(loggerDefault), test("log all annotations into MDC with custom logger name") { (startStop() @@ logging.loggerName("my-logger")).map { case (traceId, users) => val loggerOutput = TestAppender.logOutput @@ -142,47 +128,6 @@ object SLF4JSpec extends ZIOSpecDefault { ) } }.provide(loggerDefault), - test("logger name changes - legacy logger name annotation") { - val users = Chunk.fill(2)(UUID.randomUUID()) - for { - traceId <- ZIO.succeed(UUID.randomUUID()) - _ = TestAppender.reset() - _ <- ZIO.logInfo("Start") @@ SLF4J.loggerName("root-logger") - _ <- ZIO.foreach(users) { uId => - { - ZIO.logInfo("Starting user operation") *> ZIO.sleep(500.millis) *> ZIO.logInfo( - "Stopping user operation" - ) - } @@ ZIOAspect.annotated("user", uId.toString) @@ SLF4J.loggerName("user-logger") - } @@ LogAnnotation.TraceId(traceId) @@ SLF4J.loggerName("user-root-logger") - _ <- ZIO.logInfo("Done") @@ SLF4J.loggerName("root-logger") - } yield { - val loggerOutput = TestAppender.logOutput - assertTrue(loggerOutput.forall(_.logLevel == LogLevel.Info)) && assert(loggerOutput.map(_.message))( - equalTo( - Chunk( - "Start", - "Starting user operation", - "Stopping user operation", - "Starting user operation", - "Stopping user operation", - "Done" - ) - ) - ) && assert(loggerOutput.map(_.loggerName))( - equalTo( - Chunk( - "root-logger", - "user-logger", - "user-logger", - "user-logger", - "user-logger", - "root-logger" - ) - ) - ) - } - }.provide(loggerDefault), test("logger name changes") { val users = Chunk.fill(2)(UUID.randomUUID()) for { @@ -230,15 +175,6 @@ object SLF4JSpec extends ZIOSpecDefault { someErrorAssert(loggerOutput) && assertTrue(loggerOutput(0).cause.exists(_.getMessage.contains("input < 1"))) } }.provide(loggerLineCause), - test("log error with cause with custom logger name - legacy") { - (someError() @@ SLF4J.loggerName("my-logger")).map { _ => - val loggerOutput = TestAppender.logOutput - someErrorAssert(loggerOutput, "my-logger") && assertTrue( - loggerOutput(0).cause.exists(_.getMessage.contains("input < 1")) - ) && - assertTrue(!loggerOutput(0).mdc.contains(SLF4J.loggerNameAnnotationKey)) - } - }.provide(loggerLineCause), test("log error with cause with custom logger name") { (someError() @@ logging.loggerName("my-logger")).map { _ => val loggerOutput = TestAppender.logOutput diff --git a/slf4j2-bridge/src/main/java/zio/logging/slf4j/bridge/LoggerFactory.java b/slf4j2-bridge/src/main/java/zio/logging/slf4j/bridge/LoggerFactory.java index f2aea9252..b655f3dd5 100644 --- a/slf4j2-bridge/src/main/java/zio/logging/slf4j/bridge/LoggerFactory.java +++ b/slf4j2-bridge/src/main/java/zio/logging/slf4j/bridge/LoggerFactory.java @@ -28,7 +28,7 @@ final class LoggerFactory implements ILoggerFactory { private LoggerRuntime runtime = null; - void attacheRuntime(LoggerRuntime runtime) { + void attachRuntime(LoggerRuntime runtime) { this.runtime = runtime; } diff --git a/slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala b/slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala index 716662978..e10b85321 100644 --- a/slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala +++ b/slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala @@ -40,7 +40,7 @@ object Slf4jBridge { org.slf4j.LoggerFactory .getILoggerFactory() .asInstanceOf[LoggerFactory] - .attacheRuntime(new ZioLoggerRuntime(runtime)) + .attachRuntime(new ZioLoggerRuntime(runtime)) ) } } yield () diff --git a/slf4j2-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala b/slf4j2-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala index 3b46b7ffc..06c7ff166 100644 --- a/slf4j2-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala +++ b/slf4j2-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala @@ -9,23 +9,22 @@ object Slf4jBridgeExampleApp extends ZIOAppDefault { private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") - private val logFilter: LogFilter[String] = LogFilter.logLevelByName( + private val logFilterConfig = LogFilter.LogLevelByNameConfig( LogLevel.Info, "zio.logging.slf4j" -> LogLevel.Debug, "SLF4J-LOGGER" -> LogLevel.Warning ) + private val logFormat = LogFormat.label( + "name", + LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() + ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( + LogAnnotation.TraceId + ) + LogFormat.default + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger( - ConsoleLoggerConfig( - LogFormat.label( - "name", - LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() - ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( - LogAnnotation.TraceId - ) + LogFormat.default, - logFilter - ) + ConsoleLoggerConfig(logFormat, logFilterConfig) ) >+> Slf4jBridge.initialize private val uuids = List.fill(2)(UUID.randomUUID()) diff --git a/slf4j2/src/main/scala/zio/logging/slf4j/SLF4J.scala b/slf4j2/src/main/scala/zio/logging/slf4j/SLF4J.scala index 993b28e93..cbf4fec13 100644 --- a/slf4j2/src/main/scala/zio/logging/slf4j/SLF4J.scala +++ b/slf4j2/src/main/scala/zio/logging/slf4j/SLF4J.scala @@ -186,29 +186,32 @@ object SLF4J { // as in some program failure cases it may happen, that program exit sooner then log message will be logged (#616) LoggerFactory.getLogger("zio-slf4j-logger") - new ZLogger[String, Unit] { - override def apply( - trace: Trace, - fiberId: FiberId, - logLevel: LogLevel, - message: () => String, - cause: Cause[Any], - context: FiberRefs, - spans: List[LogSpan], - annotations: Map[String, String] - ): Unit = { - val slf4jLoggerName = annotations.getOrElse(zio.logging.loggerNameAnnotationKey, loggerName(trace)) - val slf4jLogger = LoggerFactory.getLogger(slf4jLoggerName) - val slf4jMarkerName = annotations.get(logMarkerNameAnnotationKey) - val slf4jMarker = slf4jMarkerName.map(n => MarkerFactory.getMarker(n)) - if (isLogLevelEnabled(slf4jLogger, slf4jMarker, logLevel)) { - val appender = logAppender(slf4jLogger, slf4jMarker, logLevel) - - format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) - appender.closeLogEntry() - } - () + Slf4jLogger(format, loggerName) + } + + private[logging] case class Slf4jLogger(format: LogFormat, loggerName: Trace => String) + extends ZLogger[String, Unit] { + override def apply( + trace: Trace, + fiberId: FiberId, + logLevel: LogLevel, + message: () => String, + cause: Cause[Any], + context: FiberRefs, + spans: List[LogSpan], + annotations: Map[String, String] + ): Unit = { + val slf4jLoggerName = annotations.getOrElse(zio.logging.loggerNameAnnotationKey, loggerName(trace)) + val slf4jLogger = LoggerFactory.getLogger(slf4jLoggerName) + val slf4jMarkerName = annotations.get(logMarkerNameAnnotationKey) + val slf4jMarker = slf4jMarkerName.map(n => MarkerFactory.getMarker(n)) + if (isLogLevelEnabled(slf4jLogger, slf4jMarker, logLevel)) { + val appender = logAppender(slf4jLogger, slf4jMarker, logLevel) + + format.unsafeFormat(appender)(trace, fiberId, logLevel, message, cause, context, spans, annotations) + appender.closeLogEntry() } + () } }