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
9 changes: 3 additions & 6 deletions Sources/Instrumentation/Sessions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Sessions
import OpenTelemetrySdk

// Record session start and end events
let sessionInstrumentation = SessionEventInstrumentation()
SessionEventInstrumentation.install()

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

```swift
let instrumentation = SessionEventInstrumentation()
SessionEventInstrumentation.install()
// Emits session.start and session.end log records
```

Expand All @@ -123,7 +123,6 @@ let session = Session(
)

print("Expired: \(session.isExpired())")
print("Duration: \(session.duration ?? 0)")
```

## Configuration
Expand Down Expand Up @@ -188,7 +187,6 @@ A `session.end` log record is created when a session expires.
"session.id": "550e8400-e29b-41d4-a716-446655440000",
"session.start_time": 1692123456789000000,
"session.end_time": 1692125256789000000,
"session.duration": 1800000000000,
"session.previous_id": "71260ACC-5286-455F-9955-5DA8C5109A07"
}
}
Expand All @@ -201,8 +199,7 @@ A `session.end` log record is created when a session expires.
| `session.id` | string | Unique identifier for the ended session | `"550e8400-e29b-41d4-a716-446655440000"` |
| `session.start_time` | double | Session start time in nanoseconds since epoch | `1692123456789000000` |
| `session.end_time` | double | Session end time in nanoseconds since epoch | `1692125256789000000` |
| `session.duration` | double | Session duration in nanoseconds | `1800000000000` (30 minutes) |
| `session.previous_id` | string | Identifier of the previous session (if any) | `"71260ACC-5286-455F-9955-5DA8C5109A07"` |
| `session.previous_id` | string | Identifier of the previous session (if any) | `"71260ACC-5286-455F-9955-5DA8C5109A07"` |

## Span and Log Attribution

Expand Down
4 changes: 2 additions & 2 deletions Sources/Instrumentation/Sessions/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public struct Session: Equatable {
/// Calculates the time between session start and end. Only available for expired sessions.
/// - Returns: The session duration in seconds, or nil if the session is still active
public var duration: TimeInterval? {
guard let endTime = endTime else { return nil }
guard let endTime else { return nil }
return endTime.timeIntervalSince(startTime)
}
}
}
8 changes: 3 additions & 5 deletions Sources/Instrumentation/Sessions/SessionConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ public struct SessionConfig {
public class SessionConfigBuilder {
public private(set) var sessionTimeout: TimeInterval = 30 * 60

public init() {}

/// Sets the session timeout duration
/// - Parameter sessionTimeout: Duration in seconds after which a session expires if left inactive
/// - Returns: The builder instance for method chaining
Expand All @@ -67,10 +65,10 @@ public class SessionConfigBuilder {
}

/// Extension to SessionConfig for builder pattern support
extension SessionConfig {
public extension SessionConfig {
/// Creates a new SessionConfigBuilder instance
/// - Returns: A new builder for creating SessionConfig
public static func builder() -> SessionConfigBuilder {
static func builder() -> SessionConfigBuilder {
return SessionConfigBuilder()
}
}
}
24 changes: 7 additions & 17 deletions Sources/Instrumentation/Sessions/SessionConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,19 @@
/// semantic conventions for session tracking.
///
/// Reference: https://opentelemetry.io/docs/specs/semconv/general/session/

import Foundation

public class SessionConstants {
// MARK: - OpenTelemetry Semantic Conventions

/// Event name for session start events
public static let sessionStartEvent = "session.start"
/// Event name for session end events
/// Event name for session end events
public static let sessionEndEvent = "session.end"
/// Attribute name for session identifier
public static let id = "session.id"
/// Attribute name for previous session identifier
public static let previousId = "session.previous_id"

// MARK: - Extension Attributes

/// Attribute name for session start timestamp
public static let startTime = "session.start_time"
/// Attribute name for session end timestamp
public static let endTime = "session.end_time"
/// Attribute name for session duration
public static let duration = "session.duration"

// MARK: - Internal Constants

/// Notification name for session events
public static let sessionEventNotification = "SessionEventInstrumentation.SessionEvent"
}
}

let SessionEventNotification = Notification.Name(SessionConstants.sessionEventNotification)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public struct SessionEvent {
/// - All session events are converted to OpenTelemetry log records with appropriate attributes
/// - Session end events include duration and end time attributes
public class SessionEventInstrumentation {
private let logger: Logger
private static var logger: Logger {
return OpenTelemetry.instance.loggerProvider.get(instrumentationScopeName: SessionEventInstrumentation.instrumentationKey)
}

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

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

static let instrumentationKey = "io.opentelemetry.sessions"

@available(*, deprecated, message: "Use SessionEventInstrumentation.install() instead")
public init() {
SessionEventInstrumentation.install()
}

/// Flag to track if the instrumentation has been applied.
/// Controls whether new sessions are queued or immediately processed via notifications.
static var isApplied = false

public init() {
logger = OpenTelemetry.instance.loggerProvider.get(instrumentationScopeName: SessionEventInstrumentation.instrumentationKey)
guard !SessionEventInstrumentation.isApplied else {
public static func install() {
guard !isApplied else {
return
}

SessionEventInstrumentation.isApplied = true
isApplied = true
// Process any queued sessions
processQueuedSessions()

// Start observing for new session notifications
NotificationCenter.default.addObserver(
forName: SessionEventInstrumentation.sessionEventNotification,
object: nil,
queue: nil
) { notification in
if let sessionEvent = notification.object as? SessionEvent {
self.createSessionEvent(session: sessionEvent.session, eventType: sessionEvent.eventType)
}
}
}

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

if sessionEvents.isEmpty {
Expand All @@ -97,7 +92,7 @@ public class SessionEventInstrumentation {
/// - Parameters:
/// - session: The session to create an event for
/// - eventType: The type of event to create (start or end)
private func createSessionEvent(session: Session, eventType: SessionEventType) {
private static func createSessionEvent(session: Session, eventType: SessionEventType) {
switch eventType {
case .start:
createSessionStartEvent(session: session)
Expand All @@ -111,21 +106,21 @@ public class SessionEventInstrumentation {
/// Creates an OpenTelemetry log record with session attributes including ID, start time,
/// and previous session ID (if available).
/// - Parameter session: The session that has started
private func createSessionStartEvent(session: Session) {
private static func createSessionStartEvent(session: Session) {
var attributes: [String: AttributeValue] = [
SessionConstants.id: AttributeValue.string(session.id),
SessionConstants.startTime: AttributeValue.double(Double(session.startTime.timeIntervalSince1970.toNanoseconds))
SemanticConventions.Session.id.rawValue: AttributeValue.string(session.id)
]

if let previousId = session.previousId {
attributes[SessionConstants.previousId] = AttributeValue.string(previousId)
attributes[SemanticConventions.Session.previousId.rawValue] = AttributeValue.string(previousId)
}

/// Create `session.start` log record according to otel semantic convention
/// https://opentelemetry.io/docs/specs/semconv/general/session/
logger.logRecordBuilder()
.setBody(AttributeValue.string(SessionConstants.sessionStartEvent))
.setEventName(SessionConstants.sessionStartEvent)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.setAttributes(attributes)
.setTimestamp(session.startTime)
.emit()
}

Expand All @@ -134,44 +129,38 @@ public class SessionEventInstrumentation {
/// Creates an OpenTelemetry log record with session attributes including ID, start time,
/// end time, duration, and previous session ID (if available).
/// - Parameter session: The expired session
private func createSessionEndEvent(session: Session) {
guard let endTime = session.endTime,
let duration = session.duration else {
private static func createSessionEndEvent(session: Session) {
guard let endTime = session.endTime else {
return
}

var attributes: [String: AttributeValue] = [
SessionConstants.id: AttributeValue.string(session.id),
SessionConstants.startTime: AttributeValue.double(Double(session.startTime.timeIntervalSince1970.toNanoseconds)),
SessionConstants.endTime: AttributeValue.double(Double(endTime.timeIntervalSince1970.toNanoseconds)),
SessionConstants.duration: AttributeValue.double(Double(duration.toNanoseconds))
SemanticConventions.Session.id.rawValue: AttributeValue.string(session.id)
]

if let previousId = session.previousId {
attributes[SessionConstants.previousId] = AttributeValue.string(previousId)
attributes[SemanticConventions.Session.previousId.rawValue] = AttributeValue.string(previousId)
}

/// Create `session.end`` log record according to otel semantic convention
/// https://opentelemetry.io/docs/specs/semconv/general/session/
logger.logRecordBuilder()
.setBody(AttributeValue.string(SessionConstants.sessionEndEvent))
.setEventName(SessionConstants.sessionEndEvent)
.setAttributes(attributes)
.setTimestamp(endTime)
.emit()
}

