Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
19 changes: 19 additions & 0 deletions android-agent/api/android-agent.api
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,37 @@ public final class io/opentelemetry/android/agent/dsl/HttpExportConfiguration {
public final fun spans (Lkotlin/jvm/functions/Function1;)V
}

public abstract interface class io/opentelemetry/android/agent/dsl/TracerProviderCustomizer {
public abstract fun customize (Lio/opentelemetry/sdk/trace/SdkTracerProviderBuilder;)Lio/opentelemetry/sdk/trace/SdkTracerProviderBuilder;
}

public abstract interface class io/opentelemetry/android/agent/dsl/LoggerProviderCustomizer {
public abstract fun customize (Lio/opentelemetry/sdk/logs/SdkLoggerProviderBuilder;)Lio/opentelemetry/sdk/logs/SdkLoggerProviderBuilder;
}

public abstract interface class io/opentelemetry/android/agent/dsl/MeterProviderCustomizer {
public abstract fun customize (Lio/opentelemetry/sdk/metrics/SdkMeterProviderBuilder;)Lio/opentelemetry/sdk/metrics/SdkMeterProviderBuilder;
}

public final class io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration {
public fun <init> ()V
public final fun diskBuffering (Lkotlin/jvm/functions/Function1;)V
public final fun getClock ()Lio/opentelemetry/sdk/common/Clock;
public final fun globalAttributes (Lkotlin/jvm/functions/Function0;)V
public final fun httpExport (Lkotlin/jvm/functions/Function1;)V
public final fun instrumentations (Lkotlin/jvm/functions/Function1;)V
public final fun otelSdkCustomizations (Lkotlin/jvm/functions/Function1;)V
public final fun resource (Lkotlin/jvm/functions/Function1;)V
public final fun session (Lkotlin/jvm/functions/Function1;)V
public final fun setClock (Lio/opentelemetry/sdk/common/Clock;)V
}

public final class io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpec {
public final fun customizeLoggerProvider (Lio/opentelemetry/android/agent/dsl/LoggerProviderCustomizer;)V
public final fun customizeMeterProvider (Lio/opentelemetry/android/agent/dsl/MeterProviderCustomizer;)V
public final fun customizeTracerProvider (Lio/opentelemetry/android/agent/dsl/TracerProviderCustomizer;)V
}

