Skip to content

Commit

Permalink
refactor: support otel export only flag
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Dec 5, 2024
1 parent 84b1649 commit b76da76
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 41 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Currently, only Spans and Logs are supported, but other signals will be added in
- Doing bytecode instrumentation to enable the capture of certain telemetry.
2. For multi-module projects, in the Gradle files of modules you want to invoke Embrace SDK API methods, add a dependency to the main Embrace SDK module: `'io.embrace:embrace-android-sdk:<version>`.
3. In the `main` directory of your app's root source folder (i.e. `app/src/main/`), add in a file called `embrace-config.json` that contains `{}` as its only line.
- Add `sdk_config.otel_export_only: true` to the JSON object
- To further configure the SDK, additional attributes can be added to this configuration file.
- See our [configuration documentation page](https://embrace.io/docs/android/features/configuration-file/) for further details.
4. In your app's Gradle properties file, add in the entry `embrace.disableMappingFileUpload=true`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,4 @@ interface ConfigService {
* The Embrace app ID. This is used to identify the app within the database.
*/
val appId: String?

/**
* Whether only OTel exporters should be used. If this returns true,
* the SDK should avoid enabling unnecessary systems (such as anything that creates requests
* to Embrace).
*/
fun isOnlyUsingOtelExporters(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal class ConfigServiceImpl(
openTelemetryCfg: OpenTelemetryConfiguration,
preferencesService: PreferencesService,
suppliedFramework: AppFramework,
instrumentedConfig: InstrumentedConfig,
private val instrumentedConfig: InstrumentedConfig,
remoteConfig: RemoteConfig?,
thresholdCheck: BehaviorThresholdCheck = BehaviorThresholdCheck { preferencesService.deviceIdentifier },
) : ConfigService {
Expand All @@ -50,16 +50,16 @@ internal class ConfigServiceImpl(

override val appId: String? = resolveAppId(instrumentedConfig.project.getAppId(), openTelemetryCfg)

override fun isOnlyUsingOtelExporters(): Boolean = appId.isNullOrEmpty()

/**
* Loads the build information from resources provided by the config file packaged within the application by Gradle at
* build-time.
*
* @return the local configuration
*/
fun resolveAppId(id: String?, openTelemetryCfg: OpenTelemetryConfiguration): String? {
require(!id.isNullOrEmpty() || openTelemetryCfg.hasConfiguredOtelExporters()) {
require(
!id.isNullOrEmpty() || openTelemetryCfg.hasConfiguredOtelExporters() || instrumentedConfig.enabledFeatures.isOtelExportOnly()
) {
"No appId supplied in embrace-config.json. This is required if you want to " +
"send data to Embrace, unless you configure an OTel exporter and add" +
" embrace.disableMappingFileUpload=true to gradle.properties."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal class ConfigModuleImpl(
private const val DEFAULT_READ_TIMEOUT_SECONDS = 60L
}

private val enabledFeatures = initModule.instrumentedConfig.enabledFeatures

override val okHttpClient by singleton {
Systrace.traceSynchronous("okhttp-client-init") {
OkHttpClient()
Expand All @@ -42,7 +44,7 @@ internal class ConfigModuleImpl(
}

override val combinedRemoteConfigSource: CombinedRemoteConfigSource? by singleton {
if (initModule.onlyOtelExportEnabled()) return@singleton null
if (enabledFeatures.isOtelExportOnly()) return@singleton null
CombinedRemoteConfigSource(
store = remoteConfigStore,
httpSource = lazy { checkNotNull(remoteConfigSource) },
Expand All @@ -63,7 +65,7 @@ internal class ConfigModuleImpl(
}

override val remoteConfigSource by singleton {
if (initModule.onlyOtelExportEnabled()) return@singleton null
if (enabledFeatures.isOtelExportOnly()) return@singleton null
OkHttpRemoteConfigSource(
okhttpClient = okHttpClient,
apiUrlBuilder = urlBuilder ?: return@singleton null,
Expand All @@ -79,7 +81,7 @@ internal class ConfigModuleImpl(
}

override val urlBuilder: ApiUrlBuilder? by singleton {
if (initModule.onlyOtelExportEnabled()) return@singleton null
if (enabledFeatures.isOtelExportOnly()) return@singleton null
Systrace.traceSynchronous("url-builder-init") {
EmbraceApiUrlBuilder(
deviceId = androidServicesModule.preferencesService.deviceIdentifier,
Expand All @@ -88,9 +90,4 @@ internal class ConfigModuleImpl(
)
}
}

private fun InitModule.onlyOtelExportEnabled(): Boolean {
instrumentedConfig.project.getAppId() ?: return true
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ internal class DeliveryModuleImpl(
override val deliveryTracer: DeliveryTracer? = null,
) : DeliveryModule {

private val enabledFeatures = initModule.instrumentedConfig.enabledFeatures
private val processIdProvider = { otelModule.openTelemetryConfiguration.processIdentifier }

override val payloadStore: PayloadStore? by singleton {
val configService = configModule.configService
if (configService.isOnlyUsingOtelExporters()) {
if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val deliveryService = deliveryService ?: return@singleton null
Expand All @@ -60,7 +61,7 @@ internal class DeliveryModuleImpl(
}

override val deliveryService: DeliveryService? by singleton {
deliveryServiceProvider?.invoke() ?: if (configModule.configService.isOnlyUsingOtelExporters()) {
deliveryServiceProvider?.invoke() ?: if (enabledFeatures.isOtelExportOnly()) {
null
} else {
EmbraceDeliveryService(
Expand All @@ -76,7 +77,7 @@ internal class DeliveryModuleImpl(
}

override val intakeService: IntakeService? by singleton {
if (configModule.configService.isOnlyUsingOtelExporters()) {
if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val payloadStorageService = payloadStorageService ?: return@singleton null
Expand All @@ -102,7 +103,7 @@ internal class DeliveryModuleImpl(
}

override val payloadCachingService: PayloadCachingService? by singleton {
if (configModule.configService.isOnlyUsingOtelExporters()) {
if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val payloadStore = payloadStore ?: return@singleton null
Expand All @@ -117,7 +118,7 @@ internal class DeliveryModuleImpl(
}

override val payloadStorageService: PayloadStorageService? by singleton {
payloadStorageServiceProvider?.invoke() ?: if (configModule.configService.isOnlyUsingOtelExporters()) {
payloadStorageServiceProvider?.invoke() ?: if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val location = StorageLocation.PAYLOAD.asFile(coreModule.context, initModule.logger)
Expand All @@ -132,7 +133,7 @@ internal class DeliveryModuleImpl(
}

override val cacheStorageService: PayloadStorageService? by singleton {
cacheStorageServiceProvider?.invoke() ?: if (configModule.configService.isOnlyUsingOtelExporters()) {
cacheStorageServiceProvider?.invoke() ?: if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val location = StorageLocation.CACHE.asFile(coreModule.context, initModule.logger)
Expand All @@ -147,7 +148,7 @@ internal class DeliveryModuleImpl(
}

override val requestExecutionService: RequestExecutionService? by singleton {
requestExecutionServiceProvider?.invoke() ?: if (configModule.configService.isOnlyUsingOtelExporters()) {
requestExecutionServiceProvider?.invoke() ?: if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val appId = configModule.configService.appId ?: return@singleton null
Expand Down Expand Up @@ -176,7 +177,7 @@ internal class DeliveryModuleImpl(
}

override val schedulingService: SchedulingService? by singleton {
if (configModule.configService.isOnlyUsingOtelExporters()) {
if (enabledFeatures.isOtelExportOnly()) {
null
} else {
val payloadStorageService = payloadStorageService ?: return@singleton null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ internal class ConfigServiceImplTest {
executor = BlockingScheduledExecutorService(blockingMode = false)
worker = BackgroundWorker(executor)
service = createService()
assertFalse(service.isOnlyUsingOtelExporters())
}

/**
Expand Down Expand Up @@ -160,7 +159,6 @@ internal class ConfigServiceImplTest {
cfg.addLogExporter(FakeLogRecordExporter())
val service = createService(config = cfg, appId = null)
assertNotNull(service)
assertTrue(service.isOnlyUsingOtelExporters())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import io.embrace.android.embracesdk.fakes.FakeDeliveryService
import io.embrace.android.embracesdk.fakes.FakeOpenTelemetryModule
import io.embrace.android.embracesdk.fakes.FakeRequestExecutionService
import io.embrace.android.embracesdk.fakes.behavior.FakeAutoDataCaptureBehavior
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule
import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule
import io.embrace.android.embracesdk.fakes.injection.FakeInitModule
import io.embrace.android.embracesdk.fakes.injection.FakeStorageModule
import io.embrace.android.embracesdk.fakes.injection.FakeWorkerThreadModule
import io.embrace.android.embracesdk.internal.config.instrumented.schema.InstrumentedConfig
import io.embrace.android.embracesdk.internal.session.orchestrator.V2PayloadStore
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
Expand All @@ -30,7 +33,11 @@ class DeliveryModuleImplTest {
@Before
fun setUp() {
configService = FakeConfigService()
val initModule = FakeInitModule()
createModule()
}

private fun createModule(cfg: InstrumentedConfig = FakeInstrumentedConfig()) {
val initModule = FakeInitModule(instrumentedConfig = cfg)
module = DeliveryModuleImpl(
FakeConfigModule(configService),
initModule,
Expand Down Expand Up @@ -62,7 +69,7 @@ class DeliveryModuleImplTest {

@Test
fun `test otel export only`() {
configService.onlyUsingOtelExporters = true
createModule(FakeInstrumentedConfig(enabledFeatures = FakeEnabledFeatureConfig(otelExportOnly = true)))
assertNotNull(module)
assertTrue(module.deliveryService is FakeDeliveryService)
assertNull(module.intakeService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ package io.embrace.android.embracesdk.internal.config.instrumented.schema
*/
interface EnabledFeatureConfig {

/**
* Whether the SDK is configured to send data to Embrace or should be used purely for OTel export.
*
* sdk_config.otel_export_only
*/
fun isOtelExportOnly(): Boolean = false

/**
* Gates Unity ANR capture.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package io.embrace.android.embracesdk.internal.config.instrumented.schema
*
* Currently this is not instrumented by the gradle plugin so the values won't change - that will
* be implemented in a future PR.
*
* IMPORTANT NOTE: these functions are only swazzled when the sdk_config.send_data_to_embrace is set to `true`.
*/
interface OtelLimitsConfig {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.embrace.android.embracesdk.testcases.features

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
import io.embrace.android.embracesdk.fakes.config.FakeProjectConfig
import io.embrace.android.embracesdk.testframework.IntegrationTestRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* Verifies the SDK can export OpenTelemetry spans without sending any data to Embrace.
*/
@RunWith(AndroidJUnit4::class)
internal class OtelExportOnlyTest {

private val otelOnlyConfig = FakeInstrumentedConfig(
project = FakeProjectConfig(appId = null),
enabledFeatures = FakeEnabledFeatureConfig(otelExportOnly = true)
)

@Rule
@JvmField
val testRule: IntegrationTestRule = IntegrationTestRule()

@Test
fun `only export OTel`() {
testRule.runTest(
instrumentedConfig = otelOnlyConfig,
testCaseAction = {
recordSession {
embrace.startSpan("test-span")?.stop()
embrace.logInfo("test-log")
}
},
assertAction = {
assertEquals(0, getSessionEnvelopes(0).size)
assertEquals(0, getLogEnvelopes(0).size)
},
otelExportAssertion = {
// span exported
val span = awaitSpanExport(1) {
it.name == "test-span"
}.single()
assertEquals("test-span", span.name)

// log exported
val log = awaitLogExport(1) {
true
}.single()
assertEquals("test-log", log.bodyValue?.value)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.embrace.android.embracesdk.testframework.actions.EmbraceOtelExportAsse
import io.embrace.android.embracesdk.testframework.actions.EmbracePayloadAssertionInterface
import io.embrace.android.embracesdk.testframework.actions.EmbracePreSdkStartInterface
import io.embrace.android.embracesdk.testframework.actions.EmbraceSetupInterface
import io.embrace.android.embracesdk.testframework.export.FilteredLogExporter
import io.embrace.android.embracesdk.testframework.export.FilteredSpanExporter
import io.embrace.android.embracesdk.testframework.server.FakeApiServer
import java.io.File
Expand Down Expand Up @@ -98,6 +99,7 @@ internal class IntegrationTestRule(
lateinit var preSdkStart: EmbracePreSdkStartInterface
private lateinit var otelAssertion: EmbraceOtelExportAssertionInterface
private lateinit var spanExporter: FilteredSpanExporter
private lateinit var logExporter: FilteredLogExporter
private lateinit var embraceImpl: EmbraceImpl
private lateinit var baseUrl: String

Expand Down Expand Up @@ -139,14 +141,16 @@ internal class IntegrationTestRule(
action = EmbraceActionInterface(setup, bootstrapper)
payloadAssertion = EmbracePayloadAssertionInterface(bootstrapper, apiServer)
spanExporter = FilteredSpanExporter()
otelAssertion = EmbraceOtelExportAssertionInterface(spanExporter)
logExporter = FilteredLogExporter()
otelAssertion = EmbraceOtelExportAssertionInterface(spanExporter, logExporter)

setupAction(setup)
with(setup) {
embraceImpl = EmbraceImpl(bootstrapper)
EmbraceHooks.setImpl(embraceImpl)
preSdkStartAction(preSdkStart)
embraceImpl.addSpanExporter(spanExporter)
embraceImpl.addLogRecordExporter(logExporter)

// persist config here before the SDK starts up
persistConfig(persistedRemoteConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package io.embrace.android.embracesdk.testframework.actions

import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.testframework.export.ExportedSpanValidator
import io.embrace.android.embracesdk.testframework.export.FilteredLogExporter
import io.embrace.android.embracesdk.testframework.export.FilteredSpanExporter
import io.opentelemetry.sdk.logs.data.LogRecordData
import io.opentelemetry.sdk.trace.data.SpanData

/**
Expand All @@ -11,6 +13,7 @@ import io.opentelemetry.sdk.trace.data.SpanData
*/
internal class EmbraceOtelExportAssertionInterface(
private val spanExporter: FilteredSpanExporter,
private val logExporter: FilteredLogExporter,
private val validator: ExportedSpanValidator = ExportedSpanValidator(),
) {

Expand All @@ -22,6 +25,26 @@ internal class EmbraceOtelExportAssertionInterface(
return spanExporter.awaitSpansWithType(type, expectedCount)
}

/**
* Retrieves spans that match the provided filter and waits until they are exported or a timeout is reached
*/
fun awaitSpanExport(
expectedCount: Int,
filter: (SpanData) -> Boolean = { true },
): List<SpanData> {
return spanExporter.awaitSpanExport(expectedCount, filter)
}

/**
* Retrieves logs that match the provided filter and waits until they are exported or a timeout is reached
*/
fun awaitLogExport(
expectedCount: Int,
filter: (LogRecordData) -> Boolean = { true },
): List<LogRecordData> {
return logExporter.awaitLogExport(expectedCount, filter)
}

/**
* Asserts that the provided spans match the golden file.
*/
Expand Down
Loading

0 comments on commit b76da76

Please sign in to comment.