Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<ItemGroup>
<ProjectReference Include="..\HelloLibrary\HelloLibrary.DotNet.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.13.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Properties\AssemblyInfo.cs" />
</ItemGroup>
Expand Down
76 changes: 73 additions & 3 deletions samples/HelloWorld/HelloWorld/MainActivity.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
using Android.App;
using System;
using Android.App;
using Android.Widget;
using Android.OS;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Reflection;

namespace HelloWorld
{
Expand All @@ -11,10 +18,19 @@ namespace HelloWorld
Name = "example.MainActivity")]
public class MainActivity : Activity
{
const string TypemapInstrumentationName = "Microsoft.Android.Runtime.TrimmableTypeMap";
const string OtlpHeadersExtra = "OTEL_EXPORTER_OTLP_HEADERS";

static readonly object TelemetryLock = new object ();
static TracerProvider tracerProvider;
static MeterProvider meterProvider;
static bool telemetryConfigured;

int count = 1;

protected override void OnCreate (Bundle savedInstanceState)
{
ConfigureTelemetry (Intent?.GetStringExtra (OtlpHeadersExtra));
base.OnCreate (savedInstanceState);

// Set our view from the "main" layout resource
Expand All @@ -28,7 +44,61 @@ protected override void OnCreate (Bundle savedInstanceState)
button.Text = string.Format ("{0} clicks!", count++);
};
}
}
}

static void ConfigureTelemetry (string otlpHeaders)
{
lock (TelemetryLock) {
if (telemetryConfigured)
return;

var endpoint = System.Environment.GetEnvironmentVariable ("OTEL_EXPORTER_OTLP_ENDPOINT");
if (string.IsNullOrEmpty (endpoint))
endpoint = "http://127.0.0.1:4318";

var resourceBuilder = ResourceBuilder.CreateDefault ()
.AddService ("helloworld-android");

tracerProvider = Sdk.CreateTracerProviderBuilder ()
.SetResourceBuilder (resourceBuilder)
.AddSource (TypemapInstrumentationName)
.AddOtlpExporter (options => {
options.Protocol = OtlpExportProtocol.HttpProtobuf;
options.Endpoint = GetOtlpEndpoint (endpoint, "v1/traces");
if (!string.IsNullOrEmpty (otlpHeaders))
options.Headers = otlpHeaders;
})
.Build ();

meterProvider = Sdk.CreateMeterProviderBuilder ()
.SetResourceBuilder (resourceBuilder)
.AddMeter (TypemapInstrumentationName)
.AddOtlpExporter (options => {
options.Protocol = OtlpExportProtocol.HttpProtobuf;
options.Endpoint = GetOtlpEndpoint (endpoint, "v1/metrics");
if (!string.IsNullOrEmpty (otlpHeaders))
options.Headers = otlpHeaders;
})
.Build ();

telemetryConfigured = true;
FlushBufferedTypemapEvents ();
}
}

static void FlushBufferedTypemapEvents ()
{
var telemetryType = typeof (Android.Runtime.JNIEnv).Assembly.GetType ("Microsoft.Android.Runtime.TrimmableTypeMapTelemetry");
var flushMethod = telemetryType?.GetMethod ("FlushBufferedEvents", BindingFlags.Static | BindingFlags.NonPublic);
flushMethod?.Invoke (null, null);
}

