SwiftLog

repository·main·Indexed 26 days ago

https://github.com/apple/swift-log

A unified, performant, and ergonomic logging API for the Swift ecosystem. SwiftLog serves as an abstraction layer that allows libraries and applications to log messages without being tied to a specific logging implementation. It provides a structured logging API with support for various log levels, metadata, and task-local logger propagation via Logger.current.

Tokens
16K
Snippets
35
Records
87
Agent score
86%

What's inside swift-log

  1. Create a StreamLogHandler for standard output or error

    main

    You can create a StreamLogHandler that writes logs to standard output or standard error using convenience factory methods. These handlers can optionally take a metadataProvider to dynamically supply metadata for every log message.

    Available factory methods:

    • standardOutput(label:)
    • standardOutput(label:metadataProvider:)
    • standardError(label:)
    • standardError(label:metadataProvider:)
    • init(label:stream:)
    • init(label:stream:metadataProvider:)
  2. Propose API changes to SwiftLog

    main

    For non-trivial changes affecting the public API, SwiftLog uses a lightweight proposal process similar to Swift Evolution. This allows for community feedback and discussion of multiple solutions before implementation.

    Steps to propose a change:

    1. Create an Issue: Ensure there is a GitHub issue describing the feature or change.
    2. Create Proposal Document: Duplicate the SLG-NNNN.md template, replacing NNNN with the next available proposal number.
    3. Fill Proposal: Link the GitHub issue within your proposal and complete the required sections.
    4. Open Pull Request: Submit a PR containing your proposal to solicit feedback.
    5. Review Period: Once a maintainer marks the proposal as ready, there is a 7-day review period. The state will transition to Ready for Implementation or Deferred.
    6. Implementation: An implementation must be ready (either in the same PR or a linked separate PR) before the proposal is merged.
    7. Approval: A proposal is considered Approved once the implementation and proposal PRs are merged and any necessary feature flags are enabled.
  3. Pass a Logger through method parameters

    main

    For APIs where logging is a relevant concern, pass a Logger instance through method parameters. It is a best practice to default the parameter to Logger.current to allow callers to use the task-local logger automatically while still providing the option for explicit injection.

    // ✅ Good: Pass the logger through method parameters,
    //          default to Logger.current in the public API.
    struct RequestProcessor {
        func processRequest(_ request: HTTPRequest, logger: Logger = Logger.current) async throws -> HTTPResponse {
            // Add structured metadata that every log statement should contain.
            var logger = logger
            logger[metadataKey: "request.method"] = "\(request.method)"
            logger[metadataKey: "request.path"] = "\(request.path)"
            logger[metadataKey: "request.id"] = "\(request.id)"
    
            logger.debug("Processing request")
    
            // Pass the logger down to maintain metadata context.
            let validatedData = try validateRequest(request, logger: logger)
            let result = try await executeBusinessLogic(validatedData, logger: logger)
    
            logger.debug("Request processed successfully")
            return result
        }
    
        private func validateRequest(_ request: HTTPRequest, logger: Logger) throws -> ValidatedRequest {
            logger.debug("Validating request parameters")
            return ValidatedRequest(request)
        }
    
        private func executeBusinessLogic(_ data: ValidatedRequest, logger: Logger) async throws -> HTTPResponse {
            logger.debug("Executing business logic")
            let dbResult = try await databaseService.query(data.query, logger: logger)
            logger.debug("Business logic completed")
            return HTTPResponse(data: dbResult)
        }
    }
  4. Choose appropriate log levels for libraries

    main

    When developing a library, you should use info level or less severe (info, debug, trace) to avoid overwhelming the host application's logging system.

    • trace: Use for detailed diagnostic information and internal state needed to diagnose hard-to-reproduce bugs. Assume this will not be used in production.
    • debug: Use for high-level operational overviews, such as connection events or major decisions. This may be enabled in some production environments.
    • info: Use sparingly for issues that cannot be communicated through other means (e.g., recoverable failures like connection retries). Do not use info for normal successful operations.

    Warning/Error levels: Libraries should generally not log at warning or more severe levels unless it is a one-time event (e.g., during startup) that cannot flood the logs.

    // ✅ Good: Trace level for detailed diagnostics
    logger.trace("Connection pool state", metadata: [
        "active": "\(activeConnections)",
        "idle": "\(idleConnections)",
        "pending": "\(pendingRequests)"
    ])
    
    // ✅ Good: Debug level for high-value operational info
    logger.debug("Database connection established", metadata: [
        "host": "\(host)",
        "database": "\(database)",
        "connectionTime": "\(duration)"
    ])
    
    // ✅ Good: Info level for issues that can't be communicated through other means
    logger.info("Connection failed, retrying", metadata: [
        "attempt": "\(attemptNumber)",
        "maxRetries": "\(maxRetries)",
        "host": "\(host)"
    ])
  5. Create and configure Loggers

    main

    A Logger is a value type used to emit log messages at various severity levels. Because it has value semantics, modifying a logger (such as changing its logLevel or adding metadata) creates a new configuration for that specific instance without affecting the original logger. This makes it safe to pass loggers between functions.

    let baseLogger = Logger(label: "MyApp")
    
    // Create a new logger with different configuration.
    var requestLogger = baseLogger
    requestLogger.logLevel = .debug
    requestLogger[metadataKey: "request-id"] = "\(UUID())"
    
    // baseLogger remains unchanged.
    // requestLogger has debug level and request-id metadata.
  6. Propagate loggers in libraries

    main

    When building libraries, do not construct your own logger using Logger(label:). Instead, propagate the caller's context to ensure metadata (like correlation IDs), log levels, and handler choices are preserved.

    Use one of these two methods:

    1. Accept a Logger parameter: Best when the API naturally allows for it or when you want to be explicit about logging.
    2. Read Logger.current from the task-local: Best when adding a logger parameter would unnecessarily pollute the API signature. This relies on Swift's structured concurrency.

    Avoid constructing loggers inside a library, as this prevents the application from controlling, filtering, or redirecting that library's output.

    // ✅ Good: Library reads Logger.current; caller scopes context via withLogger.
    public struct AnalyticsClient {
        public func track(_ event: String) {
            Logger.current.info("event", metadata: ["event.name": "\(event)"])
        }
    }
    
    // Application binds at @main and scopes per-request metadata.
    @main
    struct MyServer {
        static func main() async throws {
            let logger = Logger(label: "my-server")
            try await withLogger(logger) { _ in
                try await runServices()
            }
        }
    }
    
    func handleRequest(_ req: HTTPRequest) async throws {
        try await withLogger(mergingMetadata: ["request.id": "\(req.id)"]) { _ in
            AnalyticsClient().track("request.received")    // sees request.id automatically
        }
    }
  7. Implement structured logging with metadata

    main

    To make logs machine-readable and searchable, use the metadata parameter in logging calls instead of embedding data directly into the message string. The message should provide human-readable context, while the metadata dictionary provides the structured data for programmatic analysis.

    Avoid unstructured logging: Do not embed variables directly into the message string (e.g., logger.info("User \(id) logged in")), as this makes it difficult for automated tools to parse and filter logs.

    // ✅ Recommended: Structured logging
    // The message provides context, metadata provides data
    logger.info(
        "Accepted connection",
        metadata: [
            "connection.id": "\(id)",
            "connection.peer": "\(peer)", 
            "connections.total": "\(count)"
        ]
    )
    
    // ❌ Avoid: Unstructured logging
    // Hard to parse programmatically
    logger.info("Accepted connection \(id) from \(peer), total: \(count)")
  8. Construct handlers with complex initialization

    main

    While many handlers use a label, backends often require additional configuration like file paths, remote addresses, or credentials.

    Best Practices

    • Early Resource Allocation: Open expensive or fallible resources (like files or sockets) in your own custom initializer that can throw. This allows setup failures to surface early during application bootstrap rather than during a logging call.
    • Use Reference Types for Shared State: To keep your LogHandler a cheap, copyable struct while sharing a single destination (like a file handle), place the shared resource behind a thread-safe reference type (e.g., a final class with a Mutex).
    • Value Semantics for Configuration: Remember that value semantics apply to the handler's configuration (level and metadata), not its destination. Multiple copies of a handler writing to the same file is expected behavior.
  9. Choose a logging backend

    main
    SwiftLog is an API-only package. To actually output logs to a destination (like the console, a file, or a remote server), you must choose and install a community-maintained logging backend. You can find available implementations by searching for swift-log on the Swift Package Index.
  10. Follow metadata key conventions

    main

    When defining keys for metadata, follow these conventions to ensure consistency and ease of searching:

    1. Hierarchical dot-notation: Use dots to group related fields (e.g., db.operation, db.table).
    2. Consistent prefixing: Use a common prefix for related categories of data (e.g., http.method, http.status, http.path).
    // ✅ Good: Hierarchical keys
    logger.debug(
        "Database operation completed",
        metadata: [
            "db.operation": "SELECT",
            "db.table": "users",
            "db.duration": "\(duration)",
            "db.rows": "\(rowCount)"
        ]
    )
    
    // ✅ Good: Consistent prefixing
    logger.info(
        "HTTP response",
        metadata: [
            "http.method": "POST",
            "http.status": "201",
            "http.path": "/api/users",
            "http.duration": "\(duration)"
        ]
    )