swift-loggable package is a set of macros that support type-wide and per-function logging with ability to customize how logs are handled.
Macros within this package can be loosely divided into four groups
Only supports functions, capturing their signature, source location, parameters, return values, and any errors thrown at runtime. As of now static, throwing, async, and generic functions are supported with standard arguments as well as inout arguments, closures, and @autoclosures.
Note
Passing traits directly to @Log
or @OSLog
has the same effect as using dedicated trait macros
Accepts an optional any Loggable
instance along with optional traits. Can be used standalone or within an @Logged
context - when used inside @Logged
, it overrides any parameters passed to @Logged
in that context.
A specialized version of @Log
that does not accept an any Loggable
parameter as it uses Logger
introduced by OSLogger
protocol
Warning
@OSLog
must be used within a context annotated with @OSLogger
or one that conforms to the OSLogger
protocol
Type-wide and extension macros that introduces @Log
or @OSLog
annotations to all methods within their scope. To omit function from being logged use @Omit
macro without no parameters.
Note
Both @Logged
and @OSLogged
cannot be attached to protocols
Takes any Loggable
instance as a parameter. If provided, it is applied to all functions within its scope, unless explicitly opted out. By default, it uses Logger
with default subsystem.
Specialized implementation of @Logged
macro that marks all functions within its scope with @OSLog
. Does not take any parameters.
Both macros internally rely on the Logger
. Each macro allows for overriding the subsystem and category through parameters, with the default subsystem set to the bundle identifier and the default category set to the declaration name.
Adds conformance to OSLogger
protocol and introduces a static instance of Logger
to attached context.
Creates a static instance of Logger
in the invoked context, without adding conformance to OSLogger
protocol.
Note
#osLogger
can only be declared on a type as it introduces static property
Can only by attached to functions and must always proceed @Log
or @OSLog
macros applied explicitly or implicitly by @Logged
or @OSLogged
.
Note
The only exception from this rule is @Omit
with not parameters
Overrides level of which event is emitted. By default @Log
and @OSLog
level is set to .info
when function succeeds or .error
when error is thrown. OSLogType
conforms to this protocol.
Can be used with or without parameters, in the last case @Logged
or @OSLogged
macros will not be expanded. Currently @Omit
allows to ignore omit result, specific parameter, or all parameters.
Takes range of parameters that conforms to Taggable
protocol. Passed parameters are attached to emitted event.
Consider this code as a starting point
struct Foo {
func bar(...) async throws -> Bar { ... }
static func baz(...) -> Baz { ... }
func qux() { ... }
}
extension Foo {
mutating func quux() -> Self { ... }
}
To log every method within Foo
, simply annotate it with @Logged
+ @Logged
struct Foo { ... }
The code will be expanded as follows:
Note
Methods within extension of Foo
will not be affected
@Logged
struct Foo {
+ @Log
func bar(...) async throws { ... }
+ @Log
static func baz(...) -> Baz { ... }
+ @Log
mutating func qux() { ...} -> Self
}
extension Foo {
static func quux() -> Self { ... }
}
To log a method inside an extension, you can either annotate it with @Logged
, as shown earlier, or use @Log
. Functions annotated with @Log
expand to something like this:
extension Foo {
+ @Log
static func quux() -> Self {
+ let loggable: any Loggable = .logger
+ var event = LoggableEvent(
+ location: "Module/Foo.swift:13:37",
+ declaration: "mutating func quux() -> Self",
+ tags: []
+ )
+ func _static func quux() -> Self { ... }
+ let result =_quux()
+ event.result = .success(result)
+ loggable.emit(event: event)
+ return result
}
}
Loggable was built on the premise of not binding to a specific logging mechanism. To replace the default logic, conform the desired logger to the Loggable
protocol, like this:
struct NSLogger: Loggable {
func emit(event: LoggableEvent) {
NSLog("%@", event.description)
}
}
Additionally, for nicer syntax create an extension for Loggable
, as both @Logged
and @Log
accept any Loggable
as a parameter.
extension Loggable where Self == NSLogger {
static var nsLogger: Self { NSLogger() }
}
Now, it can be passed as a parameter to either @Log
or @Logged
as follows:
extension Foo {
+ @Log(using: .nsLogger)
static func quux() -> Self { ... }
}
When .nsLogger
or any other type that conforms to Loggable
protocol is passed as a parameter to @Logged
, it is propagated to all methods within attached context.
@Logged(using: .nsLogger)
struct Foo {
+ @Log(using: .nsLogger)
func bar(...) async throws { ... }
+ @Log(using: .nsLogger)
static func baz(...) -> Baz { ... }
+ @Log(using: .nsLogger)
mutating func qux() { ...} -> Self
}
Unlike the @Logged
macro, to apply @OSLog
to functions within a scope, the scope must first be annotated with @OSLogger
or conform to the OSLogger
protocol.
+ @OSLogger
struct Foo { ... }
After expansion, static instance of logger
has been introduced to scope as well conformance to OSLogger
.
@OSLogger
struct Foo { ... }
+ extension Foo: OSLogger {
+ static let logger = Logger(
+ subsystem: "Module"
+ category: "Foo"
+ )
+ }
Once conformed to the OSLogger protocol, we can add @OSLogged, which will apply @OSLog to each method within the scope.
@OSLogger
+ @OSLogged
struct Foo { ... }
Similarly to @Logged
, it expands like this:
@OSLogger
@OSLogged
struct Foo {
+ @OSLog
func bar(...) async throws { ... }
+ @OSLog
static func baz(...) -> Baz { ... }
+ @OSLog
mutating func qux() { ...} -> Self
}
extension Foo {
static func quux() -> Self { ... }
}
Note
Methods within extension of Foo
will not be affected
Subsystem or a category can be overridden by explicitly passing it as a parameter to @OSLogger
. Order of @OSLogger
and @OSLogged
does not matter, they are expanded independently. Final code after expansion looks as follows:
@OSLogger(subsystem: "Example", category: "Readme")
@OSLogged
struct Foo {
+ @OSLog
func bar(...) async throws { ... }
+ @OSLog
static func baz(...) -> Baz { ... }
+ @OSLog
mutating func qux() { ...} -> Self
}
+ extension Foo: OSLogger {
+ static let logger = Logger(
+ subsystem: "Example"
+ category: "Readme"
+ )
+ }
In cases where @OSLogger
cannot be directly used on a type, you can create an extension for the desired type, add conformance to the OSLogger
protocol, and invoke the #osLogger
macro, like this:
+ extension Bar: OSLogger {
+ #osLogger
}
This is how the code will be expanded:
extension: Bar: OSLogger {
+ static let logger = Logger(
+ subsystem: "Module"
+ category: "Bar"
+ )
}
Each of this macros can be use together, excluding @Omit
with not parameters as it would not make any sense. Using these macros is the same as providing parameters explicitly to @Log
and @OSLog
. Redundant parameters are ignored. Both of the following examples produce the same result.
extension Foo {
@Log(level: .debug, omit: .result, tag: "Example")
static func quux() -> Self { ... }
}
extension Foo {
@Tag("Example)
@Level(.debug)
@Omit(.result)
@Log
static func quux() -> Self { ... }
}
Each of this macros comes with their own protocol, Omittable
, Taggable
and Levelable
. All protocols conforms to Sendable & Hashable & ExpressibleByStringLiteral
. In each section below, both examples produces the same output.
extension Omittable where Self == OmittableTrait {
static var privateKey: Self { .parameter("privateKey") }
}
@OSLogged
extension Foo {
@Omit(.privateKey)
static func quux(privateKey: Data) -> Self { ... }
}
@OSLogged
extension Foo {
@OSLog(omit: "privateKey")
static func quux(privateKey: Data) -> Self { ... }
}
Warning
Omittable
internally uses result
and parameters
keywords, passing them as String
into @Omit()
or eg. @Log(omit:)
, will not ignore a parameter named result
, but will instead omit the actual function result from being captured.
extension Taggable where Self == TaggableTrait {
static var biometrics: Self { .parameter("Biometrics") }
}
extension Foo {
@Tag(.biometrics)
@Log(using: .nsLogger)
static func quux() -> Self { ... }
}
extension Foo {
@Log(using: .nsLogger, tag: "Biometrics")
static func quux() -> Self { ... }
}
extension Levelable where Self == LevelableTrait {
static var warning: Self { .level("warning") }
}
@Logged
extension Foo {
@Level(.warning)
static func quux() -> Self { ... }
}
extension Foo {
@Log(level: "warning")
static func quux() -> Self { ... }
}
Add the following dependency to your Package.swift
.package(url: "https://github.com/23122K/swift-loggable.git", branch: "main"),
Alternatively, Project → Package dependencies → + → Search or enter package URL and paste
https://github.com/23122K/swift-loggable.git
In both cases, choose dependency rule of your choice.