Skip to content

Commit

Permalink
Add recordException (#599)
Browse files Browse the repository at this point in the history
* Add `SpanException` to be attached to spans
* Add `SpanExceptionRecorder` to describe how a span should behave when recording exceptions
* Conform `Span` to `SpanExceptionRecorder`
* Extend `Span` to also allow usage of `Error` for recording exceptions
  • Loading branch information
brunnoferreira authored Sep 19, 2024
1 parent b22dd41 commit 6d6cb71
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 65 deletions.
16 changes: 16 additions & 0 deletions Examples/Logging Tracer/LoggingSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ class LoggingSpan: Span {
Logger.log("Span.addEvent(\(name), attributes:\(attributes), timestamp:\(timestamp))")
}

public func recordException(_ exception: SpanException) {
Logger.log("Span.recordException(\(exception)")
}

public func recordException(_ exception: SpanException, timestamp: Date) {
Logger.log("Span.recordException(\(exception), timestamp:\(timestamp))")
}

public func recordException(_ exception: SpanException, attributes: [String : AttributeValue]) {
Logger.log("Span.recordException(\(exception), attributes:\(attributes)")
}

public func recordException(_ exception: SpanException, attributes: [String : AttributeValue], timestamp: Date) {
Logger.log("Span.recordException(\(exception), attributes:\(attributes), timestamp:\(timestamp))")
}

public func end() {
Logger.log("Span.End, Name: \(name)")
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/OpenTelemetryApi/Trace/PropagatedSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,12 @@ class PropagatedSpan: Span {
func addEvent(name: String, attributes: [String: AttributeValue]) {}

func addEvent(name: String, attributes: [String: AttributeValue], timestamp: Date) {}

func recordException(_ exception: SpanException) {}

func recordException(_ exception: any SpanException, timestamp: Date) {}

func recordException(_ exception: any SpanException, attributes: [String : AttributeValue]) {}

func recordException(_ exception: any SpanException, attributes: [String : AttributeValue], timestamp: Date) {}
}
71 changes: 63 additions & 8 deletions Sources/OpenTelemetryApi/Trace/Span.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,62 @@ public protocol SpanBase: AnyObject, CustomStringConvertible {
/// Use this method to specify an explicit event timestamp. If not called, the implementation
/// will use the current timestamp value, which should be the default case.
/// - Parameters:
/// - name: the name of the even
/// - timestamp: the explicit event timestamp in nanos since epoch
/// - name: the name of the event.
/// - timestamp: the explicit event timestamp in nanos since epoch.
func addEvent(name: String, timestamp: Date)

/// Adds a single Event with the attributes to the Span.
/// - Parameters:
/// - name: Event name.
/// - attributes: Dictionary of attributes name/value pairs associated with the Event
/// - name: the name of the event.
/// - attributes: Dictionary of attributes name/value pairs associated with the event.
func addEvent(name: String, attributes: [String: AttributeValue])

/// Adds an event to the Span
/// Use this method to specify an explicit event timestamp. If not called, the implementation
/// will use the current timestamp value, which should be the default case.
/// - Parameters:
/// - name: the name of the even
/// - attributes: Dictionary of attributes name/value pairs associated with the Event
/// - timestamp: the explicit event timestamp in nanos since epoch
/// - name: the name of the event.
/// - attributes: Dictionary of attributes name/value pairs associated with the event
/// - timestamp: the explicit event timestamp in nanos since epoch.
func addEvent(name: String, attributes: [String: AttributeValue], timestamp: Date)
}

public protocol SpanExceptionRecorder {
/// Adds an exception event to the Span.
/// - Parameters:
/// - exception: the exception to be recorded.
func recordException(_ exception: SpanException)

/// Adds an exception event to the Span.
/// Use this method to specify an explicit event timestamp. If not called, the implementation
/// will use the current timestamp value, which should be the default case.
/// - Parameters:
/// - exception: the exception to be recorded.
/// - timestamp: the explicit event timestamp in nanos since epoch.
func recordException(_ exception: SpanException, timestamp: Date)

/// Adds an exception event to the Span, with additional attributes to go alongside the
/// default attribuites derived from the exception itself.
/// - Parameters:
/// - exception: the exception to be recorded.
/// - attributes: Dictionary of attributes name/value pairs associated with the event.
func recordException(_ exception: SpanException, attributes: [String: AttributeValue])

/// Adds an exception event to the Span, with additional attributes to go alongside the
/// default attribuites derived from the exception itself.
/// Use this method to specify an explicit event timestamp. If not called, the implementation
/// will use the current timestamp value, which should be the default case.
/// - Parameters:
/// - exception: the exception to be recorded.
/// - attributes: Dictionary of attributes name/value pairs associated with the event.
/// - timestamp: the explicit event timestamp in nanos since epoch.
func recordException(_ exception: SpanException, attributes: [String: AttributeValue], timestamp: Date)
}

/// An interface that represents a span. It has an associated SpanContext.
/// Spans are created by the SpanBuilder.startSpan method.
/// Span must be ended by calling end().
public protocol Span: SpanBase {
public protocol Span: SpanBase, SpanExceptionRecorder {
/// End the span.
func end()

Expand Down Expand Up @@ -120,6 +152,29 @@ public extension SpanBase {
}
}

public extension SpanExceptionRecorder {
/// Adds any Error as an exception event to the Span, with optional additional attributes
/// and timestamp.
/// If additonal attributes are specified, they are merged with the default attributes
/// derived from the error itself.
/// If an explicit timestamp is not provided, the implementation will use the current
/// timestamp value, which should be the default case.
/// - Parameters:
/// - exception: the exception to be recorded.
/// - attributes: Dictionary of attributes name/value pairs associated with the event.
/// - timestamp: the explicit event timestamp in nanos since epoch.
func recordException(_ exception: Error, attributes: [String: AttributeValue]? = nil, timestamp: Date? = nil) {
let exception = exception as NSError

switch (attributes, timestamp) {
case (.none, .none): recordException(exception)
case (.some(let attributes), .none): recordException(exception, attributes: attributes)
case (.none, .some(let timestamp)): recordException(exception, timestamp: timestamp)
case (.some(let attributes), .some(let timestamp)): recordException(exception, attributes: attributes, timestamp: timestamp)
}
}
}

public extension Span {
/// Helper method that populates span properties from host and port
/// - Parameters:
Expand Down
43 changes: 43 additions & 0 deletions Sources/OpenTelemetryApi/Trace/SpanException.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation

/// An interface that represents an exception that can be attached to a span.
public protocol SpanException {
var type: String { get }
var message: String? { get }
var stackTrace: [String]? { get }
}

extension NSError: SpanException {
public var type: String {
String(reflecting: self)
}

public var message: String? {
localizedDescription
}

public var stackTrace: [String]? {
nil
}
}

#if !os(Linux)
extension NSException: SpanException {
public var type: String {
name.rawValue
}

public var message: String? {
reason
}

public var stackTrace: [String]? {
callStackSymbols
}
}
#endif
35 changes: 35 additions & 0 deletions Sources/OpenTelemetrySdk/Trace/RecordEventsReadableSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,39 @@ public class RecordEventsReadableSpan: ReadableSpan {
internal func getDroppedLinksCount() -> Int {
return totalRecordedLinks - links.count
}

public func recordException(_ exception: SpanException) {
recordException(exception, timestamp: clock.now)
}

public func recordException(_ exception: any SpanException, timestamp: Date) {
recordException(exception, attributes: [:], timestamp: timestamp)
}

public func recordException(_ exception: any SpanException, attributes: [String : AttributeValue]) {
recordException(exception, attributes: attributes, timestamp: clock.now)
}

public func recordException(_ exception: any SpanException, attributes: [String : AttributeValue], timestamp: Date) {
var limitedAttributes = AttributesDictionary(capacity: maxNumberOfAttributesPerEvent)
limitedAttributes.updateValues(attributes: attributes)
limitedAttributes.updateValues(attributes: exception.eventAttributes)
addEvent(event: SpanData.Event(name: SemanticAttributes.exception.rawValue, timestamp: timestamp, attributes: limitedAttributes.attributes))
}
}

extension SpanException {
fileprivate var eventAttributes: [String: AttributeValue] {
[
SemanticAttributes.exceptionType.rawValue: type,
SemanticAttributes.exceptionMessage.rawValue: message,
SemanticAttributes.exceptionStacktrace.rawValue: stackTrace?.joined(separator: "\n")
].compactMapValues { value in
if let value, !value.isEmpty {
return .string(value)
}

return nil
}
}
}
8 changes: 8 additions & 0 deletions Tests/OpenTelemetryApiTests/Trace/PropagatedSpanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ final class PropagatedSpanTest: XCTestCase {
span.addEvent(name: "event", timestamp: Date(timeIntervalSinceReferenceDate: 0))
span.addEvent(name: "event", attributes: ["MyBooleanAttributeKey": AttributeValue.bool(true)])
span.addEvent(name: "event", attributes: ["MyBooleanAttributeKey": AttributeValue.bool(true)], timestamp: Date(timeIntervalSinceReferenceDate: 1.5))
span.recordException(NSError(domain: "test", code: 0), timestamp: Date(timeIntervalSinceReferenceDate: 3))
span.recordException(NSError(domain: "test", code: 0), attributes: ["MyBooleanAttributeKey": AttributeValue.bool(true)], timestamp: Date(timeIntervalSinceReferenceDate: 4.5))

#if !os(Linux)
span.recordException(NSException(name: .genericException, reason: nil))
span.recordException(NSException(name: .genericException, reason: nil), attributes: ["MyStringAttributeKey": AttributeValue.string("MyStringAttributeValue")])
#endif

span.status = .ok
span.end()
span.end(time: Date())
Expand Down
82 changes: 82 additions & 0 deletions Tests/OpenTelemetryApiTests/Trace/SpanExceptionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation
import XCTest
import OpenTelemetryApi

final class SpanExceptionTests: XCTestCase {
func testErrorAsSpanException() {
enum TestError: Error {
case test(code: Int)
}

let error = TestError.test(code: 5)

// `Error` can be converted to `NSError`, which automatically makes the cast to
// `SpanException` possible since `NSError` conforms to `SpanException`.
let exception = error as SpanException

XCTAssertEqual(exception.type, String(reflecting: error))
XCTAssertEqual(exception.message, error.localizedDescription)
XCTAssertNil(exception.stackTrace)
}

func testCustomNSErrorAsSpanException() throws {
struct TestCustomNSError: Error, CustomNSError {
let additionalComments: String

var errorUserInfo: [String : Any] {
[NSLocalizedDescriptionKey: "This is a custom NSError: \(additionalComments)"]
}
}

let error = TestCustomNSError(additionalComments: "SpanExceptionTests")

// `Error` can be converted to `NSError`, which automatically makes the cast to
// `SpanException` possible since `NSError` conforms to `SpanException`.
let exception = error as SpanException

XCTAssertEqual(exception.type, String(reflecting: error))
XCTAssertEqual(exception.message, error.localizedDescription)
XCTAssertNil(exception.stackTrace)

// `TestCustomNSError` conforms to `CustomNSError`, so the conversion to `NSError` when casting to
// `SpanException` should utilize that protocol and result in a custom `localizedDescription`.
let localizedDescription = try XCTUnwrap(error.errorUserInfo[NSLocalizedDescriptionKey] as? String)
XCTAssertEqual(exception.message, localizedDescription)
}

func testNSError() {
let nsError = NSError(domain: "Test Domain", code: 1)
let exception = nsError as SpanException

XCTAssertEqual(exception.type, String(reflecting: nsError))
XCTAssertEqual(exception.message, nsError.localizedDescription)
XCTAssertNil(exception.stackTrace)
}

#if !os(Linux)
func testNSException() {
final class TestException: NSException {
override var callStackSymbols: [String] {
[
"test-stack-entry-1",
"test-stack-entry-2",
"test-stack-entry-3"
]
}
}

let exceptionReason = "This is a test exception"
let nsException = TestException(name: .genericException, reason: exceptionReason)
let exception = nsException as SpanException

XCTAssertEqual(exception.type, nsException.name.rawValue)
XCTAssertEqual(exception.message, nsException.reason)
XCTAssertEqual(exception.stackTrace, nsException.callStackSymbols)
}
#endif
}
Loading

0 comments on commit 6d6cb71

Please sign in to comment.