Skip to content

Commit 406c1d4

Browse files
authored
feat: network instrumentation without KVO (#124) (#132)
1 parent 46b9650 commit 406c1d4

File tree

2 files changed

+111
-167
lines changed

2 files changed

+111
-167
lines changed
Lines changed: 110 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 Splunk Inc.
2+
Copyright 2023 Splunk Inc.
33

44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -31,13 +31,10 @@ func addLinkToSpan(span: Span, valStr: String) {
3131
span.setAttribute(key: "link.spanId", value: spanId)
3232
}
3333

34-
func endHttpSpan(span: Span?, task: URLSessionTask) {
35-
if span == nil {
36-
return
37-
}
34+
func endHttpSpan(span: Span, task: URLSessionTask) {
3835
let hr: HTTPURLResponse? = task.response as? HTTPURLResponse
3936
if hr != nil {
40-
span!.setAttribute(key: "http.status_code", value: hr!.statusCode)
37+
span.setAttribute(key: "http.status_code", value: hr!.statusCode)
4138
// Blerg, looks like an iteration here since it is case sensitive and the case insensitive search assumes single value
4239
for (key, val) in hr!.allHeaderFields {
4340
let keyStr = key as? String
@@ -46,27 +43,31 @@ func endHttpSpan(span: Span?, task: URLSessionTask) {
4643
let valStr = val as? String
4744
if valStr != nil {
4845
if valStr!.starts(with: "traceparent") {
49-
addLinkToSpan(span: span!, valStr: valStr!)
46+
addLinkToSpan(span: span, valStr: valStr!)
5047
}
5148
}
5249
}
5350
}
5451
}
5552
}
5653
if task.error != nil {
57-
span!.setAttribute(key: "error", value: true)
58-
span!.setAttribute(key: "exception.message", value: task.error!.localizedDescription)
59-
span!.setAttribute(key: "exception.type", value: String(describing: type(of: task.error!)))
54+
span.setAttribute(key: "error", value: true)
55+
span.setAttribute(key: "exception.message", value: task.error!.localizedDescription)
56+
span.setAttribute(key: "exception.type", value: String(describing: type(of: task.error!)))
6057
}
61-
span!.setAttribute(key: "http.response_content_length_uncompressed", value: Int(task.countOfBytesReceived))
58+
span.setAttribute(key: "http.response_content_length_uncompressed", value: Int(task.countOfBytesReceived))
6259
if task.countOfBytesSent != 0 {
63-
span!.setAttribute(key: "http.request_content_length", value: Int(task.countOfBytesSent))
60+
span.setAttribute(key: "http.request_content_length", value: Int(task.countOfBytesSent))
6461
}
65-
span!.end()
62+
span.end()
63+
}
64+
65+
func isSupportedTask(task: URLSessionTask) -> Bool {
66+
return task is URLSessionDataTask || task is URLSessionDownloadTask || task is URLSessionUploadTask
6667
}
6768

6869
func startHttpSpan(request: URLRequest?) -> Span? {
69-
if request == nil || request?.url == nil {
70+
if request?.url == nil {
7071
return nil
7172
}
7273
let url = request!.url!
@@ -120,117 +121,60 @@ func startHttpSpan(request: URLRequest?) -> Span? {
120121
return span
121122
}
122123

123-
class SessionTaskObserver: NSObject {
124-
var span: Span?
125-
// Observers aren't kept alive by observing...
126-
var extraRefToSelf: SessionTaskObserver?
127-
var lock: NSLock = NSLock()
128-
override init() {
129-
super.init()
130-
extraRefToSelf = self
131-
}
124+
fileprivate var ASSOC_KEY_SPAN: UInt8 = 0
132125

133-
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
134-
lock.lock()
126+
// swiftlint:disable missing_docs
127+
extension URLSessionTask {
128+
@objc open func splunk_swizzled_setState(state: URLSessionTask.State) {
135129
defer {
136-
lock.unlock()
130+
splunk_swizzled_setState(state: state)
137131
}
138-
let task = object as? URLSessionTask
139-
if task == nil {
132+
133+
if !isSupportedTask(task: self) {
140134
return
141135
}
142-
if span == nil {
143-
span = startHttpSpan(request: task!.originalRequest)
136+
137+
if state == URLSessionTask.State.running {
138+
return
144139
}
145-
// FIXME possibly also allow .canceling to close the span?
146-
if task!.state == .completed && extraRefToSelf != nil {
147-
endHttpSpan(span: span,
148-
task: task!)
149-
task!.removeObserver(self, forKeyPath: "state")
150-
extraRefToSelf = nil
140+
141+
if currentRequest?.url == nil {
142+
return
151143
}
152-
}
153-
}
154144

155-
func wireUpTaskObserver(task: URLSessionTask) {
156-
task.addObserver(SessionTaskObserver(), forKeyPath: "state", options: .new, context: nil)
157-
}
145+
let maybeSpan: Span? = objc_getAssociatedObject(self, &ASSOC_KEY_SPAN) as? Span
158146

159-
// swiftlint:disable missing_docs
160-
extension URLSession {
161-
@objc open func splunk_swizzled_dataTask(with url: NSURL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
162-
let answer = splunk_swizzled_dataTask(with: url, completionHandler: completionHandler)
163-
wireUpTaskObserver(task: answer)
164-
return answer
165-
}
166-
167-
@objc open func splunk_swizzled_dataTask(with url: NSURL) -> URLSessionDataTask {
168-
let answer = splunk_swizzled_dataTask(with: url)
169-
wireUpTaskObserver(task: answer)
170-
return answer
171-
}
172-
173-
// rename objc view of func to allow "overloading"
174-
@objc(splunkSwizzledDataTaskWithRequest:completionHandler:) open func splunk_swizzled_dataTask(with request: URLRequest, completionHandler: ((Data?, URLResponse?, Error?) -> Void)?) -> URLSessionDataTask {
175-
let answer = splunk_swizzled_dataTask(with: request, completionHandler: completionHandler)
176-
wireUpTaskObserver(task: answer)
177-
return answer
178-
}
179-
180-
@objc(splunkSwizzledDataTaskWithRequest:) open func splunk_swizzled_dataTask(with request: URLRequest) -> URLSessionDataTask {
181-
let answer = splunk_swizzled_dataTask(with: request)
182-
wireUpTaskObserver(task: answer)
183-
return answer
184-
}
185-
186-
// uploads
187-
@objc open func splunk_swizzled_uploadTask(with: URLRequest, from: Data) -> URLSessionUploadTask {
188-
let answer = splunk_swizzled_uploadTask(with: with, from: from)
189-
wireUpTaskObserver(task: answer)
190-
return answer
191-
}
192-
@objc open func splunk_swizzled_uploadTask(with: URLRequest, from: Data, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask {
193-
let answer = splunk_swizzled_uploadTask(with: with, from: from, completionHandler: completionHandler)
194-
wireUpTaskObserver(task: answer)
195-
return answer
196-
}
197-
@objc open func splunk_swizzled_uploadTask(with: URLRequest, fromFile: NSURL) -> URLSessionUploadTask {
198-
let answer = splunk_swizzled_uploadTask(with: with, fromFile: fromFile)
199-
wireUpTaskObserver(task: answer)
200-
return answer
201-
}
202-
@objc open func splunk_swizzled_uploadTask(with: URLRequest, fromFile: NSURL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask {
203-
let answer = splunk_swizzled_uploadTask(with: with, fromFile: fromFile, completionHandler: completionHandler)
204-
wireUpTaskObserver(task: answer)
205-
return answer
206-
}
207-
@objc open func splunk_swizzled_uploadTask(withStreamedRequest: URLRequest) -> URLSessionUploadTask {
208-
let answer = splunk_swizzled_uploadTask(withStreamedRequest: withStreamedRequest)
209-
wireUpTaskObserver(task: answer)
210-
return answer
147+
if maybeSpan == nil {
148+
return
149+
}
150+
151+
endHttpSpan(span: maybeSpan!, task: self)
211152
}
212-
// download tasks
213-
@objc open func splunk_swizzled_downloadTask(with url: NSURL) -> URLSessionDownloadTask {
214-
let answer = splunk_swizzled_downloadTask(with: url)
215-
wireUpTaskObserver(task: answer)
216-
return answer
153+
154+
@objc open func splunk_swizzled_resume() {
155+
defer {
156+
splunk_swizzled_resume()
157+
}
158+
159+
if !isSupportedTask(task: self) {
160+
return
161+
}
162+
163+
if self.state == URLSessionTask.State.completed ||
164+
self.state == URLSessionTask.State.canceling {
165+
return
166+
}
167+
168+
let existingSpan: Span? = objc_getAssociatedObject(self, &ASSOC_KEY_SPAN) as? Span
169+
170+
if existingSpan != nil {
171+
return
172+
}
173+
174+
startHttpSpan(request: currentRequest).map { span in
175+
objc_setAssociatedObject(self, &ASSOC_KEY_SPAN, span, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
176+
}
217177
}
218-
@objc open func splunk_swizzled_downloadTask(with url: NSURL, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
219-
let answer = splunk_swizzled_downloadTask(with: url, completionHandler: completionHandler)
220-
wireUpTaskObserver(task: answer)
221-
return answer
222-
}
223-
@objc(splunkSwizzledDownloadTaskWithRequest: completionHandler:) open func splunk_swizzled_downloadTask(with request: URLRequest, completionHandler: ((URL?, URLResponse?, Error?) -> Void)?) -> URLSessionDownloadTask {
224-
let answer = splunk_swizzled_downloadTask(with: request, completionHandler: completionHandler)
225-
wireUpTaskObserver(task: answer)
226-
return answer
227-
}
228-
229-
@objc(splunkSwizzledDownloadTaskWithRequest:) open func splunk_swizzled_downloadTask(with request: URLRequest) -> URLSessionDataTask {
230-
let answer = splunk_swizzled_downloadTask(with: request)
231-
wireUpTaskObserver(task: answer)
232-
return answer
233-
}
234178
}
235179

236180
// FIXME use setImplementation and capture, rather than exchangeImpl
@@ -244,57 +188,57 @@ func swizzle(clazz: AnyClass, orig: Selector, swizzled: Selector) {
244188
}
245189
}
246190

247-
func initalizeNetworkInstrumentation() {
248-
let urlsession = URLSession.self
249-
250-
// This syntax is obnoxious to differentiate with:request from with:url
251-
swizzle(clazz: urlsession,
252-
orig: #selector(URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URL, @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask),
253-
swizzled: #selector(URLSession.splunk_swizzled_dataTask(with:completionHandler:) as (URLSession) -> (NSURL, @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask))
254-
255-
swizzle(clazz: urlsession,
256-
orig: #selector(URLSession.dataTask(with:) as (URLSession) -> (URL) -> URLSessionDataTask),
257-
swizzled: #selector(URLSession.splunk_swizzled_dataTask(with:) as (URLSession) -> (NSURL) -> URLSessionDataTask))
258-
259-
// @objc(overrrideName) requires a runtime lookup rather than a build-time lookup (seems like a bug in the compiler)
260-
swizzle(clazz: urlsession,
261-
orig: #selector(URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask),
262-
swizzled: NSSelectorFromString("splunkSwizzledDataTaskWithRequest:completionHandler:"))
263-
264-
swizzle(clazz: urlsession,
265-
orig: #selector(URLSession.dataTask(with:) as (URLSession) -> (URLRequest) -> URLSessionDataTask),
266-
swizzled: NSSelectorFromString("splunkSwizzledDataTaskWithRequest:"))
267-
268-
// upload tasks
269-
swizzle(clazz: urlsession,
270-
orig: #selector(URLSession.uploadTask(with:from:)),
271-
swizzled: #selector(URLSession.splunk_swizzled_uploadTask(with:from:)))
272-
swizzle(clazz: urlsession,
273-
orig: #selector(URLSession.uploadTask(with:from:completionHandler:)),
274-
swizzled: #selector(URLSession.splunk_swizzled_uploadTask(with:from:completionHandler:)))
275-
swizzle(clazz: urlsession,
276-
orig: #selector(URLSession.uploadTask(with:fromFile:)),
277-
swizzled: #selector(URLSession.splunk_swizzled_uploadTask(with:fromFile:)))
278-
swizzle(clazz: urlsession,
279-
orig: #selector(URLSession.uploadTask(with:fromFile:completionHandler:)),
280-
swizzled: #selector(URLSession.splunk_swizzled_uploadTask(with:fromFile:completionHandler:)))
281-
swizzle(clazz: urlsession,
282-
orig: #selector(URLSession.uploadTask(withStreamedRequest:)),
283-
swizzled: #selector(URLSession.splunk_swizzled_uploadTask(withStreamedRequest:)))
284-
285-
// download tasks
286-
swizzle(clazz: urlsession,
287-
orig: #selector(URLSession.downloadTask(with:) as (URLSession) -> (URL) -> URLSessionDownloadTask),
288-
swizzled: #selector(URLSession.splunk_swizzled_downloadTask(with:) as (URLSession) -> (NSURL) -> URLSessionDownloadTask))
289-
swizzle(clazz: urlsession,
290-
orig: #selector(URLSession.downloadTask(with:completionHandler:) as (URLSession) -> (URL, @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask),
291-
swizzled: #selector(URLSession.splunk_swizzled_downloadTask(with:completionHandler:) as (URLSession) -> (NSURL, @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask))
292-
swizzle(clazz: urlsession,
293-
orig: #selector(URLSession.downloadTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask),
294-
swizzled: NSSelectorFromString("splunkSwizzledDownloadTaskWithRequest:completionHandler:"))
295-
swizzle(clazz: urlsession,
296-
orig: #selector(URLSession.downloadTask(with:) as (URLSession) -> (URLRequest) -> URLSessionDownloadTask),
297-
swizzled: NSSelectorFromString("splunkSwizzledDownloadTaskWithRequest:"))
298-
// FIXME figure out how to support the two ResumeData variants - state transfer is weird
191+
func swizzledUrlSessionClasses() -> [AnyClass] {
192+
let conf = URLSessionConfiguration.ephemeral
193+
let session = URLSession(configuration: conf)
194+
// The URL is just something parseable, since empty string can not be provided
195+
let localDataTask = session.dataTask(with: URL(string: "https://splunkrum")!)
196+
197+
defer {
198+
localDataTask.cancel()
199+
session.finishTasksAndInvalidate()
200+
}
201+
202+
let setStateSelector = NSSelectorFromString("setState:")
203+
204+
var classes: [AnyClass] = []
205+
guard var currentClass: AnyClass = object_getClass(localDataTask) else { return classes }
206+
var method = class_getInstanceMethod(currentClass, setStateSelector)
207+
208+
while method != nil {
209+
let classResumeImp = method_getImplementation(method!)
210+
211+
let superClass: AnyClass? = currentClass.superclass()
212+
let superClassMethod = class_getInstanceMethod(superClass, setStateSelector)
213+
let superClassResumeImp = superClassMethod.map { method_getImplementation($0) }
299214

215+
if classResumeImp != superClassResumeImp {
216+
classes.append(currentClass)
217+
}
218+
219+
if superClass == nil {
220+
return classes
221+
}
222+
223+
currentClass = superClass!
224+
method = superClassMethod
225+
}
226+
227+
return classes
228+
}
229+
230+
func swizzleUrlSession() {
231+
let classes = swizzledUrlSessionClasses()
232+
233+
let setStateSelector = NSSelectorFromString("setState:")
234+
let resumeSelector = NSSelectorFromString("resume")
235+
236+
for classToSwizzle in classes {
237+
swizzle(clazz: classToSwizzle, orig: setStateSelector, swizzled: #selector(URLSessionTask.splunk_swizzled_setState(state:)))
238+
swizzle(clazz: classToSwizzle, orig: resumeSelector, swizzled: #selector(URLSessionTask.splunk_swizzled_resume))
239+
}
240+
}
241+
242+
func initalizeNetworkInstrumentation() {
243+
swizzleUrlSession()
300244
}

SplunkRumWorkspace/TestApp/TestApp/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct ContentView: View {
3333
}
3434
func downloadRequest() {
3535
print("download!")
36-
let url = URL(string: "http://www.splunk.com")!
36+
let url = URL(string: "https://www.splunk.com")!
3737
var req = URLRequest(url: url)
3838
let task = URLSession.shared.downloadTask(with: url) {(_: URL?, _: URLResponse?, _) in
3939
print("download finished")

0 commit comments

Comments
 (0)