static Uri GetOtlpEndpoint (string endpoint, string signalPath)
{
var builder = new UriBuilder (endpoint);
var path = builder.Path;
if (string.IsNullOrEmpty (path) || path == "/")
builder.Path = signalPath;
return builder.Uri;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.xamarin.android.helloworld">
<application android:allowBackup="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:extractNativeLibs="false">
<uses-permission android:name="android.permission.INTERNET" />
<application android:allowBackup="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:extractNativeLibs="false" android:usesCleartextTraffic="true">
</application>
</manifest>
114 changes: 114 additions & 0 deletions samples/TypemapOtelAspire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Typemap OTEL Aspire sample

This AppHost launches `samples/HelloWorld` as a Release/CoreCLR/trimmable typemap Android app and sends typemap telemetry to the Aspire dashboard over OTLP/HTTP.

## What it measures

The runtime emits spans and metrics from the `Microsoft.Android.Runtime.TrimmableTypeMap` instrumentation source.

Important startup spans:

- `jnienv.initialize`: managed `JNIEnvInit.Initialize` startup boundary.
- `typemap.data.initialize`: generated trimmable typemap data load.
- `typemap.initialize`: runtime typemap initialization.
- `typemap.register_native_methods`: native registration setup.
- `typemap.buffered_events`: flush marker for operations buffered before the app configured OpenTelemetry.

Lookup and steady-state spans:

- `typemap.llvm.lookup.jni_name`
- `typemap.llvm.lookup.jni_name.uncached`
- `typemap.llvm.peer.create`
- `typemap.llvm.proxy.create`
- `typemap.llvm.activation`
- `typemap.lookup.jni_name`
- `typemap.lookup.jni_name.uncached`
- `typemap.lookup.managed_type`
- `typemap.lookup.managed_type.uncached`
- `typemap.lookup.java_object`
- `typemap.lookup.java_interfaces`
- `typemap.lookup.java_interfaces.uncached`
- `typemap.type_manager.get_simple_reference`
- `typemap.type_manager.get_simple_reference.uncached`
- `typemap.type_manager.get_invoker_type`
- `typemap.peer.create`
- `typemap.on_register_natives`

Each span also has a `duration.us` attribute for microsecond precision, because the Aspire CLI and dashboard duration fields are rounded to milliseconds.

## Prerequisites

- Build the local Android SDK first:

```bash
make all
```

- Connect exactly one Android device, or set `ANDROID_SERIAL`.
- Trust Aspire HTTPS dev certificates if needed:

```bash
dotnet dev-certs https --trust
aspire certs trust
```

## Run

Run from this directory:

```bash
export PATH="$HOME/.dotnet/tools:$PATH"
ASPIRE_ALLOW_UNSECURED_TRANSPORT=true \
ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318 \
aspire run --detach --non-interactive
```

By default the AppHost launches Release/CoreCLR/trimmable. Override the type map to compare against LLVM-IR:

```bash
ASPIRE_ALLOW_UNSECURED_TRANSPORT=true \
ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318 \
HELLOWORLD_ANDROID_TYPEMAP=llvm-ir \
aspire run --detach --non-interactive
```

The `helloworld-android` resource builds, installs, and starts the sample app. It also:

- runs `adb reverse tcp:4318 tcp:<dashboard-otlp-http-port>`;
- uninstalls any existing `com.xamarin.android.helloworld` app to avoid signature mismatch errors;
- discovers the dashboard-generated OTLP API key and passes it to the Activity as `OTEL_EXPORTER_OTLP_HEADERS`;
- keeps the Aspire resource alive while the Android app process is running.

Restart just the Android app resource after code changes:

```bash
aspire resource helloworld-android restart
```

## Inspect telemetry

The dashboard URL is printed by `aspire run`. You can also use the CLI:

```bash
aspire describe
aspire otel traces --format Json -n 100
aspire otel spans --format Json -n 500
aspire logs helloworld-android --tail 200
```

If the browser dashboard shows `An unhandled error has occurred` and the CLI still returns telemetry, clear site data/cache for the dashboard URL or open it in a fresh private window. One observed stale-browser failure was:

```text
Microsoft.JSInterop.JSException: The value 'Blazor._internal.Virtualize.setAnchorMode' is not a function.
```

## Notes

- `am start -W` only reports integer millisecond `TotalTime` and `WaitTime`.
- Use span attribute `duration.us` for microsecond duration data.
- Startup spans emitted before app-level OpenTelemetry setup are buffered and replayed as spans once the sample configures its provider.
- Do not leave Android diagnostics startup suspend enabled when using this sample normally:

```bash
adb shell setprop debug.mono.profile none
```
20 changes: 20 additions & 0 deletions samples/TypemapOtelAspire/apphost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#:sdk Aspire.AppHost.Sdk@13.3.5

var builder = DistributedApplication.CreateBuilder(args);

var otlpHttpEndpoint = Environment.GetEnvironmentVariable("ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL") ?? "http://localhost:4318";
var configuration = Environment.GetEnvironmentVariable("HELLOWORLD_ANDROID_CONFIGURATION") ?? "Release";
var runtime = Environment.GetEnvironmentVariable("HELLOWORLD_ANDROID_RUNTIME") ?? "CoreCLR";
var typemap = Environment.GetEnvironmentVariable("HELLOWORLD_ANDROID_TYPEMAP") ?? "trimmable";

builder.AddExecutable(
"helloworld-android",
"bash",
"../..",
"samples/TypemapOtelAspire/run-helloworld-android.sh")
.WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", otlpHttpEndpoint)
.WithEnvironment("HELLOWORLD_ANDROID_CONFIGURATION", configuration)
.WithEnvironment("HELLOWORLD_ANDROID_RUNTIME", runtime)
.WithEnvironment("HELLOWORLD_ANDROID_TYPEMAP", typemap);

builder.Build().Run();
20 changes: 20 additions & 0 deletions samples/TypemapOtelAspire/aspire.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"appHost": {
"path": "apphost.cs"
},
"profiles": {
"http": {
"applicationUrl": "http://localhost:15269",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:18888",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19001",
"ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:19002",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20156",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true",
"ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true"
}
}
}
}
Loading