File: README.md
SwiftLog
This repository contains a logging API implementation for Swift.
SwiftLog provides a unified, performant, and ergonomic logging API that can be
adopted by libraries and applications across the Swift ecosystem.
- 📚 Documentation and tutorials are available on the Swift Package Index
- 🚀 Contributions are welcome, please see CONTRIBUTING.md
- 🪪 License is Apache 2.0, repeated in LICENSE.txt
- 🔒 Security issues should be reported via the process in Security
- 🔀 Available Logging Backends: SwiftLog is an API package - you'll want to
choose from the many
community-maintained logging backends for production use
Quick Start
The following snippet shows how to add SwiftLog to your Swift Package:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "YourApp",
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.6.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "Logging", package: "swift-log")
]
)
]
)Then start logging:
import Logging
// Create a logger
let logger = Logger(label: "com.example.YourApp")
// Log at different levels
logger.info("Application started")
logger.warning("This is a warning")
logger.error("Something went wrong", metadata: ["error": "\(error)"])
// Add metadata for context
var requestLogger = logger
requestLogger[metadataKey: "request-id"] = "\(UUID())"
requestLogger.info("Processing request")Available log handler backends
The community has built numerous specialized logging backends.
A great way to discover available log backend implementations is searching the
Swift Package Index
for the swift-log keyword.
Building with CMake
While Swift Package Manager (SPM) is the primary build system for development and testing, CMake build support is also available for integrated or mixed build environments if needed. Note that tests are not included in the CMake build, they are managed through Swift Package Manager with swift test.
File: CMakeLists.txt
#===----------------------------------------------------------------------===#
This source file is part of the Swift Logging API open source project
Copyright (c) 2018-2026 Apple Inc. and the Swift Logging API project authors
Licensed under Apache License v2.0
See LICENSE.txt for license information
See CONTRIBUTORS.txt for the list of Swift Logging API project authors
SPDX-License-Identifier: Apache-2.0
#===----------------------------------------------------------------------===#
cmake_minimum_required(VERSION 3.19)
Initialize C language first to inspect the compiler environment
project(swift-log LANGUAGES C)
Set Swift compiler target from C compiler target if not manually set for Windows
if(WIN32 AND NOT CMAKE_Swift_COMPILER_TARGET)
if(CMAKE_C_COMPILER_ARCHITECTURE_ID MATCHES "(ARM64|arm64|aarch64)")
set(CMAKE_Swift_COMPILER_TARGET "aarch64-unknown-windows-msvc" CACHE STRING "Swift compiler target triple")
elseif(CMAKE_C_COMPILER_ARCHITECTURE_ID MATCHES "(X86|x64|AMD64|x86_64)")
set(CMAKE_Swift_COMPILER_TARGET "x86_64-unknown-windows-msvc" CACHE STRING "Swift compiler target triple")
endif()
endif()
Enable Swift after C defines the architecture
enable_language(Swift)
option(BUILD_SHARED_LIBS "Build shared libraries by default" YES)
set(CMAKE_Swift_LANGUAGE_VERSION 6)
if(CMAKE_Swift_COMPILER_VERSION VERSION_LESS "6.2")
message(FATAL_ERROR "swift-log requires Swift compiler version 6.2 or newer. Detected version: ${CMAKE_Swift_COMPILER_VERSION}")
endif()
set(SWIFT_LOG_COMMON_COMPILE_OPTIONS
"SHELL:-enable-upcoming-feature ExistentialAny"
"SHELL:-enable-upcoming-feature MemberImportVisibility"
"SHELL:-enable-upcoming-feature InternalImportsByDefault"
"SHELL:-enable-upcoming-feature NonisolatedNonsendingByDefault"
"SHELL:-Xfrontend -require-explicit-sendable"
"SHELL:-package-name swift_log")
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift)
if(MSVC)
message(STATUS "MSVC condition triggered successfully!")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$CONFIG:Debug:Debug>DLL")
set(CMAKE_Swift_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$CONFIG:Debug:Debug>DLL")
endif()
include(GNUInstallDirs)
include(SwiftSupport)
if(NOT APPLE)
find_package(dispatch QUIET)
find_package(Foundation QUIET)
endif()
add_subdirectory(Sources)
add_subdirectory(cmake/modules)
File: CONTRIBUTING.md
Legal
By submitting a pull request, you represent that you have the right to license
your contribution to Apple and the community, and agree by submitting the patch
that your contributions are licensed under the Apache 2.0 license (seeLICENSE.txt).
How to submit a bug report
Please ensure to specify the following:
- SwiftLog commit hash
- Contextual information (e.g. what you were trying to achieve with SwiftLog)
- Simplest possible steps to reproduce
- More complex the steps are, lower the priority will be.
- A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description.
- Anything that might be relevant in your opinion, such as:
- Swift version or the output of
swift --version - OS version and the output of
uname -a - Network configuration
- Swift version or the output of
Example
SwiftLog commit hash: 4fe877816ad82627602377f415b6a66850214824
Context:
While testing my application that uses with SwiftLog, I noticed that ...
Steps to reproduce:
1. ...
2. ...
3. ...
4. ...
$ swift --version
Swift version 4.0.2 (swift-4.0.2-RELEASE)
Target: x86_64-unknown-linux-gnu
Operating system: Ubuntu Linux 16.04 64-bit
$ uname -a
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
My system has IPv6 disabled.Make sure Tests work on Linux
SwiftLog uses Swift Testing to run tests on all supported platforms.
Run CI checks locally
You can run the Github Actions workflows locally using
act. To run all the jobs that run on a pull
request, use the following command:
% act pull_requestTo run just a single job, use workflow_call -j <job>, and specify the inputs
the job expects. For example, to run just shellcheck:
% act workflow_call -j soundness --input shell_check_enabled=trueTo bind-mount the working directory to the container, rather than a copy, use--bind. For example, to run just the formatting, and have the results
reflected in your working directory:
% act --bind workflow_call -j soundness --input format_check_enabled=trueIf you'd like act to always run with certain flags, these can be be placed in
an .actrc file either in the current working directory or your home
directory, for example:
--container-architecture=linux/amd64
--remote-name upstream
--action-offline-modeBenchmarks
Benchmarks for swift-log are in a separate Swift Package in the Benchmarks subfolder of this repository.
They use the benchmark plugin.
On Swift versions prior to 6.3 Benchmarks depends on the jemalloc memory allocation library, which is used by benchmark to capture memory allocation statistics.
An installation guide can be found in the Getting Started article of benchmark.
Afterwards you can run the benchmarks from CLI by going to the Benchmarks subfolder (e.g. cd Benchmarks) and invoking:
swift package benchmarkFor more information please refer to swift package benchmark --help or the documentation of benchmark.
How to contribute your work
For non-trivial changes that affect the public API, it is good practice to have a discussion phase before writing code. Please follow the proposal process to gather feedback and align on the approach with other contributors.
- Prepare your change, keeping in mind that a good patch is:
- Concise, and contains as few changes as needed to achieve the end result.
- Tested, ensuring that any tests provided failed before the patch and pass after it.
- Documented, adding API documentation as needed to cover new functions and properties.
- Accompanied by a great commit message.
- Open a pull request at https://github.com/apple/swift-log and wait for code review by the maintainers.
Automated release process
This repository uses automated releases based on semantic versioning labels. See the Auto Release Workflow documentation for details.
File: LICENSE.txt
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document."Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License."Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity."You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License."Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files."Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types."Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below)."Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof."Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution.""Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File: NOTICE.txt
The SwiftLog Project
========================
Please visit the SwiftLog web site for more information:
Copyright 2018, 2019 The SwiftLog Project
The SwiftLog Project licenses this file to you under the Apache License,
version 2.0 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Also, please refer to each LICENSE.
the 'license' directory of the distribution file, for the license terms of the
components that this product depends on.
This product contains a derivation of the lock implementation and various
scripts from SwiftNIO.
- LICENSE (Apache License 2.0):
- HOMEPAGE:
File: .spi.yml
version: 1
builder:
configs:
- documentation_targets: [Logging]
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.3/MaxLogLevelNone.critical_log_with_critical_log_level.p90.json
{
"instructions" : 734,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.3/MaxLogLevelNone.critical_log_with_critical_log_level_generic.p90.json
{
"instructions" : 734,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.3/MaxLogLevelNone.critical_log_with_critical_log_level_logLevel_set.p90.json
{
"instructions" : 734,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.3/MaxLogLevelNone.critical_log_with_critical_log_level_metadata_set.p90.json
{
"instructions" : 710,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.4/MaxLogLevelNone.critical_log_with_critical_log_level.p90.json
{
"instructions" : 810,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.4/MaxLogLevelNone.critical_log_with_critical_log_level_generic.p90.json
{
"instructions" : 803,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.4/MaxLogLevelNone.critical_log_with_critical_log_level_logLevel_set.p90.json
{
"instructions" : 803,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelNone/Thresholds/Xcode swift 6.4/MaxLogLevelNone.critical_log_with_critical_log_level_metadata_set.p90.json
{
"instructions" : 803,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.3/MaxLogLevelWarning.notice_log_with_notice_log_level.p90.json
{
"instructions" : 734,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.3/MaxLogLevelWarning.notice_log_with_notice_log_level_attributed_generic.p90.json
{
"instructions" : 734,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.3/MaxLogLevelWarning.notice_log_with_notice_log_level_generic.p90.json
{
"instructions" : 689,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.3/MaxLogLevelWarning.warning_log_with_warning_log_level.p90.json
{
"instructions" : 1993,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.3/MaxLogLevelWarning.warning_log_with_warning_log_level_generic.p90.json
{
"instructions" : 1968,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.4/MaxLogLevelWarning.notice_log_with_notice_log_level.p90.json
{
"instructions" : 815,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.4/MaxLogLevelWarning.notice_log_with_notice_log_level_attributed_generic.p90.json
{
"instructions" : 750,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.4/MaxLogLevelWarning.notice_log_with_notice_log_level_generic.p90.json
{
"instructions" : 775,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.4/MaxLogLevelWarning.warning_log_with_warning_log_level.p90.json
{
"instructions" : 2008,
"objectAllocCount" : 0
}
File: Benchmarks/MaxLogLevelWarning/Thresholds/Xcode swift 6.4/MaxLogLevelWarning.warning_log_with_warning_log_level_generic.p90.json
{
"instructions" : 2002,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_error_log_level_1_attribute.p90.json
{
"instructions" : 1050,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_error_log_level_2_attributes.p90.json
{
"instructions" : 1050,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_error_log_level_generic.p90.json
{
"instructions" : 1046,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_1.p90.json
{
"instructions" : 5483,
"objectAllocCount" : 1
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_1000.p90.json
{
"instructions" : 4939775,
"objectAllocCount" : 2016
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_20.p90.json
{
"instructions" : 82239,
"objectAllocCount" : 37
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_5.p90.json
{
"instructions" : 18351,
"objectAllocCount" : 6
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_1_attribute.p90.json
{
"instructions" : 6200,
"objectAllocCount" : 2
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_2_attributes.p90.json
{
"instructions" : 7600,
"objectAllocCount" : 3
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_current_read_fallback.p90.json
{
"instructions" : 1005,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_current_read_inside_scope.p90.json
{
"instructions" : 1017,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_generic.p90.json
{
"instructions" : 1966,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_withLogger_handler.p90.json
{
"instructions" : 4551,
"objectAllocCount" : 1
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.3/NoTraits.error_log_with_error_log_level_withLogger_mergingMetadata.p90.json
{
"instructions" : 9623,
"objectAllocCount" : 4
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_error_log_level_1_attribute.p90.json
{
"instructions" : 1119,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_error_log_level_2_attributes.p90.json
{
"instructions" : 1119,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_error_log_level_generic.p90.json
{
"instructions" : 1117,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_1.p90.json
{
"instructions" : 5327,
"objectAllocCount" : 1
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_1000.p90.json
{
"instructions" : 4263935,
"objectAllocCount" : 2016
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_20.p90.json
{
"instructions" : 69119,
"objectAllocCount" : 37
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.debug_log_with_info_log_level_metadata_nesting_depth_5.p90.json
{
"instructions" : 15471,
"objectAllocCount" : 6
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_1_attribute.p90.json
{
"instructions" : 6851,
"objectAllocCount" : 2
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_2_attributes.p90.json
{
"instructions" : 8399,
"objectAllocCount" : 3
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_current_read_fallback.p90.json
{
"instructions" : 1095,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_current_read_inside_scope.p90.json
{
"instructions" : 1038,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_generic.p90.json
{
"instructions" : 1972,
"objectAllocCount" : 0
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_withLogger_handler.p90.json
{
"instructions" : 4171,
"objectAllocCount" : 1
}
File: Benchmarks/NoTraits/Thresholds/Xcode swift 6.4/NoTraits.error_log_with_error_log_level_withLogger_mergingMetadata.p90.json
{
"instructions" : 10231,
"objectAllocCount" : 4
}
File: cmake/modules/CMakeLists.txt
#===----------------------------------------------------------------------===#
This source file is part of the Swift Logging API open source project
Copyright (c) 2018-2026 Apple Inc. and the Swift Logging API project authors
Licensed under Apache License v2.0
See LICENSE.txt for license information
See CONTRIBUTORS.txt for the list of Swift Logging API project authors
SPDX-License-Identifier: Apache-2.0
#===----------------------------------------------------------------------===#
set(SwiftLog_EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/SwiftLogExports.cmake)
configure_file(SwiftLogConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/SwiftLogConfig.cmake)
get_property(SwiftLog_EXPORTS GLOBAL PROPERTY SwiftLog_EXPORTS)
export(TARGETS ${SwiftLog_EXPORTS}
FILE ${SwiftLog_EXPORTS_FILE})
File: Sources/CMakeLists.txt
#===----------------------------------------------------------------------===#
This source file is part of the Swift Logging API open source project
Copyright (c) 2018-2026 Apple Inc. and the Swift Logging API project authors
Licensed under Apache License v2.0
See LICENSE.txt for license information
See CONTRIBUTORS.txt for the list of Swift Logging API project authors
SPDX-License-Identifier: Apache-2.0
#===----------------------------------------------------------------------===#
add_subdirectory(Logging)
add_subdirectory(InMemoryLogging)
File: Sources/InMemoryLogging/CMakeLists.txt
#===----------------------------------------------------------------------===#
This source file is part of the Swift Logging API open source project
Copyright (c) 2018-2026 Apple Inc. and the Swift Logging API project authors
Licensed under Apache License v2.0
See LICENSE.txt for license information
See CONTRIBUTORS.txt for the list of Swift Logging API project authors
SPDX-License-Identifier: Apache-2.0
#===----------------------------------------------------------------------===#
add_library(InMemoryLogging
"InMemoryLogHandler.swift"
)
set_target_properties(InMemoryLogging PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "$>;$>")
target_compile_options(InMemoryLogging PRIVATE
${SWIFT_LOG_COMMON_COMPILE_OPTIONS})
target_link_libraries(InMemoryLogging PRIVATE Logging)
_install_target(InMemoryLogging)
set_property(GLOBAL APPEND PROPERTY SwiftLog_EXPORTS InMemoryLogging)
File: Sources/Logging/CMakeLists.txt
#===----------------------------------------------------------------------===#
This source file is part of the Swift Logging API open source project
Copyright (c) 2018-2026 Apple Inc. and the Swift Logging API project authors
Licensed under Apache License v2.0
See LICENSE.txt for license information
See CONTRIBUTORS.txt for the list of Swift Logging API project authors
SPDX-License-Identifier: Apache-2.0
#===----------------------------------------------------------------------===#
add_library(Logging
"Handlers/MultiplexLogHandler.swift"
"Handlers/StreamLogHandler.swift"
"Handlers/SwiftLogNoOpLogHandler.swift"
"Locks.swift"
"LogEvent.swift"
"LogHandler.swift"
"Logger+Attributes.swift"
"Logger+With.swift"
"Logger.swift"
"LoggingSystem.swift"
"MetadataProvider.swift"
)
set_target_properties(Logging PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "$>;$>")
target_compile_options(Logging PRIVATE
${SWIFT_LOG_COMMON_COMPILE_OPTIONS})
_install_target(Logging)
set_property(GLOBAL APPEND PROPERTY SwiftLog_EXPORTS Logging)
File: Sources/Logging/Docs.docc/DisableLogLevelsDuringCompilation.md
Disable log levels during compilation
SwiftLog provides compile-time traits to eliminate less severe log levels from
your binary reducing the runtime overhead.
Overview
Motivation
When deploying applications to production, you often know in advance which log
levels will never be needed. For example, a production service might only need
warning and above, while trace and debug levels are only useful during
development. By using traits, you can completely remove these unnecessary log
levels at compile time, achieving zero runtime overhead.
Available traits
SwiftLog defines seven maximum log level traits, ordered from most permissive
to most restrictive:
MaxLogLevelDebug: Debug and above available (compiles out trace)MaxLogLevelInfo: Info and above available (compiles out trace, debug)MaxLogLevelNotice: Notice and above available (compiles out trace, debug,
info)MaxLogLevelWarning: Warning and above available (compiles out trace, debug,
info, notice)MaxLogLevelError: Error and above available (compiles out trace, debug,
info, notice, warning)MaxLogLevelCritical: Only critical available (compiles out all except
critical)MaxLogLevelNone: All logging compiled out (no log levels available)
By default (when no traits are specified), all log levels are available.
When you specify a maximum log level trait, all less severe levels are
completely removed from your binary at compile time. This applies to both
level-specific methods (e.g., logger.debug()) and calls to the genericlogger.log(level:) method. Traits are additive. If multiple max level traits
are specified, the most restrictive one takes effect.
Note: Traits should only be set by the applications and not libraries as
any traits defined in a transitive dependency will affect the whole resolution tree.
Example
To enable a trait, specify it when declaring your package dependency:
// In your Package.swift:
dependencies: [
.package(
url: "https://github.com/apple/swift-log.git",
from: "1.0.0",
traits: ["MaxLogLevelWarning"]
)
]With MaxLogLevelWarning enabled, all trace, debug, info, and notice log
statements are compiled out:
// These become no-ops (compiled out completely):
logger.trace("This will not be in the binary")
logger.debug("This will not be in the binary")
logger.info("This will not be in the binary")
logger.notice("This will not be in the binary")
logger.log(level: .debug, "This will not log anything")
// These work normally:
logger.warning("This still works")
logger.error("This still works")
logger.critical("This still works")
logger.log(level: .error, "This still works")When MaxLogLevelNone is enabled no log statement can ever fire, so
mutations that only configure the underlying LogHandler for future log
statements are also compiled out. Specifically, the body of theLogger/subscript(metadataKey:) setter and the body of theLogger/logLevel setter become empty, so calls likelogger[metadataKey: "user-id"] = "\(id)" and logger.logLevel = .info
do not reach the handler. Reads continue to work and return whatever the
handler reports.
File: Sources/Logging/Docs.docc/ImplementingALogHandler.md
Implementing a log handler
Create a custom logging backend that provides logging services for your apps and libraries.
Overview
To become a compatible logging backend that any SwiftLog consumer can use,
you need to fulfill a few requirements, primarily conforming to theLogHandler protocol.
Implement with value type semantics
Your log handler must be a struct and exhibit value semantics. This
ensures that changes to one logger don't affect others.
To verify that your handler reflects value semantics ensure that it passes this
test:
@Test
func logHandlerValueSemantics() {
LoggingSystem.bootstrap(MyLogHandler.init)
var logger1 = Logger(label: "first logger")
logger1.logLevel = .debug
logger1[metadataKey: "only-on"] = "first"
var logger2 = logger1
logger2.logLevel = .error // Must not affect logger1
logger2[metadataKey: "only-on"] = "second" // Must not affect logger1
// These expectations must pass
#expect(logger1.logLevel == .debug)
#expect(logger2.logLevel == .error)
#expect(logger1[metadataKey: "only-on"] == "first")
#expect(logger2[metadataKey: "only-on"] == "second")
}Note: In special cases, it is acceptable for a log handler to provide
global log level overrides that may affect all log handlers created.
Example implementation
Here's a complete example of a simple print-based log handler:
import Foundation
import Logging
public struct PrintLogHandler: LogHandler {
private let label: String
public var logLevel: Logger.Level = .info
public var metadata: Logger.Metadata = [:]
public init(label: String) {
self.label = label
}
public func log(event: LogEvent) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let levelString = event.level.rawValue.uppercased()
// Merge handler metadata with message metadata
let combinedMetadata = Self.prepareMetadata(
base: self.metadata,
explicit: event.metadata
)
// Format metadata
let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
// Create log line and print to console
let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(event.message)"
print(logLine)
}
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get {
return self.metadata[key]
}
set {
self.metadata[key] = newValue
}
}
static func prepareMetadata(
base: Logger.Metadata,
explicit: Logger.Metadata?
) -> Logger.Metadata? {
var metadata = base
guard let explicit else {
// all per-log-statement values are empty
return metadata
}
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
return metadata
}
}Constructing handlers that need more than a label
Many log handler backends need more than a label in order to construct them: a file path, a remote address, credentials, or
a buffer. A handler is an ordinary value that you construct, so nothing constrains its
initializer — it can take any parameters it needs, and it can throw. There is no
protocol-required init(label:). The single label argument on factory methods such asStreamLogHandler.standardOutput(label:) is a convention, not a requirement.
Open expensive or fallible resources, such as a file, a socket, early and up front in your own initializer that can throw, so that failures surface at setup time.LogHandler/log(event:) cannot throw, so an error deferred to logging time has nowhere to go.
Keep the handler struct cheap and copyable by placing any shared resource behind a thread-safe reference type.
Value semantics apply to a handler's configuration — its level and metadata — not its destination, so copies writing to
the same file are correct and expected.
The FileLogHandler below opens its file once, in an initializer that throws and exception when you create it with an overlapping Destination.
The handler struct itself stays a cheap, copyable value:
import Foundation
import Logging
import Synchronization
/// A log handler that appends formatted log lines to a file on disk.
public struct FileLogHandler: LogHandler {
/// The shared, thread-safe destination. Open it once, then share it across handlers: because
/// the destination is a reference type, all those value-semantic handlers write through a single
/// locked file handle.
public final class Destination: @unchecked Sendable {
private let fileHandle: Mutex<FileHandle>
/// Opens `url` for appending. All validation happens here, at setup time.
public init(writingTo url: URL) throws {
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: url.path) {
guard fileManager.createFile(atPath: url.path, contents: nil) else {
throw Failure.cannotCreateFile(url)
}
}
self.fileHandle = .init(try FileHandle(forWritingTo: url))
_ = try self.fileHandle.withLock { try $0.seekToEnd() }
}
func write(_ line: String) {
// `log(event:)` cannot throw, so a write failure is handled here, not propagated.
do {
try self.fileHandle.withLock { try $0.write(contentsOf: Data(line.utf8)) }
} catch {
// Surface the failure without crashing the application.
try? FileHandle.standardError.write(contentsOf: Data("FileLogHandler: \(error)\n".utf8))
}
}
/// Flushes and closes the file. Call this during shutdown.
public func close() {
self.fileHandle.withLock { try? $0.close() }
}
}
/// Errors thrown while opening a ``Destination``.
public enum Failure: Error {
case cannotCreateFile(URL)
}
private let label: String
private let destination: Destination
// Per-logger configuration — this is what must carry value semantics.
public var logLevel: Logger.Level = .info
public var metadata: Logger.Metadata = [:]
public var metadataProvider: Logger.MetadataProvider?
/// Cheap and non-throwing, so it is safe to call from a bootstrap factory closure per label.
public init(label: String, destination: Destination, metadataProvider: Logger.MetadataProvider? = nil) {
self.label = label
self.destination = destination
self.metadataProvider = metadataProvider
}
public func log(event: LogEvent) {
var merged = self.metadata
if let provided = self.metadataProvider?.get() {
merged.merge(provided, uniquingKeysWith: { _, new in new })
}
if let explicit = event.metadata {
merged.merge(explicit, uniquingKeysWith: { _, new in new })
}
let renderedMetadata =
merged.isEmpty
? ""
: " " + merged.sorted { $0.key < $1.key }.map { "\($0.key)=\($0.value)" }.joined(separator: " ")
self.destination.write("\(event.level) \(self.label):\(renderedMetadata) \(event.message)\n")
}
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get { self.metadata[key] }
set { self.metadata[key] = newValue }
}
}Using your log handler
Construct the destination where you can use try to build a logger backed by your handler, then bind it
as Logger/current for the application's lifetime using withLogger:
// Open the file once, where setup can throw.
let destination = try FileLogHandler.Destination(writingTo: URL(fileURLWithPath: "/var/log/myapp.log"))
defer { destination.close() }
// Build a logger backed by the handler, then bind it for the scope.
let logger = Logger(label: "app") { label in
FileLogHandler(label: label, destination: destination)
}
try await withLogger(logger) { logger in
logger.info("Application started")
// Code in this scope reads the handler through Logger.current.
}The try on Destination is where setup fails if the path is unwritable: validation happens
before the logger is constructed, so you never need a throwing factory overload.
Use the same pattern for handlers that connect to a remote service:
- validate and connect up front
- keep
LogHandler/log(event:)non-blocking by buffering onto a background task - provide a
close()orshutdown()(or use Swift Service Lifecycle service) to flush on shutdown
For runtime failures the handler can't avoid, such as a full disk or a broken pipe, degrade gracefully inside log(event:) by dropping the log message, retrying, or reporting to stderr.
Never call fatalError on the logging path.
Advanced features
Metadata providers
Metadata providers allow you to dynamically add contextual information to all
log messages without explicitly passing it each time. Common use cases include
request IDs, user sessions, or trace contexts that should be included in logs
throughout a request's lifecycle.
import Foundation
import Logging
public struct PrintLogHandler: LogHandler {
private let label: String
public var logLevel: Logger.Level = .info
public var metadata: Logger.Metadata = [:]
public var metadataProvider: Logger.MetadataProvider?
public init(label: String) {
self.label = label
}
public func log(event: LogEvent) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let levelString = event.level.rawValue.uppercased()
// Merge handler metadata with message metadata
let combinedMetadata = Self.prepareMetadata(
base: self.metadata,
provider: self.metadataProvider,
explicit: event.metadata
)
// Format metadata
let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
// Create log line and print to console
let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(event.message)"
print(logLine)
}
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get {
return self.metadata[key]
}
set {
self.metadata[key] = newValue
}
}
static func prepareMetadata(
base: Logger.Metadata,
provider: Logger.MetadataProvider?,
explicit: Logger.Metadata?
) -> Logger.Metadata? {
var metadata = base
let provided = provider?.get() ?? [:]
guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else {
// all per-log-statement values are empty
return metadata
}
if !provided.isEmpty {
metadata.merge(provided, uniquingKeysWith: { _, provided in provided })
}
if let explicit = explicit, !explicit.isEmpty {
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
}
return metadata
}
}Reading metadata attributes in LogHandlers
Metadata values can carry attributes alongside their string representation. Attributes are embedded insideMetadataValue via the .stringConvertible case and are accessible through the value.attributes property. This
enables features like sensitivity annotations without any changes to your handler's protocol conformance.
To read attributes in your log handler:
public func log(event: LogEvent) {
// Merge handler metadata, provider metadata, and event metadata as usual
var merged = self.metadata
if let provider = self.metadataProvider {
merged.merge(provider.get(), uniquingKeysWith: { _, rhs in rhs })
}
if let eventMetadata = event.metadata {
merged.merge(eventMetadata, uniquingKeysWith: { _, rhs in rhs })
}
// Read attributes from individual values:
for (key, value) in merged {
let attributes = value.attributes // Empty if the value carries no attributes
// Process based on your handler's needs
// For example, check for a custom attribute:
// if attributes[MyAttribute.self] == .flagged { ... }
}
}Key considerations:
Opt-in inspection: Attributes are invisible unless you call
value.attributes. Handlers that do not care about
attributes work without any changes.No new protocol requirements: Reading attributes does not require implementing any new
LogHandlerproperties
or subscripts. Themetadataproperty andmetadataKeysubscript work exactly as before.Attributes flow through metadata: Attributed values flow naturally through metadata merging,
MetadataProvider,
andMultiplexLogHandler— no special handling needed.
Performance considerations
- Avoid blocking: Don't block the calling thread for I/O operations.
- Lazy evaluation: Remember that messages and metadata are autoclosures.
- Memory efficiency: Don't hold onto large amounts of messages.
See Also
LogHandlerStreamLogHandlerMultiplexLogHandler
File: Sources/Logging/Docs.docc/index.md
``Logging``
A unified, performant, and ergonomic logging API for Swift.
Overview
SwiftLog provides a logging API package designed to establish a common API the
ecosystem can use. It allows packages to emit log messages without tying them to
any specific logging implementation, while applications can choose any
compatible logging backend.
SwiftLog is an API package which cuts the logging problem in half:
- A logging API (this package)
- Logging backend implementations (community-provided)
This separation allows libraries to adopt the API while applications choose any
compatible logging backend implementation without requiring changes from
libraries.
Getting Started
Use this package if you're writing a cross-platform application (for example, Linux and
macOS) or library, and want to target this logging API.
Adding the Dependency
Add the dependency to your Package.swift:
.package(url: "https://github.com/apple/swift-log", from: "1.6.0")And to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "Logging", package: "swift-log")
]
)Basic Usage
// Import the logging API
import Logging
// Create a logger with a label
let logger = Logger(label: "MyLogger")
// Use it to log messages
logger.info("Hello World!")This outputs:
2025-10-24T17:26:47-0700 info MyLogger: [your_app] Hello World!Default Behavior
SwiftLog provides basic console logging via StreamLogHandler. By default it
uses stdout, however, you can configure it to use stderr instead:
LoggingSystem.bootstrap(StreamLogHandler.standardError)StreamLogHandler is primarily for convenience. For production applications,
implement the LogHandler protocol directly or use a community-maintained
backend.
Topics
Logging API
- doc:UnderstandingLoggers
LoggerLoggingSystem
Log Handlers
LogHandlerLogEventMultiplexLogHandlerStreamLogHandlerSwiftLogNoOpLogHandler
Best Practices
Contributing
File: Sources/Logging/Docs.docc/LoggingBestPractices.md
Logging best practices
Best practices for effective logging with SwiftLog.
Overview
This collection of best practices helps library authors and application
developers create effective, maintainable logging that works well across diverse
environments. Each practice is designed to ensure your logs are useful for
debugging while being respectful of system resources and operational
requirements.
Who Should Use These Practices
- Library Authors: Creating reusable components that log appropriately.
- Application Developers: Implementing logging strategies in applications.
Philosophy
Good logging strikes a balance between providing useful information and avoiding
system overhead. These practices are based on real-world experience with
production systems and emphasize:
- Predictable behavior across different environments.
- Performance consciousness to avoid impacting application speed.
- Operational awareness to support production debugging and monitoring.
- Developer experience to make debugging efficient and pleasant.
Contributing to these practices
These best practices evolve based on community experience and are maintained by
the Swift Server Working Group (SSWG). Each
practice includes:
- Clear motivation explaining why the practice matters
- Concrete examples showing good and bad patterns
- Alternatives considered documenting trade-offs and rejected approaches
Topics
File: Sources/Logging/Docs.docc/UnderstandingLoggers.md
Understanding Loggers and Log Handlers
Learn how to create and configure loggers, set log levels, and use metadata to add context to your log messages.
Overview
Create or retrieve a logger to get an instance for logging messages.
Log messages have a level that you use to indicate the message's importance.
SwiftLog defines seven log levels, represented by Logger/Level, ordered from least to
most severe:
Logger/Level/traceLogger/Level/debugLogger/Level/infoLogger/Level/noticeLogger/Level/warningLogger/Level/errorLogger/Level/critical
Once a message is sent to a logger, a log handler processes it.
The app using the logger configures the handler, usually for the environment in which that app runs, processing messages appropriate to that environment.
If the app doesn't provide its own log handler, SwiftLog defaults to using a StreamLogHandler that outputs log messages to STDOUT.
Loggers
Loggers are used to emit log messages at different severity levels:
// Informational message
logger.info("Processing request")
// Something went wrong
logger.error("Houston, we have a problem")Logger is a value type with value semantics, meaning that when you modify a
logger's configuration (like its log level or metadata), it only affects that
specific logger instance:
let baseLogger = Logger(label: "MyApp")
// Create a new logger with different configuration.
var requestLogger = baseLogger
requestLogger.logLevel = .debug
requestLogger[metadataKey: "request-id"] = "\(UUID())"
// baseLogger is unchanged. It still has default log level and no metadata
// requestLogger has debug level and request-id metadata.This value type behavior makes loggers safe to pass between functions and modify
without unexpected side effects.
Log Levels
Log levels can be changed per logger without affecting others:
var logger = Logger(label: "MyLogger")
logger.logLevel = .debugFor guidance on what level to use for a message, see doc:001-ChoosingLogLevels.
Logging Metadata
Metadata provides contextual information crucial for debugging:
var logger = Logger(label: "com.example.server")
logger[metadataKey: "request.id"] = "\(UUID())"
logger.info("Processing request")Output:
2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED Processing requestSource vs Label
A Logger has an immutable label that identifies its creator, while each log
message carries a source parameter that identifies where the message originated.
Use source for filtering messages from specific subsystems.
File: Sources/Logging/Docs.docc/BestPractices/001-ChoosingLogLevels.md
001: Choosing log levels
Select appropriate log levels in applications and libraries.
Overview
SwiftLog defines seven log levels, and choosing the right level is crucial for
creating well-behaved libraries that don't overwhelm logging systems or misuse
severity levels. This practice provides clear guidance on when to use each
level.
Motivation
Libraries must be well-behaved across various use cases and cannot assume
specific logging backend configurations. Using inappropriate log levels can
flood production logs, trigger false alerts, or make debugging more difficult.
Following consistent log level guidelines ensures your library integrates well
with diverse application environments.
Log levels
SwiftLog defines seven log levels via Logger/Level, ordered from least to
most severe:
Logger/Level/traceLogger/Level/debugLogger/Level/infoLogger/Level/noticeLogger/Level/warningLogger/Level/errorLogger/Level/critical
Level guidelines
How you use log levels depends in large part if you are developing a library, or
an application which bootstraps its logging system and is in full control over
its logging environment.
For libraries
Libraries should use info level or less severe (info, debug, trace).
Libraries should not log information on warning or more severe levels,
unless it is a one-time (for example during startup) warning, that cannot lead
to overwhelming log outputs.
Each level serves different purposes:
Trace Level
- Usage: Log everything needed to diagnose hard-to-reproduce bugs.
- Performance: May impact performance; assume it won't be used in production.
- Content: Internal state, detailed operation flows, diagnostic information.
Debug Level
- Usage: May be enabled in some production deployments.
- Performance: Should not significantly undermine production performance.
- Content: High-level operation overview, connection events, major decisions.
Info Level
- Usage: Reserved for things that went wrong but can't be communicated
through other means, like throwing from a method. - Examples: Connection retry attempts, fallback mechanisms, recoverable
failures. - Guideline: Use sparingly - Don't use for normal successful operations.
For applications
Applications can use any level depending on the context and what they want
to achieve. Applications have full control over their logging strategy.
Configuring logger log levels
It depends on the use-case of your application which log level your logger
should use. For console and other end-user-visible displays: Consider using
notice level as the minimum visible level to avoid overwhelming users with
technical details.
Example
Recommended: Libraries should use info level or lower
// ✅ Good: Trace level for detailed diagnostics
logger.trace("Connection pool state", metadata: [
"active": "\(activeConnections)",
"idle": "\(idleConnections)",
"pending": "\(pendingRequests)"
])
// ✅ Good: Debug level for high-value operational info
logger.debug("Database connection established", metadata: [
"host": "\(host)",
"database": "\(database)",
"connectionTime": "\(duration)"
])
// ✅ Good: Info level for issues that can't be communicated through other means
logger.info("Connection failed, retrying", metadata: [
"attempt": "\(attemptNumber)",
"maxRetries": "\(maxRetries)",
"host": "\(host)"
])Use sparingly: Warning and error levels
// ✅ Good: One-time startup warning or error
logger.warning("Deprecated TLS version detected. Consider upgrading to TLS 1.3")Avoid: Logging potentially intentional failures at info level
Some failures may be completely intentional from the high-level perspective of a
developer or system using your library. For example: failure to resolve a
domain, failure to make a request, or failure to complete some task;
Instead, log at debug or trace levels and offer alternative ways to observe
these behaviors, for example using swift-metrics to emit counts.
// ❌ Bad: Normal operations at info level flood production logs
logger.info("Request failed")Avoid: Normal operations at info level
// ❌ Bad: Normal operations at info level flood production logs
logger.info("HTTP request received")
logger.info("Database query executed")
logger.info("Response sent")
// ✅ Good: Use appropriate levels instead
logger.debug("Processing request", metadata: ["path": "\(path)"])
logger.trace("Query", metadata: ["sql": "\(query)"])
logger.debug("Request completed", metadata: ["status": "\(status)"])File: Sources/Logging/Docs.docc/BestPractices/002-StructuredLogging.md
002: Structured logging
Use metadata to create machine-readable, searchable log entries.
Overview
Structured logging uses metadata to separate human-readable messages from
machine-readable data. This practice makes logs easier to search, filter, and
analyze programmatically while maintaining readability.
Motivation
Traditional string-based logging embeds all information in the message text,
making it more difficult for automated tools to parse and extract.
Structured logging separates these concerns; messages provide human readable
context while metadata provides structured data for tooling.
Example
Recommended: Structured logging
// ✅ Structured - message provides context, metadata provides data
logger.info(
"Accepted connection",
metadata: [
"connection.id": "\(id)",
"connection.peer": "\(peer)",
"connections.total": "\(count)"
]
)
logger.error(
"Database query failed",
metadata: [
"query.retries": "\(retries)",
"query.error": "\(error)",
"query.duration": "\(duration)"
]
)Advanced: Nested metadata for complex data
// ✅ Complex structured data
logger.trace(
"HTTP request started",
metadata: [
"request.id": "\(requestId)",
"request.method": "GET",
"request.path": "/api/users",
"request.headers": [
"user-agent": "\(userAgent)"
],
"client.ip": "\(clientIP)",
"client.country": "\(country)"
]
)Avoid: Unstructured logging
// ❌ Not structured - hard to parse programmatically
logger.info("Accepted connection \(id) from \(peer), total: \(count)")
logger.error("Database query failed after \(retries) retries: \(error)")Metadata key conventions
Use hierarchical dot-notation for related fields:
// ✅ Good: Hierarchical keys
logger.debug(
"Database operation completed",
metadata: [
"db.operation": "SELECT",
"db.table": "users",
"db.duration": "\(duration)",
"db.rows": "\(rowCount)"
]
)
// ✅ Good: Consistent prefixing
logger.info(
"HTTP response",
metadata: [
"http.method": "POST",
"http.status": "201",
"http.path": "/api/users",
"http.duration": "\(duration)"
]
)File: Sources/Logging/Docs.docc/BestPractices/003-AcceptingLoggers.md
003: Logger propagation in libraries
Propagate caller context by accepting a Logger parameter or reading the task-localLogger/current — never by constructing your own logger.
Overview
Libraries should obtain a Logger in one of two ways: accept one through a method
parameter or read Logger/current from the task-local. Both approaches
preserve the caller's metadata, log level, and handler choice. Constructing a logger
inside a library — via Logger/init(label:) — takes those choices away from the
application and breaks metadata propagation.
Motivation
The application controls logging: which backend, which log level, which metadata
travels with each request. Libraries that participate in this picture obtain a logger
the application has set up, rather than constructing their own from scratch. This
ensures correlation IDs and other contextual metadata flow through the entire call
stack, and gives the application a single place to redirect or filter all log output.
Two propagation mechanisms are available, and they coexist. Choose the explicit
parameter when the library's API already accepts a Logger (or it's natural to add
one) — the call site stays declarative about what gets logged where. Choose the
task-local when adding a logger: parameter would pollute an API that otherwise has
no logging concern in its signature. Application code binds the initial task-local;
library code reads it and updates the metadata.
Example
Recommended: Read ``Logger/current`` from the task-local
When there is no logger: parameter in the API, read the
task-local. The application's accumulated metadata (request.id, etc.) flows in
automatically without an explicit hand-off.
// ✅ Good: Library reads Logger.current; caller scopes context via withLogger.
public struct AnalyticsClient {
public func track(_ event: String) {
Logger.current.info("event", metadata: ["event.name": "\(event)"])
}
}
// Application binds at @main and scopes per-request metadata.
@main
struct MyServer {
static func main() async throws {
let logger = Logger(label: "my-server")
try await withLogger(logger) { _ in
try await runServices()
}
}
}
func handleRequest(_ req: HTTPRequest) async throws {
try await withLogger(mergingMetadata: ["request.id": "\(req.id)"]) { _ in
AnalyticsClient().track("request.received") // sees request.id automatically
}
}For per-statement metadata, pass it via the metadata: parameter on the log call:
Logger.current.info("step", metadata: ["step.name": "validate"])For a few back-to-back lines, take a local copy and stamp metadata there — also no
propagation:
var local = Logger.current
local[metadataKey: "step.name"] = "validate"
local.info("entering")
local.info("done")Alternative: Accept logger through method parameters
// ✅ Good: Pass the logger through method parameters,
// default to Logger.current in the public API.
struct RequestProcessor {
func processRequest(_ request: HTTPRequest, logger: Logger = Logger.current) async throws -> HTTPResponse {
// Add structured metadata that every log statement should contain.
var logger = logger
logger[metadataKey: "request.method"] = "\(request.method)"
logger[metadataKey: "request.path"] = "\(request.path)"
logger[metadataKey: "request.id"] = "\(request.id)"
logger.debug("Processing request")
// Pass the logger down to maintain metadata context.
let validatedData = try validateRequest(request, logger: logger)
let result = try await executeBusinessLogic(validatedData, logger: logger)
logger.debug("Request processed successfully")
return result
}
private func validateRequest(_ request: HTTPRequest, logger: Logger) throws -> ValidatedRequest {
logger.debug("Validating request parameters")
return ValidatedRequest(request)
}
private func executeBusinessLogic(_ data: ValidatedRequest, logger: Logger) async throws -> HTTPResponse {
logger.debug("Executing business logic")
let dbResult = try await databaseService.query(data.query, logger: logger)
logger.debug("Business logic completed")
return HTTPResponse(data: dbResult)
}
}Alternative: Accept logger through initializer for long-lived components
// ⚠️ Acceptable: Logger through initializer for long-lived components
final class BackgroundJobProcessor {
private let logger: Logger
init(logger: Logger) {
self.logger = logger
}
func run() async {
// Execute some long running work
logger.debug("Update about long running work")
// Execute some more long running work
}
}Avoid: Libraries creating their own loggers
Constructing Logger/init(label:) inside a library takes control over the handler, log
level, and base metadata away from the application. The application cannot redirect,
filter, or silence the library's output.
// ❌ Bad: Library creates its own logger — loses caller's context.
final class MyLibrary {
private let logger = Logger(label: "MyLibrary")
}Logger/init(label:) is for the application — typically at @main paired with .withLogger().
Library code, including internal application modules, should not construct loggers.
If they do, setting up the global factory with LoggingSystem/bootstrap(_:) allows
enforcing a specific factory on all the loggers constructed in the process. This
does not necessary need to be the same factory as was used to create the task-local
logger.
Avoid: Relying on ``Logger/current`` across non-Task boundaries
Logger/current is backed by a TaskLocal and propagates through Swift's structured
concurrency model only. Callbacks invoked on non-Task threads — GCD blocks,URLSession completion handlers, delegate methods dispatched onto specific queues,NotificationCenter observers, C-API callbacks — see the default fallback logger,
not the bound one. Metadata bound by the calling Task is invisible inside those
callbacks.
// ❌ Bad: completion handler runs without the Task context; Logger.current is the fallback.
try await withLogger(mergingMetadata: ["request.id": "r1"]) { _ in
URLSession.shared.dataTask(with: req) { data, _, _ in
Logger.current.info("response") // empty-label fallback, no request.id
}.resume()
}For libraries with async completion-handler APIs, accept an explicit Logger parameter.
If capturing and rebinding across the boundary is the only option:
// ✅ Good: capture before the boundary, rebind on the other side.
try await withLogger(mergingMetadata: ["request.id": "r1"]) { captured in
URLSession.shared.dataTask(with: req) { data, _, _ in
withLogger(captured) { _ in // Bind Logger.current inside the callback
Logger.current.info("response") // Logger.current now has request.id
}
}.resume()
}File: Sources/Logging/Docs.docc/BestPractices/004-TestingLogging.md
004: Testing code that logs
Validate what you code logs by capturing entries in memory rather than bootstrapping a process-wide backend.
Overview
Import and use InMemoryLogging, which provides an InMemoryLogHandler that collects every emitted record into an array you can inspect.
Keep a handler you create local to each test.
Each handler that you create in a test has no shared state, so the test stays isolated is safe to run on parallel with other tests.
Don't use LoggingSystem/bootstrap(_:) in a test, it sets one process-wide backend and traps if called twice.
Add the product to your test target:
.product(name: "InMemoryLogging", package: "swift-log")Capture logs from a logger you pass in
When the code under test accepts a Logger (see doc:003-AcceptingLoggers), back it with
an InMemoryLogHandler and validate the results on entries.
The following code illustrates how to set up and validate a handler using InMemoryLogging to check the expected results:
import InMemoryLogging
import Logging
import Testing
struct RequestProcessor {
func handle(id: String, logger: Logger) {
logger.info("request received", metadata: ["request.id": "\(id)"])
}
}
@Test
func stampsRequestID() {
let handler = InMemoryLogHandler()
let logger = Logger(label: "test", factory: { _ in handler })
RequestProcessor().handle(id: "42", logger: logger)
#expect(handler.entries.count == 1)
#expect(handler.entries[0].message == "request received")
#expect(handler.entries[0].metadata["request.id"] == "42")
}Each Entry is equatable and exposes level, message, metadata, and error.
The handler log level defaults to Logger/Level/info; set handler.logLevel = .trace before you use it to
capture lower levels.
Capture logs from code that reads `Logger.current`
When the code you want to test reads the task-local Logger/current instead of taking a parameter, bind an InMemoryLogHandler for the scope of that by using withLogger(logLevel:handler:metadata:_:)-2pd9p.
The handler shares its storage by reference, so the copy you hold sees what was logged.
Bind the handler for the scope of the call and validate the captured entry:
import InMemoryLogging
import Logging
import Testing
struct AnalyticsClient {
func track(_ event: String) {
Logger.current.info("event", metadata: ["event.name": "\(event)"])
}
}
@Test
func tracksEvent() {
let handler = InMemoryLogHandler()
withLogger(handler: handler) { _ in
AnalyticsClient().track("signup") // calls Logger.current.info(...)
}
#expect(handler.entries.count == 1)
#expect(handler.entries[0].metadata["event.name"] == "signup")
}File: Sources/Logging/Docs.docc/Proposals/Proposals.md
Proposals
Collaborate on API changes to SwiftLog by writing a proposal.
Overview
For non-trivial changes that affect the public API, the SwiftLog project adopts a lightweight version of the Swift Evolution process.
Writing a proposal first helps discuss multiple possible solutions early, apply useful feedback from other contributors, and avoid reimplementing the same feature multiple times.
While it's encouraged to get feedback by opening a pull request with a proposal early in the process, it's also important to consider the complexity of the implementation when evaluating different solutions. For example, this might mean including a link to a branch containing a prototype implementation of the feature in the pull request description.
Note: The goal of this process is to help solicit feedback from the whole community around the project, and we will continue to refine the proposal process itself. Use your best judgement, and don't hesitate to propose changes to the proposal structure itself!
Steps
- Make sure there's a GitHub issue for the feature or change you would like to propose.
- Duplicate the
SLG-NNNN.mddocument and replaceNNNNwith the next available proposal number. - Link the GitHub issue from your proposal, and fill in the proposal.
- Open a pull request with your proposal and solicit feedback from other contributors.
- Once a maintainer confirms that the proposal is ready for review, the state is updated accordingly. The review period is 7 days, and ends when one of the maintainers marks the proposal as Ready for Implementation, or Deferred.
- Before the pull request is merged, there should be an implementation ready, either in the same pull request, or a separate one, linked from the proposal.
- The proposal is considered Approved once the implementation, proposal PRs have been merged, and, if originally disabled by a feature flag, feature flag enabled unconditionally.
If you have any questions, ask in an issue on GitHub.
Possible review states
- Awaiting Review
- In Review
- Ready for Implementation
- In Preview
- Approved
- Deferred
Topics
Implemented Proposals
- doc:SLG-0001-metadata-providers
- doc:SLG-0002
- doc:SLG-0003
- doc:SLG-0004-metadata-value-attributes
- doc:SLG-0005-log-event-LogHandler-API
- doc:SLG-0006-task-local-logger
Proposal Template
File: Sources/Logging/Docs.docc/Proposals/SLG-0001-metadata-providers.md
SLG-0001: Metadata Providers
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.
Authors
Moritz Lang, Konrad 'ktoso' Malawski
Motivation
To benefit from tools such as Distributed Tracing it is necessary for libraries to make use of the trace information.
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.
Today, the only supported way of providing such metadata is to pass them along to each log call explicitly:
logger.info("first this ...", metadata: ["trace-id": MyTracingLibrary.currentTraceID])
logger.info("... now this", metadata: ["trace-id": MyTracingLibrary.currentTraceID])This comes with a couple of downsides:
Error-prone and repetitive
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.
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.
Impossible to implement for libraries
A large portion of logs are not generated by the end-user but by libraries such as AsyncHTTPClient, Vapor etc.
These libraries, by design, don't know about the specific metadata keys that should be included in the logs.
Those keys are after all runtime dependent, and may change depending on what tracing system is configured by the end user.
For example, a specific Tracer implementation would use a type representing a trace ID, and has a way of extracting
that trace ID from a Swift Distributed Tracing Baggage. But other libraries can't know about this specific trace ID and therefore would not be
able to pass such values along to their log statements.
Proposed solution
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).
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.
To solve this, we propose the extension of swift-log APIs with a new concept: metadata providers.
MetadataProvider is a struct nested in the Logger type, sitting alongside MetadataValue and the Metadata.
Its purpose is to provide Logger.Metadata from the asynchronous context, by e.g. looking up various task-local values,
and converting them into Logger.Metadata. This is performed by calling the get() function, like so:
extension Logger {
public struct MetadataProvider: Sendable {
// ...
public init(_ provideMetadata: @escaping @Sendable () -> Metadata)
public get() -> Metadata
}
}Defining a `MetadataProvider`
While MetadataProviders can be created in an ad-hoc fashion, the struct may be used as a namespace
to define providers in.
For example, a MyTracer implementation of swift-distributed-tracing would be expected to provide a metadata provider
that is aware of its own Baggage and specific keys it uses to carry the trace and span identifiers, like this:
import Tracing // swift-distributed-tracing
extension Logger.MetadataProvider {
static let myTracer = Logger.MetadataProvider {
guard let baggage = Baggage.current else {
return [:]
}
guard let spanContext = baggage.spanContext else {
return [:]
}
return [
"traceID": "\(spanContext.traceID)",
"spanID": "\(spanContext.spanID)",
]
}
}Using a `MetadataProvider`
A MetadataProvider can be set up either globally, on a boot-strapped logging system:
LoggingSystem.bootstrap(
metadataProvider: .myTracer,
StreamLogHandler.standardOutput
)or, using the metadata provider specific bootstrap method:
LoggingSystem.bootstrapMetadataProvider(.myTracer)It is also possible to configure a metadata provider on a per-logger basis, like this:
let logger = Logger(label: "example", metadataProvider: Logger.MetadataProvider {
guard let baggage = Baggage.current else {
return [:]
}
guard let operationID = baggage.operationID else {
return nil
}
return ["extra/opID": "\(opID)"]
})which overrides the default bootstrapped metadata provider.
NOTE: Setting the metadata provider on the logger directly means the
LoggingSystemmetadata provider
is skipped (if defined), following how an explicitly passed handlerfactory
overrides theLoggingSystemsfactory.
Once a metadata provider was set up, when a log statement is about to be emitted, the log handler implementation shall
invoke it whenever it is about to emit a log statement. This must be done from the same asynchronous context as the log statement
was made; I.e. if a log handler were to asynchronously–in a detached task–create the actual log message, the invocation of
the metadata provider still must be performed before passing the data over to the other detached task.
Usually, metadata providers will reach for the task's Swift Distributed Tracing Baggage which is the mechanism that swift-distributed-tracing
and instrumentation libraries use to propagate metadata across asynchronous, as well as process, boundaries. Handlers
may also inspect other task-local values, however they should not expect other libraries to be able to propagage those
as well as they will propagate Baggage values.
Those metadata will then be included in the log statement, e.g. like this:
var baggage = Baggage.topLevel
baggage.spanContext = SpanContext()
Baggage.withValue(baggage) {
test()
}
func test() {
log.info("Test", metadata: ["oneOff": "42"])
// info [traceID: abc, spanID: 123, onOff: 42] Test
}Multiple `MetadataProvider`s using `MetadataProvider.multiplex(_:)`
Borrowing the concept from log handlers, metadata providers also have a multiplexing implementation.
It is defined as an extension on MetadataProvider and is useful in cases where users want to utilize
more than one metadata provider at the same time:
extension Logger.MetadataProvider {
public static func multiplex(_ providers: [Logger.MetadataProvider]) -> Logger.MetadataProvider {
precondition(!providers.isEmpty, "providers MUST NOT be empty")
return Logger.MetadataProvider { baggage in
providers.reduce(into: nil) { metadata, provider in
if let providedMetadata = provider.metadata(baggage) {
if metadata != nil {
metadata!.merge(providedMetadata, uniquingKeysWith: { _, rhs in rhs })
} else {
metadata = providedMetadata
}
}
}
}
}
}Metadata keys are unique, so in case multiple metadata providers return the same key,
the last provider in the array wins and provides the value.
Note that it is possible to query the LoggingSystem for the configured, system-wide, metadata provider,
and combine it using a multiplex provider if the system-wide provider should not be replaced, but augmented
by additional providers.
Understanding Swift Distributed Tracing `Baggage`
The Baggage type is more than just a fancy type-safe container for your values which are meant to be carried across
using a single, well-known, task-local value.
Values stored in Baggage are intended to be carried across process boundaries, and e.g. libraries such as HTTP clients,
or other RPC mechanisms, or even messaging systems implement distributed tracing instruments
which inject and extract baggage values into their respective carrier objects, e.g. an HTTPRequest in the case of an HTTP client.
In other words, whenever intending to propagate information across processes utilize the Baggage type to carry it,
and ensure to provide an Instrument that is able to inject/extract the values of interest into the carrier types you are interested in.
To learn more about baggage and instrumentation, refer to the Swift Distributed Tracing library documentation.
When to use `Baggage` vs. `Logger[metadataKey:]`
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:].
var logger = Logger(label: "org.swift.my-service")
logger[metadataKey: "instanceId"] = "123"
logger.info("Service started.")
// [instanceId: 123] Service started.On the other hand, things like a trace ID are dynamic and context-dependent, therefore would be obtained via Baggage:
logger.info("Product fetched.")
// [traceId: 42] Product fetched.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:
logger.info("Product fetched.", metadata: ["productId": "42"])
logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])
// [productId: 42] Product fetched.
// [paymentMethod: apple-pay] Product fetched.If there was a Baggage value with a trace ID surrounding these log statements, they would be automatically correlatable:
var baggage = Baggage.topLevel
baggage.traceID = 42
Baggage.$current.withValue(baggage) {
logger.info("Product fetched.", metadata: ["productId": "42"])
logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])
}
// [trace-id: 42, productId: 42] Product fetched.
// [trace-id: 42, paymentMethod: apple-pay] Product fetched.Alternatives considered
MetadataProviders as a function of `Baggage -> Metadata`
This was considered and fully developed, however it would cause swift-log to have a dependency on the instrumentation type Baggage,
which was seen as in-conflict-with the interest of swift-log remaining a zero-dependency library.
Baggage is the well known type that is intended to be used for any kind of distributed systems instrumentation and tracing,
however it adds additional complexity and confusion for users who are not interested in this domain. For example, developers
may be confused about why Baggage and Logger.Metadata look somewhat similar, but behave very differently. This complexity
is inherent to the two types actually being very different, however we do not want to overwhelm newcomers or developers
who are only intending to use swift-log within process. Such developers do not need to care about the added complexities
of distributed systems.
The tradeoff we take here is that every metadata provider will have to perform its own task-local (and therefore also thread-local),
access in order to obtain the Baggage value, rather than the lookup being able to be performed once and shared between
multiple providers when a multiplex provider was configured in the system. We view this tradeoff as worth taking, as the cost
of actually formatting the metadata usually strongly dominates the cost of the task-local lookup.
Removing `LogHandler.Metadata` in favor of `Baggage`
Removing logger metadata is not a good option because it serves a slightly different style of metadata than the baggage.
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.
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:
log.info("Obtained item! Hooray!", metadata: ["item": "\(item)"])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.
Explicitly passing `Baggage` always
Baggage is designed for use cases like distributed tracing, or similar instrumentation patterns where "all" participating code may need to reach for it.
Specifically in logging, this means that every call site for every log statement would have to pass it explicitly resulting in annoying noisy code:
class StoresRepository {
func store(byID id: String, eventLoop: EventLoop, logger: Logger, baggage: Baggage) async throws -> Store {
InstrumentationSystem.tracer.withSpan("Fetch Store") { span in
logger.info("Fetched store.", baggage: span.baggage)
}
}
}
try await storesRepository.store(
byID: storeID,
eventLoop: eventLoop,
logger: logger,
baggage: baggage
)Explicitly passing `Logger` always
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.
import Tracing
import Logging
let contextualLogger = InstrumentationSystem.tracer.populateTraceMetadata(logger)
contextualLogger.info("Request received.")Tracing providing extensions on Logger
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:
extension Logger {
func tinfo(_ message: ..., baggage: Baggage?) {
// ...
}
}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:
logger.info("Hello")
// we want this to pick up Baggage, but the signature would be ambiguousAlso, 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.
File: Sources/Logging/Docs.docc/Proposals/SLG-0002.md
SLG-0002: Compile-time log level elimination using traits
Enable compile-time elimination of log levels to achieve zero runtime overhead for logs that will never be needed in production.
Overview
- Proposal: SLG-0002
- Author(s): Vladimir Kukushkin
- Status: Approved
- Issue: apple/swift-log#378
- Implementation:
- Related links:
Introduction
Add Swift package traits to allow applications to compile out less severe log levels, completely eliminating their runtime overhead from the final binary.
Motivation
In production deployments, applications often know in advance which log levels will never be needed. For example, a production service typically only needs warning and above, while trace and debug levels are exclusively useful during development. Currently, even with the log level set to .warning at runtime, the code for trace and debug statements still exists in the binary and incurs overhead.
For performance-critical applications or resource-constrained environments, this overhead is unacceptable. Applications need a way to completely remove unnecessary log levels at compile time, achieving zero runtime cost.
Before this proposal, the only workarounds were:
- Manually wrapping log statements in
#if DEBUGchecks (error-prone and verbose) - Accepting the runtime overhead (unacceptable for many use cases)
Proposed solution
This proposal introduces seven package traits that correspond to maximum log level thresholds. Applications specify a trait when declaring their dependency on SwiftLog, and all less severe log levels are completely compiled out.
In your Package.swift:
dependencies: [
.package(
url: "https://github.com/apple/swift-log.git",
from: "1.0.0",
traits: ["MaxLogLevelWarning"]
)
]With MaxLogLevelWarning enabled:
// These become no-ops (compiled out completely):
logger.trace("This will not be in the binary")
logger.debug("This will not be in the binary")
logger.info("This will not be in the binary")
logger.notice("This will not be in the binary")
logger.log(level: .debug, "This will not log anything")
// These work normally:
logger.warning("This still works")
logger.error("This still works")
logger.critical("This still works")
logger.log(level: .error, "This still works")Detailed design
The seven available traits, ordered from most permissive to most restrictive, are:
traits: [
.trait(name: "MaxLogLevelDebug", description: "Debug and above available (compiles out trace)"),
.trait(name: "MaxLogLevelInfo", description: "Info and above available (compiles out trace, debug)"),
.trait(name: "MaxLogLevelNotice", description: "Notice and above available (compiles out trace, debug, info)"),
.trait(
name: "MaxLogLevelWarning",
description: "Warning and above available (compiles out trace, debug, info, notice)"
),
.trait(
name: "MaxLogLevelError",
description: "Error and above available (compiles out trace, debug, info, notice, warning)"
),
.trait(name: "MaxLogLevelCritical", description: "Only critical available (compiles out all except critical)"),
.trait(name: "MaxLogLevelNone", description: "All logging compiled out (no log levels available)"),
.default(enabledTraits: []),
]By default (when no traits are specified), all log levels remain available.
Traits are additive: if multiple max level traits are specified in the dependency graph, the most restrictive one takes effect. This ensures that if any dependency requests stricter elimination, the entire build respects it.
Important: Traits should only be set by applications, not libraries, as any trait defined in a transitive dependency will affect the entire dependency resolution tree.
For the generic log(level:) method, the implementation switches behavior based on whether any trait is enabled:
public func log(
level: Logger.Level,
_ message: @autoclosure () -> Logger.Message,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
) {
#if MaxLogLevelDebug || MaxLogLevelInfo || MaxLogLevelNotice || MaxLogLevelWarning || MaxLogLevelError || MaxLogLevelCritical || MaxLogLevelNone
// When traits are enabled, dispatch to level-specific methods
// to leverage their compile-time elimination
switch level {
case .trace:
self.trace(message(), metadata: metadata(), source: source(), file: file, function: function, line: line)
case .debug:
self.debug(message(), metadata: metadata(), source: source(), file: file, function: function, line: line)
// ... (all levels)
}
#else
// When no traits are enabled, avoid the switch overhead
self._log(
level: level,
message(),
metadata: metadata(),
source: source(),
file: file,
function: function,
line: line
)
#endif
}This approach adds a minimal switch statement overhead when traits are enabled, but allows the level-specific methods' compile-time elimination to work even when called through log(level:). When no traits are enabled, the original zero-overhead path is preserved. Level-specific methods has no additional overhead.
API stability
For existing Logger users:
- When no traits are enabled (the default), behavior is identical to before
- Users can opt in to traits at the application level without modifying any source code
For existing LogHandler implementations:
- No changes are required to any
LogHandlerimplementation
Future directions
No future directions identified.
Alternatives considered
Per-log-level traits
An alternative approach would be to define one trait per log level that could be individually disabled:
traits: [
.trait(name: "DisableTraceLogs"),
.trait(name: "DisableDebugLogs"),
.trait(name: "DisableInfoLogs"),
.trait(name: "DisableNoticeLogs"),
.trait(name: "DisableWarningLogs"),
.trait(name: "DisableErrorLogs"),
.trait(name: "DisableCriticalLogs"),
]Users would specify multiple traits to eliminate multiple levels:
.package(
url: "https://github.com/apple/swift-log.git",
from: "1.0.0",
traits: ["DisableTraceLogs", "DisableDebugLogs", "DisableInfoLogs", "DisableNoticeLogs"]
)This approach was rejected in favor of a MaxLogLevel semantics to make it less verbose to use the functionality and to be aligned with the similar functionality in logging packages in other languages users might already be familiar with.
File: Sources/Logging/Docs.docc/Proposals/SLG-0003.md
SLG-0003: Support for Attaching Errors to Log Entries
Add standardized way of attaching data from Error instances to log entries.
Overview
- Proposal: SLG-0003
- Author(s): Samuel Murray
- Status: Approved
- Issue: apple/swift-log#291
- Implementation:
- Related links:
Introduction
Add new methods on Logger that accept an Error instance, which is passed as-is to the underlying LogHandler. To ensure backwards compatibility and a smooth adoption, a default implementation where the error is serialized into MetadataValues is provided.
Motivation
SwiftLog advocates for the use of structured logging, which in its implementation is achieved by using the metadata attribute in the log methods. This is important since it allows for automated tools to parse the logs and extract metadata values for filtering and/or visualization. When logging in a catch-block, it often makes sense to attach some information from the thrown Error in the log post, so as to capture the root cause.
do {
try someThrowingFunc()
} catch {
logger.warning("Something went wrong") // Want to somehow attach information about the error
}Currently, there is no standardized way of doing this. The simplest way to do it would be to simply attach the stringified error to the message.
logger.warning("Something went wrong: \(error)")This of course is not structured, so a better way would be:
logger.warning("Something went wrong", metadata: ["error": "\(error)"])However, since there is no clear preference for what metadata key to use, it is likely that different code bases use variations of the above. This makes it hard for automated tools to extract error information, since there is no single key to look for.
Additionally, there is more information that we could extract from the error, such as the type name.
logger.warning("Something went wrong", metadata: ["error.message": "\(error)", "error.type": "\(String(reflecting: type(of: error)))"])The above is a reasonable way to log a generic error, but it is unlikely that everyone will use the same metadata keys. It is also quite a lot of code, for something that feels like it should be easy to do - i.e. attach an error to a log.
Finally, even if you log errors in a consistent way in your code, you do not control logs generated in other modules/packages that you use. In practice, this makes it so that different parts of your application could log error information under different keys, which, again, makes it harder for tooling.
Proposed solution
The solution we propose is to add convenience methods in Logger that allows the caller to do the following:
do {
try someThrowingFunc()
} catch {
logger.warning("Something went wrong", error: error)
}The error would get passed as-is to the underlying LogHandler, which is free to serialize it in an appropriate way. We propose that such convenience methods are added for all log methods (even if it arguably makes less sense for e.g. trace and debug than warning).
There are two clear gains from this:
- It is obvious for the caller how to attach an
Errorto a log (and the caller code also looks cleaner). LogHandlers are free to serialize theErrorin an way appropriate to that handler.
A LogHandler instance can access the Error using a new protocol method. A typical LogHandler implementation could be to extract the string representation and type name, and serialize it as metadata.
struct ErrorLogHandler: LogHandler {
func log(
level: Logger.Level,
message: Logger.Message,
error: (any Error)?,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt
) {
var metadata = metadata ?? [:]
if let error {
metadata.merge([
"error.message": "\(error)",
"error.type": "\(String(reflecting: type(of: error)))",
]) { $1 }
}
// ...
}
/* ... */
}As we extend the API of both Logger and LogHandler, we need to ensure compatibility with existing solutions on both sides. Specifically, users should be able to use the new Logger methods with existing LogHandlers, and get a reasonable serialization of the error. Otherwise, early adopters would have to manually serialize it in addition to passing it using the new argument, until all LogHandlers have been updated. To achieve this, we add a default implementation on the LogHandler method, that serializes the Error as metadata, similar to the example above.
Detailed design
The proposed solution is to add a new overload for each log method (trace, ..., critical) that accepts an Error as its second argument, as well as a new overload for Logger.log, that accepts an Error as its third argument.
extension Logger {
/// Log a message at the 'trace' log level with the source that you provide.
///
/// If ``Level/trace`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func trace(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'debug' log level with the source that you provide.
///
/// If ``Level/debug`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func debug(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'info' log level with the source that you provide.
///
/// If ``Level/info`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func info(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'notice' log level with the source that you provide.
///
/// If ``Level/notice`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func notice(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'warning' log level with the source that you provide.
///
/// If ``Level/warning`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func warning(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'error' log level with the source that you provide.
///
/// If ``Level/error`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#fileID`.
/// - function: The function this log message originates from. There's usually no need to pass it explicitly, as
/// it defaults to `#function`.
/// - line: The line this log message originates from. There's usually no need to pass it explicitly, as it
/// defaults to `#line`.
@inlinable
public func error(
_ message: @autoclosure () -> Logger.Message,
error: (any Error)?,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
file: String = #fileID,
function: String = #function,
line: UInt = #line
)
/// Log a message at the 'critical' log level with the source that you provide.
///
/// If ``Level/critical`` is at least as severe as this logger's ``logLevel`` the system logs the message;
/// otherwise nothing will happen.
///
/// - parameters:
/// - message: The message to be logged. The `message` parameter supports any string interpolation literal.
/// - error: An `Error` to attach to this log message.
/// - metadata: One-off metadata to attach to this log message.
/// - source: The source this log message originates from. The value defaults
/// to the module that emits the log message.
/// - file: The file this log message originates from. There's u