Table of contents

How do I implement request and response logging in Alamofire?

Implementing comprehensive request and response logging in Alamofire is essential for debugging network requests, monitoring API performance, and troubleshooting issues in production environments. Alamofire provides several approaches to implement logging, from built-in EventMonitor protocols to custom logging solutions.

Understanding Alamofire's EventMonitor Protocol

Alamofire's EventMonitor protocol is the recommended approach for implementing logging. It provides hooks into various stages of the request lifecycle, allowing you to capture detailed information about requests and responses.

Basic EventMonitor Implementation

Here's a basic implementation of a custom EventMonitor for logging:

import Alamofire
import Foundation

class NetworkLogger: EventMonitor {
    let queue = DispatchQueue(label: "network_logger")

    // Request lifecycle events
    func requestDidFinish(_ request: Request) {
        print("🚀 Request finished: \(request)")
    }

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        switch response.result {
        case .success:
            print("✅ Success: \(request.request?.url?.absoluteString ?? "Unknown URL")")
        case .failure(let error):
            print("❌ Error: \(error.localizedDescription)")
        }
    }
}

// Usage
let logger = NetworkLogger()
let session = Session(eventMonitors: [logger])

session.request("https://api.example.com/users")
    .responseJSON { response in
        // Handle response
    }

Comprehensive Logging with EventMonitor

For more detailed logging, implement multiple EventMonitor methods:

class DetailedNetworkLogger: EventMonitor {
    let queue = DispatchQueue(label: "detailed_network_logger")

    // Request creation
    func requestDidResume(_ request: Request) {
        let body = request.request.flatMap { $0.httpBody.map { String(decoding: $0, as: UTF8.self) } } ?? "None"
        let message = """
        ⬆️ REQUEST STARTED
        URL: \(request.request?.url?.absoluteString ?? "nil")
        Method: \(request.request?.httpMethod ?? "nil")
        Headers: \(request.request?.allHTTPHeaderFields ?? [:])
        Body: \(body)
        """
        print(message)
    }

    // Request completion
    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        let message = """
        ⬇️ RESPONSE RECEIVED
        URL: \(response.request?.url?.absoluteString ?? "nil")
        Status Code: \(response.response?.statusCode ?? 0)
        Headers: \(response.response?.allHeaderFields ?? [:])
        Data Size: \(response.data?.count ?? 0) bytes
        Duration: \(response.metrics?.taskInterval.duration ?? 0) seconds
        """
        print(message)

        // Log response body if needed
        if let data = response.data, let string = String(data: data, encoding: .utf8) {
            print("Response Body: \(string)")
        }
    }

    // Request failure
    func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {
        print("🔥 REQUEST FAILED EARLY: \(error.localizedDescription)")
    }

    // Request completion with error
    func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {
        if let error = error {
            print("💥 REQUEST COMPLETED WITH ERROR: \(error.localizedDescription)")
        }
    }
}

Using Alamofire's Built-in NetworkLogger

Alamofire provides a built-in NetworkLogger that you can use for standard logging needs:

import Alamofire

let logger = NetworkLogger()
let session = Session(eventMonitors: [logger])

// The NetworkLogger will automatically log requests and responses
session.request("https://api.example.com/data")
    .responseJSON { response in
        switch response.result {
        case .success(let value):
            print("Success: \(value)")
        case .failure(let error):
            print("Error: \(error)")
        }
    }

Custom Logging with Request/Response Interceptors

For more control over logging format and destinations, create custom interceptors:

class CustomNetworkLogger {
    enum LogLevel {
        case none, basic, detailed
    }

    private let logLevel: LogLevel
    private let dateFormatter: DateFormatter

    init(logLevel: LogLevel = .detailed) {
        self.logLevel = logLevel
        self.dateFormatter = DateFormatter()
        self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
    }

    func logRequest(_ request: URLRequest) {
        guard logLevel != .none else { return }

        let timestamp = dateFormatter.string(from: Date())
        var log = "\n[\(timestamp)] 🚀 REQUEST"
        log += "\nURL: \(request.url?.absoluteString ?? "Unknown")"
        log += "\nMethod: \(request.httpMethod ?? "Unknown")"

        if logLevel == .detailed {
            log += "\nHeaders: \(request.allHTTPHeaderFields ?? [:])"

            if let body = request.httpBody,
               let bodyString = String(data: body, encoding: .utf8) {
                log += "\nBody: \(bodyString)"
            }
        }

        print(log)
    }

