diff --git a/android-agent/build.gradle.kts b/android-agent/build.gradle.kts index f97104d07..226059d6d 100644 --- a/android-agent/build.gradle.kts +++ b/android-agent/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":session")) implementation(project(":services")) implementation(libs.opentelemetry.exporter.otlp) + implementation(libs.opentelemetry.semconv.incubating) // Default instrumentations: api(project(":instrumentation:activity")) diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt index 9b5cc5ecb..f0a130407 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt @@ -6,10 +6,13 @@ package io.opentelemetry.android.agent import android.app.Application +import io.opentelemetry.android.AndroidResource import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder import io.opentelemetry.android.agent.connectivity.EndpointConnectivity import io.opentelemetry.android.agent.connectivity.HttpEndpointConnectivity +import io.opentelemetry.android.agent.metrics.FilteredResource +import io.opentelemetry.android.agent.metrics.MetricsConfig import io.opentelemetry.android.agent.session.SessionConfig import io.opentelemetry.android.agent.session.SessionIdTimeoutHandler import io.opentelemetry.android.agent.session.SessionManager @@ -32,7 +35,11 @@ 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.instrumentation.api.instrumenter.AttributesExtractor +import io.opentelemetry.sdk.metrics.InstrumentSelector +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder +import io.opentelemetry.sdk.metrics.View import java.time.Duration +import java.util.function.BiFunction object OpenTelemetryRumInitializer { /** @@ -45,6 +52,7 @@ object OpenTelemetryRumInitializer { * @param logEndpointConnectivity Log-specific endpoint configuration. * @param metricEndpointConnectivity Metric-specific endpoint configuration. * @param rumConfig Configuration used by [OpenTelemetryRumBuilder]. + * @param metricsConfig Configures which Attributes and Resource Attributes to include on metrics. * @param sessionConfig The session configuration, which includes inactivity timeout and maximum lifetime durations. * @param activityTracerCustomizer Tracer customizer for [ActivityLifecycleInstrumentation]. * @param activityNameExtractor Name extractor for [ActivityLifecycleInstrumentation]. @@ -77,6 +85,7 @@ object OpenTelemetryRumInitializer { ), rumConfig: OtelRumConfig = OtelRumConfig(), sessionConfig: SessionConfig = SessionConfig.withDefaults(), + metricsConfig: MetricsConfig = MetricsConfig.withDefaults(), activityTracerCustomizer: ((Tracer) -> Tracer)? = null, activityNameExtractor: ScreenNameExtractor? = null, fragmentTracerCustomizer: ((Tracer) -> Tracer)? = null, @@ -118,9 +127,37 @@ object OpenTelemetryRumInitializer { .setEndpoint(metricEndpointConnectivity.getUrl()) .setHeaders(metricEndpointConnectivity::getHeaders) .build() - }.build() + }.addMeterProviderCustomizer(createMetricsFilter(metricsConfig)) + .build() } + private fun createMetricsFilter(config: MetricsConfig): BiFunction = + BiFunction { builder, app -> + if (config.isEmpty()) { + builder + } else { + if (config.getMetricResourceKeysToInclude().isNotEmpty()) { + val resource = AndroidResource.createDefault(app) + val filteredResource = + FilteredResource(resource, config.getMetricResourceKeysToInclude()) + builder.setResource(filteredResource.get()) + } + if (config.getMetricAttributesToInclude().isNotEmpty()) { + builder.registerView( + InstrumentSelector + .builder() + .setName("*") // match all instruments + .build(), + View + .builder() + .setAttributeFilter(config.getMetricAttributesToInclude()) + .build(), + ) + } + builder + } + } + private fun createSessionProvider( application: Application, sessionConfig: SessionConfig, diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/FilteredResource.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/FilteredResource.kt new file mode 100644 index 000000000..5bbc5a2a1 --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/FilteredResource.kt @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.metrics + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.AttributeType +import io.opentelemetry.sdk.resources.Resource +import io.opentelemetry.sdk.resources.ResourceBuilder + +internal class FilteredResource( + private val resource: Resource, + private val includeKeys: Set, +) { + fun get(): Resource { + val builder = Resource.builder().setSchemaUrl(resource.schemaUrl) + resource.attributes.forEach { key, value -> + if (wantKey(key)) { + put(builder, key, value) + } + } + return builder.build() + } + + private fun put( + builder: ResourceBuilder, + key: AttributeKey<*>, + value: Any, + ) { + when (key.type) { + AttributeType.STRING -> + builder.put( + key as AttributeKey, + value as String, + ) + + AttributeType.LONG -> builder.put(key as AttributeKey, value as Long) + AttributeType.DOUBLE -> + builder.put( + key as AttributeKey, + value as Double, + ) + + AttributeType.BOOLEAN -> + builder.put( + key as AttributeKey, + value as Boolean, + ) + + AttributeType.STRING_ARRAY -> + builder.put( + key as AttributeKey>, + value as List, + ) + + AttributeType.LONG_ARRAY -> + builder.put( + key as AttributeKey>, + value as List, + ) + + AttributeType.DOUBLE_ARRAY -> + builder.put( + key as AttributeKey>, + value as List, + ) + + AttributeType.BOOLEAN_ARRAY -> + builder.put( + key as AttributeKey>, + value as List, + ) + } + } + + private fun wantKey(key: AttributeKey<*>): Boolean = includeKeys.contains(key.key) +} diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/MetricsConfig.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/MetricsConfig.kt new file mode 100644 index 000000000..4f5906928 --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/metrics/MetricsConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.metrics + +import io.opentelemetry.semconv.ServiceAttributes +import io.opentelemetry.semconv.incubating.OsIncubatingAttributes +import java.util.Collections.unmodifiableSet + +class MetricsConfig { + private val metricResourceKeysToInclude: MutableSet = HashSet() + private val metricAttributesToInclude: MutableSet = HashSet() + + fun includeMetricResourceAttributes(vararg keys: String): MetricsConfig { + metricResourceKeysToInclude.addAll(listOf(*keys)) + return this + } + + fun includeMetricAttributeKeys(vararg keys: String): MetricsConfig { + metricAttributesToInclude.addAll(listOf(*keys)) + return this + } + + fun getMetricResourceKeysToInclude(): Set = unmodifiableSet(metricResourceKeysToInclude) + + fun getMetricAttributesToInclude(): Set = unmodifiableSet(metricAttributesToInclude) + + fun hasMetricResourceKeysToInclude(): Boolean = metricResourceKeysToInclude.isEmpty() + + fun isEmpty(): Boolean = metricAttributesToInclude.isEmpty() && metricResourceKeysToInclude.isEmpty() + + companion object { + fun withDefaults(): MetricsConfig = + MetricsConfig() +// .includeMetricAttributeKeys("tbd") + .includeMetricResourceAttributes( + ServiceAttributes.SERVICE_NAME.key, + ServiceAttributes.SERVICE_VERSION.key, + OsIncubatingAttributes.OS_NAME.key, + OsIncubatingAttributes.OS_TYPE.key, + OsIncubatingAttributes.OS_VERSION.key, + ) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/metrics/FilteredResourceTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/metrics/FilteredResourceTest.kt new file mode 100644 index 000000000..2754e400e --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/metrics/FilteredResourceTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.metrics + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.sdk.resources.Resource +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class FilteredResourceTest { + @Test + fun `test filter`() { + val original = + Resource + .builder() + .setSchemaUrl("http://foo.bar.com") + .put("string", "string") + .put("skipme1", "xxx") + .put("long", 21L) + .put("double", 21.111) + .put("boolean", true) + .put("skipme2", true) + .put("string.array", "foo", "bar") + .put("long.array", 67, 68, 69) + .put("double.array", 1.1, 2.2, 3.3) + .put("bool.array", true, false, true) + .build() + val wantKeys = setOf("string", "long", "double", "boolean", "string.array", "long.array", "double.array", "bool.array") + val filteredResource = FilteredResource(original, wantKeys) + val result = filteredResource.get() + assertThat(result.getAttribute(AttributeKey.stringKey("string"))).isEqualTo("string") + assertThat(result.getAttribute(AttributeKey.longKey("long"))).isEqualTo(21L) + assertThat(result.getAttribute(AttributeKey.doubleKey("double"))).isEqualTo(21.111) + assertThat(result.getAttribute(AttributeKey.booleanKey("boolean"))).isTrue() + assertThat(result.getAttribute(AttributeKey.stringArrayKey("string.array"))).isEqualTo(listOf("foo", "bar")) + assertThat(result.getAttribute(AttributeKey.longArrayKey("long.array"))).isEqualTo(listOf(67L, 68L, 69L)) + assertThat(result.getAttribute(AttributeKey.doubleArrayKey("double.array"))).isEqualTo(listOf(1.1, 2.2, 3.3)) + assertThat(result.getAttribute(AttributeKey.booleanArrayKey("bool.array"))).isEqualTo(listOf(true, false, true)) + assertThat(result.getAttribute(AttributeKey.stringKey("skipme1"))).isNull() + assertThat(result.getAttribute(AttributeKey.booleanKey("skipme2"))).isNull() + } +} diff --git a/core/src/main/java/io/opentelemetry/android/AndroidResource.java b/core/src/main/java/io/opentelemetry/android/AndroidResource.java deleted file mode 100644 index c4ee93c79..000000000 --- a/core/src/main/java/io/opentelemetry/android/AndroidResource.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android; - -import static io.opentelemetry.android.common.RumConstants.RUM_SDK_VERSION; -import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; -import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MANUFACTURER; -import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MODEL_IDENTIFIER; -import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MODEL_NAME; -import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_DESCRIPTION; -import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_NAME; -import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_TYPE; -import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_VERSION; - -import android.app.Application; -import android.os.Build; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.ResourceBuilder; -import java.util.function.Supplier; - -final class AndroidResource { - - static Resource createDefault(Application application) { - String appName = readAppName(application); - ResourceBuilder resourceBuilder = - Resource.getDefault().toBuilder().put(SERVICE_NAME, appName); - - return resourceBuilder - .put(RUM_SDK_VERSION, BuildConfig.OTEL_ANDROID_VERSION) - .put(DEVICE_MODEL_NAME, Build.MODEL) - .put(DEVICE_MODEL_IDENTIFIER, Build.MODEL) - .put(DEVICE_MANUFACTURER, Build.MANUFACTURER) - .put(OS_NAME, "Android") - .put(OS_TYPE, "linux") - .put(OS_VERSION, Build.VERSION.RELEASE) - .put(OS_DESCRIPTION, getOSDescription()) - .build(); - } - - private static String readAppName(Application application) { - return trapTo( - () -> { - int stringId = - application.getApplicationContext().getApplicationInfo().labelRes; - return application.getApplicationContext().getString(stringId); - }, - "unknown_service:android"); - } - - private static String trapTo(Supplier fn, String defaultValue) { - try { - return fn.get(); - } catch (Exception e) { - return defaultValue; - } - } - - private static String getOSDescription() { - StringBuilder osDescriptionBuilder = new StringBuilder(); - return osDescriptionBuilder - .append("Android Version ") - .append(Build.VERSION.RELEASE) - .append(" (Build ") - .append(Build.ID) - .append(" API level ") - .append(Build.VERSION.SDK_INT) - .append(")") - .toString(); - } -} diff --git a/core/src/main/java/io/opentelemetry/android/AndroidResource.kt b/core/src/main/java/io/opentelemetry/android/AndroidResource.kt new file mode 100644 index 000000000..f946dece6 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/AndroidResource.kt @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android + +import android.app.Application +import android.os.Build +import io.opentelemetry.android.common.RumConstants +import io.opentelemetry.sdk.resources.Resource +import io.opentelemetry.semconv.ServiceAttributes +import io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes +import io.opentelemetry.semconv.incubating.OsIncubatingAttributes + +object AndroidResource { + @JvmStatic + fun createDefault(application: Application): Resource { + val appName = readAppName(application) + val appVersion = readAppVersion(application) + val resourceBuilder = + Resource.getDefault().toBuilder().put(ServiceAttributes.SERVICE_NAME, appName) + if (appVersion != null) { + resourceBuilder.put(ServiceAttributes.SERVICE_VERSION, appVersion) + } + + return resourceBuilder + .put(RumConstants.RUM_SDK_VERSION, BuildConfig.OTEL_ANDROID_VERSION) + .put(DeviceIncubatingAttributes.DEVICE_MODEL_NAME, Build.MODEL) + .put(DeviceIncubatingAttributes.DEVICE_MODEL_IDENTIFIER, Build.MODEL) + .put(DeviceIncubatingAttributes.DEVICE_MANUFACTURER, Build.MANUFACTURER) + .put(OsIncubatingAttributes.OS_NAME, "Android") + .put(OsIncubatingAttributes.OS_TYPE, "linux") + .put(OsIncubatingAttributes.OS_VERSION, Build.VERSION.RELEASE) + .put(OsIncubatingAttributes.OS_DESCRIPTION, oSDescription) + .build() + } + + private fun readAppName(application: Application): String = + try { + val stringId = + application.applicationContext.applicationInfo.labelRes + application.applicationContext.getString(stringId) + } catch (_: Exception) { + "unknown_service:android" + } + + private fun readAppVersion(application: Application): String? { + val ctx = application.applicationContext + return try { + val packageInfo = ctx.packageManager.getPackageInfo(ctx.packageName, 0) + packageInfo.versionName + } catch (_: Exception) { + null + } + } + + private val oSDescription: String + get() { + val osDescriptionBuilder = StringBuilder() + return osDescriptionBuilder + .append("Android Version ") + .append(Build.VERSION.RELEASE) + .append(" (Build ") + .append(Build.ID) + .append(" API level ") + .append(Build.VERSION.SDK_INT) + .append(")") + .toString() + } +} diff --git a/core/src/test/java/io/opentelemetry/android/AndroidResourceTest.java b/core/src/test/java/io/opentelemetry/android/AndroidResourceTest.java index 1e1fe6462..2d3112cf9 100644 --- a/core/src/test/java/io/opentelemetry/android/AndroidResourceTest.java +++ b/core/src/test/java/io/opentelemetry/android/AndroidResourceTest.java @@ -7,6 +7,7 @@ import static io.opentelemetry.android.common.RumConstants.RUM_SDK_VERSION; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_VERSION; import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MANUFACTURER; import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MODEL_IDENTIFIER; import static io.opentelemetry.semconv.incubating.DeviceIncubatingAttributes.DEVICE_MODEL_NAME; @@ -19,6 +20,7 @@ import android.app.Application; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.os.Build; import io.opentelemetry.sdk.resources.Resource; import org.junit.jupiter.api.Test; @@ -31,6 +33,7 @@ class AndroidResourceTest { String appName = "robotron"; + String appVersion = "1.2.3"; String rumSdkVersion = BuildConfig.OTEL_ANDROID_VERSION; String osDescription = new StringBuilder() @@ -47,18 +50,24 @@ class AndroidResourceTest { Application app; @Test - void testFullResource() { + void testFullResource() throws Exception { ApplicationInfo appInfo = new ApplicationInfo(); appInfo.labelRes = 12345; + PackageInfo packageInfo = new PackageInfo(); + packageInfo.versionName = "1.2.3"; when(app.getApplicationContext().getApplicationInfo()).thenReturn(appInfo); when(app.getApplicationContext().getString(appInfo.labelRes)).thenReturn(appName); + when(app.getApplicationContext().getPackageName()).thenReturn("mypackage"); + when(app.getApplicationContext().getPackageManager().getPackageInfo("mypackage", 0)) + .thenReturn(packageInfo); Resource expected = Resource.getDefault() .merge( Resource.builder() .put(SERVICE_NAME, appName) + .put(SERVICE_VERSION, appVersion) .put(RUM_SDK_VERSION, rumSdkVersion) .put(DEVICE_MODEL_NAME, Build.MODEL) .put(DEVICE_MODEL_IDENTIFIER, Build.MODEL) diff --git a/demo-app/collector.yaml b/demo-app/collector.yaml index 64ffc19cb..6b94f2c12 100644 --- a/demo-app/collector.yaml +++ b/demo-app/collector.yaml @@ -18,6 +18,9 @@ service: traces: receivers: [otlp] exporters: [debug/detailed, otlphttp] + metrics: + receivers: [otlp] + exporters: [debug/detailed] logs: receivers: [otlp] exporters: [debug/detailed] diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt index 978da3735..85c741554 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt @@ -50,6 +50,8 @@ class OtelDemoApplication : Application() { // This is needed to get R8 missing rules warnings. initializeOtelWithGrpc() + + createAliveCounter() } // This is not used but it's needed to verify that our consumer proguard rules cover this use case. @@ -69,6 +71,21 @@ class OtelDemoApplication : Application() { } } + // A simple counter that merely logs the number of seconds that the app has been + // alive. This is simply used to demonstrate the metrics signal before other + // meaningful metrics have been created. + private fun createAliveCounter() { + val startTime = System.currentTimeMillis() + rum?.openTelemetry + ?.getMeter("android.lifetime") + ?.counterBuilder("app.uptime.seconds") + ?.setDescription("The number of seconds the app has been alive.") + ?.setUnit("s") + ?.buildWithCallback { + measurement -> measurement.record((System.currentTimeMillis() - startTime)/1000) + } + } + companion object { var rum: OpenTelemetryRum? = null