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

Allows to add a custom stacktrace through the log API #184

Merged
merged 6 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 0 additions & 4 deletions Examples/BrandGame/BrandGame.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
7BCB66282AC4742800ABD33A /* MenuList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCB66272AC4742800ABD33A /* MenuList.swift */; };
7BCB662A2AC4767B00ABD33A /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCB66292AC4767B00ABD33A /* MainMenu.swift */; };
7BCB662E2AC476BC00ABD33A /* NetworkStressTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCB662D2AC476BC00ABD33A /* NetworkStressTest.swift */; };
FA1114462C8A4EE30026499D /* StackTraceBehavior+AsString.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1114452C8A4EE30026499D /* StackTraceBehavior+AsString.swift */; };
FA3B0A112C3C2A5600315578 /* EmbraceCore in Frameworks */ = {isa = PBXBuildFile; productRef = FA3B0A102C3C2A5600315578 /* EmbraceCore */; };
FA3B0A132C3C2A5600315578 /* EmbraceCrash in Frameworks */ = {isa = PBXBuildFile; productRef = FA3B0A122C3C2A5600315578 /* EmbraceCrash */; };
FA3B0A152C3C2A5600315578 /* EmbraceCrashlyticsSupport in Frameworks */ = {isa = PBXBuildFile; productRef = FA3B0A142C3C2A5600315578 /* EmbraceCrashlyticsSupport */; };
Expand Down Expand Up @@ -135,7 +134,6 @@
7BCB66272AC4742800ABD33A /* MenuList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuList.swift; sourceTree = "<group>"; };
7BCB66292AC4767B00ABD33A /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
7BCB662D2AC476BC00ABD33A /* NetworkStressTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStressTest.swift; sourceTree = "<group>"; };
FA1114452C8A4EE30026499D /* StackTraceBehavior+AsString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StackTraceBehavior+AsString.swift"; sourceTree = "<group>"; };
FA4C5F322C486E13005C0371 /* CreateSpanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSpanView.swift; sourceTree = "<group>"; };
FA4C5F342C487B45005C0371 /* AttributesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesView.swift; sourceTree = "<group>"; };
FA4FA4F92C62DBB600E66300 /* NoSpacesTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSpacesTextField.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -454,7 +452,6 @@
isa = PBXGroup;
children = (
FA9505152B86640F00562A4C /* LoggingView.swift */,
FA1114452C8A4EE30026499D /* StackTraceBehavior+AsString.swift */,
);
path = Logging;
sourceTree = "<group>";
Expand Down Expand Up @@ -677,7 +674,6 @@
7B3143C62AFF1E7D00BA0D2F /* ReflexGameModel.swift in Sources */,
FAFDA3D72C34273100AD17CF /* MemoryPressureSimulatorView.swift in Sources */,
7B5630A52AC1331000E0DF39 /* Color+Random.swift in Sources */,
FA1114462C8A4EE30026499D /* StackTraceBehavior+AsString.swift in Sources */,
7B62AD952B0040FE0087C2AE /* Minigame.swift in Sources */,
7BBBCD582BD0205C00BC289A /* StdoutLogExporter.swift in Sources */,
7BCB66282AC4742800ABD33A /* MenuList.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import EmbraceCommonInternal
struct LoggingView: View {
@State private var logMessage: String = "This is the log message..."
@State private var severity: Int = LogSeverity.info.rawValue
@State private var behavior: Int = StackTraceBehavior.default.rawValue
@State private var behavior: Int = Behavior.default.rawValue
@State private var key: String = ""
@State private var value: String = ""
@State private var attributes: [String: String] = [:]
Expand All @@ -19,8 +19,8 @@ struct LoggingView: View {
[.info, .warn, .error]
}()

private let behaviors: [StackTraceBehavior] = {
[.default, .notIncluded]
private let behaviors: [Behavior] = {
[.default, .notIncluded, .custom]
}()

var body: some View {
Expand Down Expand Up @@ -86,14 +86,38 @@ private extension LoggingView {
print("Wrong severity number")
return
}
guard let behavior = StackTraceBehavior(rawValue: behavior) else {
print("Wrong behavior")
guard let stackTraceBehavior = try? getStackTraceBehavior() else {
print("Wrong stacktrace behavior")
return
}
Embrace.client?.log(logMessage, severity: severity, attributes: attributes, stackTraceBehavior: behavior)
Embrace.client?.log(
logMessage,
severity: severity,
attributes: attributes,
stackTraceBehavior: stackTraceBehavior
)
cleanUpFields()
}

func getStackTraceBehavior() throws -> StackTraceBehavior {
switch Behavior(rawValue: behavior) {
case .default:
return .default
case .notIncluded:
return .notIncluded
case .custom:
return .custom(try EmbraceStackTrace(
frames: [
"0 BrandGame 0x0000000005678def [SomeClass method] + 48",
"1 Random Library 0x0000000001234abc [Random init]",
"2 \(UUID().uuidString) 0x0000000001234abc [\(UUID().uuidString) \(UUID().uuidString))]"
])
)
case .none:
throw NSError(domain: "BrandGame", code: -1, userInfo: [:])
}
}

func cleanUpFields() {
guard shouldCleanUp else {
return
Expand All @@ -105,6 +129,36 @@ private extension LoggingView {
}
}

extension LoggingView {
enum Behavior: Int {
case notIncluded
case `default`
case custom

static func from(_ stackTraceBehavior: StackTraceBehavior) -> Self {
switch stackTraceBehavior {
case .notIncluded:
return .notIncluded
case .default:
return .default
case .custom(_):
return .custom
}
}

func asString() -> String {
return switch self {
case .notIncluded:
"Not included"
case .default:
"Default"
case .custom:
"Custom"
}
}
}
}

#Preview {
LoggingView()
}

This file was deleted.

14 changes: 0 additions & 14 deletions Sources/EmbraceCommonInternal/Enums/StackTraceBehavior.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Copyright © 2025 Embrace Mobile, Inc. All rights reserved.
//

import Foundation

public enum EmbraceStackTraceError: Error {
case invalidFormat
case frameIsTooLong
}

extension EmbraceStackTraceError: LocalizedError, CustomNSError {

public static var errorDomain: String {
return "Embrace"
}

public var errorCode: Int {
switch self {
case .invalidFormat:
return -1
case .frameIsTooLong:
return -2
}
}

public var errorDescription: String? {
switch self {
case .invalidFormat:
return """
Invalid stack trace format. Each frame should follow this format:
<index> <image> <memory address> <symbol> [ + <offset> ]
The "+ <offset>" part is optional.
"""
case .frameIsTooLong:
return "The stacktrace contains frames that are longer than 10.000 characters."
}
}

public var localizedDescription: String {
return self.errorDescription ?? "No Matching Error"
}
}
83 changes: 83 additions & 0 deletions Sources/EmbraceCommonInternal/Models/EmbraceStackTrace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright © 2025 Embrace Mobile, Inc. All rights reserved.
//

import Foundation

public struct EmbraceStackTrace: Equatable {
/// The maximum amount of characters a stack trace frame can have.
private static let maximumFrameLength = 10000

/// The maximum amount of frames we support in a stacktrace.
private static let maximumAmountOfFrames = 200

/// The captured stack frames, following the format of `Thread.callStackSymbols`.
public private(set) var frames: [String]

/// Initializes a `EmbraceStackTrace` with a given list of stack frames.
///
/// Each frame is represented as a `String`, containing:
/// - The frame index.
/// - The binary name.
/// - The memory address.
/// - The symbol.
/// - The offset within the symbol.
///
/// Example format of _a single frame_:
/// ```
/// 2 EmbraceApp 0x0000000001234abc -[MyClass myMethod] + 48
/// ```
///
/// - Parameter frames: An array of frames strings, following the format of `Thread.callStackSymbols`.
/// - Throws: An `EmbraceStackTraceError.invalidFormat` if any of the frames are not in the expected format
/// - Throws: An `EmbraceStackTraceError.frameIsTooLong` if any of the frames has more than the `maximumFrameLength` (10.000 characters).
///
/// - Important: a stacktrace can't have more than `maximumAmountOfFrames` (200); if that happens, we'll trim the exceeding frames.
public init(frames: [String]) throws {
let trimmedStackTrace = EmbraceStackTrace.trimStackTrace(frames)
try EmbraceStackTrace.validateStackFrames(trimmedStackTrace)
self.frames = trimmedStackTrace
}

private static func validateStackFrames(_ frames: [String]) throws {
try frames.forEach {
if $0.count >= maximumFrameLength {
throw EmbraceStackTraceError.frameIsTooLong
}
if !isValidStackFrameFormat($0) {
throw EmbraceStackTraceError.invalidFormat
}
}
}

/// Generates a stack trace with up to `maximumAmountOfFrames` frames.
///
/// - Important: A stack trace can't have more than `maximumAmountOfFrames` (200);
/// if that happens, we'll trim the exceeding frames.
/// - Returns: An array of stack frames as `String`.
private static func trimStackTrace(_ stackFrames: [String]) -> [String] {
return Array(stackFrames.prefix(maximumAmountOfFrames))
}

/// Validates if a given frame string follows the required format.
/// - Parameter frame: a stack trace frame.
/// - Returns: whether the format is valid or invalid.
private static func isValidStackFrameFormat(_ frame: String) -> Bool {
/*
Regular expression pattern breakdown:

^\s* -> Allows optional leading spaces at the beginning
(\d+) -> Captures the frame index (a sequence of digits)
\s+ -> One or more whitespaces
([^\s]+(?:\s+[^\s]+)*) -> Captures the module name, allowing spaces between words but not at the edges
\s+ -> One or more whitespaces
(0x[0-9A-Fa-f]+) -> Captures the memory address hex (must start with `0x`)
\s+ -> One or more whitespaces
(\S.+?) -> Captures the function/method symbol ensuring it's not empty (non-greedy/lazy)
(?:\s+\+\s+(\d+))? -> Optionally captures the slide offset as it might not always be present (`+ <numbers>`)
*/
let pattern = #"^\s*(\d+)\s+([^\s]+(?:\s+[^\s]+)*)\s+(0x[0-9A-Fa-f]+)\s+(\S.+?)(?:\s+\+\s+(\d+))?$"#
return frame.range(of: pattern, options: .regularExpression) != nil
}
}