    func logResponse(_ response: HTTPURLResponse, data: Data?, error: Error?) {
        guard logLevel != .none else { return }

        let timestamp = dateFormatter.string(from: Date())
        var log = "\n[\(timestamp)] ⬇️ RESPONSE"
        log += "\nURL: \(response.url?.absoluteString ?? "Unknown")"
        log += "\nStatus Code: \(response.statusCode)"

        if logLevel == .detailed {
            log += "\nHeaders: \(response.allHeaderFields)"

            if let data = data {
                log += "\nData Size: \(data.count) bytes"

                if let bodyString = String(data: data, encoding: .utf8) {
                    log += "\nBody: \(bodyString.prefix(500))" // Limit body length
                }
            }
        }

        if let error = error {
            log += "\nError: \(error.localizedDescription)"
        }

        print(log)
    }
}

// EventMonitor wrapper
class LoggingEventMonitor: EventMonitor {
    let queue = DispatchQueue(label: "logging_monitor")
    private let logger = CustomNetworkLogger()

    func requestDidResume(_ request: Request) {
        if let urlRequest = request.request {
            logger.logRequest(urlRequest)
        }
    }

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        if let httpResponse = response.response {
            logger.logResponse(httpResponse, data: response.data, error: response.error)
        }
    }
}

Logging to Files and External Services

For production environments, you might want to log to files or external logging services:

import os.log

class ProductionNetworkLogger: EventMonitor {
    let queue = DispatchQueue(label: "production_logger")
    private let logger = Logger(subsystem: "com.yourapp.networking", category: "requests")

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        let url = response.request?.url?.absoluteString ?? "Unknown"
        let statusCode = response.response?.statusCode ?? 0
        let duration = response.metrics?.taskInterval.duration ?? 0

        // Log to system log
        logger.info("Request completed: \(url) - Status: \(statusCode) - Duration: \(duration)s")

        // Log to file (implement your file logging logic here)
        logToFile(url: url, statusCode: statusCode, duration: duration)

        // Send to analytics service
        if let error = response.error {
            sendErrorToAnalytics(url: url, error: error)
        }
    }

    private func logToFile(url: String, statusCode: Int, duration: TimeInterval) {
        // Implement file logging
        let logEntry = "\(Date()): \(url) - \(statusCode) - \(duration)s\n"
        // Write to file logic here
    }

    private func sendErrorToAnalytics(url: String, error: AFError) {
        // Send error data to your analytics service
        // This could be Crashlytics, Sentry, etc.
    }
}

Conditional Logging Based on Environment

Implement environment-aware logging that behaves differently in debug vs. production:

class EnvironmentAwareLogger: EventMonitor {
    let queue = DispatchQueue(label: "env_logger")

    #if DEBUG
    private let isDebug = true
    #else
    private let isDebug = false
    #endif

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        if isDebug {
            // Detailed logging in debug
            logDetailedResponse(response)
        } else {
            // Minimal logging in production
            logMinimalResponse(response)
        }
    }

    private func logDetailedResponse<Value>(_ response: DataResponse<Value, AFError>) {
        let message = """
        📱 DEBUG RESPONSE
        URL: \(response.request?.url?.absoluteString ?? "nil")
        Status: \(response.response?.statusCode ?? 0)
        Data: \(String(data: response.data ?? Data(), encoding: .utf8) ?? "nil")
        """
        print(message)
    }

    private func logMinimalResponse<Value>(_ response: DataResponse<Value, AFError>) {
        if let error = response.error {
            print("❌ Network Error: \(error.localizedDescription)")
        }
    }
}

Best Practices for Logging Implementation

1. Performance Considerations

When implementing logging, consider performance impact:

class PerformantLogger: EventMonitor {
    let queue = DispatchQueue(label: "performant_logger", qos: .utility)

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        // Perform logging on background queue
        queue.async {
            self.performLogging(response)
        }
    }

    private func performLogging<Value>(_ response: DataResponse<Value, AFError>) {
        // Heavy logging operations here
        // This won't block the main thread
    }
}

2. Security Considerations

Be careful not to log sensitive information:

class SecureLogger: EventMonitor {
    let queue = DispatchQueue(label: "secure_logger")

    private let sensitiveHeaders = ["authorization", "x-api-key", "cookie"]
    private let sensitiveBodyKeys = ["password", "token", "secret"]

    func logRequest(_ request: URLRequest) {
        var headers = request.allHTTPHeaderFields ?? [:]

        // Remove sensitive headers
        for sensitiveHeader in sensitiveHeaders {
            headers.removeValue(forKey: sensitiveHeader)
            headers.removeValue(forKey: sensitiveHeader.capitalized)
        }

        print("Request Headers: \(headers)")

        // Don't log sensitive request bodies
        if let body = request.httpBody,
           let bodyString = String(data: body, encoding: .utf8),
           !containsSensitiveData(bodyString) {
            print("Request Body: \(bodyString)")
        }
    }

