The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .github
    ├── ISSUE_TEMPLATE.md
    ├── PULL_REQUEST_TEMPLATE.md
    ├── release.yml
    └── workflows
    │   ├── main.yml
    │   ├── pull_request.yml
    │   └── pull_request_label.yml
├── .gitignore
├── .licenseignore
├── .mailmap
├── .spi.yml
├── .swift-format
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTORS.txt
├── LICENSE.txt
├── NOTICE.txt
├── Package.swift
├── README.md
├── SECURITY.md
├── Sources
    └── Logging
    │   ├── Docs.docc
    │       ├── BestPractices
    │       │   └── 001-ChoosingLogLevels.md
    │       ├── ImplementingALogHandler.md
    │       ├── LoggingBestPractices.md
    │       └── index.md
    │   ├── Locks.swift
    │   ├── LogHandler.swift
    │   ├── Logging.swift
    │   └── MetadataProvider.swift
├── Tests
    └── LoggingTests
    │   ├── CompatibilityTest.swift
    │   ├── GlobalLoggingTest.swift
    │   ├── LocalLoggingTest.swift
    │   ├── LoggingTest.swift
    │   ├── MDCTest.swift
    │   ├── MetadataProviderTest.swift
    │   ├── SubDirectoryOfLoggingTests
    │       └── EmitALogFromSubDirectory.swift
    │   ├── TestLogger.swift
    │   └── TestSendable.swift
├── dev
    └── git.commit.template
└── proposals
    └── 0001-metadata-providers.md


/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | 
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
 1 | ### Expected behavior
 2 | _[what you expected to happen]_
 3 | 
 4 | ### Actual behavior
 5 | _[what actually happened]_
 6 | 
 7 | ### Steps to reproduce
 8 | 
 9 | 1. ...
10 | 2. ...
11 | 
12 | ### If possible, minimal yet complete reproducer code (or URL to code)
13 | 
14 | _[anything to help us reproducing the issue]_
15 | 
16 | ### SwiftLog version/commit hash
17 | 
18 | _[the SwiftLog tag/commit hash]_
19 | 
20 | ### Swift & OS version (output of `swift --version && uname -a`)
21 | 


--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
 1 | _[One line description of your change]_
 2 | 
 3 | ### Motivation:
 4 | 
 5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_
 6 | 
 7 | ### Modifications:
 8 | 
 9 | _[Describe the modifications you've done.]_
10 | 
11 | ### Result:
12 | 
13 | _[After your change, what will change.]_
14 | 


--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
 1 | changelog:
 2 |   categories:
 3 |     - title: SemVer Major
 4 |       labels:
 5 |         - ⚠️ semver/major
 6 |     - title: SemVer Minor
 7 |       labels:
 8 |         - 🆕 semver/minor
 9 |     - title: SemVer Patch
10 |       labels:
11 |         - 🔨 semver/patch
12 |     - title: Other Changes
13 |       labels:
14 |         - semver/none
15 | 


--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
 1 | name: Main
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   schedule:
 7 |     - cron: "0 8,20 * * *"
 8 | 
 9 | jobs:
10 |   unit-tests:
11 |     name: Unit tests
12 |     uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
13 |     with:
14 |       linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
15 |       linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
16 |       linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
17 |       linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
18 |       linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
19 |       linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
20 |       windows_6_0_enabled: true
21 |       windows_6_1_enabled: true
22 |       windows_nightly_next_enabled: true
23 |       windows_nightly_main_enabled: true
24 |       windows_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
25 |       windows_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
26 |       windows_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
27 |       windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
28 | 
29 |   cxx-interop:
30 |     name: Cxx interop
31 |     uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
32 | 
33 |   static-sdk:
34 |     name: Static SDK
35 |     # Workaround https://github.com/nektos/act/issues/1875
36 |     uses: apple/swift-nio/.github/workflows/static_sdk.yml@main
37 | 
38 |   macos-tests:
39 |     name: macOS tests
40 |     uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
41 |     with:
42 |       runner_pool: nightly
43 |       build_scheme: swift-log
44 | 


--------------------------------------------------------------------------------
/.github/workflows/pull_request.yml:
--------------------------------------------------------------------------------
 1 | name: PR
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     types: [opened, reopened, synchronize]
 6 | 
 7 | jobs:
 8 |   soundness:
 9 |     name: Soundness
10 |     uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
11 |     with:
12 |       license_header_check_project_name: "Swift Logging API"
13 | 
14 |   unit-tests:
15 |     name: Unit tests
16 |     uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
17 |     with:
18 |       linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
19 |       linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
20 |       linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
21 |       linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
22 |       linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
23 |       linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
24 |       windows_6_0_enabled: true
25 |       windows_6_1_enabled: true
26 |       windows_nightly_next_enabled: true
27 |       windows_nightly_main_enabled: true
28 |       windows_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
29 |       windows_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
30 |       windows_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
31 |       windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
32 | 
33 |   cxx-interop:
34 |     name: Cxx interop
35 |     uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
36 | 
37 |   static-sdk:
38 |     name: Static SDK
39 |     # Workaround https://github.com/nektos/act/issues/1875
40 |     uses: apple/swift-nio/.github/workflows/static_sdk.yml@main
41 | 
42 |   macos-tests:
43 |     name: macOS tests
44 |     uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
45 |     with:
46 |       runner_pool: general
47 |       build_scheme: swift-log
48 | 


--------------------------------------------------------------------------------
/.github/workflows/pull_request_label.yml:
--------------------------------------------------------------------------------
 1 | name: PR label
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     types: [labeled, unlabeled, opened, reopened, synchronize]
 6 | 
 7 | jobs:
 8 |   semver-label-check:
 9 |     name: Semantic version label check
10 |     runs-on: ubuntu-latest
11 |     timeout-minutes: 1
12 |     steps:
13 |       - name: Checkout repository
14 |         uses: actions/checkout@v4
15 |         with:
16 |           persist-credentials: false
17 |       - name: Check for Semantic Version label
18 |         uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main
19 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | Package.resolved
 3 | /.build
 4 | /Packages
 5 | /*.xcodeproj
 6 | .xcode
 7 | DerivedData
 8 | .swiftpm
 9 | 
10 | .SourceKitten/
11 | docs/
12 | 
13 | .*.sw?
14 | 


--------------------------------------------------------------------------------
/.licenseignore:
--------------------------------------------------------------------------------
 1 | .gitignore
 2 | **/.gitignore
 3 | .licenseignore
 4 | .unacceptablelanguageignore
 5 | .gitattributes
 6 | .git-blame-ignore-revs
 7 | .mailfilter
 8 | .mailmap
 9 | .spi.yml
10 | .swift-format
11 | .editorconfig
12 | .github/*
13 | *.md
14 | *.txt
15 | *.yml
16 | *.yaml
17 | *.json
18 | Package.swift
19 | **/Package.swift
20 | Package@-*.swift
21 | **/Package@-*.swift
22 | Package.resolved
23 | **/Package.resolved
24 | Makefile
25 | *.modulemap
26 | **/*.modulemap
27 | **/*.docc/*
28 | *.xcprivacy
29 | **/*.xcprivacy
30 | *.symlink
31 | **/*.symlink
32 | Dockerfile
33 | **/Dockerfile
34 | Snippets/*
35 | dev/git.commit.template
36 | dev/update-benchmark-thresholds
37 | *.crt
38 | **/*.crt
39 | *.pem
40 | **/*.pem
41 | *.der
42 | **/*.der
43 | 


--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Tomer Doron <tomer@apple.com> <tomer.doron@gmail.com>
2 | Tomer Doron <tomer@apple.com> tomer doron <tomer@apple.com>
3 | Tomer Doron <tomer@apple.com> tomer doron <tomerd@apple.com>
4 | Johannes Weiss <johannesweiss@apple.com> <johannes@jweiss.io>
5 | Max Moiseev <moiseev@apple.com> <moiseev@users.noreply.github.com>
6 | Konrad `ktoso` Malawski <ktoso@apple.com> <konrad.malawski@project13.pl>
7 | 


--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 |   configs:
4 |     - documentation_targets: [Logging]
5 | 


--------------------------------------------------------------------------------
/.swift-format:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version" : 1,
 3 |   "indentation" : {
 4 |     "spaces" : 4
 5 |   },
 6 |   "tabWidth" : 4,
 7 |   "fileScopedDeclarationPrivacy" : {
 8 |     "accessLevel" : "private"
 9 |   },
10 |   "spacesAroundRangeFormationOperators" : false,
11 |   "indentConditionalCompilationBlocks" : false,
12 |   "indentSwitchCaseLabels" : false,
13 |   "lineBreakAroundMultilineExpressionChainComponents" : false,
14 |   "lineBreakBeforeControlFlowKeywords" : false,
15 |   "lineBreakBeforeEachArgument" : true,
16 |   "lineBreakBeforeEachGenericRequirement" : true,
17 |   "lineLength" : 120,
18 |   "maximumBlankLines" : 1,
19 |   "respectsExistingLineBreaks" : true,
20 |   "prioritizeKeepingFunctionOutputTogether" : true,
21 |   "rules" : {
22 |     "AllPublicDeclarationsHaveDocumentation" : false,
23 |     "AlwaysUseLiteralForEmptyCollectionInit" : false,
24 |     "AlwaysUseLowerCamelCase" : false,
25 |     "AmbiguousTrailingClosureOverload" : true,
26 |     "BeginDocumentationCommentWithOneLineSummary" : false,
27 |     "DoNotUseSemicolons" : true,
28 |     "DontRepeatTypeInStaticProperties" : true,
29 |     "FileScopedDeclarationPrivacy" : true,
30 |     "FullyIndirectEnum" : true,
31 |     "GroupNumericLiterals" : true,
32 |     "IdentifiersMustBeASCII" : true,
33 |     "NeverForceUnwrap" : false,
34 |     "NeverUseForceTry" : false,
35 |     "NeverUseImplicitlyUnwrappedOptionals" : false,
36 |     "NoAccessLevelOnExtensionDeclaration" : true,
37 |     "NoAssignmentInExpressions" : true,
38 |     "NoBlockComments" : true,
39 |     "NoCasesWithOnlyFallthrough" : true,
40 |     "NoEmptyTrailingClosureParentheses" : true,
41 |     "NoLabelsInCasePatterns" : true,
42 |     "NoLeadingUnderscores" : false,
43 |     "NoParensAroundConditions" : true,
44 |     "NoVoidReturnOnFunctionSignature" : true,
45 |     "OmitExplicitReturns" : true,
46 |     "OneCasePerLine" : true,
47 |     "OneVariableDeclarationPerLine" : true,
48 |     "OnlyOneTrailingClosureArgument" : true,
49 |     "OrderedImports" : true,
50 |     "ReplaceForEachWithForLoop" : true,
51 |     "ReturnVoidInsteadOfEmptyTuple" : true,
52 |     "UseEarlyExits" : false,
53 |     "UseExplicitNilCheckInConditions" : false,
54 |     "UseLetInEveryBoundCaseVariable" : false,
55 |     "UseShorthandTypeNames" : true,
56 |     "UseSingleLinePropertyGetter" : false,
57 |     "UseSynthesizedInitializer" : false,
58 |     "UseTripleSlashForDocumentationComments" : true,
59 |     "UseWhereClausesInForLoops" : false,
60 |     "ValidateDocumentationComments" : false
61 |   }
62 | }
63 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 | 
3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct.
4 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | ## Legal
 2 | 
 3 | By submitting a pull request, you represent that you have the right to license
 4 | your contribution to Apple and the community, and agree by submitting the patch
 5 | that your contributions are licensed under the Apache 2.0 license (see
 6 | `LICENSE.txt`).
 7 | 
 8 | ## How to submit a bug report
 9 | 
