Skip to content

Commit 6ee4973

Browse files
authored
Structured Logs: Add os/device attributes to logs (#5639)
1 parent 99104c9 commit 6ee4973

File tree

10 files changed

+163
-14
lines changed

10 files changed

+163
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add experimental support for capturing structured logs via `SentrySDK.logger` (#5532, #5593, #5639)
78
- Add experimental support for capturing structured logs via `SentrySDK.logger` (#5532, #5593)
89

910
### Improvements

Sentry.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@
794794
92235CB02E155B2600865983 /* SentryLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */; };
795795
925824C22CB5897700C9B20B /* SentrySessionReplayIntegration-Hybrid.h in Headers */ = {isa = PBXBuildFile; fileRef = D80382BE2C09C6FD0090E048 /* SentrySessionReplayIntegration-Hybrid.h */; settings = {ATTRIBUTES = (Private, ); }; };
796796
92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; };
797+
926C89732E26A30C006F3154 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 926C89722E26A30C006F3154 /* SentryScope+PrivateSwift.h */; };
797798
927A5CC42DD7626B00B82404 /* SentryEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */; };
798799
928207C42E251B8F009285A4 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */; };
799800
9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -2060,6 +2061,7 @@
20602061
92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = "<group>"; };
20612062
92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = "<group>"; };
20622063
92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = "<group>"; };
2064+
926C89722E26A30C006F3154 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = "<group>"; };
20632065
927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = "<group>"; };
20642066
928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = "<group>"; };
20652067
9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = "<group>"; };
@@ -4786,6 +4788,7 @@
47864788
63FE713F20DA4C1100CDBAE8 /* SentryCrashStackCursor_SelfThread.h in Headers */,
47874789
639FCFA41EBC809A00778193 /* SentryStacktrace.h in Headers */,
47884790
620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */,
4791+
926C89732E26A30C006F3154 /* SentryScope+PrivateSwift.h in Headers */,
47894792
63FE716320DA4C1100CDBAE8 /* SentryCrashDynamicLinker.h in Headers */,
47904793
D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */,
47914794
639FCF981EBC7B9700778193 /* SentryEvent.h in Headers */,

Sources/Sentry/Profiling/SentryProfilerSerialization.m

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
# import "SentrySDK+Private.h"
2828
# import "SentrySample.h"
2929
# import "SentryScope+Private.h"
30+
# import "SentryScope+PrivateSwift.h"
3031
# import "SentrySerialization.h"
3132
# import "SentrySwift.h"
3233
# import "SentryTime.h"
@@ -157,14 +158,14 @@
157158
payload[@"debug_meta"] = @ { @"images" : debugImages };
158159
}
159160

