Skip to content
Merged
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
39 changes: 12 additions & 27 deletions Sources/NautilusTelemetry/Exporters/Exporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,37 +108,22 @@ public struct Exporter {
return nil
}

let filteredAttributes = attributes.filteringNilValues()
var otlpAttributes = [OTLP.V1KeyValue]()

let keys = filteredAttributes.keys.sorted()
for key in keys {
if let value = attributes[key] {
if let v1AnyValue = convertToOTLP(value: value) {
let keyValue = OTLP.V1KeyValue(key: key, value: v1AnyValue)
otlpAttributes.append(keyValue)
} else {
assert(false, "failed to convert \(key), \(value)")
}
let otlpAttributes = attributes.compactMap { (key, value) -> OTLP.V1KeyValue? in
// Skip nil values wrapped in AnyHashable
if case Optional<Any>.none = value.base {
return nil
}
}

return otlpAttributes
}

}

extension Dictionary where Value == AnyHashable {

/// Filters Optional values boxed as AnyHashable
/// - Returns: a filtered dictionary, with optional values removed
func filteringNilValues() -> [Key: Value] {
compactMapValues { value -> AnyHashable? in
// Check if the AnyHashable wraps an Optional that is nil
if case Optional<Any>.none = value.base {
guard let v1AnyValue = convertToOTLP(value: value) else {
assert(false, "failed to convert \(key), \(value)")
return nil
}
return value

return OTLP.V1KeyValue(key: key, value: v1AnyValue)
}

// Sort by key for deterministic output
return otlpAttributes.sorted { ($0.key ?? "") < ($1.key ?? "") }
}

}
51 changes: 30 additions & 21 deletions Tests/NautilusTelemetryTests/Exporters/ExporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,31 +203,40 @@ final class ExporterTests: XCTestCase {
XCTAssertEqual(convertedEmptyBounds, [])
}

func testFilteringNilValues() throws {
// Test that nil values wrapped in AnyHashable are properly filtered out
let nilValue: String? = nil
let validString = "hello"
let validInt = 42
let validBool = true

let dictionary: [String: AnyHashable] = [
"nilKey": AnyHashable(nilValue),
"stringKey": AnyHashable(validString),
"intKey": AnyHashable(validInt),
"boolKey": AnyHashable(validBool),
func testConvertToOTLPAttributes() throws {
let exporter = Exporter(timeReference: timeReference, prettyPrint: false)

// Test nil attributes returns nil
XCTAssertNil(exporter.convertToOTLP(attributes: nil))

// Test sorting by key
let attributes: TelemetryAttributes = [
"zebra": "last",
"apple": "first",
"middle": 123,
]

let filtered = dictionary.filteringNilValues()
let result = try XCTUnwrap(exporter.convertToOTLP(attributes: attributes))

// Verify that the nil value was removed
XCTAssertNil(filtered["nilKey"])
// Verify sorting
XCTAssertEqual(result.count, 3)
XCTAssertEqual(result[0].key, "apple")
XCTAssertEqual(result[0].value?.stringValue, "first")
XCTAssertEqual(result[1].key, "middle")
XCTAssertEqual(result[1].value?.intValue as? Int, 123)
XCTAssertEqual(result[2].key, "zebra")
XCTAssertEqual(result[2].value?.stringValue, "last")

// Verify that valid values remain
XCTAssertEqual(filtered["stringKey"] as? String, validString)
XCTAssertEqual(filtered["intKey"] as? Int, validInt)
XCTAssertEqual(filtered["boolKey"] as? Bool, validBool)
// Test nil filtering
let nilValue: String? = nil
let attributesWithNil: TelemetryAttributes = [
"valid": "value",
"invalid": AnyHashable(nilValue),
]

// Verify the count
XCTAssertEqual(filtered.count, 3)
let filteredResult = try XCTUnwrap(exporter.convertToOTLP(attributes: attributesWithNil))
XCTAssertEqual(filteredResult.count, 1)
XCTAssertEqual(filteredResult[0].key, "valid")
XCTAssertEqual(filteredResult[0].value?.stringValue, "value")
}
}