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.