wolfgang ziegler


„make stuff and blog about it“

OpenTelemetry. NET in Blazor WebAssembly

March 6, 2021

OpenTelemetry and Blazor are technologies that I have been following closely over the last months and years. The first one professionally, the second one mostly out of personal interest.

Logos of OpenTelemetry and Blazor

Since the promise of Blazor is to run arbitrary .NET code in the browser, I figured why not give this a try and take OpenTelemetry .NET for a spin in a Blazor WebAssembly app. Admittedly, this is a bit of an unusual operation scenario for OpenTelemetry. NET, which is mainly designed for server apps. But running it inside a browser as a WebAssembly application is at least an interesting challenge.

Create the Blazor Application

Even on Linux, this has come down to these command-line statements.

mkdir OTelBlazor
cd OTelBlazor
dotnet new blazorwasm
dotnet run

Open https://localhost:5001 in your browser and a Blazor WebAssembly application is ready to be tried out.

Screenshot of the Blazor sample app's main page

Start a Tracing Backend

For collecting and visualizing the traces generated with OpenTelemetry and our Blazor application we will use Jaeger, because it:

  • is free, and open-source
  • runs on Windows, Mac and Linux
  • supports a range of input protocols and formats for trace data.

So, let's:

  • download Jager here
  • run ./jaeger-all-in-one
  • open it in a browser at http://localhost:16686

But before we can see any traces in the Jaeger UI we actually have to add OpenTelemetry to the Blazor application.

Add OpenTelemetry .NET

First, we need to pull in a few NuGet packages for OpenTelemetry.

dotnet add package OpenTelemetry --version 1.0.1
dotnet add package OpenTelemetry.Api --version 1.0.1
dotnet add package OpenTelemetry.OpenTelemetry.Exporter.Zipkin --version 1.0.1

Note: at the time of writing this blog post the version 1.0.1 was the latest release of OpenTelemetry .NET.

With these packages pulled in, let's extend the Counter.razor page with tracing instructions so that each time, the clicks the Click me button, a trace will be generated and sent to Jaeger.

The page with the counter button

This is the code for Counter.razor page:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@using OpenTelemetry;
@using OpenTelemetry.Resources
@using OpenTelemetry.Trace
@using System.Diagnostics

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        Trace(currentCount++);
    }

    private void Trace(int currentCount)
    {
      using var openTelemetry = Sdk.CreateTracerProviderBuilder()
              .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("blazor-otel"))
              .AddSource("BlazorUI")
              .AddZipkinExporter(o =>
              {
                  o.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
                  o.ExportProcessorType = ExportProcessorType.Simple;
              })
              .Build();
      using var source = new ActivitySource("BlazorUI");
      using (var activity = source.StartActivity("Click", ActivityKind.Client))
      {
          activity?.AddEvent(new ActivityEvent("The count button was clicked"));

          activity?.SetTag("count", currentCount);
          activity?.SetTag("mouse.x", 123);
          activity?.SetTag("mouse.y", 2323);
      }
    }
}

A few things are important here:

  • We call AddZipkinExporter which - as the name suggests - exports the traces via the Zipkin protocol. This seems strange at first, because Zipkin is actually a different tracing backend, but:
    • the Jaeger Exporter for OpenTelemetry .NET only uses UDP at the moment which does not work in Blazor / WebAssembly applications.
    • Jaeger also supports the Zipkin protocol, so this exporter is an obvious choice.
  • We specifically set ExportProcessorType.Simple because the default is ExportProcessorType.Batch which makes use of the .NET Threading API, which again is not available to Blazor apps.
  • The rest is mostly default OpenTelemetry .NET tracing code which produces a
    • service called blazor-otel
    • span (Activity) with the name BlazorUI that has
      • an event (visualized later as log by Jaeger)
      • some attributes (here: Tags) which we will see later in Jaeger.

When running the Blazor application and pushing the Click me button we will face our first disappointment and see the following error log message in the browser console.

Error due to unsupported .NET API usage