public final class io/opentelemetry/android/agent/dsl/SessionConfiguration {
public final fun getBackgroundInactivityTimeout-UwyO8pc ()J
public final fun getMaxLifetime-UwyO8pc ()J
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,44 @@ object OpenTelemetryRumInitializer {
cfg.resourceAction(resourceBuilder)
val resource = resourceBuilder.build()

return RumBuilder
.builder(ctx, cfg.rumConfig)
.setSessionProvider(createSessionProvider(ctx, cfg))
.setResource(resource)
.setClock(cfg.clock)
.addSpanExporterCustomizer {
OtlpHttpSpanExporter
.builder()
.setEndpoint(spansEndpoint.getUrl())
.setHeaders(spansEndpoint::getHeaders)
.setCompression(spansEndpoint.getCompression().getUpstreamName())
.build()
}.addLogRecordExporterCustomizer {
OtlpHttpLogRecordExporter
.builder()
.setEndpoint(logsEndpoints.getUrl())
.setHeaders(logsEndpoints::getHeaders)
.setCompression(logsEndpoints.getCompression().getUpstreamName())
.build()
}.addMetricExporterCustomizer {
OtlpHttpMetricExporter
.builder()
.setEndpoint(metricsEndpoint.getUrl())
.setHeaders(metricsEndpoint::getHeaders)
.setCompression(metricsEndpoint.getCompression().getUpstreamName())
.build()
}.build()
var builder =
RumBuilder
.builder(ctx, cfg.rumConfig)
.setSessionProvider(createSessionProvider(ctx, cfg))
.setResource(resource)
.setClock(cfg.clock)
.addSpanExporterCustomizer {
OtlpHttpSpanExporter
.builder()
.setEndpoint(spansEndpoint.getUrl())
.setHeaders(spansEndpoint::getHeaders)
.setCompression(spansEndpoint.getCompression().getUpstreamName())
.build()
}.addLogRecordExporterCustomizer {
OtlpHttpLogRecordExporter
.builder()
.setEndpoint(logsEndpoints.getUrl())
.setHeaders(logsEndpoints::getHeaders)
.setCompression(logsEndpoints.getCompression().getUpstreamName())
.build()
}.addMetricExporterCustomizer {
OtlpHttpMetricExporter
.builder()
.setEndpoint(metricsEndpoint.getUrl())
.setHeaders(metricsEndpoint::getHeaders)
.setCompression(metricsEndpoint.getCompression().getUpstreamName())
.build()
}
cfg.sdkCustomizations.tracerProviderCustomizers.forEach {
builder.addTracerProviderCustomizer { builder, _ -> it.customize(builder) }
}
cfg.sdkCustomizations.loggerProviderCustomizers.forEach {
builder.addLoggerProviderCustomizer { builder, _ -> it.customize(builder) }
}
cfg.sdkCustomizations.meterProviderCustomizers.forEach {
builder.addMeterProviderCustomizer { builder, _ -> it.customize(builder) }
}
return builder.build()
}

private fun Compression.getUpstreamName(): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class OpenTelemetryConfiguration internal constructor(
* Configures the [Clock] used for capturing telemetry.
*/
var clock: Clock = OtelAndroidClock(),
internal val sdkCustomizations: OtelSdkCustomizationsSpec = OtelSdkCustomizationsSpec(),
) {
internal val exportConfig = HttpExportConfiguration()
internal val sessionConfig = SessionConfiguration()
Expand Down Expand Up @@ -59,6 +60,14 @@ class OpenTelemetryConfiguration internal constructor(
rumConfig.setGlobalAttributes(action())
}

/**
* You can use this to customize additional nonstandard aspects of the OpenTelemetry SDK.
* Most users should not need to provide any customizations here.
*/
fun otelSdkCustomizations(action: OtelSdkCustomizationsSpec.() -> Unit) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add some sort of guarantee about configuration order here. For example, this could be the final action after all the other configuration (such as OTLP exporters) has been setup on the builder object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is important to consider. I think right now that the implementation does these last, but I'm not sure how to bake that into this contract.

sdkCustomizations.action()
}

/**
* Configures disk buffering behavior of exported telemetry.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.agent.dsl

import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder

@OpenTelemetryDslMarker
class OtelSdkCustomizationsSpec internal constructor() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth considering marking these new APIs as 'experimental' or 'incubating'? I suppose this is a more general question on what stability guarantees we want to provide for new APIs in modules that are otherwise stable?

My personal opinion is that marking a new API as experimental for a couple of versions is fairly normal on Android. Would be curious to hear others thoughts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a reasonably good idea to me. Open to ideas on how to actually do this in practice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Incubating annotation in android-agent is one way of achieving this. It's probably worth chatting about this at the SIG as it's a broader discussion of how we approach adding new APIs to otherwise stable modules

internal val tracerProviderCustomizers: MutableList<TracerProviderCustomizer> = mutableListOf()
internal val loggerProviderCustomizers: MutableList<LoggerProviderCustomizer> = mutableListOf()
internal val meterProviderCustomizers: MutableList<MeterProviderCustomizer> = mutableListOf()

/**
* Modify the creation of the OpenTelemetry TracerProvider by providing
* your own customizer here. The customizer is a function
* that receives an instance of [SdkTracerProviderBuilder] and returns an
* instance of [SdkTracerProviderBuilder], which should almost always be
* the same instance. If a new instance is returned, the operation can be
* destructive.
*/
fun customizeTracerProvider(customizer: TracerProviderCustomizer) {
tracerProviderCustomizers.add(customizer)
}

/**
* Modify the creation of the OpenTelemetry LoggerProvider by providing
* your own customizer here. The customizer is a function
* that receives an instance of [SdkLoggerProviderBuilder] and returns an
* instance of [SdkLoggerProviderBuilder], which should almost always be
* the same instance. If a new instance is returned, the operation can be
* destructive.
*/
fun customizeLoggerProvider(customizer: LoggerProviderCustomizer) {
loggerProviderCustomizers.add(customizer)
}

/**
* Modify the creation of the OpenTelemetry MeterProvider by providing
* your own customizer here. The customizer is a function
* that receives an instance of [SdkMeterProviderBuilder] and returns an
* instance of [SdkMeterProviderBuilder], which should almost always be
* the same instance. If a new instance is returned, the operation can be
* destructive.
*/
fun customizeMeterProvider(customizer: MeterProviderCustomizer) {
meterProviderCustomizers.add(customizer)
}
}

