Skip to content

Commit f406145

Browse files
committed
Add new best practices for structured logging and accepting loggers
1 parent 1be2c17 commit f406145

File tree

4 files changed

+212
-14
lines changed

4 files changed

+212
-14
lines changed

Sources/Logging/Docs.docc/BestPractices/001-ChoosingLogLevels.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# 001: Choosing log levels
22

3-
Best practice for selecting appropriate log levels in applications and
4-
libraries.
3+
Select appropriate log levels in applications and libraries.
54

65
## Overview
76

@@ -39,29 +38,30 @@ its logging environment.
3938

4039
#### For libraries
4140

42-
Libraries should use **info level or lower** (info, debug, trace). Each level
43-
serves different purposes:
41+
Libraries should use **info level or lower** (info, debug, trace).
4442

4543
Libraries **should not** log information on **warning or more severe levels**,
4644
unless it is a one-time (for example during startup) warning, that cannot lead
4745
to overwhelming log outputs.
4846

47+
Each level serves different purposes:
48+
4949
##### Trace Level
50-
- **Usage**: Log everything needed to diagnose hard-to-reproduce bugs
51-
- **Performance**: May impact performance; assume it won't be used in production
52-
- **Content**: Internal state, detailed operation flows, diagnostic information
50+
- **Usage**: Log everything needed to diagnose hard-to-reproduce bugs.
51+
- **Performance**: May impact performance; assume it won't be used in production.
52+
- **Content**: Internal state, detailed operation flows, diagnostic information.
5353

5454
##### Debug Level
55-
- **Usage**: May be enabled in some production deployments
56-
- **Performance**: Should not significantly undermine production performance
57-
- **Content**: High-level operation overview, connection events, major decisions
55+
- **Usage**: May be enabled in some production deployments.
56+
- **Performance**: Should not significantly undermine production performance.
57+
- **Content**: High-level operation overview, connection events, major decisions.
5858

5959
##### Info Level
6060
- **Usage**: Reserved for things that went wrong but can't be communicated
61-
through other means like throwing from a method
61+
through other means, like throwing from a method.
6262
- **Examples**: Connection retry attempts, fallback mechanisms, recoverable
63-
failures
64-
- **Guideline**: Use sparingly - not for normal successful operations
63+
failures.
64+
- **Guideline**: Use sparingly - Don't use for normal successful operations.
6565

6666
#### For applications
6767

@@ -133,6 +133,6 @@ logger.info("Response sent")
133133

