Skip to content

Commit 2dc7dd0

Browse files
Hongyan JiangGitHub Enterprise
authored andcommitted
Prepare release 1.6.1
Improve error handling on beacon send failure
1 parent d580c13 commit 2dc7dd0

File tree

14 files changed

+397
-35
lines changed

14 files changed

+397
-35
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.6.1
44
- Fix crash caused by appendMetaData() inside InstanaProperties class
5+
- Improve error handling on beacon send failure
56

67
## 1.6.0
78
- Refactor code and add more unit test cases

Dev/InstanaAgentExample/AppDelegate.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66

77
import UIKit
88
import InstanaAgent
9+
import OSLog
910

1011
@UIApplicationMain
1112
class AppDelegate: UIResponder, UIApplicationDelegate {
1213
var window: UIWindow? // needed on iOS 12 or lower
1314

1415
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15-
Instana.setup(key: InstanaKey, reportingURL: InstanaURL)
16+
let options = InstanaSetupOptions(enableCrashReporting: true)
17+
// options.slowSendInterval = 60.0
18+
if !Instana.setup(key: InstanaKey, reportingURL: InstanaURL, options: options) {
19+
let myLog = OSLog(subsystem: "com.instana.ios.InstanaAgentExample", category: "Instana")
20+
os_log("Instana setup failed", log: myLog, type: .error)
21+
}
1622
return true
1723
}
1824

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau
5959
2. The lowest versions that support crash reporting are iOS 14.0 and macOS 12.0.
6060
3. A valid bundle id other than com.instana.ios.InstanaAgentExample for your test app is also a must.
6161

62+
### How to enable slow send mode on beacon send failure
63+
64+
By default, when beacon send failed, Instana agent tries to resend it until succeed. Beacons are also sent in batches (maximum 100 beacons) every 2 seconds. For certain cellular network, this is not an ideal option.
65+
An alternative way, slow send mode, is introduced since version 1.6.1. This mode could be turned on by providing a positive slowSendInterval number (in seconds) in setup() call. Then if beacon send failed, Instana agent only sends out one beacon every \(slowSendInterval) seconds. If succeeded, beacon sending restores to normal mode, ie. in batches every 2 seconds. Otherwise sending stays in slow send mode.
66+
Slow send mode is disabled by default. Here is the example code to set slowSendInterval to 60 seconds:
67+
68+
```
69+
import InstanaAgent
70+
71+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
72+
let options = InstanaSetupOptions(options.slowSendInterval: 60.0)
73+
if !Instana.setup(key: InstanaKey, reportingURL: InstanaURL, options: options) {
74+
// Error handling here
75+
// Most likely slowSendInterval is invalid. Expected value is 2 ~ 3600 seconds
76+
}
77+
Instana.setup(key: <Your Instana Key>, reportingURL: <Your Instana instance URL>, options: options)
78+
79+
....
80+
return true
81+
}
82+
```
83+
6284
### API
6385