30 changes: 30 additions & 0 deletions Sources/EmbraceCommonInternal/Models/StackTraceBehavior.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Copyright © 2024 Embrace Mobile, Inc. All rights reserved.
//

import Foundation

/// Describes the behavior for automatically capturing stack traces.
public enum StackTraceBehavior {
/// Stack traces are not automatically captured.
case notIncluded

/// The default behavior for automatically capturing stack traces.
case `default`

/// A custom stack trace provided.
case custom(_ value: EmbraceStackTrace)
}

extension StackTraceBehavior: Equatable {
public static func == (lhs: StackTraceBehavior, rhs: StackTraceBehavior) -> Bool {
switch (lhs, rhs) {
case (.notIncluded, .notIncluded), (.default, .default):
return true
case let (.custom(lhsValue), .custom(rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}
}
15 changes: 12 additions & 3 deletions Sources/EmbraceCore/Internal/Logs/LogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,18 @@ class LogController: LogControllable {
If we want to keep this method cleaner, we could move this log to `EmbraceLogAttributesBuilder`
However that would cause to always add a frame to the stacktrace.
*/
if stackTraceBehavior == .default && (severity == .warn || severity == .error) {
let stackTrace: [String] = Thread.callStackSymbols
attributesBuilder.addStackTrace(stackTrace)
switch stackTraceBehavior {
case .default:
if severity == .warn || severity == .error {
let stackTrace: [String] = Thread.callStackSymbols
attributesBuilder.addStackTrace(stackTrace)
}
case .custom(let customStackTrace):
if severity == .warn || severity == .error {
attributesBuilder.addStackTrace(customStackTrace.frames)
}
case .notIncluded:
break
}

var finalAttributes = attributesBuilder
Expand Down
8 changes: 8 additions & 0 deletions Sources/EmbraceCore/Public/Embrace+OTel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ extension Embrace: EmbraceOpenTelemetry {
/// - severity: `LogSeverity` for the log.
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
///
/// - Important: `.info` logs will _never_ have stacktraces.
public func log(
_ message: String,
severity: LogSeverity,
Expand All @@ -143,6 +145,8 @@ extension Embrace: EmbraceOpenTelemetry {
/// - timestamp: Timestamp for the log.
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
///
/// - Important: `.info` logs will _never_ have stacktraces.
public func log(
_ message: String,
severity: LogSeverity,
Expand Down Expand Up @@ -173,6 +177,8 @@ extension Embrace: EmbraceOpenTelemetry {
/// - attachment: Data of the attachment
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
///
/// - Important: `.info` logs will _never_ have stacktraces.
public func log(
_ message: String,
severity: LogSeverity,
Expand Down Expand Up @@ -205,6 +211,8 @@ extension Embrace: EmbraceOpenTelemetry {
/// - attachmentUrl: URL to dowload the attachment data
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
///
/// - Important: `.info` logs will _never_ have stacktraces.
public func log(
_ message: String,
severity: LogSeverity,
Expand Down
Loading
Loading