├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── release.yml └── workflows │ ├── main.yml │ ├── pull_request.yml │ └── pull_request_label.yml ├── .gitignore ├── .licenseignore ├── .mailmap ├── .spi.yml ├── .swift-format ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── SECURITY.md ├── Sources └── Logging │ ├── Docs.docc │ ├── BestPractices │ │ └── 001-ChoosingLogLevels.md │ ├── ImplementingALogHandler.md │ ├── LoggingBestPractices.md │ └── index.md │ ├── Locks.swift │ ├── LogHandler.swift │ ├── Logging.swift │ └── MetadataProvider.swift ├── Tests └── LoggingTests │ ├── CompatibilityTest.swift │ ├── GlobalLoggingTest.swift │ ├── LocalLoggingTest.swift │ ├── LoggingTest.swift │ ├── MDCTest.swift │ ├── MetadataProviderTest.swift │ ├── SubDirectoryOfLoggingTests │ └── EmitALogFromSubDirectory.swift │ ├── TestLogger.swift │ └── TestSendable.swift ├── dev └── git.commit.template └── proposals └── 0001-metadata-providers.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | _[what you expected to happen]_ 3 | 4 | ### Actual behavior 5 | _[what actually happened]_ 6 | 7 | ### Steps to reproduce 8 | 9 | 1. ... 10 | 2. ... 11 | 12 | ### If possible, minimal yet complete reproducer code (or URL to code) 13 | 14 | _[anything to help us reproducing the issue]_ 15 | 16 | ### SwiftLog version/commit hash 17 | 18 | _[the SwiftLog tag/commit hash]_ 19 | 20 | ### Swift & OS version (output of `swift --version && uname -a`) 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _[One line description of your change]_ 2 | 3 | ### Motivation: 4 | 5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ 6 | 7 | ### Modifications: 8 | 9 | _[Describe the modifications you've done.]_ 10 | 11 | ### Result: 12 | 13 | _[After your change, what will change.]_ 14 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | schedule: 7 | - cron: "0 8,20 * * *" 8 | 9 | jobs: 10 | unit-tests: 11 | name: Unit tests 12 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 13 | with: 14 | linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" 15 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" 16 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 17 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 18 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 19 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 20 | windows_6_0_enabled: true 21 | windows_6_1_enabled: true 22 | windows_nightly_next_enabled: true 23 | windows_nightly_main_enabled: true 24 | windows_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 25 | windows_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 26 | windows_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 27 | windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 28 | 29 | cxx-interop: 30 | name: Cxx interop 31 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 32 | 33 | static-sdk: 34 | name: Static SDK 35 | # Workaround https://github.com/nektos/act/issues/1875 36 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 37 | 38 | macos-tests: 39 | name: macOS tests 40 | uses: apple/swift-nio/.github/workflows/macos_tests.yml@main 41 | with: 42 | runner_pool: nightly 43 | build_scheme: swift-log 44 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | soundness: 9 | name: Soundness 10 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 11 | with: 12 | license_header_check_project_name: "Swift Logging API" 13 | 14 | unit-tests: 15 | name: Unit tests 16 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 17 | with: 18 | linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" 19 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" 20 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 21 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 22 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 23 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 24 | windows_6_0_enabled: true 25 | windows_6_1_enabled: true 26 | windows_nightly_next_enabled: true 27 | windows_nightly_main_enabled: true 28 | windows_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 29 | windows_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 30 | windows_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 31 | windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 32 | 33 | cxx-interop: 34 | name: Cxx interop 35 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 36 | 37 | static-sdk: 38 | name: Static SDK 39 | # Workaround https://github.com/nektos/act/issues/1875 40 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 41 | 42 | macos-tests: 43 | name: macOS tests 44 | uses: apple/swift-nio/.github/workflows/macos_tests.yml@main 45 | with: 46 | runner_pool: general 47 | build_scheme: swift-log 48 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_label.yml: -------------------------------------------------------------------------------- 1 | name: PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, reopened, synchronize] 6 | 7 | jobs: 8 | semver-label-check: 9 | name: Semantic version label check 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 1 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | - name: Check for Semantic Version label 18 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Package.resolved 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | .xcode 7 | DerivedData 8 | .swiftpm 9 | 10 | .SourceKitten/ 11 | docs/ 12 | 13 | .*.sw? 14 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .unacceptablelanguageignore 5 | .gitattributes 6 | .git-blame-ignore-revs 7 | .mailfilter 8 | .mailmap 9 | .spi.yml 10 | .swift-format 11 | .editorconfig 12 | .github/* 13 | *.md 14 | *.txt 15 | *.yml 16 | *.yaml 17 | *.json 18 | Package.swift 19 | **/Package.swift 20 | Package@-*.swift 21 | **/Package@-*.swift 22 | Package.resolved 23 | **/Package.resolved 24 | Makefile 25 | *.modulemap 26 | **/*.modulemap 27 | **/*.docc/* 28 | *.xcprivacy 29 | **/*.xcprivacy 30 | *.symlink 31 | **/*.symlink 32 | Dockerfile 33 | **/Dockerfile 34 | Snippets/* 35 | dev/git.commit.template 36 | dev/update-benchmark-thresholds 37 | *.crt 38 | **/*.crt 39 | *.pem 40 | **/*.pem 41 | *.der 42 | **/*.der 43 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Tomer Doron <tomer@apple.com> <tomer.doron@gmail.com> 2 | Tomer Doron <tomer@apple.com> tomer doron <tomer@apple.com> 3 | Tomer Doron <tomer@apple.com> tomer doron <tomerd@apple.com> 4 | Johannes Weiss <johannesweiss@apple.com> <johannes@jweiss.io> 5 | Max Moiseev <moiseev@apple.com> <moiseev@users.noreply.github.com> 6 | Konrad `ktoso` Malawski <ktoso@apple.com> <konrad.malawski@project13.pl> 7 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Logging] 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version" : 1, 3 | "indentation" : { 4 | "spaces" : 4 5 | }, 6 | "tabWidth" : 4, 7 | "fileScopedDeclarationPrivacy" : { 8 | "accessLevel" : "private" 9 | }, 10 | "spacesAroundRangeFormationOperators" : false, 11 | "indentConditionalCompilationBlocks" : false, 12 | "indentSwitchCaseLabels" : false, 13 | "lineBreakAroundMultilineExpressionChainComponents" : false, 14 | "lineBreakBeforeControlFlowKeywords" : false, 15 | "lineBreakBeforeEachArgument" : true, 16 | "lineBreakBeforeEachGenericRequirement" : true, 17 | "lineLength" : 120, 18 | "maximumBlankLines" : 1, 19 | "respectsExistingLineBreaks" : true, 20 | "prioritizeKeepingFunctionOutputTogether" : true, 21 | "rules" : { 22 | "AllPublicDeclarationsHaveDocumentation" : false, 23 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 24 | "AlwaysUseLowerCamelCase" : false, 25 | "AmbiguousTrailingClosureOverload" : true, 26 | "BeginDocumentationCommentWithOneLineSummary" : false, 27 | "DoNotUseSemicolons" : true, 28 | "DontRepeatTypeInStaticProperties" : true, 29 | "FileScopedDeclarationPrivacy" : true, 30 | "FullyIndirectEnum" : true, 31 | "GroupNumericLiterals" : true, 32 | "IdentifiersMustBeASCII" : true, 33 | "NeverForceUnwrap" : false, 34 | "NeverUseForceTry" : false, 35 | "NeverUseImplicitlyUnwrappedOptionals" : false, 36 | "NoAccessLevelOnExtensionDeclaration" : true, 37 | "NoAssignmentInExpressions" : true, 38 | "NoBlockComments" : true, 39 | "NoCasesWithOnlyFallthrough" : true, 40 | "NoEmptyTrailingClosureParentheses" : true, 41 | "NoLabelsInCasePatterns" : true, 42 | "NoLeadingUnderscores" : false, 43 | "NoParensAroundConditions" : true, 44 | "NoVoidReturnOnFunctionSignature" : true, 45 | "OmitExplicitReturns" : true, 46 | "OneCasePerLine" : true, 47 | "OneVariableDeclarationPerLine" : true, 48 | "OnlyOneTrailingClosureArgument" : true, 49 | "OrderedImports" : true, 50 | "ReplaceForEachWithForLoop" : true, 51 | "ReturnVoidInsteadOfEmptyTuple" : true, 52 | "UseEarlyExits" : false, 53 | "UseExplicitNilCheckInConditions" : false, 54 | "UseLetInEveryBoundCaseVariable" : false, 55 | "UseShorthandTypeNames" : true, 56 | "UseSingleLinePropertyGetter" : false, 57 | "UseSynthesizedInitializer" : false, 58 | "UseTripleSlashForDocumentationComments" : true, 59 | "UseWhereClausesInForLoops" : false, 60 | "ValidateDocumentationComments" : false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Apple and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see 6 | `LICENSE.txt`). 7 | 8 | ## How to submit a bug report 9 | 10 | Please ensure to specify the following: 11 | 12 | * SwiftLog commit hash 13 | * Contextual information (e.g. what you were trying to achieve with SwiftLog) 14 | * Simplest possible steps to reproduce 15 | * More complex the steps are, lower the priority will be. 16 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 17 | * Anything that might be relevant in your opinion, such as: 18 | * Swift version or the output of `swift --version` 19 | * OS version and the output of `uname -a` 20 | * Network configuration 21 | 22 | ### Example 23 | 24 | ``` 25 | SwiftLog commit hash: 4fe877816ad82627602377f415b6a66850214824 26 | 27 | Context: 28 | While testing my application that uses with SwiftLog, I noticed that ... 29 | 30 | Steps to reproduce: 31 | 1. ... 32 | 2. ... 33 | 3. ... 34 | 4. ... 35 | 36 | $ swift --version 37 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 38 | Target: x86_64-unknown-linux-gnu 39 | 40 | Operating system: Ubuntu Linux 16.04 64-bit 41 | 42 | $ uname -a 43 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 44 | 45 | My system has IPv6 disabled. 46 | ``` 47 | 48 | ## Writing a Patch 49 | 50 | A good SwiftLog patch is: 51 | 52 | 1. Concise, and contains as few changes as needed to achieve the end result. 53 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 54 | 3. Documented, adding API documentation as needed to cover new functions and properties. 55 | 4. Accompanied by a great commit message, using our commit message template. 56 | 57 | ### Commit Message Template 58 | 59 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: 60 | 61 | git config commit.template dev/git.commit.template 62 | 63 | ### Make sure Tests work on Linux 64 | 65 | SwiftLog uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not. 66 | For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made. 67 | 68 | ### Run `./scripts/soundness.sh` 69 | 70 | The scripts directory contains a [soundness.sh script](https://github.com/apple/swift-log/blob/main/scripts/soundness.sh) 71 | that enforces additional checks, like license headers and formatting style. 72 | 73 | Please make sure to `./scripts/soundness.sh` before pushing a change upstream, otherwise it is likely the PR validation will fail 74 | on minor changes such as a missing `self.` or similar formatting issues. 75 | 76 | > The script also executes the above mentioned `generate_linux_tests.rb`. 77 | 78 | For frequent contributors, we recommend adding the script as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which you can do via executing the following command in the project root directory: 79 | 80 | ```bash 81 | cat << EOF > .git/hooks/pre-push 82 | #!/bin/bash 83 | 84 | if [[ -f "scripts/soundness.sh" ]]; then 85 | scripts/soundness.sh 86 | fi 87 | EOF 88 | ``` 89 | 90 | Which makes the script execute, and only allow the `git push` to complete if the check has passed. 91 | 92 | In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. 93 | 94 | ## How to contribute your work 95 | 96 | Please open a pull request at https://github.com/apple/swift-log. Make sure the CI passes, and then wait for code review. 97 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to the Swift Logging API. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - David Hart <david@hartbit.com> 15 | - Iain Smith <iainsmith@users.noreply.github.com> 16 | - Ian Partridge <i.partridge@uk.ibm.com> 17 | - Johannes Weiss <johannesweiss@apple.com> 18 | - Kevin Sweeney <kevin.t.sweeney@gmail.com> 19 | - Konrad `ktoso` Malawski <ktoso@apple.com> 20 | - Max Moiseev <moiseev@apple.com> 21 | - Tanner <me@tanner.xyz> 22 | - Thomas Krajacic <tkrajacic@users.noreply.github.com> 23 | - Tomer Doron <tomer@apple.com> 24 | 25 | **Updating this list** 26 | 27 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The SwiftLog Project 3 | ======================== 4 | 5 | Please visit the SwiftLog web site for more information: 6 | 7 | * https://github.com/apple/swift-log 8 | 9 | Copyright 2018, 2019 The SwiftLog Project 10 | 11 | The SwiftLog Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE.<component>.txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | ------------------------------------------------------------------------------- 28 | 29 | This product contains a derivation of the Tony Stone's 'process_test_files.rb'. 30 | 31 | * LICENSE (Apache License 2.0): 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * HOMEPAGE: 34 | * https://codegists.com/snippet/ruby/generate_xctest_linux_runnerrb_tonystone_ruby 35 | 36 | --- 37 | 38 | This product contains a derivation of the lock implementation and various 39 | scripts from SwiftNIO. 40 | 41 | * LICENSE (Apache License 2.0): 42 | * https://www.apache.org/licenses/LICENSE-2.0 43 | * HOMEPAGE: 44 | * https://github.com/apple/swift-nio 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the Swift Logging API open source project 5 | // 6 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "swift-log", 20 | products: [ 21 | .library(name: "Logging", targets: ["Logging"]) 22 | ], 23 | targets: [ 24 | .target( 25 | name: "Logging", 26 | dependencies: [] 27 | ), 28 | .testTarget( 29 | name: "LoggingTests", 30 | dependencies: ["Logging"] 31 | ), 32 | ] 33 | ) 34 | 35 | for target in package.targets { 36 | var settings = target.swiftSettings ?? [] 37 | settings.append(.enableExperimentalFeature("StrictConcurrency=complete")) 38 | target.swiftSettings = settings 39 | } 40 | 41 | // --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 42 | for target in package.targets { 43 | switch target.type { 44 | case .regular, .test, .executable: 45 | var settings = target.swiftSettings ?? [] 46 | // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md 47 | settings.append(.enableUpcomingFeature("MemberImportVisibility")) 48 | target.swiftSettings = settings 49 | case .macro, .plugin, .system, .binary: 50 | () // not applicable 51 | @unknown default: 52 | () // we don't know what to do here, do nothing 53 | } 54 | } 55 | // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftLog 2 | 3 | This repository contains a logging API implementation for Swift. 4 | SwiftLog provides a unified, performant, and ergonomic logging API that can be 5 | adopted by libraries and applications across the Swift ecosystem. 6 | 7 | - 📚 **Documentation** and **tutorials** are available on the [Swift Package Index][spi-swift-log] 8 | - 🚀 **Contributions** are welcome, please see [CONTRIBUTING.md](CONTRIBUTING.md) 9 | - 🪪 **License** is Apache 2.0, repeated in [LICENSE.txt](LICENSE.txt) 10 | - 🔒 **Security** issues should be reported via the process in [SECURITY.md](SECURITY.md) 11 | - 🔀 **Available Logging Backends**: SwiftLog is an API package - you'll want to 12 | choose from the many 13 | [community-maintained logging backends](#available-log-handler-backends) for production use 14 | 15 | ## Quick Start 16 | 17 | The following snippet shows how to add SwiftLog to your Swift Package: 18 | 19 | ```swift 20 | // swift-tools-version: 6.1 21 | import PackageDescription 22 | 23 | let package = Package( 24 | name: "YourApp", 25 | dependencies: [ 26 | .package(url: "https://github.com/apple/swift-log", from: "1.6.0") 27 | ], 28 | targets: [ 29 | .target( 30 | name: "YourApp", 31 | dependencies: [ 32 | .product(name: "Logging", package: "swift-log") 33 | ] 34 | ) 35 | ] 36 | ) 37 | ``` 38 | 39 | Then start logging: 40 | 41 | ```swift 42 | import Logging 43 | 44 | // Create a logger 45 | let logger = Logger(label: "com.example.YourApp") 46 | 47 | // Log at different levels 48 | logger.info("Application started") 49 | logger.warning("This is a warning") 50 | logger.error("Something went wrong", metadata: ["error": "\(error)"]) 51 | 52 | // Add metadata for context 53 | var requestLogger = logger 54 | requestLogger[metadataKey: "request-id"] = "\(UUID())" 55 | requestLogger.info("Processing request") 56 | ``` 57 | 58 | ## Available log handler backends 59 | 60 | The community has built numerous specialized logging backends. 61 | 62 | A great way to discover available log backend implementations is searching the 63 | [Swift Package Index](https://swiftpackageindex.com/search?query=swift-log) 64 | for the `swift-log` keyword. 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | This document specifies the security process for the SwiftLog project. 4 | 5 | ## Disclosures 6 | 7 | ### Private Disclosure Process 8 | 9 | The SwiftLog maintainers ask that known and suspected vulnerabilities be 10 | privately and responsibly disclosed by emailing 11 | [sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org) 12 | with the all the required detail. 13 | **Do not file a public issue.** 14 | 15 | #### When to report a vulnerability 16 | 17 | * You think you have discovered a potential security vulnerability in SwiftLog. 18 | * You are unsure how a vulnerability affects SwiftLog. 19 | 20 | #### What happens next? 21 | 22 | * A member of the team will acknowledge receipt of the report within 3 23 | working days (United States). This may include a request for additional 24 | information about reproducing the vulnerability. 25 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the 26 | vulnerability within 10 days of the report as per their [security 27 | guidelines][sswg-security]. 28 | * Once we have identified a fix we may ask you to validate it. We aim to do this 29 | within 30 days. In some cases this may not be possible, for example when the 30 | vulnerability exists at the protocol level and the industry must coordinate on 31 | the disclosure process. 32 | * If a CVE number is required, one will be requested from [MITRE][mitre] 33 | providing you with full credit for the discovery. 34 | * We will decide on a planned release date and let you know when it is. 35 | * Prior to release, we will inform major dependents that a security-related 36 | patch is impending. 37 | * Once the fix has been released we will publish a security advisory on GitHub 38 | and in the Server → Security Updates category on the [Swift forums][swift-forums-sec]. 39 | 40 | [sswg]: https://github.com/swift-server/sswg 41 | [sswg-security]: https://www.swift.org/sswg/security/ 42 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ 43 | [mitre]: https://cveform.mitre.org/ 44 | -------------------------------------------------------------------------------- /Sources/Logging/Docs.docc/BestPractices/001-ChoosingLogLevels.md: -------------------------------------------------------------------------------- 1 | # 001: Choosing log levels 2 | 3 | Best practice for selecting appropriate log levels in applications and 4 | libraries. 5 | 6 | ## Overview 7 | 8 | SwiftLog defines seven log levels, and choosing the right level is crucial for 9 | creating well-behaved libraries that don't overwhelm logging systems or misuse 10 | severity levels. This practice provides clear guidance on when to use each 11 | level. 12 | 13 | ### Motivation 14 | 15 | Libraries must be well-behaved across various use cases and cannot assume 16 | specific logging backend configurations. Using inappropriate log levels can 17 | flood production logs, trigger false alerts, or make debugging more difficult. 18 | Following consistent log level guidelines ensures your library integrates well 19 | with diverse application environments. 20 | 21 | ### Log levels 22 | 23 | SwiftLog defines seven log levels via ``Logger/Level``, ordered from least to 24 | most severe: 25 | 26 | - ``Logger/Level/trace`` 27 | - ``Logger/Level/debug`` 28 | - ``Logger/Level/info`` 29 | - ``Logger/Level/notice`` 30 | - ``Logger/Level/warning`` 31 | - ``Logger/Level/error`` 32 | - ``Logger/Level/critical`` 33 | 34 | ### Level guidelines 35 | 36 | How you use log levels depends in large part if you are developing a library, or 37 | an application which bootstraps its logging system and is in full control over 38 | its logging environment. 39 | 40 | #### For libraries 41 | 42 | Libraries should use **info level or lower** (info, debug, trace). Each level 43 | serves different purposes: 44 | 45 | Libraries **should not** log information on **warning or more severe levels**, 46 | unless it is a one-time (for example during startup) warning, that cannot lead 47 | to overwhelming log outputs. 48 | 49 | ##### Trace Level 50 | - **Usage**: Log everything needed to diagnose hard-to-reproduce bugs 51 | - **Performance**: May impact performance; assume it won't be used in production 52 | - **Content**: Internal state, detailed operation flows, diagnostic information 53 | 54 | ##### Debug Level 55 | - **Usage**: May be enabled in some production deployments 56 | - **Performance**: Should not significantly undermine production performance 57 | - **Content**: High-level operation overview, connection events, major decisions 58 | 59 | ##### Info Level 60 | - **Usage**: Reserved for things that went wrong but can't be communicated 61 | through other means like throwing from a method 62 | - **Examples**: Connection retry attempts, fallback mechanisms, recoverable 63 | failures 64 | - **Guideline**: Use sparingly - not for normal successful operations 65 | 66 | #### For applications 67 | 68 | Applications can use **any level** depending on the context and what they want 69 | to achieve. Applications have full control over their logging strategy. 70 | 71 | #### Configuring logger log levels 72 | 73 | It depends on the use-case of your application which log level your logger 74 | should use. For **console and other end-user-visible displays**: Consider using 75 | **notice level** as the minimum visible level to avoid overwhelming users with 76 | technical details. 77 | 78 | ### Example 79 | 80 | #### Recommended: Libraries should use info level or lower 81 | 82 | ```swift 83 | // ✅ Good: Trace level for detailed diagnostics 84 | logger.trace("Connection pool state", metadata: [ 85 | "active": "\(activeConnections)", 86 | "idle": "\(idleConnections)", 87 | "pending": "\(pendingRequests)" 88 | ]) 89 | 90 | // ✅ Good: Debug level for high-value operational info 91 | logger.debug("Database connection established", metadata: [ 92 | "host": "\(host)", 93 | "database": "\(database)", 94 | "connectionTime": "\(duration)" 95 | ]) 96 | 97 | // ✅ Good: Info level for issues that can't be communicated through other means 98 | logger.info("Connection failed, retrying", metadata: [ 99 | "attempt": "\(attemptNumber)", 100 | "maxRetries": "\(maxRetries)", 101 | "host": "\(host)" 102 | ]) 103 | ``` 104 | 105 | #### Use sparingly: Warning and error levels 106 | 107 | ```swift 108 | // ✅ Good: One-time startup warning or error 109 | logger.warning("Deprecated TLS version detected. Consider upgrading to TLS 1.3") 110 | ``` 111 | 112 | #### Avoid: Logging potentially intentional failures at info level 113 | 114 | Some failures may be completely intentional from the high-level perspective of a 115 | developer or system using your library. For example: failure to resolve a 116 | domain, failure to make a request, or failure to complete some task; 117 | 118 | Instead, log at debug or trace levels and offer alternative ways to observe 119 | these behaviors, for example using `swift-metrics` to emit counts. 120 | 121 | ```swift 122 | // ❌ Bad: Normal operations at info level flood production logs 123 | logger.info("Request failed") 124 | ``` 125 | 126 | #### Avoid: Normal operations at info level 127 | 128 | ```swift 129 | // ❌ Bad: Normal operations at info level flood production logs 130 | logger.info("HTTP request received") 131 | logger.info("Database query executed") 132 | logger.info("Response sent") 133 | 134 | // ✅ Good: Use appropriate levels instead 135 | logger.debug("Processing request", metadata: ["path": "\(path)"]) 136 | logger.trace("Query", , metadata: ["path": "\(query)"]) 137 | logger.debug("Request completed", metadata: ["status": "\(status)"]) 138 | ``` 139 | -------------------------------------------------------------------------------- /Sources/Logging/Docs.docc/ImplementingALogHandler.md: -------------------------------------------------------------------------------- 1 | # Implementing a log handler 2 | 3 | Create a custom logging backend that provides logging services for your apps 4 | and libraries. 5 | 6 | ## Overview 7 | 8 | To become a compatible logging backend that any `SwiftLog` consumer can use, 9 | you need to fulfill a few requirements, primarily conforming to the 10 | ``LogHandler`` protocol. 11 | 12 | ### Implement with value type semantics 13 | 14 | Your log handler **must be a `struct`** and exhibit value semantics. This 15 | ensures that changes to one logger don't affect others. 16 | 17 | To verify that your handler reflects value semantics ensure that it passes this 18 | test: 19 | 20 | ```swift 21 | @Test 22 | func logHandlerValueSemantics() { 23 | LoggingSystem.bootstrap(MyLogHandler.init) 24 | var logger1 = Logger(label: "first logger") 25 | logger1.logLevel = .debug 26 | logger1[metadataKey: "only-on"] = "first" 27 | 28 | var logger2 = logger1 29 | logger2.logLevel = .error // Must not affect logger1 30 | logger2[metadataKey: "only-on"] = "second" // Must not affect logger1 31 | 32 | // These expectations must pass 33 | #expect(logger1.logLevel == .debug) 34 | #expect(logger2.logLevel == .error) 35 | #expect(logger1[metadataKey: "only-on"] == "first") 36 | #expect(logger2[metadataKey: "only-on"] == "second") 37 | } 38 | ``` 39 | 40 | > Note: In special cases, it is acceptable for a log handler to provide 41 | > global log level overrides that may affect all log handlers created. 42 | 43 | ### Example implementation 44 | 45 | Here's a complete example of a simple print-based log handler: 46 | 47 | ```swift 48 | import Foundation 49 | import Logging 50 | 51 | public struct PrintLogHandler: LogHandler { 52 | private let label: String 53 | public var logLevel: Logger.Level = .info 54 | public var metadata: Logger.Metadata = [:] 55 | 56 | public init(label: String) { 57 | self.label = label 58 | } 59 | 60 | public func log( 61 | level: Logger.Level, 62 | message: Logger.Message, 63 | metadata: Logger.Metadata?, 64 | source: String, 65 | file: String, 66 | function: String, 67 | line: UInt 68 | ) { 69 | let timestamp = ISO8601DateFormatter().string(from: Date()) 70 | let levelString = level.rawValue.uppercased() 71 | 72 | // Merge handler metadata with message metadata 73 | let combinedMetadata = Self.prepareMetadata( 74 | base: self.metadata 75 | explicit: metadata 76 | ) 77 | 78 | // Format metadata 79 | let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",") 80 | 81 | // Create log line and print to console 82 | let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)" 83 | print(logLine) 84 | } 85 | 86 | public subscript(metadataKey key: String) -> Logger.Metadata.Value? { 87 | get { 88 | return self.metadata[key] 89 | } 90 | set { 91 | self.metadata[key] = newValue 92 | } 93 | } 94 | 95 | static func prepareMetadata( 96 | base: Logger.Metadata, 97 | explicit: Logger.Metadata? 98 | ) -> Logger.Metadata? { 99 | var metadata = base 100 | 101 | guard let explicit else { 102 | // all per-log-statement values are empty 103 | return metadata 104 | } 105 | 106 | metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit }) 107 | 108 | return metadata 109 | } 110 | } 111 | 112 | ``` 113 | 114 | ### Advanced features 115 | 116 | #### Metadata providers 117 | 118 | Metadata providers allow you to dynamically add contextual information to all 119 | log messages without explicitly passing it each time. Common use cases include 120 | request IDs, user sessions, or trace contexts that should be included in logs 121 | throughout a request's lifecycle. 122 | 123 | ```swift 124 | import Foundation 125 | import Logging 126 | 127 | public struct PrintLogHandler: LogHandler { 128 | private let label: String 129 | public var logLevel: Logger.Level = .info 130 | public var metadata: Logger.Metadata = [:] 131 | public var metadataProvider: Logger.MetadataProvider? 132 | 133 | public init(label: String) { 134 | self.label = label 135 | } 136 | 137 | public func log( 138 | level: Logger.Level, 139 | message: Logger.Message, 140 | metadata: Logger.Metadata?, 141 | source: String, 142 | file: String, 143 | function: String, 144 | line: UInt 145 | ) { 146 | let timestamp = ISO8601DateFormatter().string(from: Date()) 147 | let levelString = level.rawValue.uppercased() 148 | 149 | // Get provider metadata 150 | let providerMetadata = metadataProvider?.get() ?? [:] 151 | 152 | // Merge handler metadata with message metadata 153 | let combinedMetadata = Self.prepareMetadata( 154 | base: self.metadata, 155 | provider: self.metadataProvider, 156 | explicit: metadata 157 | ) 158 | 159 | // Format metadata 160 | let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",") 161 | 162 | // Create log line and print to console 163 | let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)" 164 | print(logLine) 165 | } 166 | 167 | public subscript(metadataKey key: String) -> Logger.Metadata.Value? { 168 | get { 169 | return self.metadata[key] 170 | } 171 | set { 172 | self.metadata[key] = newValue 173 | } 174 | } 175 | 176 | static func prepareMetadata( 177 | base: Logger.Metadata, 178 | provider: Logger.MetadataProvider?, 179 | explicit: Logger.Metadata? 180 | ) -> Logger.Metadata? { 181 | var metadata = base 182 | 183 | let provided = provider?.get() ?? [:] 184 | 185 | guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else { 186 | // all per-log-statement values are empty 187 | return metadata 188 | } 189 | 190 | if !provided.isEmpty { 191 | metadata.merge(provided, uniquingKeysWith: { _, provided in provided }) 192 | } 193 | 194 | if let explicit = explicit, !explicit.isEmpty { 195 | metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit }) 196 | } 197 | 198 | return metadata 199 | } 200 | } 201 | ``` 202 | 203 | ### Performance considerations 204 | 205 | 1. **Avoid blocking**: Don't block the calling thread for I/O operations. 206 | 2. **Lazy evaluation**: Remember that messages and metadata are autoclosures. 207 | 3. **Memory efficiency**: Don't hold onto large amounts of messages. 208 | 209 | ## See Also 210 | 211 | - ``LogHandler`` 212 | - ``StreamLogHandler`` 213 | - ``MultiplexLogHandler`` 214 | -------------------------------------------------------------------------------- /Sources/Logging/Docs.docc/LoggingBestPractices.md: -------------------------------------------------------------------------------- 1 | # Logging best practices 2 | 3 | Best practices for effective logging with SwiftLog. 4 | 5 | ## Overview 6 | 7 | This collection of best practices helps library authors and application 8 | developers create effective, maintainable logging that works well across diverse 9 | environments. Each practice is designed to ensure your logs are useful for 10 | debugging while being respectful of system resources and operational 11 | requirements. 12 | 13 | ### Who Should Use These Practices 14 | 15 | - **Library Authors**: Creating reusable components that log appropriately. 16 | - **Application Developers**: Implementing logging strategies in applications. 17 | 18 | ### Philosophy 19 | 20 | Good logging strikes a balance between providing useful information and avoiding 21 | system overhead. These practices are based on real-world experience with 22 | production systems and emphasize: 23 | 24 | - **Predictable behavior** across different environments. 25 | - **Performance consciousness** to avoid impacting application speed. 26 | - **Operational awareness** to support production debugging and monitoring. 27 | - **Developer experience** to make debugging efficient and pleasant. 28 | 29 | ### Contributing to these practices 30 | 31 | These best practices evolve based on community experience and are maintained by 32 | the Swift Server Working Group ([SSWG](https://www.swift.org/sswg/)). Each 33 | practice includes: 34 | 35 | - **Clear motivation** explaining why the practice matters 36 | - **Concrete examples** showing good and bad patterns 37 | - **Alternatives considered** documenting trade-offs and rejected approaches 38 | 39 | ## Topics 40 | 41 | - <doc:001-ChoosingLogLevels> 42 | -------------------------------------------------------------------------------- /Sources/Logging/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``Logging`` 2 | 3 | A unified, performant, and ergonomic logging API for Swift. 4 | 5 | ## Overview 6 | 7 | SwiftLog provides a logging API package designed to establish a common API the 8 | ecosystem can use. It allows packages to emit log messages without tying them to 9 | any specific logging implementation, while applications can choose any 10 | compatible logging backend. 11 | 12 | SwiftLog is an _API package_ which cuts the logging problem in half: 13 | 1. A logging API (this package) 14 | 2. Logging backend implementations (community-provided) 15 | 16 | This separation allows libraries to adopt the API while applications choose any 17 | compatible logging backend implementation without requiring changes from 18 | libraries. 19 | 20 | ## Getting Started 21 | 22 | Use this If you are writing a cross-platform application (for example, Linux and 23 | macOS) or library, target this logging API. 24 | 25 | ### Adding the Dependency 26 | 27 | Add the dependency to your `Package.swift`: 28 | 29 | ```swift 30 | .package(url: "https://github.com/apple/swift-log", from: "1.6.0") 31 | ``` 32 | 33 | And to your target: 34 | 35 | ```swift 36 | .target( 37 | name: "YourTarget", 38 | dependencies: [ 39 | .product(name: "Logging", package: "swift-log") 40 | ] 41 | ) 42 | ``` 43 | 44 | ### Basic Usage 45 | 46 | ```swift 47 | // Import the logging API 48 | import Logging 49 | 50 | // Create a logger with a label 51 | let logger = Logger(label: "MyLogger") 52 | 53 | // Use it to log messages 54 | logger.info("Hello World!") 55 | ``` 56 | 57 | This outputs: 58 | ``` 59 | 2019-03-13T15:46:38+0000 info: Hello World! 60 | ``` 61 | 62 | ### Default Behavior 63 | 64 | SwiftLog provides basic console logging via ``StreamLogHandler``. By default it 65 | uses `stdout`, however, you can configure it to use `stderr` instead: 66 | 67 | ```swift 68 | LoggingSystem.bootstrap(StreamLogHandler.standardError) 69 | ``` 70 | 71 | ``StreamLogHandler`` is primarily for convenience. For production applications, 72 | implement the ``LogHandler`` protocol directly or use a community-maintained 73 | backend. 74 | 75 | ## Core Concepts 76 | 77 | ### Loggers 78 | 79 | Loggers are used to emit log messages at different severity levels: 80 | 81 | ```swift 82 | // Informational message 83 | logger.info("Processing request") 84 | 85 | // Something went wrong 86 | logger.error("Houston, we have a problem") 87 | ``` 88 | 89 | ``Logger`` is a value type with value semantics, meaning that when you modify a 90 | logger's configuration (like its log level or metadata), it only affects that 91 | specific logger instance: 92 | 93 | ```swift 94 | let baseLogger = Logger(label: "MyApp") 95 | 96 | // Create a new logger with different configuration. 97 | var requestLogger = baseLogger 98 | requestLogger.logLevel = .debug 99 | requestLogger[metadataKey: "request-id"] = "\(UUID())" 100 | 101 | // baseLogger is unchanged. It still has default log level and no metadata 102 | // requestLogger has debug level and request-id metadata. 103 | ``` 104 | 105 | This value type behavior makes loggers safe to pass between functions and modify 106 | without unexpected side effects. 107 | 108 | ### Log Levels 109 | 110 | SwiftLog supports seven log levels (from least to most severe): 111 | - ``Logger/Level/trace`` 112 | - ``Logger/Level/debug`` 113 | - ``Logger/Level/info`` 114 | - ``Logger/Level/notice`` 115 | - ``Logger/Level/warning`` 116 | - ``Logger/Level/error`` 117 | - ``Logger/Level/critical`` 118 | 119 | Log levels can be changed per logger without affecting others: 120 | 121 | ```swift 122 | var logger = Logger(label: "MyLogger") 123 | logger.logLevel = .debug 124 | ``` 125 | 126 | ### Logging Metadata 127 | 128 | Metadata provides contextual information crucial for debugging: 129 | 130 | ```swift 131 | var logger = Logger(label: "com.example.server") 132 | logger[metadataKey: "request-uuid"] = "\(UUID())" 133 | logger.info("Processing request") 134 | ``` 135 | 136 | Output: 137 | ``` 138 | 2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED Processing request 139 | ``` 140 | 141 | ### Source vs Label 142 | 143 | A ``Logger`` has an immutable `label` identifying its creator, while each log 144 | message carries a `source` parameter identifying where the message originated. 145 | Use `source` for filtering messages from specific subsystems. 146 | 147 | 148 | ## Topics 149 | 150 | ### Logging API 151 | 152 | - ``Logger`` 153 | - ``LoggingSystem`` 154 | 155 | ### Log Handlers 156 | 157 | - ``LogHandler`` 158 | - ``MultiplexLogHandler`` 159 | - ``StreamLogHandler`` 160 | - ``SwiftLogNoOpLogHandler`` 161 | 162 | ### Best Practices 163 | 164 | - <doc:LoggingBestPractices> 165 | - <doc:001-ChoosingLogLevels> 166 | 167 | -------------------------------------------------------------------------------- /Sources/Logging/Locks.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | //===----------------------------------------------------------------------===// 16 | // 17 | // This source file is part of the SwiftNIO open source project 18 | // 19 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 20 | // Licensed under Apache License v2.0 21 | // 22 | // See LICENSE.txt for license information 23 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | // 25 | // SPDX-License-Identifier: Apache-2.0 26 | // 27 | //===----------------------------------------------------------------------===// 28 | 29 | #if canImport(WASILibc) 30 | // No locking on WASILibc 31 | #elseif canImport(Darwin) 32 | import Darwin 33 | #elseif os(Windows) 34 | import WinSDK 35 | #elseif canImport(Glibc) 36 | import Glibc 37 | #elseif canImport(Android) 38 | import Android 39 | #elseif canImport(Musl) 40 | import Musl 41 | #else 42 | #error("Unsupported runtime") 43 | #endif 44 | 45 | /// A threading lock based on `libpthread` instead of `libdispatch`. 46 | /// 47 | /// This object provides a lock on top of a single `pthread_mutex_t`. This kind 48 | /// of lock is safe to use with `libpthread`-based threading models, such as the 49 | /// one used by NIO. On Windows, the lock is based on the substantially similar 50 | /// `SRWLOCK` type. 51 | internal final class Lock: @unchecked Sendable { 52 | #if canImport(WASILibc) 53 | // WASILibc is single threaded, provides no locks 54 | #elseif os(Windows) 55 | fileprivate let mutex: UnsafeMutablePointer<SRWLOCK> = 56 | UnsafeMutablePointer.allocate(capacity: 1) 57 | #else 58 | fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> = 59 | UnsafeMutablePointer.allocate(capacity: 1) 60 | #endif 61 | 62 | /// Create a new lock. 63 | public init() { 64 | #if canImport(WASILibc) 65 | // WASILibc is single threaded, provides no locks 66 | #elseif os(Windows) 67 | InitializeSRWLock(self.mutex) 68 | #else 69 | var attr = pthread_mutexattr_t() 70 | pthread_mutexattr_init(&attr) 71 | pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) 72 | 73 | let err = pthread_mutex_init(self.mutex, &attr) 74 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 75 | #endif 76 | } 77 | 78 | deinit { 79 | #if canImport(WASILibc) 80 | // WASILibc is single threaded, provides no locks 81 | #elseif os(Windows) 82 | // SRWLOCK does not need to be free'd 83 | self.mutex.deallocate() 84 | #else 85 | let err = pthread_mutex_destroy(self.mutex) 86 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 87 | self.mutex.deallocate() 88 | #endif 89 | } 90 | 91 | /// Acquire the lock. 92 | /// 93 | /// Whenever possible, consider using `withLock` instead of this method and 94 | /// `unlock`, to simplify lock handling. 95 | public func lock() { 96 | #if canImport(WASILibc) 97 | // WASILibc is single threaded, provides no locks 98 | #elseif os(Windows) 99 | AcquireSRWLockExclusive(self.mutex) 100 | #else 101 | let err = pthread_mutex_lock(self.mutex) 102 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 103 | #endif 104 | } 105 | 106 | /// Release the lock. 107 | /// 108 | /// Whenever possible, consider using `withLock` instead of this method and 109 | /// `lock`, to simplify lock handling. 110 | public func unlock() { 111 | #if canImport(WASILibc) 112 | // WASILibc is single threaded, provides no locks 113 | #elseif os(Windows) 114 | ReleaseSRWLockExclusive(self.mutex) 115 | #else 116 | let err = pthread_mutex_unlock(self.mutex) 117 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 118 | #endif 119 | } 120 | } 121 | 122 | extension Lock { 123 | /// Acquire the lock for the duration of the given block. 124 | /// 125 | /// This convenience method should be preferred to `lock` and `unlock` in 126 | /// most situations, as it ensures that the lock will be released regardless 127 | /// of how `body` exits. 128 | /// 129 | /// - Parameter body: The block to execute while holding the lock. 130 | /// - Returns: The value returned by the block. 131 | @inlinable 132 | internal func withLock<T>(_ body: () throws -> T) rethrows -> T { 133 | self.lock() 134 | defer { 135 | self.unlock() 136 | } 137 | return try body() 138 | } 139 | 140 | // specialise Void return (for performance) 141 | @inlinable 142 | internal func withLockVoid(_ body: () throws -> Void) rethrows { 143 | try self.withLock(body) 144 | } 145 | } 146 | 147 | /// A reader/writer threading lock based on `libpthread` instead of `libdispatch`. 148 | /// 149 | /// This object provides a lock on top of a single `pthread_rwlock_t`. This kind 150 | /// of lock is safe to use with `libpthread`-based threading models, such as the 151 | /// one used by NIO. On Windows, the lock is based on the substantially similar 152 | /// `SRWLOCK` type. 153 | internal final class ReadWriteLock: @unchecked Sendable { 154 | #if canImport(WASILibc) 155 | // WASILibc is single threaded, provides no locks 156 | #elseif os(Windows) 157 | fileprivate let rwlock: UnsafeMutablePointer<SRWLOCK> = 158 | UnsafeMutablePointer.allocate(capacity: 1) 159 | fileprivate var shared: Bool = true 160 | #else 161 | fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = 162 | UnsafeMutablePointer.allocate(capacity: 1) 163 | #endif 164 | 165 | /// Create a new lock. 166 | public init() { 167 | #if canImport(WASILibc) 168 | // WASILibc is single threaded, provides no locks 169 | #elseif os(Windows) 170 | InitializeSRWLock(self.rwlock) 171 | #else 172 | let err = pthread_rwlock_init(self.rwlock, nil) 173 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 174 | #endif 175 | } 176 | 177 | deinit { 178 | #if canImport(WASILibc) 179 | // WASILibc is single threaded, provides no locks 180 | #elseif os(Windows) 181 | // SRWLOCK does not need to be free'd 182 | self.rwlock.deallocate() 183 | #else 184 | let err = pthread_rwlock_destroy(self.rwlock) 185 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 186 | self.rwlock.deallocate() 187 | #endif 188 | } 189 | 190 | /// Acquire a reader lock. 191 | /// 192 | /// Whenever possible, consider using `withReaderLock` instead of this 193 | /// method and `unlock`, to simplify lock handling. 194 | fileprivate func lockRead() { 195 | #if canImport(WASILibc) 196 | // WASILibc is single threaded, provides no locks 197 | #elseif os(Windows) 198 | AcquireSRWLockShared(self.rwlock) 199 | self.shared = true 200 | #else 201 | let err = pthread_rwlock_rdlock(self.rwlock) 202 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 203 | #endif 204 | } 205 | 206 | /// Acquire a writer lock. 207 | /// 208 | /// Whenever possible, consider using `withWriterLock` instead of this 209 | /// method and `unlock`, to simplify lock handling. 210 | fileprivate func lockWrite() { 211 | #if canImport(WASILibc) 212 | // WASILibc is single threaded, provides no locks 213 | #elseif os(Windows) 214 | AcquireSRWLockExclusive(self.rwlock) 215 | self.shared = false 216 | #else 217 | let err = pthread_rwlock_wrlock(self.rwlock) 218 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 219 | #endif 220 | } 221 | 222 | /// Release the lock. 223 | /// 224 | /// Whenever possible, consider using `withReaderLock` and `withWriterLock` 225 | /// instead of this method and `lockRead` and `lockWrite`, to simplify lock 226 | /// handling. 227 | fileprivate func unlock() { 228 | #if canImport(WASILibc) 229 | // WASILibc is single threaded, provides no locks 230 | #elseif os(Windows) 231 | if self.shared { 232 | ReleaseSRWLockShared(self.rwlock) 233 | } else { 234 | ReleaseSRWLockExclusive(self.rwlock) 235 | } 236 | #else 237 | let err = pthread_rwlock_unlock(self.rwlock) 238 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 239 | #endif 240 | } 241 | } 242 | 243 | extension ReadWriteLock { 244 | /// Acquire the reader lock for the duration of the given block. 245 | /// 246 | /// This convenience method should be preferred to `lockRead` and `unlock` 247 | /// in most situations, as it ensures that the lock will be released 248 | /// regardless of how `body` exits. 249 | /// 250 | /// - Parameter body: The block to execute while holding the reader lock. 251 | /// - Returns: The value returned by the block. 252 | @inlinable 253 | internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T { 254 | self.lockRead() 255 | defer { 256 | self.unlock() 257 | } 258 | return try body() 259 | } 260 | 261 | /// Acquire the writer lock for the duration of the given block. 262 | /// 263 | /// This convenience method should be preferred to `lockWrite` and `unlock` 264 | /// in most situations, as it ensures that the lock will be released 265 | /// regardless of how `body` exits. 266 | /// 267 | /// - Parameter body: The block to execute while holding the writer lock. 268 | /// - Returns: The value returned by the block. 269 | @inlinable 270 | internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T { 271 | self.lockWrite() 272 | defer { 273 | self.unlock() 274 | } 275 | return try body() 276 | } 277 | 278 | // specialise Void return (for performance) 279 | @inlinable 280 | internal func withReaderLockVoid(_ body: () throws -> Void) rethrows { 281 | try self.withReaderLock(body) 282 | } 283 | 284 | // specialise Void return (for performance) 285 | @inlinable 286 | internal func withWriterLockVoid(_ body: () throws -> Void) rethrows { 287 | try self.withWriterLock(body) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /Sources/Logging/LogHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A `LogHandler` is an implementation of a logging backend. 16 | /// 17 | /// This type is an implementation detail and should not normally be used, unless implementing your own logging backend. 18 | /// To use the SwiftLog API, please refer to the documentation of ``Logger``. 19 | /// 20 | /// # Implementation requirements 21 | /// 22 | /// To implement your own `LogHandler` you should respect a few requirements that are necessary so applications work 23 | /// as expected regardless of the selected `LogHandler` implementation. 24 | /// 25 | /// - The ``LogHandler`` must be a `struct`. 26 | /// - The metadata and `logLevel` properties must be implemented so that setting them on a `Logger` does not affect 27 | /// other `Logger`s. 28 | /// 29 | /// ### Treat log level & metadata as values 30 | /// 31 | /// When developing your `LogHandler`, please make sure the following test works. 32 | /// 33 | /// ```swift 34 | /// @Test 35 | /// func logHandlerValueSemantics() { 36 | /// LoggingSystem.bootstrap(MyLogHandler.init) 37 | /// var logger1 = Logger(label: "first logger") 38 | /// logger1.logLevel = .debug 39 | /// logger1[metadataKey: "only-on"] = "first" 40 | /// 41 | /// var logger2 = logger1 42 | /// logger2.logLevel = .error // Must not affect logger1 43 | /// logger2[metadataKey: "only-on"] = "second" // Must not affect logger1 44 | /// 45 | /// // These expectations must pass 46 | /// #expect(logger1.logLevel == .debug) 47 | /// #expect(logger2.logLevel == .error) 48 | /// #expect(logger1[metadataKey: "only-on"] == "first") 49 | /// #expect(logger2[metadataKey: "only-on"] == "second") 50 | /// } 51 | /// ``` 52 | /// 53 | /// ### Special cases 54 | /// 55 | /// In certain special cases, the log level behaving like a value on `Logger` might not be what you want. For example, 56 | /// you might want to set the log level across _all_ `Logger`s to `.debug` when say a signal (eg. `SIGUSR1`) is received 57 | /// to be able to debug special failures in production. This special case is acceptable but we urge you to create a 58 | /// solution specific to your `LogHandler` implementation to achieve that. Please find an example implementation of this 59 | /// behavior below, on reception of the signal you would call 60 | /// `LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel = .debug`, for example. 61 | /// 62 | /// ```swift 63 | /// import class Foundation.NSLock 64 | /// 65 | /// public struct LogHandlerWithGlobalLogLevelOverride: LogHandler { 66 | /// // the static properties hold the globally overridden log level (if overridden) 67 | /// private static let overrideLock = NSLock() 68 | /// private static var overrideLogLevel: Logger.Level? = nil 69 | /// 70 | /// // this holds the log level if not overridden 71 | /// private var _logLevel: Logger.Level = .info 72 | /// 73 | /// // metadata storage 74 | /// public var metadata: Logger.Metadata = [:] 75 | /// 76 | /// public init(label: String) { 77 | /// // [...] 78 | /// } 79 | /// 80 | /// public var logLevel: Logger.Level { 81 | /// // when we get asked for the log level, we check if it was globally overridden or not 82 | /// get { 83 | /// LogHandlerWithGlobalLogLevelOverride.overrideLock.lock() 84 | /// defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() } 85 | /// return LogHandlerWithGlobalLogLevelOverride.overrideLogLevel ?? self._logLevel 86 | /// } 87 | /// // we set the log level whenever we're asked (note: this might not have an effect if globally 88 | /// // overridden) 89 | /// set { 90 | /// self._logLevel = newValue 91 | /// } 92 | /// } 93 | /// 94 | /// public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, 95 | /// source: String, file: String, function: String, line: UInt) { 96 | /// // [...] 97 | /// } 98 | /// 99 | /// public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { 100 | /// get { 101 | /// return self.metadata[metadataKey] 102 | /// } 103 | /// set(newValue) { 104 | /// self.metadata[metadataKey] = newValue 105 | /// } 106 | /// } 107 | /// 108 | /// // this is the function to globally override the log level, it is not part of the `LogHandler` protocol 109 | /// public static func overrideGlobalLogLevel(_ logLevel: Logger.Level) { 110 | /// LogHandlerWithGlobalLogLevelOverride.overrideLock.lock() 111 | /// defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() } 112 | /// LogHandlerWithGlobalLogLevelOverride.overrideLogLevel = logLevel 113 | /// } 114 | /// } 115 | /// ``` 116 | /// 117 | /// Please note that the above `LogHandler` will still pass the 'log level is a value' test above it iff the global log 118 | /// level has not been overridden. And most importantly it passes the requirement listed above: A change to the log 119 | /// level on one `Logger` should not affect the log level of another `Logger` variable. 120 | public protocol LogHandler: _SwiftLogSendableLogHandler { 121 | /// The metadata provider this `LogHandler` will use when a log statement is about to be emitted. 122 | /// 123 | /// A ``Logger/MetadataProvider`` may add a constant set of metadata, 124 | /// or use task-local values to pick up contextual metadata and add it to emitted logs. 125 | var metadataProvider: Logger.MetadataProvider? { get set } 126 | 127 | /// This method is called when a `LogHandler` must emit a log message. There is no need for the `LogHandler` to 128 | /// check if the `level` is above or below the configured `logLevel` as `Logger` already performed this check and 129 | /// determined that a message should be logged. 130 | /// 131 | /// - parameters: 132 | /// - level: The log level the message was logged at. 133 | /// - message: The message to log. To obtain a `String` representation call `message.description`. 134 | /// - metadata: The metadata associated to this log message. 135 | /// - source: The source where the log message originated, for example the logging module. 136 | /// - file: The file the log message was emitted from. 137 | /// - function: The function the log line was emitted from. 138 | /// - line: The line the log message was emitted from. 139 | func log( 140 | level: Logger.Level, 141 | message: Logger.Message, 142 | metadata: Logger.Metadata?, 143 | source: String, 144 | file: String, 145 | function: String, 146 | line: UInt 147 | ) 148 | 149 | /// SwiftLog 1.0 compatibility method. Please do _not_ implement, implement 150 | /// `log(level:message:metadata:source:file:function:line:)` instead. 151 | @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") 152 | func log( 153 | level: Logging.Logger.Level, 154 | message: Logging.Logger.Message, 155 | metadata: Logging.Logger.Metadata?, 156 | file: String, 157 | function: String, 158 | line: UInt 159 | ) 160 | 161 | /// Add, remove, or change the logging metadata. 162 | /// 163 | /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must 164 | /// only affect this very `LogHandler`. 165 | /// 166 | /// - parameters: 167 | /// - metadataKey: The key for the metadata item 168 | subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set } 169 | 170 | /// Get or set the entire metadata storage as a dictionary. 171 | /// 172 | /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must 173 | /// only affect this very `LogHandler`. 174 | var metadata: Logger.Metadata { get set } 175 | 176 | /// Get or set the configured log level. 177 | /// 178 | /// - note: `LogHandler`s must treat the log level as a value type. This means that the change in metadata must 179 | /// only affect this very `LogHandler`. It is acceptable to provide some form of global log level override 180 | /// that means a change in log level on a particular `LogHandler` might not be reflected in any 181 | /// `LogHandler`. 182 | var logLevel: Logger.Level { get set } 183 | } 184 | 185 | extension LogHandler { 186 | /// Default implementation for `metadataProvider` which defaults to `nil`. 187 | /// This default exists in order to facilitate source-compatible introduction of the `metadataProvider` protocol requirement. 188 | public var metadataProvider: Logger.MetadataProvider? { 189 | get { 190 | nil 191 | } 192 | set { 193 | #if DEBUG 194 | if LoggingSystem.warnOnceLogHandlerNotSupportedMetadataProvider(Self.self) { 195 | self.log( 196 | level: .warning, 197 | message: 198 | "Attempted to set metadataProvider on \(Self.self) that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.", 199 | metadata: nil, 200 | source: "Logging", 201 | file: #file, 202 | function: #function, 203 | line: #line 204 | ) 205 | } 206 | #endif 207 | } 208 | } 209 | } 210 | 211 | extension LogHandler { 212 | @available(*, deprecated, message: "You should implement this method instead of using the default implementation") 213 | public func log( 214 | level: Logger.Level, 215 | message: Logger.Message, 216 | metadata: Logger.Metadata?, 217 | source: String, 218 | file: String, 219 | function: String, 220 | line: UInt 221 | ) { 222 | self.log(level: level, message: message, metadata: metadata, file: file, function: function, line: line) 223 | } 224 | 225 | @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") 226 | public func log( 227 | level: Logging.Logger.Level, 228 | message: Logging.Logger.Message, 229 | metadata: Logging.Logger.Metadata?, 230 | file: String, 231 | function: String, 232 | line: UInt 233 | ) { 234 | self.log( 235 | level: level, 236 | message: message, 237 | metadata: metadata, 238 | source: Logger.currentModule(filePath: file), 239 | file: file, 240 | function: function, 241 | line: line 242 | ) 243 | } 244 | } 245 | 246 | // MARK: - Sendable support helpers 247 | 248 | @preconcurrency public protocol _SwiftLogSendableLogHandler: Sendable {} 249 | -------------------------------------------------------------------------------- /Sources/Logging/MetadataProvider.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2022 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(Darwin) 16 | import Darwin 17 | #elseif os(Windows) 18 | import CRT 19 | #elseif canImport(Glibc) 20 | import Glibc 21 | #elseif canImport(Android) 22 | import Android 23 | #elseif canImport(Musl) 24 | import Musl 25 | #elseif canImport(WASILibc) 26 | import WASILibc 27 | #else 28 | #error("Unsupported runtime") 29 | #endif 30 | 31 | @preconcurrency protocol _SwiftLogSendable: Sendable {} 32 | 33 | extension Logger { 34 | /// A `MetadataProvider` is used to automatically inject runtime-generated metadata 35 | /// to all logs emitted by a logger. 36 | /// 37 | /// ### Example 38 | /// A metadata provider may be used to automatically inject metadata such as 39 | /// trace IDs: 40 | /// 41 | /// ```swift 42 | /// import Tracing // https://github.com/apple/swift-distributed-tracing 43 | /// 44 | /// let metadataProvider = MetadataProvider { 45 | /// guard let traceID = Baggage.current?.traceID else { return nil } 46 | /// return ["traceID": "\(traceID)"] 47 | /// } 48 | /// let logger = Logger(label: "example", metadataProvider: metadataProvider) 49 | /// var baggage = Baggage.topLevel 50 | /// baggage.traceID = 42 51 | /// Baggage.withValue(baggage) { 52 | /// logger.info("hello") // automatically includes ["traceID": "42"] metadata 53 | /// } 54 | /// ``` 55 | /// 56 | /// We recommend referring to [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing) 57 | /// for metadata providers which make use of its tracing and metadata propagation infrastructure. It is however 58 | /// possible to make use of metadata providers independently of tracing and instruments provided by that library, 59 | /// if necessary. 60 | public struct MetadataProvider: _SwiftLogSendable { 61 | /// Provide ``Logger.Metadata`` from current context. 62 | @usableFromInline 63 | internal let _provideMetadata: @Sendable () -> Metadata 64 | 65 | /// Create a new `MetadataProvider`. 66 | /// 67 | /// - Parameter provideMetadata: A closure extracting metadata from the current execution context. 68 | public init(_ provideMetadata: @escaping @Sendable () -> Metadata) { 69 | self._provideMetadata = provideMetadata 70 | } 71 | 72 | /// Invoke the metadata provider and return the generated contextual ``Logger/Metadata``. 73 | public func get() -> Metadata { 74 | self._provideMetadata() 75 | } 76 | } 77 | } 78 | 79 | extension Logger.MetadataProvider { 80 | /// A pseudo-`MetadataProvider` that can be used to merge metadata from multiple other `MetadataProvider`s. 81 | /// 82 | /// ### Merging conflicting keys 83 | /// 84 | /// `MetadataProvider`s are invoked left to right in the order specified in the `providers` argument. 85 | /// In case multiple providers try to add a value for the same key, the last provider "wins" and its value is being used. 86 | /// 87 | /// - Parameter providers: An array of `MetadataProvider`s to delegate to. The array must not be empty. 88 | /// - Returns: A pseudo-`MetadataProvider` merging metadata from the given `MetadataProvider`s. 89 | public static func multiplex(_ providers: [Logger.MetadataProvider]) -> Logger.MetadataProvider? { 90 | assert(!providers.isEmpty, "providers MUST NOT be empty") 91 | return Logger.MetadataProvider { 92 | providers.reduce(into: [:]) { metadata, provider in 93 | let providedMetadata = provider.get() 94 | guard !providedMetadata.isEmpty else { 95 | return 96 | } 97 | metadata.merge(providedMetadata, uniquingKeysWith: { _, rhs in rhs }) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/LoggingTests/CompatibilityTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // Not testable 16 | import Logging 17 | import XCTest 18 | 19 | final class CompatibilityTest: XCTestCase { 20 | @available(*, deprecated, message: "Testing deprecated functionality") 21 | func testAllLogLevelsWorkWithOldSchoolLogHandlerWorks() { 22 | let testLogging = OldSchoolTestLogging() 23 | 24 | var logger = Logger(label: "\(#function)", factory: { testLogging.make(label: $0) }) 25 | logger.logLevel = .trace 26 | 27 | logger.trace("yes: trace") 28 | logger.debug("yes: debug") 29 | logger.info("yes: info") 30 | logger.notice("yes: notice") 31 | logger.warning("yes: warning") 32 | logger.error("yes: error") 33 | logger.critical("yes: critical", source: "any also with some new argument that isn't propagated") 34 | 35 | // Please note that the source is _not_ propagated (because the backend doesn't support it). 36 | testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "no source") 37 | testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "no source") 38 | testLogging.history.assertExist(level: .info, message: "yes: info", source: "no source") 39 | testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "no source") 40 | testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "no source") 41 | testLogging.history.assertExist(level: .error, message: "yes: error", source: "no source") 42 | testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "no source") 43 | } 44 | } 45 | 46 | private struct OldSchoolTestLogging { 47 | private let _config = Config() // shared among loggers 48 | private let recorder = Recorder() // shared among loggers 49 | 50 | @available(*, deprecated, message: "Testing deprecated functionality") 51 | func make(label: String) -> any LogHandler { 52 | OldSchoolLogHandler( 53 | label: label, 54 | config: self.config, 55 | recorder: self.recorder, 56 | metadata: [:], 57 | logLevel: .info 58 | ) 59 | } 60 | 61 | var config: Config { self._config } 62 | var history: some History { self.recorder } 63 | } 64 | 65 | @available(*, deprecated, message: "Testing deprecated functionality") 66 | private struct OldSchoolLogHandler: LogHandler { 67 | var label: String 68 | let config: Config 69 | let recorder: Recorder 70 | 71 | func make(label: String) -> some LogHandler { 72 | TestLogHandler(label: label, config: self.config, recorder: self.recorder) 73 | } 74 | 75 | func log( 76 | level: Logger.Level, 77 | message: Logger.Message, 78 | metadata: Logger.Metadata?, 79 | file: String, 80 | function: String, 81 | line: UInt 82 | ) { 83 | self.recorder.record(level: level, metadata: metadata, message: message, source: "no source") 84 | } 85 | 86 | subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { 87 | get { 88 | self.metadata[metadataKey] 89 | } 90 | set { 91 | self.metadata[metadataKey] = newValue 92 | } 93 | } 94 | 95 | var metadata: Logger.Metadata 96 | 97 | var logLevel: Logger.Level 98 | } 99 | -------------------------------------------------------------------------------- /Tests/LoggingTests/GlobalLoggingTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | #if compiler(>=6.0) || canImport(Darwin) 20 | import Dispatch 21 | #else 22 | @preconcurrency import Dispatch 23 | #endif 24 | 25 | class GlobalLoggerTest: XCTestCase { 26 | func test1() throws { 27 | // bootstrap with our test logging impl 28 | let logging = TestLogging() 29 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 30 | 31 | // change test logging config to log traces and above 32 | logging.config.set(value: Logger.Level.debug) 33 | // run our program 34 | Struct1().doSomething() 35 | // test results 36 | logging.history.assertExist(level: .debug, message: "Struct1::doSomething") 37 | logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse") 38 | logging.history.assertExist(level: .info, message: "Struct2::doSomething") 39 | logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse") 40 | logging.history.assertExist(level: .error, message: "Struct3::doSomething") 41 | logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"]) 42 | logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["foo": "bar"]) 43 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"]) 44 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"]) 45 | logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::Local", metadata: ["baz": "qux"]) 46 | logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end") 47 | logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end") 48 | logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end") 49 | logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse::end") 50 | logging.history.assertExist(level: .debug, message: "Struct1::doSomething::end") 51 | } 52 | 53 | func test2() throws { 54 | // bootstrap with our test logging impl 55 | let logging = TestLogging() 56 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 57 | 58 | // change test logging config to log errors and above 59 | logging.config.set(value: Logger.Level.error) 60 | // run our program 61 | Struct1().doSomething() 62 | // test results 63 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething") 64 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse") 65 | logging.history.assertNotExist(level: .info, message: "Struct2::doSomething") 66 | logging.history.assertNotExist(level: .info, message: "Struct2::doSomethingElse") 67 | logging.history.assertExist(level: .error, message: "Struct3::doSomething") 68 | logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"]) 69 | logging.history.assertNotExist( 70 | level: .warning, 71 | message: "Struct3::doSomethingElseAsync", 72 | metadata: ["foo": "bar"] 73 | ) 74 | logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"]) 75 | logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"]) 76 | logging.history.assertNotExist( 77 | level: .debug, 78 | message: "Struct3::doSomethingElse::Local", 79 | metadata: ["baz": "qux"] 80 | ) 81 | logging.history.assertNotExist(level: .debug, message: "Struct3::doSomethingElse::end") 82 | logging.history.assertNotExist(level: .debug, message: "Struct3::doSomething::end") 83 | logging.history.assertNotExist(level: .debug, message: "Struct2::doSomethingElse::end") 84 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end") 85 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end") 86 | } 87 | 88 | func test3() throws { 89 | // bootstrap with our test logging impl 90 | let logging = TestLogging() 91 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 92 | 93 | // change test logging config 94 | logging.config.set(value: .warning) 95 | logging.config.set(key: "GlobalLoggerTest::Struct2", value: .info) 96 | logging.config.set(key: "TestLibrary", value: .debug) 97 | // run our program 98 | Struct1().doSomething() 99 | // test results 100 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething") 101 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse") 102 | logging.history.assertExist(level: .info, message: "Struct2::doSomething") 103 | logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse") 104 | logging.history.assertExist(level: .error, message: "Struct3::doSomething") 105 | logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"]) 106 | logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["foo": "bar"]) 107 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"]) 108 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"]) 109 | logging.history.assertNotExist( 110 | level: .debug, 111 | message: "Struct3::doSomethingElse::Local", 112 | metadata: ["baz": "qux"] 113 | ) 114 | logging.history.assertNotExist(level: .debug, message: "Struct3::doSomethingElse::end") 115 | logging.history.assertNotExist(level: .debug, message: "Struct3::doSomething::end") 116 | logging.history.assertNotExist(level: .debug, message: "Struct2::doSomethingElse::end") 117 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end") 118 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end") 119 | } 120 | } 121 | 122 | private struct Struct1 { 123 | private let logger = Logger(label: "GlobalLoggerTest::Struct1") 124 | 125 | func doSomething() { 126 | self.logger.debug("Struct1::doSomething") 127 | self.doSomethingElse() 128 | self.logger.debug("Struct1::doSomething::end") 129 | } 130 | 131 | private func doSomethingElse() { 132 | self.logger.debug("Struct1::doSomethingElse") 133 | Struct2().doSomething() 134 | self.logger.debug("Struct1::doSomethingElse::end") 135 | } 136 | } 137 | 138 | private struct Struct2 { 139 | let logger = Logger(label: "GlobalLoggerTest::Struct2") 140 | 141 | func doSomething() { 142 | self.logger.info("Struct2::doSomething") 143 | self.doSomethingElse() 144 | self.logger.debug("Struct2::doSomething::end") 145 | } 146 | 147 | private func doSomethingElse() { 148 | self.logger.info("Struct2::doSomethingElse") 149 | Struct3().doSomething() 150 | self.logger.debug("Struct2::doSomethingElse::end") 151 | } 152 | } 153 | 154 | private struct Struct3 { 155 | private let logger = Logger(label: "GlobalLoggerTest::Struct3") 156 | private let queue = DispatchQueue(label: "GlobalLoggerTest::Struct3") 157 | 158 | func doSomething() { 159 | self.logger.error("Struct3::doSomething") 160 | self.doSomethingElse() 161 | self.logger.debug("Struct3::doSomething::end") 162 | } 163 | 164 | private func doSomethingElse() { 165 | MDC.global["foo"] = "bar" 166 | self.logger.error("Struct3::doSomethingElse") 167 | let group = DispatchGroup() 168 | group.enter() 169 | let loggingMetadata = MDC.global.metadata 170 | self.queue.async { 171 | MDC.global.with(metadata: loggingMetadata) { 172 | self.logger.warning("Struct3::doSomethingElseAsync") 173 | let library = TestLibrary() 174 | library.doSomething() 175 | library.doSomethingAsync { 176 | group.leave() 177 | } 178 | } 179 | } 180 | group.wait() 181 | MDC.global["foo"] = nil 182 | // only effects the logger instance 183 | var l = self.logger 184 | l[metadataKey: "baz"] = "qux" 185 | l.debug("Struct3::doSomethingElse::Local") 186 | self.logger.debug("Struct3::doSomethingElse::end") 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Tests/LoggingTests/LocalLoggingTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | #if compiler(>=6.0) || canImport(Darwin) 20 | import Dispatch 21 | #else 22 | @preconcurrency import Dispatch 23 | #endif 24 | 25 | class LocalLoggerTest: XCTestCase { 26 | func test1() throws { 27 | // bootstrap with our test logging impl 28 | let logging = TestLogging() 29 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 30 | 31 | // change test logging config to log traces and above 32 | logging.config.set(value: Logger.Level.debug) 33 | // run our program 34 | let context = Context() 35 | Struct1().doSomething(context: context) 36 | // test results 37 | logging.history.assertExist(level: .debug, message: "Struct1::doSomething", source: "LoggingTests") 38 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse") 39 | logging.history.assertExist(level: .info, message: "Struct2::doSomething") 40 | logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse") 41 | logging.history.assertExist(level: .error, message: "Struct3::doSomething", metadata: ["bar": "baz"]) 42 | logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["bar": "baz"]) 43 | logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["bar": "baz"]) 44 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomething") 45 | logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync") 46 | logging.history.assertExist( 47 | level: .debug, 48 | message: "Struct3::doSomethingElse::Local", 49 | metadata: ["bar": "baz", "baz": "qux"] 50 | ) 51 | logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end", metadata: ["bar": "baz"]) 52 | logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end", metadata: ["bar": "baz"]) 53 | logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end") 54 | logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse::end") 55 | logging.history.assertExist(level: .debug, message: "Struct1::doSomething::end") 56 | } 57 | 58 | func test2() throws { 59 | // bootstrap with our test logging impl 60 | let logging = TestLogging() 61 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 62 | 63 | // change test logging config to log errors and above 64 | logging.config.set(value: Logger.Level.error) 65 | // run our program 66 | let context = Context() 67 | Struct1().doSomething(context: context) 68 | // test results 69 | // global context 70 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething") 71 | // global context 72 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse") 73 | // local context 74 | logging.history.assertExist(level: .info, message: "Struct2::doSomething") 75 | // local context 76 | logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse") 77 | // local context 78 | logging.history.assertExist(level: .error, message: "Struct3::doSomething", metadata: ["bar": "baz"]) 79 | // local context 80 | logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["bar": "baz"]) 81 | // local context 82 | logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["bar": "baz"]) 83 | // global context 84 | logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomething") 85 | // global context 86 | logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomethingAsync") 87 | // hyper local context 88 | logging.history.assertExist( 89 | level: .debug, 90 | message: "Struct3::doSomethingElse::Local", 91 | metadata: ["bar": "baz", "baz": "qux"] 92 | ) 93 | // local context 94 | logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end", metadata: ["bar": "baz"]) 95 | // local context 96 | logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end") 97 | // local context 98 | logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end", metadata: ["bar": "baz"]) 99 | // global context 100 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end") 101 | // global context 102 | logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end") 103 | } 104 | } 105 | 106 | // systems that follow the context pattern need to implement something like this 107 | private struct Context { 108 | var logger = Logger(label: "LocalLoggerTest::ContextLogger") 109 | 110 | // since logger is a value type, we can reuse our copy to manage logLevel 111 | var logLevel: Logger.Level { 112 | get { self.logger.logLevel } 113 | set { self.logger.logLevel = newValue } 114 | } 115 | 116 | // since logger is a value type, we can reuse our copy to manage metadata 117 | subscript(metadataKey: String) -> Logger.Metadata.Value? { 118 | get { self.logger[metadataKey: metadataKey] } 119 | set { self.logger[metadataKey: metadataKey] = newValue } 120 | } 121 | } 122 | 123 | private struct Struct1 { 124 | func doSomething(context: Context) { 125 | context.logger.debug("Struct1::doSomething") 126 | self.doSomethingElse(context: context) 127 | context.logger.debug("Struct1::doSomething::end") 128 | } 129 | 130 | private func doSomethingElse(context: Context) { 131 | let originalContext = context 132 | var context = context 133 | context.logger.logLevel = .warning 134 | context.logger.debug("Struct1::doSomethingElse") 135 | Struct2().doSomething(context: context) 136 | originalContext.logger.debug("Struct1::doSomethingElse::end") 137 | } 138 | } 139 | 140 | private struct Struct2 { 141 | func doSomething(context: Context) { 142 | var c = context 143 | c.logLevel = .info // only effects from this point on 144 | c.logger.info("Struct2::doSomething") 145 | self.doSomethingElse(context: c) 146 | c.logger.debug("Struct2::doSomething::end") 147 | } 148 | 149 | private func doSomethingElse(context: Context) { 150 | var c = context 151 | c.logLevel = .debug // only effects from this point on 152 | c.logger.info("Struct2::doSomethingElse") 153 | Struct3().doSomething(context: c) 154 | c.logger.debug("Struct2::doSomethingElse::end") 155 | } 156 | } 157 | 158 | private struct Struct3 { 159 | private let queue = DispatchQueue(label: "LocalLoggerTest::Struct3") 160 | 161 | func doSomething(context: Context) { 162 | var c = context 163 | c["bar"] = "baz" // only effects from this point on 164 | c.logger.error("Struct3::doSomething") 165 | self.doSomethingElse(context: c) 166 | c.logger.debug("Struct3::doSomething::end") 167 | } 168 | 169 | private func doSomethingElse(context: Context) { 170 | context.logger.error("Struct3::doSomethingElse") 171 | let group = DispatchGroup() 172 | group.enter() 173 | self.queue.async { 174 | context.logger.warning("Struct3::doSomethingElseAsync") 175 | let library = TestLibrary() 176 | library.doSomething() 177 | library.doSomethingAsync { 178 | group.leave() 179 | } 180 | } 181 | group.wait() 182 | // only effects the logger instance 183 | var l = context.logger 184 | l[metadataKey: "baz"] = "qux" 185 | l.debug("Struct3::doSomethingElse::Local") 186 | context.logger.debug("Struct3::doSomethingElse::end") 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Tests/LoggingTests/LoggingTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | #if canImport(Darwin) 20 | import Darwin 21 | #elseif os(Windows) 22 | import WinSDK 23 | #elseif canImport(Android) 24 | import Android 25 | #else 26 | import Glibc 27 | #endif 28 | 29 | extension LogHandler { 30 | fileprivate func with(logLevel: Logger.Level) -> any LogHandler { 31 | var result = self 32 | result.logLevel = logLevel 33 | return result 34 | } 35 | 36 | fileprivate func withMetadata(_ key: String, _ value: Logger.MetadataValue) -> any LogHandler { 37 | var result = self 38 | result.metadata[key] = value 39 | return result 40 | } 41 | } 42 | 43 | class LoggingTest: XCTestCase { 44 | func testAutoclosure() throws { 45 | // bootstrap with our test logging impl 46 | let logging = TestLogging() 47 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 48 | 49 | var logger = Logger(label: "test") 50 | logger.logLevel = .info 51 | logger.log( 52 | level: .debug, 53 | { 54 | XCTFail("debug should not be called") 55 | return "debug" 56 | }() 57 | ) 58 | logger.trace( 59 | { 60 | XCTFail("trace should not be called") 61 | return "trace" 62 | }() 63 | ) 64 | logger.debug( 65 | { 66 | XCTFail("debug should not be called") 67 | return "debug" 68 | }() 69 | ) 70 | logger.info( 71 | { 72 | "info" 73 | }() 74 | ) 75 | logger.warning( 76 | { 77 | "warning" 78 | }() 79 | ) 80 | logger.error( 81 | { 82 | "error" 83 | }() 84 | ) 85 | XCTAssertEqual(3, logging.history.entries.count, "expected number of entries to match") 86 | logging.history.assertNotExist(level: .debug, message: "trace") 87 | logging.history.assertNotExist(level: .debug, message: "debug") 88 | logging.history.assertExist(level: .info, message: "info") 89 | logging.history.assertExist(level: .warning, message: "warning") 90 | logging.history.assertExist(level: .error, message: "error") 91 | } 92 | 93 | func testMultiplex() throws { 94 | // bootstrap with our test logging impl 95 | let logging1 = TestLogging() 96 | let logging2 = TestLogging() 97 | LoggingSystem.bootstrapInternal { MultiplexLogHandler([logging1.make(label: $0), logging2.make(label: $0)]) } 98 | 99 | var logger = Logger(label: "test") 100 | logger.logLevel = .warning 101 | logger.info("hello world?") 102 | logger[metadataKey: "foo"] = "bar" 103 | logger.warning("hello world!") 104 | logging1.history.assertNotExist(level: .info, message: "hello world?") 105 | logging2.history.assertNotExist(level: .info, message: "hello world?") 106 | logging1.history.assertExist(level: .warning, message: "hello world!", metadata: ["foo": "bar"]) 107 | logging2.history.assertExist(level: .warning, message: "hello world!", metadata: ["foo": "bar"]) 108 | } 109 | 110 | func testMultiplexLogHandlerWithVariousLogLevels() throws { 111 | let logging1 = TestLogging() 112 | let logging2 = TestLogging() 113 | 114 | let logger1 = logging1.make(label: "1").with(logLevel: .info) 115 | let logger2 = logging2.make(label: "2").with(logLevel: .debug) 116 | 117 | LoggingSystem.bootstrapInternal { _ in 118 | MultiplexLogHandler([logger1, logger2]) 119 | } 120 | 121 | let multiplexLogger = Logger(label: "test") 122 | multiplexLogger.trace("trace") 123 | multiplexLogger.debug("debug") 124 | multiplexLogger.info("info") 125 | multiplexLogger.warning("warning") 126 | 127 | logging1.history.assertNotExist(level: .trace, message: "trace") 128 | logging1.history.assertNotExist(level: .debug, message: "debug") 129 | logging1.history.assertExist(level: .info, message: "info") 130 | logging1.history.assertExist(level: .warning, message: "warning") 131 | 132 | logging2.history.assertNotExist(level: .trace, message: "trace") 133 | logging2.history.assertExist(level: .debug, message: "debug") 134 | logging2.history.assertExist(level: .info, message: "info") 135 | logging2.history.assertExist(level: .warning, message: "warning") 136 | } 137 | 138 | func testMultiplexLogHandlerNeedNotMaterializeValuesMultipleTimes() throws { 139 | let logging1 = TestLogging() 140 | let logging2 = TestLogging() 141 | 142 | let logger1 = logging1.make(label: "1").with(logLevel: .info) 143 | let logger2 = logging2.make(label: "2").with(logLevel: .info) 144 | 145 | LoggingSystem.bootstrapInternal { _ in 146 | MultiplexLogHandler([logger1, logger2]) 147 | } 148 | 149 | var messageMaterializations: Int = 0 150 | var metadataMaterializations: Int = 0 151 | 152 | let multiplexLogger = Logger(label: "test") 153 | multiplexLogger.info( 154 | { () -> Logger.Message in 155 | messageMaterializations += 1 156 | return "info" 157 | }(), 158 | metadata: { () -> Logger.Metadata in 159 | metadataMaterializations += 1 160 | return [:] 161 | }() 162 | ) 163 | 164 | logging1.history.assertExist(level: .info, message: "info") 165 | logging2.history.assertExist(level: .info, message: "info") 166 | 167 | XCTAssertEqual(messageMaterializations, 1) 168 | XCTAssertEqual(metadataMaterializations, 1) 169 | } 170 | 171 | func testMultiplexLogHandlerMetadata_settingMetadataThroughToUnderlyingHandlers() { 172 | let logging1 = TestLogging() 173 | let logging2 = TestLogging() 174 | 175 | let logger1 = logging1.make(label: "1") 176 | .withMetadata("one", "111") 177 | .withMetadata("in", "in-1") 178 | let logger2 = logging2.make(label: "2") 179 | .withMetadata("two", "222") 180 | .withMetadata("in", "in-2") 181 | 182 | LoggingSystem.bootstrapInternal { _ in 183 | MultiplexLogHandler([logger1, logger2]) 184 | } 185 | 186 | var multiplexLogger = Logger(label: "test") 187 | 188 | // each logs its own metadata 189 | multiplexLogger.info("info") 190 | logging1.history.assertExist( 191 | level: .info, 192 | message: "info", 193 | metadata: [ 194 | "one": "111", 195 | "in": "in-1", 196 | ] 197 | ) 198 | logging2.history.assertExist( 199 | level: .info, 200 | message: "info", 201 | metadata: [ 202 | "two": "222", 203 | "in": "in-2", 204 | ] 205 | ) 206 | 207 | // if modified, change applies to both underlying handlers 208 | multiplexLogger[metadataKey: "new"] = "new" 209 | multiplexLogger.info("info") 210 | logging1.history.assertExist( 211 | level: .info, 212 | message: "info", 213 | metadata: [ 214 | "one": "111", 215 | "in": "in-1", 216 | "new": "new", 217 | ] 218 | ) 219 | logging2.history.assertExist( 220 | level: .info, 221 | message: "info", 222 | metadata: [ 223 | "two": "222", 224 | "in": "in-2", 225 | "new": "new", 226 | ] 227 | ) 228 | 229 | // overriding an existing value works the same way as adding a new one 230 | multiplexLogger[metadataKey: "in"] = "multi" 231 | multiplexLogger.info("info") 232 | logging1.history.assertExist( 233 | level: .info, 234 | message: "info", 235 | metadata: [ 236 | "one": "111", 237 | "in": "multi", 238 | "new": "new", 239 | ] 240 | ) 241 | logging2.history.assertExist( 242 | level: .info, 243 | message: "info", 244 | metadata: [ 245 | "two": "222", 246 | "in": "multi", 247 | "new": "new", 248 | ] 249 | ) 250 | } 251 | 252 | func testMultiplexLogHandlerMetadata_readingHandlerMetadata() { 253 | let logging1 = TestLogging() 254 | let logging2 = TestLogging() 255 | 256 | let logger1 = logging1.make(label: "1") 257 | .withMetadata("one", "111") 258 | .withMetadata("in", "in-1") 259 | let logger2 = logging2.make(label: "2") 260 | .withMetadata("two", "222") 261 | .withMetadata("in", "in-2") 262 | 263 | LoggingSystem.bootstrapInternal { _ in 264 | MultiplexLogHandler([logger1, logger2]) 265 | } 266 | 267 | let multiplexLogger = Logger(label: "test") 268 | 269 | XCTAssertEqual( 270 | multiplexLogger.handler.metadata, 271 | [ 272 | "one": "111", 273 | "two": "222", 274 | "in": "in-2", 275 | ] 276 | ) 277 | } 278 | 279 | func testMultiplexMetadataProviderSet() { 280 | let logging1 = TestLogging() 281 | let logging2 = TestLogging() 282 | 283 | let handler1 = { 284 | var handler1 = logging1.make(label: "1") 285 | handler1.metadata["one"] = "111" 286 | handler1.metadata["in"] = "in-1" 287 | handler1.metadataProvider = .constant([ 288 | "provider-1": "provided-111", 289 | "provider-overlap": "provided-111", 290 | ]) 291 | return handler1 292 | }() 293 | let handler2 = { 294 | var handler2 = logging2.make(label: "2") 295 | handler2.metadata["two"] = "222" 296 | handler2.metadata["in"] = "in-2" 297 | handler2.metadataProvider = .constant([ 298 | "provider-2": "provided-222", 299 | "provider-overlap": "provided-222", 300 | ]) 301 | return handler2 302 | }() 303 | 304 | LoggingSystem.bootstrapInternal { _ in 305 | MultiplexLogHandler([handler1, handler2]) 306 | } 307 | 308 | let multiplexLogger = Logger(label: "test") 309 | 310 | XCTAssertEqual( 311 | multiplexLogger.handler.metadata, 312 | [ 313 | "one": "111", 314 | "two": "222", 315 | "in": "in-2", 316 | "provider-1": "provided-111", 317 | "provider-2": "provided-222", 318 | "provider-overlap": "provided-222", 319 | ] 320 | ) 321 | XCTAssertEqual( 322 | multiplexLogger.handler.metadataProvider?.get(), 323 | [ 324 | "provider-1": "provided-111", 325 | "provider-2": "provided-222", 326 | "provider-overlap": "provided-222", 327 | ] 328 | ) 329 | } 330 | 331 | func testMultiplexMetadataProviderExtract() { 332 | let logging1 = TestLogging() 333 | let logging2 = TestLogging() 334 | 335 | let handler1 = { 336 | var handler1 = logging1.make(label: "1") 337 | handler1.metadataProvider = .constant([ 338 | "provider-1": "provided-111", 339 | "provider-overlap": "provided-111", 340 | ]) 341 | return handler1 342 | }() 343 | let handler2 = { 344 | var handler2 = logging2.make(label: "2") 345 | handler2.metadata["two"] = "222" 346 | handler2.metadata["in"] = "in-2" 347 | handler2.metadataProvider = .constant([ 348 | "provider-2": "provided-222", 349 | "provider-overlap": "provided-222", 350 | ]) 351 | return handler2 352 | }() 353 | 354 | LoggingSystem.bootstrapInternal( 355 | { _, metadataProvider in 356 | MultiplexLogHandler( 357 | [handler1, handler2], 358 | metadataProvider: metadataProvider 359 | ) 360 | }, 361 | metadataProvider: .constant([ 362 | "provider-overlap": "provided-outer" 363 | ]) 364 | ) 365 | 366 | let multiplexLogger = Logger(label: "test") 367 | 368 | let provider = multiplexLogger.metadataProvider! 369 | 370 | XCTAssertEqual( 371 | provider.get(), 372 | [ 373 | "provider-1": "provided-111", 374 | "provider-2": "provided-222", 375 | "provider-overlap": "provided-outer", 376 | ] 377 | ) 378 | } 379 | 380 | enum TestError: Error { 381 | case boom 382 | } 383 | 384 | func testDictionaryMetadata() { 385 | let testLogging = TestLogging() 386 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 387 | 388 | var logger = Logger(label: "\(#function)") 389 | logger[metadataKey: "foo"] = ["bar": "buz"] 390 | logger[metadataKey: "empty-dict"] = [:] 391 | logger[metadataKey: "nested-dict"] = ["l1key": ["l2key": ["l3key": "l3value"]]] 392 | logger.info("hello world!") 393 | testLogging.history.assertExist( 394 | level: .info, 395 | message: "hello world!", 396 | metadata: [ 397 | "foo": ["bar": "buz"], 398 | "empty-dict": [:], 399 | "nested-dict": ["l1key": ["l2key": ["l3key": "l3value"]]], 400 | ] 401 | ) 402 | } 403 | 404 | func testListMetadata() { 405 | let testLogging = TestLogging() 406 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 407 | 408 | var logger = Logger(label: "\(#function)") 409 | logger[metadataKey: "foo"] = ["bar", "buz"] 410 | logger[metadataKey: "empty-list"] = [] 411 | logger[metadataKey: "nested-list"] = ["l1str", ["l2str1", "l2str2"]] 412 | logger.info("hello world!") 413 | testLogging.history.assertExist( 414 | level: .info, 415 | message: "hello world!", 416 | metadata: [ 417 | "foo": ["bar", "buz"], 418 | "empty-list": [], 419 | "nested-list": ["l1str", ["l2str1", "l2str2"]], 420 | ] 421 | ) 422 | } 423 | 424 | // Example of custom "box" which may be used to implement "render at most once" semantics 425 | // Not thread-safe, thus should not be shared across threads. 426 | internal final class LazyMetadataBox: CustomStringConvertible { 427 | private var makeValue: (() -> String)? 428 | private var _value: String? 429 | 430 | public init(_ makeValue: @escaping () -> String) { 431 | self.makeValue = makeValue 432 | } 433 | 434 | /// This allows caching a value in case it is accessed via an by name subscript, 435 | // rather than as part of rendering all metadata that a LoggingContext was carrying 436 | public var value: String { 437 | if let f = self.makeValue { 438 | self._value = f() 439 | self.makeValue = nil 440 | } 441 | 442 | assert(self._value != nil, "_value MUST NOT be nil once `lazyValue` has run.") 443 | return self._value! 444 | } 445 | 446 | public var description: String { 447 | "\(self.value)" 448 | } 449 | } 450 | 451 | func testStringConvertibleMetadata() { 452 | let testLogging = TestLogging() 453 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 454 | var logger = Logger(label: "\(#function)") 455 | 456 | logger[metadataKey: "foo"] = .stringConvertible("raw-string") 457 | let lazyBox = LazyMetadataBox { "rendered-at-first-use" } 458 | logger[metadataKey: "lazy"] = .stringConvertible(lazyBox) 459 | logger.info("hello world!") 460 | testLogging.history.assertExist( 461 | level: .info, 462 | message: "hello world!", 463 | metadata: [ 464 | "foo": .stringConvertible("raw-string"), 465 | "lazy": .stringConvertible(LazyMetadataBox { "rendered-at-first-use" }), 466 | ] 467 | ) 468 | } 469 | 470 | private func dontEvaluateThisString(file: StaticString = #filePath, line: UInt = #line) -> Logger.Message { 471 | XCTFail("should not have been evaluated", file: file, line: line) 472 | return "should not have been evaluated" 473 | } 474 | 475 | func testAutoClosuresAreNotForcedUnlessNeeded() { 476 | let testLogging = TestLogging() 477 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 478 | 479 | var logger = Logger(label: "\(#function)") 480 | logger.logLevel = .error 481 | 482 | logger.debug(self.dontEvaluateThisString(), metadata: ["foo": "\(self.dontEvaluateThisString())"]) 483 | logger.debug(self.dontEvaluateThisString()) 484 | logger.info(self.dontEvaluateThisString()) 485 | logger.warning(self.dontEvaluateThisString()) 486 | logger.log(level: .warning, self.dontEvaluateThisString()) 487 | } 488 | 489 | func testLocalMetadata() { 490 | let testLogging = TestLogging() 491 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 492 | 493 | var logger = Logger(label: "\(#function)") 494 | logger.info("hello world!", metadata: ["foo": "bar"]) 495 | logger[metadataKey: "bar"] = "baz" 496 | logger[metadataKey: "baz"] = "qux" 497 | logger.warning("hello world!") 498 | logger.error("hello world!", metadata: ["baz": "quc"]) 499 | testLogging.history.assertExist(level: .info, message: "hello world!", metadata: ["foo": "bar"]) 500 | testLogging.history.assertExist( 501 | level: .warning, 502 | message: "hello world!", 503 | metadata: ["bar": "baz", "baz": "qux"] 504 | ) 505 | testLogging.history.assertExist(level: .error, message: "hello world!", metadata: ["bar": "baz", "baz": "quc"]) 506 | } 507 | 508 | func testCustomFactory() { 509 | struct CustomHandler: LogHandler { 510 | func log( 511 | level: Logger.Level, 512 | message: Logger.Message, 513 | metadata: Logger.Metadata?, 514 | source: String, 515 | file: String, 516 | function: String, 517 | line: UInt 518 | ) {} 519 | 520 | subscript(metadataKey _: String) -> Logger.Metadata.Value? { 521 | get { nil } 522 | set {} 523 | } 524 | 525 | var metadata: Logger.Metadata { 526 | get { Logger.Metadata() } 527 | set {} 528 | } 529 | 530 | var logLevel: Logger.Level { 531 | get { .info } 532 | set {} 533 | } 534 | } 535 | 536 | let logger1 = Logger(label: "foo") 537 | XCTAssertFalse(logger1.handler is CustomHandler, "expected non-custom log handler") 538 | let logger2 = Logger(label: "foo", factory: { _ in CustomHandler() }) 539 | XCTAssertTrue(logger2.handler is CustomHandler, "expected custom log handler") 540 | } 541 | 542 | func testAllLogLevelsExceptCriticalCanBeBlocked() { 543 | let testLogging = TestLogging() 544 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 545 | 546 | var logger = Logger(label: "\(#function)") 547 | logger.logLevel = .critical 548 | 549 | logger.trace("no") 550 | logger.debug("no") 551 | logger.info("no") 552 | logger.notice("no") 553 | logger.warning("no") 554 | logger.error("no") 555 | logger.critical("yes: critical") 556 | 557 | testLogging.history.assertNotExist(level: .trace, message: "no") 558 | testLogging.history.assertNotExist(level: .debug, message: "no") 559 | testLogging.history.assertNotExist(level: .info, message: "no") 560 | testLogging.history.assertNotExist(level: .notice, message: "no") 561 | testLogging.history.assertNotExist(level: .warning, message: "no") 562 | testLogging.history.assertNotExist(level: .error, message: "no") 563 | testLogging.history.assertExist(level: .critical, message: "yes: critical") 564 | } 565 | 566 | func testAllLogLevelsWork() { 567 | let testLogging = TestLogging() 568 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 569 | 570 | var logger = Logger(label: "\(#function)") 571 | logger.logLevel = .trace 572 | 573 | logger.trace("yes: trace") 574 | logger.debug("yes: debug") 575 | logger.info("yes: info") 576 | logger.notice("yes: notice") 577 | logger.warning("yes: warning") 578 | logger.error("yes: error") 579 | logger.critical("yes: critical") 580 | 581 | testLogging.history.assertExist(level: .trace, message: "yes: trace") 582 | testLogging.history.assertExist(level: .debug, message: "yes: debug") 583 | testLogging.history.assertExist(level: .info, message: "yes: info") 584 | testLogging.history.assertExist(level: .notice, message: "yes: notice") 585 | testLogging.history.assertExist(level: .warning, message: "yes: warning") 586 | testLogging.history.assertExist(level: .error, message: "yes: error") 587 | testLogging.history.assertExist(level: .critical, message: "yes: critical") 588 | } 589 | 590 | func testAllLogLevelByFunctionRefWithSource() { 591 | let testLogging = TestLogging() 592 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 593 | 594 | var logger = Logger(label: "\(#function)") 595 | logger.logLevel = .trace 596 | 597 | let trace = logger.trace(_:metadata:source:file:function:line:) 598 | let debug = logger.debug(_:metadata:source:file:function:line:) 599 | let info = logger.info(_:metadata:source:file:function:line:) 600 | let notice = logger.notice(_:metadata:source:file:function:line:) 601 | let warning = logger.warning(_:metadata:source:file:function:line:) 602 | let error = logger.error(_:metadata:source:file:function:line:) 603 | let critical = logger.critical(_:metadata:source:file:function:line:) 604 | 605 | trace("yes: trace", [:], "foo", #file, #function, #line) 606 | debug("yes: debug", [:], "foo", #file, #function, #line) 607 | info("yes: info", [:], "foo", #file, #function, #line) 608 | notice("yes: notice", [:], "foo", #file, #function, #line) 609 | warning("yes: warning", [:], "foo", #file, #function, #line) 610 | error("yes: error", [:], "foo", #file, #function, #line) 611 | critical("yes: critical", [:], "foo", #file, #function, #line) 612 | 613 | testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "foo") 614 | testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "foo") 615 | testLogging.history.assertExist(level: .info, message: "yes: info", source: "foo") 616 | testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "foo") 617 | testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "foo") 618 | testLogging.history.assertExist(level: .error, message: "yes: error", source: "foo") 619 | testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "foo") 620 | } 621 | 622 | func testAllLogLevelByFunctionRefWithoutSource() { 623 | let testLogging = TestLogging() 624 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 625 | 626 | var logger = Logger(label: "\(#function)") 627 | logger.logLevel = .trace 628 | 629 | let trace = logger.trace(_:metadata:file:function:line:) 630 | let debug = logger.debug(_:metadata:file:function:line:) 631 | let info = logger.info(_:metadata:file:function:line:) 632 | let notice = logger.notice(_:metadata:file:function:line:) 633 | let warning = logger.warning(_:metadata:file:function:line:) 634 | let error = logger.error(_:metadata:file:function:line:) 635 | let critical = logger.critical(_:metadata:file:function:line:) 636 | 637 | trace("yes: trace", [:], #fileID, #function, #line) 638 | debug("yes: debug", [:], #fileID, #function, #line) 639 | info("yes: info", [:], #fileID, #function, #line) 640 | notice("yes: notice", [:], #fileID, #function, #line) 641 | warning("yes: warning", [:], #fileID, #function, #line) 642 | error("yes: error", [:], #fileID, #function, #line) 643 | critical("yes: critical", [:], #fileID, #function, #line) 644 | 645 | testLogging.history.assertExist(level: .trace, message: "yes: trace") 646 | testLogging.history.assertExist(level: .debug, message: "yes: debug") 647 | testLogging.history.assertExist(level: .info, message: "yes: info") 648 | testLogging.history.assertExist(level: .notice, message: "yes: notice") 649 | testLogging.history.assertExist(level: .warning, message: "yes: warning") 650 | testLogging.history.assertExist(level: .error, message: "yes: error") 651 | testLogging.history.assertExist(level: .critical, message: "yes: critical") 652 | } 653 | 654 | func testLogsEmittedFromSubdirectoryGetCorrectModuleInNewerSwifts() { 655 | let testLogging = TestLogging() 656 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 657 | 658 | var logger = Logger(label: "\(#function)") 659 | logger.logLevel = .trace 660 | 661 | emitLogMessage("hello", to: logger) 662 | 663 | let moduleName = "LoggingTests" // the actual name 664 | 665 | testLogging.history.assertExist(level: .trace, message: "hello", source: moduleName) 666 | testLogging.history.assertExist(level: .debug, message: "hello", source: moduleName) 667 | testLogging.history.assertExist(level: .info, message: "hello", source: moduleName) 668 | testLogging.history.assertExist(level: .notice, message: "hello", source: moduleName) 669 | testLogging.history.assertExist(level: .warning, message: "hello", source: moduleName) 670 | testLogging.history.assertExist(level: .error, message: "hello", source: moduleName) 671 | testLogging.history.assertExist(level: .critical, message: "hello", source: moduleName) 672 | } 673 | 674 | func testLogMessageWithStringInterpolation() { 675 | let testLogging = TestLogging() 676 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 677 | 678 | var logger = Logger(label: "\(#function)") 679 | logger.logLevel = .debug 680 | 681 | let someInt = Int.random(in: 23..<42) 682 | logger.debug("My favourite number is \(someInt) and not \(someInt - 1)") 683 | testLogging.history.assertExist( 684 | level: .debug, 685 | message: "My favourite number is \(someInt) and not \(someInt - 1)" as String 686 | ) 687 | } 688 | 689 | func testLoggingAString() { 690 | let testLogging = TestLogging() 691 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 692 | 693 | var logger = Logger(label: "\(#function)") 694 | logger.logLevel = .debug 695 | 696 | let anActualString: String = "hello world!" 697 | // We can't stick an actual String in here because we expect a Logger.Message. If we want to log an existing 698 | // `String`, we can use string interpolation. The error you'll get trying to use the String directly is: 699 | // 700 | // error: Cannot convert value of type 'String' to expected argument type 'Logger.Message' 701 | logger.debug("\(anActualString)") 702 | testLogging.history.assertExist(level: .debug, message: "hello world!") 703 | } 704 | 705 | func testMultiplexMetadataProviderMergesInSpecifiedOrder() { 706 | let logging = TestLogging() 707 | 708 | let providerA = Logger.MetadataProvider { ["provider": "a", "a": "foo"] } 709 | let providerB = Logger.MetadataProvider { ["provider": "b", "b": "bar"] } 710 | let logger = Logger( 711 | label: #function, 712 | factory: { label in 713 | logging.makeWithMetadataProvider(label: label, metadataProvider: .multiplex([providerA, providerB])) 714 | } 715 | ) 716 | 717 | logger.log(level: .info, "test", metadata: ["one-off": "42"]) 718 | 719 | logging.history.assertExist( 720 | level: .info, 721 | message: "test", 722 | metadata: ["provider": "b", "a": "foo", "b": "bar", "one-off": "42"] 723 | ) 724 | } 725 | 726 | func testLoggerWithoutFactoryOverrideDefaultsToUsingLoggingSystemMetadataProvider() { 727 | let logging = TestLogging() 728 | LoggingSystem.bootstrapInternal( 729 | { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) }, 730 | metadataProvider: .init { ["provider": "42"] } 731 | ) 732 | 733 | let logger = Logger(label: #function) 734 | 735 | logger.log(level: .info, "test", metadata: ["one-off": "42"]) 736 | 737 | logging.history.assertExist( 738 | level: .info, 739 | message: "test", 740 | metadata: ["provider": "42", "one-off": "42"] 741 | ) 742 | } 743 | 744 | func testLoggerWithPredefinedLibraryMetadataProvider() { 745 | let logging = TestLogging() 746 | LoggingSystem.bootstrapInternal( 747 | { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) }, 748 | metadataProvider: .exampleProvider 749 | ) 750 | 751 | let logger = Logger(label: #function) 752 | 753 | logger.log(level: .info, "test", metadata: ["one-off": "42"]) 754 | 755 | logging.history.assertExist( 756 | level: .info, 757 | message: "test", 758 | metadata: ["example": "example-value", "one-off": "42"] 759 | ) 760 | } 761 | 762 | func testLoggerWithFactoryOverrideDefaultsToUsingLoggingSystemMetadataProvider() { 763 | let logging = TestLogging() 764 | LoggingSystem.bootstrapInternal( 765 | { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) }, 766 | metadataProvider: .init { ["provider": "42"] } 767 | ) 768 | 769 | let logger = Logger( 770 | label: #function, 771 | factory: { label in 772 | logging.makeWithMetadataProvider(label: label, metadataProvider: LoggingSystem.metadataProvider) 773 | } 774 | ) 775 | 776 | logger.log(level: .info, "test", metadata: ["one-off": "42"]) 777 | 778 | logging.history.assertExist( 779 | level: .info, 780 | message: "test", 781 | metadata: ["provider": "42", "one-off": "42"] 782 | ) 783 | } 784 | 785 | func testMultiplexerIsValue() { 786 | let multi = MultiplexLogHandler([ 787 | StreamLogHandler.standardOutput(label: "x"), StreamLogHandler.standardOutput(label: "y"), 788 | ]) 789 | LoggingSystem.bootstrapInternal { _ in 790 | print("new multi") 791 | return multi 792 | } 793 | let logger1: Logger = { 794 | var logger = Logger(label: "foo") 795 | logger.logLevel = .debug 796 | logger[metadataKey: "only-on"] = "first" 797 | return logger 798 | }() 799 | XCTAssertEqual(.debug, logger1.logLevel) 800 | var logger2 = logger1 801 | logger2.logLevel = .error 802 | logger2[metadataKey: "only-on"] = "second" 803 | XCTAssertEqual(.error, logger2.logLevel) 804 | XCTAssertEqual(.debug, logger1.logLevel) 805 | XCTAssertEqual("first", logger1[metadataKey: "only-on"]) 806 | XCTAssertEqual("second", logger2[metadataKey: "only-on"]) 807 | logger1.error("hey") 808 | } 809 | 810 | /// Protects an object such that it can only be accessed while holding a lock. 811 | private final class LockedValueBox<Value: Sendable>: @unchecked Sendable { 812 | private let lock = Lock() 813 | private var storage: Value 814 | 815 | init(initialValue: Value) { 816 | self.storage = initialValue 817 | } 818 | 819 | func withLock<Result>(_ operation: (Value) -> Result) -> Result { 820 | self.lock.withLock { 821 | operation(self.storage) 822 | } 823 | } 824 | 825 | func withLockMutating(_ operation: (inout Value) -> Void) { 826 | self.lock.withLockVoid { 827 | operation(&self.storage) 828 | } 829 | } 830 | 831 | var underlying: Value { 832 | get { self.withLock { $0 } } 833 | set { self.withLockMutating { $0 = newValue } } 834 | } 835 | } 836 | 837 | func testLoggerWithGlobalOverride() { 838 | struct LogHandlerWithGlobalLogLevelOverride: LogHandler { 839 | // the static properties hold the globally overridden log level (if overridden) 840 | private static let overrideLogLevel = LockedValueBox<Logger.Level?>(initialValue: nil) 841 | 842 | private let recorder: Recorder 843 | // this holds the log level if not overridden 844 | private var _logLevel: Logger.Level = .info 845 | 846 | // metadata storage 847 | var metadata: Logger.Metadata = [:] 848 | 849 | init(recorder: Recorder) { 850 | self.recorder = recorder 851 | } 852 | 853 | var logLevel: Logger.Level { 854 | // when we get asked for the log level, we check if it was globally overridden or not 855 | get { 856 | LogHandlerWithGlobalLogLevelOverride.overrideLogLevel.underlying ?? self._logLevel 857 | } 858 | // we set the log level whenever we're asked (note: this might not have an effect if globally 859 | // overridden) 860 | set { 861 | self._logLevel = newValue 862 | } 863 | } 864 | 865 | func log( 866 | level: Logger.Level, 867 | message: Logger.Message, 868 | metadata: Logger.Metadata?, 869 | source: String, 870 | file: String, 871 | function: String, 872 | line: UInt 873 | ) { 874 | self.recorder.record(level: level, metadata: metadata, message: message, source: source) 875 | } 876 | 877 | subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { 878 | get { 879 | self.metadata[metadataKey] 880 | } 881 | set(newValue) { 882 | self.metadata[metadataKey] = newValue 883 | } 884 | } 885 | 886 | // this is the function to globally override the log level, it is not part of the `LogHandler` protocol 887 | static func overrideGlobalLogLevel(_ logLevel: Logger.Level) { 888 | LogHandlerWithGlobalLogLevelOverride.overrideLogLevel.underlying = logLevel 889 | } 890 | } 891 | 892 | let logRecorder = Recorder() 893 | LoggingSystem.bootstrapInternal { _ in 894 | LogHandlerWithGlobalLogLevelOverride(recorder: logRecorder) 895 | } 896 | 897 | var logger1 = Logger(label: "logger-\(#file):\(#line)") 898 | var logger2 = logger1 899 | logger1.logLevel = .warning 900 | logger1[metadataKey: "only-on"] = "first" 901 | logger2.logLevel = .error 902 | logger2[metadataKey: "only-on"] = "second" 903 | XCTAssertEqual(.error, logger2.logLevel) 904 | XCTAssertEqual(.warning, logger1.logLevel) 905 | XCTAssertEqual("first", logger1[metadataKey: "only-on"]) 906 | XCTAssertEqual("second", logger2[metadataKey: "only-on"]) 907 | 908 | logger1.notice("logger1, before") 909 | logger2.notice("logger2, before") 910 | 911 | LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel(.debug) 912 | 913 | logger1.notice("logger1, after") 914 | logger2.notice("logger2, after") 915 | 916 | logRecorder.assertNotExist(level: .notice, message: "logger1, before") 917 | logRecorder.assertNotExist(level: .notice, message: "logger2, before") 918 | logRecorder.assertExist(level: .notice, message: "logger1, after") 919 | logRecorder.assertExist(level: .notice, message: "logger2, after") 920 | } 921 | 922 | func testLogLevelCases() { 923 | let levels = Logger.Level.allCases 924 | XCTAssertEqual(7, levels.count) 925 | } 926 | 927 | func testLogLevelOrdering() { 928 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.debug) 929 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.info) 930 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.notice) 931 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.warning) 932 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.error) 933 | XCTAssertLessThan(Logger.Level.trace, Logger.Level.critical) 934 | XCTAssertLessThan(Logger.Level.debug, Logger.Level.info) 935 | XCTAssertLessThan(Logger.Level.debug, Logger.Level.notice) 936 | XCTAssertLessThan(Logger.Level.debug, Logger.Level.warning) 937 | XCTAssertLessThan(Logger.Level.debug, Logger.Level.error) 938 | XCTAssertLessThan(Logger.Level.debug, Logger.Level.critical) 939 | XCTAssertLessThan(Logger.Level.info, Logger.Level.notice) 940 | XCTAssertLessThan(Logger.Level.info, Logger.Level.warning) 941 | XCTAssertLessThan(Logger.Level.info, Logger.Level.error) 942 | XCTAssertLessThan(Logger.Level.info, Logger.Level.critical) 943 | XCTAssertLessThan(Logger.Level.notice, Logger.Level.warning) 944 | XCTAssertLessThan(Logger.Level.notice, Logger.Level.error) 945 | XCTAssertLessThan(Logger.Level.notice, Logger.Level.critical) 946 | XCTAssertLessThan(Logger.Level.warning, Logger.Level.error) 947 | XCTAssertLessThan(Logger.Level.warning, Logger.Level.critical) 948 | XCTAssertLessThan(Logger.Level.error, Logger.Level.critical) 949 | } 950 | 951 | final class InterceptStream: TextOutputStream { 952 | var interceptedText: String? 953 | var strings = [String]() 954 | 955 | func write(_ string: String) { 956 | // This is a test implementation, a real implementation would include locking 957 | self.strings.append(string) 958 | self.interceptedText = (self.interceptedText ?? "") + string 959 | } 960 | } 961 | 962 | func testStreamLogHandlerWritesToAStream() { 963 | let interceptStream = InterceptStream() 964 | LoggingSystem.bootstrapInternal { _ in 965 | StreamLogHandler(label: "test", stream: interceptStream) 966 | } 967 | let log = Logger(label: "test") 968 | 969 | let testString = "my message is better than yours" 970 | log.critical("\(testString)") 971 | 972 | let messageSucceeded = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines) 973 | .hasSuffix(testString) 974 | 975 | XCTAssertTrue(messageSucceeded ?? false) 976 | XCTAssertEqual(interceptStream.strings.count, 1) 977 | } 978 | 979 | func testStreamLogHandlerOutputFormat() { 980 | let interceptStream = InterceptStream() 981 | let label = "testLabel" 982 | LoggingSystem.bootstrapInternal { label in 983 | StreamLogHandler(label: label, stream: interceptStream) 984 | } 985 | let source = "testSource" 986 | let log = Logger(label: label) 987 | 988 | let testString = "my message is better than yours" 989 | log.critical("\(testString)", source: source) 990 | 991 | let pattern = 992 | "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|-)\\d{4}\\s\(Logger.Level.critical)\\s\(label)\\s:\\s\\[\(source)\\]\\s\(testString)quot; 993 | 994 | let messageSucceeded = 995 | interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines).range( 996 | of: pattern, 997 | options: .regularExpression 998 | ) != nil 999 | 1000 | XCTAssertTrue(messageSucceeded) 1001 | XCTAssertEqual(interceptStream.strings.count, 1) 1002 | } 1003 | 1004 | func testStreamLogHandlerOutputFormatWithMetaData() { 1005 | let interceptStream = InterceptStream() 1006 | let label = "testLabel" 1007 | LoggingSystem.bootstrapInternal { label in 1008 | StreamLogHandler(label: label, stream: interceptStream) 1009 | } 1010 | let source = "testSource" 1011 | let log = Logger(label: label) 1012 | 1013 | let testString = "my message is better than yours" 1014 | log.critical("\(testString)", metadata: ["test": "test"], source: source) 1015 | 1016 | let pattern = 1017 | "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|-)\\d{4}\\s\(Logger.Level.critical)\\s\(label)\\s:\\stest=test\\s\\[\(source)\\]\\s\(testString)quot; 1018 | 1019 | let messageSucceeded = 1020 | interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines).range( 1021 | of: pattern, 1022 | options: .regularExpression 1023 | ) != nil 1024 | 1025 | XCTAssertTrue(messageSucceeded) 1026 | XCTAssertEqual(interceptStream.strings.count, 1) 1027 | } 1028 | 1029 | func testStreamLogHandlerOutputFormatWithOrderedMetadata() { 1030 | let interceptStream = InterceptStream() 1031 | let label = "testLabel" 1032 | LoggingSystem.bootstrapInternal { label in 1033 | StreamLogHandler(label: label, stream: interceptStream) 1034 | } 1035 | let log = Logger(label: label) 1036 | 1037 | let testString = "my message is better than yours" 1038 | log.critical("\(testString)", metadata: ["a": "a0", "b": "b0"]) 1039 | log.critical("\(testString)", metadata: ["b": "b1", "a": "a1"]) 1040 | 1041 | XCTAssertEqual(interceptStream.strings.count, 2) 1042 | guard interceptStream.strings.count == 2 else { 1043 | XCTFail("Intercepted \(interceptStream.strings.count) logs, expected 2") 1044 | return 1045 | } 1046 | 1047 | XCTAssert(interceptStream.strings[0].contains("a=a0 b=b0"), "LINES: \(interceptStream.strings[0])") 1048 | XCTAssert(interceptStream.strings[1].contains("a=a1 b=b1"), "LINES: \(interceptStream.strings[1])") 1049 | } 1050 | 1051 | func testStreamLogHandlerWritesIncludeMetadataProviderMetadata() { 1052 | let interceptStream = InterceptStream() 1053 | LoggingSystem.bootstrapInternal( 1054 | { _, metadataProvider in 1055 | StreamLogHandler(label: "test", stream: interceptStream, metadataProvider: metadataProvider) 1056 | }, 1057 | metadataProvider: .exampleProvider 1058 | ) 1059 | let log = Logger(label: "test") 1060 | 1061 | let testString = "my message is better than yours" 1062 | log.critical("\(testString)") 1063 | 1064 | let messageSucceeded = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines) 1065 | .hasSuffix(testString) 1066 | 1067 | XCTAssertTrue(messageSucceeded ?? false) 1068 | XCTAssertEqual(interceptStream.strings.count, 1) 1069 | let message = interceptStream.strings.first! 1070 | XCTAssertTrue(message.contains("example=example-value"), "message must contain metadata, was: \(message)") 1071 | } 1072 | 1073 | func testStdioOutputStreamWrite() { 1074 | self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in 1075 | let logStream = StdioOutputStream(file: writeFD, flushMode: .always) 1076 | LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) } 1077 | let log = Logger(label: "test") 1078 | let testString = "hello\u{0} world" 1079 | log.critical("\(testString)") 1080 | 1081 | #if os(Windows) 1082 | let size = _read(readFD, readBuffer, 256) 1083 | #else 1084 | let size = read(readFD, readBuffer, 256) 1085 | #endif 1086 | 1087 | let output = String( 1088 | decoding: UnsafeRawBufferPointer(start: UnsafeRawPointer(readBuffer), count: numericCast(size)), 1089 | as: UTF8.self 1090 | ) 1091 | let messageSucceeded = output.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix(testString) 1092 | XCTAssertTrue(messageSucceeded) 1093 | } 1094 | } 1095 | 1096 | func testStdioOutputStreamFlush() { 1097 | // flush on every statement 1098 | self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in 1099 | let logStream = StdioOutputStream(file: writeFD, flushMode: .always) 1100 | LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) } 1101 | Logger(label: "test").critical("test") 1102 | 1103 | #if os(Windows) 1104 | let size = _read(readFD, readBuffer, 256) 1105 | #else 1106 | let size = read(readFD, readBuffer, 256) 1107 | #endif 1108 | XCTAssertGreaterThan(size, -1, "expected flush") 1109 | 1110 | logStream.flush() 1111 | 1112 | #if os(Windows) 1113 | let size2 = _read(readFD, readBuffer, 256) 1114 | #else 1115 | let size2 = read(readFD, readBuffer, 256) 1116 | #endif 1117 | XCTAssertEqual(size2, -1, "expected no flush") 1118 | } 1119 | // default flushing 1120 | self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in 1121 | let logStream = StdioOutputStream(file: writeFD, flushMode: .undefined) 1122 | LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) } 1123 | Logger(label: "test").critical("test") 1124 | 1125 | #if os(Windows) 1126 | let size = _read(readFD, readBuffer, 256) 1127 | #else 1128 | let size = read(readFD, readBuffer, 256) 1129 | #endif 1130 | XCTAssertEqual(size, -1, "expected no flush") 1131 | 1132 | logStream.flush() 1133 | 1134 | #if os(Windows) 1135 | let size2 = _read(readFD, readBuffer, 256) 1136 | #else 1137 | let size2 = read(readFD, readBuffer, 256) 1138 | #endif 1139 | XCTAssertGreaterThan(size2, -1, "expected flush") 1140 | } 1141 | } 1142 | 1143 | func withWriteReadFDsAndReadBuffer(_ body: (CFilePointer, CInt, UnsafeMutablePointer<Int8>) -> Void) { 1144 | var fds: [Int32] = [-1, -1] 1145 | #if os(Windows) 1146 | fds.withUnsafeMutableBufferPointer { 1147 | let err = _pipe($0.baseAddress, 256, _O_BINARY) 1148 | XCTAssertEqual(err, 0, "_pipe failed \(err)") 1149 | } 1150 | guard let writeFD = _fdopen(fds[1], "w") else { 1151 | XCTFail("Failed to open file") 1152 | return 1153 | } 1154 | #else 1155 | fds.withUnsafeMutableBufferPointer { ptr in 1156 | let err = pipe(ptr.baseAddress!) 1157 | XCTAssertEqual(err, 0, "pipe failed \(err)") 1158 | } 1159 | guard let writeFD = fdopen(fds[1], "w") else { 1160 | XCTFail("Failed to open file") 1161 | return 1162 | } 1163 | #endif 1164 | 1165 | let writeBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256) 1166 | defer { 1167 | writeBuffer.deinitialize(count: 256) 1168 | writeBuffer.deallocate() 1169 | } 1170 | 1171 | var err = setvbuf(writeFD, writeBuffer, _IOFBF, 256) 1172 | XCTAssertEqual(err, 0, "setvbuf failed \(err)") 1173 | 1174 | let readFD = fds[0] 1175 | #if os(Windows) 1176 | let hPipe: HANDLE = HANDLE(bitPattern: _get_osfhandle(readFD))! 1177 | XCTAssertFalse(hPipe == INVALID_HANDLE_VALUE) 1178 | 1179 | var dwMode: DWORD = DWORD(PIPE_NOWAIT) 1180 | let bSucceeded = SetNamedPipeHandleState(hPipe, &dwMode, nil, nil) 1181 | XCTAssertTrue(bSucceeded) 1182 | #else 1183 | err = fcntl(readFD, F_SETFL, fcntl(readFD, F_GETFL) | O_NONBLOCK) 1184 | XCTAssertEqual(err, 0, "fcntl failed \(err)") 1185 | #endif 1186 | 1187 | let readBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256) 1188 | defer { 1189 | readBuffer.deinitialize(count: 256) 1190 | readBuffer.deallocate() 1191 | } 1192 | 1193 | // the actual test 1194 | body(writeFD, readFD, readBuffer) 1195 | 1196 | for fd in fds { 1197 | #if os(Windows) 1198 | _close(fd) 1199 | #else 1200 | close(fd) 1201 | #endif 1202 | } 1203 | } 1204 | 1205 | func testOverloadingError() { 1206 | struct Dummy: Error, LocalizedError { 1207 | var errorDescription: String? { 1208 | "errorDescription" 1209 | } 1210 | } 1211 | // bootstrap with our test logging impl 1212 | let logging = TestLogging() 1213 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 1214 | 1215 | var logger = Logger(label: "test") 1216 | logger.logLevel = .error 1217 | logger.error(error: Dummy()) 1218 | 1219 | logging.history.assertExist(level: .error, message: "errorDescription") 1220 | } 1221 | 1222 | func testCompileInitializeStandardStreamLogHandlersWithMetadataProviders() { 1223 | // avoid "unreachable code" warnings 1224 | let dontExecute = Int.random(in: 100...200) == 1 1225 | guard dontExecute else { 1226 | return 1227 | } 1228 | 1229 | // default usage 1230 | LoggingSystem.bootstrap { (label: String) in StreamLogHandler.standardOutput(label: label) } 1231 | LoggingSystem.bootstrap { (label: String) in StreamLogHandler.standardError(label: label) } 1232 | 1233 | // with metadata handler, explicitly, public api 1234 | LoggingSystem.bootstrap( 1235 | { label, metadataProvider in 1236 | StreamLogHandler.standardOutput(label: label, metadataProvider: metadataProvider) 1237 | }, 1238 | metadataProvider: .exampleProvider 1239 | ) 1240 | LoggingSystem.bootstrap( 1241 | { label, metadataProvider in 1242 | StreamLogHandler.standardError(label: label, metadataProvider: metadataProvider) 1243 | }, 1244 | metadataProvider: .exampleProvider 1245 | ) 1246 | 1247 | // with metadata handler, still pretty 1248 | LoggingSystem.bootstrap( 1249 | { (label: String, metadataProvider: Logger.MetadataProvider?) in 1250 | StreamLogHandler.standardOutput(label: label, metadataProvider: metadataProvider) 1251 | }, 1252 | metadataProvider: .exampleProvider 1253 | ) 1254 | LoggingSystem.bootstrap( 1255 | { (label: String, metadataProvider: Logger.MetadataProvider?) in 1256 | StreamLogHandler.standardError(label: label, metadataProvider: metadataProvider) 1257 | }, 1258 | metadataProvider: .exampleProvider 1259 | ) 1260 | } 1261 | 1262 | func testLoggerIsJustHoldingASinglePointer() { 1263 | let expectedSize = MemoryLayout<UnsafeRawPointer>.size 1264 | XCTAssertEqual(MemoryLayout<Logger>.size, expectedSize) 1265 | } 1266 | 1267 | func testLoggerCopyOnWrite() { 1268 | var logger1 = Logger(label: "foo") 1269 | logger1.logLevel = .error 1270 | var logger2 = logger1 1271 | logger2.logLevel = .trace 1272 | XCTAssertEqual(.error, logger1.logLevel) 1273 | XCTAssertEqual(.trace, logger2.logLevel) 1274 | } 1275 | } 1276 | 1277 | extension Logger { 1278 | public func error( 1279 | error: any Error, 1280 | metadata: @autoclosure () -> Logger.Metadata? = nil, 1281 | file: String = #fileID, 1282 | function: String = #function, 1283 | line: UInt = #line 1284 | ) { 1285 | self.error("\(error.localizedDescription)", metadata: metadata(), file: file, function: function, line: line) 1286 | } 1287 | } 1288 | 1289 | extension Logger.MetadataProvider { 1290 | static var exampleProvider: Self { 1291 | .init { ["example": .string("example-value")] } 1292 | } 1293 | 1294 | static func constant(_ metadata: Logger.Metadata) -> Self { 1295 | .init { metadata } 1296 | } 1297 | } 1298 | 1299 | // Sendable 1300 | 1301 | // used to test logging metadata which requires Sendable conformance 1302 | // @unchecked Sendable since manages it own state 1303 | extension LoggingTest.LazyMetadataBox: @unchecked Sendable {} 1304 | 1305 | // used to test logging stream which requires Sendable conformance 1306 | // @unchecked Sendable since manages it own state 1307 | extension LoggingTest.InterceptStream: @unchecked Sendable {} 1308 | -------------------------------------------------------------------------------- /Tests/LoggingTests/MDCTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | import Dispatch 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | class MDCTest: XCTestCase { 20 | func test1() throws { 21 | // bootstrap with our test logger 22 | let logging = TestLogging() 23 | LoggingSystem.bootstrapInternal { logging.make(label: $0) } 24 | 25 | // run the program 26 | MDC.global["foo"] = "bar" 27 | let group = DispatchGroup() 28 | for r in 5...10 { 29 | group.enter() 30 | DispatchQueue(label: "mdc-test-queue-\(r)").async { 31 | let add = Int.random(in: 10...1000) 32 | let remove = Int.random(in: 0...add - 1) 33 | for i in 0...add { 34 | MDC.global["key-\(i)"] = "value-\(i)" 35 | } 36 | for i in 0...remove { 37 | MDC.global["key-\(i)"] = nil 38 | } 39 | XCTAssertEqual(add - remove, MDC.global.metadata.count, "expected number of entries to match") 40 | for i in remove + 1...add { 41 | XCTAssertNotNil(MDC.global["key-\(i)"], "expecting value for key-\(i)") 42 | } 43 | for i in 0...remove { 44 | XCTAssertNil(MDC.global["key-\(i)"], "not expecting value for key-\(i)") 45 | } 46 | MDC.global.clear() 47 | group.leave() 48 | } 49 | } 50 | group.wait() 51 | XCTAssertEqual(MDC.global["foo"], "bar", "expecting to find top items") 52 | MDC.global["foo"] = nil 53 | XCTAssertTrue(MDC.global.metadata.isEmpty, "MDC should be empty") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/LoggingTests/MetadataProviderTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | #if canImport(Darwin) 20 | import Darwin 21 | #elseif os(Windows) 22 | import WinSDK 23 | #elseif canImport(Android) 24 | import Android 25 | #else 26 | import Glibc 27 | #endif 28 | 29 | final class MetadataProviderTest: XCTestCase { 30 | func testLoggingMergesOneOffMetadataWithProvidedMetadataFromExplicitlyPassed() throws { 31 | let logging = TestLogging() 32 | LoggingSystem.bootstrapInternal( 33 | { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) }, 34 | metadataProvider: .init { 35 | ["common": "initial"] 36 | } 37 | ) 38 | 39 | let logger = Logger( 40 | label: #function, 41 | metadataProvider: .init { 42 | [ 43 | "common": "provider", 44 | "provider": "42", 45 | ] 46 | } 47 | ) 48 | 49 | logger.log(level: .info, "test", metadata: ["one-off": "42", "common": "one-off"]) 50 | 51 | logging.history.assertExist( 52 | level: .info, 53 | message: "test", 54 | metadata: ["common": "one-off", "one-off": "42", "provider": "42"] 55 | ) 56 | } 57 | 58 | func testLogHandlerThatDidNotImplementProvidersButSomeoneAttemptsToSetOneOnIt() { 59 | #if DEBUG 60 | // we only emit these warnings in debug mode 61 | let logging = TestLogging() 62 | var handler = LogHandlerThatDidNotImplementMetadataProviders(testLogging: logging) 63 | 64 | handler.metadataProvider = .simpleTestProvider 65 | 66 | logging.history.assertExist( 67 | level: .warning, 68 | message: 69 | "Attempted to set metadataProvider on LogHandlerThatDidNotImplementMetadataProviders that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.", 70 | source: "Logging" 71 | ) 72 | 73 | let countBefore = logging.history.entries.count 74 | handler.metadataProvider = .simpleTestProvider 75 | XCTAssertEqual(countBefore, logging.history.entries.count, "Should only log the warning once") 76 | #endif 77 | } 78 | 79 | func testLogHandlerThatDidImplementProvidersButSomeoneAttemptsToSetOneOnIt() { 80 | #if DEBUG 81 | // we only emit these warnings in debug mode 82 | let logging = TestLogging() 83 | var handler = LogHandlerThatDidImplementMetadataProviders(testLogging: logging) 84 | 85 | handler.metadataProvider = .simpleTestProvider 86 | 87 | logging.history.assertNotExist( 88 | level: .warning, 89 | message: 90 | "Attempted to set metadataProvider on LogHandlerThatDidImplementMetadataProviders that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.", 91 | source: "Logging" 92 | ) 93 | #endif 94 | } 95 | } 96 | 97 | extension Logger.MetadataProvider { 98 | static var simpleTestProvider: Logger.MetadataProvider { 99 | Logger.MetadataProvider { 100 | ["test": "provided"] 101 | } 102 | } 103 | } 104 | 105 | public struct LogHandlerThatDidNotImplementMetadataProviders: LogHandler { 106 | let testLogging: TestLogging 107 | init(testLogging: TestLogging) { 108 | self.testLogging = testLogging 109 | } 110 | 111 | public subscript(metadataKey _: String) -> Logging.Logger.Metadata.Value? { 112 | get { 113 | nil 114 | } 115 | set(newValue) { 116 | // ignore 117 | } 118 | } 119 | 120 | public var metadata: Logging.Logger.Metadata = [:] 121 | 122 | public var logLevel: Logging.Logger.Level = .trace 123 | 124 | public func log( 125 | level: Logger.Level, 126 | message: Logger.Message, 127 | metadata: Logger.Metadata?, 128 | source: String, 129 | file: String, 130 | function: String, 131 | line: UInt 132 | ) { 133 | self.testLogging.make(label: "fake").log( 134 | level: level, 135 | message: message, 136 | metadata: metadata, 137 | source: source, 138 | file: file, 139 | function: function, 140 | line: line 141 | ) 142 | } 143 | } 144 | 145 | public struct LogHandlerThatDidImplementMetadataProviders: LogHandler { 146 | let testLogging: TestLogging 147 | init(testLogging: TestLogging) { 148 | self.testLogging = testLogging 149 | } 150 | 151 | public subscript(metadataKey _: String) -> Logging.Logger.Metadata.Value? { 152 | get { 153 | nil 154 | } 155 | set(newValue) { 156 | // ignore 157 | } 158 | } 159 | 160 | public var metadata: Logging.Logger.Metadata = [:] 161 | 162 | public var logLevel: Logging.Logger.Level = .trace 163 | 164 | public var metadataProvider: Logger.MetadataProvider? 165 | 166 | public func log( 167 | level: Logger.Level, 168 | message: Logger.Message, 169 | metadata: Logger.Metadata?, 170 | source: String, 171 | file: String, 172 | function: String, 173 | line: UInt 174 | ) { 175 | self.testLogging.make(label: "fake").log( 176 | level: level, 177 | message: message, 178 | metadata: metadata, 179 | source: source, 180 | file: file, 181 | function: function, 182 | line: line 183 | ) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Tests/LoggingTests/SubDirectoryOfLoggingTests/EmitALogFromSubDirectory.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | 17 | internal func emitLogMessage(_ message: Logger.Message, to logger: Logger) { 18 | logger.trace(message) 19 | logger.debug(message) 20 | logger.info(message) 21 | logger.notice(message) 22 | logger.warning(message) 23 | logger.error(message) 24 | logger.critical(message) 25 | } 26 | -------------------------------------------------------------------------------- /Tests/LoggingTests/TestLogger.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import XCTest 17 | 18 | @testable import Logging 19 | 20 | #if os(Windows) 21 | import WinSDK 22 | #endif 23 | #if compiler(>=6.0) || canImport(Darwin) 24 | import Dispatch 25 | #else 26 | @preconcurrency import Dispatch 27 | #endif 28 | 29 | internal struct TestLogging { 30 | private let _config = Config() // shared among loggers 31 | private let recorder = Recorder() // shared among loggers 32 | 33 | func make(label: String) -> some LogHandler { 34 | TestLogHandler( 35 | label: label, 36 | config: self.config, 37 | recorder: self.recorder, 38 | metadataProvider: LoggingSystem.metadataProvider 39 | ) 40 | } 41 | 42 | func makeWithMetadataProvider(label: String, metadataProvider: Logger.MetadataProvider?) -> (some LogHandler) { 43 | TestLogHandler( 44 | label: label, 45 | config: self.config, 46 | recorder: self.recorder, 47 | metadataProvider: metadataProvider 48 | ) 49 | } 50 | 51 | var config: Config { self._config } 52 | var history: some History { self.recorder } 53 | } 54 | 55 | internal struct TestLogHandler: LogHandler { 56 | private let recorder: Recorder 57 | private let config: Config 58 | private var logger: Logger // the actual logger 59 | 60 | let label: String 61 | public var metadataProvider: Logger.MetadataProvider? 62 | 63 | init(label: String, config: Config, recorder: Recorder, metadataProvider: Logger.MetadataProvider?) { 64 | self.label = label 65 | self.config = config 66 | self.recorder = recorder 67 | self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label)) 68 | self.logger.logLevel = .debug 69 | self.metadataProvider = metadataProvider 70 | } 71 | 72 | init(label: String, config: Config, recorder: Recorder) { 73 | self.label = label 74 | self.config = config 75 | self.recorder = recorder 76 | self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label)) 77 | self.logger.logLevel = .debug 78 | self.metadataProvider = LoggingSystem.metadataProvider 79 | } 80 | 81 | func log( 82 | level: Logger.Level, 83 | message: Logger.Message, 84 | metadata explicitMetadata: Logger.Metadata?, 85 | source: String, 86 | file: String, 87 | function: String, 88 | line: UInt 89 | ) { 90 | // baseline metadata, that was set on handler: 91 | var metadata = self._metadataSet ? self.metadata : MDC.global.metadata 92 | // contextual metadata, e.g. from task-locals: 93 | let contextualMetadata = self.metadataProvider?.get() ?? [:] 94 | if !contextualMetadata.isEmpty { 95 | metadata.merge(contextualMetadata, uniquingKeysWith: { _, contextual in contextual }) 96 | } 97 | // override using any explicit metadata passed for this log statement: 98 | if let explicitMetadata = explicitMetadata { 99 | metadata.merge(explicitMetadata, uniquingKeysWith: { _, explicit in explicit }) 100 | } 101 | 102 | self.logger.log( 103 | level: level, 104 | message, 105 | metadata: metadata, 106 | source: source, 107 | file: file, 108 | function: function, 109 | line: line 110 | ) 111 | self.recorder.record(level: level, metadata: metadata, message: message, source: source) 112 | } 113 | 114 | private var _logLevel: Logger.Level? 115 | var logLevel: Logger.Level { 116 | get { 117 | // get from config unless set 118 | self._logLevel ?? self.config.get(key: self.label) 119 | } 120 | set { 121 | self._logLevel = newValue 122 | } 123 | } 124 | 125 | private var _metadataSet = false 126 | private var _metadata = Logger.Metadata() { 127 | didSet { 128 | self._metadataSet = true 129 | } 130 | } 131 | 132 | public var metadata: Logger.Metadata { 133 | get { 134 | self._metadata 135 | } 136 | set { 137 | self._metadata = newValue 138 | } 139 | } 140 | 141 | // TODO: would be nice to delegate to local copy of logger but StdoutLogger is a reference type. why? 142 | subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? { 143 | get { 144 | self._metadata[metadataKey] 145 | } 146 | set { 147 | self._metadata[metadataKey] = newValue 148 | } 149 | } 150 | } 151 | 152 | internal class Config { 153 | private static let ALL = "*" 154 | 155 | private let lock = NSLock() 156 | private var storage = [String: Logger.Level]() 157 | 158 | func get(key: String) -> Logger.Level { 159 | self.get(key) ?? self.get(Config.ALL) ?? Logger.Level.debug 160 | } 161 | 162 | func get(_ key: String) -> Logger.Level? { 163 | guard let value = (self.lock.withLock { self.storage[key] }) else { 164 | return nil 165 | } 166 | return value 167 | } 168 | 169 | func set(key: String = Config.ALL, value: Logger.Level) { 170 | self.lock.withLock { self.storage[key] = value } 171 | } 172 | 173 | func clear() { 174 | self.lock.withLock { self.storage.removeAll() } 175 | } 176 | } 177 | 178 | internal class Recorder: History { 179 | private let lock = NSLock() 180 | private var _entries = [LogEntry]() 181 | 182 | func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, source: String) { 183 | self.lock.withLock { 184 | self._entries.append( 185 | LogEntry(level: level, metadata: metadata, message: message.description, source: source) 186 | ) 187 | } 188 | } 189 | 190 | var entries: [LogEntry] { 191 | self.lock.withLock { self._entries } 192 | } 193 | } 194 | 195 | internal protocol History { 196 | var entries: [LogEntry] { get } 197 | } 198 | 199 | extension History { 200 | func atLevel(level: Logger.Level) -> [LogEntry] { 201 | self.entries.filter { entry in 202 | level == entry.level 203 | } 204 | } 205 | 206 | var trace: [LogEntry] { 207 | self.atLevel(level: .debug) 208 | } 209 | 210 | var debug: [LogEntry] { 211 | self.atLevel(level: .debug) 212 | } 213 | 214 | var info: [LogEntry] { 215 | self.atLevel(level: .info) 216 | } 217 | 218 | var warning: [LogEntry] { 219 | self.atLevel(level: .warning) 220 | } 221 | 222 | var error: [LogEntry] { 223 | self.atLevel(level: .error) 224 | } 225 | } 226 | 227 | internal struct LogEntry { 228 | let level: Logger.Level 229 | let metadata: Logger.Metadata? 230 | let message: String 231 | let source: String 232 | } 233 | 234 | extension History { 235 | func assertExist( 236 | level: Logger.Level, 237 | message: String, 238 | metadata: Logger.Metadata? = nil, 239 | source: String? = nil, 240 | file: StaticString = #filePath, 241 | fileID: String = #fileID, 242 | line: UInt = #line 243 | ) { 244 | let source = source ?? Logger.currentModule(fileID: "\(fileID)") 245 | let entry = self.find(level: level, message: message, metadata: metadata, source: source) 246 | XCTAssertNotNil( 247 | entry, 248 | "entry not found: \(level), \(source), \(String(describing: metadata)), \(message)", 249 | file: file, 250 | line: line 251 | ) 252 | } 253 | 254 | func assertNotExist( 255 | level: Logger.Level, 256 | message: String, 257 | metadata: Logger.Metadata? = nil, 258 | source: String? = nil, 259 | file: StaticString = #filePath, 260 | fileID: String = #file, 261 | line: UInt = #line 262 | ) { 263 | let source = source ?? Logger.currentModule(fileID: "\(fileID)") 264 | let entry = self.find(level: level, message: message, metadata: metadata, source: source) 265 | XCTAssertNil( 266 | entry, 267 | "entry was found: \(level), \(source), \(String(describing: metadata)), \(message)", 268 | file: file, 269 | line: line 270 | ) 271 | } 272 | 273 | func find(level: Logger.Level, message: String, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? { 274 | self.entries.first { entry in 275 | if entry.level != level { 276 | return false 277 | } 278 | if entry.message != message { 279 | return false 280 | } 281 | if let lhs = entry.metadata, let rhs = metadata { 282 | if lhs.count != rhs.count { 283 | return false 284 | } 285 | 286 | for lk in lhs.keys { 287 | if lhs[lk] != rhs[lk] { 288 | return false 289 | } 290 | } 291 | 292 | for rk in rhs.keys { 293 | if lhs[rk] != rhs[rk] { 294 | return false 295 | } 296 | } 297 | 298 | return true 299 | } 300 | if entry.source != source { 301 | return false 302 | } 303 | 304 | return true 305 | } 306 | } 307 | } 308 | 309 | public class MDC { 310 | private let lock = NSLock() 311 | private var storage = [Int: Logger.Metadata]() 312 | 313 | public static let global = MDC() 314 | 315 | private init() {} 316 | 317 | public subscript(metadataKey: String) -> Logger.Metadata.Value? { 318 | get { 319 | self.lock.withLock { 320 | self.storage[self.threadId]?[metadataKey] 321 | } 322 | } 323 | set { 324 | self.lock.withLock { 325 | if self.storage[self.threadId] == nil { 326 | self.storage[self.threadId] = Logger.Metadata() 327 | } 328 | self.storage[self.threadId]![metadataKey] = newValue 329 | } 330 | } 331 | } 332 | 333 | public var metadata: Logger.Metadata { 334 | self.lock.withLock { 335 | self.storage[self.threadId] ?? [:] 336 | } 337 | } 338 | 339 | public func clear() { 340 | self.lock.withLock { 341 | _ = self.storage.removeValue(forKey: self.threadId) 342 | } 343 | } 344 | 345 | public func with(metadata: Logger.Metadata, _ body: () throws -> Void) rethrows { 346 | for (key, value) in metadata { 347 | self[key] = value 348 | } 349 | defer { 350 | for (key, _) in metadata { 351 | self[key] = nil 352 | } 353 | } 354 | try body() 355 | } 356 | 357 | public func with<T>(metadata: Logger.Metadata, _ body: () throws -> T) rethrows -> T { 358 | for (key, value) in metadata { 359 | self[key] = value 360 | } 361 | defer { 362 | for (key, _) in metadata { 363 | self[key] = nil 364 | } 365 | } 366 | return try body() 367 | } 368 | 369 | // for testing 370 | internal func flush() { 371 | self.lock.withLock { 372 | self.storage.removeAll() 373 | } 374 | } 375 | 376 | private var threadId: Int { 377 | #if canImport(Darwin) 378 | return Int(pthread_mach_thread_np(pthread_self())) 379 | #elseif os(Windows) 380 | return Int(GetCurrentThreadId()) 381 | #else 382 | return Int(pthread_self()) 383 | #endif 384 | } 385 | } 386 | 387 | extension NSLock { 388 | func withLock<T>(_ body: () -> T) -> T { 389 | self.lock() 390 | defer { 391 | self.unlock() 392 | } 393 | return body() 394 | } 395 | } 396 | 397 | internal struct TestLibrary: Sendable { 398 | private let logger = Logger(label: "TestLibrary") 399 | private let queue = DispatchQueue(label: "TestLibrary") 400 | 401 | public init() {} 402 | 403 | public func doSomething() { 404 | self.logger.info("TestLibrary::doSomething") 405 | } 406 | 407 | public func doSomethingAsync(completion: @escaping @Sendable () -> Void) { 408 | // libraries that use global loggers and async, need to make sure they propagate the 409 | // logging metadata when creating a new thread 410 | let metadata = MDC.global.metadata 411 | self.queue.asyncAfter(deadline: .now() + 0.1) { 412 | MDC.global.with(metadata: metadata) { 413 | self.logger.info("TestLibrary::doSomethingAsync") 414 | completion() 415 | } 416 | } 417 | } 418 | } 419 | 420 | // Sendable 421 | 422 | extension TestLogHandler: @unchecked Sendable {} 423 | extension Recorder: @unchecked Sendable {} 424 | extension Config: @unchecked Sendable {} 425 | extension MDC: @unchecked Sendable {} 426 | -------------------------------------------------------------------------------- /Tests/LoggingTests/TestSendable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Logging API open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift Logging API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import Logging 18 | 19 | class SendableTest: XCTestCase { 20 | func testSendableLogger() async { 21 | let testLogging = TestLogging() 22 | LoggingSystem.bootstrapInternal { testLogging.make(label: $0) } 23 | 24 | let logger = Logger(label: "test") 25 | let message1 = Logger.Message(stringLiteral: "critical 1") 26 | let message2 = Logger.Message(stringLiteral: "critical 2") 27 | let metadata: Logger.Metadata = ["key": "value"] 28 | 29 | let task = Task.detached { 30 | logger.info("info") 31 | logger.critical(message1) 32 | logger.critical(message2) 33 | logger.warning(.init(stringLiteral: "warning"), metadata: metadata) 34 | } 35 | 36 | await task.value 37 | testLogging.history.assertExist(level: .info, message: "info", metadata: [:]) 38 | testLogging.history.assertExist(level: .critical, message: "critical 1", metadata: [:]) 39 | testLogging.history.assertExist(level: .critical, message: "critical 2", metadata: [:]) 40 | testLogging.history.assertExist(level: .warning, message: "warning", metadata: metadata) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev/git.commit.template: -------------------------------------------------------------------------------- 1 | One line description of your change 2 | 3 | Motivation: 4 | 5 | Explain here the context, and why you're making that change. 6 | What is the problem you're trying to solve. 7 | 8 | Modifications: 9 | 10 | Describe the modifications you've done. 11 | 12 | Result: 13 | 14 | After your change, what will change. 15 | -------------------------------------------------------------------------------- /proposals/0001-metadata-providers.md: -------------------------------------------------------------------------------- 1 | # Metadata Providers 2 | 3 | Authors: [Moritz Lang](https://github.com/slashmo), [Konrad 'ktoso' Malawski](https://github.com/ktoso) 4 | 5 | ## Introduction 6 | 7 | While global metadata attributes may be manually set on a `LogHandler` level, there's currently no way of reliably providing contextual, automatically propagated, metadata when logging with swift-log. 8 | 9 | ## Motivation 10 | 11 | To benefit from tools such as [Distributed Tracing](https://github.com/apple/swift-distributed-tracing) it is necessary for libraries to make use of the trace information. 12 | 13 | Most notably, loggers should participate in tracing by including some trace metadata (such as e.g. a `trace-id`) when logging, as it transparently enables developers to benefit from log correlation using those IDs. 14 | 15 | Today, the only supported way of providing such metadata is to pass them along to each log call explicitly: 16 | 17 | ```swift 18 | logger.info("first this ...", metadata: ["trace-id": MyTracingLibrary.currentTraceID]) 19 | logger.info("... now this", metadata: ["trace-id": MyTracingLibrary.currentTraceID]) 20 | ``` 21 | 22 | This comes with a couple of downsides: 23 | 24 | ### Error-prone and repetitive 25 | 26 | It's easy to forget passing this metadata to _all_ log statements, resulting in an inconsistent debugging experience as these log statements cannot be found using correlation IDs. 27 | 28 | The repetitiveness and verboseness of logging multiple metadata in-line quickly becomes annoying and vastly decreases the signal-to-noise ratio of Swift code trying to be a good citizen and making use of log correlation techniques such as distributed tracing. 29 | 30 | ### Impossible to implement for libraries 31 | 32 | A large portion of logs are not generated by the end-user but by libraries such as AsyncHTTPClient, Vapor etc. 33 | These libraries, by design, don't know about the specific metadata keys that should be included in the logs. 34 | Those keys are after all runtime dependent, and may change depending on what tracing system is configured by the end user. 35 | 36 | For example, a specific `Tracer` implementation would use a type representing a trace ID, and has a way of extracting 37 | that trace ID from a [Swift Distributed Tracing Baggage](https://github.com/apple/swift-distributed-tracing-baggage). But other libraries can't know about this _specific_ trace ID and therefore would not be 38 | able to pass such values along to their log statements. 39 | 40 | ## Proposed solution 41 | 42 | To support this kind of runtime-generated metadata in `swift-log`, we need to extend the logging APIs in an open-ended way, to allow any kinds of metadata to be provided from the asynchronous context (i.e. task local values). 43 | 44 | We also have a desire to keep `swift-log` a "zero dependencies" library, as it intends only to focus on describing a logging API, and not incur any additional dependencies, so it can be used in the most minimal of projects. 45 | 46 | To solve this, we propose the extension of swift-log APIs with a new concept: _metadata providers_. 47 | 48 | `MetadataProvider` is a struct nested in the `Logger` type, sitting alongside `MetadataValue` and the `Metadata`. 49 | Its purpose is to _provide_ `Logger.Metadata` from the asynchronous context, by e.g. looking up various task-local values, 50 | and converting them into `Logger.Metadata`. This is performed by calling the `get()` function, like so: 51 | 52 | ```swift 53 | extension Logger { 54 | public struct MetadataProvider: Sendable { 55 | // ... 56 | public init(_ provideMetadata: @escaping @Sendable () -> Metadata) 57 | 58 | public get() -> Metadata 59 | } 60 | } 61 | ``` 62 | 63 | ### Defining a `MetadataProvider` 64 | 65 | While `MetadataProvider`s can be created in an ad-hoc fashion, the struct may be used as a namespace 66 | to define providers in. 67 | 68 | For example, a `MyTracer` implementation of swift-distributed-tracing would be expected to provide a metadata provider 69 | that is aware of its own `Baggage` and specific keys it uses to carry the trace and span identifiers, like this: 70 | 71 | ```swift 72 | import Tracing // swift-distributed-tracing 73 | 74 | extension Logger.MetadataProvider { 75 | static let myTracer = Logger.MetadataProvider { 76 | guard let baggage = Baggage.current else { 77 | return [:] 78 | } 79 | guard let spanContext = baggage.spanContext else { 80 | return [:] 81 | } 82 | 83 | return [ 84 | "traceID": "\(spanContext.traceID)", 85 | "spanID": "\(spanContext.spanID)", 86 | ] 87 | } 88 | } 89 | ``` 90 | 91 | ### Using a `MetadataProvider` 92 | 93 | A `MetadataProvider` can be set up either globally, on a boot-strapped logging system: 94 | 95 | ```swift 96 | LoggingSystem.bootstrap( 97 | metadataProvider: .myTracer, 98 | StreamLogHandler.standardOutput 99 | ) 100 | ``` 101 | 102 | or, using the metadata provider specific bootstrap method: 103 | 104 | ```swift 105 | LoggingSystem.bootstrapMetadataProvider(.myTracer) 106 | ``` 107 | 108 | It is also possible to configure a metadata provider on a per-logger basis, like this: 109 | 110 | ```swift 111 | let logger = Logger(label: "example", metadataProvider: Logger.MetadataProvider { 112 | guard let baggage = Baggage.current else { 113 | return [:] 114 | } 115 | guard let operationID = baggage.operationID else { 116 | return nil 117 | } 118 | return ["extra/opID": "\(opID)"] 119 | }) 120 | ``` 121 | 122 | which _overrides_ the default bootstrapped metadata provider. 123 | 124 | > NOTE: Setting the metadata provider on the logger directly means the `LoggingSystem` metadata provider 125 | > is skipped (if defined), following how an explicitly passed handler `factory` 126 | > overrides the `LoggingSystem`s `factory`. 127 | 128 | Once a metadata provider was set up, when a log statement is about to be emitted, the log handler implementation shall 129 | invoke it whenever it is about to emit a log statement. This must be done from the same asynchronous context as the log statement 130 | was made; I.e. if a log handler were to asynchronously–in a _detached_ task–create the actual log message, the invocation of 131 | the metadata provider still _must_ be performed before passing the data over to the other detached task. 132 | 133 | Usually, metadata providers will reach for the task's Swift Distributed Tracing [`Baggage`](https://github.com/apple/swift-distributed-tracing-baggage) which is the mechanism that swift-distributed-tracing 134 | and instrumentation libraries use to propagate metadata across asynchronous, as well as process, boundaries. Handlers 135 | may also inspect other task-local values, however they should not expect other libraries to be able to propagage those 136 | as well as they will propagate `Baggage` values. 137 | 138 | Those metadata will then be included in the log statement, e.g. like this: 139 | 140 | ```swift 141 | var baggage = Baggage.topLevel 142 | baggage.spanContext = SpanContext() 143 | Baggage.withValue(baggage) { 144 | test() 145 | } 146 | 147 | func test() { 148 | log.info("Test", metadata: ["oneOff": "42"]) 149 | // info [traceID: abc, spanID: 123, onOff: 42] Test 150 | } 151 | ``` 152 | 153 | ### Multiple `MetadataProvider`s using `MetadataProvider.multiplex(_:)` 154 | 155 | Borrowing the concept from log handlers, metadata providers also have a multiplexing implementation. 156 | It is defined as an extension on `MetadataProvider` and is useful in cases where users want to utilize 157 | more than one metadata provider at the same time: 158 | 159 | ```swift 160 | extension Logger.MetadataProvider { 161 | public static func multiplex(_ providers: [Logger.MetadataProvider]) -> Logger.MetadataProvider { 162 | precondition(!providers.isEmpty, "providers MUST NOT be empty") 163 | return Logger.MetadataProvider { baggage in 164 | providers.reduce(into: nil) { metadata, provider in 165 | if let providedMetadata = provider.metadata(baggage) { 166 | if metadata != nil { 167 | metadata!.merge(providedMetadata, uniquingKeysWith: { _, rhs in rhs }) 168 | } else { 169 | metadata = providedMetadata 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | Metadata keys are unique, so in case multiple metadata providers return the same key, 179 | the last provider in the array _wins_ and provides the value. 180 | 181 | Note that it is possible to query the `LoggingSystem` for the configured, system-wide, metadata provider, 182 | and combine it using a `multiplex` provider if the system-wide provider should not be replaced, but augmented 183 | by additional providers. 184 | 185 | ### Understanding Swift Distributed Tracing `Baggage` 186 | 187 | The `Baggage` type is more than just a fancy type-safe container for your values which are meant to be carried across 188 | using a single, well-known, [task-local value](https://developer.apple.com/documentation/swift/tasklocal). 189 | 190 | Values stored in `Baggage` are intended to be carried _across process boundaries_, and e.g. libraries such as HTTP clients, 191 | or other RPC mechanisms, or even messaging systems implement distributed tracing [instruments](https://github.com/apple/swift-distributed-tracing/blob/main/Sources/Instrumentation/Instrument.swift) 192 | which `inject` and `extract` baggage values into their respective carrier objects, e.g. an `HTTPRequest` in the case of an HTTP client. 193 | 194 | In other words, whenever intending to propagate information _across processes_ utilize the `Baggage` type to carry it, 195 | and ensure to provide an `Instrument` that is able to inject/extract the values of interest into the carrier types you are interested in. 196 | To learn more about baggage and instrumentation, refer to the [Swift Distributed Tracing](https://github.com/apple/swift-distributed-tracing/) library documentation. 197 | 198 | #### When to use `Baggage` vs. `Logger[metadataKey:]` 199 | 200 | While `Baggage` is context-dependent and changes depending on where the log methods are being called from, the metadata set on a `Logger` is static and not context-dependent. E.g, if you wanted to add things like an instance ID or a subsystem name to a `Logger`, that could be seen as static information and set via `Logger[metadataKey:]`. 201 | 202 | ```swift 203 | var logger = Logger(label: "org.swift.my-service") 204 | logger[metadataKey: "instanceId"] = "123" 205 | 206 | logger.info("Service started.") 207 | // [instanceId: 123] Service started. 208 | ``` 209 | 210 | On the other hand, things like a trace ID are dynamic and context-dependent, therefore would be obtained via `Baggage`: 211 | 212 | ```swift 213 | logger.info("Product fetched.") 214 | // [traceId: 42] Product fetched. 215 | ``` 216 | 217 | Inline-metadata is suitable for one-offs such as a `productId` or a `paymentMethod` in an online store service, but are not enough to corralate the following log statements, i.e. tying them both to the same request: 218 | 219 | ```swift 220 | logger.info("Product fetched.", metadata: ["productId": "42"]) 221 | logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"]) 222 | 223 | // [productId: 42] Product fetched. 224 | // [paymentMethod: apple-pay] Product fetched. 225 | ``` 226 | 227 | If there was a `Baggage` value with a trace ID surrounding these log statements, they would be automatically correlatable: 228 | 229 | ```swift 230 | var baggage = Baggage.topLevel 231 | baggage.traceID = 42 232 | Baggage.$current.withValue(baggage) { 233 | logger.info("Product fetched.", metadata: ["productId": "42"]) 234 | logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"]) 235 | } 236 | 237 | // [trace-id: 42, productId: 42] Product fetched. 238 | // [trace-id: 42, paymentMethod: apple-pay] Product fetched. 239 | ``` 240 | 241 | ## Alternatives considered 242 | 243 | ### MetadataProviders as a function of `Baggage -> Metadata` 244 | 245 | This was considered and fully developed, however it would cause swift-log to have a dependency on the instrumentation type `Baggage`, 246 | which was seen as in-conflict-with the interest of swift-log remaining a zero-dependency library. 247 | 248 | `Baggage` _is_ the well known type that is intended to be used for any kind of distributed systems instrumentation and tracing, 249 | however it adds additional complexity and confusion for users who are not interested in this domain. For example, developers 250 | may be confused about why `Baggage` and `Logger.Metadata` look somewhat similar, but behave very differently. This complexity 251 | is inherent to the two types actually being _very_ different, however we do not want to overwhelm newcomers or developers 252 | who are only intending to use swift-log within process. Such developers do not need to care about the added complexities 253 | of distributed systems. 254 | 255 | The tradeoff we take here is that every metadata provider will have to perform its own task-local (and therefore also thread-local), 256 | access in order to obtain the `Baggage` value, rather than the lookup being able to be performed _once_ and shared between 257 | multiple providers when a multiplex provider was configured in the system. We view this tradeoff as worth taking, as the cost 258 | of actually formatting the metadata usually strongly dominates the cost of the task-local lookup. 259 | 260 | ### Removing `LogHandler.Metadata` in favor of `Baggage` 261 | 262 | Removing logger metadata is not a good option because it serves a slightly different style of metadata than the baggage. 263 | 264 | Baggage is intended for contextual, task-local, metadata which is _carried across multiple libraries_. The values stored in baggage are well-typed, and must declare keys for accessing values in a baggage, this works well for multiple pieces of an application needing to reach for specific baggage items: everyone aware of the `traceID` key, is able to query the baggage for this key (`baggage.traceID`) and obtain a well-typed value. This comes with a higher cost on declaring keys though, as well as a global namespace for those - which is desirable for such kinds of metadata. 265 | 266 | This is not the same usage pattern as emitting a plain structured log where we'd like to include the name of an item we just queried: 267 | 268 | ```swift 269 | log.info("Obtained item! Hooray!", metadata: ["item": "\(item)"]) 270 | ``` 271 | 272 | In this example, the key/value pair for `"item"` is pretty ad-hoc, and we never need to refer to it elsewhere in the program. It never is queried by other pieces of the application, nor would it be useful to set it in baggage metadata, as the only purpose of the `item` key here is to log, and forget about it. 273 | 274 | ### Explicitly passing `Baggage` always 275 | 276 | Baggage is designed for use cases like distributed tracing, or similar instrumentation patterns where "all" participating code may need to reach for it. 277 | 278 | Specifically in logging, this means that _every_ call site for _every_ log statement would have to pass it explicitly resulting in annoying noisy code: 279 | 280 | ```swift 281 | class StoresRepository { 282 | func store(byID id: String, eventLoop: EventLoop, logger: Logger, baggage: Baggage) async throws -> Store { 283 | InstrumentationSystem.tracer.withSpan("Fetch Store") { span in 284 | logger.info("Fetched store.", baggage: span.baggage) 285 | } 286 | } 287 | } 288 | 289 | try await storesRepository.store( 290 | byID: storeID, 291 | eventLoop: eventLoop, 292 | logger: logger, 293 | baggage: baggage 294 | ) 295 | ``` 296 | 297 | ### Explicitly passing `Logger` always 298 | 299 | Imagine we don't have metadata providers, we'd have to manually set trace IDs on loggers which doesn't really work as all libraries involved would need to know about the same specific trace ID. Even if we inverted the dependency to have `Tracing` depend on `Logging` so that we'd be able to define something like "Tracing, please populate this logger with metadata", we'd have to make sure this thing is called in all places to avoid dropping contextual metadata. 300 | 301 | ```swift 302 | import Tracing 303 | import Logging 304 | 305 | let contextualLogger = InstrumentationSystem.tracer.populateTraceMetadata(logger) 306 | contextualLogger.info("Request received.") 307 | ``` 308 | 309 | ### Tracing providing extensions on Logger 310 | 311 | Instead of having `swift-log` depend on `swift-distributed-tracing-baggage` we could also create extensions for `Logger` inside `swift-distributed-tracing` and have users call these new overloaded methods instead: 312 | 313 | ```swift 314 | extension Logger { 315 | func tinfo(_ message: ..., baggage: Baggage?) { 316 | // ... 317 | } 318 | } 319 | ``` 320 | 321 | Such extensions could work like the currently-proposed APIs, but the risk of users calling the wrong methods is incredibly high and we cannot overload the existing methods' signatures because of ambiguity of call-sides without explicit Baggage being passed: 322 | 323 | ```swift 324 | logger.info("Hello") 325 | // we want this to pick up Baggage, but the signature would be ambiguous 326 | ``` 327 | 328 | Also, this extension-based contextual metadata would require basically everyone in Server-side Swift to adapt their usage of `Logging` to use these extensions instead. With the proposed APIs, we'd only need to modify `Logging` and any NIO-based libraries such as `AsyncHTTPClient`, `Vapor`, etc. and not every single log statement in every application. 329 | --------------------------------------------------------------------------------