134134
// ✅ Good: Use appropriate levels instead
135135
logger.debug("Processing request", metadata: ["path": "\(path)"])
136-
logger.trace("Query", , metadata: ["path": "\(query)"])
136+
logger.trace("Query", metadata: ["sql": "\(query)"])
137137
logger.debug("Request completed", metadata: ["status": "\(status)"])
138138
```
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# 002: Structured logging
2+
3+
Use metadata to create machine-readable, searchable log entries.
4+
5+
## Overview
6+
7+
Structured logging uses metadata to separate human-readable messages from
8+
machine-readable data. This practice makes logs easier to search, filter, and
9+
analyze programmatically while maintaining readability.
10+
11+
### Motivation
12+
13+
Traditional string-based logging embeds all information in the message text,
14+
making it more difficult for automated tools to parse and extract.
15+
Structured logging separates these concerns; messages provide human readable
16+
context while metadata provides structured data for tooling.
17+
18+
### Example
19+
20+
#### Recommended: Structured logging
21+
22+
```swift
23+
// ✅ Structured - message provides context, metadata provides data
24+
logger.info(
25+
"Accepted connection",
26+
metadata: [
27+
"connection.id": "\(id)",
28+
"connection.peer": "\(peer)",
29+
"connections.total": "\(count)"
30+
]
31+
)
32+
33+
logger.error(
34+
"Database query failed",
35+
metadata: [
36+
"query.retries": "\(retries)",
37+
"query.error": "\(error)",
38+
"query.duration": "\(duration)"
39+
]
40+
)
41+
```
42+
43+
### Advanced: Nested metadata for complex data
44+
45+
```swift
46+
// ✅ Complex structured data
47+
logger.trace(
48+
"HTTP request started",
49+
metadata: [
50+
"request.id": "\(requestId)",
51+
"request.method": "GET",
52+
"request.path": "/api/users",
53+
"request.headers": [
54+
"user-agent": "\(userAgent)"
55+
],
56+
"client.ip": "\(clientIP)",
57+
"client.country": "\(country)"
58+
]
59+
)
60+
```
61+
62+
#### Avoid: Unstructured logging
63+
64+
```swift
65+
// ❌ Not structured - hard to parse programmatically
66+
logger.info("Accepted connection \(id) from \(peer), total: \(count)")
67+
logger.error("Database query failed after \(retries) retries: \(error)")
68+
```
69+
70+
### Metadata key conventions
71+
72+
Use hierarchical dot-notation for related fields:
73+
74+
```swift
75+
// ✅ Good: Hierarchical keys
76+
logger.debug(
77+
"Database operation completed",
78+
metadata: [
79+
"db.operation": "SELECT",
80+
"db.table": "users",
81+
"db.duration": "\(duration)",
82+
"db.rows": "\(rowCount)"
83+
]
84+
)
85+
86+
// ✅ Good: Consistent prefixing
87+
logger.info(
88+
"HTTP response",
89+
metadata: [
90+
"http.method": "POST",
91+
"http.status": "201",
92+
"http.path": "/api/users",
93+
"http.duration": "\(duration)"
94+
]
95+
)
96+
```
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# 003: Accepting loggers in libraries
2+
3+
Accept loggers through method parameters to ensure proper metadata propagation.
4+
5+
## Overview
6+
7+
Libraries should accept logger instances through method parameters rather than
8+
storing them as instance variables. This practice ensures metadata (such as
9+
correlation IDs) is properly propagated down the call stack, while giving
10+
applications control over logging configuration.
11+
12+
### Motivation
13+
14+
When libraries accept loggers as method parameters, they enable automatic
15+
propagation of contextual metadata attached to the logger instance. This is
16+
especially important for distributed systems where correlation IDs must flow
17+
through the entire request processing pipeline.
18+
19+
### Example
20+
21+
#### Recommended: Accept logger through method parameters
22+
23+
```swift
24+
// ✅ Good: Pass the logger through method parameters.
25+
struct RequestProcessor {
26+
func processRequest(_ request: HTTPRequest, logger: Logger) async throws -> HTTPResponse {
27+
// Add structured metadata that every log statement should contain.
28+
var logger = logger
29+
logger[metadataKey: "request.method"] = "\(request.method)"
30+
logger[metadataKey: "request.path"] = "\(request.path)"
31+
logger[metadataKey: "request.id"] = "\(request.id)"
32+
33+
logger.debug("Processing request")
34+
35+
// Pass the logger down to maintain metadata context.
36+
let validatedData = try validateRequest(request, logger: logger)
37+
let result = try await executeBusinessLogic(validatedData, logger: logger)
38+
39+
logger.debug("Request processed successfully")
40+
return result
41+
}
42+
43+
private func validateRequest(_ request: HTTPRequest, logger: Logger) throws -> ValidatedRequest {
44+
logger.debug("Validating request parameters")
45+
// Include validation logic that uses the same logger context.
46+
return ValidatedRequest(request)
47+
}
48+
49+
private func executeBusinessLogic(_ data: ValidatedRequest, logger: Logger) async throws -> HTTPResponse {
50+
logger.debug("Executing business logic")
51+
52+
// Further propagate the logger to other services.
53+
let dbResult = try await databaseService.query(data.query, logger: logger)
54+
55+
logger.debug("Business logic completed")
56+
return HTTPResponse(data: dbResult)
57+
}
58+
}
59+
```
60+
61+
#### Alternative: Accept logger through initializer when appropriate
62+
63+
```swift
64+
// ✅ Acceptable: Logger through initializer for long-lived components
65+
final class BackgroundJobProcessor {
66+
private let logger: Logger
67+
68+
init(logger: Logger) {
69+
self.logger = logger
70+
}
71+
72+
func run() async {
73+
// Execute some long running work
74+
logger.debug("Update about long running work")
75+
// Execute some more long running work
76+
}
77+
}
78+
```
79+
80+
#### Avoid: Libraries creating their own loggers
81+
82+
Libraries might create their own loggers; however, this leads to two problems.
83+
First, users of the library can't inject their own loggers which means they have
84+
no control in customizing the log level or log handler. Secondly, it breaks the
85+
metadata propagation since users can't pass in a logger with already attached
86+
metadata.
87+
88+
```swift
89+
// ❌ Bad: Library creates its own logger
90+
final class MyLibrary {
91+
private let logger = Logger(label: "MyLibrary") // Loses all context
92+
}
93+
94+
// ✅ Good: Library accepts logger from caller
95+
final class MyLibrary {
96+
func operation(logger: Logger) {
97+
// Maintains caller's context and metadata
98+
}
99+
}
100+
```

Sources/Logging/Docs.docc/LoggingBestPractices.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ practice includes:
3939
## Topics
4040

4141
- <doc:001-ChoosingLogLevels>
42+
- <doc:002-StructuredLogging>
43+
- <doc:003-AcceptingLoggers>

0 commit comments

Comments
 (0)