160-
payload[@"os"] = @ {
161+
payload[SENTRY_CONTEXT_OS_KEY] = @ {
161162
@"name" : sentry_getOSName(),
162163
@"version" : sentry_getOSVersion(),
163164
@"build_number" : sentry_getOSBuildNumber()
164165
};
165166

166167
bool isEmulated = sentry_isSimulatorBuild();
167-
payload[@"device"] = @{
168+
payload[SENTRY_CONTEXT_DEVICE_KEY] = @{
168169
@"architecture" : sentry_getCPUArchitecture(),
169170
@"is_emulator" : @(isEmulated),
170171
@"locale" : NSLocale.currentLocale.localeIdentifier,

Sources/Sentry/SentryClient.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#import "SentryRandom.h"
3333
#import "SentrySDK+Private.h"
3434
#import "SentryScope+Private.h"
35+
#import "SentryScope+PrivateSwift.h"
3536
#import "SentrySdkInfo.h"
3637
#import "SentrySerialization.h"
3738
#import "SentrySession.h"
@@ -1016,9 +1017,9 @@ - (void)applyExtraDeviceContextToEvent:(SentryEvent *)event
10161017
NSDictionary *extraContext =
10171018
[SentryDependencyContainer.sharedInstance.extraContextProvider getExtraContext];
10181019
[self modifyContext:event
1019-
key:@"device"
1020+
key:SENTRY_CONTEXT_DEVICE_KEY
10201021
block:^(NSMutableDictionary *device) {
1021-
[device addEntriesFromDictionary:extraContext[@"device"]];
1022+
[device addEntriesFromDictionary:extraContext[SENTRY_CONTEXT_DEVICE_KEY]];
10221023
}];
10231024

10241025
[self modifyContext:event
@@ -1054,7 +1055,7 @@ - (void)applyCurrentViewNamesToEventContext:(SentryEvent *)event withScope:(Sent
10541055
- (void)removeExtraDeviceContextFromEvent:(SentryEvent *)event
10551056
{
10561057
[self modifyContext:event
1057-
key:@"device"
1058+
key:SENTRY_CONTEXT_DEVICE_KEY
10581059
block:^(NSMutableDictionary *device) {
10591060
[device removeObjectForKey:SentryDeviceContextFreeMemoryKey];
10601061
[device removeObjectForKey:@"orientation"];

Sources/Sentry/SentryCrashIntegration.m

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import "SentryOptions.h"
1414
#import "SentrySDK+Private.h"
1515
#import "SentryScope+Private.h"
16+
#import "SentryScope+PrivateSwift.h"
1617
#import "SentrySpan+Private.h"
1718
#import "SentrySwift.h"
1819
#import "SentryTracer.h"
@@ -36,7 +37,6 @@
3637
static dispatch_once_t installationToken = 0;
3738
static SentryCrashInstallationReporter *installation = nil;
3839

39-
static NSString *const DEVICE_KEY = @"device";
4040
static NSString *const LOCALE_KEY = @"locale";
4141

4242
void
@@ -259,17 +259,18 @@ - (void)currentLocaleDidChange
259259
{
260260
[SentrySDK.currentHub configureScope:^(SentryScope *_Nonnull scope) {
261261
NSMutableDictionary<NSString *, id> *device;
262-
if (scope.contextDictionary != nil && scope.contextDictionary[DEVICE_KEY] != nil) {
262+
if (scope.contextDictionary != nil
263+
&& scope.contextDictionary[SENTRY_CONTEXT_DEVICE_KEY] != nil) {
263264
device = [[NSMutableDictionary alloc]
264-
initWithDictionary:scope.contextDictionary[DEVICE_KEY]];
265+
initWithDictionary:scope.contextDictionary[SENTRY_CONTEXT_DEVICE_KEY]];
265266
} else {
266267
device = [NSMutableDictionary new];
267268
}
268269

269270
NSString *locale = [[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleIdentifier];
270271
device[LOCALE_KEY] = locale;
271272

272-
[scope setContextValue:device forKey:DEVICE_KEY];
273+
[scope setContextValue:device forKey:SENTRY_CONTEXT_DEVICE_KEY];
273274
}];
274275
}
275276

Sources/Sentry/SentryCrashWrapper.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#import "SentryCrashIntegration.h"
55
#import "SentryCrashMonitor_AppState.h"
66
#import "SentryCrashMonitor_System.h"
7+
#import "SentryScope+PrivateSwift.h"
78
#import "SentryScope.h"
89
#import "SentryUIDeviceWrapper.h"
910
#import <SentryCrashCachedData.h>
@@ -17,7 +18,6 @@
1718
# import <UIKit/UIKit.h>
1819
#endif
1920

20-
static NSString *const DEVICE_KEY = @"device";
2121
static NSString *const LOCALE_KEY = @"locale";
2222

2323
NS_ASSUME_NONNULL_BEGIN
@@ -145,7 +145,7 @@ - (void)enrichScope:(SentryScope *)scope
145145
[osData setValue:systemInfo[@"isJailbroken"] forKey:@"rooted"];
146146
}
147147

148-
[scope setContextValue:osData forKey:@"os"];
148+
[scope setContextValue:osData forKey:SENTRY_CONTEXT_OS_KEY];
149149

150150
// SystemInfo should only be nil when SentryCrash has not been installed
151151
if (systemInfo == nil || systemInfo.count == 0) {
@@ -195,7 +195,7 @@ - (void)enrichScope:(SentryScope *)scope
195195

196196
#endif
197197

198-
[scope setContextValue:deviceData forKey:DEVICE_KEY];
198+
[scope setContextValue:deviceData forKey:SENTRY_CONTEXT_DEVICE_KEY];
199199

200200
// APP
201201
NSMutableDictionary *appData = [NSMutableDictionary new];

Sources/Sentry/SentryScope.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ - (void)setContextValue:(NSDictionary<NSString *, id> *)value forKey:(NSString *
233233
}
234234
}
235235

236+
- (nullable NSDictionary<NSString *, id> *)getContextForKey:(NSString *)key
237+
{
238+
@synchronized(_contextDictionary) {
239+
return [_contextDictionary objectForKey:key];
240+
}
241+
}
242+
236243
- (void)removeContextForKey:(NSString *)key
237244
{
238245
@synchronized(_contextDictionary) {

Sources/Sentry/include/SentryScope+PrivateSwift.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
NS_ASSUME_NONNULL_BEGIN
44

5+
static NSString *const SENTRY_CONTEXT_OS_KEY = @"os";
6+
static NSString *const SENTRY_CONTEXT_DEVICE_KEY = @"device";
7+
58
// Added to only expose a limited sub-set of internal API needed in the Swift layer.
69
@interface SentryScope ()
710

811
// This is a workaround to make the traceId available in the Swift layer.
912
// Can't expose the SentryId directly for some reason.
1013
@property (nonatomic, readonly) NSString *propagationContextTraceIdString;
1114

15+
- (NSDictionary<NSString *, id> *_Nullable)getContextForKey:(NSString *)key;
16+
1217
@end
1318

1419
NS_ASSUME_NONNULL_END

Sources/Swift/Tools/SentryLogger.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ public final class SentryLogger: NSObject {
106106

107107
var logAttributes = attributes.mapValues { SentryLog.Attribute(value: $0) }
108108
addDefaultAttributes(to: &logAttributes)
109+
addOSAttributes(to: &logAttributes)
110+
addDeviceAttributes(to: &logAttributes)
109111

110112
let propagationContextTraceIdString = hub.scope.propagationContextTraceIdString
111113
let propagationContextTraceId = SentryId(uuidString: propagationContextTraceIdString)
@@ -135,4 +137,31 @@ public final class SentryLogger: NSObject {
135137
attributes["sentry.trace.parent_span_id"] = .string(span.spanId.sentrySpanIdString)
136138
}
137139
}
140+
141+
private func addOSAttributes(to attributes: inout [String: SentryLog.Attribute]) {
142+
guard let osContext = hub.scope.getContextForKey(SENTRY_CONTEXT_OS_KEY) else {
143+
return
144+
}
145+
if let osName = osContext["name"] as? String {
146+
attributes["os.name"] = .string(osName)
147+
}
148+
if let osVersion = osContext["version"] as? String {
149+
attributes["os.version"] = .string(osVersion)
150+
}
151+
}
152+
153+
private func addDeviceAttributes(to attributes: inout [String: SentryLog.Attribute]) {
154+
guard let deviceContext = hub.scope.getContextForKey(SENTRY_CONTEXT_DEVICE_KEY) else {
155+
return
156+
}
157+
// For Apple devices, brand is always "Apple"
158+
attributes["device.brand"] = .string("Apple")
159+
160+
if let deviceModel = deviceContext["model"] as? String {
161+
attributes["device.model"] = .string(deviceModel)
162+
}
163+
if let deviceFamily = deviceContext["family"] as? String {
164+
attributes["device.family"] = .string(deviceFamily)
165+
}
166+
}
138167
}

Tests/SentryTests/SentryLoggerTests.swift

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@_spi(Private) import SentryTestUtils
33
import XCTest
44

5+
// swiftlint:disable cyclomatic_complexity
6+
57
final class SentryLoggerTests: XCTestCase {
68

79
private class Fixture {
@@ -324,6 +326,83 @@ final class SentryLoggerTests: XCTestCase {
324326
XCTAssertEqual(capturedLog.traceId, expectedTraceId)
325327
}
326328

329+
func testCaptureLog_AddsOSAndDeviceAttributes() {
330+
// Set up OS context
331+
let osContext = [
332+
"name": "iOS",
333+
"version": "16.0.1"
334+
]
335+
336+
// Set up device context
337+
let deviceContext = [
338+
"family": "iOS",
339+
"model": "iPhone14,4"
340+
]
341+
342+
// Set up scope context
343+
fixture.hub.scope.setContext(value: osContext, key: "os")
344+
fixture.hub.scope.setContext(value: deviceContext, key: "device")
345+
346+
sut.info("Test log message")
347+
348+
let capturedLog = getLastCapturedLog()
349+
350+
// Verify OS attributes
351+
XCTAssertEqual(capturedLog.attributes["os.name"]?.value as? String, "iOS")
352+
XCTAssertEqual(capturedLog.attributes["os.version"]?.value as? String, "16.0.1")
353+
354+
// Verify device attributes
355+
XCTAssertEqual(capturedLog.attributes["device.brand"]?.value as? String, "Apple")
356+
XCTAssertEqual(capturedLog.attributes["device.model"]?.value as? String, "iPhone14,4")
357+
XCTAssertEqual(capturedLog.attributes["device.family"]?.value as? String, "iOS")
358+
}
359+
360+
func testCaptureLog_HandlesPartialOSAndDeviceAttributes() {
361+
// Set up partial OS context (missing version)
362+
let osContext = [
363+
"name": "macOS"
364+
]
365+
366+
// Set up partial device context (missing model)
367+
let deviceContext = [
368+
"family": "macOS"
369+
]
370+
371+
// Set up scope context
372+
fixture.hub.scope.setContext(value: osContext, key: "os")
373+
fixture.hub.scope.setContext(value: deviceContext, key: "device")
374+
375+
sut.info("Test log message")
376+
377+
let capturedLog = getLastCapturedLog()
378+
379+
// Verify only available OS attributes are added
380+
XCTAssertEqual(capturedLog.attributes["os.name"]?.value as? String, "macOS")
381+
XCTAssertNil(capturedLog.attributes["os.version"])
382+
383+
// Verify only available device attributes are added
384+
XCTAssertEqual(capturedLog.attributes["device.brand"]?.value as? String, "Apple")
385+
XCTAssertNil(capturedLog.attributes["device.model"])
386+
XCTAssertEqual(capturedLog.attributes["device.family"]?.value as? String, "macOS")
387+
}
388+
389+
func testCaptureLog_HandlesMissingOSAndDeviceContext() {
390+
// Clear any OS and device context that might be automatically populated
391+
fixture.hub.scope.removeContext(key: "os")
392+
fixture.hub.scope.removeContext(key: "device")
393+
394+
sut.info("Test log message")
395+
396+
let capturedLog = getLastCapturedLog()
397+
398+
// Verify no OS or device attributes are added when context is missing
399+
XCTAssertNil(capturedLog.attributes["os.name"])
400+
XCTAssertNil(capturedLog.attributes["os.version"])
401+
XCTAssertNil(capturedLog.attributes["device.brand"])
402+
XCTAssertNil(capturedLog.attributes["device.model"])
403+
XCTAssertNil(capturedLog.attributes["device.family"])
404+
}
405+
327406
// MARK: - Helper Methods
328407

329408
private func assertLogCaptured(
@@ -345,9 +424,31 @@ final class SentryLoggerTests: XCTestCase {
345424
XCTAssertEqual(capturedLog.body, expectedBody, "Log body mismatch", file: file, line: line)
346425
XCTAssertEqual(capturedLog.timestamp, fixture.dateProvider.date(), "Log timestamp mismatch", file: file, line: line)
347426

348-
let numberOfDefaultAttributes = 4
427+
// Count expected default attributes dynamically
428+
var expectedDefaultAttributeCount = 3 // sdk.name, sdk.version, environment are always present
429+
if fixture.options.releaseName != nil {
430+
expectedDefaultAttributeCount += 1 // sentry.release
431+
}
432+
if fixture.hub.scope.span != nil {
433+
expectedDefaultAttributeCount += 1 // sentry.trace.parent_span_id
434+
}
435+
// OS and device attributes (up to 5 more if context is available)
436+
if let contextDictionary = fixture.hub.scope.serialize()["context"] as? [String: [String: Any]] {
437+
if let osContext = contextDictionary["os"] {
438+
if osContext["name"] != nil { expectedDefaultAttributeCount += 1 }
439+
if osContext["version"] != nil { expectedDefaultAttributeCount += 1 }
440+
}
441+
if contextDictionary["device"] != nil {
442+
expectedDefaultAttributeCount += 1 // device.brand (always "Apple")
443+
if let deviceContext = contextDictionary["device"] {
444+
if deviceContext["model"] != nil { expectedDefaultAttributeCount += 1 }
445+
if deviceContext["family"] != nil { expectedDefaultAttributeCount += 1 }
446+
}
447+
}
448+
}
449+
349450
// Compare attributes
350-
XCTAssertEqual(capturedLog.attributes.count, expectedAttributes.count + numberOfDefaultAttributes, "Attribute count mismatch", file: file, line: line)
451+
XCTAssertEqual(capturedLog.attributes.count, expectedAttributes.count + expectedDefaultAttributeCount, "Attribute count mismatch", file: file, line: line)
351452

352453
for (key, expectedAttribute) in expectedAttributes {
353454
guard let actualAttribute = capturedLog.attributes[key] else {

0 commit comments

Comments
 (0)