10 | Please ensure to specify the following:
11 | 
12 | * SwiftLog commit hash
13 | * Contextual information (e.g. what you were trying to achieve with SwiftLog)
14 | * Simplest possible steps to reproduce
15 |   * More complex the steps are, lower the priority will be.
16 |   * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description.
17 | * Anything that might be relevant in your opinion, such as:
18 |   * Swift version or the output of `swift --version`
19 |   * OS version and the output of `uname -a`
20 |   * Network configuration
21 | 
22 | ### Example
23 | 
24 | ```
25 | SwiftLog commit hash: 4fe877816ad82627602377f415b6a66850214824
26 | 
27 | Context:
28 | While testing my application that uses with SwiftLog, I noticed that ...
29 | 
30 | Steps to reproduce:
31 | 1. ...
32 | 2. ...
33 | 3. ...
34 | 4. ...
35 | 
36 | $ swift --version
37 | Swift version 4.0.2 (swift-4.0.2-RELEASE)
38 | Target: x86_64-unknown-linux-gnu
39 | 
40 | Operating system: Ubuntu Linux 16.04 64-bit
41 | 
42 | $ uname -a
43 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
44 | 
45 | My system has IPv6 disabled.
46 | ```
47 | 
48 | ## Writing a Patch
49 | 
50 | A good SwiftLog patch is:
51 | 
52 | 1. Concise, and contains as few changes as needed to achieve the end result.
53 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it.
54 | 3. Documented, adding API documentation as needed to cover new functions and properties.
55 | 4. Accompanied by a great commit message, using our commit message template.
56 | 
57 | ### Commit Message Template
58 | 
59 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run:
60 | 
61 |     git config commit.template dev/git.commit.template
62 | 
63 | ### Make sure Tests work on Linux
64 | 
65 | SwiftLog uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not.
66 | For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made.
67 | 
68 | ### Run `./scripts/soundness.sh`
69 | 
70 | The scripts directory contains a [soundness.sh script](https://github.com/apple/swift-log/blob/main/scripts/soundness.sh) 
71 | that enforces additional checks, like license headers and formatting style.
72 | 
73 | Please make sure to `./scripts/soundness.sh` before pushing a change upstream, otherwise it is likely the PR validation will fail
74 | on minor changes such as a missing `self.` or similar formatting issues.
75 | 
76 | > The script also executes the above mentioned `generate_linux_tests.rb`.
77 | 
78 | For frequent contributors, we recommend adding the script as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which you can do via executing the following command in the project root directory: 
79 | 
80 | ```bash
81 | cat << EOF > .git/hooks/pre-push
82 | #!/bin/bash
83 | 
84 | if [[ -f "scripts/soundness.sh" ]]; then
85 |   scripts/soundness.sh
86 | fi
87 | EOF
88 | ```
89 | 
90 | Which makes the script execute, and only allow the `git push` to complete if the check has passed.
91 | 
92 | In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. 
93 | 
94 | ## How to contribute your work
95 | 
96 | Please open a pull request at https://github.com/apple/swift-log. Make sure the CI passes, and then wait for code review.
97 | 


--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
 1 | For the purpose of tracking copyright, this is the list of individuals and
 2 | organizations who have contributed source code to the Swift Logging API.
 3 | 
 4 | For employees of an organization/company where the copyright of work done
 5 | by employees of that company is held by the company itself, only the company
 6 | needs to be listed here.
 7 | 
 8 | ## COPYRIGHT HOLDERS
 9 | 
10 | - Apple Inc. (all contributors with '@apple.com')
11 | 
12 | ### Contributors
13 | 
14 | - David Hart <david@hartbit.com>
15 | - Iain Smith <iainsmith@users.noreply.github.com>
16 | - Ian Partridge <i.partridge@uk.ibm.com>
17 | - Johannes Weiss <johannesweiss@apple.com>
18 | - Kevin Sweeney <kevin.t.sweeney@gmail.com>
19 | - Konrad `ktoso` Malawski <ktoso@apple.com>
20 | - Max Moiseev <moiseev@apple.com>
21 | - Tanner <me@tanner.xyz>
22 | - Thomas Krajacic <tkrajacic@users.noreply.github.com>
23 | - Tomer Doron <tomer@apple.com>
24 | 
25 | **Updating this list**
26 | 
27 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap`
28 | 


--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
  1 | 
  2 |                                  Apache License
  3 |                            Version 2.0, January 2004
  4 |                         http://www.apache.org/licenses/
  5 | 
  6 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7 | 
  8 |    1. Definitions.
  9 | 
 10 |       "License" shall mean the terms and conditions for use, reproduction,
 11 |       and distribution as defined by Sections 1 through 9 of this document.
 12 | 
 13 |       "Licensor" shall mean the copyright owner or entity authorized by
 14 |       the copyright owner that is granting the License.
 15 | 
 16 |       "Legal Entity" shall mean the union of the acting entity and all
 17 |       other entities that control, are controlled by, or are under common
 18 |       control with that entity. For the purposes of this definition,
 19 |       "control" means (i) the power, direct or indirect, to cause the
 20 |       direction or management of such entity, whether by contract or
 21 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 22 |       outstanding shares, or (iii) beneficial ownership of such entity.
 23 | 
 24 |       "You" (or "Your") shall mean an individual or Legal Entity
 25 |       exercising permissions granted by this License.
 26 | 
 27 |       "Source" form shall mean the preferred form for making modifications,
 28 |       including but not limited to software source code, documentation
 29 |       source, and configuration files.
 30 | 
 31 |       "Object" form shall mean any form resulting from mechanical
 32 |       transformation or translation of a Source form, including but
 33 |       not limited to compiled object code, generated documentation,
 34 |       and conversions to other media types.
 35 | 
 36 |       "Work" shall mean the work of authorship, whether in Source or
 37 |       Object form, made available under the License, as indicated by a
 38 |       copyright notice that is included in or attached to the work
 39 |       (an example is provided in the Appendix below).
 40 | 
 41 |       "Derivative Works" shall mean any work, whether in Source or Object
 42 |       form, that is based on (or derived from) the Work and for which the
 43 |       editorial revisions, annotations, elaborations, or other modifications
 44 |       represent, as a whole, an original work of authorship. For the purposes
 45 |       of this License, Derivative Works shall not include works that remain
 46 |       separable from, or merely link (or bind by name) to the interfaces of,
 47 |       the Work and Derivative Works thereof.
 48 | 
 49 |       "Contribution" shall mean any work of authorship, including
 50 |       the original version of the Work and any modifications or additions
 51 |       to that Work or Derivative Works thereof, that is intentionally
 52 |       submitted to Licensor for inclusion in the Work by the copyright owner
 53 |       or by an individual or Legal Entity authorized to submit on behalf of
 54 |       the copyright owner. For the purposes of this definition, "submitted"
 55 |       means any form of electronic, verbal, or written communication sent
 56 |       to the Licensor or its representatives, including but not limited to
 57 |       communication on electronic mailing lists, source code control systems,
 58 |       and issue tracking systems that are managed by, or on behalf of, the
 59 |       Licensor for the purpose of discussing and improving the Work, but
 60 |       excluding communication that is conspicuously marked or otherwise
 61 |       designated in writing by the copyright owner as "Not a Contribution."
 62 | 
 63 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 64 |       on behalf of whom a Contribution has been received by Licensor and
 65 |       subsequently incorporated within the Work.
 66 | 
 67 |    2. Grant of Copyright License. Subject to the terms and conditions of
 68 |       this License, each Contributor hereby grants to You a perpetual,
 69 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 70 |       copyright license to reproduce, prepare Derivative Works of,
 71 |       publicly display, publicly perform, sublicense, and distribute the
 72 |       Work and such Derivative Works in Source or Object form.
 73 | 
 74 |    3. Grant of Patent License. Subject to the terms and conditions of
 75 |       this License, each Contributor hereby grants to You a perpetual,
 76 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 77 |       (except as stated in this section) patent license to make, have made,
 78 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 79 |       where such license applies only to those patent claims licensable
 80 |       by such Contributor that are necessarily infringed by their
 81 |       Contribution(s) alone or by combination of their Contribution(s)
 82 |       with the Work to which such Contribution(s) was submitted. If You
 83 |       institute patent litigation against any entity (including a
 84 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 85 |       or a Contribution incorporated within the Work constitutes direct
 86 |       or contributory patent infringement, then any patent licenses
 87 |       granted to You under this License for that Work shall terminate
 88 |       as of the date such litigation is filed.
 89 | 
 90 |    4. Redistribution. You may reproduce and distribute copies of the
 91 |       Work or Derivative Works thereof in any medium, with or without
 92 |       modifications, and in Source or Object form, provided that You
 93 |       meet the following conditions:
 94 | 
 95 |       (a) You must give any other recipients of the Work or
 96 |           Derivative Works a copy of this License; and
 97 | 
 98 |       (b) You must cause any modified files to carry prominent notices
 99 |           stating that You changed the files; and
100 | 
101 |       (c) You must retain, in the Source form of any Derivative Works
102 |           that You distribute, all copyright, patent, trademark, and
103 |           attribution notices from the Source form of the Work,
104 |           excluding those notices that do not pertain to any part of
105 |           the Derivative Works; and
106 | 
107 |       (d) If the Work includes a "NOTICE" text file as part of its
108 |           distribution, then any Derivative Works that You distribute must
109 |           include a readable copy of the attribution notices contained
110 |           within such NOTICE file, excluding those notices that do not
111 |           pertain to any part of the Derivative Works, in at least one
112 |           of the following places: within a NOTICE text file distributed
113 |           as part of the Derivative Works; within the Source form or
114 |           documentation, if provided along with the Derivative Works; or,
115 |           within a display generated by the Derivative Works, if and
116 |           wherever such third-party notices normally appear. The contents
117 |           of the NOTICE file are for informational purposes only and
118 |           do not modify the License. You may add Your own attribution
119 |           notices within Derivative Works that You distribute, alongside
120 |           or as an addendum to the NOTICE text from the Work, provided
121 |           that such additional attribution notices cannot be construed
122 |           as modifying the License.
123 | 
124 |       You may add Your own copyright statement to Your modifications and
125 |       may provide additional or different license terms and conditions
126 |       for use, reproduction, or distribution of Your modifications, or
127 |       for any such Derivative Works as a whole, provided Your use,
128 |       reproduction, and distribution of the Work otherwise complies with
129 |       the conditions stated in this License.
130 | 
131 |    5. Submission of Contributions. Unless You explicitly state otherwise,
132 |       any Contribution intentionally submitted for inclusion in the Work
133 |       by You to the Licensor shall be under the terms and conditions of
134 |       this License, without any additional terms or conditions.
135 |       Notwithstanding the above, nothing herein shall supersede or modify
136 |       the terms of any separate license agreement you may have executed
137 |       with Licensor regarding such Contributions.
138 | 
139 |    6. Trademarks. This License does not grant permission to use the trade
140 |       names, trademarks, service marks, or product names of the Licensor,
141 |       except as required for reasonable and customary use in describing the
142 |       origin of the Work and reproducing the content of the NOTICE file.
143 | 
144 |    7. Disclaimer of Warranty. Unless required by applicable law or
145 |       agreed to in writing, Licensor provides the Work (and each
146 |       Contributor provides its Contributions) on an "AS IS" BASIS,
147 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 |       implied, including, without limitation, any warranties or conditions
149 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 |       PARTICULAR PURPOSE. You are solely responsible for determining the
151 |       appropriateness of using or redistributing the Work and assume any
152 |       risks associated with Your exercise of permissions under this License.
153 | 
154 |    8. Limitation of Liability. In no event and under no legal theory,
155 |       whether in tort (including negligence), contract, or otherwise,
156 |       unless required by applicable law (such as deliberate and grossly
157 |       negligent acts) or agreed to in writing, shall any Contributor be
158 |       liable to You for damages, including any direct, indirect, special,
159 |       incidental, or consequential damages of any character arising as a
160 |       result of this License or out of the use or inability to use the
161 |       Work (including but not limited to damages for loss of goodwill,
162 |       work stoppage, computer failure or malfunction, or any and all
163 |       other commercial damages or losses), even if such Contributor
164 |       has been advised of the possibility of such damages.
165 | 
166 |    9. Accepting Warranty or Additional Liability. While redistributing
167 |       the Work or Derivative Works thereof, You may choose to offer,
168 |       and charge a fee for, acceptance of support, warranty, indemnity,
169 |       or other liability obligations and/or rights consistent with this
170 |       License. However, in accepting such obligations, You may act only
171 |       on Your own behalf and on Your sole responsibility, not on behalf
172 |       of any other Contributor, and only if You agree to indemnify,
173 |       defend, and hold each Contributor harmless for any liability
174 |       incurred by, or claims asserted against, such Contributor by reason
175 |       of your accepting any such warranty or additional liability.
176 | 
177 |    END OF TERMS AND CONDITIONS
178 | 
179 |    APPENDIX: How to apply the Apache License to your work.
180 | 
181 |       To apply the Apache License to your work, attach the following
182 |       boilerplate notice, with the fields enclosed by brackets "[]"
183 |       replaced with your own identifying information. (Don't include
184 |       the brackets!)  The text should be enclosed in the appropriate
185 |       comment syntax for the file format. We also recommend that a
186 |       file or class name and description of purpose be included on the
187 |       same "printed page" as the copyright notice for easier
188 |       identification within third-party archives.
189 | 
190 |    Copyright [yyyy] [name of copyright owner]
191 | 
192 |    Licensed under the Apache License, Version 2.0 (the "License");
193 |    you may not use this file except in compliance with the License.
194 |    You may obtain a copy of the License at
195 | 
196 |        http://www.apache.org/licenses/LICENSE-2.0
197 | 
198 |    Unless required by applicable law or agreed to in writing, software
199 |    distributed under the License is distributed on an "AS IS" BASIS,
200 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 |    See the License for the specific language governing permissions and
202 |    limitations under the License.
203 | 


--------------------------------------------------------------------------------
/NOTICE.txt:
--------------------------------------------------------------------------------
 1 | 
 2 |                             The SwiftLog Project
 3 |                             ========================
 4 | 
 5 | Please visit the SwiftLog web site for more information:
 6 | 
 7 |   * https://github.com/apple/swift-log
 8 | 
 9 | Copyright 2018, 2019 The SwiftLog Project
10 | 
11 | The SwiftLog Project licenses this file to you under the Apache License,
12 | version 2.0 (the "License"); you may not use this file except in compliance
13 | with the License. You may obtain a copy of the License at:
14 | 
15 |   https://www.apache.org/licenses/LICENSE-2.0
16 | 
17 | Unless required by applicable law or agreed to in writing, software
18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
20 | License for the specific language governing permissions and limitations
21 | under the License.
22 | 
23 | Also, please refer to each LICENSE.<component>.txt file, which is located in
24 | the 'license' directory of the distribution file, for the license terms of the
25 | components that this product depends on.
26 | 
27 | -------------------------------------------------------------------------------
28 | 
29 | This product contains a derivation of the Tony Stone's 'process_test_files.rb'.
30 | 
31 |   * LICENSE (Apache License 2.0):
32 |     * https://www.apache.org/licenses/LICENSE-2.0
33 |   * HOMEPAGE:
34 |     * https://codegists.com/snippet/ruby/generate_xctest_linux_runnerrb_tonystone_ruby
35 | 
36 | ---
37 | 
38 | This product contains a derivation of the lock implementation and various
39 | scripts from SwiftNIO.
40 | 
41 |   * LICENSE (Apache License 2.0):
42 |     * https://www.apache.org/licenses/LICENSE-2.0
43 |   * HOMEPAGE:
44 |     * https://github.com/apple/swift-nio
45 | 


--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version:5.9
 2 | //===----------------------------------------------------------------------===//
 3 | //
 4 | // This source file is part of the Swift Logging API open source project
 5 | //
 6 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
 7 | // Licensed under Apache License v2.0
 8 | //
 9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 | 
16 | import PackageDescription
17 | 
18 | let package = Package(
19 |     name: "swift-log",
20 |     products: [
21 |         .library(name: "Logging", targets: ["Logging"])
22 |     ],
23 |     targets: [
24 |         .target(
25 |             name: "Logging",
26 |             dependencies: []
27 |         ),
28 |         .testTarget(
29 |             name: "LoggingTests",
30 |             dependencies: ["Logging"]
31 |         ),
32 |     ]
33 | )
34 | 
35 | for target in package.targets {
36 |     var settings = target.swiftSettings ?? []
37 |     settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
38 |     target.swiftSettings = settings
39 | }
40 | 
41 | // ---    STANDARD CROSS-REPO SETTINGS DO NOT EDIT   --- //
42 | for target in package.targets {
43 |     switch target.type {
44 |     case .regular, .test, .executable:
45 |         var settings = target.swiftSettings ?? []
46 |         // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
47 |         settings.append(.enableUpcomingFeature("MemberImportVisibility"))
48 |         target.swiftSettings = settings
49 |     case .macro, .plugin, .system, .binary:
50 |         ()  // not applicable
51 |     @unknown default:
52 |         ()  // we don't know what to do here, do nothing
53 |     }
54 | }
55 | // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
56 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # SwiftLog
 2 | 
 3 | This repository contains a logging API implementation for Swift.
 4 | SwiftLog provides a unified, performant, and ergonomic logging API that can be
 5 | adopted by libraries and applications across the Swift ecosystem.
 6 | 
 7 | - 📚 **Documentation** and **tutorials** are available on the [Swift Package Index][spi-swift-log]
 8 | - 🚀 **Contributions** are welcome, please see [CONTRIBUTING.md](CONTRIBUTING.md)
 9 | - 🪪 **License** is Apache 2.0, repeated in [LICENSE.txt](LICENSE.txt)
10 | - 🔒 **Security** issues should be reported via the process in [SECURITY.md](SECURITY.md)
11 | - 🔀 **Available Logging Backends**: SwiftLog is an API package - you'll want to
12 | choose from the many
13 | [community-maintained logging backends](#available-log-handler-backends) for production use
14 | 
15 | ## Quick Start
16 | 
17 | The following snippet shows how to add SwiftLog to your Swift Package:
18 | 
19 | ```swift
20 | // swift-tools-version: 6.1
21 | import PackageDescription
22 | 
23 | let package = Package(
24 |     name: "YourApp",
25 |     dependencies: [
26 |         .package(url: "https://github.com/apple/swift-log", from: "1.6.0")
27 |     ],
28 |     targets: [
29 |         .target(
30 |             name: "YourApp",
31 |             dependencies: [
32 |                 .product(name: "Logging", package: "swift-log")
33 |             ]
34 |         )
35 |     ]
36 | )
37 | ```
38 | 
39 | Then start logging:
40 | 
41 | ```swift
42 | import Logging
43 | 
44 | // Create a logger
45 | let logger = Logger(label: "com.example.YourApp")
46 | 
47 | // Log at different levels
48 | logger.info("Application started")
49 | logger.warning("This is a warning")
50 | logger.error("Something went wrong", metadata: ["error": "\(error)"])
51 | 
52 | // Add metadata for context
53 | var requestLogger = logger
54 | requestLogger[metadataKey: "request-id"] = "\(UUID())"
55 | requestLogger.info("Processing request")
56 | ```
57 | 
58 | ## Available log handler backends
59 | 
60 | The community has built numerous specialized logging backends.
61 | 
62 | A great way to discover available log backend implementations is searching the
63 | [Swift Package Index](https://swiftpackageindex.com/search?query=swift-log)
64 | for the `swift-log` keyword.
65 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security
 2 | 
 3 | This document specifies the security process for the SwiftLog project.
 4 | 
 5 | ## Disclosures
 6 | 
 7 | ### Private Disclosure Process
 8 | 
 9 | The SwiftLog maintainers ask that known and suspected vulnerabilities be
10 | privately and responsibly disclosed by emailing
11 | [sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org)
12 | with the all the required detail.
13 | **Do not file a public issue.**
14 | 
15 | #### When to report a vulnerability
16 | 
17 | * You think you have discovered a potential security vulnerability in SwiftLog.
18 | * You are unsure how a vulnerability affects SwiftLog.
19 | 
20 | #### What happens next?
21 | 
22 | * A member of the team will acknowledge receipt of the report within 3
23 |   working days (United States). This may include a request for additional
24 |   information about reproducing the vulnerability.
25 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the
26 |   vulnerability within 10 days of the report as per their [security
27 |   guidelines][sswg-security].
28 | * Once we have identified a fix we may ask you to validate it. We aim to do this
29 |   within 30 days. In some cases this may not be possible, for example when the
30 |   vulnerability exists at the protocol level and the industry must coordinate on
31 |   the disclosure process.
32 | * If a CVE number is required, one will be requested from [MITRE][mitre]
33 |   providing you with full credit for the discovery.
34 | * We will decide on a planned release date and let you know when it is.
35 | * Prior to release, we will inform major dependents that a security-related
36 |   patch is impending.
37 | * Once the fix has been released we will publish a security advisory on GitHub
38 |   and in the Server → Security Updates category on the [Swift forums][swift-forums-sec].
39 | 
40 | [sswg]: https://github.com/swift-server/sswg
41 | [sswg-security]: https://www.swift.org/sswg/security/
42 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/
43 | [mitre]: https://cveform.mitre.org/
44 | 


--------------------------------------------------------------------------------
/Sources/Logging/Docs.docc/BestPractices/001-ChoosingLogLevels.md:
--------------------------------------------------------------------------------
  1 | # 001: Choosing log levels
  2 | 
  3 | Best practice for selecting appropriate log levels in applications and
  4 | libraries.
  5 | 
  6 | ## Overview
  7 | 
  8 | SwiftLog defines seven log levels, and choosing the right level is crucial for
  9 | creating well-behaved libraries that don't overwhelm logging systems or misuse
 10 | severity levels. This practice provides clear guidance on when to use each
 11 | level.
 12 | 
 13 | ### Motivation 
 14 | 
 15 | Libraries must be well-behaved across various use cases and cannot assume
 16 | specific logging backend configurations. Using inappropriate log levels can
 17 | flood production logs, trigger false alerts, or make debugging more difficult.
 18 | Following consistent log level guidelines ensures your library integrates well
 19 | with diverse application environments.
 20 | 
 21 | ### Log levels
 22 | 
 23 | SwiftLog defines seven log levels via ``Logger/Level``, ordered from least to
 24 | most severe:
 25 | 
 26 | - ``Logger/Level/trace``
 27 | - ``Logger/Level/debug``
 28 | - ``Logger/Level/info``
 29 | - ``Logger/Level/notice``
 30 | - ``Logger/Level/warning``
 31 | - ``Logger/Level/error``
 32 | - ``Logger/Level/critical``
 33 | 
 34 | ### Level guidelines
 35 | 
 36 | How you use log levels depends in large part if you are developing a library, or
 37 | an application which bootstraps its logging system and is in full control over
 38 | its logging environment.
 39 | 
 40 | #### For libraries
 41 | 
 42 | Libraries should use **info level or lower** (info, debug, trace). Each level
 43 | serves different purposes:
 44 | 
 45 | Libraries **should not** log information on **warning or more severe levels**,
 46 | unless it is a one-time (for example during startup) warning, that cannot lead
 47 | to overwhelming log outputs.
 48 | 
 49 | ##### Trace Level
 50 | - **Usage**: Log everything needed to diagnose hard-to-reproduce bugs
 51 | - **Performance**: May impact performance; assume it won't be used in production
 52 | - **Content**: Internal state, detailed operation flows, diagnostic information
 53 | 
 54 | ##### Debug Level  
 55 | - **Usage**: May be enabled in some production deployments
 56 | - **Performance**: Should not significantly undermine production performance
 57 | - **Content**: High-level operation overview, connection events, major decisions
 58 | 
 59 | ##### Info Level
 60 | - **Usage**: Reserved for things that went wrong but can't be communicated
 61 | through other means like throwing from a method
 62 | - **Examples**: Connection retry attempts, fallback mechanisms, recoverable
 63 |   failures
 64 | - **Guideline**: Use sparingly - not for normal successful operations
 65 | 
 66 | #### For applications
 67 | 
 68 | Applications can use **any level** depending on the context and what they want
 69 | to achieve. Applications have full control over their logging strategy.
 70 | 
 71 | #### Configuring logger log levels
 72 | 
 73 | It depends on the use-case of your application which log level your logger
 74 | should use. For **console and other end-user-visible displays**: Consider using
 75 | **notice level** as the minimum visible level to avoid overwhelming users with
 76 | technical details.
 77 | 
 78 | ### Example
 79 | 
 80 | #### Recommended: Libraries should use info level or lower
 81 | 
 82 | ```swift
 83 | // ✅ Good: Trace level for detailed diagnostics
 84 | logger.trace("Connection pool state", metadata: [
 85 |     "active": "\(activeConnections)",
 86 |     "idle": "\(idleConnections)",
 87 |     "pending": "\(pendingRequests)"
 88 | ])
 89 | 
 90 | // ✅ Good: Debug level for high-value operational info
 91 | logger.debug("Database connection established", metadata: [
 92 |     "host": "\(host)",
 93 |     "database": "\(database)",
 94 |     "connectionTime": "\(duration)"
 95 | ])
 96 | 
 97 | // ✅ Good: Info level for issues that can't be communicated through other means
 98 | logger.info("Connection failed, retrying", metadata: [
 99 |     "attempt": "\(attemptNumber)",
100 |     "maxRetries": "\(maxRetries)",
101 |     "host": "\(host)"
102 | ])
103 | ```
104 | 
105 | #### Use sparingly: Warning and error levels
106 | 
107 | ```swift
108 | // ✅ Good: One-time startup warning or error
109 | logger.warning("Deprecated TLS version detected. Consider upgrading to TLS 1.3")
110 | ```
111 | 
112 | #### Avoid: Logging potentially intentional failures at info level
113 | 
114 | Some failures may be completely intentional from the high-level perspective of a
115 | developer or system using your library. For example: failure to resolve a
116 | domain, failure to make a request, or failure to complete some task;
117 | 
118 | Instead, log at debug or trace levels and offer alternative ways to observe
119 | these behaviors, for example using `swift-metrics` to emit counts.
120 | 
121 | ```swift
122 | // ❌ Bad: Normal operations at info level flood production logs
123 | logger.info("Request failed")
124 | ```
125 | 
126 | #### Avoid: Normal operations at info level
127 | 
128 | ```swift
129 | // ❌ Bad: Normal operations at info level flood production logs
130 | logger.info("HTTP request received")
131 | logger.info("Database query executed") 
132 | logger.info("Response sent")
133 | 
134 | // ✅ Good: Use appropriate levels instead
135 | logger.debug("Processing request", metadata: ["path": "\(path)"])
136 | logger.trace("Query", , metadata: ["path": "\(query)"])
137 | logger.debug("Request completed", metadata: ["status": "\(status)"])
138 | ```
139 | 


--------------------------------------------------------------------------------
/Sources/Logging/Docs.docc/ImplementingALogHandler.md:
--------------------------------------------------------------------------------
  1 | # Implementing a log handler
  2 | 
  3 | Create a custom logging backend that provides logging services for your apps
  4 | and libraries.
  5 | 
  6 | ## Overview
  7 | 
  8 | To become a compatible logging backend that any `SwiftLog` consumer can use,
  9 | you need to fulfill a few requirements, primarily conforming to the
 10 | ``LogHandler`` protocol.
 11 | 
 12 | ### Implement with value type semantics
 13 | 
 14 | Your log handler **must be a `struct`** and exhibit value semantics. This
 15 | ensures that changes to one logger don't affect others.
 16 | 
 17 | To verify that your handler reflects value semantics ensure that it passes this
 18 | test:
 19 | 
 20 | ```swift
 21 | @Test
 22 | func logHandlerValueSemantics() {
 23 |     LoggingSystem.bootstrap(MyLogHandler.init)
 24 |     var logger1 = Logger(label: "first logger")
 25 |     logger1.logLevel = .debug
 26 |     logger1[metadataKey: "only-on"] = "first"
 27 |     
 28 |     var logger2 = logger1
 29 |     logger2.logLevel = .error                  // Must not affect logger1
 30 |     logger2[metadataKey: "only-on"] = "second" // Must not affect logger1
 31 |     
 32 |     // These expectations must pass
 33 |     #expect(logger1.logLevel == .debug)
 34 |     #expect(logger2.logLevel == .error)
 35 |     #expect(logger1[metadataKey: "only-on"] == "first")
 36 |     #expect(logger2[metadataKey: "only-on"] == "second")
 37 | }
 38 | ```
 39 | 
 40 | > Note: In special cases, it is acceptable for a log handler to provide
 41 | > global log level overrides that may affect all log handlers created.
 42 | 
 43 | ### Example implementation
 44 | 
 45 | Here's a complete example of a simple print-based log handler:
 46 | 
 47 | ```swift
 48 | import Foundation
 49 | import Logging
 50 | 
 51 | public struct PrintLogHandler: LogHandler {
 52 |     private let label: String
 53 |     public var logLevel: Logger.Level = .info
 54 |     public var metadata: Logger.Metadata = [:]
 55 |     
 56 |     public init(label: String) {
 57 |         self.label = label
 58 |     }
 59 |     
 60 |     public func log(
 61 |         level: Logger.Level,
 62 |         message: Logger.Message,
 63 |         metadata: Logger.Metadata?,
 64 |         source: String,
 65 |         file: String,
 66 |         function: String,
 67 |         line: UInt
 68 |     ) {
 69 |         let timestamp = ISO8601DateFormatter().string(from: Date())
 70 |         let levelString = level.rawValue.uppercased()
 71 |         
 72 |         // Merge handler metadata with message metadata
 73 |         let combinedMetadata = Self.prepareMetadata(
 74 |             base: self.metadata
 75 |             explicit: metadata
 76 |         )
 77 |         
 78 |         // Format metadata
 79 |         let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
 80 |         
 81 |         // Create log line and print to console
 82 |         let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)"
 83 |         print(logLine)
 84 |     }
 85 |     
 86 |     public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
 87 |         get {
 88 |             return self.metadata[key]
 89 |         }
 90 |         set {
 91 |             self.metadata[key] = newValue
 92 |         }
 93 |     }
 94 | 
 95 |     static func prepareMetadata(
 96 |         base: Logger.Metadata,
 97 |         explicit: Logger.Metadata?
 98 |     ) -> Logger.Metadata? {
 99 |         var metadata = base
100 | 
101 |         guard let explicit else {
102 |             // all per-log-statement values are empty
103 |             return metadata
104 |         }
105 | 
106 |         metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
107 | 
108 |         return metadata
109 |     }
110 | }
111 | 
112 | ```
113 | 
114 | ### Advanced features
115 | 
116 | #### Metadata providers
117 | 
118 | Metadata providers allow you to dynamically add contextual information to all
119 | log messages without explicitly passing it each time. Common use cases include
120 | request IDs, user sessions, or trace contexts that should be included in logs
121 | throughout a request's lifecycle.
122 | 
123 | ```swift
124 | import Foundation
125 | import Logging
126 | 
127 | public struct PrintLogHandler: LogHandler {
128 |     private let label: String
129 |     public var logLevel: Logger.Level = .info
130 |     public var metadata: Logger.Metadata = [:]
131 |     public var metadataProvider: Logger.MetadataProvider?
132 |     
133 |     public init(label: String) {
134 |         self.label = label
135 |     }
136 |     
137 |     public func log(
138 |         level: Logger.Level,
139 |         message: Logger.Message,
140 |         metadata: Logger.Metadata?,
141 |         source: String,
142 |         file: String,
143 |         function: String,
144 |         line: UInt
145 |     ) {
146 |         let timestamp = ISO8601DateFormatter().string(from: Date())
147 |         let levelString = level.rawValue.uppercased()
148 |         
149 |         // Get provider metadata
150 |         let providerMetadata = metadataProvider?.get() ?? [:]
151 | 
152 |         // Merge handler metadata with message metadata
153 |         let combinedMetadata = Self.prepareMetadata(
154 |             base: self.metadata,
155 |             provider: self.metadataProvider,
156 |             explicit: metadata
157 |         )
158 |         
159 |         // Format metadata
160 |         let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
161 |         
162 |         // Create log line and print to console
163 |         let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)"
164 |         print(logLine)
165 |     }
166 |     
167 |     public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
168 |         get {
169 |             return self.metadata[key]
170 |         }
171 |         set {
172 |             self.metadata[key] = newValue
173 |         }
174 |     }
175 | 
176 |     static func prepareMetadata(
177 |         base: Logger.Metadata,
178 |         provider: Logger.MetadataProvider?,
179 |         explicit: Logger.Metadata?
180 |     ) -> Logger.Metadata? {
181 |         var metadata = base
182 | 
183 |         let provided = provider?.get() ?? [:]
184 | 
185 |         guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else {
186 |             // all per-log-statement values are empty
187 |             return metadata
188 |         }
189 | 
190 |         if !provided.isEmpty {
191 |             metadata.merge(provided, uniquingKeysWith: { _, provided in provided })
192 |         }
193 | 
194 |         if let explicit = explicit, !explicit.isEmpty {
195 |             metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
196 |         }
197 | 
198 |         return metadata
199 |     }
200 | }
201 | ```
202 | 
203 | ### Performance considerations
204 | 
205 | 1. **Avoid blocking**: Don't block the calling thread for I/O operations.
206 | 2. **Lazy evaluation**: Remember that messages and metadata are autoclosures.
207 | 3. **Memory efficiency**: Don't hold onto large amounts of messages.
208 | 
209 | ## See Also
210 | 
211 | - ``LogHandler``
212 | - ``StreamLogHandler``
213 | - ``MultiplexLogHandler``
214 | 


--------------------------------------------------------------------------------
/Sources/Logging/Docs.docc/LoggingBestPractices.md:
--------------------------------------------------------------------------------
 1 | # Logging best practices
 2 | 
 3 | Best practices for effective logging with SwiftLog.
 4 | 
 5 | ## Overview
 6 | 
 7 | This collection of best practices helps library authors and application
 8 | developers create effective, maintainable logging that works well across diverse
 9 | environments. Each practice is designed to ensure your logs are useful for
10 | debugging while being respectful of system resources and operational
11 | requirements.
12 | 
13 | ### Who Should Use These Practices
14 | 
15 | - **Library Authors**: Creating reusable components that log appropriately.
16 | - **Application Developers**: Implementing logging strategies in applications.
17 | 
18 | ### Philosophy
19 | 
20 | Good logging strikes a balance between providing useful information and avoiding
21 | system overhead. These practices are based on real-world experience with
22 | production systems and emphasize:
23 | 
24 | - **Predictable behavior** across different environments.
25 | - **Performance consciousness** to avoid impacting application speed.
26 | - **Operational awareness** to support production debugging and monitoring.
27 | - **Developer experience** to make debugging efficient and pleasant.
28 | 
29 | ### Contributing to these practices
30 | 
31 | These best practices evolve based on community experience and are maintained by
32 | the Swift Server Working Group ([SSWG](https://www.swift.org/sswg/)). Each
33 | practice includes:
34 | 
35 | - **Clear motivation** explaining why the practice matters
36 | - **Concrete examples** showing good and bad patterns
37 | - **Alternatives considered** documenting trade-offs and rejected approaches
38 | 
39 | ## Topics
40 | 
41 | - <doc:001-ChoosingLogLevels>
42 | 


--------------------------------------------------------------------------------
/Sources/Logging/Docs.docc/index.md:
--------------------------------------------------------------------------------
  1 | # ``Logging``
  2 | 
  3 | A unified, performant, and ergonomic logging API for Swift.
  4 | 
  5 | ## Overview
  6 | 
  7 | SwiftLog provides a logging API package designed to establish a common API the
  8 | ecosystem can use. It allows packages to emit log messages without tying them to
  9 | any specific logging implementation, while applications can choose any
 10 | compatible logging backend.
 11 | 
 12 | SwiftLog is an _API package_ which cuts the logging problem in half:
 13 | 1. A logging API (this package)
 14 | 2. Logging backend implementations (community-provided)
 15 | 
 16 | This separation allows libraries to adopt the API while applications choose any
 17 | compatible logging backend implementation without requiring changes from
 18 | libraries.
 19 | 
 20 | ## Getting Started
 21 | 
 22 | Use this If you are writing a cross-platform application (for example, Linux and
 23 | macOS) or library, target this logging API.
 24 | 
 25 | ### Adding the Dependency
 26 | 
 27 | Add the dependency to your `Package.swift`:
 28 | 
 29 | ```swift
 30 | .package(url: "https://github.com/apple/swift-log", from: "1.6.0")
 31 | ```
 32 | 
 33 | And to your target:
 34 | 
 35 | ```swift
 36 | .target(
 37 |     name: "YourTarget",
 38 |     dependencies: [
 39 |         .product(name: "Logging", package: "swift-log")
 40 |     ]
 41 | )
 42 | ```
 43 | 
 44 | ### Basic Usage
 45 | 
 46 | ```swift
 47 | // Import the logging API
 48 | import Logging
 49 | 
 50 | // Create a logger with a label
 51 | let logger = Logger(label: "MyLogger")
 52 | 
 53 | // Use it to log messages
 54 | logger.info("Hello World!")
 55 | ```
 56 | 
 57 | This outputs:
 58 | ```
 59 | 2019-03-13T15:46:38+0000 info: Hello World!
 60 | ```
 61 | 
 62 | ### Default Behavior
 63 | 
 64 | SwiftLog provides basic console logging via ``StreamLogHandler``. By default it
 65 | uses `stdout`, however, you can configure it to use `stderr` instead:
 66 | 
 67 | ```swift
 68 | LoggingSystem.bootstrap(StreamLogHandler.standardError)
 69 | ```
 70 | 
 71 | ``StreamLogHandler`` is primarily for convenience. For production applications,
 72 | implement the ``LogHandler`` protocol directly or use a community-maintained
 73 | backend.
 74 | 
 75 | ## Core Concepts
 76 | 
 77 | ### Loggers
 78 | 
 79 | Loggers are used to emit log messages at different severity levels:
 80 | 
 81 | ```swift
 82 | // Informational message
 83 | logger.info("Processing request")
 84 | 
 85 | // Something went wrong
 86 | logger.error("Houston, we have a problem")
 87 | ```
 88 | 
 89 | ``Logger`` is a value type with value semantics, meaning that when you modify a
 90 | logger's configuration (like its log level or metadata), it only affects that
 91 | specific logger instance:
 92 | 
 93 | ```swift
 94 | let baseLogger = Logger(label: "MyApp")
 95 | 
 96 | // Create a new logger with different configuration.
 97 | var requestLogger = baseLogger
 98 | requestLogger.logLevel = .debug
 99 | requestLogger[metadataKey: "request-id"] = "\(UUID())"
