Define a custom AutoLogger instance and use it with macros.
This article focuses on creating a custom AutoLogger instance.
The example below depends on SentrySDK. You can also create a free account on sentry.io if you want to send events to your own project.
Code examples used throughout this article are adapted from the former Count Me In example app. The first line in each code snippet refers to a file within the app.
Since we will be extending types from both libraries, their imports must be marked as public.
// SentryLogger.swift
public import AutoLog
public import SentryAutoLogger has two requirements, conforming type must:
- Be
Sendable - Implement
emit(event: AutoLogEvent, configuration: AutoLogConfiguration)method
This method is called each time a function either completes successfully or throws an error.
When the default AutoLogLogger handles a failure, it also populates
event.errorContext with a rolling trail of recent emitted log summaries.
// SentryLogger.swift
public import AutoLog
public import Sentry
struct SentryLogger: AutoLogger {
func emit(event: AutoLogEvent, configuration: AutoLogConfiguration) {
// ...
}
}Note that not all events emitted via emit(event:configuration:) represent errors - successful completions of functions are also captured. We can leverage this by using Sentry's ability to attach breadcrumbs to errors when they occur, effectively creating a backtrace of user actions leading up to the failure.
Note: Learn more about breadcrumbs and scopes.
// SentryLogger.swift
extension AutoLogEvent {
var sentryBreadcrumb: Sentry.Breadcrumb {
let breadcrumb = Sentry.Breadcrumb()
if let level = self.level as? SentryLevel {
breadcrumb.level = level
}
breadcrumb.category = "Action"
breadcrumb.type = "user"
breadcrumb.message = self.description
return breadcrumb
}
var sentryErrorTags: [String: String] {
[
"error.function.signature": self.declaration,
"error.function.parameters": self.parameters.mapValues { String(reflecting: $0) }.description,
"error.function.location": self.location,
]
}
}Now that we've identified the information to log on both success and failure, it's time to implement the logic that will run when the emit method is invoked.
// SentryLogger.swift
public import Sentry
public import AutoLog
struct SentryLogger: AutoLogger {
func emit(event: AutoLogEvent, configuration _: AutoLogConfiguration) {
switch event.result {
case .success:
SentrySDK.addBreadcrumb(event.sentryBreadcrumb)
case let .failure(error):
SentrySDK.capture(error: error) { scope in
scope.setTags(event.sentryErrorTags)
}
}
}
}If you want a backend to receive a subset of events, conform it to
AutoLogEventSink and give it a route name. AutoLogOutput is the source
of truth for which events reach the destination: route, event types, type-name
filters, timing settings, failure context, formatting, and parameter privacy all
live on the output profile. Untagged events are eligible for every configured
output, so ordinary app code does not need backend-specific route tags.
// SplunkSink.swift
public import AutoLog
struct SplunkSink: AutoLogEventSink {
let route: AutoLogRoute = .splunk
func emit(event: AutoLogEvent, configuration: AutoLogConfiguration) {
// Send `event` to Splunk.
}
}
AutoLog.start(
configuration: AutoLogConfiguration(
outputs: [
.consoleDefault,
AutoLogOutput(
name: "Splunk",
route: .splunk,
eventTypes: [.interaction, .failure, .warning],
typeNameFilters: ["ViewModel"],
errorContextLimit: 10,
parameterPrivacy: .omit
)
]
),
sinks: [
SplunkSink()
]
)Explicit route tags still work when a function should go only to a narrow route:
let logger = AutoLogLogger(sinks: [SplunkSink()])
@AutoLog(using: logger)
struct AuditTrailService {
@Tag(.console, .splunk)
func uploadAuditSnapshot() {
// Console + Splunk
}
@Tag(.splunk)
func uploadBackgroundHeartbeat() {
// Splunk only
}
}You can determine whether the attached function emitted a success or failure event by checking event.result.
If your backend supports its own breadcrumb or trail concept, event.errorContext
gives you the recent console-ready summaries that immediately preceded the failure.
Crashlytics can be modeled as a normal sink selected by a .firebase output.
The package does not need a .crashlyticsAndConsole logger identity or any
Firebase-specific macros.
#if canImport(FirebaseCrashlytics)
import AutoLog
import FirebaseCrashlytics
struct CrashlyticsAutoLogSink: AutoLogEventSink {
let route: AutoLogRoute = .firebase
func emit(event: AutoLogEvent, configuration: AutoLogConfiguration) {
let crashlytics = Crashlytics.crashlytics()
crashlytics.setCustomValue(event.inferredTypeName, forKey: "autolog.type")
crashlytics.setCustomValue(event.functionName, forKey: "autolog.function")
crashlytics.setCustomValue(event.shortLocation, forKey: "autolog.location")
if case .failure(let error) = event.result {
crashlytics.record(error: error)
return
}
crashlytics.log("\(event.outputEventType.rawValue): \(event.inferredTypeName).\(event.functionName)")
}
}
#endifConfigure the output to decide what Crashlytics receives:
AutoLog.start(
configuration: AutoLogConfiguration(
outputs: [
AutoLogOutput.consoleDefault,
AutoLogOutput(
name: "Crashlytics",
route: .firebase,
eventTypes: [.interaction, .failure, .warning],
typeNameFilters: ["ViewModel"],
errorContextLimit: 10,
parameterPrivacy: .omit,
metadataKeyAllowList: [
"autolog.type",
"autolog.function",
"autolog.location"
]
)
]
),
sinks: [
CrashlyticsAutoLogSink()
]
)For cleaner syntax and easier usage of SentryLogger, create an extension to AutoLogger and declare a conditional conformance to it.
// SentryLogger.swift
extension AutoLogger where Self == SentryLogger {
static var sentry: any AutoLogger {
SentryLogger()
}
}Warning: When creating an extension, always use
any AutoLoggeras a type, otherwise the@AutoLogmacro will not work.
Each logger will likely have its own predefined set of logging levels. To integrate them with macros, simply conform to the Levelable protocol.
Levelable requires that a type is Sendable, ExpressibleByStringLiteral, and implements the static method static func level(_ value: RawValue) -> Self.
Sentry levels are represented by the SentryLevel enum. To make it conform to Levelable, start by adding conformance to Sendable and ExpressibleByStringLiteral.
// SentryLogger.swift
extension SentryLevel: @retroactive @unchecked Sendable {}
extension SentryLevel: @retroactive ExpressibleByStringLiteral {}Now, we must fulfill the requirements of both the Levelable and ExpressibleByStringLiteral protocols.
To satisfy Levelable protocol, define the required static function where the RawValue type matches the underlying type of the extended logging level, in case of SentryLevel it is UInt. Provide a fallback level of your choice for values that don’t match predefined cases.
Additionally, implement the ExpressibleByStringLiteral initializer. This is necessary because the @Level macro allows you to pass either a string literal (e.g. @Level("fatal")) or any Levelable type (e.g. @Level(.sentryFatal)), both of which should result in the same SentryLevel.fatal value.
// SentryLogger.swift
extension SentryLevel: @retroactive Levelable {
public static func level(_ value: UInt) -> Self {
SentryLevel(rawValue: value) ?? SentryLevel.none
}
public init(stringLiteral value: StringLiteralType) {
switch value {
case "none":
self = SentryLevel.none
case "debug":
self = SentryLevel.debug
case "info":
self = SentryLevel.info
case "warning":
self = SentryLevel.warning
case "error":
self = SentryLevel.error
case "fatal":
self = SentryLevel.fatal
default:
self = SentryLevel.none
}
}
}As previously mentioned, macros such as Level(_:) and Log(using:level:omit:tag:) overload accept any type conforming to Levelable as a parameter. For a cleaner and more intuitive syntax, add a conditional conformance to the Levelable protocol.
Tip: String conforms to
Levelable,OmittableandTaggableprotocols.
// SentryLogger.swift
extension Levelable where Self == SentryLevel {
static var sentryNone: any Levelable {
SentryLevel.none
}
static var sentryDebug: any Levelable {
SentryLevel.debug
}
static var sentryInfo: any Levelable {
SentryLevel.info
}
static var sentryWarning: any Levelable {
SentryLevel.warning
}
static var sentryError: any Levelable {
SentryLevel.error
}
static var sentryFatal: any Levelable {
SentryLevel.fatal
}
}After that, any error thrown within a type or function marked with either AutoLog(using:) or Log(using:) will be sent to Sentry, including all breadcrumbs leading up to the error.