├── .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 │ ├── LoggerLevel.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 2 | Tomer Doron tomer doron 3 | Tomer Doron tomer doron 4 | Johannes Weiss 5 | Max Moiseev 6 | Konrad `ktoso` Malawski 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 15 | - Iain Smith 16 | - Ian Partridge 17 | - Johannes Weiss 18 | - Kevin Sweeney 19 | - Konrad `ktoso` Malawski 20 | - Max Moiseev 21 | - Tanner 22 | - Thomas Krajacic 23 | - Tomer Doron 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..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 | First things first: This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. Apart from contributing to `SwiftLog` itself, there's another huge gap at the moment: `SwiftLog` is an _API package_ which tries to establish a common API the ecosystem can use. To make logging really work for real-world workloads, we need `SwiftLog`-compatible _logging backends_ which then either persist the log messages in files, render them in nicer colors on the terminal, or send them over to Splunk or ELK. 4 | 5 | What `SwiftLog` provides today can be found in the [API docs][api-docs]. 6 | 7 | ## Getting started 8 | 9 | If you have a server-side Swift application, or maybe a cross-platform (for example Linux & macOS) app/library, and you would like to log, we think targeting this logging API package is a great idea. Below you'll find all you need to know to get started. 10 | 11 | #### Adding the dependency 12 | 13 | `SwiftLog` is designed for Swift 5.8 and later. To depend on the logging API package, you need to declare your dependency in your `Package.swift`: 14 | 15 | ```swift 16 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 17 | ``` 18 | 19 | and to your application/library target, add `"Logging"` to your `dependencies`, e.g. like this: 20 | 21 | ```swift 22 | .target(name: "BestExampleApp", dependencies: [ 23 | .product(name: "Logging", package: "swift-log") 24 | ], 25 | ``` 26 | 27 | 28 | #### Let's log 29 | 30 | ```swift 31 | // 1) let's import the logging API package 32 | import Logging 33 | 34 | // 2) we need to create a logger, the label works similarly to a DispatchQueue label 35 | let logger = Logger(label: "com.example.BestExampleApp.main") 36 | 37 | // 3) we're now ready to use it 38 | logger.info("Hello World!") 39 | ``` 40 | 41 | #### Output 42 | 43 | ``` 44 | 2019-03-13T15:46:38+0000 info: Hello World! 45 | ``` 46 | 47 | #### Default `Logger` behavior 48 | 49 | `SwiftLog` provides for very basic console logging out-of-the-box by way of `StreamLogHandler`. It is possible to switch the default output to `stderr` like so: 50 | ```swift 51 | LoggingSystem.bootstrap(StreamLogHandler.standardError) 52 | ``` 53 | 54 | `StreamLogHandler` is primarily a convenience only and does not provide any substantial customization. Library maintainers who aim to build their own logging backends for integration and consumption should implement the `LogHandler` protocol directly as laid out in [the "On the implementation of a logging backend" section](#on-the-implementation-of-a-logging-backend-a-loghandler). 55 | 56 | For further information, please check the [API documentation][api-docs]. 57 | 58 | 59 | ## Available Logging Backends For Applications 60 | 61 | You can choose from one of the following backends to consume your logs. If you are interested in implementing one see the "Implementation considerations" section below explaining how to do so. List of existing SwiftLog API compatible libraries: 62 | 63 | | Repository | Handler Description| 64 | | ----------- | ----------- | 65 | | [Kitura/HeliumLogger](https://github.com/Kitura/HeliumLogger) |a logging backend widely used in the Kitura ecosystem | 66 | | [ianpartridge/swift-log-**syslog**](https://github.com/ianpartridge/swift-log-syslog) | a [syslog](https://en.wikipedia.org/wiki/Syslog) backend| 67 | | [Adorkable/swift-log-**format-and-pipe**](https://github.com/Adorkable/swift-log-format-and-pipe) | a backend that allows customization of the output format and the resulting destination | 68 | | [chrisaljoudi/swift-log-**oslog**](https://github.com/chrisaljoudi/swift-log-oslog) | an OSLog [Unified Logging](https://developer.apple.com/documentation/os/logging) backend for use on Apple platforms. **Important Note:** we recommend using os_log directly as described [here](https://developer.apple.com/documentation/os/logging). Using os_log through swift-log using this backend will be less efficient and will also prevent specifying the privacy of the message. The backend always uses `%{public}@` as the format string and eagerly converts all string interpolations to strings. This has two drawbacks: 1. the static components of the string interpolation would be eagerly copied by the unified logging system, which will result in loss of performance. 2. It makes all messages public, which changes the default privacy policy of os_log, and doesn't allow specifying fine-grained privacy of sections of the message. In a separate on-going work, Swift APIs for os_log are being improved and made to align closely with swift-log APIs. References: [Unifying Logging Levels](https://forums.swift.org/t/custom-string-interpolation-and-compile-time-interpretation-applied-to-logging/18799), [Making os_log accept string interpolations using compile-time interpretation](https://forums.swift.org/t/logging-levels-for-swifts-server-side-logging-apis-and-new-os-log-apis/20365). | 69 | | [Brainfinance/StackdriverLogging](https://github.com/Brainfinance/StackdriverLogging) | a structured JSON logging backend for use on Google Cloud Platform with the [Stackdriver logging agent](https://cloud.google.com/logging/docs/agent) | 70 | | [DnV1eX/GoogleCloudLogging](https://github.com/DnV1eX/GoogleCloudLogging) | a client-side library for logging application events in [Google Cloud](https://console.cloud.google.com/logs) via REST API v2. | 71 | | [vapor/console-kit](https://github.com/vapor/console-kit/) | a logger to the current terminal or stdout with stylized ([ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code)) output. The default logger for all Vapor applications | 72 | | [neallester/swift-log-testing](https://github.com/neallester/swift-log-testing) | provides access to log messages for use in assertions (within test targets) | 73 | | [wlisac/swift-log-slack](https://github.com/wlisac/swift-log-slack) | a logging backend that sends critical log messages to Slack | 74 | | [NSHipster/swift-log-github-actions](https://github.com/NSHipster/swift-log-github-actions) | a logging backend that translates logging messages into [workflow commands for GitHub Actions](https://help.github.com/en/actions/reference/workflow-commands-for-github-actions). | 75 | | [stevapple/swift-log-telegram](https://github.com/stevapple/swift-log-telegram) | a logging backend that sends log messages to any Telegram chat (Inspired by and forked from [wlisac/swift-log-slack](https://github.com/wlisac/swift-log-slack)) | 76 | | [jagreenwood/swift-log-datadog](https://github.com/jagreenwood/swift-log-datadog) | a logging backend which sends log messages to the [Datadog](https://www.datadoghq.com/log-management/) log management service | 77 | | [google/SwiftLogFireCloud](https://github.com/google/swiftlogfirecloud) | a logging backend for time series logging which pushes logs as flat files to Firebase Cloud Storage. | 78 | | [crspybits/swift-log-file](https://github.com/crspybits/swift-log-file) | a simple local file logger (using `Foundation` `FileManager`) | 79 | | [sushichop/Puppy](https://github.com/sushichop/Puppy) | a logging backend that supports multiple transports(console, file, syslog, etc.) and has the feature with formatting and file log rotation | 80 | | [ShivaHuang/swift-log-SwiftyBeaver](https://github.com/ShivaHuang/swift-log-SwiftyBeaver) | a logging backend for printing colored logging to Xcode console / file, or sending encrypted logging to [SwiftyBeaver](https://swiftybeaver.com) platform. | 81 | | [Apodini/swift-log-elk](https://github.com/Apodini/swift-log-elk) | a logging backend that formats, caches and sends log data to [elastic/logstash](https://github.com/elastic/logstash) | 82 | | [binaryscraping/swift-log-supabase](https://github.com/binaryscraping/swift-log-supabase) | a logging backend that sends log entries to [Supabase](https://github.com/supabase/supabase). | 83 | | [kiliankoe/swift-log-matrix](https://swiftpackageindex.com/kiliankoe/swift-log-matrix) | a logging backend for sending logs directly to a [Matrix](https://matrix.org) room | 84 | | [DiscordBM/DiscordLogger](https://github.com/DiscordBM/DiscordLogger) | a Discord logging implementation to send your logs over to a Discord channel in a good-looking manner and with a lot of configuration options including the ability to send only a few important log-levels such as `warning`/`error`/`critical`. | 85 | | [CocoaLumberjack](https://github.com/CocoaLumberjack/CocoaLumberjack) | a fast & simple, yet powerful & flexible logging framework for macOS, iOS, tvOS and watchOS, which includes a logging backend for swift-log. | 86 | | [rwbutler/swift-log-ecs](https://github.com/rwbutler/swift-log-ecs) | a logging backend for logging in [ECS Log format](https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html). Compatible with [Vapor](https://github.com/vapor/vapor) and allows chaining of multiple LogHandlers. | 87 | | [ShipBook/swift-log-**shipbook**](https://github.com/ShipBook/swift-log-shipbook) | a logging backend that sends log entries to [Shipbook](https://www.shipbook.io) - Shipbook gives you the power to remotely gather, search and analyze your user logs and exceptions in the cloud, on a per-user & session basis. | 88 | | [kasianov-mikhail/scout](https://github.com/kasianov-mikhail/scout) | CloudKit as a log storage | 89 | | [PADL/AndroidLogging](https://github.com/PADL/AndroidLogging) | a logging backend for the Android in-kernel log buffer | 90 | | [xtremekforever/swift-systemd](https://github.com/xtremekforever/swift-systemd) | a logging backend for the [systemd](https://systemd.io/) journal | 91 | | Your library? | [Get in touch!](https://forums.swift.org/c/server) | 92 | 93 | ## What is an API package? 94 | 95 | Glad you asked. We believe that for the Swift on Server ecosystem, it's crucial to have a logging API that can be adopted by anybody so a multitude of libraries from different parties can all log to a shared destination. More concretely this means that we believe all the log messages from all libraries end up in the same file, database, Elastic Stack/Splunk instance, or whatever you may choose. 96 | 97 | In the real-world however, there are so many opinions over how exactly a logging system should behave, what a log message should be formatted like, and where/how it should be persisted. We think it's not feasible to wait for one logging package to support everything that a specific deployment needs whilst still being easy enough to use and remain performant. That's why we decided to cut the problem in half: 98 | 99 | 1. a logging API 100 | 2. a logging backend implementation 101 | 102 | This package only provides the logging API itself and therefore `SwiftLog` is a 'logging API package'. `SwiftLog` (using `LoggingSystem.bootstrap`) can be configured to choose any compatible logging backend implementation. This way packages can adopt the API and the _application_ can choose any compatible logging backend implementation without requiring any changes from any of the libraries. 103 | 104 | Just for completeness sake: This API package does actually include an overly simplistic and non-configurable logging backend implementation which simply writes all log messages to `stdout`. The reason to include this overly simplistic logging backend implementation is to improve the first-time usage experience. Let's assume you start a project and try out `SwiftLog` for the first time, it's just a whole lot better to see something you logged appear on `stdout` in a simplistic format rather than nothing happening at all. For any real-world application, we advise configuring another logging backend implementation that logs in the style you like. 105 | 106 | ## The core concepts 107 | 108 | ### Loggers 109 | 110 | `Logger`s are used to emit log messages and therefore the most important type in `SwiftLog`, so their use should be as simple as possible. Most commonly, they are used to emit log messages in a certain log level. For example: 111 | 112 | ```swift 113 | // logging an informational message 114 | logger.info("Hello World!") 115 | 116 | // ouch, something went wrong 117 | logger.error("Houston, we have a problem: \(problem)") 118 | ``` 119 | 120 | ### Log levels 121 | 122 | The following log levels are supported: 123 | 124 | - `trace` 125 | - `debug` 126 | - `info` 127 | - `notice` 128 | - `warning` 129 | - `error` 130 | - `critical` 131 | 132 | The log level of a given logger can be changed, but the change will only affect the specific logger you changed it on. You could say the `Logger` is a _value type_ regarding the log level. 133 | 134 | 135 | ### Logging metadata 136 | 137 | Logging metadata is metadata that can be attached to loggers to add information that is crucial when debugging a problem. In servers, the usual example is attaching a request UUID to a logger that will then be present on all log messages logged with that logger. Example: 138 | 139 | ```swift 140 | var logger = logger 141 | logger[metadataKey: "request-uuid"] = "\(UUID())" 142 | logger.info("hello world") 143 | ``` 144 | 145 | will print 146 | 147 | ``` 148 | 2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED hello world 149 | ``` 150 | 151 | with the default logging backend implementation that ships with `SwiftLog`. Needless to say, the format is fully defined by the logging backend you choose. 152 | 153 | ## On the implementation of a logging backend (a `LogHandler`) 154 | 155 | Note: If you don't want to implement a custom logging backend, everything in this section is probably not very relevant, so please feel free to skip. 156 | 157 | To become a compatible logging backend that all `SwiftLog` consumers can use, you need to do two things: 1) Implement a type (usually a `struct`) that implements `LogHandler`, a protocol provided by `SwiftLog` and 2) instruct `SwiftLog` to use your logging backend implementation. 158 | 159 | A `LogHandler` or logging backend implementation is anything that conforms to the following protocol 160 | 161 | ```swift 162 | public protocol LogHandler { 163 | func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) 164 | 165 | subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set } 166 | 167 | var metadata: Logger.Metadata { get set } 168 | 169 | var logLevel: Logger.Level { get set } 170 | } 171 | ``` 172 | 173 | Instructing `SwiftLog` to use your logging backend as the one the whole application (including all libraries) should use is very simple: 174 | 175 | ```swift 176 | LoggingSystem.bootstrap(MyLogHandler.init) 177 | ``` 178 | 179 | ### Implementation considerations 180 | 181 | `LogHandler`s control most parts of the logging system: 182 | 183 | #### Under control of a `LogHandler` 184 | 185 | ##### Configuration 186 | 187 | `LogHandler`s control the two crucial pieces of `Logger` configuration, namely: 188 | 189 | - log level (`logger.logLevel` property) 190 | - logging metadata (`logger[metadataKey:]` and `logger.metadata`) 191 | 192 | For the system to work, however, it is important that `LogHandler` treat the configuration as _value types_. This means that `LogHandler`s should be `struct`s and a change in log level or logging metadata should only affect the very `LogHandler` it was changed on. 193 | 194 | However, in special cases, it is acceptable that a `LogHandler` provides some global log level override that may affect all `LogHandler`s created. 195 | 196 | ##### Emitting 197 | - emitting the log message itself 198 | 199 | ### Not under control of `LogHandler`s 200 | 201 | `LogHandler`s do not control if a message should be logged or not. `Logger` will only invoke the `log` function of a `LogHandler` if `Logger` determines that a log message should be emitted given the configured log level. 202 | 203 | ## Source vs Label 204 | 205 | A `Logger` carries an (immutable) `label` and each log message carries a `source` parameter (since SwiftLog 1.3.0). The `Logger`'s label 206 | identifies the creator of the `Logger`. If you are using structured logging by preserving metadata across multiple modules, the `Logger`'s 207 | `label` is not a good way to identify where a log message originated from as it identifies the creator of a `Logger` which is often passed 208 | around between libraries to preserve metadata and the like. 209 | 210 | If you want to filter all log messages originating from a certain subsystem, filter by `source` which defaults to the module that is emitting the 211 | log message. 212 | 213 | ## Security 214 | 215 | Please see [SECURITY.md](SECURITY.md) for SwiftLog's security process. 216 | 217 | ## Design 218 | 219 | This logging API was designed with the contributors to the Swift on Server community and approved by the [SSWG (Swift Server Work Group)](https://swift.org/server/) to the 'sandbox level' of the SSWG's [incubation process](https://www.swift.org/sswg/incubation-process.html). 220 | 221 | - [pitch](https://forums.swift.org/t/logging/16027), [discussion](https://forums.swift.org/t/discussion-server-logging-api/18834), [feedback](https://forums.swift.org/t/feedback-server-logging-api-with-revisions/19375) 222 | - [log levels](https://forums.swift.org/t/logging-levels-for-swifts-server-side-logging-apis-and-new-os-log-apis/20365) 223 | 224 | [api-docs]: https://apple.github.io/swift-log/docs/current/Logging/Structs/Logger.html 225 | -------------------------------------------------------------------------------- /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/LoggerLevel.md: -------------------------------------------------------------------------------- 1 | # ``Logging/Logger/Level`` 2 | 3 | ## Topics 4 | 5 | ### Log levels 6 | 7 | - ``trace`` 8 | - ``debug`` 9 | - ``info`` 10 | - ``notice`` 11 | - ``warning`` 12 | - ``error`` 13 | - ``critical`` 14 | -------------------------------------------------------------------------------- /Sources/Logging/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``Logging`` 2 | 3 | A Logging API for Swift. 4 | 5 | ## Getting Started 6 | 7 | If you have a server-side Swift application, or maybe a cross-platform (for example Linux & macOS) app/library, and you would like to log, we think targeting this logging API package is a great idea. Below you'll find all you need to know to get started. 8 | 9 | ### Log handler implementations 10 | 11 | This package offers the common Logging API as well as a very simple stdout log handler. 12 | 13 | In a real system or application, you will want to use one of the many community maintained log handler implementations. 14 | 15 | Please refer to the [complete list of community maintained logging backend implementations](https://github.com/apple/swift-log#selecting-a-logging-backend-implementation-applications-only). 16 | 17 | ## Topics 18 | 19 | ### Logging API 20 | 21 | - ``Logger`` 22 | - ``LoggingSystem`` 23 | 24 | ### Log Handlers 25 | 26 | - ``LogHandler`` 27 | - ``MultiplexLogHandler`` 28 | - ``StreamLogHandler`` 29 | - ``SwiftLogNoOpLogHandler`` 30 | 31 | -------------------------------------------------------------------------------- /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 = 56 | UnsafeMutablePointer.allocate(capacity: 1) 57 | #else 58 | fileprivate let mutex: UnsafeMutablePointer = 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(_ 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 = 158 | UnsafeMutablePointer.allocate(capacity: 1) 159 | fileprivate var shared: Bool = true 160 | #else 161 | fileprivate let rwlock: UnsafeMutablePointer = 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(_ 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(_ 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 | /// LoggingSystem.bootstrap(MyLogHandler.init) // your LogHandler might have a different bootstrapping step 35 | /// var logger1 = Logger(label: "first logger") 36 | /// logger1.logLevel = .debug 37 | /// logger1[metadataKey: "only-on"] = "first" 38 | /// 39 | /// var logger2 = logger1 40 | /// logger2.logLevel = .error // this must not override `logger1`'s log level 41 | /// logger2[metadataKey: "only-on"] = "second" // this must not override `logger1`'s metadata 42 | /// 43 | /// XCTAssertEqual(.debug, logger1.logLevel) 44 | /// XCTAssertEqual(.error, logger2.logLevel) 45 | /// XCTAssertEqual("first", logger1[metadataKey: "only-on"]) 46 | /// XCTAssertEqual("second", logger2[metadataKey: "only-on"]) 47 | /// ``` 48 | /// 49 | /// ### Special cases 50 | /// 51 | /// In certain special cases, the log level behaving like a value on `Logger` might not be what you want. For example, 52 | /// you might want to set the log level across _all_ `Logger`s to `.debug` when say a signal (eg. `SIGUSR1`) is received 53 | /// to be able to debug special failures in production. This special case is acceptable but we urge you to create a 54 | /// solution specific to your `LogHandler` implementation to achieve that. Please find an example implementation of this 55 | /// behavior below, on reception of the signal you would call 56 | /// `LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel = .debug`, for example. 57 | /// 58 | /// ```swift 59 | /// import class Foundation.NSLock 60 | /// 61 | /// public struct LogHandlerWithGlobalLogLevelOverride: LogHandler { 62 | /// // the static properties hold the globally overridden log level (if overridden) 63 | /// private static let overrideLock = NSLock() 64 | /// private static var overrideLogLevel: Logger.Level? = nil 65 | /// 66 | /// // this holds the log level if not overridden 67 | /// private var _logLevel: Logger.Level = .info 68 | /// 69 | /// // metadata storage 70 | /// public var metadata: Logger.Metadata = [:] 71 | /// 72 | /// public init(label: String) { 73 | /// // [...] 74 | /// } 75 | /// 76 | /// public var logLevel: Logger.Level { 77 | /// // when we get asked for the log level, we check if it was globally overridden or not 78 | /// get { 79 | /// LogHandlerWithGlobalLogLevelOverride.overrideLock.lock() 80 | /// defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() } 81 | /// return LogHandlerWithGlobalLogLevelOverride.overrideLogLevel ?? self._logLevel 82 | /// } 83 | /// // we set the log level whenever we're asked (note: this might not have an effect if globally 84 | /// // overridden) 85 | /// set { 86 | /// self._logLevel = newValue 87 | /// } 88 | /// } 89 | /// 90 | /// public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, 91 | /// source: String, file: String, function: String, line: UInt) { 92 | /// // [...] 93 | /// } 94 | /// 95 | /// public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { 96 | /// get { 97 | /// return self.metadata[metadataKey] 98 | /// } 99 | /// set(newValue) { 100 | /// self.metadata[metadataKey] = newValue 101 | /// } 102 | /// } 103 | /// 104 | /// // this is the function to globally override the log level, it is not part of the `LogHandler` protocol 105 | /// public static func overrideGlobalLogLevel(_ logLevel: Logger.Level) { 106 | /// LogHandlerWithGlobalLogLevelOverride.overrideLock.lock() 107 | /// defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() } 108 | /// LogHandlerWithGlobalLogLevelOverride.overrideLogLevel = logLevel 109 | /// } 110 | /// } 111 | /// ``` 112 | /// 113 | /// Please note that the above `LogHandler` will still pass the 'log level is a value' test above it iff the global log 114 | /// level has not been overridden. And most importantly it passes the requirement listed above: A change to the log 115 | /// level on one `Logger` should not affect the log level of another `Logger` variable. 116 | public protocol LogHandler: _SwiftLogSendableLogHandler { 117 | /// The metadata provider this `LogHandler` will use when a log statement is about to be emitted. 118 | /// 119 | /// A ``Logger/MetadataProvider`` may add a constant set of metadata, 120 | /// or use task-local values to pick up contextual metadata and add it to emitted logs. 121 | var metadataProvider: Logger.MetadataProvider? { get set } 122 | 123 | /// This method is called when a `LogHandler` must emit a log message. There is no need for the `LogHandler` to 124 | /// check if the `level` is above or below the configured `logLevel` as `Logger` already performed this check and 125 | /// determined that a message should be logged. 126 | /// 127 | /// - parameters: 128 | /// - level: The log level the message was logged at. 129 | /// - message: The message to log. To obtain a `String` representation call `message.description`. 130 | /// - metadata: The metadata associated to this log message. 131 | /// - source: The source where the log message originated, for example the logging module. 132 | /// - file: The file the log message was emitted from. 133 | /// - function: The function the log line was emitted from. 134 | /// - line: The line the log message was emitted from. 135 | func log( 136 | level: Logger.Level, 137 | message: Logger.Message, 138 | metadata: Logger.Metadata?, 139 | source: String, 140 | file: String, 141 | function: String, 142 | line: UInt 143 | ) 144 | 145 | /// SwiftLog 1.0 compatibility method. Please do _not_ implement, implement 146 | /// `log(level:message:metadata:source:file:function:line:)` instead. 147 | @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") 148 | func log( 149 | level: Logging.Logger.Level, 150 | message: Logging.Logger.Message, 151 | metadata: Logging.Logger.Metadata?, 152 | file: String, 153 | function: String, 154 | line: UInt 155 | ) 156 | 157 | /// Add, remove, or change the logging metadata. 158 | /// 159 | /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must 160 | /// only affect this very `LogHandler`. 161 | /// 162 | /// - parameters: 163 | /// - metadataKey: The key for the metadata item 164 | subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set } 165 | 166 | /// Get or set the entire metadata storage as a dictionary. 167 | /// 168 | /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must 169 | /// only affect this very `LogHandler`. 170 | var metadata: Logger.Metadata { get set } 171 | 172 | /// Get or set the configured log level. 173 | /// 174 | /// - note: `LogHandler`s must treat the log level as a value type. This means that the change in metadata must 175 | /// only affect this very `LogHandler`. It is acceptable to provide some form of global log level override 176 | /// that means a change in log level on a particular `LogHandler` might not be reflected in any 177 | /// `LogHandler`. 178 | var logLevel: Logger.Level { get set } 179 | } 180 | 181 | extension LogHandler { 182 | /// Default implementation for `metadataProvider` which defaults to `nil`. 183 | /// This default exists in order to facilitate source-compatible introduction of the `metadataProvider` protocol requirement. 184 | public var metadataProvider: Logger.MetadataProvider? { 185 | get { 186 | nil 187 | } 188 | set { 189 | #if DEBUG 190 | if LoggingSystem.warnOnceLogHandlerNotSupportedMetadataProvider(Self.self) { 191 | self.log( 192 | level: .warning, 193 | message: 194 | "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.", 195 | metadata: nil, 196 | source: "Logging", 197 | file: #file, 198 | function: #function, 199 | line: #line 200 | ) 201 | } 202 | #endif 203 | } 204 | } 205 | } 206 | 207 | extension LogHandler { 208 | @available(*, deprecated, message: "You should implement this method instead of using the default implementation") 209 | public func log( 210 | level: Logger.Level, 211 | message: Logger.Message, 212 | metadata: Logger.Metadata?, 213 | source: String, 214 | file: String, 215 | function: String, 216 | line: UInt 217 | ) { 218 | self.log(level: level, message: message, metadata: metadata, file: file, function: function, line: line) 219 | } 220 | 221 | @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") 222 | public func log( 223 | level: Logging.Logger.Level, 224 | message: Logging.Logger.Message, 225 | metadata: Logging.Logger.Metadata?, 226 | file: String, 227 | function: String, 228 | line: UInt 229 | ) { 230 | self.log( 231 | level: level, 232 | message: message, 233 | metadata: metadata, 234 | source: Logger.currentModule(filePath: file), 235 | file: file, 236 | function: function, 237 | line: line 238 | ) 239 | } 240 | } 241 | 242 | // MARK: - Sendable support helpers 243 | 244 | @preconcurrency public protocol _SwiftLogSendableLogHandler: Sendable {} 245 | -------------------------------------------------------------------------------- /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: @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(_ 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(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)$" 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)$" 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) -> 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.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.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.size 1264 | XCTAssertEqual(MemoryLayout.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(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(_ 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 | --------------------------------------------------------------------------------