Skip to content

Commit 8cc06f0

Browse files
authored
refactor!: record sessions as log events (#1000)
* refactor!: record sessions as log events * revert to locks * refactor: remove scary force unwrap nils * fix: off by one error * chore: add locked_ prefix * deprecate SessionConstants.id and prevId * remove session.duration
1 parent 0667175 commit 8cc06f0

12 files changed

+472
-345
lines changed

Sources/Instrumentation/Sessions/README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Sessions
1919
import OpenTelemetrySdk
2020

2121
// Record session start and end events
22-
let sessionInstrumentation = SessionEventInstrumentation()
22+
SessionEventInstrumentation.install()
2323

2424
// Add session attributes to spans
2525
let sessionSpanProcessor = SessionSpanProcessor()
@@ -107,7 +107,7 @@ let processor = SessionLogRecordProcessor(nextProcessor: yourProcessor)
107107
Creates OpenTelemetry log records for session lifecycle events.
108108

109109
```swift
110-
let instrumentation = SessionEventInstrumentation()
110+
SessionEventInstrumentation.install()
111111
// Emits session.start and session.end log records
112112
```
113113

@@ -123,7 +123,6 @@ let session = Session(
123123
)
124124

125125
print("Expired: \(session.isExpired())")
126-
print("Duration: \(session.duration ?? 0)")
127126
```
128127

129128
## Configuration
@@ -188,7 +187,6 @@ A `session.end` log record is created when a session expires.
188187
"session.id": "550e8400-e29b-41d4-a716-446655440000",
189188
"session.start_time": 1692123456789000000,
190189
"session.end_time": 1692125256789000000,
191-
"session.duration": 1800000000000,
192190
"session.previous_id": "71260ACC-5286-455F-9955-5DA8C5109A07"
193191
}
194192
}
@@ -201,8 +199,7 @@ A `session.end` log record is created when a session expires.
201199
| `session.id` | string | Unique identifier for the ended session | `"550e8400-e29b-41d4-a716-446655440000"` |
202200
| `session.start_time` | double | Session start time in nanoseconds since epoch | `1692123456789000000` |
203201
| `session.end_time` | double | Session end time in nanoseconds since epoch | `1692125256789000000` |
204-
| `session.duration` | double | Session duration in nanoseconds | `1800000000000` (30 minutes) |
205-
| `session.previous_id` | string | Identifier of the previous session (if any) | `"71260ACC-5286-455F-9955-5DA8C5109A07"` |
202+
| `session.previous_id` | string | Identifier of the previous session (if any) | `"71260ACC-5286-455F-9955-5DA8C5109A07"` |
206203

207204
## Span and Log Attribution
208205

Sources/Instrumentation/Sessions/Session.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public struct Session: Equatable {
8484
/// Calculates the time between session start and end. Only available for expired sessions.
8585
/// - Returns: The session duration in seconds, or nil if the session is still active
8686
public var duration: TimeInterval? {
87-
guard let endTime = endTime else { return nil }
87+
guard let endTime else { return nil }
8888
return endTime.timeIntervalSince(startTime)
8989
}
90-
}
90+
}

Sources/Instrumentation/Sessions/SessionConfig.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ public struct SessionConfig {
4949
public class SessionConfigBuilder {
5050
public private(set) var sessionTimeout: TimeInterval = 30 * 60
5151

52-
public init() {}
53-
5452
/// Sets the session timeout duration
5553
/// - Parameter sessionTimeout: Duration in seconds after which a session expires if left inactive
5654
/// - Returns: The builder instance for method chaining
@@ -67,10 +65,10 @@ public class SessionConfigBuilder {
6765
}
6866

6967
/// Extension to SessionConfig for builder pattern support
70-
extension SessionConfig {
68+
public extension SessionConfig {
7169
/// Creates a new SessionConfigBuilder instance
7270
/// - Returns: A new builder for creating SessionConfig
73-
public static func builder() -> SessionConfigBuilder {
71+
static func builder() -> SessionConfigBuilder {
7472
return SessionConfigBuilder()
7573
}
76-
}
74+
}

Sources/Instrumentation/Sessions/SessionConstants.swift

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,19 @@
99
/// semantic conventions for session tracking.
1010
///
1111
/// Reference: https://opentelemetry.io/docs/specs/semconv/general/session/
12+
13+
import Foundation
14+
1215
public class SessionConstants {
1316
// MARK: - OpenTelemetry Semantic Conventions
1417

1518
/// Event name for session start events
1619
public static let sessionStartEvent = "session.start"
17-
/// Event name for session end events
20+
/// Event name for session end events
1821
public static let sessionEndEvent = "session.end"
19-
/// Attribute name for session identifier
20-
public static let id = "session.id"
21-
/// Attribute name for previous session identifier
22-
public static let previousId = "session.previous_id"
23-
24-
// MARK: - Extension Attributes
25-
26-
/// Attribute name for session start timestamp
27-
public static let startTime = "session.start_time"
28-
/// Attribute name for session end timestamp
29-
public static let endTime = "session.end_time"
30-
/// Attribute name for session duration
31-
public static let duration = "session.duration"
32-
33-
// MARK: - Internal Constants
3422

3523
/// Notification name for session events
3624
public static let sessionEventNotification = "SessionEventInstrumentation.SessionEvent"
37-
}
25+
}
26+
27+
let SessionEventNotification = Notification.Name(SessionConstants.sessionEventNotification)

Sources/Instrumentation/Sessions/SessionEventInstrumentation.swift

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ public struct SessionEvent {
3030
/// - All session events are converted to OpenTelemetry log records with appropriate attributes
3131
/// - Session end events include duration and end time attributes
3232
public class SessionEventInstrumentation {
33-
private let logger: Logger
33+
private static var logger: Logger {
34+
return OpenTelemetry.instance.loggerProvider.get(instrumentationScopeName: SessionEventInstrumentation.instrumentationKey)
35+
}
3436

3537
/// Queue for storing session events that were created before instrumentation was initialized.
3638
/// This allows capturing session events that occur during application startup before
@@ -43,42 +45,35 @@ public class SessionEventInstrumentation {
4345

4446
/// Notification name for new session events.
4547
/// Used to broadcast session creation and expiration events after instrumentation is applied.
46-
static let sessionEventNotification = Notification.Name(SessionConstants.sessionEventNotification)
48+
@available(*, deprecated, message: "Use SessionEventNotification instead")
49+
static let sessionEventNotification = SessionEventNotification
4750

4851
static let instrumentationKey = "io.opentelemetry.sessions"
4952

53+
@available(*, deprecated, message: "Use SessionEventInstrumentation.install() instead")
54+
public init() {
55+
SessionEventInstrumentation.install()
56+
}
57+
5058
/// Flag to track if the instrumentation has been applied.
5159
/// Controls whether new sessions are queued or immediately processed via notifications.
5260
static var isApplied = false
53-
54-
public init() {
55-
logger = OpenTelemetry.instance.loggerProvider.get(instrumentationScopeName: SessionEventInstrumentation.instrumentationKey)
56-
guard !SessionEventInstrumentation.isApplied else {
61+
public static func install() {
62+
guard !isApplied else {
5763
return
5864
}
5965

60-
SessionEventInstrumentation.isApplied = true
66+
isApplied = true
6167
// Process any queued sessions
6268
processQueuedSessions()
63-
64-
// Start observing for new session notifications
65-
NotificationCenter.default.addObserver(
66-
forName: SessionEventInstrumentation.sessionEventNotification,
67-
object: nil,
68-
queue: nil
69-
) { notification in
70-
if let sessionEvent = notification.object as? SessionEvent {
71-
self.createSessionEvent(session: sessionEvent.session, eventType: sessionEvent.eventType)
72-
}
73-
}
7469
}
7570

7671
/// Process any sessions that were queued before instrumentation was applied.
7772
///
7873
/// This method is called during the `apply()` process to handle any sessions that
7974
/// were created before the instrumentation was initialized. It creates log records
8075
/// for all queued sessions and then clears the queue.
81-
private func processQueuedSessions() {
76+
private static func processQueuedSessions() {
8277
let sessionEvents = SessionEventInstrumentation.queue
8378

8479
if sessionEvents.isEmpty {
@@ -97,7 +92,7 @@ public class SessionEventInstrumentation {
9792
/// - Parameters:
9893
/// - session: The session to create an event for
9994
/// - eventType: The type of event to create (start or end)
100-
private func createSessionEvent(session: Session, eventType: SessionEventType) {
95+
private static func createSessionEvent(session: Session, eventType: SessionEventType) {
10196
switch eventType {
10297
case .start:
10398
createSessionStartEvent(session: session)
@@ -111,21 +106,21 @@ public class SessionEventInstrumentation {
111106
/// Creates an OpenTelemetry log record with session attributes including ID, start time,
112107
/// and previous session ID (if available).
113108
/// - Parameter session: The session that has started
114-
private func createSessionStartEvent(session: Session) {
109+
private static func createSessionStartEvent(session: Session) {
115110
var attributes: [String: AttributeValue] = [
116-
SessionConstants.id: AttributeValue.string(session.id),
117-
SessionConstants.startTime: AttributeValue.double(Double(session.startTime.timeIntervalSince1970.toNanoseconds))
111+
SemanticConventions.Session.id.rawValue: AttributeValue.string(session.id)
118112
]
119113

120114
if let previousId = session.previousId {
121-
attributes[SessionConstants.previousId] = AttributeValue.string(previousId)
115+
attributes[SemanticConventions.Session.previousId.rawValue] = AttributeValue.string(previousId)
122116
}
123117

124118
/// Create `session.start` log record according to otel semantic convention
125119
/// https://opentelemetry.io/docs/specs/semconv/general/session/
126120
logger.logRecordBuilder()
127-
.setBody(AttributeValue.string(SessionConstants.sessionStartEvent))
121+
.setEventName(SessionConstants.sessionStartEvent)
128122
.setAttributes(attributes)
123+
.setTimestamp(session.startTime)
129124
.emit()
130125
}
131126

@@ -134,44 +129,38 @@ public class SessionEventInstrumentation {
134129
/// Creates an OpenTelemetry log record with session attributes including ID, start time,
135130
/// end time, duration, and previous session ID (if available).
136131
/// - Parameter session: The expired session
137-
private func createSessionEndEvent(session: Session) {
138-
guard let endTime = session.endTime,
139-
let duration = session.duration else {
132+
private static func createSessionEndEvent(session: Session) {
133+
guard let endTime = session.endTime else {
140134
return
141135
}
142136

143137
var attributes: [String: AttributeValue] = [
144-
SessionConstants.id: AttributeValue.string(session.id),
145-
SessionConstants.startTime: AttributeValue.double(Double(session.startTime.timeIntervalSince1970.toNanoseconds)),
146-
SessionConstants.endTime: AttributeValue.double(Double(endTime.timeIntervalSince1970.toNanoseconds)),
147-
SessionConstants.duration: AttributeValue.double(Double(duration.toNanoseconds))
138+
SemanticConventions.Session.id.rawValue: AttributeValue.string(session.id)
148139
]
149140

150141
if let previousId = session.previousId {
151-
attributes[SessionConstants.previousId] = AttributeValue.string(previousId)
142+
attributes[SemanticConventions.Session.previousId.rawValue] = AttributeValue.string(previousId)
152143
}
153144

154145
/// Create `session.end`` log record according to otel semantic convention
155146
/// https://opentelemetry.io/docs/specs/semconv/general/session/
156147
logger.logRecordBuilder()
157-
.setBody(AttributeValue.string(SessionConstants.sessionEndEvent))
148+
.setEventName(SessionConstants.sessionEndEvent)
158149
.setAttributes(attributes)
150+
.setTimestamp(endTime)
159151
.emit()
160152
}
161153

