Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Crash Reporting code #20

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
full-build:
runs-on: macOS-latest
runs-on: macOS-13
steps:
- name: Checkout
uses: actions/checkout@v1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -14,6 +14,7 @@
86461EFC269729C0007C6DC0 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 86461EFB269729C0007C6DC0 /* CrashReporter */; };
86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = 86461F0426972A11007C6DC0 /* sample_v1.plcrash */; };
86D3180A271655B300B43379 /* SplunkOtel in Frameworks */ = {isa = PBXBuildFile; productRef = 86D31809271655B300B43379 /* SplunkOtel */; };
BA5DB5512D10A99F0090298A /* sample_v3.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = BA5DB5502D10A99F0090298A /* sample_v3.plcrash */; };
D774545D28E38CF40056159F /* DeviceStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D774545C28E38CF40056159F /* DeviceStats.swift */; };
D7C64D1228E494C50086368D /* DeviceStatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C64D1128E494C50086368D /* DeviceStatsTests.swift */; };
D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = D7D1428F293804A200CAD87E /* sample_v2.plcrash */; };
Expand All @@ -38,6 +39,7 @@
86461EEA26972906007C6DC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
86461EF626972964007C6DC0 /* CrashReporting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = "<group>"; };
86461F0426972A11007C6DC0 /* sample_v1.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file.plcrash; path = sample_v1.plcrash; sourceTree = "<group>"; };
BA5DB5502D10A99F0090298A /* sample_v3.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample_v3.plcrash; sourceTree = "<group>"; };
D774545C28E38CF40056159F /* DeviceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStats.swift; sourceTree = "<group>"; };
D7C64D1128E494C50086368D /* DeviceStatsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatsTests.swift; sourceTree = "<group>"; };
D7D1428F293804A200CAD87E /* sample_v2.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample_v2.plcrash; sourceTree = "<group>"; };
Expand Down Expand Up @@ -96,6 +98,7 @@
86461EE726972906007C6DC0 /* SplunkRumCrashReportingTests */ = {
isa = PBXGroup;
children = (
BA5DB5502D10A99F0090298A /* sample_v3.plcrash */,
D7D1428F293804A200CAD87E /* sample_v2.plcrash */,
86461F0426972A11007C6DC0 /* sample_v1.plcrash */,
86461EE826972906007C6DC0 /* CrashTests.swift */,
Expand Down Expand Up @@ -213,6 +216,7 @@
buildActionMask = 2147483647;
files = (
86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */,
BA5DB5512D10A99F0090298A /* sample_v3.plcrash in Resources */,
D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
173 changes: 163 additions & 10 deletions SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let CrashReportingVersionString = "0.6.0"

var TheCrashReporter: PLCrashReporter?
private var customDataDictionary: [String: String] = [String: String]()
private var allUsedImageNames: [String] = []

func initializeCrashReporting() {
let startupSpan = buildTracer().spanBuilder(spanName: "SplunkRumCrashReporting").startSpan()
Expand All @@ -39,11 +40,20 @@ func initializeCrashReporting() {
return
}
let crashReporter = crashReporter_!
let success = crashReporter.enable()
SplunkRum.debugLog("PLCrashReporter enabled: "+success.description)
if !success {
startupSpan.setAttribute(key: "error.message", value: "Cannot enable PLCrashReporter")
return

// Stop enable if debugger attached
var inDebugger = false
if isDebuggerAttached() {
startupSpan.setAttribute(key: "error.message", value: "Debugger present. Will not construct PLCrashReporter")
SplunkRum.debugLog("Debugger present. Will not enable PLCrashReporter")
inDebugger = true
}
if inDebugger == false {
let success = crashReporter.enable()
SplunkRum.debugLog("PLCrashReporter enabled: "+success.description)
if !success {
startupSpan.setAttribute(key: "error.message", value: "Cannot enable PLCrashReporter")
}
}
TheCrashReporter = crashReporter
updateCrashReportSessionId()
Expand All @@ -58,6 +68,9 @@ func initializeCrashReporting() {
}
SplunkRum.debugLog("Had a pending crash report")
do {
allUsedImageNames.removeAll()
let path = crashReporter.crashReportPath()
print(path as Any)
let data = crashReporter.loadPendingCrashReportData()
try loadPendingCrashReport(data)
} catch {
Expand All @@ -66,8 +79,8 @@ func initializeCrashReporting() {
// yes, fall through to purge
}
crashReporter.purgePendingCrashReport()

}

private func buildTracer() -> Tracer {
return OpenTelemetry.instance.tracerProvider.get(instrumentationName: "splunk-ios-crashreporting", instrumentationVersion: CrashReportingVersionString)

Expand Down Expand Up @@ -129,7 +142,7 @@ func loadPendingCrashReport(_ data: Data!) throws {
span.setAttribute(key: "crash.freeDiskSpace", value: customData!["freeDiskSpace"]!)
span.setAttribute(key: "crash.freeMemory", value: customData!["freeMemory"]!)
} else {
span.setAttribute(key: "crash.rumSessionId", value: String(decoding: report.customData, as: UTF8.self))
span.setAttribute(key: "crash.rumSessionId", value: String(bytes: report.customData, encoding: String.Encoding.utf8) ?? "Unknown")
}
}
// "marketing version" here matches up to our use of CFBundleShortVersionString
Expand All @@ -138,10 +151,25 @@ func loadPendingCrashReport(_ data: Data!) throws {
span.addEvent(name: "crash.timestamp", timestamp: report.systemInfo.timestamp)
span.setAttribute(key: "exception.type", value: exceptionType ?? "unknown")
span.setAttribute(key: "crash.address", value: report.signalInfo.address.description)
for case let thread as PLCrashReportThreadInfo in report.threads where thread.crashed {
span.setAttribute(key: "exception.stacktrace", value: crashedThreadToStack(report: report, thread: thread))
break

var allThreads: [Any] = []
for case let thread as PLCrashReportThreadInfo in report.threads {

// Original crashed thread handler
if thread.crashed {
span.setAttribute(key: "exception.stacktrace", value: crashedThreadToStack(report: report, thread: thread))
}

// Detailed thread handler
allThreads.append(detailedThreadToStackFrames(report: report, thread: thread))
}
let threadPayload = convertArrayToJSONString(allThreads) ?? "Unable to create stack frames"
span.setAttribute(key: "exception.stackFrames", value: threadPayload)
var images: [Any] = []
images = imageList(images: report.images)
let imagesPayload = convertArrayToJSONString(images) ?? "Unable to create images"
span.setAttribute(key: "exception.images", value: imagesPayload)

if report.hasExceptionInfo {
span.setAttribute(key: "exception.type", value: report.exceptionInfo.exceptionName)
span.setAttribute(key: "exception.message", value: report.exceptionInfo.exceptionReason)
Expand Down Expand Up @@ -200,3 +228,128 @@ func formatStackFrame(frame: PLCrashReportStackFrameInfo, frameNum: Int, report:
initializeCrashReporting()
}
}

// Symbolication Support Code

// Extracts detail for one thread
func detailedThreadToStackFrames(report: PLCrashReport, thread: PLCrashReportThreadInfo) -> [String: Any] {

var oneThread: [String: Any] = [:]
var allStackFrames: [Any] = []

let threadNum = thread.threadNumber as NSNumber
oneThread["threadNumber"] = threadNum.stringValue
oneThread["crashed"] = thread.crashed

var frameNum = 0
while frameNum < thread.stackFrames.count {
var oneFrame: [String: Any] = [:]

let frame = thread.stackFrames[frameNum] as! PLCrashReportStackFrameInfo
let instructionPointer = frame.instructionPointer
oneFrame["instructionPointer"] = instructionPointer

var baseAddress: UInt64 = 0
var offset: UInt64 = 0
var imageName = "???"

let imageInfo = report.image(forAddress: instructionPointer)
if imageInfo != nil {
imageName = imageInfo?.imageName ?? "???"
baseAddress = imageInfo!.imageBaseAddress
offset = instructionPointer - baseAddress
}
oneFrame["imageName"] = imageName
allUsedImageNames.append(imageName)

if frame.symbolInfo != nil {
let symbolName = frame.symbolInfo.symbolName
let symOffset = instructionPointer - frame.symbolInfo.startAddress
oneFrame["symbolName"] = symbolName
oneFrame["offset"] = symOffset
} else {
oneFrame["baseAddress"] = baseAddress
oneFrame["offset"] = offset
}
allStackFrames.append(oneFrame)
frameNum += 1
}
oneThread["stackFrames"] = allStackFrames
return oneThread
}

// Returns true if debugger is attached
private func isDebuggerAttached() -> Bool {
var debuggerIsAttached = false

var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var info = kinfo_proc()
var infoSize = MemoryLayout<kinfo_proc>.size

_ = name.withUnsafeMutableBytes { (nameBytePtr: UnsafeMutableRawBufferPointer) -> Bool in
guard let nameBytesBlindMemory = nameBytePtr.bindMemory(to: Int32.self).baseAddress else {
return false
}
return sysctl(nameBytesBlindMemory, 4, &info, &infoSize, nil, 0) != -1
}
if !debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0 {
debuggerIsAttached = true
}
return debuggerIsAttached
}

// Returns array of code images used by app
func imageList(images: [Any]) -> [Any] {
var outputImages: [Any] = []
for image in images {
var imageDictionary: [String: Any] = [:]
guard let image = image as? PLCrashReportBinaryImageInfo else {
continue
}

// Only add the image to the list if it was noted in the stack traces
if allUsedImageNames.contains(image.imageName) {
imageDictionary["codeType"] = cpuTypeDictionary(cpuType: image.codeType)
imageDictionary["baseAddress"] = image.imageBaseAddress
imageDictionary["imageSize"] = image.imageSize
imageDictionary["imagePath"] = image.imageName
imageDictionary["imageUUID"] = image.imageUUID

outputImages.append(imageDictionary)
}
}
return outputImages
}

// Returns formatted cpu data
func cpuTypeDictionary(cpuType: PLCrashReportProcessorInfo) -> [String: String] {
var dictionary: [String: String] = [:]
dictionary.updateValue(String(cpuType.type), forKey: "cType")
dictionary.updateValue(String(cpuType.subtype), forKey: "cSubType")
return dictionary
}

// JSON support code
func convertDictionaryToJSONString(_ dictionary: [String: Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted) else {

return nil
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {

return nil
}
return jsonString
}

func convertArrayToJSONString(_ array: [Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: array, options: .prettyPrinted) else {

return nil
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {

return nil
}
return jsonString
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,59 @@ class CrashTests: XCTestCase {
XCTAssertEqual(startup!.attributes["component"]?.description, "appstart")

}
func testBasics_v3() throws {
let crashPath = Bundle(for: CrashTests.self).url(forResource: "sample_v3", withExtension: "plcrash")!
let crashData = try Data(contentsOf: crashPath)

SplunkRumBuilder(beaconUrl: "http://127.0.0.1:8989/v1/traces", rumAuth: "FAKE")
.allowInsecureBeacon(enabled: true)
.debug(enabled: true)
.build()
let tracerProvider = TracerProviderBuilder()
.add(spanProcessor: SimpleSpanProcessor(spanExporter: TestSpanExporter()))
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
localSpans.removeAll()

SplunkRumCrashReporting.start()
try loadPendingCrashReport(crashData)

XCTAssertEqual(localSpans.count, 2)
let crashReport = localSpans.first(where: { (span) -> Bool in
return span.name == "SIGTRAP"
})
let startup = localSpans.first(where: { (span) -> Bool in
return span.name == "SplunkRumCrashReporting"
})

XCTAssertNotNil(crashReport)
XCTAssertNotEqual(crashReport!.attributes["splunk.rumSessionId"], crashReport!.attributes["crash.rumSessionId"])
XCTAssertEqual(crashReport!.attributes["crash.rumSessionId"]?.description, "a9ef9e0a7683eaf973ec8fa4b31df3f9")
XCTAssertEqual(crashReport!.attributes["crash.address"]?.description, "6786470812")
XCTAssertEqual(crashReport!.attributes["component"]?.description, "crash")
XCTAssertEqual(crashReport!.attributes["error"]?.description, "true")
XCTAssertEqual(crashReport!.attributes["exception.type"]?.description, "SIGTRAP")
XCTAssertTrue(crashReport!.attributes["exception.stacktrace"]?.description.contains("UIKitCore") ?? false)
XCTAssertEqual(crashReport!.attributes["crash.batteryLevel"]?.description, "100.0%")
XCTAssertEqual(crashReport!.attributes["crash.freeDiskSpace"]?.description, "628.03 GB")
XCTAssertEqual(crashReport!.attributes["crash.freeMemory"]?.description, "31.88 GB")
XCTAssertEqual(crashReport!.attributes["crash.app.version"]?.description, "1.0")
XCTAssertNotNil(crashReport!.attributes["exception.stackFrames"])
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("threadNumber") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("crashed") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("instructionPointer") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("baseAddress") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("imageName") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.stackFrames"]?.description.contains("offset") ?? false)
XCTAssertNotNil(crashReport!.attributes["exception.images"])
XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imageUUID") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imageSize") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imagePath") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("codeType") ?? false)
XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("baseAddress") ?? false)

XCTAssertNotNil(startup)
XCTAssertEqual(startup!.attributes["component"]?.description, "appstart")

}
}
Binary file not shown.
Loading