open-telemetry / opentelemetry-android

OpenTelemetry Tooling for Android
Apache License 2.0
147 stars 33 forks source link

Question - Android kotlin send logs and metrics to otel automatically? #395

Closed wirthandras closed 4 months ago

wirthandras commented 4 months ago

Hi,

I integrated this lib into an android kotlin code base with the following gradle import:

//Telemetry
implementation("io.opentelemetry.android:android-agent:0.6.0-alpha")
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
// https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-exporter-otlp
implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.38.0'

And my startup code:

import android.app.Application
import android.util.Log
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.OpenTelemetryRumBuilder
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration
import io.opentelemetry.api.common.AttributeKey.stringKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.metrics.Meter
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter
import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader
import io.opentelemetry.sdk.resources.Resource
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import java.util.function.BiFunction

private const val TAG = "otel.demo"
class NavigatorApplication : Application() {

    private val dependencyInjectionModules = module {
        single { applicationContext }
        single { WayPointService() }
        single { LocalCachingService(get()) }
    }

    override fun onCreate() {
        super.onCreate()

        initializeTelemetry()

        startKoin {
            androidContext(this@NavigatorApplication)
            modules(dependencyInjectionModules)
        }
    }

    private fun initializeTelemetry() {
        // 10.0.2.2 is apparently a special binding to the host running the emulator
        val traceIngestUrl = "http://10.0.2.2:4318/v1/traces"
        val logIngestUrl = "http://10.0.2.2:4318/v1/logs"
        val metricsIngestUrl = "http://10.0.2.2:4318/v1/metrics"

        val resource = Resource.getDefault().toBuilder().build()

        Log.i(TAG, "Initializing the opentelemetry-android-agent")
        val diskBufferingConfig =
            DiskBufferingConfiguration.builder()
                .setEnabled(true)
                .setMaxCacheSize(10_000_000)
                .build()
        val config =
            OtelRumConfig()
                .setGlobalAttributes(Attributes.of(stringKey("toolkit"), "jetpack compose"))
                .setDiskBufferingConfiguration(diskBufferingConfig)

        val spanExporter = OtlpHttpSpanExporter.builder()
            .setEndpoint(traceIngestUrl)
            .build()

        val logRecordExporter = OtlpHttpLogRecordExporter.builder()
            .setEndpoint(logIngestUrl)
            .build()

        val metricExporter = OtlpHttpMetricExporter.builder()
            .setEndpoint(metricsIngestUrl)
            .build()

        val loggerProviderCustomizer: BiFunction<SdkLoggerProviderBuilder, Application, SdkLoggerProviderBuilder> =
            BiFunction { sdkLoggerProviderBuilder, _ ->
                sdkLoggerProviderBuilder.addLogRecordProcessor(BatchLogRecordProcessor.builder(logRecordExporter).build())
            }

        val meterProviderCustomizer: BiFunction<SdkMeterProviderBuilder, Application, SdkMeterProviderBuilder> =
            BiFunction { sdkMeterProviderBuilder, _ ->
                sdkMeterProviderBuilder.registerMetricReader(
                    PeriodicMetricReader.builder(metricExporter).build()
                )
            }

        val otelRumBuilder: OpenTelemetryRumBuilder =
            OpenTelemetryRum.builder(this, config)
                .addSpanExporterCustomizer {
                    spanExporter
                }
                .addTracerProviderCustomizer { tracerProviderBuilder: SdkTracerProviderBuilder, app: Application? ->
                    val otlpExporter = OtlpHttpSpanExporter.builder()
                        .setEndpoint(traceIngestUrl) // Set your collector endpoint here
                        .build()
                    val batchSpanProcessor = BatchSpanProcessor.builder(otlpExporter).build()
                    tracerProviderBuilder.addSpanProcessor(batchSpanProcessor)
                }
                .addLoggerProviderCustomizer(loggerProviderCustomizer)
                .addMeterProviderCustomizer(meterProviderCustomizer)

        try {
            rum = otelRumBuilder.build()
            Log.d(TAG, "RUM session started: " + rum!!.rumSessionId)

            val tracer: Tracer = rum?.openTelemetry?.tracerProvider?.get("example-tracer") ?: return
            val span = tracer.spanBuilder("example-span").startSpan()
            span.end()

            val logger = rum?.openTelemetry?.logsBridge?.get("example-logger") ?: return
            logger.logRecordBuilder()
                .setBody("This is a log message")
                .emit()

            val meter: Meter = rum?.openTelemetry?.meterProvider?.get("example-meter") ?: return
            val counter = meter.counterBuilder("example_counter_2").build()
            counter.add(1)
        } catch (e: Exception) {
            Log.e(TAG, "Oh no!", e)
        }
    }

