Skip to content

Latest commit

 

History

History
885 lines (730 loc) · 29.8 KB

File metadata and controls

885 lines (730 loc) · 29.8 KB
title description sidebar_position
Logs
Understanding logs in the Embrace iOS SDK 6.x
3

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Logs

Logs provide contextual information about your application's state and events that occur during a session. The Embrace SDK's logging capabilities help you track, troubleshoot, and receive alerts about important events.

What are Logs?

In the Embrace SDK, logs are records of events with associated severity levels, timestamps, and optional attributes. They provide critical information that can help you understand:

  • Error conditions
  • Warning situations
  • Important state changes
  • Significant user actions
  • System events

Logs are especially valuable for capturing information that requires immediate attention or for troubleshooting specific issues.

When to Use Logs

While Embrace automatically captures sessions and their associated data, there are scenarios where you may want to add custom logs:

  • Hunting difficult bugs
  • Troubleshooting for high-value users
  • Monitoring new version rollouts
  • Tracking critical business events
  • Capturing error conditions that need immediate attention

Using the Log API

The Embrace SDK provides a simple API for logging messages:

let attributes = ["property_a": "value_a", "property_b": "value_b"]
try EmbraceIO.shared.log(
    "Loading not finished in time.", // message
    severity: .warn,
    timestamp: Date.now,
    attributes: attributes
)
let attributes = ["property_a": "value_a", "property_b": "value_b"]
Embrace.client?.log(
    "Loading not finished in time.", // message
    severity: .warn,
    timestamp: Date.now,
    attributes: attributes
)

The log method takes the following parameters:

  • message: A string representing the log message itself
  • severity: The LogSeverity of the event (e.g., info, warn, error)
  • timestamp: When this log event occurred
  • attributes: A dictionary of key-value pairs for additional context and filtering

Log Limits

Embrace enforces certain limits on logs:

  • Maximum number of logs per session
    • 500 Error Logs
    • 200 Warning Logs
    • 100 Info Logs
  • Maximum log message length: 4,000 characters. Messages are truncated if they exceed the limit.

If your application exceeds these limits, the newest logs will be ignored.

Log Batching

To optimize device and network performance, Embrace batches logs according to the following criteria:

  • A maximum of 2 seconds between logs: After receiving a log, we wait for 2 seconds. If no additional log arrives during that period, we send it to the backend.
  • A maximum of 5 seconds for batch lifetime: Log batches should not exist for more than 5 seconds.
  • A maximum of 50 logs per batch: This prevents issues with large batches.

File Attachments with Logs

You can attach data to logs to provide more context for troubleshooting:

Embrace-Hosted Attachments

let attributes = ["property_a": "value_a", "property_b": "value_b"]
try EmbraceIO.shared.log(
    "This is a log with an Embrace-hosted attachment", // message
    severity: .info,
    timestamp: Date.now,
    attachment: someData, // NSData/Data
    attributes: attributes
)
let attributes = ["property_a": "value_a", "property_b": "value_b"]
Embrace.client?.log(
    "This is a log with an Embrace-hosted attachment", // message
    severity: .info,
    timestamp: Date.now,
    attachment: someData, // NSData/Data
    attributes: attributes
)

Limitations for Embrace-hosted attachments:

  • Maximum of 5 attachments per session
  • Maximum attachment size of 1 MiB (1048576 bytes)

User-Hosted Attachments

If you need more attachments or larger files, you can host the attachments yourself and reference them:

let attributes = ["property_a": "value_a", "property_b": "value_b"]
try EmbraceIO.shared.log(
    "This is a log with a user-hosted attachment", // message
    severity: .info,
    timestamp: Date.now,
    attachmentId: attachmentId, // String
    attachmentUrl: attachmentUrl, // URL
    attributes: attributes
)
let attributes = ["property_a": "value_a", "property_b": "value_b"]
Embrace.client?.log(
    "This is a log with a user-hosted attachment", // message
    severity: .info,
    timestamp: Date.now,
    attachmentId: attachmentId, // String
    attachmentUrl: attachmentUrl, // URL
    attributes: attributes
)

Log Alerting

One of the most powerful features of Embrace logs is the ability to set up alerts based on log events. Using the Embrace Dashboard, you can configure email alerts to be sent to your team when certain thresholds are met.

For example, if you have a steady rate of 1% for a given log event, you can set a threshold to receive an alert if it rises significantly.

