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

handle logger creation failures #52

Merged
merged 19 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
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
95 changes: 76 additions & 19 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,30 @@ object Capture {
fieldProviders: List<FieldProvider> = listOf(),
dateProvider: DateProvider? = null,
apiUrl: HttpUrl = defaultCaptureApiUrl,
) {
configure(
apiKey,
sessionStrategy,
configuration,
fieldProviders,
dateProvider,
apiUrl,
CaptureJniLibrary,
)
}

@Synchronized
@JvmStatic
@JvmOverloads
@Suppress("SwallowedException")
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 +147,28 @@ object Capture {
return
}

// If the logger has already been configured, do nothing.
if (default.get() != null) {
Log.w("capture", "Attempted to initialize Capture more than once")
return
// Ideally we would use `getAndUpdate` in here but it's available for API 24 and up only.
when (default.getAndSet(LoggerState.ConfigurationStarted)) {
is LoggerState.NotConfigured -> {
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")
Augustyniak marked this conversation as resolved.
Show resolved Hide resolved
default.set(LoggerState.ConfigurationFailure)
}
} else -> {
Log.w("capture", "Attempted to initialize Capture more than once")
}
}

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 +390,7 @@ object Capture {
*/
@JvmStatic
fun log(httpRequestInfo: HttpRequestInfo) {
default.get()?.log(httpRequestInfo)
logger()?.log(httpRequestInfo)
}

/**
Expand All @@ -344,7 +401,7 @@ object Capture {
*/
@JvmStatic
fun log(httpResponseInfo: HttpResponseInfo) {
default.get()?.log(httpResponseInfo)
logger()?.log(httpResponseInfo)
}
}
}
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 @@ -30,7 +30,7 @@ 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 +45,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 +65,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,102 @@
// 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.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(),
)
}
}
Loading
Loading