    override fun onTerminate() {
        stopKoin()
        super.onTerminate()
    }

    companion object {
        var rum: OpenTelemetryRum? = null

        fun tracer(name: String): Tracer? {
            return rum?.openTelemetry?.tracerProvider?.get(name)
        }
    }
}

I can confirm the telemetry solution is working with directed statements (I mean about this part of code):

val tracer: Tracer = rum?.openTelemetry?.tracerProvider?.get("example-tracer") ?: return
            val span = tracer.spanBuilder("example-span").startSpan()
            span.end()

            val logger = rum?.openTelemetry?.logsBridge?.get("example-logger") ?: return
            logger.logRecordBuilder()
                .setBody("This is a log message")
                .emit()

            val meter: Meter = rum?.openTelemetry?.meterProvider?.get("example-meter") ?: return
            val counter = meter.counterBuilder("example_counter_2").build()
            counter.add(1)

So these direct statements landed on our jeager(Trace), prometheus(metric), and local file (log) output as expected.

But it seems to me all of the logs which can be seen in logcat (in android studio) are missing

I checked the following tickets: https://github.com/open-telemetry/opentelemetry-android/issues/302 https://github.com/open-telemetry/opentelemetry-android/issues/142 My implementation based on this: https://github.com/open-telemetry/opentelemetry-android/issues/326#issuecomment-2085330777

based on the example i tried to add instrumentation with new imports:

// https://mvnrepository.com/artifact/io.opentelemetry.android/instrumentation
    implementation 'io.opentelemetry.android:instrumentation:0.4.0-alpha'
import io.opentelemetry.android.instrumentation.InstrumentedApplication
import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker
import io.opentelemetry.android.instrumentation.lifecycle.AndroidLifecycleInstrumentationBuilder
import io.opentelemetry.android.instrumentation.startup.AppStartupTimer

.addInstrumentation { app: InstrumentedApplication ->
                    AndroidLifecycleInstrumentationBuilder()
                        .setVisibleScreenTracker(VisibleScreenTracker())
                        .setStartupTimer(AppStartupTimer()).build().installOn(app)
                }

but I got the following compile time error:

Type mismatch.
Required:
((io.opentelemetry.android.instrumentation.common.InstrumentedApplication!) → Unit)!
Found:
(io.opentelemetry.android.instrumentation.InstrumentedApplication) → Unit

What is the way to enrich my app to send logs and metrics with this code?

Shall I create something custom code which access metrics and logs and send via direct statments? Or shall I wait a next version of lib which cover this? Anything else? I am little bit unsure about the current situation since I saw in referenced tickets it should works https://github.com/open-telemetry/opentelemetry-android/issues/142#issuecomment-1795851676 https://github.com/open-telemetry/opentelemetry-android/issues/326#issuecomment-2082521963

Thanks in advance

LikeTheSalad commented 4 months ago

Hi!

If I understood correctly, you'd like to export the logs that are created using the Android Log class. Are you using the Android Log class directly in your code every time you create a log? Or are you using a wrapper tool, such as Timber for example?

wirthandras commented 4 months ago

I am using Android Log class

LikeTheSalad commented 4 months ago

I see, thank you. That use case would require an automatic instrumentation feature that is not currently available, although I think this isn't the first time I've seen a request to provide this feature, so I don't see why it can't be added in the future. Right now I'm focusing on an instrumentation refactor so I believe that adding new instrumentations should wait until that's done to avoid conflicts later on.

Though, in the meantime (if you wouldn't mind changing all the places in your code where you reference the Android Log class), you could use a wrapper that takes care of sending your logs to both places, the logcat and also OTel. I think this could work when using Timber for example (I haven't tested it though):

    Timber.plant(Timber.DebugTree()) // To show the logs in logcat
    Timber.plant(object : Timber.Tree() { // This tree will export the logs
        val logger = rum?.openTelemetry?.logsBridge?.get("example-logger")

        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
            logger?.logRecordBuilder()?.setBody(message)?.emit()
        }
    })

The downside is that you'd have to always create logs using timber, like so:Timber.d() instead of Log.d().

wirthandras commented 4 months ago

Ok thanks, I will keep an eye on the upcoming versions

breedx-splk commented 4 months ago

This also sounds like it's related to #142.