6486
See [API page](https://www.ibm.com/docs/en/instana-observability/current?topic=monitoring-ios-api#instana-ios-agent-api).

Sources/InstanaAgent/Beacons/BeaconFlusher.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class BeaconFlusher {
4242
}
4343

4444
typealias Sender = (URLRequest, @escaping (Swift.Result<Int, Error>) -> Void) -> Void
45+
weak var reporter: Reporter?
4546
let config: InstanaConfiguration
4647
let debounce: TimeInterval
4748
let items: Set<CoreBeacon>
@@ -55,16 +56,15 @@ class BeaconFlusher {
5556
private let externalSend: Sender? // Used for Unit Testing
5657
private var sentBeacons = Set<CoreBeacon>()
5758
private(set) var urlTasks = [URLSessionTask]()
58-
var shouldPerformRetry: Bool {
59-
!errors.isEmpty && retryStep < config.maxRetries
60-
}
6159

62-
init(items: Set<CoreBeacon>,
60+
init(reporter: Reporter?,
61+
items: Set<CoreBeacon>,
6362
debounce: TimeInterval,
6463
config: InstanaConfiguration,
6564
queue: DispatchQueue,
6665
send: Sender? = nil,
6766
completion: @escaping ((BeaconFlusher.Result) -> Void)) {
67+
self.reporter = reporter
6868
self.items = items
6969
self.config = config
7070
self.debounce = debounce
@@ -115,7 +115,7 @@ class BeaconFlusher {
115115
disapatchGroup.notify(queue: queue) { [weak self] in
116116
guard let self = self else { return }
117117
self.urlTasks.removeAll()
118-
if self.shouldPerformRetry {
118+
if self.shouldPerformRetry() {
119119
self.retry()
120120
} else {
121121
self.complete()
@@ -124,6 +124,25 @@ class BeaconFlusher {
124124
didStartFlush?()
125125
}
126126

127+
// When error occurred, either goes into slow send mode, or retry sending.
128+
internal func shouldPerformRetry() -> Bool {
129+
let canDoSlowSend = config.slowSendInterval > 0
130+
131+
guard !errors.isEmpty else {
132+
if canDoSlowSend {
133+
// No error, reset the flag
134+
reporter?.setSlowSendStartTime(nil)
135+
}
136+
return false
137+
}
138+
139+
if canDoSlowSend {
140+
reporter?.setSlowSendStartTime(Date())
141+
return false
142+
}
143+
return retryStep < config.maxRetries
144+
}
145+
127146
private func retry() {
128147
retryStep += 1
129148
queue.asyncAfter(deadline: .now() + .milliseconds(retryDelayMilliseconds(for: retryStep)), execute: flush)

Sources/InstanaAgent/Beacons/CoreBeacon/CoreBeacon.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ struct CoreBeacon: Codable {
120120
*/
121121
var m: MetaData?
122122

123+
mutating func updateMetaDataWithSlowSendStartTime(_ time: Date?) {
124+
let key = "slowSendStartTime"
125+
if time == nil {
126+
m?.removeValue(forKey: key)
127+
} else {
128+
if m == nil {
129+
m = [:]
130+
}
131+
m![key] = String(time!.millisecondsSince1970)
132+
}
133+
}
134+
123135
/**
124136
* User ID
125137
*

Sources/InstanaAgent/Beacons/Reporter.swift

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ public class Reporter {
1111
var completionHandler = [Completion]()
1212
let queue: InstanaPersistableQueue<CoreBeacon>
1313
private let dispatchQueue = DispatchQueue(label: "com.instana.ios.agent.reporter", qos: .utility)
14+
private var sendFirstBeacon = true // first beacon is sent all by itself, not in a batch
15+
private var slowSendStartTime: Date?
1416
private var flusher: BeaconFlusher?
15-
private let send: BeaconFlusher.Sender?
17+
internal var send: BeaconFlusher.Sender?
1618
private let rateLimiter: ReporterRateLimiter
1719
private let batterySafeForNetworking: () -> Bool
1820
private let networkUtility: NetworkUtility
@@ -108,10 +110,27 @@ public class Reporter {
108110
scheduleFlush()
109111
}
110112

113+
internal var isInSlowSendMode: Bool {
114+
session.configuration.slowSendInterval > 0 && (sendFirstBeacon || slowSendStartTime != nil)
115+
}
116+
117+
internal func setSlowSendStartTime(_ time: Date?) {
118+
if time == nil {
119+
if slowSendStartTime != nil {
120+
session.logger.add("Slow send ended at \(String(describing: Date()))")
121+
slowSendStartTime = nil
122+
}
123+
} else if slowSendStartTime == nil {
124+
// if slow send started, do not update so as to keep the earliest time
125+
slowSendStartTime = time
126+
session.logger.add("Slow send started at \(String(describing: time!))")
127+
}
128+
}
129+
111130
func scheduleFlush() {
112131
guard !queue.items.isEmpty else { return }
113132
let start = Date()
114-
let debounce = queue.isFull ? 0.0 : flushDebounce
133+
var debounce: TimeInterval
115134
let connectionType = networkUtility.connectionType
116135
guard connectionType != .none else {
117136
return handle(flushResult: .failure([InstanaError.offline]))
@@ -122,7 +141,24 @@ public class Reporter {
122141
if suspendReporting.contains(.lowBattery), !batterySafeForNetworking() {
123142
return handle(flushResult: .failure([InstanaError.lowBattery]))
124143
}
125-
let flusher = BeaconFlusher(items: queue.items, debounce: debounce, config: session.configuration, queue: dispatchQueue, send: send) { [weak self] result in
144+
var beacons: Set<CoreBeacon> = Set([])
145+
if isInSlowSendMode {
146+
if sendFirstBeacon {
147+
debounce = flushDebounce
148+
sendFirstBeacon = false
149+
} else {
150+
debounce = session.configuration.slowSendInterval
151+
}
152+
var beacon = queue.items.first!
153+
beacon.updateMetaDataWithSlowSendStartTime(slowSendStartTime)
154+
beacons.insert(beacon)
155+
} else {
156+
debounce = queue.isFull ? 0.0 : flushDebounce
157+
beacons = queue.items
158+
}
159+
let flusher = BeaconFlusher(reporter: self, items: beacons, debounce: debounce,
160+
config: session.configuration, queue: dispatchQueue,
161+
send: send) { [weak self] result in
126162
guard let self = self else { return }
127163
self.handle(flushResult: result, start)
128164
}

Sources/InstanaAgent/Configuration/InstanaConfiguraton.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import Foundation
1717

1818
// Use a reference type to avoid a copy when having concurrency
1919
class InstanaConfiguration {
20+
static let maxSlowSendInterval = 3600.0
21+
2022
enum SuspendReporting {
2123
/// Reporting is suspended while the device battery is low.
2224
case lowBattery
@@ -46,6 +48,8 @@ class InstanaConfiguration {
4648
}
4749

4850
struct Defaults {
51+
// 0 seconds means disable network probing
52+
static let slowSendInterval: Instana.Types.Seconds = 0.0
4953
static let reporterSendDebounce: Instana.Types.Seconds = 2.0
5054
static let reporterSendLowBatteryDebounce: Instana.Types.Seconds = 10.0
5155
static let gzipReport = ProcessInfo.ignoreZIPReporting ? false : true
@@ -62,6 +66,7 @@ class InstanaConfiguration {
6266
var httpCaptureConfig: HTTPCaptureConfig
6367
var suspendReporting: Set<SuspendReporting>
6468
var monitorTypes: Set<MonitorTypes>
69+
var slowSendInterval: Instana.Types.Seconds
6570
var reporterSendDebounce: Instana.Types.Seconds
6671
var reporterSendLowBatteryDebounce: Instana.Types.Seconds
6772
var maxRetries: Int
@@ -72,7 +77,8 @@ class InstanaConfiguration {
7277
var reporterRateLimits: [ReporterRateLimitConfig]
7378
var isValid: Bool { !key.isEmpty && !reportingURL.absoluteString.isEmpty }
7479

75-
required init(reportingURL: URL, key: String, httpCaptureConfig: HTTPCaptureConfig, enableCrashReporting: Bool) {
80+
required init(reportingURL: URL, key: String, httpCaptureConfig: HTTPCaptureConfig,
81+
enableCrashReporting: Bool, slowSendInterval: Double) {
7682
self.reportingURL = reportingURL
7783
self.key = key
7884
self.httpCaptureConfig = httpCaptureConfig
@@ -81,6 +87,7 @@ class InstanaConfiguration {
8187
if enableCrashReporting {
8288
monitorTypes.insert(.crash)
8389
}
90+
self.slowSendInterval = slowSendInterval
8491
reporterSendDebounce = Defaults.reporterSendDebounce
8592
reporterSendLowBatteryDebounce = Defaults.reporterSendLowBatteryDebounce
8693
maxRetries = Defaults.maxRetries
@@ -92,8 +99,8 @@ class InstanaConfiguration {
9299
}
93100

94101
static func `default`(key: String, reportingURL: URL, httpCaptureConfig: HTTPCaptureConfig = .automatic,
95-
enableCrashReporting: Bool) -> InstanaConfiguration {
102+
enableCrashReporting: Bool, slowSendInterval: Double = 0.0) -> InstanaConfiguration {
96103
self.init(reportingURL: reportingURL, key: key, httpCaptureConfig: httpCaptureConfig,
97-
enableCrashReporting: enableCrashReporting)
104+
enableCrashReporting: enableCrashReporting, slowSendInterval: slowSendInterval)
98105
}
99106
}

Sources/InstanaAgent/Instana.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,46 @@ import Foundation
7070
}
7171
}
7272

73-
/// Configures and sets up the Instana agent with the default configuration.
73+
/// Configures and sets up the Instana agent.
74+
///
75+
/// - Note: Should be called only once, as soon as posible. Preferably in `application(_:, didFinishLaunchingWithOptions:)`
76+
/// - Parameters:
77+
/// - key: Instana key to identify your application.
78+
/// - reportingURL: Reporting URL for the Instana backend.
79+
/// - options: InstanaSetupOptions which includes collectionEnabled, enableCrashReporting, slowSendInterval etc.
80+
///
81+
/// - Returns: true on success, false on error
82+
@objc
83+
public static func setup(key: String, reportingURL: URL, options: InstanaSetupOptions?) -> Bool {
84+
var httpCaptureConfig = HTTPCaptureConfig.automatic
85+
var collectionEnabled = true
86+
var enableCrashReporting = false
87+
var slowSendInterval = 0.0
88+
if let options = options {
89+
httpCaptureConfig = options.httpCaptureConfig
90+
collectionEnabled = options.collectionEnabled
91+
enableCrashReporting = options.enableCrashReporting
92+
93+
let debounce = InstanaConfiguration.Defaults.reporterSendDebounce
94+
if options.slowSendInterval != 0.0,
95+
options.slowSendInterval < debounce || options.slowSendInterval > InstanaConfiguration.maxSlowSendInterval {
96+
// Illegal slowSendInterval. Expected value 2 ~ 3600 in seconds
97+
return false
98+
}
99+
slowSendInterval = options.slowSendInterval
100+
}
101+
let config = InstanaConfiguration.default(key: key, reportingURL: reportingURL,
102+
httpCaptureConfig: httpCaptureConfig,
103+
enableCrashReporting: enableCrashReporting,
104+
slowSendInterval: slowSendInterval)
105+
let session = InstanaSession(configuration: config, propertyHandler: InstanaPropertyHandler(),
106+
collectionEnabled: collectionEnabled)
107+
Instana.current = Instana(session: session)
108+
return true
109+
}
110+
111+
/// Deprecated! Use setup( ) with InstanaSetupOptions instead.
112+
/// Configures and sets up the Instana agent with the default configuration. (deprecated)
74113
/// - HTTP sessions will be captured automatically by default
75114
///
76115
/// - Note: Should be called only once, as soon as posible. Preferably in `application(_:, didFinishLaunchingWithOptions:)`
@@ -88,6 +127,7 @@ import Foundation
88127
Instana.current = Instana(session: session)
89128
}
90129

130+
/// Deprecated! Use setup( ) with InstanaSetupOptions instead.
91131
/// Configures and sets up the Instana agent with a custom HTTP capture configuration.
92132
///
93133
/// - Note: Should be called only once, as soon as posible. Preferably in `application(_:, didFinishLaunchingWithOptions:)`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// Copyright © 2023 IBM Corp. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
@objc public class InstanaSetupOptions: NSObject {
8+
public var httpCaptureConfig: HTTPCaptureConfig
9+
public var collectionEnabled: Bool
10+
public var enableCrashReporting: Bool
11+
public var slowSendInterval: Instana.Types.Seconds
12+
13+
/// Instana custom configuration for setup.
14+
///
15+
/// - Parameters:
16+
/// - httpCaptureConfig: HTTP monitoring configuration to set the capture behavior (automatic, manual, automaticAndManual or none) HTTP requests & responses
17+
/// - collectionEnabled: Enable or disable collection (instrumentation) on setup. Can be changed later via the property `collectionEnabled` (Default: true)
18+
/// - enableCrashReporting: Subscribe to metricKit events so as to enable crash reporting.
19+
/// App must have explicitly asked user permission to subscribe before this call.
20+
/// - slowSendInterval: Enable slow send mode on beacon send failure when a positive number is passed
21+
@objc public
22+
init(httpCaptureConfig: HTTPCaptureConfig = .automatic,
23+
collectionEnabled: Bool = true, enableCrashReporting: Bool = false,
24+
slowSendInterval: Instana.Types.Seconds = 0.0) {
25+
self.httpCaptureConfig = httpCaptureConfig
26+
self.collectionEnabled = collectionEnabled
27+
self.enableCrashReporting = enableCrashReporting
28+
self.slowSendInterval = slowSendInterval
29+
}
30+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension Double {
4+
static func == (lhs: Double, rhs: Double) -> Bool {
5+
return abs(lhs - rhs) < 0.000001
6+
}
7+
8+
static func != (lhs: Double, rhs: Double) -> Bool {
9+
return !(lhs == rhs)
10+
}
11+
}

0 commit comments

Comments
 (0)