Skip to content

Commit c2de357

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

File tree

4 files changed

+217
-12
lines changed

4 files changed

+217
-12
lines changed

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

Lines changed: 11 additions & 12 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

@@ -47,21 +46,21 @@ unless it is a one-time (for example during startup) warning, that cannot lead
4746
to overwhelming log outputs.
4847

4948
##### 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
49+
- **Usage**: Log everything needed to diagnose hard-to-reproduce bugs.
50+
- **Performance**: May impact performance; assume it won't be used in production.
51+
- **Content**: Internal state, detailed operation flows, diagnostic information.
5352

5453
##### 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
54+
- **Usage**: May be enabled in some production deployments.
55+
- **Performance**: Should not significantly undermine production performance.
56+
- **Content**: High-level operation overview, connection events, major decisions.
5857

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

6665
#### For applications
6766

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

134133
// ✅ Good: Use appropriate levels instead
135134
logger.debug("Processing request", metadata: ["path": "\(path)"])
136-
logger.trace("Query", , metadata: ["path": "\(query)"])
135+
logger.trace("Query", metadata: ["sql": "\(query)"])
137136
logger.debug("Request completed", metadata: ["status": "\(status)"])
138137
```
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 difficult for automated tools to extract specific information.
15+
Structured logging separates concerns: messages provide human context while
16+
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)ms",
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)ms"
94+
]
95+
)
96+
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 and tracing
17+
information must flow through the entire request processing pipeline.
18+
19+
### Example
20+
21+
#### Recommended: Accept logger through method parameters
22+
23+
```swift
24+
// ✅ Good: Logger passed through method parameters
25+
struct RequestProcessor {
26+
func processRequest(_ request: HTTPRequest, logger: Logger) async throws -> HTTPResponse {
27+
// Add metadata to the logger 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 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+
// Validation logic with 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 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 processJob(_ job: Job) async {
73+
// For background jobs, method parameter approach is still preferred
74+
// to allow per-job metadata
75+
var logger = logger
76+
logger[metadataKey: "job.id"] = "\(job.id)"
77+
78+
await processJobInternal(job, logger: logger)
79+
}
80+
81+
private func processJobInternal(_ job: Job, logger: Logger) async {
82+
logger.debug("Processing job")
83+
// Process with contextual logger
84+
}
85+
}
86+
```
87+
88+
#### Avoid: Libraries creating their own loggers
89+
90+
Libraries might create their own loggers; however, this leads to two problems.
91+
First, the user of the library can't inject their own loggers which means he has
92+
no control in customizing the log level or log handler. Secondly, it breaks the
93+
metadata propagation since the user can't pass in a logger with already attached
94+
metadata.
95+
96+
```swift
97+
// ❌ Bad: Library creates its own logger
98+
final class MyLibrary {
99+
private let logger = Logger(label: "MyLibrary") // Loses all context
100+
}
101+
102+
// ✅ Good: Library accepts logger from caller
103+
final class MyLibrary {
104+
func operation(logger: Logger) {
105+
// Maintains caller's context and metadata
106+
}
107+
}
108+
```

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)