Logs vs Other Concepts

  • Logs vs Traces: While traces focus on performance and operation flow, logs provide contextual information about events and states
  • Logs vs Sessions: Logs are events within a session that provide additional context
  • Logs vs Breadcrumbs: Logs are more detailed and can trigger alerts, while breadcrumbs are simpler markers of user journey steps

Best Practices

  • Be selective: Excessive logging can impact performance and battery life
  • Use appropriate severity levels: Reserve error and warning levels for actual problematic situations
  • Add meaningful attributes: This helps with filtering and understanding context
  • Keep messages clear and concise: This makes troubleshooting easier
  • Consider timing: Use the timestamp parameter to accurately reflect when events actually occurred
  • Batch related logs: Log related information together using consistent attribute keys

Common Logging Patterns

Network Failure Logging

Track network issues with detailed context for troubleshooting:

func handleNetworkRequest(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    let startTime = Date()

    URLSession.shared.dataTask(with: url) { data, response, error in
        let duration = Date().timeIntervalSince(startTime)
        let httpResponse = response as? HTTPURLResponse

        if let error = error {
            // Log network errors with detailed context
            try? EmbraceIO.shared.log(
                "Network request failed",
                severity: .error,
                attributes: [
                    "network.url": url.absoluteString,
                    "network.error_code": String((error as NSError).code),
                    "network.error_domain": (error as NSError).domain,
                    "network.duration_ms": String(Int(duration * 1000)),
                    "network.retry_count": "0" // Increment if retrying
                ]
            )
        } else if let httpResponse = httpResponse {
            if httpResponse.statusCode >= 400 {
                // Log HTTP errors
                try? EmbraceIO.shared.log(
                    "HTTP request returned error status",
                    severity: .warning,
                    attributes: [
                        "network.url": url.absoluteString,
                        "network.status_code": String(httpResponse.statusCode),
                        "network.duration_ms": String(Int(duration * 1000)),
                        "network.response_size": String(data?.count ?? 0)
                    ]
                )
            } else {
                // Log successful requests for monitoring
                try? EmbraceIO.shared.log(
                    "Network request completed successfully",
                    severity: .info,
                    attributes: [
                        "network.url": url.absoluteString,
                        "network.status_code": String(httpResponse.statusCode),
                        "network.duration_ms": String(Int(duration * 1000)),
                        "network.response_size": String(data?.count ?? 0)
                    ]
                )
            }
        }

        completion(data != nil ? .success(data!) : .failure(error ?? URLError(.unknown)))
    }.resume()
}
func handleNetworkRequest(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    let startTime = Date()

    URLSession.shared.dataTask(with: url) { data, response, error in
        let duration = Date().timeIntervalSince(startTime)
        let httpResponse = response as? HTTPURLResponse

        if let error = error {
            // Log network errors with detailed context
            Embrace.client?.log(
                "Network request failed",
                severity: .error,
                attributes: [
                    "network.url": url.absoluteString,
                    "network.error_code": String((error as NSError).code),
                    "network.error_domain": (error as NSError).domain,
                    "network.duration_ms": String(Int(duration * 1000)),
                    "network.retry_count": "0" // Increment if retrying
                ]
            )
        } else if let httpResponse = httpResponse {
            if httpResponse.statusCode >= 400 {
                // Log HTTP errors
                Embrace.client?.log(
                    "HTTP request returned error status",
                    severity: .warning,
                    attributes: [
                        "network.url": url.absoluteString,
                        "network.status_code": String(httpResponse.statusCode),
                        "network.duration_ms": String(Int(duration * 1000)),
                        "network.response_size": String(data?.count ?? 0)
                    ]
                )
            } else {
                // Log successful requests for monitoring
                Embrace.client?.log(
                    "Network request completed successfully",
                    severity: .info,
                    attributes: [
                        "network.url": url.absoluteString,
                        "network.status_code": String(httpResponse.statusCode),
                        "network.duration_ms": String(Int(duration * 1000)),
                        "network.response_size": String(data?.count ?? 0)
                    ]
                )
            }
        }

        completion(data != nil ? .success(data!) : .failure(error ?? URLError(.unknown)))
    }.resume()
}

Authentication Flow Logging

Track authentication events for security monitoring:

class AuthenticationManager {
    func login(username: String, password: String) async throws -> AuthResult {
        let loginStartTime = Date()

        // Log login attempt
        try? EmbraceIO.shared.log(
            "User login attempt started",
            severity: .info,
            attributes: [
                "auth.username_hash": username.sha256, // Hash for privacy
                "auth.method": "password",
                "auth.client_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
            ]
        )

        do {
            let result = try await performLogin(username: username, password: password)
            let duration = Date().timeIntervalSince(loginStartTime)

            // Log successful login
            try? EmbraceIO.shared.log(
                "User login successful",
                severity: .info,
                attributes: [
                    "auth.username_hash": username.sha256,
                    "auth.duration_ms": String(Int(duration * 1000)),
                    "auth.user_tier": result.userTier,
                    "auth.session_type": result.sessionType
                ]
            )

            // Update user context
            EmbraceIO.shared.userIdentifier = result.userId
            try? EmbraceIO.shared.addPersona(PersonaTag("authenticated"), lifespan: .process)

            return result

        } catch let authError as AuthenticationError {
            let duration = Date().timeIntervalSince(loginStartTime)

            // Log authentication failure with detailed error info
            try? EmbraceIO.shared.log(
                "User login failed",
                severity: .warning,
                attributes: [
                    "auth.username_hash": username.sha256,
                    "auth.error_type": authError.type.rawValue,
                    "auth.error_code": String(authError.code),
                    "auth.duration_ms": String(Int(duration * 1000)),
                    "auth.retry_count": String(authError.retryCount)
                ]
            )

            throw authError
        }
    }

    func logout() {
        try? EmbraceIO.shared.log(
            "User logout initiated",
            severity: .info,
            attributes: [
                "auth.logout_type": "user_initiated",
                "auth.session_duration": String(getCurrentSessionDuration())
            ]
        )

        // Clear user context
        EmbraceIO.shared.userIdentifier = nil
        EmbraceIO.shared.clearUserProperties()
    }
}
class AuthenticationManager {
    func login(username: String, password: String) async throws -> AuthResult {
        let loginStartTime = Date()

        // Log login attempt
        Embrace.client?.log(
            "User login attempt started",
            severity: .info,
            attributes: [
                "auth.username_hash": username.sha256, // Hash for privacy
                "auth.method": "password",
                "auth.client_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
            ]
        )

        do {
            let result = try await performLogin(username: username, password: password)
            let duration = Date().timeIntervalSince(loginStartTime)

            // Log successful login
            Embrace.client?.log(
                "User login successful",
                severity: .info,
                attributes: [
                    "auth.username_hash": username.sha256,
                    "auth.duration_ms": String(Int(duration * 1000)),
                    "auth.user_tier": result.userTier,
                    "auth.session_type": result.sessionType
                ]
            )

            // Update user context
            Embrace.client?.metadata.userIdentifier = result.userId
            try? Embrace.client?.metadata.add(persona: PersonaTag("authenticated"), lifespan: .process)

            return result

        } catch let authError as AuthenticationError {
            let duration = Date().timeIntervalSince(loginStartTime)

            // Log authentication failure with detailed error info
            Embrace.client?.log(
                "User login failed",
                severity: .warning,
                attributes: [
                    "auth.username_hash": username.sha256,
                    "auth.error_type": authError.type.rawValue,
                    "auth.error_code": String(authError.code),
                    "auth.duration_ms": String(Int(duration * 1000)),
                    "auth.retry_count": String(authError.retryCount)
                ]
            )

            throw authError
        }
    }

    func logout() {
        Embrace.client?.log(
            "User logout initiated",
            severity: .info,
            attributes: [
                "auth.logout_type": "user_initiated",
                "auth.session_duration": String(getCurrentSessionDuration())
            ]
        )

        // Clear user context
        Embrace.client?.metadata.userIdentifier = nil
        Embrace.client?.metadata.clearUserProperties()
    }
}

Performance Bottleneck Logging

Log performance issues with detailed timing information:

class PerformanceLogger {
    func trackCriticalOperation<T>(
        operation: String,
        expectedDuration: TimeInterval,
        work: () async throws -> T
    ) async rethrows -> T {
        let startTime = Date()
        let startMemory = getCurrentMemoryUsage()

        do {
            let result = try await work()
            let duration = Date().timeIntervalSince(startTime)
            let endMemory = getCurrentMemoryUsage()
            let memoryDelta = endMemory - startMemory

            // Log performance metrics
            let severity: LogSeverity = duration > expectedDuration ? .warning : .info
            let message = duration > expectedDuration ?
                "Operation exceeded expected duration" :
                "Operation completed within expected time"

            try? EmbraceIO.shared.log(
                message,
                severity: severity,
                attributes: [
                    "performance.operation": operation,
                    "performance.duration_ms": String(Int(duration * 1000)),
                    "performance.expected_duration_ms": String(Int(expectedDuration * 1000)),
                    "performance.memory_delta_mb": String(format: "%.2f", memoryDelta / 1024 / 1024),
                    "performance.exceeded_threshold": String(duration > expectedDuration),
                    "performance.device_model": UIDevice.current.model
                ]
            )

            return result

        } catch {
            let duration = Date().timeIntervalSince(startTime)

            // Log performance issues that led to errors
            try? EmbraceIO.shared.log(
                "Critical operation failed with performance implications",
                severity: .error,
                attributes: [
                    "performance.operation": operation,
                    "performance.duration_ms": String(Int(duration * 1000)),
                    "performance.error_type": String(describing: type(of: error)),
                    "performance.error_message": error.localizedDescription
                ]
            )

            throw error
        }
    }

