Skip to content

Commit

Permalink
handle logger creation failures (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
Augustyniak authored Oct 1, 2024
1 parent a9faf8f commit 751653c
Show file tree
Hide file tree
Showing 22 changed files with 449 additions and 146 deletions.
2 changes: 1 addition & 1 deletion examples/swift/hello_world/LoggerCustomer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class LoggerCustomer: NSObject, URLSessionDelegate {
configuration: .init(),
fieldProviders: [CustomFieldProvider()],
apiURL: kBitdriftURL
)
)?
.enableIntegrations([.urlSession()], disableSwizzling: true)

Logger.addField(withKey: "field_container_field_key", value: "field_container_value")
Expand Down
97 changes: 79 additions & 18 deletions platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,46 @@ import okhttp3.HttpUrl
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration

internal sealed class LoggerState {
/**
* The logger has not yet been configured.
*/
data object NotConfigured : LoggerState()

/**
* The logger has been successfully configured and is ready for use. Subsequent attempts to configure the logger will be ignored.
*/
class Configured(val logger: LoggerImpl) : LoggerState()

/**
* The configuration has started but is not yet complete. Subsequent attempts to configure the logger will be ignored.
*/
data object ConfigurationStarted : LoggerState()

/**
* The configuration was attempted but failed. Subsequent attempts to configure the logger will be ignored.
*/
data object ConfigurationFailure : LoggerState()
}