/// Add a session to the queue or send notification if instrumentation is already applied.
///
/// This static method is the main entry point for handling new sessions. It either:
/// - Adds the session to the static queue if instrumentation hasn't been applied yet (max 32 items)
/// - Adds the session to the static queue if instrumentation hasn't been applied yet (max 10 items)
/// - Posts a notification with the session if instrumentation has been applied
///
/// - Parameter session: The session to process
static func addSession(session: Session, eventType: SessionEventType) {
if isApplied {
NotificationCenter.default.post(
name: sessionEventNotification,
object: SessionEvent(session: session, eventType: eventType)
)
createSessionEvent(session: session, eventType: eventType)
} else {
/// SessionManager creates sessions before SessionEventInstrumentation is applied,
/// which the notification observer cannot see. So we need to keep the sessions in a queue.
Expand All @@ -181,4 +170,4 @@ public class SessionEventInstrumentation {
queue.append(SessionEvent(session: session, eventType: eventType))
}
}
}
}
24 changes: 12 additions & 12 deletions Sources/Instrumentation/Sessions/SessionLogRecordProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ public class SessionLogRecordProcessor: LogRecordProcessor {
public func onEmit(logRecord: ReadableLogRecord) {
var enhancedRecord = logRecord

// For session.start and session.end events, preserve existing session attributes
if let body = logRecord.body,
case let .string(bodyString) = body,
bodyString == SessionConstants.sessionStartEvent || bodyString == SessionConstants.sessionEndEvent {
// Session start and end events already have their intended session ids
// Overwriting them would cause session end to have wrong current and previous session ids
} else {
// For other log records, add current session attributes
// Only add session attributes if they don't already exist
if logRecord.attributes[SemanticConventions.Session.id.rawValue] == nil || logRecord.attributes[SemanticConventions.Session.previousId.rawValue] == nil {
let session = sessionManager.getSession()
enhancedRecord.setAttribute(key: SessionConstants.id, value: AttributeValue.string(session.id))
if let previousId = session.previousId {
enhancedRecord.setAttribute(key: SessionConstants.previousId, value: AttributeValue.string(previousId))

// Add session.id if not already present
if logRecord.attributes[SemanticConventions.Session.id.rawValue] == nil {
enhancedRecord.setAttribute(key: SemanticConventions.Session.id.rawValue, value: session.id)
}

// Add session.previous_id if not already present and session has a previous ID
if logRecord.attributes[SemanticConventions.Session.previousId.rawValue] == nil, let previousId = session.previousId {
enhancedRecord.setAttribute(key: SemanticConventions.Session.previousId.rawValue, value: previousId)
}
}

Expand All @@ -51,4 +51,4 @@ public class SessionLogRecordProcessor: LogRecordProcessor {
public func forceFlush(explicitTimeout: TimeInterval?) -> ExportResult {
return .success
}
}
}
Loading
Loading