Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Embrace client stop() API #158

Merged
merged 5 commits into from
Jan 21, 2025
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
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
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
Loading