/**
* Top level namespace Capture SDK.
*/
object Capture {
private val default: AtomicReference<LoggerImpl?> = AtomicReference(null)
private val default: AtomicReference<LoggerState> = AtomicReference(LoggerState.NotConfigured)

/**
* Returns a handle to the underlying logger instance, if Capture has been configured.
*
* @return ILogger a logger handle
*/
fun logger(): ILogger? {
return default.get()
return when (val state = default.get()) {
is LoggerState.NotConfigured -> null
is LoggerState.Configured -> state.logger
is LoggerState.ConfigurationStarted -> null
is LoggerState.ConfigurationFailure -> null
}
}

/**
Expand Down Expand Up @@ -82,6 +109,29 @@ object Capture {
fieldProviders: List<FieldProvider> = listOf(),
dateProvider: DateProvider? = null,
apiUrl: HttpUrl = defaultCaptureApiUrl,
) {
configure(
apiKey,
sessionStrategy,
configuration,
fieldProviders,
dateProvider,
apiUrl,
CaptureJniLibrary,
)
}

@Synchronized
@JvmStatic
@JvmOverloads
internal fun configure(
apiKey: String,
sessionStrategy: SessionStrategy,
configuration: Configuration = Configuration(),
fieldProviders: List<FieldProvider> = listOf(),
dateProvider: DateProvider? = null,
apiUrl: HttpUrl = defaultCaptureApiUrl,
bridge: IBridge,
) {
// Note that we need to use @Synchronized to prevent multiple loggers from being initialized,
// while subsequent logger access relies on volatile reads.
Expand All @@ -96,22 +146,26 @@ object Capture {
return
}

// If the logger has already been configured, do nothing.
if (default.get() != null) {
// Ideally we would use `getAndUpdate` in here but it's available for API 24 and up only.
if (default.compareAndSet(LoggerState.NotConfigured, LoggerState.ConfigurationStarted)) {
try {
val logger = LoggerImpl(
apiKey = apiKey,
apiUrl = apiUrl,
fieldProviders = fieldProviders,
dateProvider = dateProvider ?: SystemDateProvider(),
configuration = configuration,
sessionStrategy = sessionStrategy,
bridge = bridge,
)
default.set(LoggerState.Configured(logger))
} catch (e: Throwable) {
Log.w("capture", "Capture initialization failed", e)
default.set(LoggerState.ConfigurationFailure)
}
} else {
Log.w("capture", "Attempted to initialize Capture more than once")
return
}

val logger = LoggerImpl(
apiKey = apiKey,
apiUrl = apiUrl,
fieldProviders = fieldProviders,
dateProvider = dateProvider ?: SystemDateProvider(),
configuration = configuration,
sessionStrategy = sessionStrategy,
)

default.set(logger)
}

/**
Expand Down Expand Up @@ -333,7 +387,7 @@ object Capture {
*/
@JvmStatic
fun log(httpRequestInfo: HttpRequestInfo) {
default.get()?.log(httpRequestInfo)
logger()?.log(httpRequestInfo)
}

/**
Expand All @@ -344,7 +398,14 @@ object Capture {
*/
@JvmStatic
fun log(httpResponseInfo: HttpResponseInfo) {
default.get()?.log(httpResponseInfo)
logger()?.log(httpResponseInfo)
}

/**
* Used for testing purposes.
*/
internal fun resetShared() {
default.set(LoggerState.NotConfigured)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface StackTraceProvider {
}

@Suppress("UndocumentedPublicClass")
internal object CaptureJniLibrary {
internal object CaptureJniLibrary : IBridge {

/**
* Loads the shared library. This is safe to call multiple times.
Expand All @@ -49,7 +49,7 @@ internal object CaptureJniLibrary {
* @param preferences the preferences storage to use for persistent storage of simple settings and configuration.
* @param errorReporter the error reporter to use for reporting error to bitdrift services.
*/
external fun createLogger(
external override fun createLogger(
sdkDirectory: String,
apiKey: String,
sessionStrategy: SessionStrategyConfiguration,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture

import io.bitdrift.capture.error.IErrorReporter
import io.bitdrift.capture.network.ICaptureNetwork
import io.bitdrift.capture.providers.session.SessionStrategyConfiguration

internal interface IBridge {
fun createLogger(
sdkDirectory: String,
apiKey: String,
sessionStrategy: SessionStrategyConfiguration,
metadataProvider: IMetadataProvider,
resourceUtilizationTarget: IResourceUtilizationTarget,
eventsListenerTarget: IEventsListenerTarget,
applicationId: String,
applicationVersion: String,
network: ICaptureNetwork,
preferences: IPreferences,
errorReporter: IErrorReporter,
): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ internal class LoggerImpl(
private val apiClient: OkHttpApiClient = OkHttpApiClient(apiUrl, apiKey),
private var deviceCodeService: DeviceCodeService = DeviceCodeService(apiClient),
private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager,
private val bridge: IBridge = CaptureJniLibrary,
) : ILogger {

private val metadataProvider: MetadataProvider
Expand Down Expand Up @@ -144,7 +145,7 @@ internal class LoggerImpl(
processingQueue,
)

this.loggerId = CaptureJniLibrary.createLogger(
val loggerId = bridge.createLogger(
sdkDirectory,
apiKey,
sessionStrategy.createSessionStrategyConfiguration { appExitSaveCurrentSessionId(it) },
Expand All @@ -164,6 +165,10 @@ internal class LoggerImpl(
localErrorReporter,
)

check(loggerId != -1L) { "initialization of the rust logger failed" }

this.loggerId = loggerId

runtime = JniRuntime(this.loggerId)
diskUsageMonitor.runtime = runtime

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ import org.robolectric.annotation.Config
@Config(sdk = [21])
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class CaptureTest {

// This Test needs to run first since the following tests need to initialize
// the ContextHolder before they can run.
@Test
fun a_configure_skips_logger_creation_when_context_not_initialized() {
fun aConfigureSkipsLoggerCreationWhenContextNotInitialized() {
assertThat(Capture.logger()).isNull()

Logger.configure(
Expand All @@ -45,7 +44,7 @@ class CaptureTest {
// Accessing fields prior to the configuration of the logger may lead to crash since it can
// potentially call into a native method that's used to sanitize passed url path.
@Test
fun b_does_not_access_fields_if_logger_not_configured() {
fun bDoesNotAccessFieldsIfLoggerNotConfigured() {
assertThat(Capture.logger()).isNull()

val requestInfo = HttpRequestInfo("GET", path = HttpUrlPath("/foo/12345"))
Expand All @@ -65,7 +64,7 @@ class CaptureTest {
}

@Test
fun c_idempotent_configure() {
fun cIdempotentConfigure() {
val initializer = ContextHolder()
initializer.create(ApplicationProvider.getApplicationContext())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture

import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.bitdrift.capture.providers.session.SessionStrategy
import org.assertj.core.api.Assertions
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21])
class ConfigurationTest {
@Test
fun configurationFailure() {
val initializer = ContextHolder()
initializer.create(ApplicationProvider.getApplicationContext())

val bridge: IBridge = mock {}
whenever(
bridge.createLogger(
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
),
).thenReturn(-1L)

// We start without configured logger.
Assertions.assertThat(Capture.logger()).isNull()

Capture.Logger.configure(
apiKey = "test1",
sessionStrategy = SessionStrategy.Fixed(),
dateProvider = null,
bridge = bridge,
)

// The configuration failed so the logger is still `null`.
Assertions.assertThat(Capture.logger()).isNull()

// We confirm that we actually tried to configure the logger.
verify(bridge, times(1)).createLogger(
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
)

// We perform another attempt to configure the logger to verify that
// consecutive configure calls are no-ops.
Capture.Logger.configure(
apiKey = "test1",
sessionStrategy = SessionStrategy.Fixed(),
dateProvider = null,
bridge = bridge,
)

Assertions.assertThat(Capture.logger()).isNull()

// We verify that the second configure call was a no-op.
verify(bridge, times(1)).createLogger(
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
)
}

@After
fun tearDown() {
Capture.Logger.resetShared()
}
}
Loading

0 comments on commit 751653c

Please sign in to comment.