├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── release-drafter.yml ├── renovate.json └── workflows │ ├── auto-approve.yml │ ├── ci.yml │ └── release-drafter.yml ├── .gitignore ├── .jvmopts ├── .mergify.yml ├── .nvmrc ├── .scalafix.conf ├── .scalafmt.conf ├── .vscode └── settings.json ├── CLA.md ├── LICENSE ├── README.md ├── benchmarks └── src │ └── main │ └── scala │ └── zio │ └── logging │ └── FilterBenchmarks.scala ├── build.sbt ├── core ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── LoggingPackagePlatformSpecific.scala ├── jvm-native │ └── src │ │ ├── main │ │ └── scala │ │ │ └── zio │ │ │ └── logging │ │ │ ├── FileLoggerLayers.scala │ │ │ ├── LoggingPackagePlatformSpecific.scala │ │ │ └── internal │ │ │ ├── FileWriter.scala │ │ │ └── WriterProvider.scala │ │ └── test │ │ └── scala │ │ └── zio │ │ └── logging │ │ ├── FileLoggerConfigSpec.scala │ │ └── internal │ │ └── WriterProviderSpec.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── zio │ │ └── logging │ │ ├── ConsoleLoggerConfig.scala │ │ ├── FileLoggerConfig.scala │ │ ├── FilteredLogger.scala │ │ ├── LogAnnotation.scala │ │ ├── LogColor.scala │ │ ├── LogContext.scala │ │ ├── LogFilter.scala │ │ ├── LogFormat.scala │ │ ├── LogGroup.scala │ │ ├── LoggerLayers.scala │ │ ├── LoggerNameExtractor.scala │ │ ├── LoggingPackageAllPlatforms.scala │ │ ├── MetricLogger.scala │ │ ├── ReconfigurableLogger.scala │ │ ├── internal │ │ ├── JsonEscape.scala │ │ ├── JsonValidator.scala │ │ └── LogAppender.scala │ │ └── package.scala │ └── test │ └── scala │ └── zio │ └── logging │ ├── AppendLoggerNameSpec.scala │ ├── ConsoleLoggerConfigSpec.scala │ ├── JsonGenerator.scala │ ├── JsonLogFormatSpec.scala │ ├── LogAnnotationSpec.scala │ ├── LogFilterSpec.scala │ ├── LogFormatPatternSpec.scala │ ├── LogFormatSpec.scala │ ├── LogGroupSpec.scala │ ├── LoggerNameExtractorSpec.scala │ ├── MetricsSpec.scala │ ├── ReconfigurableLoggerSpec.scala │ ├── internal │ └── JsonValidatorSpec.scala │ └── test │ └── TestService.scala ├── docs ├── console-logger.md ├── file-logger.md ├── formatting-log-records.md ├── index.md ├── jpl.md ├── jul-bridge.md ├── log-filter.md ├── logger-context-and-annotations.md ├── metrics.md ├── package.json ├── reconfigurable-logger.md ├── sidebars.js ├── slf4j1-bridge.md ├── slf4j1.md ├── slf4j2-bridge.md ├── slf4j2.md └── testing.md ├── examples ├── core │ ├── jvm │ │ └── src │ │ │ ├── main │ │ │ ├── resources │ │ │ │ └── logger.conf │ │ │ └── scala │ │ │ │ └── zio │ │ │ │ └── logging │ │ │ │ ├── api │ │ │ │ └── http │ │ │ │ │ ├── ApiDomain.scala │ │ │ │ │ ├── ApiEndpoints.scala │ │ │ │ │ └── ApiHandlers.scala │ │ │ │ └── example │ │ │ │ ├── ConfigurableLoggerApp.scala │ │ │ │ ├── ConsoleColoredApp.scala │ │ │ │ ├── ConsoleJsonApp.scala │ │ │ │ ├── FileApp.scala │ │ │ │ ├── LoggerReconfigureApp.scala │ │ │ │ ├── MetricsApp.scala │ │ │ │ └── PingService.scala │ │ │ └── test │ │ │ └── scala │ │ │ └── zio │ │ │ └── logging │ │ │ ├── api │ │ │ └── http │ │ │ │ ├── ApiEndpointsSpec.scala │ │ │ │ └── ApiHandlersSpec.scala │ │ │ └── example │ │ │ └── PingServiceSpec.scala │ └── shared │ │ └── src │ │ ├── main │ │ └── scala │ │ │ └── zio │ │ │ └── logging │ │ │ ├── ConfigurableLogger.scala │ │ │ └── example │ │ │ └── SimpleApp.scala │ │ └── test │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ └── LoggingSpec.scala ├── jpl │ └── src │ │ └── main │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ └── JplSimpleApp.scala ├── jul-bridge │ └── src │ │ └── main │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ └── JULBridgeExampleApp.scala ├── slf4j-logback │ └── src │ │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ ├── CustomTracingAnnotationApp.scala │ │ ├── PingService.scala │ │ ├── Slf4jExampleApp.scala │ │ ├── Slf4jFailureApp.scala │ │ └── Slf4jSimpleApp.scala ├── slf4j2-bridge │ └── src │ │ └── main │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ └── Slf4jBridgeExampleApp.scala ├── slf4j2-log4j │ └── src │ │ └── main │ │ ├── resources │ │ └── log4j2.xml │ │ └── scala │ │ └── zio │ │ └── logging │ │ └── example │ │ └── Slf4jSimpleApp.scala └── slf4j2-logback │ └── src │ └── main │ ├── resources │ └── logback.xml │ └── scala │ └── zio │ └── logging │ └── example │ └── Slf4j2SimpleApp.scala ├── jpl └── src │ ├── main │ └── scala │ │ └── zio │ │ └── logging │ │ └── backend │ │ └── JPL.scala │ └── test │ └── scala │ └── zio │ └── logging │ └── backend │ ├── JPLSpec.scala │ ├── TestAppender.scala │ └── TestJPLogger.scala ├── jul-bridge └── src │ ├── main │ └── scala │ │ └── zio │ │ └── logging │ │ └── jul │ │ └── bridge │ │ ├── JULBridge.scala │ │ └── ZioLoggerRuntime.scala │ └── test │ └── scala │ └── zio │ └── logging │ └── jul │ └── bridge │ └── JULBridgeSpec.scala ├── project ├── BuildHelper.scala ├── MimaSettings.scala ├── Versions.scala ├── build.properties └── plugins.sbt ├── sbt ├── slf4j-bridge └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── slf4j │ │ │ ├── helpers │ │ │ └── ZioLoggerBase.java │ │ │ └── impl │ │ │ ├── StaticLoggerBinder.java │ │ │ └── StaticMDCBinder.java │ └── scala │ │ ├── org │ │ └── slf4j │ │ │ └── impl │ │ │ ├── LoggerRuntime.scala │ │ │ ├── StaticMarkerBinder.scala │ │ │ ├── ZioLogger.scala │ │ │ └── ZioLoggerFactory.scala │ │ └── zio │ │ └── logging │ │ └── slf4j │ │ └── bridge │ │ ├── LoggerData.scala │ │ ├── Slf4jBridge.scala │ │ └── ZioLoggerRuntime.scala │ └── test │ └── scala │ └── zio │ └── logging │ └── slf4j │ └── bridge │ ├── Slf4jBridgeExampleApp.scala │ └── Slf4jBridgeSpec.scala ├── slf4j └── src │ ├── main │ └── scala │ │ └── zio │ │ └── logging │ │ └── slf4j │ │ └── SLF4J.scala │ └── test │ ├── resources │ └── logback-test.xml │ └── scala │ └── zio │ └── logging │ └── backend │ ├── SLF4JSpec.scala │ └── TestAppender.scala ├── slf4j2-bridge └── src │ ├── main │ ├── java │ │ ├── module-info.java │ │ └── zio │ │ │ └── logging │ │ │ └── slf4j │ │ │ └── bridge │ │ │ └── ZioSLF4JServiceProvider.java │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.slf4j.spi.SLF4JServiceProvider │ └── scala │ │ └── zio │ │ └── logging │ │ └── slf4j │ │ └── bridge │ │ ├── LoggerData.scala │ │ ├── LoggerRuntime.scala │ │ ├── Slf4jBridge.scala │ │ ├── ZioLogger.scala │ │ ├── ZioLoggerFactory.scala │ │ └── ZioLoggerRuntime.scala │ └── test │ └── scala │ └── zio │ └── logging │ └── slf4j │ └── bridge │ ├── Slf4jBridgeExampleApp.scala │ └── Slf4jBridgeSpec.scala └── slf4j2 └── src ├── main └── scala │ └── zio │ └── logging │ └── slf4j │ └── SLF4J.scala └── test ├── resources └── logback-test.xml └── scala └── zio └── logging └── backend ├── SLF4JSpec.scala └── TestAppender.scala /.gitattributes: -------------------------------------------------------------------------------- 1 | sbt linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zio/zio-logging 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Thanks for considering contributing to Zio-logging! 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and 6 | effective for everyone involved. 7 | 8 | ## Bug reports 9 | 10 | Well-written, thorough bug reports are a great way to contribute to the project. 11 | 12 | Before raising a new issue, please check [our issues list][link-issues] to determine whether the 13 | issue you encountered has already been reported. 14 | 15 | Note that a good bug report should be self-explanatory, therefore it is very important to be as 16 | detailed as possible. The following questions might serve as a template for writing such reports: 17 | 18 | * What were you trying to achieve? 19 | * What are the expected results? 20 | * What are the received results? 21 | * What are the steps to reproduce the issue? 22 | * In what environment did you encounter the issue? 23 | 24 | ## Feature requests 25 | 26 | Feature requests are welcome. Please take your time to document the feature as much as possible - it 27 | is up to you to convince the project's maintainers of the merits of this feature, and its alignment 28 | with the scope and goals of the project. 29 | 30 | ## Pull requests 31 | 32 | Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should 33 | remain focused in scope and avoid containing unrelated commits. 34 | 35 | Please ask first before embarking on any significant pull request (e.g. implementing features or 36 | refactoring code), otherwise you risk spending a lot of time working on something that the project's 37 | maintainers might not want to merge into the project. 38 | 39 | To start contributing, fork the project, clone your fork, and configure the remotes: 40 | 41 | ```bash 42 | git clone https://github.com//zio-logging.git 43 | cd zio-logging 44 | git remote add upstream https://github.com/zio/zio-logging.git 45 | ``` 46 | 47 | If you cloned a while ago, make sure to update your branch with the changes from upstream: 48 | 49 | ```bash 50 | git pull --rebase upstream master 51 | ``` 52 | 53 | Create a new topic branch off the **master** and push it to your fork: 54 | 55 | ```bash 56 | git checkout -b 57 | git push origin 58 | ``` 59 | 60 | Before submitting a pull request, make sure the following requirements are met: 61 | 62 | * Changes are committed in small, coherent and compilable increments. 63 | * Every commit has a [Good Commit Message™][link-otp]. 64 | * Existing tests don't fail. 65 | 66 | Once ready, [open a pull request][link-pr] with a clear title and description. 67 | 68 | [link-issues]: https://github.com/zio/zio-logging/issues 69 | [link-otp]: https://github.com/erlang/otp/wiki/Writing-good-commit-messages 70 | [link-pr]: https://help.github.com/articles/about-pull-requests/ 71 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | # What's Changed 5 | $CHANGES 6 | categories: 7 | - title: 'Breaking' 8 | label: 'type: breaking' 9 | - title: 'New' 10 | label: 'type: feature' 11 | - title: 'Bug Fixes' 12 | label: 'type: bug' 13 | - title: 'Maintenance' 14 | label: 'type: maintenance' 15 | - title: 'Documentation' 16 | label: 'type: docs' 17 | - title: 'Dependency Updates' 18 | label: 'type: dependencies' 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'type: breaking' 24 | minor: 25 | labels: 26 | - 'type: feature' 27 | patch: 28 | labels: 29 | - 'type: bug' 30 | - 'type: maintenance' 31 | - 'type: docs' 32 | - 'type: dependencies' 33 | - 'type: security' 34 | 35 | exclude-labels: 36 | - 'skip-changelog' 37 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "automerge": true, 3 | "rebaseWhen": "conflicted", 4 | "labels": ["type: dependencies"], 5 | "packageRules": [ 6 | { 7 | "matchManagers": [ 8 | "sbt" 9 | ], 10 | "enabled": false 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve 2 | 3 | on: 4 | pull_request_target 5 | 6 | jobs: 7 | auto-approve: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: hmarr/auto-approve-action@v3.2.1 11 | if: github.actor == 'scala-steward' || github.actor == 'renovate[bot]' 12 | with: 13 | github-token: "${{ secrets.GITHUB_TOKEN }}" 14 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: release-drafter/release-drafter@v5 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | project/zecret 2 | project/travis-deploy-key 3 | project/secrets.tar.xz 4 | target 5 | test-output/ 6 | .sbtopts 7 | .bsp 8 | project/.sbt 9 | test-output/ 10 | .bloop 11 | .metals 12 | metals.sbt 13 | */metals.sbt 14 | .idea 15 | coursier 16 | .DS_Store 17 | metals.sbt 18 | project/metals.sbt 19 | project/project/metals.sbt 20 | sbt.json 21 | .bsp/ 22 | project/project/ 23 | *.iml 24 | 25 | # if you are here to add your IDE's files please read this instead: 26 | # https://stackoverflow.com/questions/7335420/global-git-ignore#22885996 27 | website/node_modules 28 | website/.docusaurus 29 | website/build 30 | website/docs 31 | website/static/api* 32 | website/versioned_docs 33 | website/i18n/en.json 34 | website/yarn.lock 35 | website/package-lock.json 36 | website/static/api 37 | .bsp/ 38 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -XX:+PrintCommandLineFlags 2 | -Xss2m 3 | -Xmx6g 4 | -XX:+UseG1GC 5 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: assign and label scala-steward's PRs 3 | conditions: 4 | - author=scala-steward 5 | actions: 6 | assign: 7 | users: ["@zio/zio-logging"] 8 | label: 9 | add: ["type: dependencies"] 10 | - name: label scala-steward's breaking PRs 11 | conditions: 12 | - author=scala-steward 13 | - "body~=(labels: library-update, semver-major)|(labels: sbt-plugin-update, semver-major)" 14 | actions: 15 | label: 16 | add: ["type: breaking"] 17 | - name: merge Scala Steward's PRs 18 | conditions: 19 | - base=master 20 | - author=scala-steward 21 | - "body~=(labels: library-update, semver-minor)|(labels: library-update, semver-patch)|(labels: sbt-plugin-update, semver-minor)|(labels: sbt-plugin-update, semver-patch)|(labels: scalafix-rule-update)|(labels: test-library-update)" 22 | - "status-success=license/cla" 23 | - "status-success=ci" 24 | actions: 25 | merge: 26 | method: squash 27 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | Disable 3 | DisableSyntax 4 | ExplicitResultTypes 5 | LeakingImplicitClassVal 6 | NoAutoTupling 7 | NoValInForComprehension 8 | OrganizeImports 9 | ProcedureSyntax 10 | RemoveUnused 11 | ] 12 | 13 | Disable { 14 | ifSynthetic = [ 15 | "scala/Option.option2Iterable" 16 | "scala/Predef.any2stringadd" 17 | ] 18 | } 19 | 20 | OrganizeImports { 21 | # Align with IntelliJ IDEA so that they don't fight each other 22 | groupedImports = Merge 23 | } 24 | 25 | RemoveUnused { 26 | imports = false // handled by OrganizeImports 27 | } 28 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.7" 2 | runner.dialect = scala212 3 | maxColumn = 120 4 | align.preset = most 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings.style = Asterisk 8 | docstrings.wrap = "no" 9 | lineEndings = preserve 10 | includeCurlyBraceInSelectChains = false 11 | spaces { 12 | inImportCurlyBraces = true 13 | } 14 | optIn.annotationNewlines = true 15 | 16 | rewrite.rules = [RedundantBraces] 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /core/js/src/main/scala/zio/logging/LoggingPackagePlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | private[logging] trait LoggingPackagePlatformSpecific 19 | -------------------------------------------------------------------------------- /core/jvm-native/src/main/scala/zio/logging/LoggingPackagePlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | private[logging] trait LoggingPackagePlatformSpecific extends FileLoggerLayers 19 | -------------------------------------------------------------------------------- /core/jvm-native/src/main/scala/zio/logging/internal/FileWriter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.internal 17 | 18 | import zio.logging.FileLoggerConfig 19 | import zio.logging.FileLoggerConfig.FileRollingPolicy 20 | 21 | import java.io.Writer 22 | import java.nio.charset.Charset 23 | import java.nio.file.Path 24 | 25 | private[logging] class FileWriter( 26 | destination: Path, 27 | charset: Charset, 28 | autoFlushBatchSize: Int, 29 | bufferedIOSize: Option[Int], 30 | rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] 31 | ) extends Writer { 32 | private val writerProvider: WriterProvider = rollingPolicy match { 33 | case Some(policy) => 34 | policy match { 35 | case FileRollingPolicy.TimeBasedRollingPolicy => 36 | WriterProvider.TimeBasedRollingWriterProvider(destination, charset, bufferedIOSize) 37 | } 38 | case None => WriterProvider.SimpleWriterProvider(destination, charset, bufferedIOSize) 39 | } 40 | 41 | private var entriesWritten: Long = 0 42 | 43 | final def write(buffer: Array[Char], offset: Int, length: Int): Unit = 44 | writerProvider.writer.write(buffer, offset, length) 45 | 46 | final def writeln(line: String): Unit = { 47 | val writer = writerProvider.writer 48 | writer.write(line) 49 | writer.write(System.lineSeparator) 50 | 51 | entriesWritten += 1 52 | 53 | if (entriesWritten % autoFlushBatchSize == 0) 54 | writer.flush() 55 | } 56 | 57 | final def flush(): Unit = writerProvider.writer.flush() 58 | 59 | final def close(): Unit = writerProvider.writer.close() 60 | 61 | } 62 | -------------------------------------------------------------------------------- /core/jvm-native/src/main/scala/zio/logging/internal/WriterProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.internal 17 | 18 | import java.io.{ BufferedWriter, FileOutputStream, OutputStreamWriter, Writer } 19 | import java.nio.charset.Charset 20 | import java.nio.file.{ FileSystems, Path } 21 | import java.time.LocalDateTime 22 | import java.time.format.DateTimeFormatter 23 | import java.time.temporal.ChronoUnit 24 | 25 | private[logging] sealed trait WriterProvider { 26 | def writer: Writer 27 | } 28 | 29 | private[logging] object WriterProvider { 30 | final case class SimpleWriterProvider( 31 | destination: Path, 32 | charset: Charset, 33 | bufferedIOSize: Option[Int] 34 | ) extends WriterProvider { 35 | override val writer: Writer = { 36 | val output = new OutputStreamWriter(new FileOutputStream(destination.toFile, true), charset) 37 | bufferedIOSize match { 38 | case Some(bufferSize) => new BufferedWriter(output, bufferSize) 39 | case None => output 40 | } 41 | } 42 | } 43 | 44 | final case class TimeBasedRollingWriterProvider( 45 | destination: Path, 46 | charset: Charset, 47 | bufferedIOSize: Option[Int], 48 | time: () => LocalDateTime = TimeBasedRollingWriterProvider.makeNewTime 49 | ) extends WriterProvider { 50 | import java.util.concurrent.locks.ReentrantLock 51 | import TimeBasedRollingWriterProvider._ 52 | 53 | private var timeInUse = time() 54 | private var currentWriter: Writer = makeWriter(makePath(destination, timeInUse), charset, bufferedIOSize) 55 | private val lock: ReentrantLock = new ReentrantLock() 56 | 57 | override def writer: Writer = { 58 | val newTime = time() 59 | if (newTime != timeInUse) { 60 | lock.lock() 61 | try 62 | if (newTime != timeInUse) { 63 | currentWriter.close() 64 | currentWriter = makeWriter(makePath(destination, newTime), charset, bufferedIOSize) 65 | timeInUse = newTime 66 | } 67 | finally 68 | lock.unlock() 69 | } 70 | currentWriter 71 | } 72 | } 73 | object TimeBasedRollingWriterProvider { 74 | private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 75 | private def makeNewTime() = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS) 76 | 77 | def makePath(destination: Path, time: LocalDateTime): Path = { 78 | val formattedTime = dateTimeFormatter.format(time) 79 | val fileNameArray = destination.getFileName.toString.split("\\.") 80 | val timeFileName = if (fileNameArray.length >= 2) { 81 | fileNameArray.dropRight(1).mkString(".") + "-" + formattedTime + "." + fileNameArray.last 82 | } else { 83 | fileNameArray.head + "-" + formattedTime 84 | } 85 | val timeDestination = FileSystems.getDefault.getPath(destination.getParent.toString, timeFileName) 86 | timeDestination 87 | } 88 | 89 | private def makeWriter(path: Path, charset: Charset, bufferedIOSize: Option[Int]): Writer = { 90 | val output = new OutputStreamWriter(new FileOutputStream(path.toFile, true), charset) 91 | bufferedIOSize match { 92 | case Some(bufferSize) => new BufferedWriter(output, bufferSize) 93 | case None => output 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/jvm-native/src/test/scala/zio/logging/FileLoggerConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.test._ 4 | import zio.{ Config, ConfigProvider, LogLevel } 5 | 6 | import java.net.URI 7 | import java.nio.charset.StandardCharsets 8 | import java.nio.file.Paths 9 | 10 | object FileLoggerConfigSpec extends ZIOSpecDefault { 11 | 12 | val spec: Spec[Environment, Any] = suite("FileLoggerConfig")( 13 | test("load valid config") { 14 | 15 | val logFormat = 16 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 17 | 18 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 19 | Map( 20 | "logger/format" -> logFormat, 21 | "logger/path" -> "file:///tmp/test.log", 22 | "logger/autoFlushBatchSize" -> "2", 23 | "logger/bufferedIOSize" -> "4096", 24 | "logger/rollingPolicy/type" -> "TimeBasedRollingPolicy", 25 | "logger/filter/rootLevel" -> LogLevel.Info.label, 26 | "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label 27 | ), 28 | "/" 29 | ) 30 | 31 | configProvider.load(FileLoggerConfig.config.nested("logger")).map { loadedConfig => 32 | assertTrue(loadedConfig.charset == StandardCharsets.UTF_8) && 33 | assertTrue(loadedConfig.destination == Paths.get(URI.create("file:///tmp/test.log"))) && 34 | assertTrue(loadedConfig.autoFlushBatchSize == 2) && 35 | assertTrue(loadedConfig.bufferedIOSize == Some(4096)) && 36 | assertTrue(loadedConfig.rollingPolicy == Some(FileLoggerConfig.FileRollingPolicy.TimeBasedRollingPolicy)) 37 | } 38 | }, 39 | test("load default config") { 40 | 41 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 42 | Map( 43 | "logger/path" -> "file:///tmp/test.log" 44 | ), 45 | "/" 46 | ) 47 | 48 | configProvider.load(FileLoggerConfig.config.nested("logger")).map { loadedConfig => 49 | assertTrue(loadedConfig.charset == StandardCharsets.UTF_8) && 50 | assertTrue(loadedConfig.destination == Paths.get(URI.create("file:///tmp/test.log"))) && 51 | assertTrue(loadedConfig.autoFlushBatchSize == 1) && 52 | assertTrue(loadedConfig.bufferedIOSize.isEmpty) 53 | } 54 | }, 55 | test("equals config with same sources") { 56 | 57 | val logFormat = 58 | "%highlight{%timestamp %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 59 | 60 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 61 | Map( 62 | "logger/format" -> logFormat, 63 | "logger/path" -> "file:///tmp/test.log", 64 | "logger/autoFlushBatchSize" -> "2", 65 | "logger/bufferedIOSize" -> "4096", 66 | "logger/rollingPolicy/type" -> "TimeBasedRollingPolicy", 67 | "logger/filter/rootLevel" -> LogLevel.Info.label, 68 | "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label 69 | ), 70 | "/" 71 | ) 72 | 73 | import zio.prelude._ 74 | for { 75 | c1 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) 76 | c2 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) 77 | } yield assertTrue( 78 | c1.format == c2.format, 79 | c1 === c2 80 | ) 81 | }, 82 | test("fail on invalid charset and filter config") { 83 | val logFormat = 84 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 85 | 86 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 87 | Map( 88 | "logger/format" -> logFormat, 89 | "logger/charset" -> "INVALID_CHARSET", 90 | "logger/filter/rootLevel" -> "INVALID_LOG_LEVEL" 91 | ), 92 | "/" 93 | ) 94 | 95 | configProvider 96 | .load(FileLoggerConfig.config.nested("logger")) 97 | .exit 98 | .map { e => 99 | assert(e)(Assertion.failsWithA[Config.Error]) 100 | } 101 | }, 102 | test("fail on invalid format config") { 103 | val logFormat = 104 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{%level} [%fiberId] %name:%line %message %cause}" 105 | 106 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 107 | Map( 108 | "logger/format" -> logFormat, 109 | "logger/path" -> "file:///tmp/test.log" 110 | ), 111 | "/" 112 | ) 113 | 114 | configProvider 115 | .load(FileLoggerConfig.config.nested("logger")) 116 | .exit 117 | .map { e => 118 | assert(e)(Assertion.failsWithA[Config.Error]) 119 | } 120 | } 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /core/jvm-native/src/test/scala/zio/logging/internal/WriterProviderSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.internal 2 | 3 | import zio.ZIO 4 | import zio.test._ 5 | 6 | import java.nio.charset.StandardCharsets 7 | import java.nio.file.FileSystems 8 | import java.time.{ LocalDate, LocalDateTime } 9 | import java.util.concurrent.atomic.AtomicReference 10 | 11 | object WriterProviderSpec extends ZIOSpecDefault { 12 | 13 | val spec: Spec[Environment, Any] = suite("WriterProvider")( 14 | test("Make path include date") { 15 | import WriterProvider.TimeBasedRollingWriterProvider.makePath 16 | 17 | val localDateTime = LocalDate.of(2023, 3, 21).atStartOfDay() 18 | val destination1 = FileSystems.getDefault.getPath("/tmp/file_app") 19 | val destination2 = FileSystems.getDefault.getPath("/tmp/file_app.log") 20 | val destination3 = FileSystems.getDefault.getPath("/tmp/file.app.log") 21 | val destination4 = FileSystems.getDefault.getPath("/tmp/file.app.out.log") 22 | 23 | assertTrue( 24 | (makePath(destination1, localDateTime).toString == "/tmp/file_app-2023-03-21") && 25 | (makePath(destination2, localDateTime).toString == "/tmp/file_app-2023-03-21.log") && 26 | (makePath(destination3, localDateTime).toString == "/tmp/file.app-2023-03-21.log") && 27 | (makePath(destination4, localDateTime).toString == "/tmp/file.app.out-2023-03-21.log") 28 | ) 29 | }, 30 | test("Called multiple times with same time, if it return same writer") { 31 | val timeRef = new AtomicReference[LocalDateTime](LocalDateTime.now()) 32 | 33 | val testMakeNewTime: () => LocalDateTime = () => timeRef.get() 34 | 35 | val writerProvider = WriterProvider.TimeBasedRollingWriterProvider( 36 | destination = FileSystems.getDefault.getPath("/tmp/file_app"), 37 | charset = StandardCharsets.UTF_8, 38 | bufferedIOSize = Some(1), 39 | time = testMakeNewTime 40 | ) 41 | 42 | val parallelExecution = ZIO 43 | .foreachPar(1 to 5)(_ => ZIO.succeed(writerProvider.writer)) 44 | 45 | for { 46 | sameWriter1 <- parallelExecution.map(_.toSet) 47 | _ = timeRef.set(LocalDateTime.now().plusDays(1)) 48 | sameWriter2 <- parallelExecution.map(_.toSet) 49 | } yield assertTrue(sameWriter1.size == 1 && sameWriter2.size == 1 && sameWriter1 != sameWriter2) 50 | } 51 | ) @@ TestAspect.unix 52 | } 53 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/ConsoleLoggerConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio.prelude._ 19 | import zio.{ Config, NonEmptyChunk, ZIO, ZLayer } 20 | 21 | final case class ConsoleLoggerConfig( 22 | format: LogFormat, 23 | filter: LogFilter.LogLevelByNameConfig 24 | ) { 25 | def toFilter[M]: LogFilter[M] = filter.toFilter 26 | } 27 | 28 | object ConsoleLoggerConfig { 29 | 30 | val default: ConsoleLoggerConfig = ConsoleLoggerConfig(LogFormat.default, LogFilter.LogLevelByNameConfig.default) 31 | 32 | val config: Config[ConsoleLoggerConfig] = { 33 | val formatConfig = LogFormat.config.nested("format").withDefault(LogFormat.default) 34 | val filterConfig = LogFilter.LogLevelByNameConfig.config.nested("filter") 35 | (formatConfig ++ filterConfig).map { case (format, filter) => 36 | ConsoleLoggerConfig( 37 | format, 38 | filter 39 | ) 40 | } 41 | } 42 | 43 | implicit val equal: Equal[ConsoleLoggerConfig] = Equal.make { (l, r) => 44 | l.format == r.format && l.filter === r.filter 45 | } 46 | 47 | def load(configPath: NonEmptyChunk[String] = loggerConfigPath): ZIO[Any, Config.Error, ConsoleLoggerConfig] = 48 | ZIO.config(ConsoleLoggerConfig.config.nested(configPath.head, configPath.tail: _*)) 49 | 50 | def make( 51 | configPath: NonEmptyChunk[String] = loggerConfigPath 52 | ): ZLayer[Any, Config.Error, ConsoleLoggerConfig] = 53 | ZLayer.fromZIO(load(configPath)) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/FileLoggerConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio._ 19 | import zio.prelude._ 20 | 21 | import java.net.URI 22 | import java.nio.charset.{ Charset, StandardCharsets } 23 | import java.nio.file.{ Path, Paths } 24 | import scala.util.{ Failure, Success, Try } 25 | 26 | final case class FileLoggerConfig( 27 | destination: Path, 28 | format: LogFormat, 29 | filter: LogFilter.LogLevelByNameConfig, 30 | charset: Charset = StandardCharsets.UTF_8, 31 | autoFlushBatchSize: Int = 1, 32 | bufferedIOSize: Option[Int] = None, 33 | rollingPolicy: Option[FileLoggerConfig.FileRollingPolicy] = None 34 | ) { 35 | def toFilter[M]: LogFilter[M] = filter.toFilter 36 | } 37 | 38 | object FileLoggerConfig { 39 | 40 | sealed trait FileRollingPolicy 41 | 42 | object FileRollingPolicy { 43 | 44 | final case object TimeBasedRollingPolicy extends FileRollingPolicy 45 | 46 | val config: Config[FileRollingPolicy] = { 47 | val tpe = Config.string("type") 48 | val timeBased = Config.succeed(TimeBasedRollingPolicy) 49 | 50 | tpe.switch("TimeBasedRollingPolicy" -> timeBased) 51 | } 52 | } 53 | 54 | private def charsetValue(value: String): Either[Config.Error.InvalidData, Charset] = 55 | Try(Charset.forName(value)) match { 56 | case Success(v) => Right(v) 57 | case Failure(_) => 58 | Left(Config.Error.InvalidData(Chunk.empty, s"Expected a Charset, but found ${value}")) 59 | } 60 | 61 | private def pathValue(value: String): Either[Config.Error.InvalidData, Path] = 62 | Try(Paths.get(URI.create(value))) match { 63 | case Success(v) => Right(v) 64 | case Failure(_) => 65 | Left(Config.Error.InvalidData(Chunk.empty, s"Expected a Path, but found ${value}")) 66 | } 67 | 68 | val config: Config[FileLoggerConfig] = { 69 | val autoFlushBatchSizeConfig = Config.int.nested("autoFlushBatchSize").withDefault(1) 70 | val bufferedIOSizeConfig = Config.int.nested("bufferedIOSize").optional 71 | val filterConfig = LogFilter.LogLevelByNameConfig.config.nested("filter") 72 | val charsetConfig = 73 | Config.string.mapOrFail(charsetValue).nested("charset").withDefault(StandardCharsets.UTF_8) 74 | val pathConfig = Config.string.mapOrFail(pathValue).nested("path") 75 | val formatConfig = LogFormat.config.nested("format").withDefault(LogFormat.default) 76 | val rollingPolicyConfig = FileRollingPolicy.config.nested("rollingPolicy").optional 77 | 78 | (pathConfig ++ formatConfig ++ filterConfig ++ charsetConfig ++ autoFlushBatchSizeConfig ++ bufferedIOSizeConfig ++ rollingPolicyConfig).map { 79 | case (path, format, filter, charset, autoFlushBatchSize, bufferedIOSize, rollingPolicy) => 80 | FileLoggerConfig( 81 | path, 82 | format, 83 | filter, 84 | charset, 85 | autoFlushBatchSize, 86 | bufferedIOSize, 87 | rollingPolicy 88 | ) 89 | } 90 | } 91 | 92 | implicit val equal: Equal[FileLoggerConfig] = Equal.make { (l, r) => 93 | l.destination == r.destination && 94 | l.charset == r.charset && 95 | l.autoFlushBatchSize == r.autoFlushBatchSize && 96 | l.bufferedIOSize == r.bufferedIOSize && 97 | l.rollingPolicy == r.rollingPolicy && 98 | l.format == r.format && 99 | l.filter === r.filter 100 | } 101 | 102 | def load(configPath: NonEmptyChunk[String] = loggerConfigPath): ZIO[Any, Config.Error, FileLoggerConfig] = 103 | ZIO.config(FileLoggerConfig.config.nested(configPath.head, configPath.tail: _*)) 104 | 105 | def make( 106 | configPath: NonEmptyChunk[String] = loggerConfigPath 107 | ): ZLayer[Any, Config.Error, FileLoggerConfig] = 108 | ZLayer.fromZIO(load(configPath)) 109 | 110 | } 111 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/FilteredLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio.{ Cause, FiberId, FiberRefs, LogLevel, LogSpan, Trace, ZLogger } 19 | 20 | final case class FilteredLogger[Message, Output](logger: zio.ZLogger[Message, Output], filter: LogFilter[Message]) 21 | extends ZLogger[Message, Option[Output]] { 22 | 23 | override def apply( 24 | trace: Trace, 25 | fiberId: FiberId, 26 | logLevel: LogLevel, 27 | message: () => Message, 28 | cause: Cause[Any], 29 | context: FiberRefs, 30 | spans: List[LogSpan], 31 | annotations: Map[String, String] 32 | ): Option[Output] = 33 | if (filter(trace, fiberId, logLevel, message, cause, context, spans, annotations)) { 34 | Some(logger(trace, fiberId, logLevel, message, cause, context, spans, annotations)) 35 | } else None 36 | 37 | def withFilter(newFilter: LogFilter[Message]): FilteredLogger[Message, Output] = FilteredLogger(logger, newFilter) 38 | } 39 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/LogAnnotation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio._ 19 | 20 | import java.{ util => ju } 21 | 22 | /** 23 | * A `LogAnnotation` describes a particular type of statically-typed log 24 | * annotation applied to log lines. Log annotations combine in user-defined 25 | * ways, which means they can have arbitrary structure. In the end, however, 26 | * it must be possible to render each log annotation as a string. 27 | * {{{ 28 | * myEffect @@ UserId("jdoe") 29 | * }}} 30 | */ 31 | final case class LogAnnotation[A: Tag](name: String, combine: (A, A) => A, render: A => String) { 32 | self => 33 | type Id 34 | type Type = A 35 | 36 | def apply(value: A): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = 37 | LogAnnotation.LogAnnotationAspect(Map(self -> value)) 38 | 39 | def id: Id = (name, tag).asInstanceOf[Id] 40 | 41 | /** 42 | * The class tag of the annotation type, used for disambiguation purposes only. 43 | */ 44 | def tag: Tag[A] = implicitly[Tag[A]] 45 | 46 | override def hashCode: Int = id.hashCode 47 | 48 | override def equals(that: Any): Boolean = 49 | that match { 50 | case that: LogAnnotation[_] => self.id == that.id 51 | case _ => false 52 | } 53 | 54 | override def toString: String = s"LogAnnotation($name, $tag)" 55 | } 56 | 57 | object LogAnnotation { 58 | 59 | private[logging] final case class LogAnnotationAspect(annotations: Map[LogAnnotation[_], Any]) 60 | extends ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] { 61 | 62 | override def >>>[LowerR, UpperR, LowerE, UpperE, LowerA, UpperA]( 63 | that: ZIOAspect[LowerR, UpperR, LowerE, UpperE, LowerA, UpperA] 64 | ): ZIOAspect[LowerR, UpperR, LowerE, UpperE, LowerA, UpperA] = 65 | that match { 66 | case LogAnnotationAspect(thatAnnotations) => LogAnnotationAspect(annotations ++ thatAnnotations) 67 | case that => super.andThen(that) 68 | } 69 | 70 | final def apply[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] = 71 | logContext.getWith { context => 72 | logContext.locally { 73 | annotations.foldLeft(context) { case (context, (annotation, value)) => 74 | context.annotate(annotation.asInstanceOf[LogAnnotation[Any]], value) 75 | } 76 | }(zio) 77 | } 78 | } 79 | 80 | /** 81 | * The `TraceId` annotation keeps track of distributed trace id. 82 | */ 83 | val TraceId: LogAnnotation[ju.UUID] = LogAnnotation[java.util.UUID]( 84 | name = "trace_id", 85 | combine = (_: ju.UUID, r: ju.UUID) => r, 86 | render = _.toString 87 | ) 88 | 89 | /** 90 | * The `TraceSpans` annotation keeps track of distributed spans. 91 | */ 92 | val TraceSpans: LogAnnotation[List[String]] = LogAnnotation[List[String]]( 93 | name = "trace_spans", 94 | combine = (l, r) => l ++ r, 95 | render = _.mkString(":") 96 | ) 97 | 98 | /** 99 | * The `UserId` annotation keeps track of user id. 100 | */ 101 | val UserId: LogAnnotation[String] = LogAnnotation[String]( 102 | name = "user_id", 103 | combine = (_: String, r: String) => r, 104 | render = _.toString 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/LogColor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio.{ Chunk, Config } 19 | 20 | import scala.io.AnsiColor 21 | 22 | final case class LogColor private (private[logging] val ansi: String) extends AnyVal 23 | 24 | object LogColor { 25 | val RED: LogColor = LogColor(AnsiColor.RED) 26 | val BLUE: LogColor = LogColor(AnsiColor.BLUE) 27 | val YELLOW: LogColor = LogColor(AnsiColor.YELLOW) 28 | val CYAN: LogColor = LogColor(AnsiColor.CYAN) 29 | val GREEN: LogColor = LogColor(AnsiColor.GREEN) 30 | val MAGENTA: LogColor = LogColor(AnsiColor.MAGENTA) 31 | val WHITE: LogColor = LogColor(AnsiColor.WHITE) 32 | val RESET: LogColor = LogColor(AnsiColor.RESET) 33 | 34 | private[logging] val logColorMapping: Map[String, LogColor] = Map( 35 | "RED" -> LogColor.RED, 36 | "BLUE" -> LogColor.BLUE, 37 | "YELLOW" -> LogColor.YELLOW, 38 | "CYAN" -> LogColor.CYAN, 39 | "GREEN" -> LogColor.GREEN, 40 | "MAGENTA" -> LogColor.MAGENTA, 41 | "WHITE" -> LogColor.WHITE, 42 | "RESET" -> LogColor.RESET 43 | ) 44 | 45 | private[logging] def logColorValue(value: String): Either[Config.Error.InvalidData, LogColor] = 46 | logColorMapping.get(value.toUpperCase) match { 47 | case Some(v) => Right(v) 48 | case None => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a LogColor, but found ${value}")) 49 | } 50 | 51 | val config: Config[LogColor] = Config.string.mapOrFail(logColorValue) 52 | } 53 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/LogContext.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | /** 19 | * A `LogContext` stores context associated with logging operations. 20 | */ 21 | final case class LogContext private (private val map: Map[LogAnnotation[_], Any]) { self => 22 | 23 | /** 24 | * Merges this context with the specified context. 25 | */ 26 | def ++(that: LogContext): LogContext = self merge that 27 | 28 | /** 29 | * Annotates the context with the specified annotation and value, returning 30 | * the new context. 31 | */ 32 | def annotate[A](annotation: LogAnnotation[A], newA: A): LogContext = 33 | get(annotation) match { 34 | case None => new LogContext(map + (annotation -> newA)) 35 | case Some(oldA) => new LogContext(map + (annotation -> annotation.combine(oldA, newA))) 36 | } 37 | 38 | /** 39 | * Renders value for given annotation 40 | */ 41 | def apply[A](logAnnotation: LogAnnotation[A]): Option[String] = 42 | get(logAnnotation).map(logAnnotation.render(_)) 43 | 44 | /** 45 | * Retrieves the specified annotation from the context. 46 | */ 47 | def get[A](annotation: LogAnnotation[A]): Option[A] = 48 | map.get(annotation).map(_.asInstanceOf[A]) 49 | 50 | /** 51 | * Retrieves the specified annotation by name from the context, return rendered value. 52 | */ 53 | def get(name: String): Option[String] = 54 | map.collectFirst { 55 | case (annotation: LogAnnotation[Any], value: Any) if (annotation.name == name) => 56 | annotation.render(value) 57 | } 58 | 59 | /** 60 | * Merges this context with the specified context. 61 | */ 62 | def merge(that: LogContext): LogContext = { 63 | val allKeys = self.map.keySet ++ that.map.keySet 64 | 65 | new LogContext( 66 | allKeys.foldLeft(Map.empty[LogAnnotation[_], Any]) { case (map, annotation) => 67 | map + 68 | (annotation -> ((self.map.get(annotation), that.map.get(annotation)) match { 69 | case (Some(l), Some(r)) => 70 | annotation.combine(l.asInstanceOf[annotation.Type], r.asInstanceOf[annotation.Type]) 71 | case (None, Some(r)) => r 72 | case (Some(l), None) => l 73 | case (None, None) => throw new IllegalStateException("Impossible") 74 | })) 75 | } 76 | ) 77 | } 78 | 79 | /** 80 | * Renders all log annotations in the context. 81 | */ 82 | def asMap: Map[String, String] = 83 | map.asInstanceOf[Map[LogAnnotation[Any], Any]].map { case (annotation, value) => 84 | annotation.name -> annotation.render(value) 85 | } 86 | } 87 | 88 | object LogContext { 89 | 90 | /** 91 | * An empty context. 92 | */ 93 | val empty: LogContext = new LogContext(Map()) 94 | } 95 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/LoggerNameExtractor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio.{ FiberRefs, Trace } 19 | 20 | sealed trait LoggerNameExtractor { self => 21 | 22 | def apply( 23 | trace: Trace, 24 | context: FiberRefs, 25 | annotations: Map[String, String] 26 | ): Option[String] 27 | 28 | /** 29 | * Returns a new extractor which return logger name from this one or other if this is empty 30 | */ 31 | final def ||(other: LoggerNameExtractor): LoggerNameExtractor = 32 | self.or(other) 33 | 34 | /** 35 | * The alphanumeric version of the `||` operator. 36 | */ 37 | final def or(other: LoggerNameExtractor): LoggerNameExtractor = 38 | LoggerNameExtractor.OrExtractor(self, other) 39 | 40 | /** 41 | * Converts this extractor into a log format 42 | */ 43 | final def toLogFormat(default: String = "zio-logger"): LogFormat = LogFormat.loggerName(self, default) 44 | 45 | /** 46 | * Converts this extractor into a log group 47 | */ 48 | final def toLogGroup(default: String = "zio-logger"): LogGroup[Any, String] = 49 | LogGroup.fromLoggerNameExtractor(self, default) 50 | 51 | } 52 | 53 | object LoggerNameExtractor { 54 | 55 | private[logging] final case class FnExtractor(fn: (Trace, FiberRefs, Map[String, String]) => Option[String]) 56 | extends LoggerNameExtractor { 57 | override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = 58 | fn(trace, context, annotations) 59 | } 60 | 61 | private[logging] final case class AnnotationExtractor(name: String) extends LoggerNameExtractor { 62 | override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = 63 | annotations.get(name) 64 | } 65 | 66 | private[logging] final case class OrExtractor(first: LoggerNameExtractor, second: LoggerNameExtractor) 67 | extends LoggerNameExtractor { 68 | 69 | override def apply(trace: Trace, context: FiberRefs, annotations: Map[String, String]): Option[String] = 70 | first(trace, context, annotations).orElse(second(trace, context, annotations)) 71 | } 72 | 73 | def make(fn: (Trace, FiberRefs, Map[String, String]) => Option[String]): LoggerNameExtractor = FnExtractor(fn) 74 | 75 | /** 76 | * Extractor which take logger name from [[Trace]] 77 | * 78 | * trace with value ''example.LivePingService.ping(PingService.scala:22)'' 79 | * will have ''example.LivePingService'' as logger name 80 | */ 81 | val trace: LoggerNameExtractor = FnExtractor { (trace, _, _) => 82 | val parsed = Trace.parseOrNull(trace) 83 | if (parsed ne null) { 84 | val location = parsed.location 85 | val last = location.lastIndexOf(".") 86 | val name = if (last > 0) { 87 | location.substring(0, last) 88 | } else location 89 | Some(name) 90 | } else { 91 | None 92 | } 93 | } 94 | 95 | /** 96 | * Extractor which take logger name from annotation 97 | * 98 | * @param name name of annotation 99 | */ 100 | def annotation(name: String): LoggerNameExtractor = AnnotationExtractor(name) 101 | 102 | /** 103 | * Extractor which take logger name from annotation or [[Trace]] if specified annotation is not present 104 | * 105 | * @param name name of annotation 106 | */ 107 | def annotationOrTrace(name: String): LoggerNameExtractor = 108 | annotation(name) || LoggerNameExtractor.trace 109 | 110 | /** 111 | * Extractor which take logger name from annotation with key [[zio.logging.loggerNameAnnotationKey]] or [[Trace]] if specified annotation is not present 112 | */ 113 | val loggerNameAnnotationOrTrace: LoggerNameExtractor = 114 | annotationOrTrace(loggerNameAnnotationKey) 115 | 116 | } 117 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/LoggingPackageAllPlatforms.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio 17 | package logging 18 | 19 | private[logging] trait LoggingPackageAllPlatforms extends LoggerLayers { 20 | 21 | /** 22 | * The [[logContext]] fiber reference is used to store typed, structured log 23 | * annotations, which can be utilized by backends to enrich log messages. 24 | * 25 | * Because [[logContext]] is an ordinary [[zio.FiberRef]], it may be get, set, 26 | * and updated like any other fiber reference. However, the idiomatic way to 27 | * interact with [[logContext]] is by using [[zio.logging.LogAnnotation]]. 28 | * 29 | * For example: 30 | * 31 | * {{{ 32 | * myResponseHandler(request) @@ UserId(request.userId) 33 | * }}} 34 | * 35 | * This code would add the structured log annotation [[LogAnnotation.UserId]] 36 | * to all log messages emitted by the `myResponseHandler(request)` effect. 37 | */ 38 | val logContext: FiberRef[LogContext] = 39 | zio.Unsafe.unsafe { implicit u => 40 | FiberRef.unsafe.make(LogContext.empty, ZIO.identityFn[LogContext], (old, newV) => old ++ newV) 41 | } 42 | 43 | /** 44 | * log aspect annotation key for logger name 45 | */ 46 | val loggerNameAnnotationKey = "logger_name" 47 | 48 | val loggerConfigPath: NonEmptyChunk[String] = NonEmptyChunk("logger") 49 | 50 | /** 51 | * Logger name aspect, by this aspect is possible to set logger name (in general, logger name is extracted from [[Trace]]) 52 | * 53 | * annotation key: [[zio.logging.loggerNameAnnotationKey]] 54 | */ 55 | def loggerName(value: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = 56 | ZIOAspect.annotated(loggerNameAnnotationKey, value) 57 | 58 | /** 59 | * Logger name aspect, by this aspect is possible to set logger name (in general, logger name is extracted from [[Trace]]) 60 | * 61 | * annotation key: [[zio.logging.loggerNameAnnotationKey]] 62 | */ 63 | def loggerName(fn: Option[String] => String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = 64 | new ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] { 65 | def apply[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] = 66 | for { 67 | annotations <- ZIO.logAnnotations 68 | currentLoggerName = annotations.get(loggerNameAnnotationKey) 69 | newLoggerName = fn(currentLoggerName) 70 | a <- ZIO.logAnnotate(loggerNameAnnotationKey, newLoggerName)(zio) 71 | } yield a 72 | } 73 | 74 | /** 75 | * Append logger name aspect, by which it is possible to append a logger name to the current logger name. 76 | * The new name is appended using a dot (.) as the delimiter. 77 | * 78 | * annotation key: [[zio.logging.loggerNameAnnotationKey]] 79 | */ 80 | def appendLoggerName(value: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] = 81 | loggerName(currentLoggerName => currentLoggerName.fold(value)(currentLoggerName => s"$currentLoggerName.$value")) 82 | 83 | implicit final class LogAnnotationZIOSyntax[R, E, A](private val self: ZIO[R, E, A]) { 84 | def logAnnotate[V: Tag](key: LogAnnotation[V], value: V): ZIO[R, E, A] = 85 | self @@ key(value) 86 | } 87 | 88 | implicit final class ZLoggerOps[-Message, +Output](private val self: ZLogger[Message, Output]) { 89 | 90 | /** 91 | * Returns a version of logger that only logs messages when this filter is satisfied 92 | */ 93 | def filter[M <: Message](filter: LogFilter[M]): ZLogger[M, Option[Output]] = FilteredLogger(self, filter) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/MetricLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio.metrics.{ Metric, MetricLabel } 19 | import zio.{ Cause, FiberId, FiberRef, FiberRefs, LogLevel, LogSpan, Trace, Unsafe, ZLogger } 20 | 21 | final case class MetricLogger(counter: Metric.Counter[Long], logLevelLabel: String) extends ZLogger[String, Unit] { 22 | 23 | override def apply( 24 | trace: Trace, 25 | fiberId: FiberId, 26 | logLevel: LogLevel, 27 | message: () => String, 28 | cause: Cause[Any], 29 | context: FiberRefs, 30 | spans: List[LogSpan], 31 | annotations: Map[String, String] 32 | ): Unit = { 33 | val tags = context.get(FiberRef.currentTags).getOrElse(Set.empty) 34 | counter.unsafe.update(1, tags + MetricLabel(logLevelLabel, logLevel.label.toLowerCase))(Unsafe.unsafe) 35 | () 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/ReconfigurableLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging 17 | 18 | import zio._ 19 | import zio.prelude._ 20 | 21 | import java.util.concurrent.atomic.AtomicReference 22 | 23 | sealed trait ReconfigurableLogger[-Message, +Output, Config] extends ZLogger[Message, Output] { 24 | 25 | def get: (Config, ZLogger[Message, Output]) 26 | 27 | def set[M <: Message, O >: Output](config: Config, logger: ZLogger[M, O]): Unit 28 | } 29 | 30 | object ReconfigurableLogger { 31 | 32 | def apply[Message, Output, Config]( 33 | config: Config, 34 | logger: ZLogger[Message, Output] 35 | ): ReconfigurableLogger[Message, Output, Config] = { 36 | val configuredLogger: AtomicReference[(Config, ZLogger[Message, Output])] = 37 | new AtomicReference[(Config, ZLogger[Message, Output])]((config, logger)) 38 | 39 | new ReconfigurableLogger[Message, Output, Config] { 40 | 41 | override def get: (Config, ZLogger[Message, Output]) = configuredLogger.get() 42 | 43 | override def set[M <: Message, O >: Output](config: Config, logger: ZLogger[M, O]): Unit = 44 | configuredLogger.set((config, logger.asInstanceOf[ZLogger[Message, Output]])) 45 | 46 | override def apply( 47 | trace: Trace, 48 | fiberId: FiberId, 49 | logLevel: LogLevel, 50 | message: () => Message, 51 | cause: Cause[Any], 52 | context: FiberRefs, 53 | spans: List[LogSpan], 54 | annotations: Map[String, String] 55 | ): Output = 56 | configuredLogger.get()._2.apply(trace, fiberId, logLevel, message, cause, context, spans, annotations) 57 | } 58 | } 59 | 60 | def make[R, E, M, O, C: Equal]( 61 | loadConfig: => ZIO[R, E, C], 62 | makeLogger: (C, Option[ZLogger[M, O]]) => ZIO[R, E, ZLogger[M, O]], 63 | updateLogger: Schedule[R, Any, Any] = Schedule.fixed(10.seconds) 64 | ): ZIO[R with Scope, E, ReconfigurableLogger[M, O, C]] = 65 | for { 66 | initialConfig <- loadConfig 67 | initialLogger <- makeLogger(initialConfig, None) 68 | reconfigurableLogger = ReconfigurableLogger[M, O, C](initialConfig, initialLogger) 69 | _ <- loadConfig.flatMap { newConfig => 70 | val (currentConfig, currentLogger) = reconfigurableLogger.get 71 | if (currentConfig !== newConfig) { 72 | makeLogger(newConfig, Some(currentLogger)).map { newLogger => 73 | reconfigurableLogger.set(newConfig, newLogger) 74 | }.unit 75 | } else ZIO.unit 76 | }.schedule(updateLogger).forkScoped 77 | } yield reconfigurableLogger 78 | 79 | } 80 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/internal/JsonEscape.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.internal 17 | 18 | import scala.annotation.switch 19 | 20 | object JsonEscape { 21 | def apply(s: CharSequence): CharSequence = { 22 | val sb = new java.lang.StringBuilder 23 | var i = 0 24 | val len = s.length 25 | while (i < len) { 26 | (s.charAt(i): @switch) match { 27 | case '"' => sb.append("\\\"") 28 | case '\\' => sb.append("\\\\") 29 | case '\b' => sb.append("\\b") 30 | case '\f' => sb.append("\\f") 31 | case '\n' => sb.append("\\n") 32 | case '\r' => sb.append("\\r") 33 | case '\t' => sb.append("\\t") 34 | case c => 35 | if (c < ' ') sb.append("\\u%04x".format(c.toInt)) 36 | else sb.append(c) 37 | } 38 | i += 1 39 | } 40 | sb 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/logging/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio 17 | 18 | package object logging extends LoggingPackageAllPlatforms with logging.LoggingPackagePlatformSpecific 19 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/AppendLoggerNameSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.ZIO 4 | import zio.test._ 5 | 6 | object AppendLoggerNameSpec extends ZIOSpecDefault { 7 | 8 | val spec: Spec[Environment, Any] = suite("AppendLoggerNameSpec")( 9 | test("appendLoggerName") { 10 | for { 11 | name <- ZIO.logAnnotations.map(annotations => annotations.get(loggerNameAnnotationKey)) @@ appendLoggerName( 12 | "logging" 13 | ) @@ appendLoggerName("zio") 14 | 15 | } yield assertTrue(name == Some("zio.logging")) 16 | } 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/ConsoleLoggerConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.test._ 4 | import zio.{ Config, ConfigProvider, LogLevel } 5 | 6 | object ConsoleLoggerConfigSpec extends ZIOSpecDefault { 7 | 8 | val spec: Spec[Environment, Any] = suite("ConsoleLoggerConfig")( 9 | test("load valid config") { 10 | 11 | val logFormat = 12 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 13 | 14 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 15 | Map( 16 | "logger/format" -> logFormat, 17 | "logger/filter/rootLevel" -> LogLevel.Info.label, 18 | "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label 19 | ), 20 | "/" 21 | ) 22 | 23 | configProvider.load(ConsoleLoggerConfig.config.nested("logger")).map { _ => 24 | assertTrue(true) 25 | } 26 | }, 27 | test("load default with missing config") { 28 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 29 | Map.empty, 30 | "/" 31 | ) 32 | 33 | configProvider.load(ConsoleLoggerConfig.config.nested("logger")).map { _ => 34 | assertTrue(true) 35 | } 36 | }, 37 | test("equals config with same sources") { 38 | 39 | // "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" //FIXME 40 | 41 | val logFormat = 42 | "%highlight{%timestamp %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 43 | 44 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 45 | Map( 46 | "logger/format" -> logFormat, 47 | "logger/filter/rootLevel" -> LogLevel.Info.label, 48 | "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label 49 | ), 50 | "/" 51 | ) 52 | 53 | import zio.prelude._ 54 | for { 55 | c1 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) 56 | c2 <- configProvider.load(ConsoleLoggerConfig.config.nested("logger")) 57 | } yield assertTrue( 58 | c1.format == c2.format, 59 | c1 === c2 60 | ) 61 | }, 62 | test("fail on invalid filter config") { 63 | val logFormat = 64 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 65 | 66 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 67 | Map( 68 | "logger/format" -> logFormat, 69 | "logger/filter/rootLevel" -> "INVALID_LOG_LEVEL" 70 | ), 71 | "/" 72 | ) 73 | 74 | configProvider 75 | .load(ConsoleLoggerConfig.config.nested("logger")) 76 | .exit 77 | .map { e => 78 | assert(e)(Assertion.failsWithA[Config.Error]) 79 | } 80 | }, 81 | test("fail on invalid format config") { 82 | val logFormat = 83 | "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{%level} [%fiberId] %name:%line %message %cause}" 84 | 85 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 86 | Map( 87 | "logger/format" -> logFormat 88 | ), 89 | "/" 90 | ) 91 | 92 | configProvider 93 | .load(ConsoleLoggerConfig.config.nested("logger")) 94 | .exit 95 | .map { e => 96 | assert(e)(Assertion.failsWithA[Config.Error]) 97 | } 98 | } 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/LogAnnotationSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.logging.LogAnnotation 4 | import zio.test.Assertion._ 5 | import zio.test._ 6 | import zio.{ Chunk, Runtime, ZIO, _ } 7 | 8 | import java.util.UUID 9 | 10 | object LogAnnotationSpec extends ZIOSpecDefault { 11 | 12 | private def getOutputLogAnnotationValues(annotation: LogAnnotation[_]): ZIO[Any, Nothing, Chunk[Option[String]]] = 13 | ZTestLogger.logOutput.map { loggerOutput => 14 | loggerOutput.map(_.context.get(logContext).flatMap(_.get(annotation.name))) 15 | } 16 | 17 | override def spec: Spec[TestEnvironment, Any] = suite("LogAnnotationSpec")( 18 | test("annotations aspect combinators") { 19 | assertTrue( 20 | (LogAnnotation.UserId("u") @@ LogAnnotation.TraceId(UUID.randomUUID())) 21 | .isInstanceOf[LogAnnotation.LogAnnotationAspect] 22 | ) && assertTrue( 23 | (LogAnnotation.UserId("u") >>> LogAnnotation.TraceId(UUID.randomUUID())) 24 | .isInstanceOf[LogAnnotation.LogAnnotationAspect] 25 | ) 26 | }, 27 | test("annotations from multiple levels with @@") { 28 | val users = Chunk.fill(2)(UUID.randomUUID()) 29 | for { 30 | traceId <- ZIO.succeed(UUID.randomUUID()) 31 | _ <- ZIO.foreach(users) { uId => 32 | { 33 | ZIO.logInfo("start") *> ZIO.sleep(100.millis) *> ZIO.logInfo("stop") 34 | } @@ LogAnnotation.UserId(uId.toString) *> ZIO.logInfo("next") 35 | } @@ LogAnnotation.TraceId(traceId) 36 | outputTraceIds <- getOutputLogAnnotationValues(LogAnnotation.TraceId) 37 | outputUserIds <- getOutputLogAnnotationValues(LogAnnotation.UserId) 38 | } yield assert(outputTraceIds.flatten)(equalTo(Chunk.fill(6)(traceId.toString))) && 39 | assert(outputUserIds.flatten)(equalTo(users.flatMap(u => Chunk.fill(2)(u.toString)))) 40 | }.provideLayer(ZTestLogger.default), 41 | test("annotations from same levels with @@") { 42 | val users = Chunk.fill(2)(UUID.randomUUID()) 43 | for { 44 | traceId <- ZIO.succeed(UUID.randomUUID()) 45 | _ <- ZIO.foreach(users) { uId => 46 | { 47 | ZIO.logInfo("start") *> ZIO.sleep(100.millis) *> ZIO.logInfo("stop") 48 | } @@ (LogAnnotation.UserId(uId.toString) @@ LogAnnotation.TraceId(traceId)) 49 | } 50 | outputTraceIds <- getOutputLogAnnotationValues(LogAnnotation.TraceId) 51 | outputUserIds <- getOutputLogAnnotationValues(LogAnnotation.UserId) 52 | } yield assert(outputTraceIds.flatten)(equalTo(Chunk.fill(4)(traceId.toString))) && 53 | assert(outputUserIds.flatten)(equalTo(users.flatMap(u => Chunk.fill(2)(u.toString)))) 54 | }.provideLayer(ZTestLogger.default) 55 | ).provideLayer(Runtime.removeDefaultLoggers) @@ TestAspect.withLiveClock 56 | } 57 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/LogFormatPatternSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.Chunk 4 | import zio.logging.LogFormat.Pattern 5 | import zio.test._ 6 | 7 | object LogFormatPatternSpec extends ZIOSpecDefault { 8 | 9 | val spec: Spec[Environment, Any] = suite("LogFormat.Pattern")( 10 | test("parse pattern from string") { 11 | 12 | val pattern = Pattern.parse("%timestamp %level xyz %message %cause %span{abc}") 13 | 14 | assertTrue( 15 | pattern == Right( 16 | Pattern.Patterns( 17 | Chunk( 18 | Pattern.Timestamp.default, 19 | Pattern.Text(" "), 20 | Pattern.LogLevel, 21 | Pattern.Text(" xyz "), 22 | Pattern.LogMessage, 23 | Pattern.Text(" "), 24 | Pattern.Cause, 25 | Pattern.Text(" "), 26 | Pattern.Span("abc") 27 | ) 28 | ) 29 | ) 30 | ) 31 | }, 32 | test("parse pattern with escaped reserved chars from string") { 33 | 34 | val pattern = 35 | Pattern.parse( 36 | "%color{CYAN}{%timestamp} %fixed{7}{%level} %% %} xyz %message %cause %label{abcSpan}{%span{abc}}" 37 | ) 38 | 39 | assertTrue( 40 | pattern == Right( 41 | Pattern.Patterns( 42 | Chunk( 43 | Pattern.Color(LogColor.CYAN, Pattern.Timestamp.default), 44 | Pattern.Text(" "), 45 | Pattern.Fixed(7, Pattern.LogLevel), 46 | Pattern.Text(" "), 47 | Pattern.EscapedArgPrefix, 48 | Pattern.Text(" "), 49 | Pattern.EscapedCloseBracket, 50 | Pattern.Text(" xyz "), 51 | Pattern.LogMessage, 52 | Pattern.Text(" "), 53 | Pattern.Cause, 54 | Pattern.Text(" "), 55 | Pattern.Label("abcSpan", Pattern.Span("abc")) 56 | ) 57 | ) 58 | ) 59 | ) 60 | }, 61 | test("parse pattern with labels from string") { 62 | 63 | val pattern = 64 | Pattern.parse( 65 | "%label{timestamp}{%fixed{32}{%timestamp}} %label{level}{%level} %label{thread}{%fiberId} %label{message}{%message} %label{cause}{%cause}" 66 | ) 67 | 68 | assertTrue( 69 | pattern == Right( 70 | Pattern.Patterns( 71 | Chunk( 72 | Pattern.Label("timestamp", Pattern.Fixed(32, Pattern.Timestamp.default)), 73 | Pattern.Text(" "), 74 | Pattern.Label("level", Pattern.LogLevel), 75 | Pattern.Text(" "), 76 | Pattern.Label("thread", Pattern.FiberId), 77 | Pattern.Text(" "), 78 | Pattern.Label("message", Pattern.LogMessage), 79 | Pattern.Text(" "), 80 | Pattern.Label("cause", Pattern.Cause) 81 | ) 82 | ) 83 | ) 84 | ) 85 | }, 86 | test("parse pattern with highlight from string") { 87 | 88 | val pattern = Pattern.parse("%timestamp %highlight{%level %{xyz%} %message %cause %span{abc}}") 89 | 90 | assertTrue( 91 | pattern == Right( 92 | Pattern.Patterns( 93 | Chunk( 94 | Pattern.Timestamp.default, 95 | Pattern.Text(" "), 96 | Pattern.Highlight( 97 | Pattern.Patterns( 98 | Chunk( 99 | Pattern.LogLevel, 100 | Pattern.Text(" "), 101 | Pattern.EscapedOpenBracket, 102 | Pattern.Text("xyz"), 103 | Pattern.EscapedCloseBracket, 104 | Pattern.Text(" "), 105 | Pattern.LogMessage, 106 | Pattern.Text(" "), 107 | Pattern.Cause, 108 | Pattern.Text(" "), 109 | Pattern.Span("abc") 110 | ) 111 | ) 112 | ) 113 | ) 114 | ) 115 | ) 116 | ) 117 | } 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/LogGroupSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.test._ 4 | import zio.{ Cause, FiberId, FiberRefs, LogLevel, Trace } 5 | 6 | object LogGroupSpec extends ZIOSpecDefault { 7 | val spec: Spec[Environment, Any] = suite("LogGroupSpec")( 8 | test("logLevel") { 9 | val group = LogGroup.logLevel 10 | check(Gen.elements(LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Debug)) { level => 11 | val result = group( 12 | Trace.empty, 13 | FiberId.None, 14 | level, 15 | () => "", 16 | Cause.empty, 17 | FiberRefs.empty, 18 | Nil, 19 | Map.empty 20 | ) 21 | assertTrue(result == level) 22 | } 23 | }, 24 | test("loggerName with trace") { 25 | val group = LogGroup.loggerName 26 | check(Gen.alphaNumericString) { value => 27 | val result = group( 28 | Trace.apply(value, "", 1), 29 | FiberId.None, 30 | LogLevel.Info, 31 | () => "", 32 | Cause.empty, 33 | FiberRefs.empty, 34 | Nil, 35 | Map.empty 36 | ) 37 | assertTrue(result == value) 38 | } 39 | }, 40 | test("loggerName with logger name annotation") { 41 | val group = LogGroup.loggerName 42 | check(Gen.alphaNumericString) { value => 43 | val result = group( 44 | Trace.empty, 45 | FiberId.None, 46 | LogLevel.Info, 47 | () => "", 48 | Cause.empty, 49 | FiberRefs.empty, 50 | Nil, 51 | Map(loggerNameAnnotationKey -> value) 52 | ) 53 | assertTrue(result == value) 54 | } 55 | }, 56 | test("loggerNameAndLevel") { 57 | val group = LogGroup.loggerNameAndLevel 58 | check(Gen.alphaNumericString, Gen.elements(LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Debug)) { 59 | (value, level) => 60 | val result = group( 61 | Trace.apply(value, "", 1), 62 | FiberId.None, 63 | level, 64 | () => "", 65 | Cause.empty, 66 | FiberRefs.empty, 67 | Nil, 68 | Map.empty 69 | ) 70 | assertTrue(result == (value -> level)) 71 | } 72 | }, 73 | test("++") { 74 | val group = LogGroup.loggerName ++ LogGroup.logLevel 75 | check(Gen.alphaNumericString, Gen.elements(LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Debug)) { 76 | (value, level) => 77 | val result = group( 78 | Trace.apply(value, "", 1), 79 | FiberId.None, 80 | level, 81 | () => "", 82 | Cause.empty, 83 | FiberRefs.empty, 84 | Nil, 85 | Map.empty 86 | ) 87 | assertTrue(result == (value -> level)) 88 | } 89 | }, 90 | test("contramap") { 91 | val group = LogGroup[String, String]((_, _, _, line, _, _, _, _) => line()).contramap[Long](_.toString) 92 | check(Gen.long) { value => 93 | val result = group( 94 | Trace.empty, 95 | FiberId.None, 96 | LogLevel.Info, 97 | () => value, 98 | Cause.empty, 99 | FiberRefs.empty, 100 | Nil, 101 | Map.empty 102 | ) 103 | assertTrue(result == value.toString) 104 | } 105 | }, 106 | test("map") { 107 | val group = LogGroup.loggerName.map("PREFIX_" + _) 108 | check(Gen.alphaNumericString) { value => 109 | val result = group( 110 | Trace.apply(value, "", 1), 111 | FiberId.None, 112 | LogLevel.Info, 113 | () => "", 114 | Cause.empty, 115 | FiberRefs.empty, 116 | Nil, 117 | Map.empty 118 | ) 119 | assertTrue(result == ("PREFIX_" + value)) 120 | } 121 | }, 122 | test("zipWith") { 123 | val group = LogGroup.loggerName.zipWith(LogGroup.logLevel)(_ -> _) 124 | check(Gen.alphaNumericString, Gen.elements(LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Debug)) { 125 | (value, level) => 126 | val result = group( 127 | Trace.apply(value, "", 1), 128 | FiberId.None, 129 | level, 130 | () => "", 131 | Cause.empty, 132 | FiberRefs.empty, 133 | Nil, 134 | Map.empty 135 | ) 136 | assertTrue(result == (value -> level)) 137 | } 138 | } 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/LoggerNameExtractorSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.test._ 4 | import zio.{ FiberRefs, Trace } 5 | 6 | object LoggerNameExtractorSpec extends ZIOSpecDefault { 7 | val spec: Spec[Environment, Any] = suite("LoggerNameExtractorSpec")( 8 | test("annotation") { 9 | val extractor = LoggerNameExtractor.annotation("my_logger_name") 10 | check(Gen.alphaNumericString) { value => 11 | val result = extractor( 12 | Trace.empty, 13 | FiberRefs.empty, 14 | Map("my_logger_name" -> value) 15 | ) 16 | assertTrue(result == Some(value)) 17 | } 18 | }, 19 | test("annotationOrTrace") { 20 | val extractor = LoggerNameExtractor.annotationOrTrace("my_logger_name") 21 | check(Gen.alphaNumericString, Gen.alphaNumericString, Gen.boolean) { (trace, annotation, hasAnnotation) => 22 | val annotations = if (hasAnnotation) Map("my_logger_name" -> annotation) else Map.empty[String, String] 23 | val value = 24 | if (hasAnnotation) annotation 25 | else { 26 | val last = trace.lastIndexOf(".") 27 | if (last > 0) { 28 | trace.substring(0, last) 29 | } else trace 30 | } 31 | val result = extractor( 32 | Trace.apply(trace, "", 1), 33 | FiberRefs.empty, 34 | annotations 35 | ) 36 | assertTrue(result == Some(value)) 37 | } 38 | }, 39 | test("loggerNameAnnotationOrTrace") { 40 | val extractor = LoggerNameExtractor.loggerNameAnnotationOrTrace 41 | check(Gen.alphaNumericString, Gen.alphaNumericString, Gen.boolean) { (trace, annotation, hasAnnotation) => 42 | val annotations = if (hasAnnotation) Map(loggerNameAnnotationKey -> annotation) else Map.empty[String, String] 43 | val value = 44 | if (hasAnnotation) annotation 45 | else { 46 | val last = trace.lastIndexOf(".") 47 | if (last > 0) { 48 | trace.substring(0, last) 49 | } else trace 50 | } 51 | val result = extractor( 52 | Trace.apply(trace, "", 1), 53 | FiberRefs.empty, 54 | annotations 55 | ) 56 | assertTrue(result == Some(value)) 57 | } 58 | }, 59 | test("loggerNameAnnotationOrTrace with empty trace") { 60 | val extractor = LoggerNameExtractor.loggerNameAnnotationOrTrace 61 | check(Gen.alphaNumericString) { annotation => 62 | val annotations = Map(loggerNameAnnotationKey -> annotation) 63 | val result = extractor( 64 | Trace.empty, 65 | FiberRefs.empty, 66 | annotations 67 | ) 68 | assertTrue(result == Some(annotation)) 69 | } 70 | }, 71 | test("trace") { 72 | val extractor = LoggerNameExtractor.trace 73 | check(Gen.alphaNumericString) { trace => 74 | val last = trace.lastIndexOf(".") 75 | val value = if (last > 0) { 76 | trace.substring(0, last) 77 | } else trace 78 | val result = extractor( 79 | Trace.apply(trace, "", 1), 80 | FiberRefs.empty, 81 | Map.empty 82 | ) 83 | assertTrue(result == Some(value)) 84 | } 85 | } 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/MetricsSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.logging.test.TestService 4 | import zio.test._ 5 | import zio.{ LogLevel, ZIO } 6 | 7 | object MetricsSpec extends ZIOSpecDefault { 8 | 9 | val spec: Spec[Environment, Any] = suite("MetricsSpec")( 10 | test("logs totals metrics") { 11 | (for { 12 | _ <- ZIO.logDebug("debug") 13 | _ <- ZIO.logInfo("info") 14 | _ <- ZIO.logWarning("warning") 15 | _ <- ZIO.logError("error") 16 | _ <- ZIO.logTrace("trace") 17 | _ <- TestService.testDebug 18 | _ <- TestService.testInfo 19 | _ <- TestService.testWarning 20 | _ <- TestService.testError 21 | _ <- TestService.testTrace 22 | debugCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Debug.label.toLowerCase).value 23 | infoCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Info.label.toLowerCase).value 24 | warnCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Warning.label.toLowerCase).value 25 | errorCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Error.label.toLowerCase).value 26 | fatalCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Fatal.label.toLowerCase).value 27 | clearCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.None.label.toLowerCase).value 28 | traceCounter <- loggedTotalMetric.tagged(logLevelMetricLabel, LogLevel.Trace.label.toLowerCase).value 29 | } yield assertTrue(debugCounter.count == 2d) && assertTrue(infoCounter.count == 2d) && assertTrue( 30 | warnCounter.count == 2d 31 | ) && assertTrue(errorCounter.count == 2d) && assertTrue(fatalCounter.count == 0d) 32 | && assertTrue(clearCounter.count == 0d) && assertTrue(traceCounter.count == 2d)) 33 | .provideLayer(logMetrics) 34 | } 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/ReconfigurableLoggerSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging 2 | 3 | import zio.test._ 4 | import zio.{ Chunk, Config, ConfigProvider, LogLevel, Queue, Runtime, Schedule, ZIO, ZLayer, _ } 5 | 6 | object ReconfigurableLoggerSpec extends ZIOSpecDefault { 7 | 8 | def configuredLogger( 9 | queue: zio.Queue[String] 10 | ): ZLayer[Any, Config.Error, Unit] = 11 | ZLayer.scoped { 12 | for { 13 | logger <- ReconfigurableLogger 14 | .make[Any, Config.Error, String, Any, ConsoleLoggerConfig]( 15 | ConsoleLoggerConfig.load(), 16 | (config, _) => 17 | ZIO.succeed { 18 | config.format.toLogger.map { line => 19 | zio.Unsafe.unsafe { implicit u => 20 | Runtime.default.unsafe.run(queue.offer(line)) 21 | } 22 | }.filter(config.toFilter) 23 | }, 24 | Schedule.fixed(200.millis) 25 | ) 26 | _ <- ZIO.withLoggerScoped(logger) 27 | } yield () 28 | } 29 | 30 | val spec: Spec[Environment, Any] = suite("ReconfigurableLogger2")( 31 | test("log with changed config") { 32 | 33 | val initialProperties = Map( 34 | "logger/format" -> "%message", 35 | "logger/filter/rootLevel" -> LogLevel.Info.label, 36 | "logger/filter/mappings/zio.logging.example.LivePingService" -> LogLevel.Debug.label 37 | ) 38 | 39 | for { 40 | _ <- ZIO.foreach(initialProperties) { case (k, v) => 41 | TestSystem.putProperty(k, v).as(k -> v) 42 | } 43 | 44 | queue <- Queue.unbounded[String] 45 | 46 | runTest = 47 | for { 48 | _ <- ZIO.logInfo("info") 49 | _ <- ZIO.logDebug("debug") 50 | elements1 <- queue.takeAll 51 | _ <- TestSystem.putProperty("logger/format", "%level %message") 52 | _ <- ZIO.sleep(1000.millis) 53 | _ <- ZIO.logWarning("warn") 54 | _ <- ZIO.logDebug("debug") 55 | elements2 <- queue.takeAll 56 | _ <- TestSystem.putProperty("logger/format", "L: %level M: %message") 57 | _ <- TestSystem.putProperty("logger/filter/rootLevel", LogLevel.Debug.label) 58 | _ <- ZIO.sleep(1000.millis) 59 | _ <- ZIO.logDebug("debug") 60 | elements3 <- queue.takeAll 61 | } yield assertTrue( 62 | elements1 == Chunk("info") && elements2 == Chunk("WARN warn") && elements3 == Chunk( 63 | "L: DEBUG M: debug" 64 | ) 65 | ) 66 | 67 | result <- 68 | runTest.provide( 69 | Runtime.removeDefaultLoggers >>> Runtime 70 | .setConfigProvider(ConfigProvider.fromProps("/")) >>> configuredLogger(queue) 71 | ) 72 | } yield result 73 | 74 | } 75 | ) @@ TestAspect.withLiveClock 76 | } 77 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/zio/logging/test/TestService.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.test 2 | 3 | import zio.{ UIO, ZIO } 4 | 5 | object TestService { 6 | 7 | val testDebug: UIO[Unit] = ZIO.logDebug("test debug") 8 | 9 | val testInfo: UIO[Unit] = ZIO.logInfo("test info") 10 | 11 | val testWarning: UIO[Unit] = ZIO.logWarning("test warning") 12 | 13 | val testError: UIO[Unit] = ZIO.logError("test error") 14 | 15 | val testTrace: UIO[Unit] = ZIO.logTrace("test trace") 16 | 17 | } 18 | -------------------------------------------------------------------------------- /docs/file-logger.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: file-logger 3 | title: "File Logger" 4 | --- 5 | 6 | logger layer with configuration from config provider: 7 | 8 | ```scala 9 | import zio.logging.fileLogger 10 | import zio.{ ConfigProvider, Runtime } 11 | 12 | val configProvider: ConfigProvider = ??? 13 | 14 | val logger = Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> fileLogger() 15 | ``` 16 | 17 | logger layer with given configuration: 18 | 19 | ```scala 20 | import zio.logging.{ fileLogger, FileLoggerConfig } 21 | import zio.Runtime 22 | 23 | val config: FileLoggerConfig = ??? 24 | 25 | val logger = Runtime.removeDefaultLoggers >>> fileLogger(config) 26 | ``` 27 | 28 | there are other versions of file loggers: 29 | * `zio.logging.fileJsonLogger` - output in json format 30 | * file async: 31 | * `zio.logging.fileAsynLogger` - output in string format 32 | * `zio.logging.fileAsyncJsonLogger` - output in json format 33 | 34 | ## Configuration 35 | 36 | the configuration for file logger (`zio.logging.FileLoggerConfig`) has the following configuration structure: 37 | 38 | ``` 39 | logger { 40 | # log format, default value: LogFormat.default 41 | format = "%label{timestamp}{%fixed{32}{%timestamp}} %label{level}{%level} %label{thread}{%fiberId} %label{message}{%message} %label{cause}{%cause}" 42 | 43 | # URI to file 44 | path = "file:///tmp/console_app.log" 45 | 46 | # charset configuration, default value: UTF-8 47 | charset = "UTF-8" 48 | 49 | # auto flush batch size, default value: 1 50 | autoFlushBatchSize = 1 51 | 52 | # if defined, buffered writer is used, with given buffer size 53 | # bufferedIOSize = 8192 54 | 55 | # if defined, file log rolling policy is used 56 | rollingPolicy { 57 | type = TimeBasedRollingPolicy # time based file rolling policy based on date - currently only this one is supported 58 | } 59 | 60 | # log filter 61 | filter { 62 | # see filter configuration 63 | rootLevel = INFO 64 | } 65 | } 66 | ``` 67 | 68 | see also [log format configuration](formatting-log-records.md#log-format-configuration) and [filter configuration](log-filter.md#configuration) 69 | 70 | 71 | ## Examples 72 | 73 | You can find the source code [here](https://github.com/zio/zio-logging/tree/master/examples) 74 | 75 | ### File Logger 76 | 77 | [//]: # (TODO: make snippet type-checked using mdoc) 78 | 79 | ```scala 80 | package zio.logging.example 81 | 82 | import zio.config.typesafe.TypesafeConfigProvider 83 | import zio.logging.fileLogger 84 | import zio.{ Config, ConfigProvider, ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, ZLayer } 85 | 86 | object FileApp extends ZIOAppDefault { 87 | 88 | val configString: String = 89 | s""" 90 | |logger { 91 | | format = "%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause" 92 | | path = "file:///tmp/file_app.log" 93 | | rollingPolicy { 94 | | type = TimeBasedRollingPolicy 95 | | } 96 | |} 97 | |""".stripMargin 98 | 99 | val configProvider: ConfigProvider = TypesafeConfigProvider.fromHoconString(configString) 100 | 101 | override val bootstrap: ZLayer[Any, Config.Error, Unit] = 102 | Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> fileLogger() 103 | 104 | override def run: ZIO[Scope, Any, ExitCode] = 105 | for { 106 | _ <- ZIO.logInfo("Start") 107 | _ <- ZIO.fail("FAILURE") 108 | _ <- ZIO.logInfo("Done") 109 | } yield ExitCode.success 110 | } 111 | ``` 112 | 113 | Expected file name: `file_app-2023-03-05.log` 114 | 115 | Expected file content: 116 | 117 | ``` 118 | 2023-03-05T12:37:31+0100 INFO [zio-fiber-4] zio.logging.example.FileApp:40 Start 119 | 2023-03-05T12:37:31+0100 ERROR [zio-fiber-0] zio-logger: Exception in thread "zio-fiber-4" java.lang.String: FAILURE 120 | at zio.logging.example.FileApp.run(FileApp.scala:41) 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/jpl.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: jpl 3 | title: "Java Platform/System Logger" 4 | --- 5 | 6 | [`Java Platform/System Logger`](https://openjdk.org/jeps/264) is logging API which was introduced in Java 9. 7 | 8 | In order to use this logging backend, we need to add the following line in our build.sbt file: 9 | 10 | ```scala 11 | libraryDependencies += "dev.zio" %% "zio-logging-jpl" % "@VERSION@" 12 | ``` 13 | 14 | Logger layer: 15 | 16 | [//]: # (TODO: make snippet type-checked using mdoc) 17 | 18 | ```scala 19 | import zio.logging.backend.JPL 20 | 21 | val logger = Runtime.removeDefaultLoggers >>> JPL.jpl 22 | ``` 23 | 24 | Default `JPL` logger setup: 25 | * logger name (by default) is extracted from `zio.Trace` 26 | * for example, trace `zio.logging.example.JplSimpleApp.run(JplSimpleApp.scala:17)` will have `zio.logging.example.JplSimpleApp` as logger name 27 | * NOTE: custom logger name may be set by `zio.logging.loggerName` aspect 28 | * all annotations (logger name annotation is excluded) are placed at the beginning of log message 29 | * cause is logged as throwable 30 | 31 | See also [LogFormat and LogAppender](formatting-log-records.md#logformat-and-logappender) 32 | 33 | Custom logger name set by aspect: 34 | 35 | ```scala 36 | ZIO.logInfo("Starting user operation") @@ zio.logging.loggerName("zio.logging.example.UserOperation") 37 | ``` 38 | 39 | 40 | ## Examples 41 | 42 | You can find the source code [here](https://github.com/zio/zio-logging/tree/master/examples) 43 | 44 | ### Java Platform/System logger name and annotations 45 | 46 | [//]: # (TODO: make snippet type-checked using mdoc) 47 | 48 | ```scala 49 | package zio.logging.example 50 | 51 | import zio.logging.LogAnnotation 52 | import zio.logging.backend.JPL 53 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 54 | 55 | import java.util.UUID 56 | 57 | object JplSimpleApp extends ZIOAppDefault { 58 | 59 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> JPL.jpl 60 | 61 | private val users = List.fill(2)(UUID.randomUUID()) 62 | 63 | override def run: ZIO[Scope, Any, ExitCode] = 64 | for { 65 | _ <- ZIO.logInfo("Start") 66 | traceId <- ZIO.succeed(UUID.randomUUID()) 67 | _ <- ZIO.foreachPar(users) { uId => 68 | { 69 | ZIO.logInfo("Starting user operation") *> 70 | ZIO.sleep(500.millis) *> 71 | ZIO.logInfo("Stopping user operation") 72 | } @@ ZIOAspect.annotated("user", uId.toString) 73 | } @@ LogAnnotation.TraceId(traceId) @@ zio.logging.loggerName("zio.logging.example.UserOperation") 74 | _ <- ZIO.logInfo("Done") 75 | } yield ExitCode.success 76 | 77 | } 78 | ``` 79 | 80 | Expected Console Output: 81 | ``` 82 | Oct 28, 2022 1:47:01 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 83 | INFO: Start 84 | Oct 28, 2022 1:47:01 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 85 | INFO: user=59c114fd-676d-4df9-a5a0-b8e132468fbf trace_id=7d3e3b84-dd3b-44ff-915a-04fb2d135e28 Starting user operation 86 | Oct 28, 2022 1:47:01 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 87 | INFO: user=e1ebf0cc-2f61-484f-afcd-de7e20ec7829 trace_id=7d3e3b84-dd3b-44ff-915a-04fb2d135e28 Starting user operation 88 | Oct 28, 2022 1:47:02 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 89 | INFO: user=e1ebf0cc-2f61-484f-afcd-de7e20ec7829 trace_id=7d3e3b84-dd3b-44ff-915a-04fb2d135e28 Stopping user operation 90 | Oct 28, 2022 1:47:02 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 91 | INFO: user=59c114fd-676d-4df9-a5a0-b8e132468fbf trace_id=7d3e3b84-dd3b-44ff-915a-04fb2d135e28 Stopping user operation 92 | Oct 28, 2022 1:47:02 PM zio.logging.backend.JPL$$anon$1 $anonfun$closeLogEntry$1 93 | INFO: Done 94 | ``` 95 | 96 | ## Feature changes 97 | 98 | ### Version 2.2.0 99 | 100 | Deprecated log annotation with key `jpl_logger_name` (`JPL.loggerNameAnnotationKey`) removed, 101 | only common log annotation with key `logger_name` (`zio.logging.loggerNameAnnotationKey`) for logger name is supported now. 102 | -------------------------------------------------------------------------------- /docs/log-filter.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: log-filter 3 | title: "Log Filter" 4 | --- 5 | 6 | A `LogFilter` represents function/conditions for log filtering. 7 | 8 | Following filter 9 | 10 | [//]: # (TODO: make snippet type-checked using mdoc) 11 | 12 | ```scala 13 | import zio.LogLevel 14 | import zio.logging.LogFilter 15 | 16 | val filter = LogFilter.logLevelByName( 17 | LogLevel.Debug, 18 | "io.netty" -> LogLevel.Info, 19 | "io.grpc.netty" -> LogLevel.Info, 20 | "org.my.**.ServiceX" -> LogLevel.Trace, // glob-like (any paths) filter 21 | "org.my.X*Layers" -> LogLevel.Info // glob-like (single or partial path) filter 22 | ) 23 | ``` 24 | 25 | will use the `Debug` log level for everything except for log events with the logger name 26 | prefixed by either `List("io", "netty")` or `List("io", "grpc", "netty")` or `List("org", "my", "**", "ServiceX")` or `List("org", "my", "X*Layers")`. 27 | Logger name is extracted from log annotation or `zio.Trace`. 28 | 29 | `LogFilter.filter` returns a version of `zio.ZLogger` that only logs messages when this filter is satisfied. 30 | 31 | 32 | ## Configuration 33 | 34 | the configuration for filter (`zio.logging.LogFilter.LogLevelByNameConfig`) has the following configuration structure: 35 | 36 | ``` 37 | { 38 | # LogLevel values: ALL, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, OFF 39 | 40 | # root LogLevel, default value: INFO 41 | rootLevel = DEBUG 42 | 43 | # LogLevel configurations for specific logger names, or prefixes, default value: empty 44 | mappings { 45 | "io.netty" = "INFO" 46 | "io.grpc.netty" = "INFO" 47 | } 48 | } 49 | ``` 50 | 51 | this configuration is equivalent to following: 52 | 53 | ```scala 54 | import zio.LogLevel 55 | import zio.logging.LogFilter 56 | 57 | val config = 58 | LogFilter.LogLevelByNameConfig(LogLevel.Debug, Map("io.netty" -> LogLevel.Info, "io.grpc.netty" -> LogLevel.Info)) 59 | 60 | val filter = LogFilter.logLevelByName(config) 61 | ``` -------------------------------------------------------------------------------- /docs/logger-context-and-annotations.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: logger-context-and-annotations 3 | title: "Logger Context and Annotations" 4 | --- 5 | 6 | The `logContext` fiber reference is used to store typed, structured log 7 | annotations, which can be utilized by backends to enrich log messages. 8 | 9 | Because `logContext` is an ordinary `zio.FiberRef`, it may be get, set, 10 | and updated like any other fiber reference. However, the idiomatic way to 11 | interact with `logContext` is by using `zio.logging.LogAnnotation`. 12 | 13 | For example: 14 | 15 | [//]: # (TODO: make snippet type-checked using mdoc) 16 | 17 | ```scala 18 | myResponseHandler(request) @@ LogAnnotation.UserId(request.userId) 19 | ``` 20 | 21 | This code would add the structured log annotation `LogAnnotation.UserId` 22 | to all log messages emitted by the `myResponseHandler(request)` effect. 23 | 24 | The user of the library is allowed to add a custom `LogAnnotation`: 25 | 26 | ```scala mdoc:silent 27 | import zio.logging.LogAnnotation 28 | 29 | val customLogAnnotation = LogAnnotation[Int]("custom_annotation", _ + _, _.toString) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: metrics 3 | title: "Log Metrics" 4 | --- 5 | 6 | Log metrics collecting metrics related to ZIO logging (all `ZIO.log*` functions). 7 | As ZIO core supporting multiple loggers, this logging metrics collector is implemented as specific `ZLogger` 8 | which is responsible just for collecting metrics of all logs - `ZIO.log*` functions. 9 | 10 | The Metrics layer 11 | 12 | ```scala 13 | val layer = zio.logging.logMetrics 14 | ``` 15 | 16 | will add a default metric named `zio_log_total` with the label `level` which will be 17 | incremented for each log message with the value of `level` being the corresponding log level label in lower case. 18 | 19 | Metrics: 20 | 21 | * `LogLevel.All` -> `all` 22 | * `LogLevel.Fatal` -> `fatal` 23 | * `LogLevel.Error` -> `error` 24 | * `LogLevel.Warning` -> `warn` 25 | * `LogLevel.Info` -> `info` 26 | * `LogLevel.Debug` -> `debug` 27 | * `LogLevel.Trace` -> `trace` 28 | * `LogLevel.None` -> `off` 29 | 30 | Custom names for the metric and label can be set via: 31 | 32 | ```scala 33 | val layer = zio.logging.logMetricsWith("log_counter", "log_level") 34 | ``` 35 | 36 | ## Examples 37 | 38 | You can find the source 39 | code [here](https://github.com/zio/zio-logging/tree/master/examples) 40 | 41 | ### Console logger with metrics 42 | 43 | [//]: # (TODO: make snippet type-checked using mdoc) 44 | 45 | ```scala 46 | package zio.logging.example 47 | 48 | import zio.logging.{ consoleLogger, logMetrics } 49 | import zio.metrics.connectors.MetricsConfig 50 | import zio.metrics.connectors.prometheus.{ PrometheusPublisher, prometheusLayer, publisherLayer } 51 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer, _ } 52 | 53 | object MetricsApp extends ZIOAppDefault { 54 | 55 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 56 | Runtime.removeDefaultLoggers >>> (consoleLogger() ++ logMetrics) 57 | 58 | override def run: ZIO[Scope, Any, ExitCode] = 59 | (for { 60 | _ <- ZIO.logInfo("Start") 61 | _ <- ZIO.logWarning("Some warning") 62 | _ <- ZIO.logError("Some error") 63 | _ <- ZIO.logError("Another error") 64 | _ <- ZIO.sleep(1.second) 65 | metricValues <- ZIO.serviceWithZIO[PrometheusPublisher](_.get) 66 | _ <- Console.printLine(metricValues) 67 | _ <- ZIO.logInfo("Done") 68 | } yield ExitCode.success) 69 | .provideLayer((ZLayer.succeed(MetricsConfig(200.millis)) ++ publisherLayer) >+> prometheusLayer) 70 | 71 | } 72 | ``` 73 | 74 | Expected Console Output: 75 | 76 | ``` 77 | timestamp=2023-03-15T08:44:39.93193+01:00 level=INFO thread=zio-fiber-6 message="Start" 78 | timestamp=2023-03-15T08:44:39.951764+01:00 level=WARN thread=zio-fiber-6 message="Some warning" 79 | timestamp=2023-03-15T08:44:39.95388+01:00 level=ERROR thread=zio-fiber-6 message="Some error" 80 | timestamp=2023-03-15T08:44:39.954738+01:00 level=ERROR thread=zio-fiber-6 message="Another error" 81 | # TYPE zio_log_total counter 82 | # HELP zio_log_total 83 | zio_log_total{level="error",} 2.0 1678866280778 84 | # TYPE zio_log_total counter 85 | # HELP zio_log_total 86 | zio_log_total{level="warn",} 1.0 1678866280778 87 | # TYPE zio_log_total counter 88 | # HELP zio_log_total 89 | zio_log_total{level="info",} 1.0 1678866280778 90 | timestamp=2023-03-15T08:44:40.972877+01:00 level=INFO thread=zio-fiber-6 message="Done" 91 | ``` -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zio.dev/zio-logging", 3 | "description": "ZIO Logging Documentation", 4 | "license": "Apache-2.0" 5 | } 6 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | const sidebars = { 2 | sidebar: [ 3 | { 4 | type: "category", 5 | label: "ZIO Logging", 6 | link: { type: "doc", id: "index" }, 7 | items: [ 8 | 'formatting-log-records', 9 | 'logger-context-and-annotations', 10 | 'log-filter', 11 | 'console-logger', 12 | 'file-logger', 13 | 'reconfigurable-logger', 14 | 'jpl', 15 | 'jul-bridge', 16 | 'slf4j2', 17 | 'slf4j1', 18 | 'slf4j2-bridge', 19 | 'slf4j1-bridge', 20 | 'metrics', 21 | 'testing' 22 | ] 23 | } 24 | ] 25 | }; 26 | 27 | module.exports = sidebars; 28 | -------------------------------------------------------------------------------- /docs/slf4j2.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: slf4j2 3 | title: "SLF4J v2" 4 | --- 5 | 6 | The Simple Logging Facade for Java ([`SLF4J v2`](https://www.slf4j.org/) - using JDK9+ module system [JPMS](http://openjdk.java.net/projects/jigsaw/spec/)) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j). 7 | 8 | In order to use this logging backend, we need to add the following line in our build.sbt file: 9 | 10 | ```scala 11 | libraryDependencies += "dev.zio" %% "zio-logging-slf4j2" % "@VERSION@" 12 | ``` 13 | 14 | >**_NOTE:_** SLF4J v2 implementation is similar to [v1](slf4j1.md), 15 | however there are some differences, v1 using [MDC context](https://logback.qos.ch/manual/mdc.html), working with JDK8, 16 | v2 using [key-value pairs](https://www.slf4j.org/manual.html#fluent), working with JDK9+. 17 | 18 | Logger layer: 19 | 20 | ```scala 21 | import zio.logging.backend.SLF4J 22 | 23 | val logger = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 24 | ``` 25 | 26 | Default `SLF4J` logger setup: 27 | * logger name (by default) is extracted from `zio.Trace` 28 | * for example, trace `zio.logging.example.Slf4jSimpleApp.run(Slf4jSimpleApp.scala:17)` will have `zio.logging.example.Slf4jSimpleApp` as logger name 29 | * NOTE: custom logger name may be set by `zio.logging.loggerName` aspect 30 | * all annotations (logger name and log marker name annotations are excluded) are passed like [key-value pairs](https://www.slf4j.org/manual.html#fluent) 31 | * cause is logged as throwable 32 | 33 | See also [LogFormat and LogAppender](formatting-log-records.md#logformat-and-logappender) 34 | 35 | Custom logger name set by aspect: 36 | 37 | ```scala 38 | ZIO.logInfo("Starting user operation") @@ zio.logging.loggerName("zio.logging.example.UserOperation") 39 | ``` 40 | 41 | Log marker name set by aspect: 42 | 43 | ```scala 44 | ZIO.logInfo("Confidential user operation") @@ SLF4J.logMarkerName("CONFIDENTIAL") 45 | ``` 46 | 47 | 48 | ## Examples 49 | 50 | You can find the source code [here](https://github.com/zio/zio-logging/tree/master/examples) 51 | 52 | 53 | ### SLF4J logger name and annotations 54 | 55 | [//]: # (TODO: make snippet type-checked using mdoc) 56 | 57 | ```scala 58 | package zio.logging.example 59 | 60 | import zio.logging.LogAnnotation 61 | import zio.logging.backend.SLF4J 62 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 63 | 64 | import java.util.UUID 65 | 66 | object Slf4jSimpleApp extends ZIOAppDefault { 67 | 68 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 69 | 70 | private val users = List.fill(2)(UUID.randomUUID()) 71 | 72 | override def run: ZIO[Scope, Any, ExitCode] = 73 | for { 74 | _ <- ZIO.logInfo("Start") 75 | traceId <- ZIO.succeed(UUID.randomUUID()) 76 | _ <- ZIO.foreachPar(users) { uId => 77 | { 78 | ZIO.logInfo("Starting user operation") *> 79 | ZIO.logInfo("Confidential user operation") @@ SLF4J.logMarkerName("CONFIDENTIAL") *> 80 | ZIO.sleep(500.millis) *> 81 | ZIO.logInfo("Stopping user operation") 82 | } @@ ZIOAspect.annotated("user", uId.toString) 83 | } @@ LogAnnotation.TraceId(traceId) @@ zio.logging.loggerName("zio.logging.example.UserOperation") 84 | _ <- ZIO.logInfo("Done") 85 | } yield ExitCode.success 86 | 87 | } 88 | ``` 89 | 90 | Logback configuration: 91 | 92 | ```xml 93 | 94 | 95 | 96 | 97 | %d{HH:mm:ss.SSS} [%thread] [%kvp] %-5level %logger{36} %msg%n 98 | 99 | 100 | 101 | CONFIDENTIAL_FILTER 102 | CONFIDENTIAL 103 | DENY 104 | 105 | 106 | 107 | 108 | 109 | ``` 110 | 111 | Expected Console Output: 112 | ``` 113 | 12:13:17.495 [ZScheduler-Worker-8] [] INFO zio.logging.example.Slf4j2SimpleApp Start 114 | 12:13:17.601 [ZScheduler-Worker-11] [trace_id="7826dd28-7e37-4b54-b84d-05399bb6cbeb" user="7b6a1af5-bad2-4846-aa49-138d31b40a24"] INFO zio.logging.example.UserOperation Starting user operation 115 | 12:13:17.601 [ZScheduler-Worker-10] [trace_id="7826dd28-7e37-4b54-b84d-05399bb6cbeb" user="4df9cdbc-e770-4bc9-b884-756e671a6435"] INFO zio.logging.example.UserOperation Starting user operation 116 | 12:13:18.167 [ZScheduler-Worker-13] [trace_id="7826dd28-7e37-4b54-b84d-05399bb6cbeb" user="7b6a1af5-bad2-4846-aa49-138d31b40a24"] INFO zio.logging.example.UserOperation Stopping user operation 117 | 12:13:18.167 [ZScheduler-Worker-15] [trace_id="7826dd28-7e37-4b54-b84d-05399bb6cbeb" user="4df9cdbc-e770-4bc9-b884-756e671a6435"] INFO zio.logging.example.UserOperation Stopping user operation 118 | 12:13:18.173 [ZScheduler-Worker-13] [] INFO zio.logging.example.Slf4j2SimpleApp Done 119 | ``` -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: testing 3 | title: "Testing" 4 | --- 5 | 6 | ZIO 2 test library has test logger implementation for testing: 7 | 8 | ```scala 9 | libraryDependencies += "dev.zio" %% "zio-test" % ZioVersion % Test 10 | ``` 11 | 12 | Test logger layer: 13 | 14 | ```scala 15 | zio.test.ZTestLogger.default 16 | ``` 17 | 18 | You can find the source code of examples [here](https://github.com/zio/zio-logging/tree/master/examples/src/test/scala/zio/logging/example) 19 | 20 | Test example: 21 | 22 | [//]: # (TODO: make snippet type-checked using mdoc) 23 | 24 | ```scala 25 | package zio.logging.example 26 | 27 | import zio.logging.{ LogAnnotation, logContext } 28 | import zio.test.Assertion._ 29 | import zio.test._ 30 | import zio.{ Chunk, LogLevel, Runtime, ZIO, ZIOAspect, _ } 31 | 32 | import java.util.UUID 33 | 34 | object LoggingSpec extends ZIOSpecDefault { 35 | 36 | override def spec: Spec[TestEnvironment, Any] = suite("LoggingSpec")( 37 | test("start stop log output") { 38 | val users = Chunk.fill(2)(UUID.randomUUID()) 39 | for { 40 | traceId <- ZIO.succeed(UUID.randomUUID()) 41 | _ <- ZIO.foreach(users) { uId => 42 | { 43 | ZIO.logInfo("Starting operation") *> ZIO.sleep(500.millis) *> ZIO.logInfo("Stopping operation") 44 | } @@ ZIOAspect.annotated("user", uId.toString) 45 | } @@ LogAnnotation.TraceId(traceId) 46 | _ <- ZIO.logInfo("Done") 47 | loggerOutput <- ZTestLogger.logOutput 48 | } yield assertTrue(loggerOutput.size == 5) && assertTrue( 49 | loggerOutput.forall(_.logLevel == LogLevel.Info) 50 | ) && assert(loggerOutput.map(_.message()))( 51 | equalTo( 52 | Chunk( 53 | "Starting operation", 54 | "Stopping operation", 55 | "Starting operation", 56 | "Stopping operation", 57 | "Done" 58 | ) 59 | ) 60 | ) && assert(loggerOutput.map(_.context.get(logContext).flatMap(_.asMap.get(LogAnnotation.TraceId.name))))( 61 | equalTo( 62 | Chunk.fill(4)(Some(traceId.toString)) :+ None 63 | ) 64 | ) && assert(loggerOutput.map(_.annotations.get("user")))( 65 | equalTo(users.flatMap(u => Chunk.fill(2)(Some(u.toString))) :+ None) 66 | ) 67 | } 68 | ).provideLayer( 69 | Runtime.removeDefaultLoggers >>> ZTestLogger.default 70 | ) @@ TestAspect.withLiveClock 71 | } 72 | ``` -------------------------------------------------------------------------------- /examples/core/jvm/src/main/resources/logger.conf: -------------------------------------------------------------------------------- 1 | logger { 2 | format = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %kv{trace_id} %kv{user_id} %cause}" 3 | filter { 4 | rootLevel = "INFO" 5 | mappings { 6 | "zio.logging.example" = "DEBUG" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/api/http/ApiDomain.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.api.http 17 | 18 | import zio.LogLevel 19 | import zio.logging.LoggerConfigurer 20 | import zio.schema.{ DeriveSchema, Schema } 21 | 22 | object ApiDomain { 23 | 24 | sealed trait Error { 25 | def message: String 26 | } 27 | 28 | object Error { 29 | final case class NotFound(message: String = "Not Found") extends Error 30 | 31 | final case class Internal(message: String = "Internal") extends Error 32 | 33 | implicit val notFoundSchema: Schema[Error.NotFound] = DeriveSchema.gen[Error.NotFound] 34 | implicit val internalSchema: Schema[Error.Internal] = DeriveSchema.gen[Error.Internal] 35 | implicit val schema: Schema[Error] = DeriveSchema.gen[Error] 36 | } 37 | 38 | implicit val logLevelSchema: Schema[LogLevel] = { 39 | val levelToLabel: Map[LogLevel, String] = LogLevel.levels.map(level => (level, level.label)).toMap 40 | val labelToLevel: Map[String, LogLevel] = levelToLabel.map(_.swap) 41 | 42 | Schema[String] 43 | .transformOrFail[LogLevel](v => labelToLevel.get(v).toRight("Failed"), v => levelToLabel.get(v).toRight("Failed")) 44 | } 45 | 46 | final case class LoggerConfig(name: String, level: LogLevel) 47 | 48 | object LoggerConfig { 49 | implicit val schema: Schema[LoggerConfig] = DeriveSchema.gen[LoggerConfig] 50 | 51 | def from(value: LoggerConfigurer.LoggerConfig): LoggerConfig = 52 | LoggerConfig(value.name, value.level) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/api/http/ApiEndpoints.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.api.http 17 | 18 | import zio._ 19 | import zio.http._ 20 | import zio.http.codec.PathCodec.{ literal, string } 21 | import zio.http.codec.{ HttpCodec, PathCodec } 22 | import zio.http.endpoint.AuthType.None 23 | import zio.http.endpoint._ 24 | import zio.http.endpoint.openapi.{ OpenAPI, OpenAPIGen } 25 | import zio.logging.api.http.ApiDomain.Error 26 | 27 | object ApiEndpoints { 28 | import ApiDomain.logLevelSchema 29 | 30 | def rootPathCodec(rootPath: Seq[String]): PathCodec[Unit] = 31 | if (rootPath.isEmpty) { 32 | PathCodec.empty 33 | } else { 34 | rootPath.map(literal).reduce(_ / _) 35 | } 36 | 37 | def getLoggerConfigs( 38 | rootPath: Seq[String] = Seq.empty 39 | ): Endpoint[Unit, Unit, Error.Internal, List[ApiDomain.LoggerConfig], None] = 40 | Endpoint(Method.GET / rootPathCodec(rootPath) / literal("logger")) 41 | .out[List[ApiDomain.LoggerConfig]] 42 | .outError[ApiDomain.Error.Internal](Status.InternalServerError) 43 | 44 | def getLoggerConfig( 45 | rootPath: Seq[String] = Seq.empty 46 | ): Endpoint[String, String, Error, ApiDomain.LoggerConfig, None] = 47 | Endpoint(Method.GET / rootPathCodec(rootPath) / literal("logger") / string("name")) 48 | .out[ApiDomain.LoggerConfig] 49 | .outErrors[ApiDomain.Error]( 50 | HttpCodec.error[ApiDomain.Error.Internal](Status.InternalServerError), 51 | HttpCodec.error[ApiDomain.Error.NotFound](Status.NotFound) 52 | ) 53 | 54 | def setLoggerConfig( 55 | rootPath: Seq[String] = Seq.empty 56 | ): Endpoint[String, (String, LogLevel), Error.Internal, ApiDomain.LoggerConfig, None] = 57 | Endpoint(Method.PUT / rootPathCodec(rootPath) / literal("logger") / string("name")) 58 | .in[LogLevel] 59 | .out[ApiDomain.LoggerConfig] 60 | .outError[ApiDomain.Error.Internal](Status.InternalServerError) 61 | 62 | def openAPI(rootPath: Seq[String] = Seq.empty): OpenAPI = 63 | OpenAPIGen.fromEndpoints( 64 | title = "Logger Configurations API", 65 | version = "1.0", 66 | getLoggerConfigs(rootPath), 67 | getLoggerConfig(rootPath), 68 | setLoggerConfig(rootPath) 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/api/http/ApiHandlers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.api.http 17 | 18 | import zio.ZIO 19 | import zio.http.{ Route, Routes } 20 | import zio.logging.LoggerConfigurer 21 | import zio.logging.api.http.ApiDomain.Error 22 | 23 | object ApiHandlers { 24 | 25 | def getLoggerConfigs(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = 26 | ApiEndpoints 27 | .getLoggerConfigs(rootPath) 28 | .implement { _ => 29 | LoggerConfigurer 30 | .getLoggerConfigs() 31 | .map(_.map(ApiDomain.LoggerConfig.from)) 32 | .mapError(_ => Error.Internal()) 33 | 34 | } 35 | 36 | def getLoggerConfig(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = 37 | ApiEndpoints 38 | .getLoggerConfig(rootPath) 39 | .implement { name => 40 | LoggerConfigurer.getLoggerConfig(name).mapError(_ => Error.Internal()).flatMap { 41 | case Some(r) => ZIO.succeed(ApiDomain.LoggerConfig.from(r)) 42 | case _ => ZIO.fail(Error.NotFound()) 43 | } 44 | } 45 | 46 | def setLoggerConfigs(rootPath: Seq[String] = Seq.empty): Route[LoggerConfigurer, Nothing] = 47 | ApiEndpoints 48 | .setLoggerConfig(rootPath) 49 | .implement { case (name, logLevel) => 50 | LoggerConfigurer 51 | .setLoggerConfig(name, logLevel) 52 | .map(ApiDomain.LoggerConfig.from) 53 | .mapError(_ => Error.Internal()) 54 | } 55 | 56 | def routes(rootPath: Seq[String] = Seq.empty): Routes[LoggerConfigurer, Nothing] = 57 | Routes(getLoggerConfigs(rootPath), getLoggerConfig(rootPath), setLoggerConfigs(rootPath)) 58 | } 59 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/ConfigurableLoggerApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import com.typesafe.config.ConfigFactory 19 | import zio.config.typesafe.TypesafeConfigProvider 20 | import zio.http._ 21 | import zio.logging.api.http.ApiHandlers 22 | import zio.logging.{ 23 | ConfigurableLogger, 24 | ConsoleLoggerConfig, 25 | LogAnnotation, 26 | LoggerConfigurer, 27 | ZLoggerZIOLayerOps, 28 | makeConsoleLogger 29 | } 30 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 31 | 32 | import java.util.UUID 33 | 34 | /* 35 | curl -u "admin:admin" 'http://localhost:8080/example/logger' 36 | 37 | curl -u "admin:admin" 'http://localhost:8080/example/logger/root' 38 | 39 | curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/root' --header 'Content-Type: application/json' --data-raw '"WARN"' 40 | 41 | curl -u "admin:admin" --location --request PUT 'http://localhost:8080/example/logger/zio.logging.example' --header 'Content-Type: application/json' --data-raw '"WARN"' 42 | 43 | */ 44 | object ConfigurableLoggerApp extends ZIOAppDefault { 45 | 46 | def configurableLogger() = 47 | ConsoleLoggerConfig 48 | .load() 49 | .flatMap { config => 50 | makeConsoleLogger(config).map { logger => 51 | ConfigurableLogger.make(logger, config.filter) 52 | } 53 | } 54 | .install 55 | 56 | val configProvider: ConfigProvider = TypesafeConfigProvider.fromTypesafeConfig(ConfigFactory.load("logger.conf")) 57 | 58 | override val bootstrap: ZLayer[Any, Config.Error, Unit] = 59 | Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> configurableLogger() 60 | 61 | def exec(): ZIO[Any, Nothing, Unit] = 62 | for { 63 | ok <- Random.nextBoolean 64 | traceId <- ZIO.succeed(UUID.randomUUID()) 65 | _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) 66 | userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) 67 | _ <- ZIO.foreachPar(userIds) { userId => 68 | { 69 | ZIO.logDebug("Starting operation") *> 70 | ZIO.logInfo("OK operation").when(ok) *> 71 | ZIO.logError("Error operation").when(!ok) *> 72 | ZIO.logDebug("Stopping operation") 73 | } @@ LogAnnotation.UserId(userId) 74 | } @@ LogAnnotation.TraceId(traceId) 75 | _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) 76 | } yield () 77 | 78 | val httpApp: Routes[LoggerConfigurer with Any, Nothing] = 79 | ApiHandlers.routes("example" :: Nil) @@ Middleware.basicAuth("admin", "admin") 80 | 81 | override def run: ZIO[Scope, Any, ExitCode] = 82 | (for { 83 | _ <- Server.serve(httpApp).fork 84 | _ <- exec().repeat(Schedule.fixed(500.millis)) 85 | } yield ExitCode.success).provide(LoggerConfigurer.layer ++ Server.default) 86 | 87 | } 88 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/ConsoleColoredApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.config.typesafe.TypesafeConfigProvider 19 | import zio.logging.consoleLogger 20 | import zio.{ Cause, Config, ConfigProvider, ExitCode, Runtime, Scope, URIO, ZIO, ZIOAppDefault, ZLayer } 21 | 22 | object ConsoleColoredApp extends ZIOAppDefault { 23 | 24 | val configString: String = 25 | s""" 26 | |logger { 27 | | 28 | | format = "%highlight{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause}" 29 | | 30 | | filter { 31 | | mappings { 32 | | "zio.logging.example.LivePingService" = "DEBUG" 33 | | } 34 | | } 35 | |} 36 | |""".stripMargin 37 | 38 | val configProvider: ConfigProvider = TypesafeConfigProvider.fromHoconString(configString) 39 | 40 | override val bootstrap: ZLayer[Any, Config.Error, Unit] = 41 | Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> consoleLogger() 42 | 43 | private def ping(address: String): URIO[PingService, Unit] = 44 | PingService 45 | .ping(address) 46 | .foldZIO( 47 | e => ZIO.logErrorCause(s"ping: $address - error", Cause.fail(e)), 48 | r => ZIO.logInfo(s"ping: $address - result: $r") 49 | ) 50 | 51 | override def run: ZIO[Scope, Any, ExitCode] = 52 | (for { 53 | _ <- ping("127.0.0.1") 54 | _ <- ping("x8.8.8.8") 55 | } yield ExitCode.success).provide(LivePingService.layer) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/ConsoleJsonApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.{ LogAnnotation, consoleJsonLogger } 19 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 20 | 21 | import java.util.UUID 22 | 23 | object ConsoleJsonApp extends ZIOAppDefault { 24 | 25 | final case class User(firstName: String, lastName: String) { 26 | def toJson: String = s"""{"first_name":"$firstName","last_name":"$lastName"}""".stripMargin 27 | } 28 | 29 | private val userLogAnnotation = LogAnnotation[User]("user", (_, u) => u, _.toJson) 30 | private val uuid = LogAnnotation[UUID]("uuid", (_, i) => i, _.toString) 31 | 32 | val logFormat = 33 | "%label{timestamp}{%timestamp{yyyy-MM-dd'T'HH:mm:ssZ}} %label{level}{%level} %label{fiberId}{%fiberId} %label{message}{%message} %label{cause}{%cause} %label{name}{%name} %kvs" 34 | 35 | val configProvider: ConfigProvider = ConfigProvider.fromMap( 36 | Map( 37 | "logger/format" -> logFormat, 38 | "logger/filter/rootLevel" -> LogLevel.Info.label 39 | ), 40 | "/" 41 | ) 42 | 43 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 44 | Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> consoleJsonLogger() 45 | 46 | private val uuids = List.fill(2)(UUID.randomUUID()) 47 | 48 | override def run: ZIO[Scope, Any, ExitCode] = 49 | for { 50 | traceId <- ZIO.succeed(UUID.randomUUID()) 51 | _ <- ZIO.foreachPar(uuids) { uId => 52 | { 53 | ZIO.logInfo("Starting operation") *> 54 | ZIO.sleep(500.millis) *> 55 | ZIO.logInfo("Stopping operation") 56 | } @@ userLogAnnotation(User("John", "Doe")) @@ uuid(uId) 57 | } @@ LogAnnotation.TraceId(traceId) 58 | _ <- ZIO.logInfo("Done") 59 | } yield ExitCode.success 60 | 61 | } 62 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/FileApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.config.typesafe.TypesafeConfigProvider 19 | import zio.logging.fileLogger 20 | import zio.{ Config, ConfigProvider, ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, ZLayer } 21 | 22 | object FileApp extends ZIOAppDefault { 23 | 24 | val configString: String = 25 | s""" 26 | |logger { 27 | | format = "%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause" 28 | | path = "file:///tmp/file_app.log" 29 | | rollingPolicy { 30 | | type = TimeBasedRollingPolicy 31 | | } 32 | |} 33 | |""".stripMargin 34 | 35 | val configProvider: ConfigProvider = TypesafeConfigProvider.fromHoconString(configString) 36 | 37 | override val bootstrap: ZLayer[Any, Config.Error, Unit] = 38 | Runtime.removeDefaultLoggers >>> Runtime.setConfigProvider(configProvider) >>> fileLogger() 39 | 40 | override def run: ZIO[Scope, Any, ExitCode] = 41 | for { 42 | _ <- ZIO.logInfo("Start") 43 | _ <- ZIO.fail("FAILURE") 44 | _ <- ZIO.logInfo("Done") 45 | } yield ExitCode.success 46 | } 47 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/LoggerReconfigureApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import com.typesafe.config.ConfigFactory 19 | import zio.config.typesafe.TypesafeConfigProvider 20 | import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, ReconfigurableLogger, _ } 21 | import zio.{ Config, ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 22 | 23 | import java.util.UUID 24 | 25 | object LoggerReconfigureApp extends ZIOAppDefault { 26 | 27 | def configuredLogger( 28 | loadConfig: => ZIO[Any, Config.Error, ConsoleLoggerConfig] 29 | ): ZLayer[Any, Config.Error, Unit] = 30 | ReconfigurableLogger 31 | .make[Any, Config.Error, String, Any, ConsoleLoggerConfig]( 32 | loadConfig, 33 | (config, _) => makeConsoleLogger(config), 34 | Schedule.fixed(500.millis) 35 | ) 36 | .installUnscoped 37 | 38 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 39 | Runtime.removeDefaultLoggers >>> configuredLogger( 40 | for { 41 | config <- ZIO.succeed(ConfigFactory.load("logger.conf")) 42 | _ <- Console.printLine(config.getConfig("logger")).orDie 43 | loggerConfig <- ConsoleLoggerConfig.load().withConfigProvider(TypesafeConfigProvider.fromTypesafeConfig(config)) 44 | } yield loggerConfig 45 | ) 46 | 47 | def exec(): ZIO[Any, Nothing, Unit] = 48 | for { 49 | ok <- Random.nextBoolean 50 | traceId <- ZIO.succeed(UUID.randomUUID()) 51 | _ <- ZIO.logDebug("Start") @@ LogAnnotation.TraceId(traceId) 52 | userIds <- ZIO.succeed(List.fill(2)(UUID.randomUUID().toString)) 53 | _ <- ZIO.foreachPar(userIds) { userId => 54 | { 55 | ZIO.logDebug("Starting operation") *> 56 | ZIO.logInfo("OK operation").when(ok) *> 57 | ZIO.logError("Error operation").when(!ok) *> 58 | ZIO.logDebug("Stopping operation") 59 | } @@ LogAnnotation.UserId(userId) 60 | } @@ LogAnnotation.TraceId(traceId) 61 | _ <- ZIO.logDebug("Done") @@ LogAnnotation.TraceId(traceId) 62 | } yield () 63 | 64 | override def run: ZIO[Scope, Any, ExitCode] = 65 | for { 66 | _ <- exec().repeat(Schedule.fixed(500.millis)) 67 | } yield ExitCode.success 68 | 69 | } 70 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/MetricsApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.{ consoleLogger, logMetrics } 19 | import zio.metrics.connectors.MetricsConfig 20 | import zio.metrics.connectors.prometheus.{ PrometheusPublisher, prometheusLayer, publisherLayer } 21 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer, _ } 22 | 23 | object MetricsApp extends ZIOAppDefault { 24 | 25 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 26 | Runtime.removeDefaultLoggers >>> (consoleLogger() ++ logMetrics) 27 | 28 | override def run: ZIO[Scope, Any, ExitCode] = 29 | (for { 30 | _ <- ZIO.logInfo("Start") 31 | _ <- ZIO.logWarning("Some warning") 32 | _ <- ZIO.logError("Some error") 33 | _ <- ZIO.logError("Another error") 34 | _ <- ZIO.sleep(1.second) 35 | metricValues <- ZIO.serviceWithZIO[PrometheusPublisher](_.get) 36 | _ <- Console.printLine(metricValues) 37 | _ <- ZIO.logInfo("Done") 38 | } yield ExitCode.success) 39 | .provideLayer((ZLayer.succeed(MetricsConfig(200.millis)) ++ publisherLayer) >+> prometheusLayer) 40 | 41 | } 42 | -------------------------------------------------------------------------------- /examples/core/jvm/src/main/scala/zio/logging/example/PingService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.{ Task, ULayer, ZIO, ZLayer } 19 | 20 | import java.net.InetAddress 21 | 22 | trait PingService { 23 | def ping(address: String): Task[Boolean] 24 | } 25 | 26 | object PingService { 27 | def ping(address: String): ZIO[PingService, Throwable, Boolean] = ZIO.serviceWithZIO[PingService](_.ping(address)) 28 | } 29 | 30 | final class LivePingService extends PingService { 31 | override def ping(address: String): Task[Boolean] = 32 | for { 33 | inetAddress <- 34 | ZIO 35 | .attempt(InetAddress.getByName(address)) 36 | .tapErrorCause(error => ZIO.logErrorCause(s"ping: $address - invalid address error", error)) 37 | _ <- ZIO.logDebug(s"ping: $inetAddress") 38 | result <- ZIO.attempt(inetAddress.isReachable(10000)) 39 | } yield result 40 | } 41 | 42 | object LivePingService { 43 | val layer: ULayer[PingService] = ZLayer.succeed(new LivePingService) 44 | } 45 | -------------------------------------------------------------------------------- /examples/core/jvm/src/test/scala/zio/logging/api/http/ApiEndpointsSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.api.http 2 | 3 | import zio.http.codec.PathCodec.literal 4 | import zio.http.codec._ 5 | import zio.test._ 6 | import zio.{ LogLevel, Scope } 7 | 8 | object ApiEndpointsSpec extends ZIOSpecDefault { 9 | 10 | def spec: Spec[Environment with TestEnvironment with Scope, Any] = suite("ApiEndpointsSpec")( 11 | test("rootPathCodec") { 12 | def testRootPathCodec(rootPath: Seq[String], expected: PathCodec[Unit]) = 13 | assertTrue( 14 | ApiEndpoints.rootPathCodec(rootPath).encode(()).getOrElse(zio.http.Path.empty) == expected 15 | .encode(()) 16 | .getOrElse(zio.http.Path.empty) 17 | ) 18 | 19 | testRootPathCodec(Nil, PathCodec.empty) && testRootPathCodec( 20 | "example" :: Nil, 21 | literal("example") 22 | ) && testRootPathCodec("v1" :: "example" :: Nil, literal("v1") / literal("example")) 23 | }, 24 | test("getLoggerConfigs") { 25 | 26 | def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = 27 | assertTrue( 28 | ApiEndpoints.getLoggerConfigs(rootPath).input.encodeRequest(()).path == expected 29 | .encode(()) 30 | .getOrElse(zio.http.Path.empty) 31 | ) 32 | 33 | testPath(Nil, literal("logger")) && testPath( 34 | "example" :: Nil, 35 | literal("example") / literal("logger") 36 | ) 37 | }, 38 | test("getLoggerConfig") { 39 | 40 | def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = 41 | assertTrue( 42 | ApiEndpoints.getLoggerConfig(rootPath).input.encodeRequest("my-logger").path == expected 43 | .encode(()) 44 | .getOrElse(zio.http.Path.empty) 45 | ) 46 | 47 | testPath(Nil, literal("logger") / literal("my-logger")) && testPath( 48 | "example" :: Nil, 49 | literal("example") / literal("logger") / literal("my-logger") 50 | ) 51 | }, 52 | test("setLoggerConfigs") { 53 | 54 | def testPath(rootPath: Seq[String], expected: PathCodec[Unit]) = 55 | assertTrue( 56 | ApiEndpoints 57 | .setLoggerConfig(rootPath) 58 | .input 59 | .encodeRequest(("my-logger", LogLevel.Info)) 60 | .path == expected 61 | .encode(()) 62 | .getOrElse(zio.http.Path.empty) 63 | ) 64 | 65 | testPath(Nil, literal("logger") / literal("my-logger")) && testPath( 66 | "example" :: Nil, 67 | literal("example") / literal("logger") / literal("my-logger") 68 | ) 69 | } 70 | ) 71 | 72 | } 73 | -------------------------------------------------------------------------------- /examples/core/jvm/src/test/scala/zio/logging/api/http/ApiHandlersSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.api.http 2 | 3 | import zio.http._ 4 | import zio.http.codec._ 5 | import zio.logging.LoggerConfigurer 6 | import zio.test._ 7 | import zio.{ LogLevel, Scope, ULayer, ZIO, ZLayer } 8 | 9 | object ApiHandlersSpec extends ZIOSpecDefault { 10 | 11 | val loggerConfigurer: ULayer[LoggerConfigurer] = ZLayer.succeed { 12 | new LoggerConfigurer { 13 | override def getLoggerConfigs(): ZIO[Any, Throwable, List[LoggerConfigurer.LoggerConfig]] = 14 | ZIO.succeed(LoggerConfigurer.LoggerConfig("root", LogLevel.Info) :: Nil) 15 | 16 | override def getLoggerConfig( 17 | name: String 18 | ): ZIO[Any, Throwable, Option[LoggerConfigurer.LoggerConfig]] = 19 | ZIO.succeed(Some(LoggerConfigurer.LoggerConfig(name, LogLevel.Info))) 20 | 21 | override def setLoggerConfig( 22 | name: String, 23 | level: LogLevel 24 | ): ZIO[Any, Throwable, LoggerConfigurer.LoggerConfig] = 25 | ZIO.succeed(LoggerConfigurer.LoggerConfig(name, level)) 26 | } 27 | } 28 | 29 | def spec: Spec[TestEnvironment with Scope, Any] = suite("ApiHandlersSpec")( 30 | test("get all") { 31 | val routes = ApiHandlers.routes("example" :: Nil) 32 | 33 | for { 34 | request <- ZIO.attempt(Request.get(URL.decode("/example/logger").toOption.get)) 35 | response <- routes.runZIO(request) 36 | content <- HttpCodec.content[List[ApiDomain.LoggerConfig]].decodeResponse(response) 37 | } yield assertTrue(response.status.isSuccess) && assertTrue( 38 | content == List(ApiDomain.LoggerConfig("root", LogLevel.Info)) 39 | ) 40 | }, 41 | test("get") { 42 | val routes = ApiHandlers.routes("example" :: Nil) 43 | for { 44 | request <- ZIO.attempt(Request.get(URL.decode("/example/logger/example.Service").toOption.get)) 45 | response <- routes.runZIO(request) 46 | content <- HttpCodec.content[ApiDomain.LoggerConfig].decodeResponse(response) 47 | } yield assertTrue(response.status.isSuccess) && assertTrue( 48 | content == ApiDomain.LoggerConfig("example.Service", LogLevel.Info) 49 | ) 50 | }, 51 | test("set") { 52 | import ApiDomain.logLevelSchema 53 | val routes = ApiHandlers.routes("example" :: Nil) 54 | for { 55 | request <- ZIO.attempt( 56 | Request 57 | .put( 58 | URL.decode("/example/logger/example.Service").toOption.get, 59 | HttpCodec.content[LogLevel].encodeRequest(LogLevel.Warning).body 60 | ) 61 | ) 62 | response <- routes.runZIO(request) 63 | content <- HttpCodec.content[ApiDomain.LoggerConfig].decodeResponse(response) 64 | } yield assertTrue(response.status.isSuccess) && assertTrue( 65 | content == ApiDomain.LoggerConfig("example.Service", LogLevel.Warning) 66 | ) 67 | } 68 | ).provideSomeLayer[Scope](loggerConfigurer) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /examples/core/jvm/src/test/scala/zio/logging/example/PingServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.test.{ Spec, TestAspect, TestEnvironment, ZIOSpecDefault, ZTestLogger, assertTrue } 19 | import zio.{ LogLevel, Runtime } 20 | 21 | import java.net.UnknownHostException 22 | 23 | object PingServiceSpec extends ZIOSpecDefault { 24 | 25 | override def spec: Spec[TestEnvironment, Any] = suite("PingServiceSpec")( 26 | test("ping with valid address") { 27 | for { 28 | pingResult <- PingService.ping("127.0.0.1") 29 | loggerOutput <- ZTestLogger.logOutput 30 | } yield assertTrue(pingResult) && assertTrue(loggerOutput.size == 1) && assertTrue( 31 | loggerOutput(0).logLevel == LogLevel.Debug && loggerOutput(0).message() == "ping: /127.0.0.1" 32 | ) 33 | }, 34 | test("fail on invalid address") { 35 | for { 36 | pingResult <- PingService.ping("x8.8.8.8").either 37 | loggerOutput <- ZTestLogger.logOutput 38 | } yield assertTrue( 39 | pingResult.isLeft && pingResult.fold(_.isInstanceOf[UnknownHostException], _ => false) 40 | ) && assertTrue(loggerOutput.size == 1) && assertTrue( 41 | loggerOutput(0).logLevel == LogLevel.Error && loggerOutput(0) 42 | .message() == "ping: x8.8.8.8 - invalid address error" && loggerOutput(0).cause.failureOption.exists( 43 | _.isInstanceOf[UnknownHostException] 44 | ) 45 | ) 46 | } 47 | ).provideLayer( 48 | LivePingService.layer ++ (Runtime.removeDefaultLoggers >>> ZTestLogger.default) 49 | ) @@ TestAspect.sequential 50 | } 51 | -------------------------------------------------------------------------------- /examples/core/shared/src/main/scala/zio/logging/example/SimpleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio._ 19 | import zio.logging.{ ConsoleLoggerConfig, consoleLogger } 20 | 21 | object SimpleApp extends ZIOAppDefault { 22 | 23 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 24 | Runtime.removeDefaultLoggers >>> consoleLogger(ConsoleLoggerConfig.default) 25 | 26 | override def run: ZIO[Scope, Any, ExitCode] = 27 | for { 28 | _ <- ZIO.logInfo("Start") 29 | _ <- ZIO.logError("FAILURE") 30 | _ <- ZIO.logInfo("Done") 31 | } yield ExitCode.success 32 | 33 | } 34 | -------------------------------------------------------------------------------- /examples/core/shared/src/test/scala/zio/logging/example/LoggingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.{ LogAnnotation, logContext } 19 | import zio.test.Assertion._ 20 | import zio.test._ 21 | import zio.{ Chunk, LogLevel, Runtime, ZIO, ZIOAspect, _ } 22 | 23 | import java.util.UUID 24 | 25 | object LoggingSpec extends ZIOSpecDefault { 26 | 27 | override def spec: Spec[TestEnvironment, Any] = suite("LoggingSpec")( 28 | test("start stop log output") { 29 | val users = Chunk.fill(2)(UUID.randomUUID()) 30 | for { 31 | traceId <- ZIO.succeed(UUID.randomUUID()) 32 | _ <- ZIO.foreach(users) { uId => 33 | { 34 | ZIO.logInfo("Starting operation") *> ZIO.sleep(500.millis) *> ZIO.logInfo("Stopping operation") 35 | } @@ ZIOAspect.annotated("user", uId.toString) 36 | } @@ LogAnnotation.TraceId(traceId) 37 | _ <- ZIO.logInfo("Done") 38 | loggerOutput <- ZTestLogger.logOutput 39 | } yield assertTrue(loggerOutput.size == 5) && assertTrue( 40 | loggerOutput.forall(_.logLevel == LogLevel.Info) 41 | ) && assert(loggerOutput.map(_.message()))( 42 | equalTo( 43 | Chunk( 44 | "Starting operation", 45 | "Stopping operation", 46 | "Starting operation", 47 | "Stopping operation", 48 | "Done" 49 | ) 50 | ) 51 | ) && assert(loggerOutput.map(_.context.get(logContext).flatMap(_.asMap.get(LogAnnotation.TraceId.name))))( 52 | equalTo( 53 | Chunk.fill(4)(Some(traceId.toString)) :+ None 54 | ) 55 | ) && assert(loggerOutput.map(_.annotations.get("user")))( 56 | equalTo(users.flatMap(u => Chunk.fill(2)(Some(u.toString))) :+ None) 57 | ) 58 | } 59 | ).provideLayer( 60 | Runtime.removeDefaultLoggers >>> ZTestLogger.default 61 | ) @@ TestAspect.withLiveClock 62 | } 63 | -------------------------------------------------------------------------------- /examples/jpl/src/main/scala/zio/logging/example/JplSimpleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.LogAnnotation 19 | import zio.logging.backend.JPL 20 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 21 | 22 | import java.util.UUID 23 | 24 | object JplSimpleApp extends ZIOAppDefault { 25 | 26 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> JPL.jpl 27 | 28 | private val users = List.fill(2)(UUID.randomUUID()) 29 | 30 | override def run: ZIO[Scope, Any, ExitCode] = 31 | for { 32 | _ <- ZIO.logInfo("Start") 33 | traceId <- ZIO.succeed(UUID.randomUUID()) 34 | _ <- ZIO.foreachPar(users) { uId => 35 | { 36 | ZIO.logInfo("Starting user operation") *> 37 | ZIO.sleep(500.millis) *> 38 | ZIO.logInfo("Stopping user operation") 39 | } @@ ZIOAspect.annotated("user", uId.toString) 40 | } @@ LogAnnotation.TraceId(traceId) @@ zio.logging.loggerName("zio.logging.example.UserOperation") 41 | _ <- ZIO.logInfo("Done") 42 | } yield ExitCode.success 43 | 44 | } 45 | -------------------------------------------------------------------------------- /examples/jul-bridge/src/main/scala/zio/logging/example/JULBridgeExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.jul.bridge.JULBridge 19 | import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } 20 | import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 21 | 22 | import java.util.UUID 23 | 24 | object JULBridgeExampleApp extends ZIOAppDefault { 25 | 26 | private val julLogger = java.util.logging.Logger.getLogger("JUL-LOGGER") 27 | 28 | private val logFilterConfig = LogFilter.LogLevelByNameConfig( 29 | LogLevel.Info, 30 | "zio.logging.slf4j" -> LogLevel.Debug, 31 | "SLF4J-LOGGER" -> LogLevel.Warning 32 | ) 33 | 34 | private val logFormat = LogFormat.label( 35 | "name", 36 | LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() 37 | ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( 38 | LogAnnotation.TraceId 39 | ) + LogFormat.default 40 | 41 | private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) 42 | 43 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 44 | Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> JULBridge.init(loggerConfig.toFilter) 45 | 46 | private val uuids = List.fill(2)(UUID.randomUUID()) 47 | 48 | override def run: ZIO[Scope, Any, ExitCode] = 49 | for { 50 | _ <- ZIO.logInfo("Start") 51 | _ <- ZIO.foreachPar(uuids) { u => 52 | ZIO.succeed(julLogger.info("Test INFO!")) *> ZIO.succeed( 53 | julLogger.warning("Test WARNING!") 54 | ) @@ LogAnnotation.UserId( 55 | u.toString 56 | ) 57 | } @@ LogAnnotation.TraceId(UUID.randomUUID()) 58 | _ <- ZIO.logDebug("Done") 59 | } yield ExitCode.success 60 | 61 | } 62 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] [%mdc] %-5level %logger{36} %msg%n 6 | 7 | 8 | 9 | CONFIDENTIAL_FILTER 10 | CONFIDENTIAL 11 | DENY 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/scala/zio/logging/example/CustomTracingAnnotationApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.backend.SLF4J 19 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 20 | 21 | import java.util.UUID 22 | 23 | trait Tracing { 24 | def getCurrentSpan(): UIO[String] 25 | } 26 | 27 | final class LiveTracing extends Tracing { 28 | override def getCurrentSpan(): UIO[String] = ZIO.succeed(UUID.randomUUID().toString) 29 | } 30 | 31 | object LiveTracing { 32 | val layer: ULayer[Tracing] = ZLayer.succeed(new LiveTracing) 33 | } 34 | 35 | object CustomTracingAnnotationApp extends ZIOAppDefault { 36 | 37 | private def currentTracingSpanAspect(key: String): ZIOAspect[Nothing, Tracing, Nothing, Any, Nothing, Any] = 38 | new ZIOAspect[Nothing, Tracing, Nothing, Any, Nothing, Any] { 39 | def apply[R <: Tracing, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] = 40 | ZIO.serviceWithZIO[Tracing] { tracing => 41 | tracing.getCurrentSpan().flatMap { span => 42 | ZIO.logAnnotate(key, span)(zio) 43 | } 44 | } 45 | } 46 | 47 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 48 | 49 | private val users = List.fill(2)(UUID.randomUUID()) 50 | 51 | override def run: ZIO[Scope, Any, ExitCode] = 52 | (for { 53 | _ <- ZIO.foreachPar(users) { uId => 54 | { 55 | ZIO.logInfo("Starting operation") *> 56 | ZIO.sleep(500.millis) *> 57 | ZIO.logInfo("Stopping operation") 58 | } @@ ZIOAspect.annotated("user", uId.toString) 59 | } @@ currentTracingSpanAspect("trace_id") 60 | _ <- ZIO.logInfo("Done") 61 | } yield ExitCode.success).provide(LiveTracing.layer) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/scala/zio/logging/example/PingService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.{ Task, ULayer, ZIO, ZLayer } 19 | 20 | import java.net.InetAddress 21 | 22 | trait PingService { 23 | def ping(address: String): Task[Boolean] 24 | } 25 | 26 | object PingService { 27 | def ping(address: String): ZIO[PingService, Throwable, Boolean] = ZIO.serviceWithZIO[PingService](_.ping(address)) 28 | } 29 | 30 | final class LivePingService extends PingService { 31 | override def ping(address: String): Task[Boolean] = 32 | for { 33 | inetAddress <- 34 | ZIO 35 | .attempt(InetAddress.getByName(address)) 36 | .tapErrorCause(error => ZIO.logErrorCause(s"ping: $address - invalid address error", error)) 37 | _ <- ZIO.logDebug(s"ping: $inetAddress") 38 | result <- ZIO.attempt(inetAddress.isReachable(10000)) 39 | } yield result 40 | } 41 | 42 | object LivePingService { 43 | val layer: ULayer[PingService] = ZLayer.succeed(new LivePingService) 44 | } 45 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/scala/zio/logging/example/Slf4jExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.backend.SLF4J 19 | import zio.{ ExitCode, Runtime, Scope, URIO, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 20 | 21 | object Slf4jExampleApp extends ZIOAppDefault { 22 | 23 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 24 | 25 | private def ping(address: String): URIO[PingService, Unit] = 26 | PingService 27 | .ping(address) 28 | .tap(result => ZIO.logInfo(s"ping: $address - result: $result")) 29 | .tapErrorCause(error => ZIO.logErrorCause(s"ping: $address - error", error)) 30 | .unit 31 | .ignore 32 | 33 | override def run: ZIO[Scope, Any, ExitCode] = 34 | (for { 35 | _ <- ping("127.0.0.1") 36 | _ <- ping("x8.8.8.8") 37 | } yield ExitCode.success).provide(LivePingService.layer) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/scala/zio/logging/example/Slf4jFailureApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.backend.SLF4J 19 | import zio.{ Runtime, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 20 | 21 | object Slf4jFailureApp extends ZIOAppDefault { 22 | 23 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 24 | 25 | def run: ZIO[Any, Throwable, Unit] = 26 | ZIO.dieMessage("die") <&> ZIO.unit 27 | } 28 | -------------------------------------------------------------------------------- /examples/slf4j-logback/src/main/scala/zio/logging/example/Slf4jSimpleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.LogAnnotation 19 | import zio.logging.backend.SLF4J 20 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 21 | 22 | import java.util.UUID 23 | 24 | object Slf4jSimpleApp extends ZIOAppDefault { 25 | 26 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 27 | 28 | private val users = List.fill(2)(UUID.randomUUID()) 29 | 30 | override def run: ZIO[Scope, Any, ExitCode] = 31 | for { 32 | _ <- ZIO.logInfo("Start") 33 | traceId <- ZIO.succeed(UUID.randomUUID()) 34 | _ <- ZIO.foreachPar(users) { uId => 35 | { 36 | ZIO.logInfo("Starting user operation") *> 37 | ZIO.logInfo("Confidential user operation") @@ SLF4J.logMarkerName("CONFIDENTIAL") *> 38 | ZIO.sleep(500.millis) *> 39 | ZIO.logInfo("Stopping user operation") 40 | } @@ ZIOAspect.annotated("user", uId.toString) 41 | } @@ LogAnnotation.TraceId(traceId) @@ zio.logging.loggerName("zio.logging.example.UserOperation") 42 | _ <- ZIO.logInfo("Done") 43 | } yield ExitCode.success 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/slf4j2-bridge/src/main/scala/zio/logging/example/Slf4jBridgeExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.slf4j.bridge.Slf4jBridge 19 | import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } 20 | import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 21 | 22 | import java.util.UUID 23 | 24 | object Slf4jBridgeExampleApp extends ZIOAppDefault { 25 | 26 | private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") 27 | 28 | private val logFilterConfig = LogFilter.LogLevelByNameConfig( 29 | LogLevel.Info, 30 | "zio.logging.slf4j" -> LogLevel.Debug, 31 | "SLF4J-LOGGER" -> LogLevel.Warning 32 | ) 33 | 34 | private val logFormat = LogFormat.label( 35 | "name", 36 | LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() 37 | ) + LogFormat.allAnnotations(Set(zio.logging.loggerNameAnnotationKey)) + LogFormat.default 38 | 39 | private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) 40 | 41 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 42 | Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> Slf4jBridge.init(loggerConfig.toFilter) 43 | 44 | private val uuids = List.fill(2)(UUID.randomUUID()) 45 | 46 | override def run: ZIO[Scope, Any, ExitCode] = 47 | for { 48 | _ <- ZIO.logInfo("Start") 49 | _ <- ZIO.foreachPar(uuids) { u => 50 | ZIO.succeed(slf4jLogger.info("Test {}!", "INFO")) *> ZIO.succeed( 51 | slf4jLogger.atWarn().addArgument("WARNING").log("Test {}!") 52 | ) @@ LogAnnotation.UserId( 53 | u.toString 54 | ) 55 | } @@ LogAnnotation.TraceId(UUID.randomUUID()) 56 | _ <- ZIO.logDebug("Done") 57 | } yield ExitCode.success 58 | 59 | } 60 | -------------------------------------------------------------------------------- /examples/slf4j2-log4j/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/slf4j2-log4j/src/main/scala/zio/logging/example/Slf4jSimpleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.LogAnnotation 19 | import zio.logging.backend.SLF4J 20 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 21 | 22 | import java.util.UUID 23 | 24 | object Slf4jSimpleApp extends ZIOAppDefault { 25 | 26 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 27 | 28 | private val users = List.fill(2)(UUID.randomUUID()) 29 | 30 | override def run: ZIO[Scope, Any, ExitCode] = 31 | for { 32 | _ <- ZIO.logInfo("Start") 33 | traceId <- ZIO.succeed(UUID.randomUUID()) 34 | _ <- ZIO.foreachPar(users) { uId => 35 | { 36 | ZIO.logInfo("Starting user operation") *> 37 | ZIO.logInfo("Confidential user operation") @@ SLF4J.logMarkerName("CONFIDENTIAL") *> 38 | ZIO.sleep(500.millis) *> 39 | ZIO.logInfo("Stopping user operation") 40 | } @@ ZIOAspect.annotated("user", uId.toString) 41 | } @@ LogAnnotation.TraceId(traceId) @@ logging.loggerName("zio.logging.example.UserOperation") 42 | _ <- ZIO.logInfo("Done") 43 | } yield ExitCode.success 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/slf4j2-logback/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] [%kvp] %-5level %logger{36} %msg%n 6 | 7 | 8 | 9 | CONFIDENTIAL_FILTER 10 | CONFIDENTIAL 11 | DENY 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/slf4j2-logback/src/main/scala/zio/logging/example/Slf4j2SimpleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.example 17 | 18 | import zio.logging.LogAnnotation 19 | import zio.logging.backend.SLF4J 20 | import zio.{ ExitCode, Runtime, Scope, ZIO, ZIOAppDefault, _ } 21 | 22 | import java.util.UUID 23 | 24 | object Slf4j2SimpleApp extends ZIOAppDefault { 25 | 26 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j 27 | 28 | private val users = List.fill(2)(UUID.randomUUID()) 29 | 30 | override def run: ZIO[Scope, Any, ExitCode] = 31 | for { 32 | _ <- ZIO.logInfo("Start") 33 | traceId <- ZIO.succeed(UUID.randomUUID()) 34 | _ <- ZIO.foreachPar(users) { uId => 35 | { 36 | ZIO.logInfo("Starting user operation") *> 37 | ZIO.logInfo("Confidential user operation") @@ SLF4J.logMarkerName("CONFIDENTIAL") *> 38 | ZIO.sleep(500.millis) *> 39 | ZIO.logInfo("Stopping user operation") 40 | } @@ ZIOAspect.annotated("user", uId.toString) 41 | } @@ LogAnnotation.TraceId(traceId) @@ zio.logging.loggerName("zio.logging.example.UserOperation") 42 | _ <- ZIO.logInfo("Done") 43 | } yield ExitCode.success 44 | 45 | } 46 | -------------------------------------------------------------------------------- /jpl/src/test/scala/zio/logging/backend/TestAppender.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.backend 2 | 3 | import zio.{ Chunk, LogLevel } 4 | 5 | import java.util.concurrent.atomic.AtomicReference 6 | import scala.annotation.tailrec 7 | 8 | object TestAppender { 9 | 10 | val logLevelMapping: Map[System.Logger.Level, LogLevel] = Map( 11 | System.Logger.Level.ALL -> LogLevel.All, 12 | System.Logger.Level.TRACE -> LogLevel.Trace, 13 | System.Logger.Level.DEBUG -> LogLevel.Debug, 14 | System.Logger.Level.INFO -> LogLevel.Info, 15 | System.Logger.Level.WARNING -> LogLevel.Warning, 16 | System.Logger.Level.ERROR -> LogLevel.Error, 17 | System.Logger.Level.OFF -> LogLevel.None 18 | ) 19 | 20 | final case class LogEntry( 21 | loggerName: String, 22 | threadName: String, 23 | logLevel: LogLevel, 24 | message: String, 25 | timestamp: Long, 26 | cause: Option[Throwable] 27 | ) 28 | 29 | private val logEntriesRef: AtomicReference[Chunk[LogEntry]] = new AtomicReference[Chunk[LogEntry]](Chunk.empty) 30 | 31 | private[backend] def appendLogEntry(event: LogEntry): Unit = { 32 | 33 | @tailrec 34 | def append(entry: LogEntry): Unit = { 35 | val old = logEntriesRef.get() 36 | if (logEntriesRef.compareAndSet(old, old :+ entry)) () 37 | else append(entry) 38 | } 39 | 40 | append(event) 41 | } 42 | 43 | def reset(): Unit = logEntriesRef.set(Chunk.empty) 44 | 45 | def logOutput: Chunk[LogEntry] = logEntriesRef.get() 46 | 47 | } 48 | -------------------------------------------------------------------------------- /jpl/src/test/scala/zio/logging/backend/TestJPLogger.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.backend 2 | 3 | import zio.logging.backend.TestAppender.{ LogEntry, logLevelMapping } 4 | 5 | import java.text.MessageFormat 6 | import java.util.ResourceBundle 7 | import java.util.concurrent.ConcurrentHashMap 8 | import scala.util.Try 9 | 10 | class TestJPLogger(val name: String) extends System.Logger { 11 | override def getName: String = name 12 | 13 | override def isLoggable(level: System.Logger.Level): Boolean = 14 | level.getSeverity >= System.Logger.Level.INFO.getSeverity 15 | 16 | private def getString(bundle: ResourceBundle, key: String): String = 17 | Option(bundle).flatMap(b => Try(b.getString(key)).toOption).getOrElse(key) 18 | 19 | override def log(level: System.Logger.Level, bundle: ResourceBundle, msg: String, thrown: Throwable): Unit = { 20 | if (isLoggable(level)) { 21 | val entry = LogEntry( 22 | name, 23 | Thread.currentThread().getName, 24 | logLevelMapping(level), 25 | getString(bundle, msg), 26 | System.currentTimeMillis(), 27 | Option(thrown) 28 | ) 29 | 30 | TestAppender.appendLogEntry(entry) 31 | } 32 | () 33 | } 34 | 35 | override def log(level: System.Logger.Level, bundle: ResourceBundle, format: String, params: AnyRef*): Unit = { 36 | if (isLoggable(level)) { 37 | val entry = LogEntry( 38 | name, 39 | Thread.currentThread().getName, 40 | logLevelMapping(level), 41 | MessageFormat.format(getString(bundle, format), params), 42 | System.currentTimeMillis(), 43 | None 44 | ) 45 | 46 | TestAppender.appendLogEntry(entry) 47 | } 48 | () 49 | } 50 | 51 | } 52 | 53 | object TestJPLoggerSystem { 54 | private val loggers = new ConcurrentHashMap[String, TestJPLogger]() 55 | 56 | def getLogger(name: String): System.Logger = 57 | loggers.computeIfAbsent(name, n => new TestJPLogger(n)) 58 | } 59 | -------------------------------------------------------------------------------- /jul-bridge/src/main/scala/zio/logging/jul/bridge/JULBridge.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.jul.bridge 17 | 18 | import zio.logging.LogFilter 19 | import zio.{ Config, NonEmptyChunk, Runtime, Semaphore, Unsafe, ZIO, ZLayer } 20 | 21 | import java.io.ByteArrayInputStream 22 | import java.util.logging.LogManager 23 | 24 | object JULBridge { 25 | 26 | val logFilterConfigPath: NonEmptyChunk[String] = zio.logging.loggerConfigPath :+ "filter" 27 | 28 | /** 29 | * initialize java.util.Logging bridge 30 | */ 31 | def initialize: ZLayer[Any, Nothing, Unit] = init(LogFilter.acceptAll) 32 | 33 | /** 34 | * initialize java.util.Logging bridge with `LogFilter` 35 | * 36 | * @param filter Log filter 37 | */ 38 | def init(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = Runtime.enableCurrentFiber ++ layer(filter) 39 | 40 | /** 41 | * initialize java.util.Logging bridge with `LogFilter` from configuration 42 | * @param configPath configuration path 43 | */ 44 | def init(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZLayer[Any, Config.Error, Unit] = 45 | Runtime.enableCurrentFiber ++ layer(configPath) 46 | 47 | /** 48 | * initialize java.util.Logging bridge without `FiberRef` propagation 49 | */ 50 | def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = initWithoutFiberRefPropagation( 51 | LogFilter.acceptAll 52 | ) 53 | 54 | /** 55 | * initialize java.util.Logging bridge with `LogFilter`, without `FiberRef` propagation 56 | * @param filter Log filter 57 | */ 58 | def initWithoutFiberRefPropagation(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = layer(filter) 59 | 60 | private val initLock = Semaphore.unsafe.make(1)(Unsafe.unsafe) 61 | 62 | private def layer(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = 63 | ZLayer(make(filter)) 64 | 65 | private def layer(configPath: NonEmptyChunk[String]): ZLayer[Any, Config.Error, Unit] = 66 | ZLayer(make(configPath)) 67 | 68 | def make(filter: LogFilter[Any]): ZIO[Any, Nothing, Unit] = 69 | for { 70 | runtime <- ZIO.runtime[Any] 71 | _ <- initLock.withPermit { 72 | ZIO.succeed { 73 | val configuration = ".level= ALL" 74 | LogManager.getLogManager.readConfiguration(new ByteArrayInputStream(configuration.getBytes)) 75 | 76 | java.util.logging.Logger 77 | .getLogger("") 78 | .addHandler(new ZioLoggerRuntime(runtime, filter)) 79 | } 80 | } 81 | } yield () 82 | 83 | def make(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZIO[Any, Config.Error, Unit] = 84 | LogFilter.LogLevelByNameConfig.load(configPath).flatMap(c => make(c.toFilter)) 85 | 86 | } 87 | -------------------------------------------------------------------------------- /jul-bridge/src/main/scala/zio/logging/jul/bridge/ZioLoggerRuntime.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.jul.bridge 17 | 18 | import zio.logging.LogFilter 19 | import zio.{ Cause, Fiber, FiberId, FiberRef, FiberRefs, LogLevel, Runtime, Trace, Unsafe } 20 | 21 | import java.util.logging.{ Handler, Level, LogRecord } 22 | 23 | final class ZioLoggerRuntime(runtime: Runtime[Any], filter: LogFilter[Any]) extends Handler { 24 | 25 | override def publish(record: LogRecord): Unit = { 26 | if (!isEnabled(record.getLoggerName, record.getLevel)) { 27 | return 28 | } 29 | 30 | Unsafe.unsafe { implicit u => 31 | val msg = record.getMessage 32 | val level = record.getLevel 33 | val name = record.getLoggerName 34 | val throwable = record.getThrown 35 | 36 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 37 | val trace = Trace.empty 38 | val fiberId = FiberId.Gen.Live.make(trace) 39 | val currentFiber = Fiber._currentFiber.get() 40 | 41 | val currentFiberRefs = if (currentFiber eq null) { 42 | runtime.fiberRefs.joinAs(fiberId)(FiberRefs.empty) 43 | } else { 44 | runtime.fiberRefs.joinAs(fiberId)(currentFiber.unsafe.getFiberRefs()) 45 | } 46 | 47 | val logSpan = zio.LogSpan(name, java.lang.System.currentTimeMillis()) 48 | val loggerName = (zio.logging.loggerNameAnnotationKey -> name) 49 | 50 | val fiberRefs = currentFiberRefs 51 | .updatedAs(fiberId)(FiberRef.currentLogSpan, logSpan :: currentFiberRefs.getOrDefault(FiberRef.currentLogSpan)) 52 | .updatedAs(fiberId)( 53 | FiberRef.currentLogAnnotations, 54 | currentFiberRefs.getOrDefault(FiberRef.currentLogAnnotations) + loggerName 55 | ) 56 | 57 | val fiberRuntime = zio.internal.FiberRuntime(fiberId, fiberRefs, runtime.runtimeFlags) 58 | 59 | val cause = if (throwable != null) { 60 | Cause.die(throwable) 61 | } else { 62 | Cause.empty 63 | } 64 | 65 | fiberRuntime.log(() => msg, cause, Some(logLevel), trace) 66 | 67 | } 68 | } 69 | 70 | override def flush(): Unit = () 71 | 72 | override def close(): Unit = () 73 | 74 | private def isEnabled(name: String, level: Level): Boolean = { 75 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 76 | 77 | filter( 78 | Trace(name, "", 0), 79 | FiberId.None, 80 | logLevel, 81 | () => "", 82 | Cause.empty, 83 | FiberRefs.empty, 84 | List.empty, 85 | Map(zio.logging.loggerNameAnnotationKey -> name) 86 | ) 87 | } 88 | } 89 | 90 | object ZioLoggerRuntime { 91 | 92 | private val logLevelMapping: Map[Level, LogLevel] = Map( 93 | Level.SEVERE -> LogLevel.Fatal, 94 | Level.WARNING -> LogLevel.Warning, 95 | Level.INFO -> LogLevel.Info, 96 | Level.CONFIG -> LogLevel.Info, 97 | Level.FINE -> LogLevel.Debug, 98 | Level.FINER -> LogLevel.Debug, 99 | Level.FINEST -> LogLevel.Trace, 100 | Level.ALL -> LogLevel.All, 101 | Level.OFF -> LogLevel.None 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /project/BuildHelper.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | 4 | object BuildHelper { 5 | def skipScala3Docs = Seq( 6 | Compile / doc / sources := { 7 | val old = (Compile / doc / sources).value 8 | if (scalaBinaryVersion.value == "3") { 9 | Nil 10 | } else { 11 | old 12 | } 13 | } 14 | ) 15 | 16 | def jpmsOverwriteModulePath(modulePaths: Seq[File])(options: Seq[String]): Seq[String] = { 17 | val modPathString = modulePaths.map(_.getAbsolutePath).mkString(java.io.File.pathSeparator) 18 | val option = "--module-path" 19 | val index = options.indexWhere(_ == option) 20 | if (index == -1) options ++ List(option, modPathString) 21 | else options.patch(index + 1, List(modPathString), 1) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /project/MimaSettings.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.tools.mima.core.ProblemFilters._ 2 | import com.typesafe.tools.mima.core._ 3 | import com.typesafe.tools.mima.plugin.MimaKeys._ 4 | import sbt.Keys.{ name, organization } 5 | import sbt._ 6 | 7 | object MimaSettings { 8 | lazy val bincompatVersionToCompare = "2.4.0" 9 | 10 | def mimaSettings(failOnProblem: Boolean) = 11 | Seq( 12 | mimaPreviousArtifacts := Set(organization.value %% name.value % bincompatVersionToCompare), 13 | mimaBinaryIssueFilters ++= Seq( 14 | exclude[Problem]("zio.logging.internal.*") 15 | ), 16 | mimaFailOnProblem := failOnProblem 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /project/Versions.scala: -------------------------------------------------------------------------------- 1 | object Versions { 2 | val slf4jVersion = "1.7.36" 3 | val slf4j2Version = "2.0.17" 4 | val logbackVersion = "1.2.13" 5 | val logback2Version = "1.4.12" 6 | val scalaCollectionCompatVersion = "2.13.0" 7 | val logstashLogbackEncoderVersion = "6.6" 8 | val scalaJavaTimeVersion = "2.6.0" 9 | val zioMetricsConnectorsVersion = "2.3.1" 10 | val zioConfig = "4.0.4" 11 | val zioParser = "0.1.11" 12 | val zioPrelude = "1.0.0-RC40" 13 | val zioHttp = "3.3.3" 14 | val log4jVersion = "2.24.3" 15 | } 16 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val zioSbtVersion = "0.4.0-alpha.31" 2 | 3 | addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % zioSbtVersion) 4 | addSbtPlugin("dev.zio" % "zio-sbt-website" % zioSbtVersion) 5 | addSbtPlugin("dev.zio" % "zio-sbt-ci" % zioSbtVersion) 6 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 7 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") 8 | 9 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 10 | 11 | resolvers ++= Resolver.sonatypeOssRepos("public") 12 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/java/org/slf4j/impl/StaticLoggerBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl; 17 | 18 | import org.slf4j.ILoggerFactory; 19 | import org.slf4j.spi.LoggerFactoryBinder; 20 | 21 | public class StaticLoggerBinder implements LoggerFactoryBinder { 22 | /** 23 | * The unique instance of this class. 24 | */ 25 | private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); 26 | 27 | /** 28 | * Return the singleton of this class. 29 | * 30 | * @return the StaticLoggerBinder singleton 31 | */ 32 | public static final StaticLoggerBinder getSingleton() { 33 | return SINGLETON; 34 | } 35 | 36 | /** 37 | * Declare the version of the SLF4J API this implementation is compiled against. 38 | * The value of this field is modified with each major release. 39 | */ 40 | // to avoid constant folding by the compiler, this field must *not* be final 41 | public static String REQUESTED_API_VERSION = "1.6.99"; // !final 42 | 43 | private static final String loggerFactoryClassStr = ZioLoggerFactory.class.getName(); 44 | 45 | /** 46 | * The ILoggerFactory instance returned by the {@link #getLoggerFactory} 47 | * method should always be the same object 48 | */ 49 | private final ILoggerFactory loggerFactory; 50 | 51 | private StaticLoggerBinder() { 52 | loggerFactory = new ZioLoggerFactory(); 53 | } 54 | 55 | public ILoggerFactory getLoggerFactory() { 56 | return loggerFactory; 57 | } 58 | 59 | public String getLoggerFactoryClassStr() { 60 | return loggerFactoryClassStr; 61 | } 62 | } -------------------------------------------------------------------------------- /slf4j-bridge/src/main/java/org/slf4j/impl/StaticMDCBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl; 17 | 18 | import org.slf4j.helpers.BasicMDCAdapter; 19 | import org.slf4j.spi.MDCAdapter; 20 | 21 | public class StaticMDCBinder { 22 | public static final StaticMDCBinder SINGLETON = new StaticMDCBinder(); 23 | 24 | private StaticMDCBinder() { 25 | } 26 | 27 | public MDCAdapter getMDCA() { 28 | return new BasicMDCAdapter(); 29 | } 30 | 31 | public String getMDCAdapterClassStr() { 32 | return BasicMDCAdapter.class.getName(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/org/slf4j/impl/LoggerRuntime.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl 17 | 18 | import org.slf4j.Marker 19 | import org.slf4j.event.Level 20 | import zio.logging.slf4j.bridge.LoggerData 21 | 22 | trait LoggerRuntime { 23 | 24 | def log( 25 | logger: LoggerData, 26 | level: Level, 27 | marker: Marker, 28 | messagePattern: String, 29 | arguments: Array[AnyRef], 30 | throwable: Throwable 31 | ): Unit 32 | 33 | def isEnabled(logger: LoggerData, level: Level): Boolean 34 | } 35 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/org/slf4j/impl/StaticMarkerBinder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl 17 | 18 | import org.slf4j.IMarkerFactory 19 | import org.slf4j.helpers.BasicMarkerFactory 20 | import org.slf4j.spi.MarkerFactoryBinder 21 | 22 | class StaticMarkerBinder extends MarkerFactoryBinder { 23 | private val markerFactory = new BasicMarkerFactory 24 | 25 | override def getMarkerFactory: IMarkerFactory = markerFactory 26 | override def getMarkerFactoryClassStr: String = classOf[BasicMarkerFactory].getName 27 | } 28 | 29 | object StaticMarkerBinder { 30 | private val singleton = new StaticMarkerBinder 31 | 32 | def getSingleton: StaticMarkerBinder = singleton 33 | } 34 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/org/slf4j/impl/ZioLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl 17 | 18 | import org.slf4j.Marker 19 | import org.slf4j.event.Level 20 | import org.slf4j.helpers.ZioLoggerBase 21 | import zio.logging.slf4j.bridge.LoggerData 22 | 23 | final class ZioLogger(name: String, factory: ZioLoggerFactory) extends ZioLoggerBase(name) { 24 | private val data: LoggerData = LoggerData(name) 25 | 26 | override protected def log( 27 | level: Level, 28 | marker: Marker, 29 | messagePattern: String, 30 | arguments: Array[AnyRef], 31 | throwable: Throwable 32 | ): Unit = 33 | factory.log(data, level, marker, messagePattern, arguments, throwable) 34 | 35 | override protected def isEnabled(level: Level): Boolean = factory.isEnabled(data, level) 36 | } 37 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/org/slf4j/impl/ZioLoggerFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.slf4j.impl 17 | 18 | import org.slf4j.event.Level 19 | import org.slf4j.{ ILoggerFactory, Logger, Marker } 20 | import zio.logging.slf4j.bridge.LoggerData 21 | 22 | import java.util.concurrent.ConcurrentHashMap 23 | import scala.jdk.CollectionConverters._ 24 | 25 | final class ZioLoggerFactory extends ILoggerFactory { 26 | private var runtime: LoggerRuntime = null 27 | private val loggers = new ConcurrentHashMap[String, Logger]().asScala 28 | 29 | private[impl] def attachRuntime(runtime: LoggerRuntime): Unit = 30 | this.runtime = runtime 31 | 32 | private[impl] def log( 33 | logger: LoggerData, 34 | level: Level, 35 | marker: Marker, 36 | messagePattern: String, 37 | arguments: Array[AnyRef], 38 | throwable: Throwable 39 | ): Unit = 40 | if (runtime != null) runtime.log(logger, level, marker, messagePattern, arguments, throwable) 41 | 42 | private[impl] def isEnabled(logger: LoggerData, level: Level): Boolean = 43 | if (runtime != null) runtime.isEnabled(logger, level) else false 44 | 45 | override def getLogger(name: String): Logger = 46 | loggers.getOrElseUpdate(name, new ZioLogger(name, this)) 47 | } 48 | 49 | object ZioLoggerFactory { 50 | 51 | def initialize(runtime: LoggerRuntime): Unit = { 52 | val factory = StaticLoggerBinder.getSingleton.getLoggerFactory 53 | .asInstanceOf[ZioLoggerFactory] 54 | 55 | factory.attachRuntime(runtime) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/LoggerData.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | final case class LoggerData(name: String) { 19 | 20 | lazy val annotations: Map[String, String] = Map(zio.logging.loggerNameAnnotationKey -> name) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import org.slf4j.impl.ZioLoggerFactory 19 | import zio.logging.LogFilter 20 | import zio.{ Config, NonEmptyChunk, Runtime, Semaphore, Unsafe, ZIO, ZLayer } 21 | 22 | object Slf4jBridge { 23 | 24 | val logFilterConfigPath: NonEmptyChunk[String] = zio.logging.loggerConfigPath :+ "filter" 25 | 26 | /** 27 | * initialize SLF4J bridge 28 | */ 29 | def initialize: ZLayer[Any, Nothing, Unit] = init(LogFilter.acceptAll) 30 | 31 | /** 32 | * initialize SLF4J bridge with `LogFilter` 33 | * @param filter Log filter 34 | */ 35 | def init(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = Runtime.enableCurrentFiber ++ layer(filter) 36 | 37 | /** 38 | * initialize SLF4J bridge with `LogFilter` from configuration 39 | * @param configPath configuration path 40 | */ 41 | def init(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZLayer[Any, Config.Error, Unit] = 42 | Runtime.enableCurrentFiber ++ layer(configPath) 43 | 44 | /** 45 | * initialize SLF4J bridge without `FiberRef` propagation 46 | */ 47 | def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = initWithoutFiberRefPropagation( 48 | LogFilter.acceptAll 49 | ) 50 | 51 | /** 52 | * initialize SLF4J bridge with `LogFilter`, without `FiberRef` propagation 53 | * @param filter Log filter 54 | */ 55 | def initWithoutFiberRefPropagation(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = layer(filter) 56 | 57 | private val initLock = Semaphore.unsafe.make(1)(Unsafe.unsafe) 58 | 59 | private def layer(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = 60 | ZLayer(make(filter)) 61 | 62 | private def layer(configPath: NonEmptyChunk[String]): ZLayer[Any, Config.Error, Unit] = 63 | ZLayer(make(configPath)) 64 | 65 | def make(filter: LogFilter[Any]): ZIO[Any, Nothing, Unit] = 66 | for { 67 | runtime <- ZIO.runtime[Any] 68 | _ <- initLock.withPermit { 69 | ZIO.succeed(ZioLoggerFactory.initialize(new ZioLoggerRuntime(runtime, filter))) 70 | } 71 | } yield () 72 | 73 | def make(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZIO[Any, Config.Error, Unit] = 74 | LogFilter.LogLevelByNameConfig.load(configPath).flatMap(c => make(c.toFilter)) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /slf4j-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import org.slf4j.Marker 19 | import org.slf4j.event.Level 20 | import org.slf4j.helpers.MessageFormatter 21 | import org.slf4j.impl.LoggerRuntime 22 | import zio.logging.LogFilter 23 | import zio.{ Cause, Fiber, FiberId, FiberRef, FiberRefs, LogLevel, Runtime, Trace, Unsafe } 24 | 25 | final class ZioLoggerRuntime(runtime: Runtime[Any], filter: LogFilter[Any]) extends LoggerRuntime { 26 | 27 | override def log( 28 | logger: LoggerData, 29 | level: Level, 30 | marker: Marker, 31 | messagePattern: String, 32 | arguments: Array[AnyRef], 33 | throwable: Throwable 34 | ): Unit = 35 | Unsafe.unsafe { implicit u => 36 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 37 | val trace = Trace.empty 38 | val fiberId = FiberId.Gen.Live.make(trace) 39 | val currentFiber = Fiber._currentFiber.get() 40 | 41 | val currentFiberRefs = if (currentFiber eq null) { 42 | runtime.fiberRefs.joinAs(fiberId)(FiberRefs.empty) 43 | } else { 44 | runtime.fiberRefs.joinAs(fiberId)(currentFiber.unsafe.getFiberRefs()) 45 | } 46 | 47 | val logSpan = zio.LogSpan(logger.name, java.lang.System.currentTimeMillis()) 48 | 49 | val fiberRefs = currentFiberRefs 50 | .updatedAs(fiberId)(FiberRef.currentLogSpan, logSpan :: currentFiberRefs.getOrDefault(FiberRef.currentLogSpan)) 51 | .updatedAs(fiberId)( 52 | FiberRef.currentLogAnnotations, 53 | currentFiberRefs.getOrDefault(FiberRef.currentLogAnnotations) ++ logger.annotations 54 | ) 55 | 56 | val fiberRuntime = zio.internal.FiberRuntime(fiberId, fiberRefs, runtime.runtimeFlags) 57 | 58 | lazy val msg = if (arguments != null) { 59 | MessageFormatter.arrayFormat(messagePattern, arguments.toArray).getMessage 60 | } else { 61 | messagePattern 62 | } 63 | 64 | val cause = if (throwable != null) { 65 | Cause.die(throwable) 66 | } else { 67 | Cause.empty 68 | } 69 | 70 | fiberRuntime.log(() => msg, cause, Some(logLevel), trace) 71 | } 72 | 73 | override def isEnabled(logger: LoggerData, level: Level): Boolean = { 74 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 75 | 76 | filter( 77 | Trace.empty, 78 | FiberId.None, 79 | logLevel, 80 | () => "", 81 | Cause.empty, 82 | FiberRefs.empty, 83 | List.empty, 84 | logger.annotations 85 | ) 86 | } 87 | 88 | } 89 | 90 | object ZioLoggerRuntime { 91 | 92 | val logLevelMapping: Map[Level, LogLevel] = Map( 93 | Level.TRACE -> LogLevel.Trace, 94 | Level.DEBUG -> LogLevel.Debug, 95 | Level.INFO -> LogLevel.Info, 96 | Level.WARN -> LogLevel.Warning, 97 | Level.ERROR -> LogLevel.Error 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /slf4j-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.slf4j.bridge 2 | 3 | import zio.logging._ 4 | import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 5 | 6 | import java.util.UUID 7 | 8 | object Slf4jBridgeExampleApp extends ZIOAppDefault { 9 | 10 | private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") 11 | 12 | private val logFilterConfig = LogFilter.LogLevelByNameConfig( 13 | LogLevel.Info, 14 | "zio.logging.slf4j" -> LogLevel.Debug, 15 | "SLF4J-LOGGER" -> LogLevel.Warning 16 | ) 17 | 18 | private val logFormat = LogFormat.label( 19 | "name", 20 | LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() 21 | ) + LogFormat.logAnnotation(LogAnnotation.UserId) + LogFormat.logAnnotation( 22 | LogAnnotation.TraceId 23 | ) + LogFormat.default 24 | 25 | private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) 26 | 27 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 28 | Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> Slf4jBridge.init(loggerConfig.toFilter) 29 | 30 | private val uuids = List.fill(2)(UUID.randomUUID()) 31 | 32 | override def run: ZIO[Scope, Any, ExitCode] = 33 | for { 34 | _ <- ZIO.logInfo("Start") 35 | _ <- ZIO.foreachPar(uuids) { u => 36 | ZIO.succeed(slf4jLogger.info("Test {}!", "INFO")) *> ZIO.succeed( 37 | slf4jLogger.warn("Test {}!", "WARNING") 38 | ) @@ LogAnnotation.UserId( 39 | u.toString 40 | ) 41 | } @@ LogAnnotation.TraceId(UUID.randomUUID()) 42 | _ <- ZIO.logDebug("Done") 43 | } yield ExitCode.success 44 | 45 | } 46 | -------------------------------------------------------------------------------- /slf4j/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | CONFIDENTIAL_FILTER 16 | CONFIDENTIAL 17 | DENY 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /slf4j/src/test/scala/zio/logging/backend/TestAppender.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.backend 2 | 3 | import ch.qos.logback.classic.Level 4 | import ch.qos.logback.classic.spi.{ ILoggingEvent, IThrowableProxy } 5 | import ch.qos.logback.core.AppenderBase 6 | import zio.{ Chunk, LogLevel } 7 | 8 | import java.util.concurrent.atomic.AtomicReference 9 | import scala.annotation.tailrec 10 | import scala.jdk.CollectionConverters._ 11 | 12 | class TestAppender extends AppenderBase[ILoggingEvent] { 13 | override def append(event: ILoggingEvent): Unit = 14 | TestAppender.appendLogEntry(event) 15 | } 16 | 17 | object TestAppender { 18 | 19 | val logLevelMapping: Map[Level, LogLevel] = Map( 20 | Level.ALL -> LogLevel.All, 21 | Level.TRACE -> LogLevel.Trace, 22 | Level.DEBUG -> LogLevel.Debug, 23 | Level.INFO -> LogLevel.Info, 24 | Level.WARN -> LogLevel.Warning, 25 | Level.ERROR -> LogLevel.Error, 26 | Level.OFF -> LogLevel.None 27 | ) 28 | 29 | final case class LogEntry( 30 | loggerName: String, 31 | threadName: String, 32 | logLevel: LogLevel, 33 | message: String, 34 | timestamp: Long, 35 | cause: Option[IThrowableProxy], 36 | mdc: Map[String, String] 37 | ) 38 | 39 | object LogEntry { 40 | def apply(event: ILoggingEvent): LogEntry = 41 | LogEntry( 42 | event.getLoggerName, 43 | event.getThreadName, 44 | logLevelMapping(event.getLevel), 45 | event.getMessage, 46 | event.getTimeStamp, 47 | Option(event.getThrowableProxy), 48 | event.getMDCPropertyMap.asScala.toMap 49 | ) 50 | } 51 | 52 | private val logEntriesRef: AtomicReference[Chunk[LogEntry]] = new AtomicReference[Chunk[LogEntry]](Chunk.empty) 53 | 54 | private def appendLogEntry(event: ILoggingEvent): Unit = { 55 | 56 | @tailrec 57 | def append(entry: LogEntry): Unit = { 58 | val old = logEntriesRef.get() 59 | if (logEntriesRef.compareAndSet(old, old :+ entry)) () 60 | else append(entry) 61 | } 62 | 63 | append(LogEntry(event)) 64 | } 65 | 66 | def reset(): Unit = logEntriesRef.set(Chunk.empty) 67 | 68 | def logOutput: Chunk[LogEntry] = logEntriesRef.get() 69 | 70 | } 71 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module org.slf4j.zio { 2 | requires org.slf4j; 3 | provides org.slf4j.spi.SLF4JServiceProvider with zio.logging.slf4j.bridge.ZioSLF4JServiceProvider; 4 | } -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/java/zio/logging/slf4j/bridge/ZioSLF4JServiceProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge; 17 | 18 | 19 | import org.slf4j.ILoggerFactory; 20 | import org.slf4j.IMarkerFactory; 21 | import org.slf4j.helpers.BasicMDCAdapter; 22 | import org.slf4j.helpers.BasicMarkerFactory; 23 | import org.slf4j.spi.MDCAdapter; 24 | 25 | final public class ZioSLF4JServiceProvider implements org.slf4j.spi.SLF4JServiceProvider { 26 | public static final String REQUESTED_API_VERSION = "2.0.99"; 27 | 28 | private ILoggerFactory loggerFactory; 29 | private IMarkerFactory markerFactory; 30 | private MDCAdapter mdcAdapter; 31 | 32 | @Override 33 | public ILoggerFactory getLoggerFactory() { 34 | return loggerFactory; 35 | } 36 | 37 | @Override 38 | public IMarkerFactory getMarkerFactory() { 39 | return markerFactory; 40 | } 41 | 42 | @Override 43 | public MDCAdapter getMDCAdapter() { 44 | return mdcAdapter; 45 | } 46 | 47 | @Override 48 | public String getRequestedApiVersion() { 49 | return REQUESTED_API_VERSION; 50 | } 51 | 52 | @Override 53 | public void initialize() { 54 | markerFactory = new BasicMarkerFactory(); 55 | loggerFactory = new ZioLoggerFactory(); 56 | mdcAdapter = new BasicMDCAdapter(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider: -------------------------------------------------------------------------------- 1 | zio.logging.slf4j.bridge.ZioSLF4JServiceProvider -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/LoggerData.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | final case class LoggerData(name: String) { 19 | 20 | lazy val annotations: Map[String, String] = Map(zio.logging.loggerNameAnnotationKey -> name) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/LoggerRuntime.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import org.slf4j.event.{ KeyValuePair, Level } 19 | 20 | trait LoggerRuntime { 21 | 22 | def log( 23 | logger: LoggerData, 24 | level: Level, 25 | messagePattern: String, 26 | arguments: Array[AnyRef], 27 | throwable: Throwable, 28 | keyValues: java.util.List[KeyValuePair] 29 | ): Unit 30 | 31 | def isEnabled(logger: LoggerData, level: Level): Boolean 32 | } 33 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/Slf4jBridge.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import zio.logging.LogFilter 19 | import zio.{ Config, NonEmptyChunk, Runtime, Semaphore, Unsafe, ZIO, ZLayer } 20 | 21 | object Slf4jBridge { 22 | 23 | val logFilterConfigPath: NonEmptyChunk[String] = zio.logging.loggerConfigPath :+ "filter" 24 | 25 | /** 26 | * initialize SLF4J bridge 27 | */ 28 | def initialize: ZLayer[Any, Nothing, Unit] = init(LogFilter.acceptAll) 29 | 30 | /** 31 | * initialize SLF4J bridge with `LogFilter` 32 | * @param filter Log filter 33 | */ 34 | def init(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = Runtime.enableCurrentFiber ++ layer(filter) 35 | 36 | /** 37 | * initialize SLF4J bridge with `LogFilter` from configuration 38 | * @param configPath configuration path 39 | */ 40 | def init(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZLayer[Any, Config.Error, Unit] = 41 | Runtime.enableCurrentFiber ++ layer(configPath) 42 | 43 | /** 44 | * initialize SLF4J bridge without `FiberRef` propagation 45 | */ 46 | def initializeWithoutFiberRefPropagation: ZLayer[Any, Nothing, Unit] = initWithoutFiberRefPropagation( 47 | LogFilter.acceptAll 48 | ) 49 | 50 | /** 51 | * initialize SLF4J bridge with `LogFilter`, without `FiberRef` propagation 52 | * @param filter Log filter 53 | */ 54 | def initWithoutFiberRefPropagation(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = layer(filter) 55 | 56 | private val initLock = Semaphore.unsafe.make(1)(Unsafe.unsafe) 57 | 58 | private def layer(filter: LogFilter[Any]): ZLayer[Any, Nothing, Unit] = 59 | ZLayer(make(filter)) 60 | 61 | private def layer(configPath: NonEmptyChunk[String]): ZLayer[Any, Config.Error, Unit] = 62 | ZLayer(make(configPath)) 63 | 64 | def make(filter: LogFilter[Any]): ZIO[Any, Nothing, Unit] = 65 | for { 66 | runtime <- ZIO.runtime[Any] 67 | _ <- initLock.withPermit { 68 | ZIO.succeed(ZioLoggerFactory.initialize(new ZioLoggerRuntime(runtime, filter))) 69 | } 70 | } yield () 71 | 72 | def make(configPath: NonEmptyChunk[String] = logFilterConfigPath): ZIO[Any, Config.Error, Unit] = 73 | LogFilter.LogLevelByNameConfig.load(configPath).flatMap(c => make(c.toFilter)) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLogger.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.slf4j.bridge 2 | 3 | import org.slf4j.Marker 4 | import org.slf4j.event.{ KeyValuePair, Level, LoggingEvent } 5 | import org.slf4j.helpers.AbstractLogger 6 | import org.slf4j.spi.LoggingEventAware 7 | 8 | import java.util.Collections; 9 | 10 | final class ZioLogger private[bridge] (name: String, factory: ZioLoggerFactory) 11 | extends AbstractLogger 12 | with LoggingEventAware { 13 | 14 | private val data: LoggerData = LoggerData(name) 15 | 16 | override def getName: String = name 17 | 18 | override protected def getFullyQualifiedCallerName: String = null 19 | 20 | override protected def handleNormalizedLoggingCall( 21 | level: Level, 22 | marker: Marker, 23 | messagePattern: String, 24 | arguments: Array[AnyRef], 25 | throwable: Throwable 26 | ): Unit = 27 | factory.log(data, level, messagePattern, arguments, throwable, Collections.emptyList[KeyValuePair]()) 28 | 29 | override def isTraceEnabled: Boolean = factory.isEnabled(data, Level.TRACE) 30 | 31 | override def isTraceEnabled(marker: Marker): Boolean = isTraceEnabled 32 | 33 | override def isDebugEnabled: Boolean = factory.isEnabled(data, Level.DEBUG) 34 | 35 | override def isDebugEnabled(marker: Marker): Boolean = isDebugEnabled 36 | 37 | override def isInfoEnabled: Boolean = factory.isEnabled(data, Level.INFO) 38 | 39 | override def isInfoEnabled(marker: Marker): Boolean = isInfoEnabled 40 | 41 | override def isWarnEnabled: Boolean = factory.isEnabled(data, Level.WARN) 42 | 43 | override def isWarnEnabled(marker: Marker): Boolean = isWarnEnabled 44 | 45 | override def isErrorEnabled: Boolean = factory.isEnabled(data, Level.ERROR) 46 | 47 | override def isErrorEnabled(marker: Marker): Boolean = isErrorEnabled 48 | 49 | override def log(event: LoggingEvent): Unit = 50 | if (factory.isEnabled(data, event.getLevel)) 51 | factory.log( 52 | data, 53 | event.getLevel, 54 | event.getMessage, 55 | event.getArgumentArray, 56 | event.getThrowable, 57 | event.getKeyValuePairs 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import org.slf4j.event.{ KeyValuePair, Level } 19 | import org.slf4j.{ ILoggerFactory, Logger } 20 | 21 | import java.util.concurrent.ConcurrentHashMap 22 | import scala.jdk.CollectionConverters._ 23 | 24 | final class ZioLoggerFactory extends ILoggerFactory { 25 | private var runtime: LoggerRuntime = null 26 | private val loggers = new ConcurrentHashMap[String, Logger]().asScala 27 | 28 | private[bridge] def attachRuntime(runtime: LoggerRuntime): Unit = 29 | this.runtime = runtime 30 | 31 | private[bridge] def log( 32 | logger: LoggerData, 33 | level: Level, 34 | messagePattern: String, 35 | arguments: Array[AnyRef], 36 | throwable: Throwable, 37 | keyValues: java.util.List[KeyValuePair] 38 | ): Unit = 39 | if (runtime != null) runtime.log(logger, level, messagePattern, arguments, throwable, keyValues) 40 | 41 | private[bridge] def isEnabled(logger: LoggerData, level: Level): Boolean = 42 | if (runtime != null) runtime.isEnabled(logger, level) else false 43 | 44 | override def getLogger(name: String): Logger = 45 | loggers.getOrElseUpdate(name, new ZioLogger(name, this)) 46 | } 47 | 48 | object ZioLoggerFactory { 49 | 50 | def initialize(runtime: LoggerRuntime): Unit = { 51 | val factory = org.slf4j.LoggerFactory 52 | .getILoggerFactory() 53 | .asInstanceOf[ZioLoggerFactory] 54 | 55 | factory.attachRuntime(runtime) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/main/scala/zio/logging/slf4j/bridge/ZioLoggerRuntime.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2024 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package zio.logging.slf4j.bridge 17 | 18 | import org.slf4j.event.{ KeyValuePair, Level } 19 | import org.slf4j.helpers.MessageFormatter 20 | import zio.logging.LogFilter 21 | import zio.{ Cause, Fiber, FiberId, FiberRef, FiberRefs, LogLevel, Runtime, Trace, Unsafe } 22 | 23 | import scala.jdk.CollectionConverters._ 24 | 25 | final class ZioLoggerRuntime(runtime: Runtime[Any], filter: LogFilter[Any]) extends LoggerRuntime { 26 | 27 | override def log( 28 | logger: LoggerData, 29 | level: Level, 30 | messagePattern: String, 31 | arguments: Array[AnyRef], 32 | throwable: Throwable, 33 | keyValues: java.util.List[KeyValuePair] 34 | ): Unit = 35 | Unsafe.unsafe { implicit u => 36 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 37 | val trace = Trace.empty 38 | val fiberId = FiberId.Gen.Live.make(trace) 39 | val currentFiber = Fiber._currentFiber.get() 40 | 41 | val currentFiberRefs = if (currentFiber eq null) { 42 | runtime.fiberRefs.joinAs(fiberId)(FiberRefs.empty) 43 | } else { 44 | runtime.fiberRefs.joinAs(fiberId)(currentFiber.unsafe.getFiberRefs()) 45 | } 46 | 47 | val logSpan = zio.LogSpan(logger.name, java.lang.System.currentTimeMillis()) 48 | 49 | val logAnnotations = if (keyValues != null && !keyValues.isEmpty) { 50 | keyValues.asScala.map(kv => (kv.key, kv.value.toString)).toMap ++ logger.annotations 51 | } else { 52 | logger.annotations 53 | } 54 | 55 | val fiberRefs = currentFiberRefs 56 | .updatedAs(fiberId)(FiberRef.currentLogSpan, logSpan :: currentFiberRefs.getOrDefault(FiberRef.currentLogSpan)) 57 | .updatedAs(fiberId)( 58 | FiberRef.currentLogAnnotations, 59 | currentFiberRefs.getOrDefault(FiberRef.currentLogAnnotations) ++ logAnnotations 60 | ) 61 | 62 | val fiberRuntime = zio.internal.FiberRuntime(fiberId, fiberRefs, runtime.runtimeFlags) 63 | 64 | lazy val msg = if (arguments != null) { 65 | MessageFormatter.arrayFormat(messagePattern, arguments.toArray).getMessage 66 | } else { 67 | messagePattern 68 | } 69 | 70 | val cause = if (throwable != null) { 71 | Cause.die(throwable) 72 | } else { 73 | Cause.empty 74 | } 75 | 76 | fiberRuntime.log(() => msg, cause, Some(logLevel), trace) 77 | } 78 | 79 | override def isEnabled(logger: LoggerData, level: Level): Boolean = { 80 | val logLevel = ZioLoggerRuntime.logLevelMapping(level) 81 | 82 | filter( 83 | Trace.empty, 84 | FiberId.None, 85 | logLevel, 86 | () => "", 87 | Cause.empty, 88 | FiberRefs.empty, 89 | List.empty, 90 | logger.annotations 91 | ) 92 | } 93 | 94 | } 95 | 96 | object ZioLoggerRuntime { 97 | 98 | val logLevelMapping: Map[Level, LogLevel] = Map( 99 | Level.TRACE -> LogLevel.Trace, 100 | Level.DEBUG -> LogLevel.Debug, 101 | Level.INFO -> LogLevel.Info, 102 | Level.WARN -> LogLevel.Warning, 103 | Level.ERROR -> LogLevel.Error 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /slf4j2-bridge/src/test/scala/zio/logging/slf4j/bridge/Slf4jBridgeExampleApp.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.slf4j.bridge 2 | 3 | import zio.logging.{ ConsoleLoggerConfig, LogAnnotation, LogFilter, LogFormat, LoggerNameExtractor, consoleJsonLogger } 4 | import zio.{ ExitCode, LogLevel, Runtime, Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer } 5 | 6 | import java.util.UUID 7 | 8 | object Slf4jBridgeExampleApp extends ZIOAppDefault { 9 | 10 | private val slf4jLogger = org.slf4j.LoggerFactory.getLogger("SLF4J-LOGGER") 11 | 12 | private val logFilterConfig = LogFilter.LogLevelByNameConfig( 13 | LogLevel.Info, 14 | "zio.logging.slf4j" -> LogLevel.Debug, 15 | "SLF4J-LOGGER" -> LogLevel.Warning 16 | ) 17 | 18 | private val logFormat = LogFormat.label( 19 | "name", 20 | LoggerNameExtractor.loggerNameAnnotationOrTrace.toLogFormat() 21 | ) + LogFormat.allAnnotations(Set(zio.logging.loggerNameAnnotationKey)) + LogFormat.default 22 | 23 | private val loggerConfig = ConsoleLoggerConfig(logFormat, logFilterConfig) 24 | 25 | override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = 26 | Runtime.removeDefaultLoggers >>> consoleJsonLogger(loggerConfig) >+> Slf4jBridge.init(loggerConfig.toFilter) 27 | 28 | private val uuids = List.fill(2)(UUID.randomUUID()) 29 | 30 | override def run: ZIO[Scope, Any, ExitCode] = 31 | for { 32 | _ <- ZIO.logInfo("Start") 33 | _ <- ZIO.foreachPar(uuids) { u => 34 | ZIO.succeed(slf4jLogger.info("Test {}!", "INFO")) *> ZIO.succeed( 35 | slf4jLogger.atWarn().addArgument("WARNING").log("Test {}!") 36 | ) @@ LogAnnotation.UserId( 37 | u.toString 38 | ) 39 | } @@ LogAnnotation.TraceId(UUID.randomUUID()) 40 | _ <- ZIO.logDebug("Done") 41 | } yield ExitCode.success 42 | 43 | } 44 | -------------------------------------------------------------------------------- /slf4j2/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | CONFIDENTIAL_FILTER 16 | CONFIDENTIAL 17 | DENY 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /slf4j2/src/test/scala/zio/logging/backend/TestAppender.scala: -------------------------------------------------------------------------------- 1 | package zio.logging.backend 2 | 3 | import ch.qos.logback.classic.Level 4 | import ch.qos.logback.classic.spi.{ ILoggingEvent, IThrowableProxy } 5 | import ch.qos.logback.core.AppenderBase 6 | import zio.{ Chunk, LogLevel } 7 | 8 | import java.util.concurrent.atomic.AtomicReference 9 | import scala.annotation.tailrec 10 | import scala.jdk.CollectionConverters._ 11 | 12 | class TestAppender extends AppenderBase[ILoggingEvent] { 13 | override def append(event: ILoggingEvent): Unit = 14 | TestAppender.appendLogEntry(event) 15 | } 16 | 17 | object TestAppender { 18 | 19 | val logLevelMapping: Map[Level, LogLevel] = Map( 20 | Level.ALL -> LogLevel.All, 21 | Level.TRACE -> LogLevel.Trace, 22 | Level.DEBUG -> LogLevel.Debug, 23 | Level.INFO -> LogLevel.Info, 24 | Level.WARN -> LogLevel.Warning, 25 | Level.ERROR -> LogLevel.Error, 26 | Level.OFF -> LogLevel.None 27 | ) 28 | 29 | final case class LogEntry( 30 | loggerName: String, 31 | threadName: String, 32 | logLevel: LogLevel, 33 | message: String, 34 | timestamp: Long, 35 | cause: Option[IThrowableProxy], 36 | keyValues: List[(String, String)] 37 | ) 38 | 39 | object LogEntry { 40 | def apply(event: ILoggingEvent): LogEntry = { 41 | val keyValues = if (event.getKeyValuePairs != null) { 42 | event.getKeyValuePairs.asScala.map(kv => (kv.key, kv.value.toString)).toList 43 | } else List.empty[(String, String)] 44 | val cause = Option(event.getThrowableProxy) 45 | val level = logLevelMapping(event.getLevel) 46 | 47 | LogEntry( 48 | event.getLoggerName, 49 | event.getThreadName, 50 | level, 51 | event.getMessage, 52 | event.getTimeStamp, 53 | cause, 54 | keyValues 55 | ) 56 | } 57 | } 58 | 59 | private val logEntriesRef: AtomicReference[Chunk[LogEntry]] = new AtomicReference[Chunk[LogEntry]](Chunk.empty) 60 | 61 | private def appendLogEntry(event: ILoggingEvent): Unit = { 62 | 63 | @tailrec 64 | def append(entry: LogEntry): Unit = { 65 | val old = logEntriesRef.get() 66 | if (logEntriesRef.compareAndSet(old, old :+ entry)) () 67 | else append(entry) 68 | } 69 | 70 | append(LogEntry(event)) 71 | } 72 | 73 | def reset(): Unit = logEntriesRef.set(Chunk.empty) 74 | 75 | def logOutput: Chunk[LogEntry] = logEntriesRef.get() 76 | 77 | } 78 | --------------------------------------------------------------------------------