fun interface TracerProviderCustomizer {
fun customize(builder: SdkTracerProviderBuilder): SdkTracerProviderBuilder
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potential downside is that this does tie our API further to symbols declared in opentelemetry-java's SDK module. If we ever do decide to use opentelemetry-kotlin then this takes us further from that goal - but perhaps that's ok for now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this did occur to me as I was hacking on this, and I respect that you're especially sensitive to this (with KSP compat being an end goal). I'm not sure how to do things like this without some amount of dependency on java core for now....

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the benefits outweigh the downside here tbh.

}

fun interface LoggerProviderCustomizer {
fun customize(builder: SdkLoggerProviderBuilder): SdkLoggerProviderBuilder
}

fun interface MeterProviderCustomizer {
fun customize(builder: SdkMeterProviderBuilder): SdkMeterProviderBuilder
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So these types aren't completely necessary -- but I thought that it read a bit cleaner than using the underlying functional methods from the SDK. If we're worried about the increase in surface area, then we could probably just remove them and refer to the functions...but it's a bit verbose.

What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case you have in mind for these customizers in the initializer? They seem to be providing a huge hammer to me. If I'm not mistaken, they would allow users to override our processors and our exporters, so they might get into trouble by doing so and then seeing that sessions, disk buffering, and export parameters don't work as configured via the DSL options.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typealias might be an option here if you want a degree of separation but without creating an extra interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typealias might be an option here if you want a degree of separation but without creating an extra interface

Yes! Thank you. I think the typealias is considerably cleaner here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case you have in mind for these customizers in the initializer?

Good question. The ones that I've heard, and what was discussed in #1577, is the ability to add custom SpanProcessors into the sdk. Without some help from the initializer, the developer would need to use the OpenTelemetryRumBuilder directly, missing out on the benefits/convenience of the Initializer.

Another use case that has been brought up more than once is the ability to expire/swap HTTP exporter headers/tokens...so basically customizing the exporters. That's not covered here, but I think we will need to address it before too long.

They seem to be providing a huge hammer to me. If I'm not mistaken, they would allow users to override our processors and our exporters, so they might get into trouble by doing so and then seeing that sessions, disk buffering, and export parameters don't work as configured via the DSL options.

You're completely correct -- this is a big hammer! You're also right, that I think this allows users to break things that the agent tries to help with. I think this is already true of the upstream autoconfigure as well -- it's possible to ab/use the autoconfigure callbacks to undo some of the defaults (like breaking the BSP which is responsible for exporting, for example).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for the details.

The ones that I've heard, and what was discussed in #1577, is the ability to add custom SpanProcessors into the sdk.

This is fair enough. I believe we should be more specific with the api surface additions to make its evolution more iterative, though. I see the benefits of adding a big hammer like this one in terms of making it future-proof, but I'm afraid it can cause strange and difficult to address isssues down the line. Based on that, why don't we provide an API to add processors specifically instead?

Another use case that has been brought up more than once is the ability to expire/swap HTTP exporter headers/tokens...so basically customizing the exporters.

This should probably be a separate API that would allow to customize the exporters that we create in the agent. Not only to avoid providing a big hammer, but also because of how the Java SDK works. By adding a [Signal]ProviderCustomizer as proposed in this PR, I don't think that would help users to customize our exporters, as they can't access those via the Java SDK builders. So a specific exporter customizer seems to be the only feasible approach.

You're completely correct -- this is a big hammer! You're also right, that I think this allows users to break things that the agent tries to help with. I think this is already true of the upstream autoconfigure as well -- it's possible to ab/use the autoconfigure callbacks to undo some of the defaults (like breaking the BSP which is responsible for exporting, for example).

Don't get me wrong, I do not to like the idea of having to be some sort of police for our users and to tell them what they should and shouldn't do with their code. The thing is that experienced users can already do whatever they want to by using the rum builder, while being conscious that they might break things, which is why I believe that whoever decides to use the agent is expecting not only simplicity but also reliability, and it will become difficult to guarantee the reliability if we make it too flexible. Because of that, and to being able to provide better support for agent users when they run into trouble, I think it's best to increase the API surface to support concrete use cases in mind, such as allowing for custom processors in this case, and thus prevent other accidental changes.

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.agent.dsl

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.opentelemetry.android.agent.OpenTelemetryRumInitializer
import io.opentelemetry.sdk.trace.IdGenerator
import io.opentelemetry.sdk.trace.SdkTracerProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RuntimeEnvironment
import java.util.concurrent.atomic.AtomicBoolean

@RunWith(AndroidJUnit4::class)
class OtelSdkCustomizationsSpecTest {
@Test
fun `can configure tracer provider`() {
val traceId = "b9a654847cb3514b6e5bd80cb168ed1c"
val spanId = "0666666666666666"
val idGen =
object : IdGenerator {
override fun generateSpanId(): String? = spanId

override fun generateTraceId(): String? = traceId
}
val agent =
OpenTelemetryRumInitializer.initialize(
context = RuntimeEnvironment.getApplication(),
configuration = {
otelSdkCustomizations {
customizeTracerProvider { _ ->
SdkTracerProvider.builder().setIdGenerator(idGen)
}
}
},
)
val tracer =
agent.openTelemetry.tracerProvider
.tracerBuilder("test")
.build()
val span = tracer.spanBuilder("test").startSpan()
assertThat(span.spanContext.spanId).isEqualTo(spanId)
}

@Test
fun `can configure logger provider`() {
val seen = AtomicBoolean(false)
val agent =
OpenTelemetryRumInitializer.initialize(
context = RuntimeEnvironment.getApplication(),
configuration = {
otelSdkCustomizations {
customizeLoggerProvider { builder ->
builder.addLogRecordProcessor { _, _ ->
run {
seen.set(true)
}
}
}
}
},
)
val logger =
agent.openTelemetry.logsBridge
.loggerBuilder("test")
.build()
logger.logRecordBuilder().setBody("howdy").emit()
assertThat(seen.get()).isTrue
}

@Test
fun `can configure meter provider`() {
val seen = AtomicBoolean(false)
val agent =
OpenTelemetryRumInitializer.initialize(
context = RuntimeEnvironment.getApplication(),
configuration = {
otelSdkCustomizations {
customizeMeterProvider { builder ->
run {
seen.set(true)
builder
}
}
}
},
)
val counter =
agent.openTelemetry.meterProvider
.get("test")
.counterBuilder("test")
.build()
counter.add(1)
assertThat(seen.get()).isTrue
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import io.opentelemetry.api.logs.LogRecordBuilder
import io.opentelemetry.api.logs.LoggerProvider
import io.opentelemetry.api.metrics.LongCounter
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.context.Context
import io.opentelemetry.sdk.trace.ReadWriteSpan
import io.opentelemetry.sdk.trace.ReadableSpan
import io.opentelemetry.sdk.trace.SpanProcessor
import kotlin.random.Random

const val TAG = "otel.demo"

Expand All @@ -40,6 +45,18 @@ class OtelDemoApplication : Application() {
globalAttributes {
Attributes.of(stringKey("toolkit"), "jetpack compose")
}
otelSdkCustomizations {
customizeTracerProvider { builder ->
builder.addSpanProcessor(OtelDemoCustomSpanProcessor())
}
customizeLoggerProvider { builder ->
builder.addLogRecordProcessor { _, logRecord ->
// onEmit()
logRecord.setAttribute(stringKey("awesome"), "sauce-" + Random.nextInt())
}
}
customizeMeterProvider { builder -> builder /* customize as you like! */ }
}
}
)
Log.d(TAG, "RUM session started: " + rum?.getRumSessionId())
Expand Down Expand Up @@ -69,3 +86,20 @@ class OtelDemoApplication : Application() {
}
}
}

class OtelDemoCustomSpanProcessor: SpanProcessor {
override fun onStart(
parentContext: Context,
span: ReadWriteSpan
) {
span.setAttribute("my.custom.attribute", Random.nextLong())
}

override fun isStartRequired(): Boolean = true

override fun isEndRequired(): Boolean = false

override fun onEnd(span: ReadableSpan) {
//no-op
}
}
Loading