Skip to content

Commit

Permalink
Adding Embrace client stop() API (#158)
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoEmbrace authored Jan 21, 2025
1 parent 7e3ea2c commit 239a70f
Show file tree
Hide file tree
Showing 32 changed files with 445 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//
// Copyright © 2025 Embrace Mobile, Inc. All rights reserved.
//

public protocol EmbraceSDKStateProvider: AnyObject {
var isEnabled: Bool { get}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public extension InstrumentableViewController {
startTime: startTime,
endTime: endTime,
attributes: attributes
)
)
}

/// Method used to add attributes to the active trace associated with the render process of a `UIViewController`.
Expand Down
18 changes: 6 additions & 12 deletions Sources/EmbraceCore/Capture/UX/View/ViewCaptureService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ private extension ViewCaptureService {
selector: selector,
implementationType: (@convention(c) (UIViewController, Selector) -> Void).self,
blockImplementationType: (@convention(block) (UIViewController) -> Void).self
) { originalImplementation in
{ viewController in
) { originalImplementation in { viewController in
// If the state was already fulfilled, then call the original implementation.
if let state = viewController.emb_instrumentation_state, state.viewDidLoadSpanCreated {
originalImplementation(viewController, selector)
Expand All @@ -133,8 +132,7 @@ private extension ViewCaptureService {
selector: selector,
implementationType: (@convention(c) (UIViewController, Selector, Bool) -> Void).self,
blockImplementationType: (@convention(block) (UIViewController, Bool) -> Void).self
) { originalImplementation in
{ viewController, animated in
) { originalImplementation in { viewController, animated in
// If by this time (`viewWillAppear` being called) there's no `emb_instrumentation_state` associated
// to the viewController, then we don't swizzle as the "instrument render" feature might be disabled.
if let state = viewController.emb_instrumentation_state {
Expand Down Expand Up @@ -173,8 +171,7 @@ private extension ViewCaptureService {
selector: selector,
implementationType: (@convention(c) (UIViewController, Selector, Bool) -> Void).self,
blockImplementationType: (@convention(block) (UIViewController, Bool) -> Void).self
) { originalImplementation in
{ viewController, animated in
) { originalImplementation in { viewController, animated in
// If the state was already fulfilled, then call the original implementation.
if let state = viewController.emb_instrumentation_state, state.viewDidAppearSpanCreated {
originalImplementation(viewController, selector, animated)
Expand Down Expand Up @@ -206,8 +203,7 @@ private extension ViewCaptureService {
selector: selector,
implementationType: (@convention(c) (UIViewController, Selector, Bool) -> Void).self,
blockImplementationType: (@convention(block) (UIViewController, Bool) -> Void).self
) { originalImplementation in
{ viewController, animated in
) { originalImplementation in { viewController, animated in
self.handler.onViewDidDisappear(viewController)
originalImplementation(viewController, selector, animated)
}
Expand All @@ -229,8 +225,7 @@ private extension ViewCaptureService {
blockImplementationType: (
@convention(block) (UIViewController, NSCoder) -> UIViewController?
).self
) { originalImplementation in
{ viewController, coder in
) { originalImplementation in { viewController, coder in
// Get the class and bundle path of the view controller being initialized and check
// if the view controller belongs to the main bundle (this excludes, for eaxmple, UIKit classes)
let viewControllerClass = type(of: viewController)
Expand Down Expand Up @@ -266,8 +261,7 @@ private extension ViewCaptureService {
blockImplementationType: (
@convention(block) (UIViewController, String?, Bundle?) -> UIViewController
).self
) { originalImplementation in
{ viewController, nibName, bundle in
) { originalImplementation in { viewController, nibName, bundle in
// Get the class and bundle path of the view controller being initialized and check
// if the view controller belongs to the main bundle (this excludes, for eaxmple, UIKit classes)
let viewControllerClass = type(of: viewController)
Expand Down
113 changes: 87 additions & 26 deletions Sources/EmbraceCore/Embrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
/// The `Embrace.Options` that were used to configure the SDK.
@objc public private(set) var options: Embrace.Options

/// Returns the current state of the SDK.
@objc public private(set) var state: EmbraceSDKState = .notInitialized

/// Returns whether the SDK was started.
@objc public private(set) var started: Bool
@available(*, deprecated, message: "Use `state` instead.")
@objc public var started: Bool {
return state == .started
}

/// Returns the `DeviceIdentifier` used by Embrace for the current device.
public private(set) var deviceId: DeviceIdentifier
Expand All @@ -52,6 +58,12 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
}
}

/// Returns true if the SDK is started and was not disabled through remote configurations.
@objc public var isSDKEnabled: Bool {
let remoteConfigEnabled = config?.isSDKEnabled ?? true
return state == .started && remoteConfigEnabled
}

/// Returns the version of the Embrace SDK.
@objc public class var sdkVersion: String {
return EmbraceMeta.sdkVersion
Expand All @@ -60,13 +72,6 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
/// Returns the current `MetadataHandler` used to store resources and session properties.
@objc public let metadata: MetadataHandler

var isSDKEnabled: Bool {
if let config = config {
return config.isSDKEnabled
}
return true
}

let config: EmbraceConfig?
let storage: EmbraceStorage
let upload: EmbraceUpload?
Expand Down Expand Up @@ -140,9 +145,8 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
init(options: Embrace.Options,
logControllable: LogControllable? = nil,
embraceStorage: EmbraceStorage? = nil) throws {
self.started = false
self.options = options

self.options = options
self.logLevel = options.logLevel

self.storage = try embraceStorage ?? Embrace.createStorage(options: options)
Expand All @@ -158,20 +162,32 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
self.sessionController = SessionController(storage: storage, upload: upload, config: config)
self.sessionLifecycle = Embrace.createSessionLifecycle(controller: sessionController)
self.metadata = MetadataHandler(storage: storage, sessionController: sessionController)
self.logController = logControllable ?? LogController(
storage: storage,
upload: upload,
controller: sessionController,
config: config
)

var logController: LogController?
if let logControllable = logControllable {
self.logController = logControllable
} else {
let controller = LogController(
storage: storage,
upload: upload,
controller: sessionController
)
logController = controller
self.logController = controller
}

super.init()

sessionController.sdkStateProvider = self
logController?.sdkStateProvider = self

// setup otel
EmbraceOTel.setup(spanProcessors: .processors(for: storage, export: options.export))
EmbraceOTel.setup(spanProcessors: .processors(for: storage, export: options.export, sdkStateProvider: self))
let logSharedState = DefaultEmbraceLogSharedState.create(
storage: self.storage,
controller: logController,
exporter: options.export?.logExporter
controller: self.logController,
exporter: options.export?.logExporter,
sdkStateProvider: self
)
EmbraceOTel.setup(logSharedState: logSharedState)
sessionLifecycle.setup()
Expand All @@ -183,6 +199,8 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
selector: #selector(onConfigUpdated),
name: .embraceConfigUpdated, object: nil
)

state = .initialized
}

/// Method used to start the Embrace SDK.
Expand All @@ -199,8 +217,8 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
sessionLifecycle.setup()

Embrace.synchronizationQueue.sync {
guard started == false else {
Embrace.logger.warning("Embrace was already started!")
guard state == .initialized else {
Embrace.logger.warning("The Embrace SDK can only be started once!")
return
}

Expand All @@ -213,12 +231,12 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
defer { processStartSpan.end() }

recordSpan(name: "emb-sdk-start", parent: processStartSpan, type: .performance) { _ in
started = true
state = .started

sessionLifecycle.start()
sessionLifecycle.startSession()
captureServices.install()

processingQueue.async { [weak self] in
self.processingQueue.async { [weak self] in

self?.captureServices.start()

Expand All @@ -242,9 +260,41 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
return self
}

/// Method used to stop the Embrace SDK from capturing and generating data.
/// - Throws: `EmbraceSetupError.invalidThread` if not called from the main thread.
/// - Note: This method won't do anything if the Embrace SDK was already stopped.
/// - Note: The SDK can't be started again once stopped.
/// - Returns: The `Embrace` client instance.
@discardableResult
@objc public func stop() throws -> Embrace {
guard Thread.isMainThread else {
throw EmbraceSetupError.invalidThread("Embrace must be stopped on the main thread")
}

Embrace.synchronizationQueue.sync {
guard state != .stopped else {
Embrace.logger.warning("Embrace was already stopped!")
return
}

guard state == .started else {
Embrace.logger.warning("Embrace was not started so it can't be stopped!")
return
}

state = .stopped

sessionLifecycle.stop()
sessionController.clear()
captureServices.stop()
}

return self
}

/// Returns the current session identifier, if any.
@objc public func currentSessionId() -> String? {
guard config == nil || config?.isSDKEnabled == true else {
guard isSDKEnabled else {
return nil
}

Expand All @@ -258,19 +308,30 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta

/// Forces the Embrace SDK to start a new session.
/// - Note: If there was a session running, it will be ended before starting a new one.
/// - Note: This method won't do anything if the SDK is stopped.
@objc public func startNewSession() {
guard isSDKEnabled else {
return
}

sessionLifecycle.startSession()
}

/// Force the Embrace SDK to stop the current session, if any.
/// Forces the Embrace SDK to stop the current session, if any.
/// - Note: This method won't do anything if the SDK is stopped.
@objc public func endCurrentSession() {
guard isSDKEnabled else {
return
}

sessionLifecycle.endSession()
}

/// Called every time the remote config changes
@objc private func onConfigUpdated() {
if let config = config {
Embrace.logger.limits = config.internalLogLimits

if !config.isSDKEnabled {
Embrace.logger.debug("SDK was disabled")
captureServices.stop()
Expand Down
11 changes: 11 additions & 0 deletions Sources/EmbraceCore/Internal/Embrace+EmbraceSDKStateProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright © 2025 Embrace Mobile, Inc. All rights reserved.
//

import EmbraceCommonInternal

extension Embrace: EmbraceSDKStateProvider {
public var isEnabled: Bool {
return isSDKEnabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Foundation
import EmbraceOTelInternal
import EmbraceStorageInternal
import EmbraceCommonInternal
import OpenTelemetrySdk

class DefaultEmbraceLogSharedState: EmbraceLogSharedState {
Expand All @@ -31,7 +32,8 @@ extension DefaultEmbraceLogSharedState {
static func create(
storage: EmbraceStorage,
controller: LogControllable,
exporter: LogRecordExporter? = nil
exporter: LogRecordExporter? = nil,
sdkStateProvider: EmbraceSDKStateProvider
) -> DefaultEmbraceLogSharedState {
var exporters: [LogRecordExporter] = [
StorageEmbraceLogExporter(
Expand All @@ -49,7 +51,7 @@ extension DefaultEmbraceLogSharedState {

return DefaultEmbraceLogSharedState(
config: DefaultEmbraceLoggerConfig(),
processors: .default(withExporters: exporters),
processors: .default(withExporters: exporters, sdkStateProvider: sdkStateProvider),
resourceProvider: ResourceStorageExporter(storage: storage)
)
}
Expand Down
18 changes: 5 additions & 13 deletions Sources/EmbraceCore/Internal/Logs/LogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class LogController: LogControllable {
private(set) weak var sessionController: SessionControllable?
private weak var storage: Storage?
private weak var upload: EmbraceLogUploader?
private weak var config: EmbraceConfig?

weak var sdkStateProvider: EmbraceSDKStateProvider?

var otel: EmbraceOTelBridge = EmbraceOTel() // var so we can inject a mock for testing

Expand All @@ -41,21 +42,12 @@ class LogController: LogControllable {
static let attachmentLimit: Int = 5
static let attachmentSizeLimit: Int = 1048576 // 1 MiB

private var isSDKEnabled: Bool {
guard let config = config else {
return true
}
return config.isSDKEnabled
}

init(storage: Storage?,
upload: EmbraceLogUploader?,
controller: SessionControllable,
config: EmbraceConfig?) {
controller: SessionControllable) {
self.storage = storage
self.upload = upload
self.sessionController = controller
self.config = config
}

func uploadAllPersistedLogs() {
Expand Down Expand Up @@ -156,7 +148,7 @@ class LogController: LogControllable {

extension LogController {
func batchFinished(withLogs logs: [LogRecord]) {
guard isSDKEnabled else {
guard sdkStateProvider?.isEnabled == true else {
return
}

Expand All @@ -175,7 +167,7 @@ extension LogController {

private extension LogController {
func send(batches: [LogsBatch]) {
guard isSDKEnabled else {
guard sdkStateProvider?.isEnabled == true else {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
import Foundation
import EmbraceOTelInternal
import EmbraceStorageInternal
import EmbraceCommonInternal
import OpenTelemetrySdk

extension Collection where Element == SpanProcessor {
static func processors(for storage: EmbraceStorage, export: OpenTelemetryExport?) -> [SpanProcessor] {
static func processors(
for storage: EmbraceStorage,
export: OpenTelemetryExport?,
sdkStateProvider: EmbraceSDKStateProvider
) -> [SpanProcessor] {
var processors: [SpanProcessor] = [
SingleSpanProcessor(
spanExporter: StorageSpanExporter(
options: .init(storage: storage),
logger: Embrace.logger
)
),
sdkStateProvider: sdkStateProvider
)
]

Expand Down
Loading

0 comments on commit 239a70f

Please sign in to comment.