Apparently the usage of the class System.Diagnostics.Process is the culprit since this is not available to Blazor Apps.

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: The type initializer for 'OpenTelemetry.Resources.ResourceBuilder' threw an exception.
System.TypeInitializationException: The type initializer for 'OpenTelemetry.Resources.ResourceBuilder' threw an exception.
 ---> System.PlatformNotSupportedException: System.Diagnostics.Process is not supported on this platform.
   at System.Diagnostics.Process.GetCurrentProcess()
   at OpenTelemetry.Resources.ResourceBuilder..cctor() in /home/z1c0/dev/otel/opentelemetry-dotnet/src/OpenTelemetry/Resources/ResourceBuilder.cs:line 33
   --- End of inner exception stack trace ---
   at OpenTelemetry.Trace.TracerProviderBuilderSdk..ctor() in /home/z1c0/dev/otel/opentelemetry-dotnet/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs:line 34
   at OpenTelemetry.Sdk.CreateTracerProviderBuilder() in /home/z1c0/dev/otel/opentelemetry-dotnet/src/OpenTelemetry/Sdk.cs:line 63
   at OTelBlazor.Pages.Counter.Trace(Int32 currentCount) in /home/z1c0/dev/OTelBlazor/Pages/Counter.razor:line 25
   at OTelBlazor.Pages.Counter.IncrementCount() in /home/z1c0/dev/OTelBlazor/Pages/Counter.razor:line 19
...

This is unfortunate, but let's be pragmatic here and make ...

A few Adaptions

Let's clone the OpenTelemetry .NET GitHub repository and fix this.

git clone https://github.com/open-telemetry/opentelemetry-dotnet.git

Open the file ResourceBuilder.cs and change the property DefaultResource like this, so that the System.Diagnostics.Process code is commented out.

private static Resource DefaultResource { get; } = new Resource(new Dictionary<string, object>
{
  [ResourceSemanticConventions.AttributeServiceName] = "unknown_service"
    //+ (string.IsNullOrWhiteSpace(System.Diagnostics.Process.GetCurrentProcess().ProcessName)
    //? string.Empty :
    //":" + System.Diagnostics.Process.GetCurrentProcess().ProcessName),
});

To save you time: there's another class (ResourceBuilderExtensions) using a Blazor-unsupported API. Let's change this as well, namely the property TelemetryResource where we need to comment usage of FileVersionInfo.

  //private static readonly string FileVersion = FileVersionInfo.GetVersionInfo(typeof(Resource).Assembly.Location).FileVersion;

  private static Resource TelemetryResource { get; } = new Resource(new Dictionary<string, object>
  {
    [ResourceSemanticConventions.AttributeTelemetrySdkName] = "opentelemetry",
    [ResourceSemanticConventions.AttributeTelemetrySdkLanguage] = "dotnet",
    //[ResourceSemanticConventions.AttributeTelemetrySdkVersion] = FileVersion,
  });

These (admittedly hacky) changes neither affect the build nor the functionality of our sample. So let's rebuild and the only thing left to do is making use of our custom OpenTelemetry .NET assemblies now instead of the NuGet packages.

The OTelBlazor.csproj should now look similar to this, depending on the path to your locally build assemblies.

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.3" PrivateAssets="all" />
    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="OpenTelemetry">
      <HintPath>/REPLACE_WITH_YOUR_PATH/OpenTelemetry.dll</HintPath>
    </Reference>
    <Reference Include="OpenTelemetry.Api">
      <HintPath>/REPLACE_WITH_YOUR_PATH/OpenTelemetry.Api.dll</HintPath>
    </Reference>
    <Reference Include="OpenTelemetry.Exporter.Zipkin">
      <HintPath>/REPLACE_WITH_YOUR_PATH/OpenTelemetry.Exporter.Zipkin.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

Almost there

Since these basic impediments are cleared, now is a good time to launch Jaeger, with the Zipkin endpoint enabled (this is not on by default). Also, we add a few arguments to prevent problems with CORS that would otherwise prevent us from exporting trace data.

./jaeger-all-in-one \
  --collector.zipkin.host-port=localhost:9411 \
  --collector.zipkin.allowed-origins="*" \
  --collector.zipkin.allowed-headers="*"

Pushing the Click me button in the Blazor app now should not trigger any more error messages. Keep a close watch on the log window.

No more errors in the Blazor app

So, let's move over to Jaeger and - lo and behold - a service named otel-blazor appears in the dropdown.

The otel-blazor service appears in Jaeger

If we take a look at the traces associated with this service, we immediately recognize the data generated by our button click.

A span with the data sent by the Blazor app

Nice! But is this useful?

The could-should meme

Well, at least it was an interesting challenge and experiment, but I'm sure there is a lot of untapped potential here when it comes to real user monitoring and connecting user interactions on Blazor pages with transactions in services and the backend.

I will probably raise some GitHub issues for file PRs for the issues I had to workaround now and then we will see if this triggers discussion and thoughts towards Blazor, WebAssembly and OpenTelemetry.

Note: The full Blazor sample (with the adapted OpenTelemetry .NET assemblies) is available here on GitHub.