Skip to content

Commit e75ec4a

Browse files
Hongyan JiangGitHub Enterprise
authored andcommitted
add drop beacon handler
1 parent b5a71f6 commit e75ec4a

27 files changed

+772
-7
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 1.8.4
4+
- add drop beacon handler
5+
36
## 1.8.3
47
- update minimum supported iOS version to 12 (was 11 before)
58
- do not rate limit session start beacon and crash beacons

InstanaAgent.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616
#
1717

1818
s.name = "InstanaAgent"
19-
s.version = "1.8.3"
19+
s.version = "1.8.4"
2020
s.summary = "Instana iOS agent."
2121

2222
# This description is used to generate tags and improve search results.

Sources/InstanaAgent/Beacons/Beacons Types/Beacon.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class Beacon: Identifiable {
1616
self.viewName = ViewChange.validate(viewName: viewName)
1717
}
1818

19+
func extractDropBeaconValues() -> DropBeacon? {
20+
return nil
21+
}
22+
1923
// A hex encoded 64 bit random ID.
2024
static func generateUniqueIdImpl() -> String {
2125
let validIdCharacters = Array("0123456789abcdef")

Sources/InstanaAgent/Beacons/Beacons Types/CustomBeacon.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class CustomBeacon: Beacon {
1616
let error: Error?
1717
let metaData: MetaData?
1818
let customMetric: Double?
19+
let eventType: String? // internal meta
1920

2021
init(timestamp: Instana.Types.Milliseconds? = nil,
2122
name: String,
@@ -24,7 +25,8 @@ class CustomBeacon: Beacon {
2425
error: Error? = nil,
2526
metaData: MetaData? = nil,
2627
viewName: String? = CustomBeaconDefaultViewNameID,
27-
customMetric: Double? = nil) {
28+
customMetric: Double? = nil,
29+
eventType: String? = nil) {
2830
self.duration = duration
2931
self.name = name
3032
self.error = error
@@ -38,6 +40,14 @@ class CustomBeacon: Beacon {
3840
start = timestamp
3941
}
4042
self.customMetric = customMetric
43+
self.eventType = eventType
4144
super.init(timestamp: start, viewName: viewName)
4245
}
46+
47+
override func extractDropBeaconValues() -> CustomEventDropBeacon {
48+
return CustomEventDropBeacon(timestamp: timestamp, eventName: name, view: viewName,
49+
errorCount: (error != nil) ? 1 : nil,
50+
errorMessage: (error != nil) ? "\(error!.localizedDescription)" : nil,
51+
customMetric: (customMetric != nil) ? "\(customMetric!)" : nil)
52+
}
4353
}

Sources/InstanaAgent/Beacons/Beacons Types/HTTPBeacon.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ class HTTPBeacon: Beacon {
4040
self.backendTracingID = backendTracingID
4141
super.init(timestamp: timestamp, viewName: viewName)
4242
}
43+
44+
override func extractDropBeaconValues() -> HTTPDropBeacon {
45+
return HTTPDropBeacon(timestamp: timestamp, url: url.absoluteString,
46+
hsStatusCode: String(responseCode),
47+
view: viewName, hmMethod: method, headers: header)
48+
}
4349
}

Sources/InstanaAgent/Beacons/Beacons Types/ViewChange.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class ViewChange: Beacon {
5151
return true
5252
}
5353

54+
override func extractDropBeaconValues() -> ViewDropBeacon {
55+
return ViewDropBeacon(timestamp: timestamp, viewName: viewName, imMap: viewInternalCPMetaMap)
56+
}
57+
5458
static func validate(viewName: String?) -> String? {
5559
guard let value = viewName else { return nil }
5660
return value.cleanEscapeAndTruncate(at: InstanaProperties.viewMaxLength)

Sources/InstanaAgent/Beacons/CoreBeacon/CoreBeaconFactory.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ extension CoreBeacon {
197197
if let customMetric = beacon.customMetric {
198198
cm = String(customMetric)
199199
}
200+
if let eventType = beacon.eventType {
201+
im = MetaData()
202+
im![internalMetaDataKeyCustom_eventType] = eventType
203+
}
200204
}
201205

202206
private mutating func add(error: Error) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Copyright © 2024 IBM Corp. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
///
8+
/// CustomEvent dropped beacon class
9+
///
10+
public class CustomEventDropBeacon: DropBeacon {
11+
var eventName: String
12+
var view: String?
13+
var errorCount: Int?
14+
var errorMessage: String?
15+
var customMetric: String?
16+
17+
init(timestamp: Instana.Types.Milliseconds, eventName: String, view: String?,
18+
errorCount: Int?, errorMessage: String?, customMetric: String?) {
19+
self.eventName = eventName
20+
self.view = view
21+
self.errorCount = errorCount
22+
self.errorMessage = errorMessage
23+
self.customMetric = customMetric
24+
super.init(timestamp: timestamp)
25+
}
26+
27+
override func getKey() -> String {
28+
let view1 = view ?? ""
29+
let errorMessage1 = errorMessage ?? ""
30+
let errorCount1 = errorCount ?? 0
31+
return "\(eventName)|\(errorMessage1)|\(errorCount1)|\(view1)"
32+
}
33+
34+
override func toString() -> String? {
35+
let view1 = view ?? ""
36+
let errorMessage1 = errorMessage ?? ""
37+
let errorCount1 = errorCount ?? 0
38+
let customMetric1 = customMetric ?? ""
39+
let zInfoExtra = ["eventName": eventName, "errorMessage": validateLength(errorMessage1, maxLen: 200),
40+
"customMetric": validateLength(customMetric1, maxLen: 100),
41+
"view": view1, "errorCount": errorCount1] as [String: Any]
42+
return convertToString(type: "CUSTOM_EVENT", subDict: zInfoExtra)
43+
}
44+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright © 2024 IBM Corp. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
///
8+
/// Base class for dropped beacon
9+
///
10+
///
11+
public class DropBeacon {
12+
var count: Int
13+
var timeMin: Instana.Types.Milliseconds
14+
var timeMax: Instana.Types.Milliseconds
15+
16+
init(timestamp: Instana.Types.Milliseconds) {
17+
count = 1
18+
timeMin = timestamp
19+
timeMax = timestamp
20+
}
21+
22+
func getKey() -> String {
23+
return "dropBeaconPlaceholderKey"
24+
}
25+
26+
func toString() -> String? {
27+
return nil
28+
}
29+
30+
func dictionaryToJsonString(_ dict: [String: Any]?, needSort: Bool = true) -> String? {
31+
guard let dict = dict else {
32+
return nil
33+
}
34+
do {
35+
let options: JSONSerialization.WritingOptions = needSort ? [.sortedKeys] : []
36+
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: options)
37+
if let jsonString = String(data: jsonData, encoding: .utf8) {
38+
return validateLength(jsonString)
39+
}
40+
} catch {
41+
Instana.current?.session.logger.add("Error converting dictionary to JSON: \(error.localizedDescription)")
42+
}
43+
return nil
44+
}
45+
46+
func convertToString(type: String, subDict: [String: Any]) -> String? {
47+
let zInfoBase = ["tMin": timeMin, "tMax": timeMax]
48+
let zInfoDict = subDict.reduce(into: zInfoBase) { result, element in
49+
result[element.key] = element.value
50+
}
51+
let dict = ["type": type, "count": count, "zInfo": zInfoDict] as [String: Any]
52+
return dictionaryToJsonString(dict)
53+
}
54+
55+
public func validateLength(_ str: String, maxLen: Int = 1024) -> String {
56+
if str.count <= maxLen {
57+
return str
58+
}
59+
return str.prefix(maxLen - 3) + "..."
60+
}
61+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// Copyright © 2024 IBM Corp. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
///
8+
/// A handler for beacons that were dropped because the maximum beacon send limit was reached.
9+
/// This will sample the dropped beacons and attempt to resend them as a custom event, once the
10+
/// limit is no longer exceeded.
11+
///
12+
public class DropBeaconHandler {
13+
var httpUniqueMap: [String: DropBeacon]
14+
var viewUniqueMap: [String: DropBeacon]
15+
var customUniqueMap: [String: DropBeacon]
16+
var MIN_BEACONS_REQUIRED = 2
17+
let SAMPLING_BEACON_LIMIT = 5
18+
var droppingStartTime: Instana.Types.Milliseconds
19+
var droppingStartView: String?
20+
21+
init() {
22+
httpUniqueMap = [String: DropBeacon]()
23+
viewUniqueMap = [String: DropBeacon]()
24+
customUniqueMap = [String: DropBeacon]()
25+
droppingStartTime = 0
26+
droppingStartView = nil
27+
}
28+
29+
func addBeaconToDropHandler(beacon: Beacon) {
30+
guard let extractedBeacon = beacon.extractDropBeaconValues() else {
31+
return
32+
}
33+
34+
if droppingStartTime == 0 {
35+
droppingStartTime = Date().millisecondsSince1970
36+
}
37+
38+
let key = extractedBeacon.getKey()
39+
if extractedBeacon is HTTPDropBeacon {
40+
saveDroppedBeacons(dropBeacon: extractedBeacon, key: key, uniqueMap: &httpUniqueMap)
41+
} else if extractedBeacon is ViewDropBeacon {
42+
saveDroppedBeacons(dropBeacon: extractedBeacon, key: key, uniqueMap: &viewUniqueMap)
43+
} else if extractedBeacon is CustomEventDropBeacon {
44+
saveDroppedBeacons(dropBeacon: extractedBeacon, key: key, uniqueMap: &customUniqueMap)
45+
}
46+
47+
if droppingStartView == nil {
48+
droppingStartView = beacon.viewName
49+
}
50+
}
51+
52+
func mergeDroppedBeacons() -> CustomBeacon? {
53+
if droppingStartTime == 0 {
54+
// no dropped beacons
55+
return nil
56+
}
57+
58+
var totalDropBeaconCount = 0
59+
var mergedBeaconsMap = [String: String]()
60+
totalDropBeaconCount += mergeDroppedBeacons(uniqueMap: httpUniqueMap,
61+
keyPrefix: "HTTP", mergedBeacons: &mergedBeaconsMap)
62+
totalDropBeaconCount += mergeDroppedBeacons(uniqueMap: viewUniqueMap,
63+
keyPrefix: "VIEW", mergedBeacons: &mergedBeaconsMap)
64+
totalDropBeaconCount += mergeDroppedBeacons(uniqueMap: customUniqueMap,
65+
keyPrefix: "CUSTOM_EVENT", mergedBeacons: &mergedBeaconsMap)
66+
67+
guard totalDropBeaconCount > 0 else {
68+
reset() // Throw away accumulated dropped beacons if there are not many
69+
return nil
70+
}
71+
72+
let mergedBeacons = CustomBeacon(timestamp: droppingStartTime,
73+
name: "INSTANA_DROPPED_BEACON_SAMPLE",
74+
metaData: mergedBeaconsMap,
75+
viewName: droppingStartView,
76+
customMetric: Double(totalDropBeaconCount),
77+
eventType: "beacon-drop")
78+
79+
reset() // Prepare for next round, might throw away some unsent beacons
80+
return mergedBeacons
81+
}
82+
83+
private func reset() {
84+
droppingStartTime = 0
85+
droppingStartView = nil
86+
httpUniqueMap = [String: DropBeacon]()
87+
viewUniqueMap = [String: DropBeacon]()
88+
customUniqueMap = [String: DropBeacon]()
89+
}
90+
91+
private func saveDroppedBeacons(dropBeacon: DropBeacon, key: String, uniqueMap: inout [String: DropBeacon]) {
92+
if let existingBeacon = uniqueMap[key] {
93+
existingBeacon.count += 1
94+
if dropBeacon.timeMin < existingBeacon.timeMin {
95+
existingBeacon.timeMin = dropBeacon.timeMin
96+
}
97+
if dropBeacon.timeMax > existingBeacon.timeMax {
98+
existingBeacon.timeMax = dropBeacon.timeMax
99+
}
100+
} else {
101+
uniqueMap[key] = dropBeacon
102+
}
103+
}
104+
105+
private func mergeDroppedBeacons(uniqueMap: [String: DropBeacon], keyPrefix: String,
106+
mergedBeacons: inout [String: String]) -> Int {
107+
let totalDropBeaconCount = uniqueMap.reduce(into: 0) { result, entry in
108+
result += entry.value.count
109+
}
110+
111+
if totalDropBeaconCount > MIN_BEACONS_REQUIRED {
112+
// Though only maximum SAMPLING_BEACON_LIMIT beacons are collected,
113+
// totalDropBeaconCount includes the not sampled beacons.
114+
let descendingDropBeacons = uniqueMap.sorted { $0.value.count > $1.value.count }.prefix(SAMPLING_BEACON_LIMIT)
115+
for (_, entryValue) in descendingDropBeacons {
116+
let key = generateKey(prefix: keyPrefix)
117+
mergedBeacons[key] = entryValue.toString()
118+
}
119+
return totalDropBeaconCount
120+
}
121+
return 0
122+
}
123+
124+
private func generateKey(prefix: String) -> String {
125+
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
126+
let randomStr = String((0 ..< 6).map { _ in chars.randomElement()! })
127+
return "\(prefix)-\(randomStr)"
128+
}
129+
}

0 commit comments

Comments
 (0)