162154
/// Add a session to the queue or send notification if instrumentation is already applied.
163155
///
164156
/// This static method is the main entry point for handling new sessions. It either:
165-
/// - Adds the session to the static queue if instrumentation hasn't been applied yet (max 32 items)
157+
/// - Adds the session to the static queue if instrumentation hasn't been applied yet (max 10 items)
166158
/// - Posts a notification with the session if instrumentation has been applied
167159
///
168160
/// - Parameter session: The session to process
169161
static func addSession(session: Session, eventType: SessionEventType) {
170162
if isApplied {
171-
NotificationCenter.default.post(
172-
name: sessionEventNotification,
173-
object: SessionEvent(session: session, eventType: eventType)
174-
)
163+
createSessionEvent(session: session, eventType: eventType)
175164
} else {
176165
/// SessionManager creates sessions before SessionEventInstrumentation is applied,
177166
/// which the notification observer cannot see. So we need to keep the sessions in a queue.
@@ -181,4 +170,4 @@ public class SessionEventInstrumentation {
181170
queue.append(SessionEvent(session: session, eventType: eventType))
182171
}
183172
}
184-
}
173+
}

Sources/Instrumentation/Sessions/SessionLogRecordProcessor.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@ public class SessionLogRecordProcessor: LogRecordProcessor {
2424
public func onEmit(logRecord: ReadableLogRecord) {
2525
var enhancedRecord = logRecord
2626

27-
// For session.start and session.end events, preserve existing session attributes
28-
if let body = logRecord.body,
29-
case let .string(bodyString) = body,
30-
bodyString == SessionConstants.sessionStartEvent || bodyString == SessionConstants.sessionEndEvent {
31-
// Session start and end events already have their intended session ids
32-
// Overwriting them would cause session end to have wrong current and previous session ids
33-
} else {
34-
// For other log records, add current session attributes
27+
// Only add session attributes if they don't already exist
28+
if logRecord.attributes[SemanticConventions.Session.id.rawValue] == nil || logRecord.attributes[SemanticConventions.Session.previousId.rawValue] == nil {
3529
let session = sessionManager.getSession()
36-
enhancedRecord.setAttribute(key: SessionConstants.id, value: AttributeValue.string(session.id))
37-
if let previousId = session.previousId {
38-
enhancedRecord.setAttribute(key: SessionConstants.previousId, value: AttributeValue.string(previousId))
30+
31+
// Add session.id if not already present
32+
if logRecord.attributes[SemanticConventions.Session.id.rawValue] == nil {
33+
enhancedRecord.setAttribute(key: SemanticConventions.Session.id.rawValue, value: session.id)
34+
}
35+
36+
// Add session.previous_id if not already present and session has a previous ID
37+
if logRecord.attributes[SemanticConventions.Session.previousId.rawValue] == nil, let previousId = session.previousId {
38+
enhancedRecord.setAttribute(key: SemanticConventions.Session.previousId.rawValue, value: previousId)
3939
}
4040
}
4141

@@ -51,4 +51,4 @@ public class SessionLogRecordProcessor: LogRecordProcessor {
5151
public func forceFlush(explicitTimeout: TimeInterval?) -> ExportResult {
5252
return .success
5353
}
54-
}
54+
}

0 commit comments

Comments
 (0)