    private func containsSensitiveData(_ body: String) -> Bool {
        let lowercaseBody = body.lowercased()
        return sensitiveBodyKeys.contains { lowercaseBody.contains($0) }
    }
}

Integration with Popular Logging Frameworks

CocoaLumberjack Integration

import CocoaLumberjack

class LumberjackLogger: EventMonitor {
    let queue = DispatchQueue(label: "lumberjack_logger")

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        let url = response.request?.url?.absoluteString ?? "Unknown"
        let statusCode = response.response?.statusCode ?? 0

        if let error = response.error {
            DDLogError("Network Error: \(url) - \(error.localizedDescription)")
        } else {
            DDLogInfo("Network Success: \(url) - Status: \(statusCode)")
        }
    }
}

Advanced Logging Patterns

Filtering Specific Requests

You can filter which requests to log based on URL patterns or other criteria:

class FilteredLogger: EventMonitor {
    let queue = DispatchQueue(label: "filtered_logger")
    private let allowedHosts: Set<String>
    private let ignoredPaths: Set<String>

    init(allowedHosts: Set<String> = [], ignoredPaths: Set<String> = []) {
        self.allowedHosts = allowedHosts
        self.ignoredPaths = ignoredPaths
    }

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        guard shouldLogRequest(response.request) else { return }

        // Your logging logic here
        print("Filtered log: \(response.request?.url?.absoluteString ?? "Unknown")")
    }

    private func shouldLogRequest(_ request: URLRequest?) -> Bool {
        guard let url = request?.url else { return false }

        // Skip logging for ignored paths
        if let path = url.path as String?,
           ignoredPaths.contains(where: { path.contains($0) }) {
            return false
        }

        // Only log allowed hosts if specified
        if !allowedHosts.isEmpty,
           let host = url.host,
           !allowedHosts.contains(host) {
            return false
        }

        return true
    }
}

Request/Response Metrics Collection

Collect metrics for performance monitoring:

class MetricsLogger: EventMonitor {
    let queue = DispatchQueue(label: "metrics_logger")

    private struct RequestMetrics {
        let url: String
        let method: String
        let statusCode: Int
        let duration: TimeInterval
        let dataSize: Int
    }

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        let metrics = RequestMetrics(
            url: response.request?.url?.absoluteString ?? "Unknown",
            method: response.request?.httpMethod ?? "Unknown",
            statusCode: response.response?.statusCode ?? 0,
            duration: response.metrics?.taskInterval.duration ?? 0,
            dataSize: response.data?.count ?? 0
        )

        // Log metrics to your analytics service
        logMetrics(metrics)
    }

    private func logMetrics(_ metrics: RequestMetrics) {
        // Send to your metrics collection service
        print("📊 Metrics - URL: \(metrics.url), Duration: \(metrics.duration)s, Size: \(metrics.dataSize) bytes")
    }
}

Testing Your Logging Implementation

Unit Testing EventMonitors

Test your logging implementation to ensure it captures the expected information:

import XCTest
import Alamofire

class NetworkLoggerTests: XCTestCase {
    class TestLogger: EventMonitor {
        let queue = DispatchQueue(label: "test_logger")
        var loggedRequests: [(String, Int)] = []

        func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
            let url = response.request?.url?.absoluteString ?? "Unknown"
            let statusCode = response.response?.statusCode ?? 0
            loggedRequests.append((url, statusCode))
        }
    }

    func testLoggingCapturesRequests() {
        let logger = TestLogger()
        let session = Session(eventMonitors: [logger])

        let expectation = XCTestExpectation(description: "Request completion")

        session.request("https://httpbin.org/get")
            .responseJSON { _ in
                expectation.fulfill()
            }

        wait(for: [expectation], timeout: 10)

        XCTAssertEqual(logger.loggedRequests.count, 1)
        XCTAssertTrue(logger.loggedRequests[0].0.contains("httpbin.org"))
        XCTAssertEqual(logger.loggedRequests[0].1, 200)
    }
}

Conclusion

Implementing comprehensive request and response logging in Alamofire requires understanding the EventMonitor protocol and choosing the right approach for your needs. Whether you use the built-in NetworkLogger, create custom EventMonitor implementations, or integrate with external logging services, proper logging is crucial for debugging and monitoring your network requests.

Remember to consider performance implications, security concerns, and environment-specific requirements when implementing your logging solution. For complex applications requiring detailed network monitoring, consider combining multiple logging approaches or integrating with professional monitoring tools.

Similar to how you might monitor network requests in Puppeteer for web scraping applications, implementing robust logging in Alamofire helps you maintain visibility into your iOS app's network operations and troubleshoot issues effectively. When dealing with authentication scenarios, you can also reference our guide on handling HTTP authentication with Alamofire to ensure your logging captures authentication-related request details appropriately.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon