Log slow API requests with server spans and client timing

This commit is contained in:
Ian Gulliver
2026-10-21 14:45:19 -07:00
parent 671165ec71
commit b4c1dbab37
31 changed files with 458 additions and 185 deletions
+41
View File
@@ -0,0 +1,41 @@
import Foundation
final class RequestTiming: NSObject, URLSessionTaskDelegate, Sendable {
private static let slow: TimeInterval = 0.2
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
guard let t = metrics.transactionMetrics.last, let response = t.response as? HTTPURLResponse, response.statusCode != 101, response.value(forHTTPHeaderField: "Content-Type") != "text/event-stream", let fetched = t.fetchStartDate, let sent = t.requestStartDate, let first = t.responseStartDate, let done = t.responseEndDate else {
return
}
let finished = Date()
let serverTiming = response.value(forHTTPHeaderField: "Server-Timing") ?? ""
let server = Self.serverTotal(serverTiming)
let queue = (t.connectStartDate ?? sent).timeIntervalSince(fetched)
let connect = t.connectStartDate.flatMap { start in t.connectEndDate.map { $0.timeIntervalSince(start) } } ?? 0
let network = first.timeIntervalSince(sent) - server
let download = done.timeIntervalSince(first)
let total = done.timeIntervalSince(fetched)
let line = "\(task.originalRequest?.httpMethod ?? "") \(task.originalRequest?.url?.path ?? "")"
Task { @MainActor in
let app = Date().timeIntervalSince(finished)
if total + app < Self.slow {
return
}
log.notice("request slow \(line, privacy: .public) total=\(Self.ms(total + app), privacy: .public) queue=\(Self.ms(queue), privacy: .public) connect=\(Self.ms(connect), privacy: .public) network=\(Self.ms(network), privacy: .public) server=\(Self.ms(server), privacy: .public) download=\(Self.ms(download), privacy: .public) app=\(Self.ms(app), privacy: .public) spans=\(serverTiming, privacy: .public)")
}
}
private static func serverTotal(_ header: String) -> TimeInterval {
for metric in header.split(separator: ",") {
let parts = metric.split(separator: ";").map { $0.trimmingCharacters(in: .whitespaces) }
if parts.first == "total", let dur = parts.dropFirst().first(where: { $0.hasPrefix("dur=") }), let value = Double(dur.dropFirst(4)) {
return value / 1000
}
}
return 0
}
private static func ms(_ seconds: TimeInterval) -> String {
"\(Int((seconds * 1000).rounded()))ms"
}
}