Skip to content

Commit 987d0b4

Browse files
author
Rafal Augustyniak
committed
add record error with message method
1 parent cc420c3 commit 987d0b4

File tree

2 files changed

+63
-25
lines changed

2 files changed

+63
-25
lines changed

Sources/NautilusTelemetry/Tracing/Span.swift

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public final class Span: Identifiable {
120120
status = .ok
121121
}
122122

123-
/// Record an error into this span
123+
/// Record an error into the span.
124124
/// - Parameters:
125125
/// - error: any error object -- NSErrors have special handling to capture domain and code.
126126
/// - includeBacktrace: whether to include a backtrace. This defaults to false, and is costly at runtime.
@@ -134,6 +134,23 @@ public final class Span: Identifiable {
134134
status = .error(message: message) // this duplicates exception.message, but makes the reporting work better
135135
}
136136

137+
/// Record an error with a given message into the span.
138+
/// - Parameters:
139+
/// - type: a type of an error.
140+
/// - message: an error message to record.
141+
/// - includeBacktrace: whether to include a backtrace. This defaults to false, and is costly at runtime.
142+
public func recordError(withType type: String, message: String, includeBacktrace: Bool = false) {
143+
let attributes = Self.exceptionAttributes(
144+
type: type,
145+
message: message,
146+
stacktrace: includeBacktrace ? Self.captureStacktrace() : nil
147+
)
148+
149+
let exceptionEvent = Event(name: "exception", attributes: attributes)
150+
addEvent(exceptionEvent)
151+
status = .error(message: message)
152+
}
153+
137154
// MARK: Internal
138155

139156
let name: String
@@ -154,43 +171,53 @@ public final class Span: Identifiable {
154171
}
155172

156173
static func exceptionAttributes(_ error: any Error, includeBacktrace: Bool) -> [String: String] {
157-
var attributes: [String: String]
174+
let exceptionType: String
175+
let exceptionMessage: String
158176

159177
// All swift errors bridge to NSError, so instead check the type explicitly
160178
if type(of: error) is NSError.Type {
161179
// a "real" NSError
162180
let nsError = error as NSError
163-
let message = (error as NSError).localizedDescription
164-
attributes = [
165-
// OpenTelemetry doesn't have the concept of error codes. Pack it in exception.type.
166-
"exception.type": "NSError.\(nsError.domain).\(nsError.code)",
167-
"exception.message": message,
168-
]
181+
// OpenTelemetry doesn't have the concept of error codes. Pack it in exception.type.
182+
exceptionType = "NSError.\(nsError.domain).\(nsError.code)"
183+
exceptionMessage = (error as NSError).localizedDescription
169184
} else {
170-
let message = String(describing: error)
171-
attributes = [
172-
"exception.type": String(reflecting: type(of: error)),
173-
"exception.message": message,
174-
]
175-
}
176-
177-
if includeBacktrace {
178-
// TBD: figure out proper backtracing and Swift symbol demangling?
179-
// This doesn't seem to exist yet: https://forums.swift.org/t/demangle-function/25416
180-
// This looks OK, but is ≈9K lines: https://github.com/oozoofrog/SwiftDemangle
181-
// Will try this, once it lands as public API:
182-
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0419-backtrace-api.md
183-
let callStackLimit = 20
184-
let callstackSymbols = Thread.callStackSymbols.prefix(callStackLimit)
185-
let callstack = callstackSymbols.joined(separator: "\n")
186-
attributes["exception.stacktrace"] = callstack
185+
exceptionType = String(reflecting: type(of: error))
186+
exceptionMessage = String(describing: error)
187187
}
188188

189189
// nsError.underlyingErrors contains lower-level info for network errors and may be interesting here
190190

191+
return exceptionAttributes(
192+
type: exceptionType,
193+
message: exceptionMessage,
194+
stacktrace: includeBacktrace ? captureStacktrace() : nil
195+
)
196+
}
197+
198+
private static func exceptionAttributes(type: String, message: String, stacktrace: String?) -> [String: String] {
199+
var attributes = [
200+
"exception.type": type,
201+
"exception.message": message
202+
]
203+
if let stacktrace {
204+
attributes["exception.stacktrace"] = stacktrace
205+
}
206+
191207
return attributes
192208
}
193209

210+
private static func captureStacktrace() -> String {
211+
// TBD: figure out proper backtracing and Swift symbol demangling?
212+
// This doesn't seem to exist yet: https://forums.swift.org/t/demangle-function/25416
213+
// This looks OK, but is ≈9K lines: https://github.com/oozoofrog/SwiftDemangle
214+
// Will try this, once it lands as public API:
215+
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0419-backtrace-api.md
216+
let callStackLimit = 20
217+
let callstackSymbols = Thread.callStackSymbols.prefix(callStackLimit)
218+
return callstackSymbols.joined(separator: "\n")
219+
}
220+
194221
/// Records a result. This convenience method records either a success or an error,
195222
/// depending on the given result.
196223
/// - Parameters:

Tests/NautilusTelemetryTests/SpanTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,15 @@ final class SpanTests: XCTestCase {
250250
XCTAssertEqual(exceptionAttributes["exception.message"], "failure")
251251
}
252252

253+
func test_recordErrorWithMessage() throws {
254+
let span = tracer.startSpan(name: "errorSpan")
255+
span.recordError(withType: "custom", message: "custom error message")
256+
257+
let exceptionEvent = try XCTUnwrap(span.events?.first)
258+
let exceptionAttributes = try XCTUnwrap(exceptionEvent.attributes)
259+
XCTAssertEqual(span.status, .error(message: "custom error message"))
260+
XCTAssertEqual(exceptionAttributes["exception.type"], "custom")
261+
XCTAssertEqual(exceptionAttributes["exception.message"], "custom error message")
262+
}
263+
253264
}

0 commit comments

Comments
 (0)