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:
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.
Custom EventMonitor (Recommended)
Create a logger that conforms to EventMonitor:
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:
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
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:
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)
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)
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