Skip to content

Commit cf2bdd0

Browse files
authored
Add nil filtering & improve macOS support (#43)
* Nil filtering * macOS support
1 parent ac92dfd commit cf2bdd0

6 files changed

Lines changed: 76 additions & 8 deletions

File tree

Sources/NautilusTelemetry/Exporters/Exporter.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import Foundation
99

10+
// MARK: - Exporter
11+
1012
/// Provides conversions to OTLP-JSON format
1113
public struct Exporter {
1214

@@ -106,9 +108,10 @@ public struct Exporter {
106108
return nil
107109
}
108110

111+
let filteredAttributes = attributes.filteringNilValues()
109112
var otlpAttributes = [OTLP.V1KeyValue]()
110113

111-
let keys = attributes.keys.sorted()
114+
let keys = filteredAttributes.keys.sorted()
112115
for key in keys {
113116
if let value = attributes[key] {
114117
if let v1AnyValue = convertToOTLP(value: value) {
@@ -124,3 +127,18 @@ public struct Exporter {
124127
}
125128

126129
}
130+
131+
extension Dictionary where Value == AnyHashable {
132+
133+
/// Filters Optional values boxed as AnyHashable
134+
/// - Returns: a filtered dictionary, with optional values removed
135+
func filteringNilValues() -> [Key: Value] {
136+
compactMapValues { value -> AnyHashable? in
137+
// Check if the AnyHashable wraps an Optional that is nil
138+
if case Optional<Any>.none = value.base {
139+
return nil
140+
}
141+
return value
142+
}
143+
}
144+
}

Sources/NautilusTelemetry/Instrumentation/HardwareDetails.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ public enum HardwareDetails {
2525
// MARK: Private
2626

2727
private static var isOnMac: Bool = {
28-
// This doesn't change after launch, so evaluate once.
28+
#if os(macOS)
29+
true
30+
#else
31+
/// This doesn't change after launch, so evaluate once.
2932
var isOnMac = false
3033

3134
#if targetEnvironment(simulator)
@@ -43,8 +46,8 @@ public enum HardwareDetails {
4346
}
4447
}
4548
#endif
46-
4749
return isOnMac
50+
#endif
4851
}()
4952

5053
private static func sysctlbyname(_ name: String) -> String {

Sources/NautilusTelemetry/Instrumentation/ResourceAttributes.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public struct ResourceAttributes {
2424
vendorIdentifier: String,
2525
deviceModelIdentifier: String,
2626
osType: String = "darwin",
27-
osName: String = "iOS",
27+
osName: String = defaultOSName,
2828
osVersion: String,
2929
additionalAttributes: TelemetryAttributes?
3030
) {
@@ -40,6 +40,14 @@ public struct ResourceAttributes {
4040

4141
// MARK: Public
4242

43+
#if os(macOS)
44+
public static let defaultOSName = "macOS"
45+
#elseif os(iOS)
46+
public static let defaultOSName = "iOS"
47+
#else
48+
public static let defaultOSName = "unknown"
49+
#endif
50+
4351
/// Create a default set of resource attributes.
4452
/// - Parameter additionalAttributes: Additional attributes, that may override existing attributes. Must conform to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/attribute-naming.md
4553
/// - Returns: Built attributes.
@@ -96,7 +104,7 @@ public struct ResourceAttributes {
96104
var attributes = TelemetryAttributes()
97105

98106
// https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/
99-
attributes["service.name"] = "ios.app"
107+
attributes["service.name"] = "\(osName.lowercased()).app"
100108
attributes["service.namespace"] = bundleIdentifier
101109
attributes["service.version"] = applicationVersion
102110
attributes["telemetry.sdk.name"] = "NautilusTelemetry"
@@ -108,7 +116,7 @@ public struct ResourceAttributes {
108116
attributes["device.manufacturer"] = "Apple"
109117
attributes["device.model"] = deviceModelIdentifier
110118

111-
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/os.md
119+
// https://opentelemetry.io/docs/specs/semconv/resource/os/
112120
attributes["os.type"] = osType
113121
attributes["os.name"] = osName
114122
attributes["os.version"] = osVersion

Sources/NautilusTelemetry/Tracing/Span.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ public final class Span: Identifiable {
117117
/// - name: a name, conforming to https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions
118118
/// - value: a value.
119119
public func addAttribute(_ name: String, _ value: AnyHashable?) {
120+
guard let value else { return }
121+
120122
// AnyHashable is not Sendable. For now, make this unchecked, but could consider wrapping ala:
121123
// https://github.com/pointfreeco/swift-concurrency-extras/blob/main/Sources/ConcurrencyExtras/AnyHashableSendable.swift
122124
lock.withLockUnchecked {

Tests/NautilusTelemetryTests/Exporters/ExporterTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,32 @@ final class ExporterTests: XCTestCase {
202202
let convertedEmptyBounds = exporter.convertToOTLP(explicitBounds: emptyBounds)
203203
XCTAssertEqual(convertedEmptyBounds, [])
204204
}
205+
206+
func testFilteringNilValues() throws {
207+
// Test that nil values wrapped in AnyHashable are properly filtered out
208+
let nilValue: String? = nil
209+
let validString = "hello"
210+
let validInt = 42
211+
let validBool = true
212+
213+
let dictionary: [String: AnyHashable] = [
214+
"nilKey": AnyHashable(nilValue),
215+
"stringKey": AnyHashable(validString),
216+
"intKey": AnyHashable(validInt),
217+
"boolKey": AnyHashable(validBool),
218+
]
219+
220+
let filtered = dictionary.filteringNilValues()
221+
222+
// Verify that the nil value was removed
223+
XCTAssertNil(filtered["nilKey"])
224+
225+
// Verify that valid values remain
226+
XCTAssertEqual(filtered["stringKey"] as? String, validString)
227+
XCTAssertEqual(filtered["intKey"] as? Int, validInt)
228+
XCTAssertEqual(filtered["boolKey"] as? Bool, validBool)
229+
230+
// Verify the count
231+
XCTAssertEqual(filtered.count, 3)
232+
}
205233
}

Tests/NautilusTelemetryTests/Utilities/ResourceAttributesTests.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@ final class ResourceAttributesTests: XCTestCase {
1919

2020
_ = try XCTUnwrap(exporter.convertToOTLP(attributes: attributes)) // make sure it converts
2121

22-
_ = try XCTUnwrap(attributes["service.name"])
22+
let serviceName = try XCTUnwrap(attributes["service.name"] as? String)
2323
_ = try XCTUnwrap(attributes["service.version"])
2424
_ = try XCTUnwrap(attributes["telemetry.sdk.name"])
2525
_ = try XCTUnwrap(attributes["telemetry.sdk.language"])
2626
_ = try XCTUnwrap(attributes["device.id"])
2727
_ = try XCTUnwrap(attributes["foo"])
2828
_ = try XCTUnwrap(attributes["os.type"])
29-
_ = try XCTUnwrap(attributes["os.name"])
29+
let osName = try XCTUnwrap(attributes["os.name"] as? String)
3030
let osVersion = try XCTUnwrap(attributes["os.version"] as? String)
3131

32+
// Verify platform-specific OS name
33+
#if os(macOS)
34+
XCTAssertEqual(osName, "macOS")
35+
XCTAssertEqual(serviceName, "macos.app")
36+
#elseif os(iOS)
37+
XCTAssertEqual(osName, "iOS")
38+
XCTAssertEqual(serviceName, "ios.app")
39+
#endif
40+
3241
let components = osVersion.split(separator: ".")
3342
XCTAssert(components.count >= 2)
3443

0 commit comments

Comments
 (0)