Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions Sources/Logging/Docs.docc/Proposals/SLG-0002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 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](https://github.com/kukushechkin)
- Status: **Ready for Implementation**
- Issue: [apple/swift-log#378](https://github.com/apple/swift-log/issues/378)
- Implementation:
- [apple/swift-log#389](https://github.com/apple/swift-log/pull/389)
- Related links:
- [Lightweight proposals process description](https://github.com/apple/swift-log/blob/main/Sources/Logging/Docs.docc/Proposals/Proposals.md)

### 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 DEBUG` checks (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`:

```swift
dependencies: [
.package(
url: "https://github.com/apple/swift-log.git",
from: "1.0.0",
traits: ["MaxLogLevelWarning"]
)
]
```

With `MaxLogLevelWarning` enabled:

```swift
// 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:

```swift
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:

```swift
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 `LogHandler` implementation

### 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:

```swift
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:

```swift
.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.