Alamofire
Logging
Networking
iOS Development
Swift

How can I log each request/response using Alamofire?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Alamofire provides built-in request and response logging through EventMonitor protocol conformance. In Alamofire 5+, you create a class that conforms to EventMonitor, implement the callbacks you care about, and pass it to your Session. For a quick solution, Alamofire 5 includes a built-in ClosureEventMonitor class. For more control, the AlamofireNetworkActivityLogger package or a custom EventMonitor gives you full logging of headers, bodies, status codes, and timing.

Quick Logging with cURLDescription

The simplest approach — print the cURL equivalent of each request:

swift
1AF.request("https://api.example.com/users")
2    .cURLDescription { description in
3        print(description)
4        // curl -v \
5        //   -X GET \
6        //   -H "Accept: application/json" \
7        //   https://api.example.com/users
8    }
9    .responseDecodable(of: [User].self) { response in
10        // Handle response
11    }

cURLDescription outputs a copy-pasteable cURL command, useful for reproducing requests outside the app.

Create a logger that conforms to EventMonitor:

swift
1import Alamofire
2import Foundation
3
4final class NetworkLogger: EventMonitor {
5    let queue = DispatchQueue(label: "com.app.networklogger")
6
7    func requestDidResume(_ request: Request) {
8        let url = request.request?.url?.absoluteString ?? "Unknown URL"
9        let method = request.request?.httpMethod ?? "Unknown"
10        print("⬆️ \(method) \(url)")
11
12        if let headers = request.request?.allHTTPHeaderFields {
13            print("   Headers: \(headers)")
14        }
15
16        if let body = request.request?.httpBody,
17           let bodyString = String(data: body, encoding: .utf8) {
18            print("   Body: \(bodyString)")
19        }
20    }
21
22    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
23        let url = request.request?.url?.absoluteString ?? "Unknown URL"
24        let statusCode = response.response?.statusCode ?? 0
25        let duration = request.metrics?.taskInterval.duration ?? 0
26
27        switch response.result {
28        case .success:
29            print("⬇️ \(statusCode) \(url) (\(String(format: "%.2f", duration))s)")
30        case .failure(let error):
31            print("❌ \(statusCode) \(url) - \(error.localizedDescription)")
32        }
33    }
34}

Register it with your session:

swift
1let session = Session(eventMonitors: [NetworkLogger()])
2
3// All requests through this session are logged
4session.request("https://api.example.com/users")
5    .responseDecodable(of: [User].self) { response in
6        // Handle response
7    }

Logging Response Bodies

swift
1final class VerboseNetworkLogger: EventMonitor {
2    let queue = DispatchQueue(label: "com.app.verboselogger")
3
4    func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
5        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
6
7        // Request info
8        if let httpRequest = request.request {
9            print("REQUEST: \(httpRequest.httpMethod ?? "") \(httpRequest.url?.absoluteString ?? "")")
10            print("HEADERS: \(httpRequest.allHTTPHeaderFields ?? [:])")
11            if let body = httpRequest.httpBody, let str = String(data: body, encoding: .utf8) {
12                print("BODY: \(str)")
13            }
14        }
15
16        // Response info
17        if let httpResponse = response.response {
18            print("STATUS: \(httpResponse.statusCode)")
19            print("RESPONSE HEADERS: \(httpResponse.allHeaderFields)")
20        }
21
22        // Response body
23        if let data = response.data,
24           let json = String(data: data, encoding: .utf8) {
25            let truncated = json.prefix(1000)
26            print("RESPONSE BODY: \(truncated)\(json.count > 1000 ? "..." : "")")
27        }
28
29        // Timing
30        if let metrics = request.metrics {
31            print("DURATION: \(String(format: "%.3f", metrics.taskInterval.duration))s")
32        }
33
34        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
35    }
36}

Using ClosureEventMonitor

Alamofire 5 includes ClosureEventMonitor for quick setup without creating a new class:

swift
1let monitor = ClosureEventMonitor()
2
3monitor.requestDidResume = { request in
4    print("Started: \(request)")
5}
6
7monitor.requestDidFinish = { request in
8    print("Finished: \(request)")
9}
10
11let session = Session(eventMonitors: [monitor])

Conditional Logging (Debug Only)

swift
1final class DebugNetworkLogger: EventMonitor {
2    let queue = DispatchQueue(label: "com.app.debuglogger")
3
4    func requestDidResume(_ request: Request) {
5        #if DEBUG
6        print("[NET] \(request.request?.httpMethod ?? "") \(request.request?.url?.absoluteString ?? "")")
7        #endif
8    }
9
10    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
11        #if DEBUG
12        let status = response.response?.statusCode ?? 0
13        let url = request.request?.url?.absoluteString ?? ""
14        print("[NET] \(status) \(url)")
15        #endif
16    }
17}
18
19// Only attach in debug builds
20#if DEBUG
21let session = Session(eventMonitors: [DebugNetworkLogger()])
22#else
23let session = Session()
24#endif

Logging to OSLog (Unified Logging)

swift
1import os.log
2
3final class OSLogNetworkLogger: EventMonitor {
4    let queue = DispatchQueue(label: "com.app.oslogger")
5    private let logger = Logger(subsystem: "com.myapp", category: "Network")
6
7    func requestDidResume(_ request: Request) {
8        logger.info("Request: \(request.request?.url?.absoluteString ?? "", privacy: .public)")
9    }
10
11    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
12        let status = response.response?.statusCode ?? 0
13        let url = request.request?.url?.absoluteString ?? ""
14
15        switch response.result {
16        case .success:
17            logger.info("Response: \(status) \(url, privacy: .public)")
18        case .failure(let error):
19            logger.error("Failed: \(status) \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)")
20        }
21    }
22}

OSLog integrates with Console.app and Instruments for filtering and searching logs on device.

Common Pitfalls

  • Logging response bodies in production: Response bodies can contain sensitive data (tokens, personal information) and large payloads. Always wrap verbose logging in #if DEBUG or use a log level that is disabled in release builds.
  • Using print instead of os_log/OSLog: print outputs to stdout which is invisible in production apps. Use os_log or the Logger API for logs that persist on device and are filterable in Console.app.
  • Blocking the main thread with logging: EventMonitor callbacks run on the queue you specify. Use a serial background queue (not DispatchQueue.main) to avoid blocking UI updates while formatting large response bodies.
  • Forgetting to use the custom Session: Logging only works for requests made through the Session that has the EventMonitor attached. Requests made through the global AF singleton (which is a default Session) are not logged unless you replace it.
  • Logging with Alamofire 4 patterns: Alamofire 4 used URLProtocol subclassing or RequestAdapter for logging. Alamofire 5 replaced these with EventMonitor, which is simpler and officially supported. Old patterns may compile but miss events or cause unexpected behavior.

Summary

  • Use EventMonitor protocol to create custom request/response loggers in Alamofire 5+
  • Register the monitor when creating the Session: Session(eventMonitors: [logger])
  • cURLDescription provides a quick copy-pasteable cURL command for any request
  • Use #if DEBUG to prevent sensitive data from being logged in release builds
  • Log to os_log/Logger for production-grade logging that works with Console.app
  • Truncate large response bodies to avoid memory and performance issues in logging

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.