100 | 
101 | // baseLogger is unchanged. It still has default log level and no metadata
102 | // requestLogger has debug level and request-id metadata.
103 | ```
104 | 
105 | This value type behavior makes loggers safe to pass between functions and modify
106 | without unexpected side effects.
107 | 
108 | ### Log Levels
109 | 
110 | SwiftLog supports seven log levels (from least to most severe):
111 | - ``Logger/Level/trace``
112 | - ``Logger/Level/debug`` 
113 | - ``Logger/Level/info``
114 | - ``Logger/Level/notice``
115 | - ``Logger/Level/warning``
116 | - ``Logger/Level/error``
117 | - ``Logger/Level/critical``
118 | 
119 | Log levels can be changed per logger without affecting others:
120 | 
121 | ```swift
122 | var logger = Logger(label: "MyLogger")
123 | logger.logLevel = .debug
124 | ```
125 | 
126 | ### Logging Metadata
127 | 
128 | Metadata provides contextual information crucial for debugging:
129 | 
130 | ```swift
131 | var logger = Logger(label: "com.example.server")
132 | logger[metadataKey: "request-uuid"] = "\(UUID())"
133 | logger.info("Processing request")
134 | ```
135 | 
136 | Output:
137 | ```
138 | 2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED Processing request
139 | ```
140 | 
141 | ### Source vs Label
142 | 
143 | A ``Logger`` has an immutable `label` identifying its creator, while each log
144 | message carries a `source` parameter identifying where the message originated.
145 | Use `source` for filtering messages from specific subsystems.
146 | 
147 | 
148 | ## Topics
149 | 
150 | ### Logging API
151 | 
152 | - ``Logger``
153 | - ``LoggingSystem``
154 | 
155 | ### Log Handlers
156 | 
157 | - ``LogHandler``
158 | - ``MultiplexLogHandler``
159 | - ``StreamLogHandler``
160 | - ``SwiftLogNoOpLogHandler``
161 | 
162 | ### Best Practices
163 | 
164 | - <doc:LoggingBestPractices>
165 | - <doc:001-ChoosingLogLevels>
166 | 
167 | 


--------------------------------------------------------------------------------
/Sources/Logging/Locks.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | //===----------------------------------------------------------------------===//
 16 | //
 17 | // This source file is part of the SwiftNIO open source project
 18 | //
 19 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
 20 | // Licensed under Apache License v2.0
 21 | //
 22 | // See LICENSE.txt for license information
 23 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
 24 | //
 25 | // SPDX-License-Identifier: Apache-2.0
 26 | //
 27 | //===----------------------------------------------------------------------===//
 28 | 
 29 | #if canImport(WASILibc)
 30 | // No locking on WASILibc
 31 | #elseif canImport(Darwin)
 32 | import Darwin
 33 | #elseif os(Windows)
 34 | import WinSDK
 35 | #elseif canImport(Glibc)
 36 | import Glibc
 37 | #elseif canImport(Android)
 38 | import Android
 39 | #elseif canImport(Musl)
 40 | import Musl
 41 | #else
 42 | #error("Unsupported runtime")
 43 | #endif
 44 | 
 45 | /// A threading lock based on `libpthread` instead of `libdispatch`.
 46 | ///
 47 | /// This object provides a lock on top of a single `pthread_mutex_t`. This kind
 48 | /// of lock is safe to use with `libpthread`-based threading models, such as the
 49 | /// one used by NIO. On Windows, the lock is based on the substantially similar
 50 | /// `SRWLOCK` type.
 51 | internal final class Lock: @unchecked Sendable {
 52 |     #if canImport(WASILibc)
 53 |     // WASILibc is single threaded, provides no locks
 54 |     #elseif os(Windows)
 55 |     fileprivate let mutex: UnsafeMutablePointer<SRWLOCK> =
 56 |         UnsafeMutablePointer.allocate(capacity: 1)
 57 |     #else
 58 |     fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> =
 59 |         UnsafeMutablePointer.allocate(capacity: 1)
 60 |     #endif
 61 | 
 62 |     /// Create a new lock.
 63 |     public init() {
 64 |         #if canImport(WASILibc)
 65 |         // WASILibc is single threaded, provides no locks
 66 |         #elseif os(Windows)
 67 |         InitializeSRWLock(self.mutex)
 68 |         #else
 69 |         var attr = pthread_mutexattr_t()
 70 |         pthread_mutexattr_init(&attr)
 71 |         pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
 72 | 
 73 |         let err = pthread_mutex_init(self.mutex, &attr)
 74 |         precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
 75 |         #endif
 76 |     }
 77 | 
 78 |     deinit {
 79 |         #if canImport(WASILibc)
 80 |         // WASILibc is single threaded, provides no locks
 81 |         #elseif os(Windows)
 82 |         // SRWLOCK does not need to be free'd
 83 |         self.mutex.deallocate()
 84 |         #else
 85 |         let err = pthread_mutex_destroy(self.mutex)
 86 |         precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
 87 |         self.mutex.deallocate()
 88 |         #endif
 89 |     }
 90 | 
 91 |     /// Acquire the lock.
 92 |     ///
 93 |     /// Whenever possible, consider using `withLock` instead of this method and
 94 |     /// `unlock`, to simplify lock handling.
 95 |     public func lock() {
 96 |         #if canImport(WASILibc)
 97 |         // WASILibc is single threaded, provides no locks
 98 |         #elseif os(Windows)
 99 |         AcquireSRWLockExclusive(self.mutex)
100 |         #else
101 |         let err = pthread_mutex_lock(self.mutex)
102 |         precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
103 |         #endif
104 |     }
105 | 
106 |     /// Release the lock.
107 |     ///
108 |     /// Whenever possible, consider using `withLock` instead of this method and
109 |     /// `lock`, to simplify lock handling.
110 |     public func unlock() {
111 |         #if canImport(WASILibc)
112 |         // WASILibc is single threaded, provides no locks
113 |         #elseif os(Windows)
114 |         ReleaseSRWLockExclusive(self.mutex)
115 |         #else
116 |         let err = pthread_mutex_unlock(self.mutex)
117 |         precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
118 |         #endif
119 |     }
120 | }
121 | 
122 | extension Lock {
123 |     /// Acquire the lock for the duration of the given block.
124 |     ///
125 |     /// This convenience method should be preferred to `lock` and `unlock` in
126 |     /// most situations, as it ensures that the lock will be released regardless
127 |     /// of how `body` exits.
128 |     ///
129 |     /// - Parameter body: The block to execute while holding the lock.
130 |     /// - Returns: The value returned by the block.
131 |     @inlinable
132 |     internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
133 |         self.lock()
134 |         defer {
135 |             self.unlock()
136 |         }
137 |         return try body()
138 |     }
139 | 
140 |     // specialise Void return (for performance)
141 |     @inlinable
142 |     internal func withLockVoid(_ body: () throws -> Void) rethrows {
143 |         try self.withLock(body)
144 |     }
145 | }
146 | 
147 | /// A reader/writer threading lock based on `libpthread` instead of `libdispatch`.
148 | ///
149 | /// This object provides a lock on top of a single `pthread_rwlock_t`. This kind
150 | /// of lock is safe to use with `libpthread`-based threading models, such as the
151 | /// one used by NIO. On Windows, the lock is based on the substantially similar
152 | /// `SRWLOCK` type.
153 | internal final class ReadWriteLock: @unchecked Sendable {
154 |     #if canImport(WASILibc)
155 |     // WASILibc is single threaded, provides no locks
156 |     #elseif os(Windows)
157 |     fileprivate let rwlock: UnsafeMutablePointer<SRWLOCK> =
158 |         UnsafeMutablePointer.allocate(capacity: 1)
159 |     fileprivate var shared: Bool = true
160 |     #else
161 |     fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> =
162 |         UnsafeMutablePointer.allocate(capacity: 1)
163 |     #endif
164 | 
165 |     /// Create a new lock.
166 |     public init() {
167 |         #if canImport(WASILibc)
168 |         // WASILibc is single threaded, provides no locks
169 |         #elseif os(Windows)
170 |         InitializeSRWLock(self.rwlock)
171 |         #else
172 |         let err = pthread_rwlock_init(self.rwlock, nil)
173 |         precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
174 |         #endif
175 |     }
176 | 
177 |     deinit {
178 |         #if canImport(WASILibc)
179 |         // WASILibc is single threaded, provides no locks
180 |         #elseif os(Windows)
181 |         // SRWLOCK does not need to be free'd
182 |         self.rwlock.deallocate()
183 |         #else
184 |         let err = pthread_rwlock_destroy(self.rwlock)
185 |         precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
186 |         self.rwlock.deallocate()
187 |         #endif
188 |     }
189 | 
190 |     /// Acquire a reader lock.
191 |     ///
192 |     /// Whenever possible, consider using `withReaderLock` instead of this
193 |     /// method and `unlock`, to simplify lock handling.
194 |     fileprivate func lockRead() {
195 |         #if canImport(WASILibc)
196 |         // WASILibc is single threaded, provides no locks
197 |         #elseif os(Windows)
198 |         AcquireSRWLockShared(self.rwlock)
199 |         self.shared = true
200 |         #else
201 |         let err = pthread_rwlock_rdlock(self.rwlock)
202 |         precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
203 |         #endif
204 |     }
205 | 
206 |     /// Acquire a writer lock.
207 |     ///
208 |     /// Whenever possible, consider using `withWriterLock` instead of this
209 |     /// method and `unlock`, to simplify lock handling.
210 |     fileprivate func lockWrite() {
211 |         #if canImport(WASILibc)
212 |         // WASILibc is single threaded, provides no locks
213 |         #elseif os(Windows)
214 |         AcquireSRWLockExclusive(self.rwlock)
215 |         self.shared = false
216 |         #else
217 |         let err = pthread_rwlock_wrlock(self.rwlock)
218 |         precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
219 |         #endif
220 |     }
221 | 
222 |     /// Release the lock.
223 |     ///
224 |     /// Whenever possible, consider using `withReaderLock` and `withWriterLock`
225 |     /// instead of this method and `lockRead` and `lockWrite`, to simplify lock
226 |     /// handling.
227 |     fileprivate func unlock() {
228 |         #if canImport(WASILibc)
229 |         // WASILibc is single threaded, provides no locks
230 |         #elseif os(Windows)
231 |         if self.shared {
232 |             ReleaseSRWLockShared(self.rwlock)
233 |         } else {
234 |             ReleaseSRWLockExclusive(self.rwlock)
235 |         }
236 |         #else
237 |         let err = pthread_rwlock_unlock(self.rwlock)
238 |         precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
239 |         #endif
240 |     }
241 | }
242 | 
243 | extension ReadWriteLock {
244 |     /// Acquire the reader lock for the duration of the given block.
245 |     ///
246 |     /// This convenience method should be preferred to `lockRead` and `unlock`
247 |     /// in most situations, as it ensures that the lock will be released
248 |     /// regardless of how `body` exits.
249 |     ///
250 |     /// - Parameter body: The block to execute while holding the reader lock.
251 |     /// - Returns: The value returned by the block.
252 |     @inlinable
253 |     internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
254 |         self.lockRead()
255 |         defer {
256 |             self.unlock()
257 |         }
258 |         return try body()
259 |     }
260 | 
261 |     /// Acquire the writer lock for the duration of the given block.
262 |     ///
263 |     /// This convenience method should be preferred to `lockWrite` and `unlock`
264 |     /// in most situations, as it ensures that the lock will be released
265 |     /// regardless of how `body` exits.
266 |     ///
267 |     /// - Parameter body: The block to execute while holding the writer lock.
268 |     /// - Returns: The value returned by the block.
269 |     @inlinable
270 |     internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
271 |         self.lockWrite()
272 |         defer {
273 |             self.unlock()
274 |         }
275 |         return try body()
276 |     }
277 | 
278 |     // specialise Void return (for performance)
279 |     @inlinable
280 |     internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
281 |         try self.withReaderLock(body)
282 |     }
283 | 
284 |     // specialise Void return (for performance)
285 |     @inlinable
286 |     internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
287 |         try self.withWriterLock(body)
288 |     }
289 | }
290 | 


--------------------------------------------------------------------------------
/Sources/Logging/LogHandler.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | /// A `LogHandler` is an implementation of a logging backend.
 16 | ///
 17 | /// This type is an implementation detail and should not normally be used, unless implementing your own logging backend.
 18 | /// To use the SwiftLog API, please refer to the documentation of ``Logger``.
 19 | ///
 20 | /// # Implementation requirements
 21 | ///
 22 | /// To implement your own `LogHandler` you should respect a few requirements that are necessary so applications work
 23 | /// as expected regardless of the selected `LogHandler` implementation.
 24 | ///
 25 | /// - The ``LogHandler`` must be a `struct`.
 26 | /// - The metadata and `logLevel` properties must be implemented so that setting them on a `Logger` does not affect
 27 | ///   other `Logger`s.
 28 | ///
 29 | /// ### Treat log level & metadata as values
 30 | ///
 31 | /// When developing your `LogHandler`, please make sure the following test works.
 32 | ///
 33 | /// ```swift
 34 | /// @Test
 35 | /// func logHandlerValueSemantics() {
 36 | ///     LoggingSystem.bootstrap(MyLogHandler.init)
 37 | ///     var logger1 = Logger(label: "first logger")
 38 | ///     logger1.logLevel = .debug
 39 | ///     logger1[metadataKey: "only-on"] = "first"
 40 | ///
 41 | ///     var logger2 = logger1
 42 | ///     logger2.logLevel = .error                  // Must not affect logger1
 43 | ///     logger2[metadataKey: "only-on"] = "second" // Must not affect logger1
 44 | ///
 45 | ///     // These expectations must pass
 46 | ///     #expect(logger1.logLevel == .debug)
 47 | ///     #expect(logger2.logLevel == .error)
 48 | ///     #expect(logger1[metadataKey: "only-on"] == "first")
 49 | ///     #expect(logger2[metadataKey: "only-on"] == "second")
 50 | /// }
 51 | /// ```
 52 | ///
 53 | /// ### Special cases
 54 | ///
 55 | /// In certain special cases, the log level behaving like a value on `Logger` might not be what you want. For example,
 56 | /// you might want to set the log level across _all_ `Logger`s to `.debug` when say a signal (eg. `SIGUSR1`) is received
 57 | /// to be able to debug special failures in production. This special case is acceptable but we urge you to create a
 58 | /// solution specific to your `LogHandler` implementation to achieve that. Please find an example implementation of this
 59 | /// behavior below, on reception of the signal you would call
 60 | /// `LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel = .debug`, for example.
 61 | ///
 62 | /// ```swift
 63 | /// import class Foundation.NSLock
 64 | ///
 65 | /// public struct LogHandlerWithGlobalLogLevelOverride: LogHandler {
 66 | ///     // the static properties hold the globally overridden log level (if overridden)
 67 | ///     private static let overrideLock = NSLock()
 68 | ///     private static var overrideLogLevel: Logger.Level? = nil
 69 | ///
 70 | ///     // this holds the log level if not overridden
 71 | ///     private var _logLevel: Logger.Level = .info
 72 | ///
 73 | ///     // metadata storage
 74 | ///     public var metadata: Logger.Metadata = [:]
 75 | ///
 76 | ///     public init(label: String) {
 77 | ///         // [...]
 78 | ///     }
 79 | ///
 80 | ///     public var logLevel: Logger.Level {
 81 | ///         // when we get asked for the log level, we check if it was globally overridden or not
 82 | ///         get {
 83 | ///             LogHandlerWithGlobalLogLevelOverride.overrideLock.lock()
 84 | ///             defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() }
 85 | ///             return LogHandlerWithGlobalLogLevelOverride.overrideLogLevel ?? self._logLevel
 86 | ///         }
 87 | ///         // we set the log level whenever we're asked (note: this might not have an effect if globally
 88 | ///         // overridden)
 89 | ///         set {
 90 | ///             self._logLevel = newValue
 91 | ///         }
 92 | ///     }
 93 | ///
 94 | ///     public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?,
 95 | ///                     source: String, file: String, function: String, line: UInt) {
 96 | ///         // [...]
 97 | ///     }
 98 | ///
 99 | ///     public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
100 | ///         get {
101 | ///             return self.metadata[metadataKey]
102 | ///         }
103 | ///         set(newValue) {
104 | ///             self.metadata[metadataKey] = newValue
105 | ///         }
106 | ///     }
107 | ///
108 | ///     // this is the function to globally override the log level, it is not part of the `LogHandler` protocol
109 | ///     public static func overrideGlobalLogLevel(_ logLevel: Logger.Level) {
110 | ///         LogHandlerWithGlobalLogLevelOverride.overrideLock.lock()
111 | ///         defer { LogHandlerWithGlobalLogLevelOverride.overrideLock.unlock() }
112 | ///         LogHandlerWithGlobalLogLevelOverride.overrideLogLevel = logLevel
113 | ///     }
114 | /// }
115 | /// ```
116 | ///
117 | /// Please note that the above `LogHandler` will still pass the 'log level is a value' test above it iff the global log
118 | /// level has not been overridden. And most importantly it passes the requirement listed above: A change to the log
119 | /// level on one `Logger` should not affect the log level of another `Logger` variable.
120 | public protocol LogHandler: _SwiftLogSendableLogHandler {
121 |     /// The metadata provider this `LogHandler` will use when a log statement is about to be emitted.
122 |     ///
123 |     /// A ``Logger/MetadataProvider`` may add a constant set of metadata,
124 |     /// or use task-local values to pick up contextual metadata and add it to emitted logs.
125 |     var metadataProvider: Logger.MetadataProvider? { get set }
126 | 
127 |     /// This method is called when a `LogHandler` must emit a log message. There is no need for the `LogHandler` to
128 |     /// check if the `level` is above or below the configured `logLevel` as `Logger` already performed this check and
129 |     /// determined that a message should be logged.
130 |     ///
131 |     /// - parameters:
132 |     ///     - level: The log level the message was logged at.
133 |     ///     - message: The message to log. To obtain a `String` representation call `message.description`.
134 |     ///     - metadata: The metadata associated to this log message.
135 |     ///     - source: The source where the log message originated, for example the logging module.
136 |     ///     - file: The file the log message was emitted from.
137 |     ///     - function: The function the log line was emitted from.
138 |     ///     - line: The line the log message was emitted from.
139 |     func log(
140 |         level: Logger.Level,
141 |         message: Logger.Message,
142 |         metadata: Logger.Metadata?,
143 |         source: String,
144 |         file: String,
145 |         function: String,
146 |         line: UInt
147 |     )
148 | 
149 |     /// SwiftLog 1.0 compatibility method. Please do _not_ implement, implement
150 |     /// `log(level:message:metadata:source:file:function:line:)` instead.
151 |     @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)")
152 |     func log(
153 |         level: Logging.Logger.Level,
154 |         message: Logging.Logger.Message,
155 |         metadata: Logging.Logger.Metadata?,
156 |         file: String,
157 |         function: String,
158 |         line: UInt
159 |     )
160 | 
161 |     /// Add, remove, or change the logging metadata.
162 |     ///
163 |     /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must
164 |     ///         only affect this very `LogHandler`.
165 |     ///
166 |     /// - parameters:
167 |     ///    - metadataKey: The key for the metadata item
168 |     subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set }
169 | 
170 |     /// Get or set the entire metadata storage as a dictionary.
171 |     ///
172 |     /// - note: `LogHandler`s must treat logging metadata as a value type. This means that the change in metadata must
173 |     ///         only affect this very `LogHandler`.
174 |     var metadata: Logger.Metadata { get set }
175 | 
176 |     /// Get or set the configured log level.
177 |     ///
178 |     /// - note: `LogHandler`s must treat the log level as a value type. This means that the change in metadata must
179 |     ///         only affect this very `LogHandler`. It is acceptable to provide some form of global log level override
180 |     ///         that means a change in log level on a particular `LogHandler` might not be reflected in any
181 |     ///        `LogHandler`.
182 |     var logLevel: Logger.Level { get set }
183 | }
184 | 
185 | extension LogHandler {
186 |     /// Default implementation for `metadataProvider` which defaults to `nil`.
187 |     /// This default exists in order to facilitate source-compatible introduction of the `metadataProvider` protocol requirement.
188 |     public var metadataProvider: Logger.MetadataProvider? {
189 |         get {
190 |             nil
191 |         }
192 |         set {
193 |             #if DEBUG
194 |             if LoggingSystem.warnOnceLogHandlerNotSupportedMetadataProvider(Self.self) {
195 |                 self.log(
196 |                     level: .warning,
197 |                     message:
198 |                         "Attempted to set metadataProvider on \(Self.self) that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.",
199 |                     metadata: nil,
200 |                     source: "Logging",
201 |                     file: #file,
202 |                     function: #function,
203 |                     line: #line
204 |                 )
205 |             }
206 |             #endif
207 |         }
208 |     }
209 | }
210 | 
211 | extension LogHandler {
212 |     @available(*, deprecated, message: "You should implement this method instead of using the default implementation")
213 |     public func log(
214 |         level: Logger.Level,
215 |         message: Logger.Message,
216 |         metadata: Logger.Metadata?,
217 |         source: String,
218 |         file: String,
219 |         function: String,
220 |         line: UInt
221 |     ) {
222 |         self.log(level: level, message: message, metadata: metadata, file: file, function: function, line: line)
223 |     }
224 | 
225 |     @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)")
226 |     public func log(
227 |         level: Logging.Logger.Level,
228 |         message: Logging.Logger.Message,
229 |         metadata: Logging.Logger.Metadata?,
230 |         file: String,
231 |         function: String,
232 |         line: UInt
233 |     ) {
234 |         self.log(
235 |             level: level,
236 |             message: message,
237 |             metadata: metadata,
238 |             source: Logger.currentModule(filePath: file),
239 |             file: file,
240 |             function: function,
241 |             line: line
242 |         )
243 |     }
244 | }
245 | 
246 | // MARK: - Sendable support helpers
247 | 
248 | @preconcurrency public protocol _SwiftLogSendableLogHandler: Sendable {}
249 | 


--------------------------------------------------------------------------------
/Sources/Logging/MetadataProvider.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2022 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | #if canImport(Darwin)
 16 | import Darwin
 17 | #elseif os(Windows)
 18 | import CRT
 19 | #elseif canImport(Glibc)
 20 | import Glibc
 21 | #elseif canImport(Android)
 22 | import Android
 23 | #elseif canImport(Musl)
 24 | import Musl
 25 | #elseif canImport(WASILibc)
 26 | import WASILibc
 27 | #else
 28 | #error("Unsupported runtime")
 29 | #endif
 30 | 
 31 | @preconcurrency protocol _SwiftLogSendable: Sendable {}
 32 | 
 33 | extension Logger {
 34 |     /// A `MetadataProvider` is used to automatically inject runtime-generated metadata
 35 |     /// to all logs emitted by a logger.
 36 |     ///
 37 |     /// ### Example
 38 |     /// A metadata provider may be used to automatically inject metadata such as
 39 |     /// trace IDs:
 40 |     ///
 41 |     /// ```swift
 42 |     /// import Tracing // https://github.com/apple/swift-distributed-tracing
 43 |     ///
 44 |     /// let metadataProvider = MetadataProvider {
 45 |     ///     guard let traceID = Baggage.current?.traceID else { return nil }
 46 |     ///     return ["traceID": "\(traceID)"]
 47 |     /// }
 48 |     /// let logger = Logger(label: "example", metadataProvider: metadataProvider)
 49 |     /// var baggage = Baggage.topLevel
 50 |     /// baggage.traceID = 42
 51 |     /// Baggage.withValue(baggage) {
 52 |     ///     logger.info("hello") // automatically includes ["traceID": "42"] metadata
 53 |     /// }
 54 |     /// ```
 55 |     ///
 56 |     /// We recommend referring to [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing)
 57 |     /// for metadata providers which make use of its tracing and metadata propagation infrastructure. It is however
 58 |     /// possible to make use of metadata providers independently of tracing and instruments provided by that library,
 59 |     /// if necessary.
 60 |     public struct MetadataProvider: _SwiftLogSendable {
 61 |         /// Provide ``Logger.Metadata`` from current context.
 62 |         @usableFromInline
 63 |         internal let _provideMetadata: @Sendable () -> Metadata
 64 | 
 65 |         /// Create a new `MetadataProvider`.
 66 |         ///
 67 |         /// - Parameter provideMetadata: A closure extracting metadata from the current execution context.
 68 |         public init(_ provideMetadata: @escaping @Sendable () -> Metadata) {
 69 |             self._provideMetadata = provideMetadata
 70 |         }
 71 | 
 72 |         /// Invoke the metadata provider and return the generated contextual ``Logger/Metadata``.
 73 |         public func get() -> Metadata {
 74 |             self._provideMetadata()
 75 |         }
 76 |     }
 77 | }
 78 | 
 79 | extension Logger.MetadataProvider {
 80 |     /// A pseudo-`MetadataProvider` that can be used to merge metadata from multiple other `MetadataProvider`s.
 81 |     ///
 82 |     /// ### Merging conflicting keys
 83 |     ///
 84 |     /// `MetadataProvider`s are invoked left to right in the order specified in the `providers` argument.
 85 |     /// In case multiple providers try to add a value for the same key, the last provider "wins" and its value is being used.
 86 |     ///
 87 |     /// - Parameter providers: An array of `MetadataProvider`s to delegate to. The array must not be empty.
 88 |     /// - Returns: A pseudo-`MetadataProvider` merging metadata from the given `MetadataProvider`s.
 89 |     public static func multiplex(_ providers: [Logger.MetadataProvider]) -> Logger.MetadataProvider? {
 90 |         assert(!providers.isEmpty, "providers MUST NOT be empty")
 91 |         return Logger.MetadataProvider {
 92 |             providers.reduce(into: [:]) { metadata, provider in
 93 |                 let providedMetadata = provider.get()
 94 |                 guard !providedMetadata.isEmpty else {
 95 |                     return
 96 |                 }
 97 |                 metadata.merge(providedMetadata, uniquingKeysWith: { _, rhs in rhs })
 98 |             }
 99 |         }
100 |     }
101 | }
102 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/CompatibilityTest.swift:
--------------------------------------------------------------------------------
 1 | //===----------------------------------------------------------------------===//
 2 | //
 3 | // This source file is part of the Swift Logging API open source project
 4 | //
 5 | // Copyright (c) 2020 Apple Inc. and the Swift Logging API project authors
 6 | // Licensed under Apache License v2.0
 7 | //
 8 | // See LICENSE.txt for license information
 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 | 
15 | // Not testable
16 | import Logging
17 | import XCTest
18 | 
19 | final class CompatibilityTest: XCTestCase {
20 |     @available(*, deprecated, message: "Testing deprecated functionality")
21 |     func testAllLogLevelsWorkWithOldSchoolLogHandlerWorks() {
22 |         let testLogging = OldSchoolTestLogging()
23 | 
24 |         var logger = Logger(label: "\(#function)", factory: { testLogging.make(label: $0) })
25 |         logger.logLevel = .trace
26 | 
27 |         logger.trace("yes: trace")
28 |         logger.debug("yes: debug")
29 |         logger.info("yes: info")
30 |         logger.notice("yes: notice")
31 |         logger.warning("yes: warning")
32 |         logger.error("yes: error")
33 |         logger.critical("yes: critical", source: "any also with some new argument that isn't propagated")
34 | 
35 |         // Please note that the source is _not_ propagated (because the backend doesn't support it).
36 |         testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "no source")
37 |         testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "no source")
38 |         testLogging.history.assertExist(level: .info, message: "yes: info", source: "no source")
39 |         testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "no source")
40 |         testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "no source")
41 |         testLogging.history.assertExist(level: .error, message: "yes: error", source: "no source")
42 |         testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "no source")
43 |     }
44 | }
45 | 
46 | private struct OldSchoolTestLogging {
47 |     private let _config = Config()  // shared among loggers
48 |     private let recorder = Recorder()  // shared among loggers
49 | 
50 |     @available(*, deprecated, message: "Testing deprecated functionality")
51 |     func make(label: String) -> any LogHandler {
52 |         OldSchoolLogHandler(
53 |             label: label,
54 |             config: self.config,
55 |             recorder: self.recorder,
56 |             metadata: [:],
57 |             logLevel: .info
58 |         )
59 |     }
60 | 
61 |     var config: Config { self._config }
62 |     var history: some History { self.recorder }
63 | }
64 | 
65 | @available(*, deprecated, message: "Testing deprecated functionality")
66 | private struct OldSchoolLogHandler: LogHandler {
67 |     var label: String
68 |     let config: Config
69 |     let recorder: Recorder
70 | 
71 |     func make(label: String) -> some LogHandler {
72 |         TestLogHandler(label: label, config: self.config, recorder: self.recorder)
73 |     }
74 | 
75 |     func log(
76 |         level: Logger.Level,
77 |         message: Logger.Message,
78 |         metadata: Logger.Metadata?,
79 |         file: String,
80 |         function: String,
81 |         line: UInt
82 |     ) {
83 |         self.recorder.record(level: level, metadata: metadata, message: message, source: "no source")
84 |     }
85 | 
86 |     subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
87 |         get {
88 |             self.metadata[metadataKey]
89 |         }
90 |         set {
91 |             self.metadata[metadataKey] = newValue
92 |         }
93 |     }
94 | 
95 |     var metadata: Logger.Metadata
96 | 
97 |     var logLevel: Logger.Level
98 | }
99 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/GlobalLoggingTest.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | import XCTest
 16 | 
 17 | @testable import Logging
 18 | 
 19 | #if compiler(>=6.0) || canImport(Darwin)
 20 | import Dispatch
 21 | #else
 22 | @preconcurrency import Dispatch
 23 | #endif
 24 | 
 25 | class GlobalLoggerTest: XCTestCase {
 26 |     func test1() throws {
 27 |         // bootstrap with our test logging impl
 28 |         let logging = TestLogging()
 29 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
 30 | 
 31 |         // change test logging config to log traces and above
 32 |         logging.config.set(value: Logger.Level.debug)
 33 |         // run our program
 34 |         Struct1().doSomething()
 35 |         // test results
 36 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomething")
 37 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse")
 38 |         logging.history.assertExist(level: .info, message: "Struct2::doSomething")
 39 |         logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse")
 40 |         logging.history.assertExist(level: .error, message: "Struct3::doSomething")
 41 |         logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"])
 42 |         logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["foo": "bar"])
 43 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"])
 44 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"])
 45 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::Local", metadata: ["baz": "qux"])
 46 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end")
 47 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end")
 48 |         logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end")
 49 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse::end")
 50 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomething::end")
 51 |     }
 52 | 
 53 |     func test2() throws {
 54 |         // bootstrap with our test logging impl
 55 |         let logging = TestLogging()
 56 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
 57 | 
 58 |         // change test logging config to log errors and above
 59 |         logging.config.set(value: Logger.Level.error)
 60 |         // run our program
 61 |         Struct1().doSomething()
 62 |         // test results
 63 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething")
 64 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse")
 65 |         logging.history.assertNotExist(level: .info, message: "Struct2::doSomething")
 66 |         logging.history.assertNotExist(level: .info, message: "Struct2::doSomethingElse")
 67 |         logging.history.assertExist(level: .error, message: "Struct3::doSomething")
 68 |         logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"])
 69 |         logging.history.assertNotExist(
 70 |             level: .warning,
 71 |             message: "Struct3::doSomethingElseAsync",
 72 |             metadata: ["foo": "bar"]
 73 |         )
 74 |         logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"])
 75 |         logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"])
 76 |         logging.history.assertNotExist(
 77 |             level: .debug,
 78 |             message: "Struct3::doSomethingElse::Local",
 79 |             metadata: ["baz": "qux"]
 80 |         )
 81 |         logging.history.assertNotExist(level: .debug, message: "Struct3::doSomethingElse::end")
 82 |         logging.history.assertNotExist(level: .debug, message: "Struct3::doSomething::end")
 83 |         logging.history.assertNotExist(level: .debug, message: "Struct2::doSomethingElse::end")
 84 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end")
 85 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end")
 86 |     }
 87 | 
 88 |     func test3() throws {
 89 |         // bootstrap with our test logging impl
 90 |         let logging = TestLogging()
 91 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
 92 | 
 93 |         // change test logging config
 94 |         logging.config.set(value: .warning)
 95 |         logging.config.set(key: "GlobalLoggerTest::Struct2", value: .info)
 96 |         logging.config.set(key: "TestLibrary", value: .debug)
 97 |         // run our program
 98 |         Struct1().doSomething()
 99 |         // test results
100 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething")
101 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse")
102 |         logging.history.assertExist(level: .info, message: "Struct2::doSomething")
103 |         logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse")
104 |         logging.history.assertExist(level: .error, message: "Struct3::doSomething")
105 |         logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["foo": "bar"])
106 |         logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["foo": "bar"])
107 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomething", metadata: ["foo": "bar"])
108 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync", metadata: ["foo": "bar"])
109 |         logging.history.assertNotExist(
110 |             level: .debug,
111 |             message: "Struct3::doSomethingElse::Local",
112 |             metadata: ["baz": "qux"]
113 |         )
114 |         logging.history.assertNotExist(level: .debug, message: "Struct3::doSomethingElse::end")
115 |         logging.history.assertNotExist(level: .debug, message: "Struct3::doSomething::end")
116 |         logging.history.assertNotExist(level: .debug, message: "Struct2::doSomethingElse::end")
117 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end")
118 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end")
119 |     }
120 | }
121 | 
122 | private struct Struct1 {
123 |     private let logger = Logger(label: "GlobalLoggerTest::Struct1")
124 | 
125 |     func doSomething() {
126 |         self.logger.debug("Struct1::doSomething")
127 |         self.doSomethingElse()
128 |         self.logger.debug("Struct1::doSomething::end")
129 |     }
130 | 
131 |     private func doSomethingElse() {
132 |         self.logger.debug("Struct1::doSomethingElse")
133 |         Struct2().doSomething()
134 |         self.logger.debug("Struct1::doSomethingElse::end")
135 |     }
136 | }
137 | 
138 | private struct Struct2 {
139 |     let logger = Logger(label: "GlobalLoggerTest::Struct2")
140 | 
141 |     func doSomething() {
142 |         self.logger.info("Struct2::doSomething")
143 |         self.doSomethingElse()
144 |         self.logger.debug("Struct2::doSomething::end")
145 |     }
146 | 
147 |     private func doSomethingElse() {
148 |         self.logger.info("Struct2::doSomethingElse")
149 |         Struct3().doSomething()
150 |         self.logger.debug("Struct2::doSomethingElse::end")
151 |     }
152 | }
153 | 
154 | private struct Struct3 {
155 |     private let logger = Logger(label: "GlobalLoggerTest::Struct3")
156 |     private let queue = DispatchQueue(label: "GlobalLoggerTest::Struct3")
157 | 
158 |     func doSomething() {
159 |         self.logger.error("Struct3::doSomething")
160 |         self.doSomethingElse()
161 |         self.logger.debug("Struct3::doSomething::end")
162 |     }
163 | 
164 |     private func doSomethingElse() {
165 |         MDC.global["foo"] = "bar"
166 |         self.logger.error("Struct3::doSomethingElse")
167 |         let group = DispatchGroup()
168 |         group.enter()
169 |         let loggingMetadata = MDC.global.metadata
170 |         self.queue.async {
171 |             MDC.global.with(metadata: loggingMetadata) {
172 |                 self.logger.warning("Struct3::doSomethingElseAsync")
173 |                 let library = TestLibrary()
174 |                 library.doSomething()
175 |                 library.doSomethingAsync {
176 |                     group.leave()
177 |                 }
178 |             }
179 |         }
180 |         group.wait()
181 |         MDC.global["foo"] = nil
182 |         // only effects the logger instance
183 |         var l = self.logger
184 |         l[metadataKey: "baz"] = "qux"
185 |         l.debug("Struct3::doSomethingElse::Local")
186 |         self.logger.debug("Struct3::doSomethingElse::end")
187 |     }
188 | }
189 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/LocalLoggingTest.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | import XCTest
 16 | 
 17 | @testable import Logging
 18 | 
 19 | #if compiler(>=6.0) || canImport(Darwin)
 20 | import Dispatch
 21 | #else
 22 | @preconcurrency import Dispatch
 23 | #endif
 24 | 
 25 | class LocalLoggerTest: XCTestCase {
 26 |     func test1() throws {
 27 |         // bootstrap with our test logging impl
 28 |         let logging = TestLogging()
 29 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
 30 | 
 31 |         // change test logging config to log traces and above
 32 |         logging.config.set(value: Logger.Level.debug)
 33 |         // run our program
 34 |         let context = Context()
 35 |         Struct1().doSomething(context: context)
 36 |         // test results
 37 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomething", source: "LoggingTests")
 38 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse")
 39 |         logging.history.assertExist(level: .info, message: "Struct2::doSomething")
 40 |         logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse")
 41 |         logging.history.assertExist(level: .error, message: "Struct3::doSomething", metadata: ["bar": "baz"])
 42 |         logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["bar": "baz"])
 43 |         logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["bar": "baz"])
 44 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomething")
 45 |         logging.history.assertExist(level: .info, message: "TestLibrary::doSomethingAsync")
 46 |         logging.history.assertExist(
 47 |             level: .debug,
 48 |             message: "Struct3::doSomethingElse::Local",
 49 |             metadata: ["bar": "baz", "baz": "qux"]
 50 |         )
 51 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end", metadata: ["bar": "baz"])
 52 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end", metadata: ["bar": "baz"])
 53 |         logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end")
 54 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomethingElse::end")
 55 |         logging.history.assertExist(level: .debug, message: "Struct1::doSomething::end")
 56 |     }
 57 | 
 58 |     func test2() throws {
 59 |         // bootstrap with our test logging impl
 60 |         let logging = TestLogging()
 61 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
 62 | 
 63 |         // change test logging config to log errors and above
 64 |         logging.config.set(value: Logger.Level.error)
 65 |         // run our program
 66 |         let context = Context()
 67 |         Struct1().doSomething(context: context)
 68 |         // test results
 69 |         // global context
 70 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething")
 71 |         // global context
 72 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse")
 73 |         // local context
 74 |         logging.history.assertExist(level: .info, message: "Struct2::doSomething")
 75 |         // local context
 76 |         logging.history.assertExist(level: .info, message: "Struct2::doSomethingElse")
 77 |         // local context
 78 |         logging.history.assertExist(level: .error, message: "Struct3::doSomething", metadata: ["bar": "baz"])
 79 |         // local context
 80 |         logging.history.assertExist(level: .error, message: "Struct3::doSomethingElse", metadata: ["bar": "baz"])
 81 |         // local context
 82 |         logging.history.assertExist(level: .warning, message: "Struct3::doSomethingElseAsync", metadata: ["bar": "baz"])
 83 |         // global context
 84 |         logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomething")
 85 |         // global context
 86 |         logging.history.assertNotExist(level: .info, message: "TestLibrary::doSomethingAsync")
 87 |         // hyper local context
 88 |         logging.history.assertExist(
 89 |             level: .debug,
 90 |             message: "Struct3::doSomethingElse::Local",
 91 |             metadata: ["bar": "baz", "baz": "qux"]
 92 |         )
 93 |         // local context
 94 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomethingElse::end", metadata: ["bar": "baz"])
 95 |         // local context
 96 |         logging.history.assertExist(level: .debug, message: "Struct2::doSomethingElse::end")
 97 |         // local context
 98 |         logging.history.assertExist(level: .debug, message: "Struct3::doSomething::end", metadata: ["bar": "baz"])
 99 |         // global context
100 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomethingElse::end")
101 |         // global context
102 |         logging.history.assertNotExist(level: .debug, message: "Struct1::doSomething::end")
103 |     }
104 | }
105 | 
106 | //  systems that follow the context pattern  need to implement something like this
107 | private struct Context {
108 |     var logger = Logger(label: "LocalLoggerTest::ContextLogger")
109 | 
110 |     // since logger is a value type, we can reuse our copy to manage logLevel
111 |     var logLevel: Logger.Level {
112 |         get { self.logger.logLevel }
113 |         set { self.logger.logLevel = newValue }
114 |     }
115 | 
116 |     // since logger is a value type, we can reuse our copy to manage metadata
117 |     subscript(metadataKey: String) -> Logger.Metadata.Value? {
118 |         get { self.logger[metadataKey: metadataKey] }
119 |         set { self.logger[metadataKey: metadataKey] = newValue }
120 |     }
121 | }
122 | 
123 | private struct Struct1 {
124 |     func doSomething(context: Context) {
125 |         context.logger.debug("Struct1::doSomething")
126 |         self.doSomethingElse(context: context)
127 |         context.logger.debug("Struct1::doSomething::end")
128 |     }
129 | 
130 |     private func doSomethingElse(context: Context) {
131 |         let originalContext = context
132 |         var context = context
133 |         context.logger.logLevel = .warning
134 |         context.logger.debug("Struct1::doSomethingElse")
135 |         Struct2().doSomething(context: context)
136 |         originalContext.logger.debug("Struct1::doSomethingElse::end")
137 |     }
138 | }
139 | 
140 | private struct Struct2 {
141 |     func doSomething(context: Context) {
142 |         var c = context
143 |         c.logLevel = .info  // only effects from this point on
144 |         c.logger.info("Struct2::doSomething")
145 |         self.doSomethingElse(context: c)
146 |         c.logger.debug("Struct2::doSomething::end")
147 |     }
148 | 
149 |     private func doSomethingElse(context: Context) {
150 |         var c = context
151 |         c.logLevel = .debug  // only effects from this point on
152 |         c.logger.info("Struct2::doSomethingElse")
153 |         Struct3().doSomething(context: c)
154 |         c.logger.debug("Struct2::doSomethingElse::end")
155 |     }
156 | }
157 | 
158 | private struct Struct3 {
159 |     private let queue = DispatchQueue(label: "LocalLoggerTest::Struct3")
160 | 
161 |     func doSomething(context: Context) {
162 |         var c = context
163 |         c["bar"] = "baz"  // only effects from this point on
164 |         c.logger.error("Struct3::doSomething")
165 |         self.doSomethingElse(context: c)
166 |         c.logger.debug("Struct3::doSomething::end")
167 |     }
168 | 
169 |     private func doSomethingElse(context: Context) {
170 |         context.logger.error("Struct3::doSomethingElse")
171 |         let group = DispatchGroup()
172 |         group.enter()
173 |         self.queue.async {
174 |             context.logger.warning("Struct3::doSomethingElseAsync")
175 |             let library = TestLibrary()
176 |             library.doSomething()
177 |             library.doSomethingAsync {
178 |                 group.leave()
179 |             }
180 |         }
181 |         group.wait()
182 |         // only effects the logger instance
183 |         var l = context.logger
184 |         l[metadataKey: "baz"] = "qux"
185 |         l.debug("Struct3::doSomethingElse::Local")
186 |         context.logger.debug("Struct3::doSomethingElse::end")
187 |     }
188 | }
189 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/LoggingTest.swift:
--------------------------------------------------------------------------------
   1 | //===----------------------------------------------------------------------===//
   2 | //
   3 | // This source file is part of the Swift Logging API open source project
   4 | //
   5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
   6 | // Licensed under Apache License v2.0
   7 | //
   8 | // See LICENSE.txt for license information
   9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
  10 | //
  11 | // SPDX-License-Identifier: Apache-2.0
  12 | //
  13 | //===----------------------------------------------------------------------===//
  14 | 
  15 | import XCTest
  16 | 
  17 | @testable import Logging
  18 | 
  19 | #if canImport(Darwin)
  20 | import Darwin
  21 | #elseif os(Windows)
  22 | import WinSDK
  23 | #elseif canImport(Android)
  24 | import Android
  25 | #else
  26 | import Glibc
  27 | #endif
  28 | 
  29 | extension LogHandler {
  30 |     fileprivate func with(logLevel: Logger.Level) -> any LogHandler {
  31 |         var result = self
  32 |         result.logLevel = logLevel
  33 |         return result
  34 |     }
  35 | 
  36 |     fileprivate func withMetadata(_ key: String, _ value: Logger.MetadataValue) -> any LogHandler {
  37 |         var result = self
  38 |         result.metadata[key] = value
  39 |         return result
  40 |     }
  41 | }
  42 | 
  43 | class LoggingTest: XCTestCase {
  44 |     func testAutoclosure() throws {
  45 |         // bootstrap with our test logging impl
  46 |         let logging = TestLogging()
  47 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
  48 | 
  49 |         var logger = Logger(label: "test")
  50 |         logger.logLevel = .info
  51 |         logger.log(
  52 |             level: .debug,
  53 |             {
  54 |                 XCTFail("debug should not be called")
  55 |                 return "debug"
  56 |             }()
  57 |         )
  58 |         logger.trace(
  59 |             {
  60 |                 XCTFail("trace should not be called")
  61 |                 return "trace"
  62 |             }()
  63 |         )
  64 |         logger.debug(
  65 |             {
  66 |                 XCTFail("debug should not be called")
  67 |                 return "debug"
  68 |             }()
  69 |         )
  70 |         logger.info(
  71 |             {
  72 |                 "info"
  73 |             }()
  74 |         )
  75 |         logger.warning(
  76 |             {
  77 |                 "warning"
  78 |             }()
  79 |         )
  80 |         logger.error(
  81 |             {
  82 |                 "error"
  83 |             }()
  84 |         )
  85 |         XCTAssertEqual(3, logging.history.entries.count, "expected number of entries to match")
  86 |         logging.history.assertNotExist(level: .debug, message: "trace")
  87 |         logging.history.assertNotExist(level: .debug, message: "debug")
  88 |         logging.history.assertExist(level: .info, message: "info")
  89 |         logging.history.assertExist(level: .warning, message: "warning")
  90 |         logging.history.assertExist(level: .error, message: "error")
  91 |     }
  92 | 
  93 |     func testMultiplex() throws {
  94 |         // bootstrap with our test logging impl
  95 |         let logging1 = TestLogging()
  96 |         let logging2 = TestLogging()
  97 |         LoggingSystem.bootstrapInternal { MultiplexLogHandler([logging1.make(label: $0), logging2.make(label: $0)]) }
  98 | 
  99 |         var logger = Logger(label: "test")
 100 |         logger.logLevel = .warning
 101 |         logger.info("hello world?")
 102 |         logger[metadataKey: "foo"] = "bar"
 103 |         logger.warning("hello world!")
 104 |         logging1.history.assertNotExist(level: .info, message: "hello world?")
 105 |         logging2.history.assertNotExist(level: .info, message: "hello world?")
 106 |         logging1.history.assertExist(level: .warning, message: "hello world!", metadata: ["foo": "bar"])
 107 |         logging2.history.assertExist(level: .warning, message: "hello world!", metadata: ["foo": "bar"])
 108 |     }
 109 | 
 110 |     func testMultiplexLogHandlerWithVariousLogLevels() throws {
 111 |         let logging1 = TestLogging()
 112 |         let logging2 = TestLogging()
 113 | 
 114 |         let logger1 = logging1.make(label: "1").with(logLevel: .info)
 115 |         let logger2 = logging2.make(label: "2").with(logLevel: .debug)
 116 | 
 117 |         LoggingSystem.bootstrapInternal { _ in
 118 |             MultiplexLogHandler([logger1, logger2])
 119 |         }
 120 | 
 121 |         let multiplexLogger = Logger(label: "test")
 122 |         multiplexLogger.trace("trace")
 123 |         multiplexLogger.debug("debug")
 124 |         multiplexLogger.info("info")
 125 |         multiplexLogger.warning("warning")
 126 | 
 127 |         logging1.history.assertNotExist(level: .trace, message: "trace")
 128 |         logging1.history.assertNotExist(level: .debug, message: "debug")
 129 |         logging1.history.assertExist(level: .info, message: "info")
 130 |         logging1.history.assertExist(level: .warning, message: "warning")
 131 | 
 132 |         logging2.history.assertNotExist(level: .trace, message: "trace")
 133 |         logging2.history.assertExist(level: .debug, message: "debug")
 134 |         logging2.history.assertExist(level: .info, message: "info")
 135 |         logging2.history.assertExist(level: .warning, message: "warning")
 136 |     }
 137 | 
 138 |     func testMultiplexLogHandlerNeedNotMaterializeValuesMultipleTimes() throws {
 139 |         let logging1 = TestLogging()
 140 |         let logging2 = TestLogging()
 141 | 
 142 |         let logger1 = logging1.make(label: "1").with(logLevel: .info)
 143 |         let logger2 = logging2.make(label: "2").with(logLevel: .info)
 144 | 
 145 |         LoggingSystem.bootstrapInternal { _ in
 146 |             MultiplexLogHandler([logger1, logger2])
 147 |         }
 148 | 
 149 |         var messageMaterializations: Int = 0
 150 |         var metadataMaterializations: Int = 0
 151 | 
 152 |         let multiplexLogger = Logger(label: "test")
 153 |         multiplexLogger.info(
 154 |             { () -> Logger.Message in
 155 |                 messageMaterializations += 1
 156 |                 return "info"
 157 |             }(),
 158 |             metadata: { () -> Logger.Metadata in
 159 |                 metadataMaterializations += 1
 160 |                 return [:]
 161 |             }()
 162 |         )
 163 | 
 164 |         logging1.history.assertExist(level: .info, message: "info")
 165 |         logging2.history.assertExist(level: .info, message: "info")
 166 | 
 167 |         XCTAssertEqual(messageMaterializations, 1)
 168 |         XCTAssertEqual(metadataMaterializations, 1)
 169 |     }
 170 | 
 171 |     func testMultiplexLogHandlerMetadata_settingMetadataThroughToUnderlyingHandlers() {
 172 |         let logging1 = TestLogging()
 173 |         let logging2 = TestLogging()
 174 | 
 175 |         let logger1 = logging1.make(label: "1")
 176 |             .withMetadata("one", "111")
 177 |             .withMetadata("in", "in-1")
 178 |         let logger2 = logging2.make(label: "2")
 179 |             .withMetadata("two", "222")
 180 |             .withMetadata("in", "in-2")
 181 | 
 182 |         LoggingSystem.bootstrapInternal { _ in
 183 |             MultiplexLogHandler([logger1, logger2])
 184 |         }
 185 | 
 186 |         var multiplexLogger = Logger(label: "test")
 187 | 
 188 |         // each logs its own metadata
 189 |         multiplexLogger.info("info")
 190 |         logging1.history.assertExist(
 191 |             level: .info,
 192 |             message: "info",
 193 |             metadata: [
 194 |                 "one": "111",
 195 |                 "in": "in-1",
 196 |             ]
 197 |         )
 198 |         logging2.history.assertExist(
 199 |             level: .info,
 200 |             message: "info",
 201 |             metadata: [
 202 |                 "two": "222",
 203 |                 "in": "in-2",
 204 |             ]
 205 |         )
 206 | 
 207 |         // if modified, change applies to both underlying handlers
 208 |         multiplexLogger[metadataKey: "new"] = "new"
 209 |         multiplexLogger.info("info")
 210 |         logging1.history.assertExist(
 211 |             level: .info,
 212 |             message: "info",
 213 |             metadata: [
 214 |                 "one": "111",
 215 |                 "in": "in-1",
 216 |                 "new": "new",
 217 |             ]
 218 |         )
 219 |         logging2.history.assertExist(
 220 |             level: .info,
 221 |             message: "info",
 222 |             metadata: [
 223 |                 "two": "222",
 224 |                 "in": "in-2",
 225 |                 "new": "new",
 226 |             ]
 227 |         )
 228 | 
 229 |         // overriding an existing value works the same way as adding a new one
 230 |         multiplexLogger[metadataKey: "in"] = "multi"
 231 |         multiplexLogger.info("info")
 232 |         logging1.history.assertExist(
 233 |             level: .info,
 234 |             message: "info",
 235 |             metadata: [
 236 |                 "one": "111",
 237 |                 "in": "multi",
 238 |                 "new": "new",
 239 |             ]
 240 |         )
 241 |         logging2.history.assertExist(
 242 |             level: .info,
 243 |             message: "info",
 244 |             metadata: [
 245 |                 "two": "222",
 246 |                 "in": "multi",
 247 |                 "new": "new",
 248 |             ]
 249 |         )
 250 |     }
 251 | 
 252 |     func testMultiplexLogHandlerMetadata_readingHandlerMetadata() {
 253 |         let logging1 = TestLogging()
 254 |         let logging2 = TestLogging()
 255 | 
 256 |         let logger1 = logging1.make(label: "1")
 257 |             .withMetadata("one", "111")
 258 |             .withMetadata("in", "in-1")
 259 |         let logger2 = logging2.make(label: "2")
 260 |             .withMetadata("two", "222")
 261 |             .withMetadata("in", "in-2")
 262 | 
 263 |         LoggingSystem.bootstrapInternal { _ in
 264 |             MultiplexLogHandler([logger1, logger2])
 265 |         }
 266 | 
 267 |         let multiplexLogger = Logger(label: "test")
 268 | 
 269 |         XCTAssertEqual(
 270 |             multiplexLogger.handler.metadata,
 271 |             [
 272 |                 "one": "111",
 273 |                 "two": "222",
 274 |                 "in": "in-2",
 275 |             ]
 276 |         )
 277 |     }
 278 | 
 279 |     func testMultiplexMetadataProviderSet() {
 280 |         let logging1 = TestLogging()
 281 |         let logging2 = TestLogging()
 282 | 
 283 |         let handler1 = {
 284 |             var handler1 = logging1.make(label: "1")
 285 |             handler1.metadata["one"] = "111"
 286 |             handler1.metadata["in"] = "in-1"
 287 |             handler1.metadataProvider = .constant([
 288 |                 "provider-1": "provided-111",
 289 |                 "provider-overlap": "provided-111",
 290 |             ])
 291 |             return handler1
 292 |         }()
 293 |         let handler2 = {
 294 |             var handler2 = logging2.make(label: "2")
 295 |             handler2.metadata["two"] = "222"
 296 |             handler2.metadata["in"] = "in-2"
 297 |             handler2.metadataProvider = .constant([
 298 |                 "provider-2": "provided-222",
 299 |                 "provider-overlap": "provided-222",
 300 |             ])
 301 |             return handler2
 302 |         }()
 303 | 
 304 |         LoggingSystem.bootstrapInternal { _ in
 305 |             MultiplexLogHandler([handler1, handler2])
 306 |         }
 307 | 
 308 |         let multiplexLogger = Logger(label: "test")
 309 | 
 310 |         XCTAssertEqual(
 311 |             multiplexLogger.handler.metadata,
 312 |             [
 313 |                 "one": "111",
 314 |                 "two": "222",
 315 |                 "in": "in-2",
 316 |                 "provider-1": "provided-111",
 317 |                 "provider-2": "provided-222",
 318 |                 "provider-overlap": "provided-222",
 319 |             ]
 320 |         )
 321 |         XCTAssertEqual(
 322 |             multiplexLogger.handler.metadataProvider?.get(),
 323 |             [
 324 |                 "provider-1": "provided-111",
 325 |                 "provider-2": "provided-222",
 326 |                 "provider-overlap": "provided-222",
 327 |             ]
 328 |         )
 329 |     }
 330 | 
 331 |     func testMultiplexMetadataProviderExtract() {
 332 |         let logging1 = TestLogging()
 333 |         let logging2 = TestLogging()
 334 | 
 335 |         let handler1 = {
 336 |             var handler1 = logging1.make(label: "1")
 337 |             handler1.metadataProvider = .constant([
 338 |                 "provider-1": "provided-111",
 339 |                 "provider-overlap": "provided-111",
 340 |             ])
 341 |             return handler1
 342 |         }()
 343 |         let handler2 = {
 344 |             var handler2 = logging2.make(label: "2")
 345 |             handler2.metadata["two"] = "222"
 346 |             handler2.metadata["in"] = "in-2"
 347 |             handler2.metadataProvider = .constant([
 348 |                 "provider-2": "provided-222",
 349 |                 "provider-overlap": "provided-222",
 350 |             ])
 351 |             return handler2
 352 |         }()
 353 | 
 354 |         LoggingSystem.bootstrapInternal(
 355 |             { _, metadataProvider in
 356 |                 MultiplexLogHandler(
 357 |                     [handler1, handler2],
 358 |                     metadataProvider: metadataProvider
 359 |                 )
 360 |             },
 361 |             metadataProvider: .constant([
 362 |                 "provider-overlap": "provided-outer"
 363 |             ])
 364 |         )
 365 | 
 366 |         let multiplexLogger = Logger(label: "test")
 367 | 
 368 |         let provider = multiplexLogger.metadataProvider!
 369 | 
 370 |         XCTAssertEqual(
 371 |             provider.get(),
 372 |             [
 373 |                 "provider-1": "provided-111",
 374 |                 "provider-2": "provided-222",
 375 |                 "provider-overlap": "provided-outer",
 376 |             ]
 377 |         )
 378 |     }
 379 | 
 380 |     enum TestError: Error {
 381 |         case boom
 382 |     }
 383 | 
 384 |     func testDictionaryMetadata() {
 385 |         let testLogging = TestLogging()
 386 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 387 | 
 388 |         var logger = Logger(label: "\(#function)")
 389 |         logger[metadataKey: "foo"] = ["bar": "buz"]
 390 |         logger[metadataKey: "empty-dict"] = [:]
 391 |         logger[metadataKey: "nested-dict"] = ["l1key": ["l2key": ["l3key": "l3value"]]]
 392 |         logger.info("hello world!")
 393 |         testLogging.history.assertExist(
 394 |             level: .info,
 395 |             message: "hello world!",
 396 |             metadata: [
 397 |                 "foo": ["bar": "buz"],
 398 |                 "empty-dict": [:],
 399 |                 "nested-dict": ["l1key": ["l2key": ["l3key": "l3value"]]],
 400 |             ]
 401 |         )
 402 |     }
 403 | 
 404 |     func testListMetadata() {
 405 |         let testLogging = TestLogging()
 406 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 407 | 
 408 |         var logger = Logger(label: "\(#function)")
 409 |         logger[metadataKey: "foo"] = ["bar", "buz"]
 410 |         logger[metadataKey: "empty-list"] = []
 411 |         logger[metadataKey: "nested-list"] = ["l1str", ["l2str1", "l2str2"]]
 412 |         logger.info("hello world!")
 413 |         testLogging.history.assertExist(
 414 |             level: .info,
 415 |             message: "hello world!",
 416 |             metadata: [
 417 |                 "foo": ["bar", "buz"],
 418 |                 "empty-list": [],
 419 |                 "nested-list": ["l1str", ["l2str1", "l2str2"]],
 420 |             ]
 421 |         )
 422 |     }
 423 | 
 424 |     // Example of custom "box" which may be used to implement "render at most once" semantics
 425 |     // Not thread-safe, thus should not be shared across threads.
 426 |     internal final class LazyMetadataBox: CustomStringConvertible {
 427 |         private var makeValue: (() -> String)?
 428 |         private var _value: String?
 429 | 
 430 |         public init(_ makeValue: @escaping () -> String) {
 431 |             self.makeValue = makeValue
 432 |         }
 433 | 
 434 |         /// This allows caching a value in case it is accessed via an by name subscript,
 435 |         // rather than as part of rendering all metadata that a LoggingContext was carrying
 436 |         public var value: String {
 437 |             if let f = self.makeValue {
 438 |                 self._value = f()
 439 |                 self.makeValue = nil
 440 |             }
 441 | 
 442 |             assert(self._value != nil, "_value MUST NOT be nil once `lazyValue` has run.")
 443 |             return self._value!
 444 |         }
 445 | 
 446 |         public var description: String {
 447 |             "\(self.value)"
 448 |         }
 449 |     }
 450 | 
 451 |     func testStringConvertibleMetadata() {
 452 |         let testLogging = TestLogging()
 453 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 454 |         var logger = Logger(label: "\(#function)")
 455 | 
 456 |         logger[metadataKey: "foo"] = .stringConvertible("raw-string")
 457 |         let lazyBox = LazyMetadataBox { "rendered-at-first-use" }
 458 |         logger[metadataKey: "lazy"] = .stringConvertible(lazyBox)
 459 |         logger.info("hello world!")
 460 |         testLogging.history.assertExist(
 461 |             level: .info,
 462 |             message: "hello world!",
 463 |             metadata: [
 464 |                 "foo": .stringConvertible("raw-string"),
 465 |                 "lazy": .stringConvertible(LazyMetadataBox { "rendered-at-first-use" }),
 466 |             ]
 467 |         )
 468 |     }
 469 | 
 470 |     private func dontEvaluateThisString(file: StaticString = #filePath, line: UInt = #line) -> Logger.Message {
 471 |         XCTFail("should not have been evaluated", file: file, line: line)
 472 |         return "should not have been evaluated"
 473 |     }
 474 | 
 475 |     func testAutoClosuresAreNotForcedUnlessNeeded() {
 476 |         let testLogging = TestLogging()
 477 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 478 | 
 479 |         var logger = Logger(label: "\(#function)")
 480 |         logger.logLevel = .error
 481 | 
 482 |         logger.debug(self.dontEvaluateThisString(), metadata: ["foo": "\(self.dontEvaluateThisString())"])
 483 |         logger.debug(self.dontEvaluateThisString())
 484 |         logger.info(self.dontEvaluateThisString())
 485 |         logger.warning(self.dontEvaluateThisString())
 486 |         logger.log(level: .warning, self.dontEvaluateThisString())
 487 |     }
 488 | 
 489 |     func testLocalMetadata() {
 490 |         let testLogging = TestLogging()
 491 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 492 | 
 493 |         var logger = Logger(label: "\(#function)")
 494 |         logger.info("hello world!", metadata: ["foo": "bar"])
 495 |         logger[metadataKey: "bar"] = "baz"
 496 |         logger[metadataKey: "baz"] = "qux"
 497 |         logger.warning("hello world!")
 498 |         logger.error("hello world!", metadata: ["baz": "quc"])
 499 |         testLogging.history.assertExist(level: .info, message: "hello world!", metadata: ["foo": "bar"])
 500 |         testLogging.history.assertExist(
 501 |             level: .warning,
 502 |             message: "hello world!",
 503 |             metadata: ["bar": "baz", "baz": "qux"]
 504 |         )
 505 |         testLogging.history.assertExist(level: .error, message: "hello world!", metadata: ["bar": "baz", "baz": "quc"])
 506 |     }
 507 | 
 508 |     func testCustomFactory() {
 509 |         struct CustomHandler: LogHandler {
 510 |             func log(
 511 |                 level: Logger.Level,
 512 |                 message: Logger.Message,
 513 |                 metadata: Logger.Metadata?,
 514 |                 source: String,
 515 |                 file: String,
 516 |                 function: String,
 517 |                 line: UInt
 518 |             ) {}
 519 | 
 520 |             subscript(metadataKey _: String) -> Logger.Metadata.Value? {
 521 |                 get { nil }
 522 |                 set {}
 523 |             }
 524 | 
 525 |             var metadata: Logger.Metadata {
 526 |                 get { Logger.Metadata() }
 527 |                 set {}
 528 |             }
 529 | 
 530 |             var logLevel: Logger.Level {
 531 |                 get { .info }
 532 |                 set {}
 533 |             }
 534 |         }
 535 | 
 536 |         let logger1 = Logger(label: "foo")
 537 |         XCTAssertFalse(logger1.handler is CustomHandler, "expected non-custom log handler")
 538 |         let logger2 = Logger(label: "foo", factory: { _ in CustomHandler() })
 539 |         XCTAssertTrue(logger2.handler is CustomHandler, "expected custom log handler")
 540 |     }
 541 | 
 542 |     func testAllLogLevelsExceptCriticalCanBeBlocked() {
 543 |         let testLogging = TestLogging()
 544 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 545 | 
 546 |         var logger = Logger(label: "\(#function)")
 547 |         logger.logLevel = .critical
 548 | 
 549 |         logger.trace("no")
 550 |         logger.debug("no")
 551 |         logger.info("no")
 552 |         logger.notice("no")
 553 |         logger.warning("no")
 554 |         logger.error("no")
 555 |         logger.critical("yes: critical")
 556 | 
 557 |         testLogging.history.assertNotExist(level: .trace, message: "no")
 558 |         testLogging.history.assertNotExist(level: .debug, message: "no")
 559 |         testLogging.history.assertNotExist(level: .info, message: "no")
 560 |         testLogging.history.assertNotExist(level: .notice, message: "no")
 561 |         testLogging.history.assertNotExist(level: .warning, message: "no")
 562 |         testLogging.history.assertNotExist(level: .error, message: "no")
 563 |         testLogging.history.assertExist(level: .critical, message: "yes: critical")
 564 |     }
 565 | 
 566 |     func testAllLogLevelsWork() {
 567 |         let testLogging = TestLogging()
 568 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 569 | 
 570 |         var logger = Logger(label: "\(#function)")
 571 |         logger.logLevel = .trace
 572 | 
 573 |         logger.trace("yes: trace")
 574 |         logger.debug("yes: debug")
 575 |         logger.info("yes: info")
 576 |         logger.notice("yes: notice")
 577 |         logger.warning("yes: warning")
 578 |         logger.error("yes: error")
 579 |         logger.critical("yes: critical")
 580 | 
 581 |         testLogging.history.assertExist(level: .trace, message: "yes: trace")
 582 |         testLogging.history.assertExist(level: .debug, message: "yes: debug")
 583 |         testLogging.history.assertExist(level: .info, message: "yes: info")
 584 |         testLogging.history.assertExist(level: .notice, message: "yes: notice")
 585 |         testLogging.history.assertExist(level: .warning, message: "yes: warning")
 586 |         testLogging.history.assertExist(level: .error, message: "yes: error")
 587 |         testLogging.history.assertExist(level: .critical, message: "yes: critical")
 588 |     }
 589 | 
 590 |     func testAllLogLevelByFunctionRefWithSource() {
 591 |         let testLogging = TestLogging()
 592 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 593 | 
 594 |         var logger = Logger(label: "\(#function)")
 595 |         logger.logLevel = .trace
 596 | 
 597 |         let trace = logger.trace(_:metadata:source:file:function:line:)
 598 |         let debug = logger.debug(_:metadata:source:file:function:line:)
 599 |         let info = logger.info(_:metadata:source:file:function:line:)
 600 |         let notice = logger.notice(_:metadata:source:file:function:line:)
 601 |         let warning = logger.warning(_:metadata:source:file:function:line:)
 602 |         let error = logger.error(_:metadata:source:file:function:line:)
 603 |         let critical = logger.critical(_:metadata:source:file:function:line:)
 604 | 
 605 |         trace("yes: trace", [:], "foo", #file, #function, #line)
 606 |         debug("yes: debug", [:], "foo", #file, #function, #line)
 607 |         info("yes: info", [:], "foo", #file, #function, #line)
 608 |         notice("yes: notice", [:], "foo", #file, #function, #line)
 609 |         warning("yes: warning", [:], "foo", #file, #function, #line)
 610 |         error("yes: error", [:], "foo", #file, #function, #line)
 611 |         critical("yes: critical", [:], "foo", #file, #function, #line)
 612 | 
 613 |         testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "foo")
 614 |         testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "foo")
 615 |         testLogging.history.assertExist(level: .info, message: "yes: info", source: "foo")
 616 |         testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "foo")
 617 |         testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "foo")
 618 |         testLogging.history.assertExist(level: .error, message: "yes: error", source: "foo")
 619 |         testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "foo")
 620 |     }
 621 | 
 622 |     func testAllLogLevelByFunctionRefWithoutSource() {
 623 |         let testLogging = TestLogging()
 624 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 625 | 
 626 |         var logger = Logger(label: "\(#function)")
 627 |         logger.logLevel = .trace
 628 | 
 629 |         let trace = logger.trace(_:metadata:file:function:line:)
 630 |         let debug = logger.debug(_:metadata:file:function:line:)
 631 |         let info = logger.info(_:metadata:file:function:line:)
 632 |         let notice = logger.notice(_:metadata:file:function:line:)
 633 |         let warning = logger.warning(_:metadata:file:function:line:)
 634 |         let error = logger.error(_:metadata:file:function:line:)
 635 |         let critical = logger.critical(_:metadata:file:function:line:)
 636 | 
 637 |         trace("yes: trace", [:], #fileID, #function, #line)
 638 |         debug("yes: debug", [:], #fileID, #function, #line)
 639 |         info("yes: info", [:], #fileID, #function, #line)
 640 |         notice("yes: notice", [:], #fileID, #function, #line)
 641 |         warning("yes: warning", [:], #fileID, #function, #line)
 642 |         error("yes: error", [:], #fileID, #function, #line)
 643 |         critical("yes: critical", [:], #fileID, #function, #line)
 644 | 
 645 |         testLogging.history.assertExist(level: .trace, message: "yes: trace")
 646 |         testLogging.history.assertExist(level: .debug, message: "yes: debug")
 647 |         testLogging.history.assertExist(level: .info, message: "yes: info")
 648 |         testLogging.history.assertExist(level: .notice, message: "yes: notice")
 649 |         testLogging.history.assertExist(level: .warning, message: "yes: warning")
 650 |         testLogging.history.assertExist(level: .error, message: "yes: error")
 651 |         testLogging.history.assertExist(level: .critical, message: "yes: critical")
 652 |     }
 653 | 
 654 |     func testLogsEmittedFromSubdirectoryGetCorrectModuleInNewerSwifts() {
 655 |         let testLogging = TestLogging()
 656 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 657 | 
 658 |         var logger = Logger(label: "\(#function)")
 659 |         logger.logLevel = .trace
 660 | 
 661 |         emitLogMessage("hello", to: logger)
 662 | 
 663 |         let moduleName = "LoggingTests"  // the actual name
 664 | 
 665 |         testLogging.history.assertExist(level: .trace, message: "hello", source: moduleName)
 666 |         testLogging.history.assertExist(level: .debug, message: "hello", source: moduleName)
 667 |         testLogging.history.assertExist(level: .info, message: "hello", source: moduleName)
 668 |         testLogging.history.assertExist(level: .notice, message: "hello", source: moduleName)
 669 |         testLogging.history.assertExist(level: .warning, message: "hello", source: moduleName)
 670 |         testLogging.history.assertExist(level: .error, message: "hello", source: moduleName)
 671 |         testLogging.history.assertExist(level: .critical, message: "hello", source: moduleName)
 672 |     }
 673 | 
 674 |     func testLogMessageWithStringInterpolation() {
 675 |         let testLogging = TestLogging()
 676 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 677 | 
 678 |         var logger = Logger(label: "\(#function)")
 679 |         logger.logLevel = .debug
 680 | 
 681 |         let someInt = Int.random(in: 23..<42)
 682 |         logger.debug("My favourite number is \(someInt) and not \(someInt - 1)")
 683 |         testLogging.history.assertExist(
 684 |             level: .debug,
 685 |             message: "My favourite number is \(someInt) and not \(someInt - 1)" as String
 686 |         )
 687 |     }
 688 | 
 689 |     func testLoggingAString() {
 690 |         let testLogging = TestLogging()
 691 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
 692 | 
 693 |         var logger = Logger(label: "\(#function)")
 694 |         logger.logLevel = .debug
 695 | 
 696 |         let anActualString: String = "hello world!"
 697 |         // We can't stick an actual String in here because we expect a Logger.Message. If we want to log an existing
 698 |         // `String`, we can use string interpolation. The error you'll get trying to use the String directly is:
 699 |         //
 700 |         //     error: Cannot convert value of type 'String' to expected argument type 'Logger.Message'
 701 |         logger.debug("\(anActualString)")
 702 |         testLogging.history.assertExist(level: .debug, message: "hello world!")
 703 |     }
 704 | 
 705 |     func testMultiplexMetadataProviderMergesInSpecifiedOrder() {
 706 |         let logging = TestLogging()
 707 | 
 708 |         let providerA = Logger.MetadataProvider { ["provider": "a", "a": "foo"] }
 709 |         let providerB = Logger.MetadataProvider { ["provider": "b", "b": "bar"] }
 710 |         let logger = Logger(
 711 |             label: #function,
 712 |             factory: { label in
 713 |                 logging.makeWithMetadataProvider(label: label, metadataProvider: .multiplex([providerA, providerB]))
 714 |             }
 715 |         )
 716 | 
 717 |         logger.log(level: .info, "test", metadata: ["one-off": "42"])
 718 | 
 719 |         logging.history.assertExist(
 720 |             level: .info,
 721 |             message: "test",
 722 |             metadata: ["provider": "b", "a": "foo", "b": "bar", "one-off": "42"]
 723 |         )
 724 |     }
 725 | 
 726 |     func testLoggerWithoutFactoryOverrideDefaultsToUsingLoggingSystemMetadataProvider() {
 727 |         let logging = TestLogging()
 728 |         LoggingSystem.bootstrapInternal(
 729 |             { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) },
 730 |             metadataProvider: .init { ["provider": "42"] }
 731 |         )
 732 | 
 733 |         let logger = Logger(label: #function)
 734 | 
 735 |         logger.log(level: .info, "test", metadata: ["one-off": "42"])
 736 | 
 737 |         logging.history.assertExist(
 738 |             level: .info,
 739 |             message: "test",
 740 |             metadata: ["provider": "42", "one-off": "42"]
 741 |         )
 742 |     }
 743 | 
 744 |     func testLoggerWithPredefinedLibraryMetadataProvider() {
 745 |         let logging = TestLogging()
 746 |         LoggingSystem.bootstrapInternal(
 747 |             { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) },
 748 |             metadataProvider: .exampleProvider
 749 |         )
 750 | 
 751 |         let logger = Logger(label: #function)
 752 | 
 753 |         logger.log(level: .info, "test", metadata: ["one-off": "42"])
 754 | 
 755 |         logging.history.assertExist(
 756 |             level: .info,
 757 |             message: "test",
 758 |             metadata: ["example": "example-value", "one-off": "42"]
 759 |         )
 760 |     }
 761 | 
 762 |     func testLoggerWithFactoryOverrideDefaultsToUsingLoggingSystemMetadataProvider() {
 763 |         let logging = TestLogging()
 764 |         LoggingSystem.bootstrapInternal(
 765 |             { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) },
 766 |             metadataProvider: .init { ["provider": "42"] }
 767 |         )
 768 | 
 769 |         let logger = Logger(
 770 |             label: #function,
 771 |             factory: { label in
 772 |                 logging.makeWithMetadataProvider(label: label, metadataProvider: LoggingSystem.metadataProvider)
 773 |             }
 774 |         )
 775 | 
 776 |         logger.log(level: .info, "test", metadata: ["one-off": "42"])
 777 | 
 778 |         logging.history.assertExist(
 779 |             level: .info,
 780 |             message: "test",
 781 |             metadata: ["provider": "42", "one-off": "42"]
 782 |         )
 783 |     }
 784 | 
 785 |     func testMultiplexerIsValue() {
 786 |         let multi = MultiplexLogHandler([
 787 |             StreamLogHandler.standardOutput(label: "x"), StreamLogHandler.standardOutput(label: "y"),
 788 |         ])
 789 |         LoggingSystem.bootstrapInternal { _ in
 790 |             print("new multi")
 791 |             return multi
 792 |         }
 793 |         let logger1: Logger = {
 794 |             var logger = Logger(label: "foo")
 795 |             logger.logLevel = .debug
 796 |             logger[metadataKey: "only-on"] = "first"
 797 |             return logger
 798 |         }()
 799 |         XCTAssertEqual(.debug, logger1.logLevel)
 800 |         var logger2 = logger1
 801 |         logger2.logLevel = .error
 802 |         logger2[metadataKey: "only-on"] = "second"
 803 |         XCTAssertEqual(.error, logger2.logLevel)
 804 |         XCTAssertEqual(.debug, logger1.logLevel)
 805 |         XCTAssertEqual("first", logger1[metadataKey: "only-on"])
 806 |         XCTAssertEqual("second", logger2[metadataKey: "only-on"])
 807 |         logger1.error("hey")
 808 |     }
 809 | 
 810 |     /// Protects an object such that it can only be accessed while holding a lock.
 811 |     private final class LockedValueBox<Value: Sendable>: @unchecked Sendable {
 812 |         private let lock = Lock()
 813 |         private var storage: Value
 814 | 
 815 |         init(initialValue: Value) {
 816 |             self.storage = initialValue
 817 |         }
 818 | 
 819 |         func withLock<Result>(_ operation: (Value) -> Result) -> Result {
 820 |             self.lock.withLock {
 821 |                 operation(self.storage)
 822 |             }
 823 |         }
 824 | 
 825 |         func withLockMutating(_ operation: (inout Value) -> Void) {
 826 |             self.lock.withLockVoid {
 827 |                 operation(&self.storage)
 828 |             }
 829 |         }
 830 | 
 831 |         var underlying: Value {
 832 |             get { self.withLock { $0 } }
 833 |             set { self.withLockMutating { $0 = newValue } }
 834 |         }
 835 |     }
 836 | 
 837 |     func testLoggerWithGlobalOverride() {
 838 |         struct LogHandlerWithGlobalLogLevelOverride: LogHandler {
 839 |             // the static properties hold the globally overridden log level (if overridden)
 840 |             private static let overrideLogLevel = LockedValueBox<Logger.Level?>(initialValue: nil)
 841 | 
 842 |             private let recorder: Recorder
 843 |             // this holds the log level if not overridden
 844 |             private var _logLevel: Logger.Level = .info
 845 | 
 846 |             // metadata storage
 847 |             var metadata: Logger.Metadata = [:]
 848 | 
 849 |             init(recorder: Recorder) {
 850 |                 self.recorder = recorder
 851 |             }
 852 | 
 853 |             var logLevel: Logger.Level {
 854 |                 // when we get asked for the log level, we check if it was globally overridden or not
 855 |                 get {
 856 |                     LogHandlerWithGlobalLogLevelOverride.overrideLogLevel.underlying ?? self._logLevel
 857 |                 }
 858 |                 // we set the log level whenever we're asked (note: this might not have an effect if globally
 859 |                 // overridden)
 860 |                 set {
 861 |                     self._logLevel = newValue
 862 |                 }
 863 |             }
 864 | 
 865 |             func log(
 866 |                 level: Logger.Level,
 867 |                 message: Logger.Message,
 868 |                 metadata: Logger.Metadata?,
 869 |                 source: String,
 870 |                 file: String,
 871 |                 function: String,
 872 |                 line: UInt
 873 |             ) {
 874 |                 self.recorder.record(level: level, metadata: metadata, message: message, source: source)
 875 |             }
 876 | 
 877 |             subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
 878 |                 get {
 879 |                     self.metadata[metadataKey]
 880 |                 }
 881 |                 set(newValue) {
 882 |                     self.metadata[metadataKey] = newValue
 883 |                 }
 884 |             }
 885 | 
 886 |             // this is the function to globally override the log level, it is not part of the `LogHandler` protocol
 887 |             static func overrideGlobalLogLevel(_ logLevel: Logger.Level) {
 888 |                 LogHandlerWithGlobalLogLevelOverride.overrideLogLevel.underlying = logLevel
 889 |             }
 890 |         }
 891 | 
 892 |         let logRecorder = Recorder()
 893 |         LoggingSystem.bootstrapInternal { _ in
 894 |             LogHandlerWithGlobalLogLevelOverride(recorder: logRecorder)
 895 |         }
 896 | 
 897 |         var logger1 = Logger(label: "logger-\(#file):\(#line)")
 898 |         var logger2 = logger1
 899 |         logger1.logLevel = .warning
 900 |         logger1[metadataKey: "only-on"] = "first"
 901 |         logger2.logLevel = .error
 902 |         logger2[metadataKey: "only-on"] = "second"
 903 |         XCTAssertEqual(.error, logger2.logLevel)
 904 |         XCTAssertEqual(.warning, logger1.logLevel)
 905 |         XCTAssertEqual("first", logger1[metadataKey: "only-on"])
 906 |         XCTAssertEqual("second", logger2[metadataKey: "only-on"])
 907 | 
 908 |         logger1.notice("logger1, before")
 909 |         logger2.notice("logger2, before")
 910 | 
 911 |         LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel(.debug)
 912 | 
 913 |         logger1.notice("logger1, after")
 914 |         logger2.notice("logger2, after")
 915 | 
 916 |         logRecorder.assertNotExist(level: .notice, message: "logger1, before")
 917 |         logRecorder.assertNotExist(level: .notice, message: "logger2, before")
 918 |         logRecorder.assertExist(level: .notice, message: "logger1, after")
 919 |         logRecorder.assertExist(level: .notice, message: "logger2, after")
 920 |     }
 921 | 
 922 |     func testLogLevelCases() {
 923 |         let levels = Logger.Level.allCases
 924 |         XCTAssertEqual(7, levels.count)
 925 |     }
 926 | 
 927 |     func testLogLevelOrdering() {
 928 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.debug)
 929 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.info)
 930 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.notice)
 931 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.warning)
 932 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.error)
 933 |         XCTAssertLessThan(Logger.Level.trace, Logger.Level.critical)
 934 |         XCTAssertLessThan(Logger.Level.debug, Logger.Level.info)
 935 |         XCTAssertLessThan(Logger.Level.debug, Logger.Level.notice)
 936 |         XCTAssertLessThan(Logger.Level.debug, Logger.Level.warning)
 937 |         XCTAssertLessThan(Logger.Level.debug, Logger.Level.error)
 938 |         XCTAssertLessThan(Logger.Level.debug, Logger.Level.critical)
 939 |         XCTAssertLessThan(Logger.Level.info, Logger.Level.notice)
 940 |         XCTAssertLessThan(Logger.Level.info, Logger.Level.warning)
 941 |         XCTAssertLessThan(Logger.Level.info, Logger.Level.error)
 942 |         XCTAssertLessThan(Logger.Level.info, Logger.Level.critical)
 943 |         XCTAssertLessThan(Logger.Level.notice, Logger.Level.warning)
 944 |         XCTAssertLessThan(Logger.Level.notice, Logger.Level.error)
 945 |         XCTAssertLessThan(Logger.Level.notice, Logger.Level.critical)
 946 |         XCTAssertLessThan(Logger.Level.warning, Logger.Level.error)
 947 |         XCTAssertLessThan(Logger.Level.warning, Logger.Level.critical)
 948 |         XCTAssertLessThan(Logger.Level.error, Logger.Level.critical)
 949 |     }
 950 | 
 951 |     final class InterceptStream: TextOutputStream {
 952 |         var interceptedText: String?
 953 |         var strings = [String]()
 954 | 
 955 |         func write(_ string: String) {
 956 |             // This is a test implementation, a real implementation would include locking
 957 |             self.strings.append(string)
 958 |             self.interceptedText = (self.interceptedText ?? "") + string
 959 |         }
 960 |     }
 961 | 
 962 |     func testStreamLogHandlerWritesToAStream() {
 963 |         let interceptStream = InterceptStream()
 964 |         LoggingSystem.bootstrapInternal { _ in
 965 |             StreamLogHandler(label: "test", stream: interceptStream)
 966 |         }
 967 |         let log = Logger(label: "test")
 968 | 
 969 |         let testString = "my message is better than yours"
 970 |         log.critical("\(testString)")
 971 | 
 972 |         let messageSucceeded = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines)
 973 |             .hasSuffix(testString)
 974 | 
 975 |         XCTAssertTrue(messageSucceeded ?? false)
 976 |         XCTAssertEqual(interceptStream.strings.count, 1)
 977 |     }
 978 | 
 979 |     func testStreamLogHandlerOutputFormat() {
 980 |         let interceptStream = InterceptStream()
 981 |         let label = "testLabel"
 982 |         LoggingSystem.bootstrapInternal { label in
 983 |             StreamLogHandler(label: label, stream: interceptStream)
 984 |         }
 985 |         let source = "testSource"
 986 |         let log = Logger(label: label)
 987 | 
 988 |         let testString = "my message is better than yours"
 989 |         log.critical("\(testString)", source: source)
 990 | 
 991 |         let pattern =
 992 |             "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|-)\\d{4}\\s\(Logger.Level.critical)\\s\(label)\\s:\\s\\[\(source)\\]\\s\(testString)
quot;
 993 | 
 994 |         let messageSucceeded =
 995 |             interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines).range(
 996 |                 of: pattern,
 997 |                 options: .regularExpression
 998 |             ) != nil
 999 | 
1000 |         XCTAssertTrue(messageSucceeded)
1001 |         XCTAssertEqual(interceptStream.strings.count, 1)
1002 |     }
1003 | 
1004 |     func testStreamLogHandlerOutputFormatWithMetaData() {
1005 |         let interceptStream = InterceptStream()
1006 |         let label = "testLabel"
1007 |         LoggingSystem.bootstrapInternal { label in
1008 |             StreamLogHandler(label: label, stream: interceptStream)
1009 |         }
1010 |         let source = "testSource"
1011 |         let log = Logger(label: label)
1012 | 
1013 |         let testString = "my message is better than yours"
1014 |         log.critical("\(testString)", metadata: ["test": "test"], source: source)
1015 | 
1016 |         let pattern =
1017 |             "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|-)\\d{4}\\s\(Logger.Level.critical)\\s\(label)\\s:\\stest=test\\s\\[\(source)\\]\\s\(testString)
quot;
1018 | 
1019 |         let messageSucceeded =
1020 |             interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines).range(
1021 |                 of: pattern,
1022 |                 options: .regularExpression
1023 |             ) != nil
1024 | 
1025 |         XCTAssertTrue(messageSucceeded)
1026 |         XCTAssertEqual(interceptStream.strings.count, 1)
1027 |     }
1028 | 
1029 |     func testStreamLogHandlerOutputFormatWithOrderedMetadata() {
1030 |         let interceptStream = InterceptStream()
1031 |         let label = "testLabel"
1032 |         LoggingSystem.bootstrapInternal { label in
1033 |             StreamLogHandler(label: label, stream: interceptStream)
1034 |         }
1035 |         let log = Logger(label: label)
1036 | 
1037 |         let testString = "my message is better than yours"
1038 |         log.critical("\(testString)", metadata: ["a": "a0", "b": "b0"])
1039 |         log.critical("\(testString)", metadata: ["b": "b1", "a": "a1"])
1040 | 
1041 |         XCTAssertEqual(interceptStream.strings.count, 2)
1042 |         guard interceptStream.strings.count == 2 else {
1043 |             XCTFail("Intercepted \(interceptStream.strings.count) logs, expected 2")
1044 |             return
1045 |         }
1046 | 
1047 |         XCTAssert(interceptStream.strings[0].contains("a=a0 b=b0"), "LINES: \(interceptStream.strings[0])")
1048 |         XCTAssert(interceptStream.strings[1].contains("a=a1 b=b1"), "LINES: \(interceptStream.strings[1])")
1049 |     }
1050 | 
1051 |     func testStreamLogHandlerWritesIncludeMetadataProviderMetadata() {
1052 |         let interceptStream = InterceptStream()
1053 |         LoggingSystem.bootstrapInternal(
1054 |             { _, metadataProvider in
1055 |                 StreamLogHandler(label: "test", stream: interceptStream, metadataProvider: metadataProvider)
1056 |             },
1057 |             metadataProvider: .exampleProvider
1058 |         )
1059 |         let log = Logger(label: "test")
1060 | 
1061 |         let testString = "my message is better than yours"
1062 |         log.critical("\(testString)")
1063 | 
1064 |         let messageSucceeded = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines)
1065 |             .hasSuffix(testString)
1066 | 
1067 |         XCTAssertTrue(messageSucceeded ?? false)
1068 |         XCTAssertEqual(interceptStream.strings.count, 1)
1069 |         let message = interceptStream.strings.first!
1070 |         XCTAssertTrue(message.contains("example=example-value"), "message must contain metadata, was: \(message)")
1071 |     }
1072 | 
1073 |     func testStdioOutputStreamWrite() {
1074 |         self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in
1075 |             let logStream = StdioOutputStream(file: writeFD, flushMode: .always)
1076 |             LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) }
1077 |             let log = Logger(label: "test")
1078 |             let testString = "hello\u{0} world"
1079 |             log.critical("\(testString)")
1080 | 
1081 |             #if os(Windows)
1082 |             let size = _read(readFD, readBuffer, 256)
1083 |             #else
1084 |             let size = read(readFD, readBuffer, 256)
1085 |             #endif
1086 | 
1087 |             let output = String(
1088 |                 decoding: UnsafeRawBufferPointer(start: UnsafeRawPointer(readBuffer), count: numericCast(size)),
1089 |                 as: UTF8.self
1090 |             )
1091 |             let messageSucceeded = output.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix(testString)
1092 |             XCTAssertTrue(messageSucceeded)
1093 |         }
1094 |     }
1095 | 
1096 |     func testStdioOutputStreamFlush() {
1097 |         // flush on every statement
1098 |         self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in
1099 |             let logStream = StdioOutputStream(file: writeFD, flushMode: .always)
1100 |             LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) }
1101 |             Logger(label: "test").critical("test")
1102 | 
1103 |             #if os(Windows)
1104 |             let size = _read(readFD, readBuffer, 256)
1105 |             #else
1106 |             let size = read(readFD, readBuffer, 256)
1107 |             #endif
1108 |             XCTAssertGreaterThan(size, -1, "expected flush")
1109 | 
1110 |             logStream.flush()
1111 | 
1112 |             #if os(Windows)
1113 |             let size2 = _read(readFD, readBuffer, 256)
1114 |             #else
1115 |             let size2 = read(readFD, readBuffer, 256)
1116 |             #endif
1117 |             XCTAssertEqual(size2, -1, "expected no flush")
1118 |         }
1119 |         // default flushing
1120 |         self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in
1121 |             let logStream = StdioOutputStream(file: writeFD, flushMode: .undefined)
1122 |             LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) }
1123 |             Logger(label: "test").critical("test")
1124 | 
1125 |             #if os(Windows)
1126 |             let size = _read(readFD, readBuffer, 256)
1127 |             #else
1128 |             let size = read(readFD, readBuffer, 256)
1129 |             #endif
1130 |             XCTAssertEqual(size, -1, "expected no flush")
1131 | 
1132 |             logStream.flush()
1133 | 
1134 |             #if os(Windows)
1135 |             let size2 = _read(readFD, readBuffer, 256)
1136 |             #else
1137 |             let size2 = read(readFD, readBuffer, 256)
1138 |             #endif
1139 |             XCTAssertGreaterThan(size2, -1, "expected flush")
1140 |         }
1141 |     }
1142 | 
1143 |     func withWriteReadFDsAndReadBuffer(_ body: (CFilePointer, CInt, UnsafeMutablePointer<Int8>) -> Void) {
1144 |         var fds: [Int32] = [-1, -1]
1145 |         #if os(Windows)
1146 |         fds.withUnsafeMutableBufferPointer {
1147 |             let err = _pipe($0.baseAddress, 256, _O_BINARY)
1148 |             XCTAssertEqual(err, 0, "_pipe failed \(err)")
1149 |         }
1150 |         guard let writeFD = _fdopen(fds[1], "w") else {
1151 |             XCTFail("Failed to open file")
1152 |             return
1153 |         }
1154 |         #else
1155 |         fds.withUnsafeMutableBufferPointer { ptr in
1156 |             let err = pipe(ptr.baseAddress!)
1157 |             XCTAssertEqual(err, 0, "pipe failed \(err)")
1158 |         }
1159 |         guard let writeFD = fdopen(fds[1], "w") else {
1160 |             XCTFail("Failed to open file")
1161 |             return
1162 |         }
1163 |         #endif
1164 | 
1165 |         let writeBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256)
1166 |         defer {
1167 |             writeBuffer.deinitialize(count: 256)
1168 |             writeBuffer.deallocate()
1169 |         }
1170 | 
1171 |         var err = setvbuf(writeFD, writeBuffer, _IOFBF, 256)
1172 |         XCTAssertEqual(err, 0, "setvbuf failed \(err)")
1173 | 
1174 |         let readFD = fds[0]
1175 |         #if os(Windows)
1176 |         let hPipe: HANDLE = HANDLE(bitPattern: _get_osfhandle(readFD))!
1177 |         XCTAssertFalse(hPipe == INVALID_HANDLE_VALUE)
1178 | 
1179 |         var dwMode: DWORD = DWORD(PIPE_NOWAIT)
1180 |         let bSucceeded = SetNamedPipeHandleState(hPipe, &dwMode, nil, nil)
1181 |         XCTAssertTrue(bSucceeded)
1182 |         #else
1183 |         err = fcntl(readFD, F_SETFL, fcntl(readFD, F_GETFL) | O_NONBLOCK)
1184 |         XCTAssertEqual(err, 0, "fcntl failed \(err)")
1185 |         #endif
1186 | 
1187 |         let readBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256)
1188 |         defer {
1189 |             readBuffer.deinitialize(count: 256)
1190 |             readBuffer.deallocate()
1191 |         }
1192 | 
1193 |         // the actual test
1194 |         body(writeFD, readFD, readBuffer)
1195 | 
1196 |         for fd in fds {
1197 |             #if os(Windows)
1198 |             _close(fd)
1199 |             #else
1200 |             close(fd)
1201 |             #endif
1202 |         }
1203 |     }
1204 | 
1205 |     func testOverloadingError() {
1206 |         struct Dummy: Error, LocalizedError {
1207 |             var errorDescription: String? {
1208 |                 "errorDescription"
1209 |             }
1210 |         }
1211 |         // bootstrap with our test logging impl
1212 |         let logging = TestLogging()
1213 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
1214 | 
1215 |         var logger = Logger(label: "test")
1216 |         logger.logLevel = .error
1217 |         logger.error(error: Dummy())
1218 | 
1219 |         logging.history.assertExist(level: .error, message: "errorDescription")
1220 |     }
1221 | 
1222 |     func testCompileInitializeStandardStreamLogHandlersWithMetadataProviders() {
1223 |         // avoid "unreachable code" warnings
1224 |         let dontExecute = Int.random(in: 100...200) == 1
1225 |         guard dontExecute else {
1226 |             return
1227 |         }
1228 | 
1229 |         // default usage
1230 |         LoggingSystem.bootstrap { (label: String) in StreamLogHandler.standardOutput(label: label) }
1231 |         LoggingSystem.bootstrap { (label: String) in StreamLogHandler.standardError(label: label) }
1232 | 
1233 |         // with metadata handler, explicitly, public api
1234 |         LoggingSystem.bootstrap(
1235 |             { label, metadataProvider in
1236 |                 StreamLogHandler.standardOutput(label: label, metadataProvider: metadataProvider)
1237 |             },
1238 |             metadataProvider: .exampleProvider
1239 |         )
1240 |         LoggingSystem.bootstrap(
1241 |             { label, metadataProvider in
1242 |                 StreamLogHandler.standardError(label: label, metadataProvider: metadataProvider)
1243 |             },
1244 |             metadataProvider: .exampleProvider
1245 |         )
1246 | 
1247 |         // with metadata handler, still pretty
1248 |         LoggingSystem.bootstrap(
1249 |             { (label: String, metadataProvider: Logger.MetadataProvider?) in
1250 |                 StreamLogHandler.standardOutput(label: label, metadataProvider: metadataProvider)
1251 |             },
1252 |             metadataProvider: .exampleProvider
1253 |         )
1254 |         LoggingSystem.bootstrap(
1255 |             { (label: String, metadataProvider: Logger.MetadataProvider?) in
1256 |                 StreamLogHandler.standardError(label: label, metadataProvider: metadataProvider)
1257 |             },
1258 |             metadataProvider: .exampleProvider
1259 |         )
1260 |     }
1261 | 
1262 |     func testLoggerIsJustHoldingASinglePointer() {
1263 |         let expectedSize = MemoryLayout<UnsafeRawPointer>.size
1264 |         XCTAssertEqual(MemoryLayout<Logger>.size, expectedSize)
1265 |     }
1266 | 
1267 |     func testLoggerCopyOnWrite() {
1268 |         var logger1 = Logger(label: "foo")
1269 |         logger1.logLevel = .error
1270 |         var logger2 = logger1
1271 |         logger2.logLevel = .trace
1272 |         XCTAssertEqual(.error, logger1.logLevel)
1273 |         XCTAssertEqual(.trace, logger2.logLevel)
1274 |     }
1275 | }
1276 | 
1277 | extension Logger {
1278 |     public func error(
1279 |         error: any Error,
1280 |         metadata: @autoclosure () -> Logger.Metadata? = nil,
1281 |         file: String = #fileID,
1282 |         function: String = #function,
1283 |         line: UInt = #line
1284 |     ) {
1285 |         self.error("\(error.localizedDescription)", metadata: metadata(), file: file, function: function, line: line)
1286 |     }
1287 | }
1288 | 
1289 | extension Logger.MetadataProvider {
1290 |     static var exampleProvider: Self {
1291 |         .init { ["example": .string("example-value")] }
1292 |     }
1293 | 
1294 |     static func constant(_ metadata: Logger.Metadata) -> Self {
1295 |         .init { metadata }
1296 |     }
1297 | }
1298 | 
1299 | // Sendable
1300 | 
1301 | // used to test logging metadata which requires Sendable conformance
1302 | // @unchecked Sendable since manages it own state
1303 | extension LoggingTest.LazyMetadataBox: @unchecked Sendable {}
1304 | 
1305 | // used to test logging stream which requires Sendable conformance
1306 | // @unchecked Sendable since manages it own state
1307 | extension LoggingTest.InterceptStream: @unchecked Sendable {}
1308 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/MDCTest.swift:
--------------------------------------------------------------------------------
 1 | //===----------------------------------------------------------------------===//
 2 | //
 3 | // This source file is part of the Swift Logging API open source project
 4 | //
 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
 6 | // Licensed under Apache License v2.0
 7 | //
 8 | // See LICENSE.txt for license information
 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 | import Dispatch
15 | import XCTest
16 | 
17 | @testable import Logging
18 | 
19 | class MDCTest: XCTestCase {
20 |     func test1() throws {
21 |         // bootstrap with our test logger
22 |         let logging = TestLogging()
23 |         LoggingSystem.bootstrapInternal { logging.make(label: $0) }
24 | 
25 |         // run the program
26 |         MDC.global["foo"] = "bar"
27 |         let group = DispatchGroup()
28 |         for r in 5...10 {
29 |             group.enter()
30 |             DispatchQueue(label: "mdc-test-queue-\(r)").async {
31 |                 let add = Int.random(in: 10...1000)
32 |                 let remove = Int.random(in: 0...add - 1)
33 |                 for i in 0...add {
34 |                     MDC.global["key-\(i)"] = "value-\(i)"
35 |                 }
36 |                 for i in 0...remove {
37 |                     MDC.global["key-\(i)"] = nil
38 |                 }
39 |                 XCTAssertEqual(add - remove, MDC.global.metadata.count, "expected number of entries to match")
40 |                 for i in remove + 1...add {
41 |                     XCTAssertNotNil(MDC.global["key-\(i)"], "expecting value for key-\(i)")
42 |                 }
43 |                 for i in 0...remove {
44 |                     XCTAssertNil(MDC.global["key-\(i)"], "not expecting value for key-\(i)")
45 |                 }
46 |                 MDC.global.clear()
47 |                 group.leave()
48 |             }
49 |         }
50 |         group.wait()
51 |         XCTAssertEqual(MDC.global["foo"], "bar", "expecting to find top items")
52 |         MDC.global["foo"] = nil
53 |         XCTAssertTrue(MDC.global.metadata.isEmpty, "MDC should be empty")
54 |     }
55 | }
56 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/MetadataProviderTest.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | import XCTest
 16 | 
 17 | @testable import Logging
 18 | 
 19 | #if canImport(Darwin)
 20 | import Darwin
 21 | #elseif os(Windows)
 22 | import WinSDK
 23 | #elseif canImport(Android)
 24 | import Android
 25 | #else
 26 | import Glibc
 27 | #endif
 28 | 
 29 | final class MetadataProviderTest: XCTestCase {
 30 |     func testLoggingMergesOneOffMetadataWithProvidedMetadataFromExplicitlyPassed() throws {
 31 |         let logging = TestLogging()
 32 |         LoggingSystem.bootstrapInternal(
 33 |             { logging.makeWithMetadataProvider(label: $0, metadataProvider: $1) },
 34 |             metadataProvider: .init {
 35 |                 ["common": "initial"]
 36 |             }
 37 |         )
 38 | 
 39 |         let logger = Logger(
 40 |             label: #function,
 41 |             metadataProvider: .init {
 42 |                 [
 43 |                     "common": "provider",
 44 |                     "provider": "42",
 45 |                 ]
 46 |             }
 47 |         )
 48 | 
 49 |         logger.log(level: .info, "test", metadata: ["one-off": "42", "common": "one-off"])
 50 | 
 51 |         logging.history.assertExist(
 52 |             level: .info,
 53 |             message: "test",
 54 |             metadata: ["common": "one-off", "one-off": "42", "provider": "42"]
 55 |         )
 56 |     }
 57 | 
 58 |     func testLogHandlerThatDidNotImplementProvidersButSomeoneAttemptsToSetOneOnIt() {
 59 |         #if DEBUG
 60 |         // we only emit these warnings in debug mode
 61 |         let logging = TestLogging()
 62 |         var handler = LogHandlerThatDidNotImplementMetadataProviders(testLogging: logging)
 63 | 
 64 |         handler.metadataProvider = .simpleTestProvider
 65 | 
 66 |         logging.history.assertExist(
 67 |             level: .warning,
 68 |             message:
 69 |                 "Attempted to set metadataProvider on LogHandlerThatDidNotImplementMetadataProviders that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.",
 70 |             source: "Logging"
 71 |         )
 72 | 
 73 |         let countBefore = logging.history.entries.count
 74 |         handler.metadataProvider = .simpleTestProvider
 75 |         XCTAssertEqual(countBefore, logging.history.entries.count, "Should only log the warning once")
 76 |         #endif
 77 |     }
 78 | 
 79 |     func testLogHandlerThatDidImplementProvidersButSomeoneAttemptsToSetOneOnIt() {
 80 |         #if DEBUG
 81 |         // we only emit these warnings in debug mode
 82 |         let logging = TestLogging()
 83 |         var handler = LogHandlerThatDidImplementMetadataProviders(testLogging: logging)
 84 | 
 85 |         handler.metadataProvider = .simpleTestProvider
 86 | 
 87 |         logging.history.assertNotExist(
 88 |             level: .warning,
 89 |             message:
 90 |                 "Attempted to set metadataProvider on LogHandlerThatDidImplementMetadataProviders that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.",
 91 |             source: "Logging"
 92 |         )
 93 |         #endif
 94 |     }
 95 | }
 96 | 
 97 | extension Logger.MetadataProvider {
 98 |     static var simpleTestProvider: Logger.MetadataProvider {
 99 |         Logger.MetadataProvider {
100 |             ["test": "provided"]
101 |         }
102 |     }
103 | }
104 | 
105 | public struct LogHandlerThatDidNotImplementMetadataProviders: LogHandler {
106 |     let testLogging: TestLogging
107 |     init(testLogging: TestLogging) {
108 |         self.testLogging = testLogging
109 |     }
110 | 
111 |     public subscript(metadataKey _: String) -> Logging.Logger.Metadata.Value? {
112 |         get {
113 |             nil
114 |         }
115 |         set(newValue) {
116 |             // ignore
117 |         }
118 |     }
119 | 
120 |     public var metadata: Logging.Logger.Metadata = [:]
121 | 
122 |     public var logLevel: Logging.Logger.Level = .trace
123 | 
124 |     public func log(
125 |         level: Logger.Level,
126 |         message: Logger.Message,
127 |         metadata: Logger.Metadata?,
128 |         source: String,
129 |         file: String,
130 |         function: String,
131 |         line: UInt
132 |     ) {
133 |         self.testLogging.make(label: "fake").log(
134 |             level: level,
135 |             message: message,
136 |             metadata: metadata,
137 |             source: source,
138 |             file: file,
139 |             function: function,
140 |             line: line
141 |         )
142 |     }
143 | }
144 | 
145 | public struct LogHandlerThatDidImplementMetadataProviders: LogHandler {
146 |     let testLogging: TestLogging
147 |     init(testLogging: TestLogging) {
148 |         self.testLogging = testLogging
149 |     }
150 | 
151 |     public subscript(metadataKey _: String) -> Logging.Logger.Metadata.Value? {
152 |         get {
153 |             nil
154 |         }
155 |         set(newValue) {
156 |             // ignore
157 |         }
158 |     }
159 | 
160 |     public var metadata: Logging.Logger.Metadata = [:]
161 | 
162 |     public var logLevel: Logging.Logger.Level = .trace
163 | 
164 |     public var metadataProvider: Logger.MetadataProvider?
165 | 
166 |     public func log(
167 |         level: Logger.Level,
168 |         message: Logger.Message,
169 |         metadata: Logger.Metadata?,
170 |         source: String,
171 |         file: String,
172 |         function: String,
173 |         line: UInt
174 |     ) {
175 |         self.testLogging.make(label: "fake").log(
176 |             level: level,
177 |             message: message,
178 |             metadata: metadata,
179 |             source: source,
180 |             file: file,
181 |             function: function,
182 |             line: line
183 |         )
184 |     }
185 | }
186 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/SubDirectoryOfLoggingTests/EmitALogFromSubDirectory.swift:
--------------------------------------------------------------------------------
 1 | //===----------------------------------------------------------------------===//
 2 | //
 3 | // This source file is part of the Swift Logging API open source project
 4 | //
 5 | // Copyright (c) 2021 Apple Inc. and the Swift Logging API project authors
 6 | // Licensed under Apache License v2.0
 7 | //
 8 | // See LICENSE.txt for license information
 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 | 
15 | import Logging
16 | 
17 | internal func emitLogMessage(_ message: Logger.Message, to logger: Logger) {
18 |     logger.trace(message)
19 |     logger.debug(message)
20 |     logger.info(message)
21 |     logger.notice(message)
22 |     logger.warning(message)
23 |     logger.error(message)
24 |     logger.critical(message)
25 | }
26 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/TestLogger.swift:
--------------------------------------------------------------------------------
  1 | //===----------------------------------------------------------------------===//
  2 | //
  3 | // This source file is part of the Swift Logging API open source project
  4 | //
  5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
  6 | // Licensed under Apache License v2.0
  7 | //
  8 | // See LICENSE.txt for license information
  9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
 10 | //
 11 | // SPDX-License-Identifier: Apache-2.0
 12 | //
 13 | //===----------------------------------------------------------------------===//
 14 | 
 15 | import Foundation
 16 | import XCTest
 17 | 
 18 | @testable import Logging
 19 | 
 20 | #if os(Windows)
 21 | import WinSDK
 22 | #endif
 23 | #if compiler(>=6.0) || canImport(Darwin)
 24 | import Dispatch
 25 | #else
 26 | @preconcurrency import Dispatch
 27 | #endif
 28 | 
 29 | internal struct TestLogging {
 30 |     private let _config = Config()  // shared among loggers
 31 |     private let recorder = Recorder()  // shared among loggers
 32 | 
 33 |     func make(label: String) -> some LogHandler {
 34 |         TestLogHandler(
 35 |             label: label,
 36 |             config: self.config,
 37 |             recorder: self.recorder,
 38 |             metadataProvider: LoggingSystem.metadataProvider
 39 |         )
 40 |     }
 41 | 
 42 |     func makeWithMetadataProvider(label: String, metadataProvider: Logger.MetadataProvider?) -> (some LogHandler) {
 43 |         TestLogHandler(
 44 |             label: label,
 45 |             config: self.config,
 46 |             recorder: self.recorder,
 47 |             metadataProvider: metadataProvider
 48 |         )
 49 |     }
 50 | 
 51 |     var config: Config { self._config }
 52 |     var history: some History { self.recorder }
 53 | }
 54 | 
 55 | internal struct TestLogHandler: LogHandler {
 56 |     private let recorder: Recorder
 57 |     private let config: Config
 58 |     private var logger: Logger  // the actual logger
 59 | 
 60 |     let label: String
 61 |     public var metadataProvider: Logger.MetadataProvider?
 62 | 
 63 |     init(label: String, config: Config, recorder: Recorder, metadataProvider: Logger.MetadataProvider?) {
 64 |         self.label = label
 65 |         self.config = config
 66 |         self.recorder = recorder
 67 |         self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label))
 68 |         self.logger.logLevel = .debug
 69 |         self.metadataProvider = metadataProvider
 70 |     }
 71 | 
 72 |     init(label: String, config: Config, recorder: Recorder) {
 73 |         self.label = label
 74 |         self.config = config
 75 |         self.recorder = recorder
 76 |         self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label))
 77 |         self.logger.logLevel = .debug
 78 |         self.metadataProvider = LoggingSystem.metadataProvider
 79 |     }
 80 | 
 81 |     func log(
 82 |         level: Logger.Level,
 83 |         message: Logger.Message,
 84 |         metadata explicitMetadata: Logger.Metadata?,
 85 |         source: String,
 86 |         file: String,
 87 |         function: String,
 88 |         line: UInt
 89 |     ) {
 90 |         // baseline metadata, that was set on handler:
 91 |         var metadata = self._metadataSet ? self.metadata : MDC.global.metadata
 92 |         // contextual metadata, e.g. from task-locals:
 93 |         let contextualMetadata = self.metadataProvider?.get() ?? [:]
 94 |         if !contextualMetadata.isEmpty {
 95 |             metadata.merge(contextualMetadata, uniquingKeysWith: { _, contextual in contextual })
 96 |         }
 97 |         // override using any explicit metadata passed for this log statement:
 98 |         if let explicitMetadata = explicitMetadata {
 99 |             metadata.merge(explicitMetadata, uniquingKeysWith: { _, explicit in explicit })
100 |         }
101 | 
102 |         self.logger.log(
103 |             level: level,
104 |             message,
105 |             metadata: metadata,
106 |             source: source,
107 |             file: file,
108 |             function: function,
109 |             line: line
110 |         )
111 |         self.recorder.record(level: level, metadata: metadata, message: message, source: source)
112 |     }
113 | 
114 |     private var _logLevel: Logger.Level?
115 |     var logLevel: Logger.Level {
116 |         get {
117 |             // get from config unless set
118 |             self._logLevel ?? self.config.get(key: self.label)
119 |         }
120 |         set {
121 |             self._logLevel = newValue
122 |         }
123 |     }
124 | 
125 |     private var _metadataSet = false
126 |     private var _metadata = Logger.Metadata() {
127 |         didSet {
128 |             self._metadataSet = true
129 |         }
130 |     }
131 | 
132 |     public var metadata: Logger.Metadata {
133 |         get {
134 |             self._metadata
135 |         }
136 |         set {
137 |             self._metadata = newValue
138 |         }
139 |     }
140 | 
141 |     // TODO: would be nice to delegate to local copy of logger but StdoutLogger is a reference type. why?
142 |     subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? {
143 |         get {
144 |             self._metadata[metadataKey]
145 |         }
146 |         set {
147 |             self._metadata[metadataKey] = newValue
148 |         }
149 |     }
150 | }
151 | 
152 | internal class Config {
153 |     private static let ALL = "*"
154 | 
155 |     private let lock = NSLock()
156 |     private var storage = [String: Logger.Level]()
157 | 
158 |     func get(key: String) -> Logger.Level {
159 |         self.get(key) ?? self.get(Config.ALL) ?? Logger.Level.debug
160 |     }
161 | 
162 |     func get(_ key: String) -> Logger.Level? {
163 |         guard let value = (self.lock.withLock { self.storage[key] }) else {
164 |             return nil
165 |         }
166 |         return value
167 |     }
168 | 
169 |     func set(key: String = Config.ALL, value: Logger.Level) {
170 |         self.lock.withLock { self.storage[key] = value }
171 |     }
172 | 
173 |     func clear() {
174 |         self.lock.withLock { self.storage.removeAll() }
175 |     }
176 | }
177 | 
178 | internal class Recorder: History {
179 |     private let lock = NSLock()
180 |     private var _entries = [LogEntry]()
181 | 
182 |     func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, source: String) {
183 |         self.lock.withLock {
184 |             self._entries.append(
185 |                 LogEntry(level: level, metadata: metadata, message: message.description, source: source)
186 |             )
187 |         }
188 |     }
189 | 
190 |     var entries: [LogEntry] {
191 |         self.lock.withLock { self._entries }
192 |     }
193 | }
194 | 
195 | internal protocol History {
196 |     var entries: [LogEntry] { get }
197 | }
198 | 
199 | extension History {
200 |     func atLevel(level: Logger.Level) -> [LogEntry] {
201 |         self.entries.filter { entry in
202 |             level == entry.level
203 |         }
204 |     }
205 | 
206 |     var trace: [LogEntry] {
207 |         self.atLevel(level: .debug)
208 |     }
209 | 
210 |     var debug: [LogEntry] {
211 |         self.atLevel(level: .debug)
212 |     }
213 | 
214 |     var info: [LogEntry] {
215 |         self.atLevel(level: .info)
216 |     }
217 | 
218 |     var warning: [LogEntry] {
219 |         self.atLevel(level: .warning)
220 |     }
221 | 
222 |     var error: [LogEntry] {
223 |         self.atLevel(level: .error)
224 |     }
225 | }
226 | 
227 | internal struct LogEntry {
228 |     let level: Logger.Level
229 |     let metadata: Logger.Metadata?
230 |     let message: String
231 |     let source: String
232 | }
233 | 
234 | extension History {
235 |     func assertExist(
236 |         level: Logger.Level,
237 |         message: String,
238 |         metadata: Logger.Metadata? = nil,
239 |         source: String? = nil,
240 |         file: StaticString = #filePath,
241 |         fileID: String = #fileID,
242 |         line: UInt = #line
243 |     ) {
244 |         let source = source ?? Logger.currentModule(fileID: "\(fileID)")
245 |         let entry = self.find(level: level, message: message, metadata: metadata, source: source)
246 |         XCTAssertNotNil(
247 |             entry,
248 |             "entry not found: \(level), \(source), \(String(describing: metadata)), \(message)",
249 |             file: file,
250 |             line: line
251 |         )
252 |     }
253 | 
254 |     func assertNotExist(
255 |         level: Logger.Level,
256 |         message: String,
257 |         metadata: Logger.Metadata? = nil,
258 |         source: String? = nil,
259 |         file: StaticString = #filePath,
260 |         fileID: String = #file,
261 |         line: UInt = #line
262 |     ) {
263 |         let source = source ?? Logger.currentModule(fileID: "\(fileID)")
264 |         let entry = self.find(level: level, message: message, metadata: metadata, source: source)
265 |         XCTAssertNil(
266 |             entry,
267 |             "entry was found: \(level), \(source), \(String(describing: metadata)), \(message)",
268 |             file: file,
269 |             line: line
270 |         )
271 |     }
272 | 
273 |     func find(level: Logger.Level, message: String, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? {
274 |         self.entries.first { entry in
275 |             if entry.level != level {
276 |                 return false
277 |             }
278 |             if entry.message != message {
279 |                 return false
280 |             }
281 |             if let lhs = entry.metadata, let rhs = metadata {
282 |                 if lhs.count != rhs.count {
283 |                     return false
284 |                 }
285 | 
286 |                 for lk in lhs.keys {
287 |                     if lhs[lk] != rhs[lk] {
288 |                         return false
289 |                     }
290 |                 }
291 | 
292 |                 for rk in rhs.keys {
293 |                     if lhs[rk] != rhs[rk] {
294 |                         return false
295 |                     }
296 |                 }
297 | 
298 |                 return true
299 |             }
300 |             if entry.source != source {
301 |                 return false
302 |             }
303 | 
304 |             return true
305 |         }
306 |     }
307 | }
308 | 
309 | public class MDC {
310 |     private let lock = NSLock()
311 |     private var storage = [Int: Logger.Metadata]()
312 | 
313 |     public static let global = MDC()
314 | 
315 |     private init() {}
316 | 
317 |     public subscript(metadataKey: String) -> Logger.Metadata.Value? {
318 |         get {
319 |             self.lock.withLock {
320 |                 self.storage[self.threadId]?[metadataKey]
321 |             }
322 |         }
323 |         set {
324 |             self.lock.withLock {
325 |                 if self.storage[self.threadId] == nil {
326 |                     self.storage[self.threadId] = Logger.Metadata()
327 |                 }
328 |                 self.storage[self.threadId]![metadataKey] = newValue
329 |             }
330 |         }
331 |     }
332 | 
333 |     public var metadata: Logger.Metadata {
334 |         self.lock.withLock {
335 |             self.storage[self.threadId] ?? [:]
336 |         }
337 |     }
338 | 
339 |     public func clear() {
340 |         self.lock.withLock {
341 |             _ = self.storage.removeValue(forKey: self.threadId)
342 |         }
343 |     }
344 | 
345 |     public func with(metadata: Logger.Metadata, _ body: () throws -> Void) rethrows {
346 |         for (key, value) in metadata {
347 |             self[key] = value
348 |         }
349 |         defer {
350 |             for (key, _) in metadata {
351 |                 self[key] = nil
352 |             }
353 |         }
354 |         try body()
355 |     }
356 | 
357 |     public func with<T>(metadata: Logger.Metadata, _ body: () throws -> T) rethrows -> T {
358 |         for (key, value) in metadata {
359 |             self[key] = value
360 |         }
361 |         defer {
362 |             for (key, _) in metadata {
363 |                 self[key] = nil
364 |             }
365 |         }
366 |         return try body()
367 |     }
368 | 
369 |     // for testing
370 |     internal func flush() {
371 |         self.lock.withLock {
372 |             self.storage.removeAll()
373 |         }
374 |     }
375 | 
376 |     private var threadId: Int {
377 |         #if canImport(Darwin)
378 |         return Int(pthread_mach_thread_np(pthread_self()))
379 |         #elseif os(Windows)
380 |         return Int(GetCurrentThreadId())
381 |         #else
382 |         return Int(pthread_self())
383 |         #endif
384 |     }
385 | }
386 | 
387 | extension NSLock {
388 |     func withLock<T>(_ body: () -> T) -> T {
389 |         self.lock()
390 |         defer {
391 |             self.unlock()
392 |         }
393 |         return body()
394 |     }
395 | }
396 | 
397 | internal struct TestLibrary: Sendable {
398 |     private let logger = Logger(label: "TestLibrary")
399 |     private let queue = DispatchQueue(label: "TestLibrary")
400 | 
401 |     public init() {}
402 | 
403 |     public func doSomething() {
404 |         self.logger.info("TestLibrary::doSomething")
405 |     }
406 | 
407 |     public func doSomethingAsync(completion: @escaping @Sendable () -> Void) {
408 |         // libraries that use global loggers and async, need to make sure they propagate the
409 |         // logging metadata when creating a new thread
410 |         let metadata = MDC.global.metadata
411 |         self.queue.asyncAfter(deadline: .now() + 0.1) {
412 |             MDC.global.with(metadata: metadata) {
413 |                 self.logger.info("TestLibrary::doSomethingAsync")
414 |                 completion()
415 |             }
416 |         }
417 |     }
418 | }
419 | 
420 | // Sendable
421 | 
422 | extension TestLogHandler: @unchecked Sendable {}
423 | extension Recorder: @unchecked Sendable {}
424 | extension Config: @unchecked Sendable {}
425 | extension MDC: @unchecked Sendable {}
426 | 


--------------------------------------------------------------------------------
/Tests/LoggingTests/TestSendable.swift:
--------------------------------------------------------------------------------
 1 | //===----------------------------------------------------------------------===//
 2 | //
 3 | // This source file is part of the Swift Logging API open source project
 4 | //
 5 | // Copyright (c) 2022 Apple Inc. and the Swift Logging API project authors
 6 | // Licensed under Apache License v2.0
 7 | //
 8 | // See LICENSE.txt for license information
 9 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 | 
15 | import XCTest
16 | 
17 | @testable import Logging
18 | 
19 | class SendableTest: XCTestCase {
20 |     func testSendableLogger() async {
21 |         let testLogging = TestLogging()
22 |         LoggingSystem.bootstrapInternal { testLogging.make(label: $0) }
23 | 
24 |         let logger = Logger(label: "test")
25 |         let message1 = Logger.Message(stringLiteral: "critical 1")
26 |         let message2 = Logger.Message(stringLiteral: "critical 2")
27 |         let metadata: Logger.Metadata = ["key": "value"]
28 | 
29 |         let task = Task.detached {
30 |             logger.info("info")
31 |             logger.critical(message1)
32 |             logger.critical(message2)
33 |             logger.warning(.init(stringLiteral: "warning"), metadata: metadata)
34 |         }
35 | 
36 |         await task.value
37 |         testLogging.history.assertExist(level: .info, message: "info", metadata: [:])
38 |         testLogging.history.assertExist(level: .critical, message: "critical 1", metadata: [:])
39 |         testLogging.history.assertExist(level: .critical, message: "critical 2", metadata: [:])
40 |         testLogging.history.assertExist(level: .warning, message: "warning", metadata: metadata)
41 |     }
42 | }
43 | 


--------------------------------------------------------------------------------
/dev/git.commit.template:
--------------------------------------------------------------------------------
 1 | One line description of your change
 2 | 
 3 | Motivation:
 4 | 
 5 | Explain here the context, and why you're making that change.
 6 | What is the problem you're trying to solve.
 7 | 
 8 | Modifications:
 9 | 
10 | Describe the modifications you've done.
11 | 
12 | Result:
13 | 
14 | After your change, what will change.
15 | 


--------------------------------------------------------------------------------
/proposals/0001-metadata-providers.md:
--------------------------------------------------------------------------------
  1 | # Metadata Providers
  2 | 
  3 | Authors: [Moritz Lang](https://github.com/slashmo), [Konrad 'ktoso' Malawski](https://github.com/ktoso)
  4 | 
  5 | ## Introduction
  6 | 
  7 | While global metadata attributes may be manually set on a `LogHandler` level, there's currently no way of reliably providing contextual, automatically propagated, metadata when logging with swift-log.
  8 | 
  9 | ## Motivation
 10 | 
 11 | To benefit from tools such as [Distributed Tracing](https://github.com/apple/swift-distributed-tracing) it is necessary for libraries to make use of the trace information.
 12 | 
 13 | Most notably, loggers should participate in tracing by including some trace metadata (such as e.g. a `trace-id`) when logging, as it transparently enables developers to benefit from log correlation using those IDs.
 14 | 
 15 | Today, the only supported way of providing such metadata is to pass them along to each log call explicitly:
 16 | 
 17 | ```swift
 18 | logger.info("first this ...", metadata: ["trace-id": MyTracingLibrary.currentTraceID])
 19 | logger.info("... now this", metadata: ["trace-id": MyTracingLibrary.currentTraceID])
 20 | ```
 21 | 
 22 | This comes with a couple of downsides:
 23 | 
 24 | ### Error-prone and repetitive
 25 | 
 26 | It's easy to forget passing this metadata to _all_ log statements, resulting in an inconsistent debugging experience as these log statements cannot be found using correlation IDs.
 27 | 
 28 | The repetitiveness and verboseness of logging multiple metadata in-line quickly becomes annoying and vastly decreases the signal-to-noise ratio of Swift code trying to be a good citizen and making use of log correlation techniques such as distributed tracing.
 29 | 
 30 | ### Impossible to implement for libraries
 31 | 
 32 | A large portion of logs are not generated by the end-user but by libraries such as AsyncHTTPClient, Vapor etc.
 33 | These libraries, by design, don't know about the specific metadata keys that should be included in the logs.
 34 | Those keys are after all runtime dependent, and may change depending on what tracing system is configured by the end user.
 35 | 
 36 | For example, a specific `Tracer` implementation would use a type representing a trace ID, and has a way of extracting
 37 | that trace ID from a [Swift Distributed Tracing Baggage](https://github.com/apple/swift-distributed-tracing-baggage). But other libraries can't know about this _specific_ trace ID and therefore would not be
 38 | able to pass such values along to their log statements.
 39 | 
 40 | ## Proposed solution
 41 | 
 42 | To support this kind of runtime-generated metadata in `swift-log`, we need to extend the logging APIs in an open-ended way, to allow any kinds of metadata to be provided from the asynchronous context (i.e. task local values).
 43 | 
 44 | We also have a desire to keep `swift-log` a "zero dependencies" library, as it intends only to focus on describing a logging API, and not incur any additional dependencies, so it can be used in the most minimal of projects.
 45 | 
 46 | To solve this, we propose the extension of swift-log APIs with a new concept: _metadata providers_.
 47 | 
 48 | `MetadataProvider` is a struct nested in the `Logger` type, sitting alongside `MetadataValue` and the `Metadata`. 
 49 | Its purpose is to _provide_ `Logger.Metadata` from the asynchronous context, by e.g. looking up various task-local values,
 50 | and converting them into `Logger.Metadata`. This is performed by calling the `get()` function, like so:
 51 | 
 52 | ```swift
 53 | extension Logger {
 54 |     public struct MetadataProvider: Sendable {
 55 |         // ... 
 56 |         public init(_ provideMetadata: @escaping @Sendable () -> Metadata)
 57 |     
 58 |         public get() -> Metadata
 59 |     }
 60 | }
 61 | ```
 62 | 
 63 | ### Defining a `MetadataProvider`
 64 | 
 65 | While `MetadataProvider`s can be created in an ad-hoc fashion, the struct may be used as a namespace
 66 | to define providers in. 
 67 | 
 68 | For example, a `MyTracer` implementation of swift-distributed-tracing would be expected to provide a metadata provider
 69 | that is aware of its own `Baggage` and specific keys it uses to carry the trace and span identifiers, like this:
 70 | 
 71 | ```swift
 72 | import Tracing // swift-distributed-tracing
 73 | 
 74 | extension Logger.MetadataProvider {
 75 |     static let myTracer = Logger.MetadataProvider {
 76 |         guard let baggage = Baggage.current else {
 77 |             return [:]
 78 |         }
 79 |         guard let spanContext = baggage.spanContext else { 
 80 |             return [:]
 81 |         }
 82 |         
 83 |         return [
 84 |           "traceID": "\(spanContext.traceID)",
 85 |           "spanID": "\(spanContext.spanID)",
 86 |         ]
 87 |     }
 88 | }
 89 | ```
 90 | 
 91 | ### Using a `MetadataProvider`
 92 | 
 93 | A `MetadataProvider` can be set up either globally, on a boot-strapped logging system:
 94 | 
 95 | ```swift
 96 | LoggingSystem.bootstrap(
 97 |     metadataProvider: .myTracer,
 98 |     StreamLogHandler.standardOutput
 99 | )
100 | ```
101 | 
102 | or, using the metadata provider specific bootstrap method:
103 | 
104 | ```swift
105 | LoggingSystem.bootstrapMetadataProvider(.myTracer)
106 | ```
107 | 
108 | It is also possible to configure a metadata provider on a per-logger basis, like this:
109 | 
110 | ```swift
111 | let logger = Logger(label: "example", metadataProvider: Logger.MetadataProvider { 
112 |     guard let baggage = Baggage.current else {
113 |         return [:]
114 |     }
115 |     guard let operationID = baggage.operationID else { 
116 |         return nil
117 |     }
118 |     return ["extra/opID": "\(opID)"]
119 | })
120 | ```
121 | 
122 | which _overrides_ the default bootstrapped metadata provider.
123 | 
124 | > NOTE: Setting the metadata provider on the logger directly means the `LoggingSystem` metadata provider
125 | > is skipped (if defined), following how an explicitly passed handler `factory`
126 | > overrides the `LoggingSystem`s `factory`.
127 | 
128 | Once a metadata provider was set up, when a log statement is about to be emitted, the log handler implementation shall
129 | invoke it whenever it is about to emit a log statement. This must be done from the same asynchronous context as the log statement
130 | was made; I.e. if a log handler were to asynchronously–in a _detached_ task–create the actual log message, the invocation of
131 | the metadata provider still _must_ be performed before passing the data over to the other detached task.
132 | 
133 | Usually, metadata providers will reach for the task's Swift Distributed Tracing [`Baggage`](https://github.com/apple/swift-distributed-tracing-baggage) which is the mechanism that swift-distributed-tracing 
134 | and instrumentation libraries use to propagate metadata across asynchronous, as well as process, boundaries. Handlers 
135 | may also inspect other task-local values, however they should not expect other libraries to be able to propagage those
136 | as well as they will propagate `Baggage` values.
137 | 
138 | Those metadata will then be included in the log statement, e.g. like this:
139 | 
140 | ```swift
141 | var baggage = Baggage.topLevel
142 | baggage.spanContext = SpanContext()
143 | Baggage.withValue(baggage) {
144 |     test()
145 | }
146 | 
147 | func test() {
148 |     log.info("Test", metadata: ["oneOff": "42"])
149 |     // info [traceID: abc, spanID: 123, onOff: 42] Test
150 | }
151 | ```
152 | 
153 | ### Multiple `MetadataProvider`s using `MetadataProvider.multiplex(_:)`
154 | 
155 | Borrowing the concept from log handlers, metadata providers also have a multiplexing implementation.
156 | It is defined as an extension on `MetadataProvider` and is useful in cases where users want to utilize
157 | more than one metadata provider at the same time:
158 | 
159 | ```swift
160 | extension Logger.MetadataProvider {
161 |     public static func multiplex(_ providers: [Logger.MetadataProvider]) -> Logger.MetadataProvider {
162 |         precondition(!providers.isEmpty, "providers MUST NOT be empty")
163 |         return Logger.MetadataProvider { baggage in
164 |             providers.reduce(into: nil) { metadata, provider in
165 |                 if let providedMetadata = provider.metadata(baggage) {
166 |                     if metadata != nil {
167 |                         metadata!.merge(providedMetadata, uniquingKeysWith: { _, rhs in rhs })
168 |                     } else {
169 |                         metadata = providedMetadata
170 |                     }
171 |                 }
172 |             }
173 |         }
174 |     }
175 | }
176 | ```
177 | 
178 | Metadata keys are unique, so in case multiple metadata providers return the same key,
179 | the last provider in the array _wins_ and provides the value.
180 | 
181 | Note that it is possible to query the `LoggingSystem` for the configured, system-wide, metadata provider,
182 | and combine it using a `multiplex` provider if the system-wide provider should not be replaced, but augmented
183 | by additional providers.
184 | 
185 | ### Understanding Swift Distributed Tracing `Baggage`
186 | 
187 | The `Baggage` type is more than just a fancy type-safe container for your values which are meant to be carried across
188 | using a single, well-known, [task-local value](https://developer.apple.com/documentation/swift/tasklocal).
189 | 
190 | Values stored in `Baggage` are intended to be carried _across process boundaries_, and e.g. libraries such as HTTP clients,
191 | or other RPC mechanisms, or even messaging systems implement distributed tracing [instruments](https://github.com/apple/swift-distributed-tracing/blob/main/Sources/Instrumentation/Instrument.swift) 
192 | which `inject` and `extract` baggage values into their respective carrier objects, e.g. an `HTTPRequest` in the case of an HTTP client.
193 | 
194 | In other words, whenever intending to propagate information _across processes_ utilize the `Baggage` type to carry it, 
195 | and ensure to provide an `Instrument` that is able to inject/extract the values of interest into the carrier types you are interested in.
196 | To learn more about baggage and instrumentation, refer to the [Swift Distributed Tracing](https://github.com/apple/swift-distributed-tracing/) library documentation.
197 | 
198 | #### When to use `Baggage` vs. `Logger[metadataKey:]`
199 | 
200 | While `Baggage` is context-dependent and changes depending on where the log methods are being called from, the metadata set on a `Logger` is static and not context-dependent. E.g, if you wanted to add things like an instance ID or a subsystem name to a `Logger`, that could be seen as static information and set via `Logger[metadataKey:]`.
201 | 
202 | ```swift
203 | var logger = Logger(label: "org.swift.my-service")
204 | logger[metadataKey: "instanceId"] = "123"
205 | 
206 | logger.info("Service started.")
207 | // [instanceId: 123] Service started.
208 | ```
209 | 
210 | On the other hand, things like a trace ID are dynamic and context-dependent, therefore would be obtained via `Baggage`:
211 | 
212 | ```swift
213 | logger.info("Product fetched.")
214 | // [traceId: 42] Product fetched.
215 | ```
216 | 
217 | Inline-metadata is suitable for one-offs such as a `productId` or a `paymentMethod` in an online store service, but are not enough to corralate the following log statements, i.e. tying them both to the same request:
218 | 
219 | ```swift
220 | logger.info("Product fetched.", metadata: ["productId": "42"])
221 | logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])
222 | 
223 | // [productId: 42] Product fetched.
224 | // [paymentMethod: apple-pay] Product fetched.
225 | ```
226 | 
227 | If there was a `Baggage` value with a trace ID surrounding these log statements, they would be automatically correlatable:
228 | 
229 | ```swift
230 | var baggage = Baggage.topLevel
231 | baggage.traceID = 42
232 | Baggage.$current.withValue(baggage) {
233 |     logger.info("Product fetched.", metadata: ["productId": "42"])
234 |     logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])
235 | }
236 | 
237 | // [trace-id: 42, productId: 42] Product fetched.
238 | // [trace-id: 42, paymentMethod: apple-pay] Product fetched.
239 | ```
240 | 
241 | ## Alternatives considered
242 | 
243 | ### MetadataProviders as a function of `Baggage -> Metadata`
244 | 
245 | This was considered and fully developed, however it would cause swift-log to have a dependency on the instrumentation type `Baggage`, 
246 | which was seen as in-conflict-with the interest of swift-log remaining a zero-dependency library.
247 | 
248 | `Baggage` _is_ the well known type that is intended to be used for any kind of distributed systems instrumentation and tracing,
249 | however it adds additional complexity and confusion for users who are not interested in this domain. For example, developers
250 | may be confused about why `Baggage` and `Logger.Metadata` look somewhat similar, but behave very differently. This complexity
251 | is inherent to the two types actually being _very_ different, however we do not want to overwhelm newcomers or developers
252 | who are only intending to use swift-log within process. Such developers do not need to care about the added complexities 
253 | of distributed systems.
254 | 
255 | The tradeoff we take here is that every metadata provider will have to perform its own task-local (and therefore also thread-local),
256 | access in order to obtain the `Baggage` value, rather than the lookup being able to be performed _once_ and shared between 
257 | multiple providers when a multiplex provider was configured in the system. We view this tradeoff as worth taking, as the cost
258 | of actually formatting the metadata usually strongly dominates the cost of the task-local lookup. 
259 | 
260 | ### Removing `LogHandler.Metadata` in favor of `Baggage`
261 | 
262 | Removing logger metadata is not a good option because it serves a slightly different style of metadata than the baggage.
263 | 
264 | Baggage is intended for contextual, task-local, metadata which is _carried across multiple libraries_. The values stored in baggage are well-typed, and must declare keys for accessing values in a baggage, this works well for multiple pieces of an application needing to reach for specific baggage items: everyone aware of the `traceID` key, is able to query the baggage for this key (`baggage.traceID`) and obtain a well-typed value. This comes with a higher cost on declaring keys though, as well as a global namespace for those - which is desirable for such kinds of metadata.
265 | 
266 | This is not the same usage pattern as emitting a plain structured log where we'd like to include the name of an item we just queried:
267 | 
268 | ```swift
269 | log.info("Obtained item! Hooray!", metadata: ["item": "\(item)"])
270 | ```
271 | 
272 | In this example, the key/value pair for `"item"` is pretty ad-hoc, and we never need to refer to it elsewhere in the program. It never is queried by other pieces of the application, nor would it be useful to set it in baggage metadata, as the only purpose of the `item` key here is to log, and forget about it.
273 | 
274 | ### Explicitly passing `Baggage` always
275 | 
276 | Baggage is designed for use cases like distributed tracing, or similar instrumentation patterns where "all" participating code may need to reach for it.
277 | 
278 | Specifically in logging, this means that _every_ call site for _every_ log statement would have to pass it explicitly resulting in annoying noisy code:
279 | 
280 | ```swift
281 | class StoresRepository {
282 |     func store(byID id: String, eventLoop: EventLoop, logger: Logger, baggage: Baggage) async throws -> Store {
283 |         InstrumentationSystem.tracer.withSpan("Fetch Store") { span in
284 |             logger.info("Fetched store.", baggage: span.baggage)
285 |         }
286 |     }
287 | }
288 | 
289 | try await storesRepository.store(
290 |     byID: storeID,
291 |     eventLoop: eventLoop,
292 |     logger: logger,
293 |     baggage: baggage
294 | )
295 | ```
296 | 
297 | ### Explicitly passing `Logger` always
298 | 
299 | Imagine we don't have metadata providers, we'd have to manually set trace IDs on loggers which doesn't really work as all libraries involved would need to know about the same specific trace ID. Even if we inverted the dependency to have `Tracing` depend on `Logging` so that we'd be able to define something like "Tracing, please populate this logger with metadata", we'd have to make sure this thing is called in all places to avoid dropping contextual metadata.
300 | 
301 | ```swift
302 | import Tracing
303 | import Logging
304 | 
305 | let contextualLogger = InstrumentationSystem.tracer.populateTraceMetadata(logger)
306 | contextualLogger.info("Request received.")
307 | ```
308 | 
309 | ### Tracing providing extensions on Logger
310 | 
311 | Instead of having `swift-log` depend on `swift-distributed-tracing-baggage` we could also create extensions for `Logger` inside `swift-distributed-tracing` and have users call these new overloaded methods instead:
312 | 
313 | ```swift
314 | extension Logger {
315 |     func tinfo(_ message: ..., baggage: Baggage?) {
316 |         // ...
317 |     }
318 | }
319 | ```
320 | 
321 | Such extensions could work like the currently-proposed APIs, but the risk of users calling the wrong methods is incredibly high and we cannot overload the existing methods' signatures because of ambiguity of call-sides without explicit Baggage being passed:
322 | 
323 | ```swift
324 | logger.info("Hello")
325 | // we want this to pick up Baggage, but the signature would be ambiguous
326 | ```
327 | 
328 | Also, this extension-based contextual metadata would require basically everyone in Server-side Swift to adapt their usage of `Logging` to use these extensions instead. With the proposed APIs, we'd only need to modify `Logging` and any NIO-based libraries such as `AsyncHTTPClient`, `Vapor`, etc. and not every single log statement in every application.
329 | 


--------------------------------------------------------------------------------