From 178679f762c9d3bce7d854275bf94b3cbcddb4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=8BAndrzej=20Ressel?= Date: Mon, 27 May 2024 17:04:30 +0200 Subject: [PATCH] Implement java.util.logging bridge (#849) --- .github/workflows/ci.yml | 32 +-- README.md | 12 + build.sbt | 24 +- docs/jul-bridge.md | 131 ++++++++++ docs/sidebars.js | 1 + .../logging/example/JULBridgeExampleApp.scala | 61 +++++ .../zio/logging/jul/bridge/JULBridge.scala | 86 +++++++ .../logging/jul/bridge/ZioLoggerRuntime.scala | 103 ++++++++ .../logging/jul/bridge/JULBridgeSpec.scala | 232 ++++++++++++++++++ 9 files changed, 665 insertions(+), 17 deletions(-) create mode 100644 docs/jul-bridge.md create mode 100644 examples/jul-bridge/src/main/scala/zio/logging/example/JULBridgeExampleApp.scala create mode 100644 jul-bridge/src/main/scala/zio/logging/jul/bridge/JULBridge.scala create mode 100644 jul-bridge/src/main/scala/zio/logging/jul/bridge/ZioLoggerRuntime.scala create mode 100644 jul-bridge/src/test/scala/zio/logging/jul/bridge/JULBridgeSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c2056b3..a63d77be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,36 +85,36 @@ jobs: uses: actions/checkout@v3.3.0 with: fetch-depth: '0' + - name: Test + if: ${{ (matrix.java == '17') && (matrix.scala == '3.3.0') }} + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' - name: Test if: ${{ (matrix.java == '17') && (matrix.scala == '2.13.10') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' - name: Test if: ${{ (matrix.java == '8') && (matrix.scala == '2.13.10') }} - run: 'sbt ++${{ matrix.scala }} coreJS/test slf4jBridge/test coreJVM/test slf4j/test ' + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test julBridge/test coreJS/test coreJVM/test ' - name: Test - if: ${{ (matrix.java == '8') && (matrix.scala == '2.12.17') }} - run: 'sbt ++${{ matrix.scala }} coreJS/test slf4jBridge/test coreJVM/test slf4j/test ' + if: ${{ (matrix.java == '17') && (matrix.scala == '2.12.17') }} + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' - name: Test if: ${{ (matrix.java == '11') && (matrix.scala == '3.3.0') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' - - name: Test - if: ${{ (matrix.java == '11') && (matrix.scala == '2.12.17') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' - - name: Test - if: ${{ (matrix.java == '17') && (matrix.scala == '2.12.17') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' - name: Test if: ${{ (matrix.java == '11') && (matrix.scala == '2.13.10') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' - name: Test - if: ${{ (matrix.java == '17') && (matrix.scala == '3.3.0') }} - run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test slf4j2Bridge/test coreJS/test coreJVM/test ' + if: ${{ (matrix.java == '11') && (matrix.scala == '2.12.17') }} + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test jpl/test slf4j2/test julBridge/test slf4j2Bridge/test coreJS/test coreJVM/test ' + - name: Test + if: ${{ (matrix.java == '8') && (matrix.scala == '2.12.17') }} + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test julBridge/test coreJS/test coreJVM/test ' - name: Test if: ${{ (matrix.java == '8') && (matrix.scala == '3.3.0') }} - run: 'sbt ++${{ matrix.scala }} coreJS/test slf4jBridge/test coreJVM/test slf4j/test ' + run: 'sbt ++${{ matrix.scala }} slf4j/test slf4jBridge/test julBridge/test coreJS/test coreJVM/test ' - name: Compile additional subprojects if: ${{ ((startsWith(matrix.scala, '2.12.')) || (startsWith(matrix.scala, '2.13.'))) && (matrix.java == '11') }} - run: sbt ++${{ matrix.scala }} examplesCore/compile examplesJpl/compile examplesSlf4j2Bridge/compile examplesSlf4jLogback/compile examplesSlf4j2Logback/compile examplesSlf4j2Log4j/compile benchmarks/compile + run: sbt ++${{ matrix.scala }} examplesCore/compile examplesJpl/compile examplesSlf4j2Bridge/compile examplesSlf4jLogback/compile examplesSlf4j2Logback/compile examplesSlf4j2Log4j/compile examplesJulBridge/compile benchmarks/compile ci: name: ci runs-on: ubuntu-latest diff --git a/README.md b/README.md index 48983c48..26ce611b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ Other modules: See [Java Platform/System Logger](docs/jpl.md) section for more details. +* java.util.logging bridge - with this logging bridge, it is possible to use `zio-logging` for JUL loggers (usually third-party non-ZIO libraries), add the one of following lines to your `build.sbt` file: + + ```scala + // JUL bridge + libraryDependencies += "dev.zio" %% "zio-logging-jul-bridge" % "2.2.2" + ``` + + When to use this module: you are already using JUL logger in some other project, and you like to have same log outputs. + See [java.util.logging bridge](docs/jul-bridge.md) section for more details. + + + ## Example Let's try an example of ZIO Logging which demonstrates a simple application of ZIO logging. diff --git a/build.sbt b/build.sbt index 6a544774..f75cf24d 100644 --- a/build.sbt +++ b/build.sbt @@ -17,6 +17,7 @@ inThisBuild( (coreJS / thisProject).value.id -> (coreJS / crossScalaVersions).value, (coreJVM / thisProject).value.id -> (coreJVM / crossScalaVersions).value, (jpl / thisProject).value.id -> (jpl / crossScalaVersions).value, + (julBridge / thisProject).value.id -> (julBridge / crossScalaVersions).value, (slf4j / thisProject).value.id -> (slf4j / crossScalaVersions).value, (slf4j2 / thisProject).value.id -> (slf4j2 / crossScalaVersions).value, (slf4jBridge / thisProject).value.id -> (slf4jBridge / crossScalaVersions).value, @@ -37,7 +38,8 @@ inThisBuild( ), run = Some( "sbt ++${{ matrix.scala }} examplesCore/compile examplesJpl/compile examplesSlf4j2Bridge/compile " + - "examplesSlf4jLogback/compile examplesSlf4j2Logback/compile examplesSlf4j2Log4j/compile benchmarks/compile" + "examplesSlf4jLogback/compile examplesSlf4j2Logback/compile examplesSlf4j2Log4j/compile " + + "examplesJulBridge/compile benchmarks/compile" ) ) ), @@ -79,9 +81,11 @@ lazy val root = project slf4jBridge, slf4j2Bridge, jpl, + julBridge, benchmarks, examplesCore, examplesJpl, + examplesJulBridge, examplesSlf4j2Bridge, examplesSlf4jLogback, examplesSlf4j2Logback, @@ -175,6 +179,16 @@ lazy val slf4j2Bridge = project ) .settings(enableZIO()) +lazy val julBridge = project + .in(file("jul-bridge")) + .dependsOn(coreJVM) + .settings(stdSettings("zio-logging-jul-bridge", turnCompilerWarningIntoErrors = false)) + .settings(enableZIO(enableTesting = true)) + .settings(mimaSettings(failOnProblem = true)) + .settings( + Test / fork := true + ) + lazy val jpl = project .in(file("jpl")) .dependsOn(coreJVM) @@ -251,6 +265,14 @@ lazy val examplesJpl = project publish / skip := true ) +lazy val examplesJulBridge = project + .in(file("examples/jul-bridge")) + .dependsOn(julBridge) + .settings(stdSettings("zio-logging-examples-jul-bridge", turnCompilerWarningIntoErrors = false)) + .settings( + publish / skip := true + ) + lazy val examplesSlf4j2Bridge = project .in(file("examples/slf4j2-bridge")) .dependsOn(slf4j2Bridge) diff --git a/docs/jul-bridge.md b/docs/jul-bridge.md new file mode 100644 index 00000000..40ad4270 --- /dev/null +++ b/docs/jul-bridge.md @@ -0,0 +1,131 @@ +--- +id: jul-bridge +title: "java.util.logging bridge" +--- + +It is possible to use `zio-logging` for included `java.util.logging` Loggers (do not confuse with `java.platform.logging`), +usually third-party non-ZIO libraries (most notable: OpenTelemetry used by ZIO-telemetry). To do so, import the `zio-logging-jul-bridge` module +```scala +libraryDependencies += "dev.zio" %% "zio-logging-jul-bridge" % "@VERSION@" +``` + +and use one of the `JULBridge` layers when setting up logging + +```scala +import zio.logging.jul.bridge.JULBridge + +program.provideCustom(JULBridge.init()) +``` + +`JULBridge` layers: +* `JULBridge.init(configPath: NonEmptyChunk[String] = logFilterConfigPath)` - setup with `LogFilter` from [filter configuration](log-filter.md#configuration), default configuration path: `logger.filter`, default `LogLevel` is `INFO` +* `JULBridge.init(filter: LogFilter[Any])` - setup with given `LogFilter` +* `JULBridge.initialize` - setup without filtering + +Need for log filtering in JUL bridge: filtering in JUL is made on higher level than `jul-bridge` (on `Logger` level and not `Handler` level - which `JULBridge` is). Due to that the whole +filtering in JUL is disabled and is implemented in JULBridge. This may cause degraded performance and much more logs when using other Handlers. + +
+ +JUL logger name is stored in log annotation with key `logger_name` (`zio.logging.loggerNameAnnotationKey`), following log format + +```scala +import zio.logging.jul.bridge.JULBridge +import zio.logging.LoggerNameExtractor + +val loggerName = LoggerNameExtractor.loggerNameAnnotationOrTrace +val loggerNameFormat = loggerName.toLogFormat() +``` +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 logFilterConfig = LogFilter.LogLevelByNameConfig( + LogLevel.Info, + "zio.logging.jul " -> LogLevel.Debug, + "JUL-LOGGER" -> LogLevel.Warning +) + +val logFilter: LogFilter[String] = logFilterConfig.toFilter +``` +
+ + +JUL bridge with custom logger can be setup: + +```scala +import zio.logging.jul.bridge.JULBridge +import zio.logging.consoleJsonLogger + +val logger = Runtime.removeDefaultLoggers >>> consoleJsonLogger() >+> JULBridge.init() +``` + +
+ +## Examples + +You can find the source code [here](https://github.com/zio/zio-logging/tree/master/examples) + +### JUL bridge with JSON console logger + +[//]: # (TODO: make snippet type-checked using mdoc) + + +```scala +package zio.logging.example + +import zio.logging.jul.bridge.JULBridge +import zio.logging.{ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger} +import zio.{ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} + +import java.util.UUID + +object JULBridgeExampleApp extends ZIOAppDefault { + + private val julLogger = java.util.logging.Logger.getLogger("JUL-LOGGER") + + 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 + + private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) + + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = + Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> JULBridge.init(loggerConfig.toFilter) + + private val uuids = List.fill(2)(UUID.randomUUID()) + + override def run: ZIO[Scope, Any, ExitCode] = + for { + _ <- ZIO.logInfo("Start") + _ <- ZIO.foreachPar(uuids) { u => + ZIO.succeed(julLogger.info("Test INFO!")) *> ZIO.succeed( + julLogger.warning("Test WARNING!") + ) @@ LogAnnotation.UserId( + u.toString + ) + } @@ LogAnnotation.TraceId(UUID.randomUUID()) + _ <- ZIO.logDebug("Done") + } yield ExitCode.success + +} +``` + +Expected console output: +``` +{"name":"zio.logging.example.JULbridgeExampleApp","timestamp":"2024-05-26T13:50:20.6832831+02:0","level":"INFO","thread":"zio-fiber-1143120685","message":"Start"} +{"name":"JUL-LOGGER","trace_id":"08e9e10a-d3c5-4f90-8627-2ae4ddee1522","timestamp":"2024-05-26T13:50:20.7112909+02:0","level":"INFO","thread":"zio-fiber-1683803358","message":"Test INFO!"} +{"name":"JUL-LOGGER","trace_id":"08e9e10a-d3c5-4f90-8627-2ae4ddee1522","timestamp":"2024-05-26T13:50:20.7112909+02:0","level":"INFO","thread":"zio-fiber-71852457","message":"Test INFO!"} +{"name":"JUL-LOGGER","user_id":"85f762cc-e62c-4576-9f14-6a3ad0918d99","trace_id":"08e9e10a-d3c5-4f90-8627-2ae4ddee1522","timestamp":"2024-05-26T13:50:20.7142882+02:0","level":"WARN","thread":"zio-fiber-1911711828","message":"Test WARNING!"} +{"name":"JUL-LOGGER","user_id":"47850c02-bb60-4b6a-9c0f-0aa095066d10","trace_id":"08e9e10a-d3c5-4f90-8627-2ae4ddee1522","timestamp":"2024-05-26T13:50:20.7142882+02:0","level":"WARN","thread":"zio-fiber-1801412106","message":"Test WARNING!"} +``` \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index cf12c259..d8345ccd 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -12,6 +12,7 @@ const sidebars = { 'file-logger', 'reconfigurable-logger', 'jpl', + 'jul-bridge', 'slf4j2', 'slf4j1', 'slf4j2-bridge', diff --git a/examples/jul-bridge/src/main/scala/zio/logging/example/JULBridgeExampleApp.scala b/examples/jul-bridge/src/main/scala/zio/logging/example/JULBridgeExampleApp.scala new file mode 100644 index 00000000..b364b655 --- /dev/null +++ b/examples/jul-bridge/src/main/scala/zio/logging/example/JULBridgeExampleApp.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2024 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 zio.logging.jul.bridge.JULBridge +import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } +import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } + +import java.util.UUID + +object JULBridgeExampleApp extends ZIOAppDefault { + + private val julLogger = java.util.logging.Logger.getLogger("JUL-LOGGER") + + 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 + + private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) + + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = + Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> JULBridge.init(loggerConfig.toFilter) + + private val uuids = List.fill(2)(UUID.randomUUID()) + + override def run: ZIO[Scope, Any, ExitCode] = + for { + _ <- ZIO.logInfo("Start") + _ <- ZIO.foreachPar(uuids) { u => + ZIO.succeed(julLogger.info("Test INFO!")) *> ZIO.succeed( + julLogger.warning("Test WARNING!") + ) @@ LogAnnotation.UserId( + u.toString + ) + } @@ LogAnnotation.TraceId(UUID.randomUUID()) + _ <- ZIO.logDebug("Done") + } yield ExitCode.success + +} diff --git a/jul-bridge/src/main/scala/zio/logging/jul/bridge/JULBridge.scala b/jul-bridge/src/main/scala/zio/logging/jul/bridge/JULBridge.scala new file mode 100644 index 00000000..0280b6a6 --- /dev/null +++ b/jul-bridge/src/main/scala/zio/logging/jul/bridge/JULBridge.scala @@ -0,0 +1,86 @@ +/* + * Copyright 2019-2024 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.jul.bridge + +import zio.logging.LogFilter +import zio.{ Config, NonEmptyChunk, Runtime, Semaphore, Unsafe, ZIO, ZLayer } + +import java.io.ByteArrayInputStream +import java.util.logging.{ LogManager, Logger } + +object JULBridge { + + val logFilterConfigPath: NonEmptyChunk[String] = zio.logging.loggerConfigPath :+ "filter" + + /** + * initialize java.util.Logging bridge + */ + def initialize: ZLayer[Any, Nothing, Unit] = init(LogFilter.acceptAll) + + /** + * initialize java.util.Logging bridge with `LogFilter` + * + * @param filter Log filter + */ + def init(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = Runtime.enableCurrentFiber ++ layer(filter) + + /** + * initialize java.util.Logging bridge with `LogFilter` from configuration + * @param configPath configuration path + */ + def init(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZLayer[Any, Config.Error, Unit] = + Runtime.enableCurrentFiber ++ layer(configPath) + + /** + * initialize java.util.Logging bridge without `FiberRef` propagation + */ + def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = initWithoutFiberRefPropagation( + LogFilter.acceptAll + ) + + /** + * initialize java.util.Logging bridge with `LogFilter`, without `FiberRef` propagation + * @param filter Log filter + */ + def initWithoutFiberRefPropagation(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = layer(filter) + + private val initLock = Semaphore.unsafe.make(1)(Unsafe.unsafe) + + private def layer(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = + ZLayer(make(filter)) + + private def layer(configPath: NonEmptyChunk[String]): ZLayer[Any, Config.Error, Unit] = + ZLayer(make(configPath)) + + def make(filter: LogFilter[Any]): ZIO[Any, Nothing, Unit] = + for { + runtime <- ZIO.runtime[Any] + _ <- initLock.withPermit { + ZIO.succeed { + val configuration = ".level= ALL" + LogManager.getLogManager.readConfiguration(new ByteArrayInputStream(configuration.getBytes)) + + java.util.logging.Logger + .getLogger("") + .addHandler(new ZioLoggerRuntime(runtime, filter)) + } + } + } yield () + + def make(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZIO[Any, Config.Error, Unit] = + LogFilter.LogLevelByNameConfig.load(configPath).flatMap(c => make(c.toFilter)) + +} diff --git a/jul-bridge/src/main/scala/zio/logging/jul/bridge/ZioLoggerRuntime.scala b/jul-bridge/src/main/scala/zio/logging/jul/bridge/ZioLoggerRuntime.scala new file mode 100644 index 00000000..0d025688 --- /dev/null +++ b/jul-bridge/src/main/scala/zio/logging/jul/bridge/ZioLoggerRuntime.scala @@ -0,0 +1,103 @@ +/* + * Copyright 2019-2024 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.jul.bridge + +import zio.logging.LogFilter +import zio.{ Cause, Fiber, FiberId, FiberRef, FiberRefs, LogLevel, Runtime, Trace, Unsafe } + +import java.util.logging.{ Handler, Level, LogRecord } + +final class ZioLoggerRuntime(runtime: Runtime[Any], filter: LogFilter[Any]) extends Handler { + + override def publish(record: LogRecord): Unit = { + if (!isEnabled(record.getLoggerName, record.getLevel)) { + return + } + + Unsafe.unsafe { implicit u => + val msg = record.getMessage + val level = record.getLevel + val name = record.getLoggerName + val throwable = record.getThrown + + val logLevel = ZioLoggerRuntime.logLevelMapping(level) + val trace = Trace.empty + val fiberId = FiberId.make(trace) + val currentFiber = Fiber._currentFiber.get() + + val currentFiberRefs = if (currentFiber eq null) { + runtime.fiberRefs.joinAs(fiberId)(FiberRefs.empty) + } else { + runtime.fiberRefs.joinAs(fiberId)(currentFiber.unsafe.getFiberRefs()) + } + + val logSpan = zio.LogSpan(name, java.lang.System.currentTimeMillis()) + val loggerName = (zio.logging.loggerNameAnnotationKey -> name) + + val fiberRefs = currentFiberRefs + .updatedAs(fiberId)(FiberRef.currentLogSpan, logSpan :: currentFiberRefs.getOrDefault(FiberRef.currentLogSpan)) + .updatedAs(fiberId)( + FiberRef.currentLogAnnotations, + currentFiberRefs.getOrDefault(FiberRef.currentLogAnnotations) + loggerName + ) + + val fiberRuntime = zio.internal.FiberRuntime(fiberId, fiberRefs, runtime.runtimeFlags) + + val cause = if (throwable != null) { + Cause.die(throwable) + } else { + Cause.empty + } + + fiberRuntime.log(() => msg, cause, Some(logLevel), trace) + + } + } + + override def flush(): Unit = () + + override def close(): Unit = () + + private def isEnabled(name: String, level: Level): Boolean = { + val logLevel = ZioLoggerRuntime.logLevelMapping(level) + + filter( + Trace(name, "", 0), + FiberId.None, + logLevel, + () => "", + Cause.empty, + FiberRefs.empty, + List.empty, + Map(zio.logging.loggerNameAnnotationKey -> name) + ) + } +} + +object ZioLoggerRuntime { + + private val logLevelMapping: Map[Level, LogLevel] = Map( + Level.SEVERE -> LogLevel.Fatal, + Level.WARNING -> LogLevel.Warning, + Level.INFO -> LogLevel.Info, + Level.CONFIG -> LogLevel.Info, + Level.FINE -> LogLevel.Debug, + Level.FINER -> LogLevel.Debug, + Level.FINEST -> LogLevel.Trace, + Level.ALL -> LogLevel.All, + Level.OFF -> LogLevel.None + ) +} diff --git a/jul-bridge/src/test/scala/zio/logging/jul/bridge/JULBridgeSpec.scala b/jul-bridge/src/test/scala/zio/logging/jul/bridge/JULBridgeSpec.scala new file mode 100644 index 00000000..140942ba --- /dev/null +++ b/jul-bridge/src/test/scala/zio/logging/jul/bridge/JULBridgeSpec.scala @@ -0,0 +1,232 @@ +package zio.logging.jul.bridge + +import zio.logging.LogFilter +import zio.test._ +import zio.{ Cause, Chunk, ConfigProvider, LogLevel, Runtime, ZIO, ZIOAspect } + +import java.util.logging.Level._ +import java.util.logging.Logger + +object JULBridgeSpec extends ZIOSpecDefault { + + final case class LogEntry( + span: List[String], + level: LogLevel, + annotations: Map[String, String], + message: String, + cause: Cause[Any] + ) + + override def spec = + suite("Slf4jBridge")( + test("parallel init") { + for { + _ <- + ZIO.foreachPar((1 to 5).toList) { _ => + ZIO + .succeed(Logger.getLogger("SLF4J-LOGGER").warning("Test WARNING!")) + .provide(JULBridge.initialize) + } + } yield assertCompletes + }, + test("logs through slf4j") { + val testFailure = new RuntimeException("test error") + for { + _ <- + (for { + logger <- ZIO.attempt(Logger.getLogger("test.logger")) + _ <- ZIO.logSpan("span")(ZIO.attempt(logger.fine("test fine message"))) @@ ZIOAspect + .annotated("trace_id", "tId") + _ <- ZIO.attempt(logger.warning("hello world")) @@ ZIOAspect.annotated("user_id", "uId") + _ <- ZIO.attempt(logger.warning("3..2..1 ... go!")) + _ <- ZIO.attempt(logger.log(WARNING, "warn cause", testFailure)) + _ <- ZIO.attempt(logger.log(SEVERE, "severe", testFailure)) + _ <- ZIO.attempt(logger.log(SEVERE, "severe")) + } 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(zio.logging.loggerNameAnnotationKey -> "test.logger", "trace_id" -> "tId"), + "test fine message", + Cause.empty + ), + LogEntry( + List("test.logger"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger", "user_id" -> "uId"), + "hello world", + Cause.empty + ), + LogEntry( + List("test.logger"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "3..2..1 ... go!", + Cause.empty + ), + LogEntry( + List("test.logger"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "warn cause", + Cause.die(testFailure) + ), + LogEntry( + List("test.logger"), + LogLevel.Fatal, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "severe", + Cause.die(testFailure) + ), + LogEntry( + List("test.logger"), + LogLevel.Fatal, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "severe", + Cause.empty + ) + ) + ) + }.provide(JULBridge.initialize), + test("Implements Logger#getName") { + for { + logger <- ZIO.attempt(Logger.getLogger("zio.test.logger")) + } yield assertTrue(logger.getName == "zio.test.logger") + }.provide(JULBridge.initialize), + test("logs through slf4j without fiber ref propagation") { + for { + _ <- (for { + logger <- ZIO.attempt(Logger.getLogger("test.logger")) + _ <- ZIO.logSpan("span")(ZIO.attempt(logger.fine("test fine message"))) @@ ZIOAspect + .annotated("trace_id", "tId") + _ <- ZIO.attempt(logger.warning("hello world")) @@ ZIOAspect.annotated("user_id", "uId") + } 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"), + LogLevel.Debug, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "test fine message", + Cause.empty + ), + LogEntry( + List("test.logger"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger"), + "hello world", + Cause.empty + ) + ) + ) + }.provide(JULBridge.initializeWithoutFiberRefPropagation), + test("logs through slf4j with filter") { + filterTest + }.provide( + JULBridge.init( + LogFilter.logLevelByName( + LogLevel.Debug, + "test.logger" -> LogLevel.Info, + "test.test.logger" -> LogLevel.Warning + ) + ) + ), + test("logs through slf4j with filter from config") { + filterTest + }.provide { + val configProvider: ConfigProvider = ConfigProvider.fromMap( + Map( + "logger/filter/rootLevel" -> "DEBUG", + "logger/filter/mappings/test.logger" -> "INFO", + "logger/filter/mappings/test.test.logger" -> "WARN" + ), + "/" + ) + Runtime.setConfigProvider(configProvider) >>> JULBridge.init() + } + ) @@ TestAspect.after(removeExistingHandlers) @@ TestAspect.sequential + + def filterTest: ZIO[Any, Nothing, TestResult] = + for { + _ <- (for { + logger1 <- ZIO.attempt(Logger.getLogger("test.abc")) + logger2 <- ZIO.attempt(Logger.getLogger("test.logger.def")) + logger3 <- ZIO.attempt(Logger.getLogger("test.test.logger.xyz")) + _ <- ZIO.attempt(logger1.fine("test debug message")) + _ <- ZIO.attempt(logger1.warning("test warning message")) + _ <- ZIO.attempt(logger2.fine("hello2 world fine")) + _ <- ZIO.attempt(logger2.warning("hello2 world warning")) + _ <- ZIO.attempt(logger3.info("hello3 world info")) + _ <- ZIO.attempt(logger3.warning("hello3 world warning")) + } 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.abc"), + LogLevel.Debug, + Map(zio.logging.loggerNameAnnotationKey -> "test.abc"), + "test debug message", + Cause.empty + ), + LogEntry( + List("test.abc"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.abc"), + "test warning message", + Cause.empty + ), + LogEntry( + List("test.logger.def"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.logger.def"), + "hello2 world warning", + Cause.empty + ), + LogEntry( + List("test.test.logger.xyz"), + LogLevel.Warning, + Map(zio.logging.loggerNameAnnotationKey -> "test.test.logger.xyz"), + "hello3 world warning", + Cause.empty + ) + ) + ) + + def removeExistingHandlers: ZIO[Any, Nothing, Unit] = + ZIO.succeed( + Logger.getLogger("").getHandlers.foreach(Logger.getLogger("").removeHandler(_)) + ) + +}