    func logMemoryPressure(level: MemoryPressureLevel) {
        try? EmbraceIO.shared.log(
            "Memory pressure detected",
            severity: level == .critical ? .error : .warning,
            attributes: [
                "memory.pressure_level": level.rawValue,
                "memory.current_usage_mb": String(format: "%.2f", getCurrentMemoryUsage() / 1024 / 1024),
                "memory.available_mb": String(format: "%.2f", getAvailableMemory() / 1024 / 1024),
                "memory.app_state": UIApplication.shared.applicationState.description
            ]
        )
    }

    private func getCurrentMemoryUsage() -> UInt64 {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4

        let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
            }
        }

        return kerr == KERN_SUCCESS ? info.resident_size : 0
    }
}

enum MemoryPressureLevel: String {
    case normal = "normal"
    case warning = "warning"
    case critical = "critical"
}
class PerformanceLogger {
    func trackCriticalOperation<T>(
        operation: String,
        expectedDuration: TimeInterval,
        work: () async throws -> T
    ) async rethrows -> T {
        let startTime = Date()
        let startMemory = getCurrentMemoryUsage()

        do {
            let result = try await work()
            let duration = Date().timeIntervalSince(startTime)
            let endMemory = getCurrentMemoryUsage()
            let memoryDelta = endMemory - startMemory

            // Log performance metrics
            let severity: LogSeverity = duration > expectedDuration ? .warning : .info
            let message = duration > expectedDuration ?
                "Operation exceeded expected duration" :
                "Operation completed within expected time"

            Embrace.client?.log(
                message,
                severity: severity,
                attributes: [
                    "performance.operation": operation,
                    "performance.duration_ms": String(Int(duration * 1000)),
                    "performance.expected_duration_ms": String(Int(expectedDuration * 1000)),
                    "performance.memory_delta_mb": String(format: "%.2f", memoryDelta / 1024 / 1024),
                    "performance.exceeded_threshold": String(duration > expectedDuration),
                    "performance.device_model": UIDevice.current.model
                ]
            )

            return result

        } catch {
            let duration = Date().timeIntervalSince(startTime)

            // Log performance issues that led to errors
            Embrace.client?.log(
                "Critical operation failed with performance implications",
                severity: .error,
                attributes: [
                    "performance.operation": operation,
                    "performance.duration_ms": String(Int(duration * 1000)),
                    "performance.error_type": String(describing: type(of: error)),
                    "performance.error_message": error.localizedDescription
                ]
            )

            throw error
        }
    }

    func logMemoryPressure(level: MemoryPressureLevel) {
        Embrace.client?.log(
            "Memory pressure detected",
            severity: level == .critical ? .error : .warning,
            attributes: [
                "memory.pressure_level": level.rawValue,
                "memory.current_usage_mb": String(format: "%.2f", getCurrentMemoryUsage() / 1024 / 1024),
                "memory.available_mb": String(format: "%.2f", getAvailableMemory() / 1024 / 1024),
                "memory.app_state": UIApplication.shared.applicationState.description
            ]
        )
    }

    private func getCurrentMemoryUsage() -> UInt64 {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4

        let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
            }
        }

        return kerr == KERN_SUCCESS ? info.resident_size : 0
    }
}

enum MemoryPressureLevel: String {
    case normal = "normal"
    case warning = "warning"
    case critical = "critical"
}

Database Operation Logging

Track database performance and errors:

class DatabaseLogger {
    func logDatabaseOperation<T>(
        operation: String,
        table: String,
        work: () throws -> T
    ) rethrows -> T {
        let startTime = Date()

        do {
            let result = try work()
            let duration = Date().timeIntervalSince(startTime)

            // Log successful database operations
            try? EmbraceIO.shared.log(
                "Database operation completed",
                severity: duration > 1.0 ? .warning : .debug, // Warn if over 1 second
                attributes: [
                    "database.operation": operation,
                    "database.table": table,
                    "database.duration_ms": String(Int(duration * 1000)),
                    "database.thread": Thread.isMainThread ? "main" : "background"
                ]
            )

            return result

        } catch {
            let duration = Date().timeIntervalSince(startTime)

            // Log database errors with context
            try? EmbraceIO.shared.log(
                "Database operation failed",
                severity: .error,
                attributes: [
                    "database.operation": operation,
                    "database.table": table,
                    "database.duration_ms": String(Int(duration * 1000)),
                    "database.error_code": String((error as NSError).code),
                    "database.error_domain": (error as NSError).domain,
                    "database.error_message": error.localizedDescription,
                    "database.thread": Thread.isMainThread ? "main" : "background"
                ]
            )

            throw error
        }
    }
}
class DatabaseLogger {
    func logDatabaseOperation<T>(
        operation: String,
        table: String,
        work: () throws -> T
    ) rethrows -> T {
        let startTime = Date()

        do {
            let result = try work()
            let duration = Date().timeIntervalSince(startTime)

            // Log successful database operations
            Embrace.client?.log(
                "Database operation completed",
                severity: duration > 1.0 ? .warning : .debug, // Warn if over 1 second
                attributes: [
                    "database.operation": operation,
                    "database.table": table,
                    "database.duration_ms": String(Int(duration * 1000)),
                    "database.thread": Thread.isMainThread ? "main" : "background"
                ]
            )

            return result

        } catch {
            let duration = Date().timeIntervalSince(startTime)

            // Log database errors with context
            Embrace.client?.log(
                "Database operation failed",
                severity: .error,
                attributes: [
                    "database.operation": operation,
                    "database.table": table,
                    "database.duration_ms": String(Int(duration * 1000)),
                    "database.error_code": String((error as NSError).code),
                    "database.error_domain": (error as NSError).domain,
                    "database.error_message": error.localizedDescription,
                    "database.thread": Thread.isMainThread ? "main" : "background"
                ]
            )

            throw error
        }
    }
}

UI Responsiveness Logging

Track UI performance issues:

class UIPerformanceLogger {
    func logViewControllerLifecycle(
        viewController: String,
        lifecycle: String,
        duration: TimeInterval
    ) {
        let severity: LogSeverity = duration > 0.5 ? .warning : .debug

        try? EmbraceIO.shared.log(
            "View controller lifecycle event",
            severity: severity,
            attributes: [
                "ui.view_controller": viewController,
                "ui.lifecycle_event": lifecycle,
                "ui.duration_ms": String(Int(duration * 1000)),
                "ui.is_slow": String(duration > 0.5),
                "ui.device_orientation": UIDevice.current.orientation.description
            ]
        )
    }

    func logMainThreadBlock(duration: TimeInterval, operation: String) {
        if duration > 0.1 { // Log if main thread blocked for more than 100ms
            try? EmbraceIO.shared.log(
                "Main thread blocked",
                severity: duration > 0.5 ? .error : .warning,
                attributes: [
                    "ui.blocked_duration_ms": String(Int(duration * 1000)),
                    "ui.blocking_operation": operation,
                    "ui.severity_level": duration > 0.5 ? "critical" : "moderate"
                ]
            )
        }
    }
}
class UIPerformanceLogger {
    func logViewControllerLifecycle(
        viewController: String,
        lifecycle: String,
        duration: TimeInterval
    ) {
        let severity: LogSeverity = duration > 0.5 ? .warning : .debug

        Embrace.client?.log(
            "View controller lifecycle event",
            severity: severity,
            attributes: [
                "ui.view_controller": viewController,
                "ui.lifecycle_event": lifecycle,
                "ui.duration_ms": String(Int(duration * 1000)),
                "ui.is_slow": String(duration > 0.5),
                "ui.device_orientation": UIDevice.current.orientation.description
            ]
        )
    }

    func logMainThreadBlock(duration: TimeInterval, operation: String) {
        if duration > 0.1 { // Log if main thread blocked for more than 100ms
            Embrace.client?.log(
                "Main thread blocked",
                severity: duration > 0.5 ? .error : .warning,
                attributes: [
                    "ui.blocked_duration_ms": String(Int(duration * 1000)),
                    "ui.blocking_operation": operation,
                    "ui.severity_level": duration > 0.5 ? "critical" : "moderate"
                ]
            )
        }
    }
}

These patterns provide comprehensive logging for the most common performance and reliability issues in iOS applications, helping you quickly identify and resolve problems.