United Kingdom: +44 (0)208 088 8978

OpenTelemetry with ASP.NET Core, F# and Azure

In part two of his blog series Ryan shows how you can get started on your observability journey using ASP.NET Core and the SAFE template.

We're hiring Software Developers

Click here to find out more

In part one of this blog about OpenTelemetry we considered its constituent parts and explored how it can help us instrument our applications for observability in a platform and vendor agnostic manner.

This time I am going to jump straight into a practical demonstration of how to get up and running with Open Telemetry and ASP .NET. We'll use F# with the SAFE Stack as our starting point, however you could easily port this example to other tools or languages in the ASP.NET ecosystem.

I'm going to keep it really simple. In particular, we are going to export our data directly to Application Insights using the preview Azure Monitor exporter.

When instrumenting a real app, the recommended approach is to export all of your data to the OpenTelemetry Collector. This acts as a vendor neutral ingestion, aggregation and pre-processing service before exporting to the monitoring solution of your choice.

For this demo I am going to be starting with the SAFE template version 3.1.1.

1. Import the required libraries

We're going to need to pull in a couple of packages from Nuget.

In the SAFE template we have Paket, so after restoring our tools with...

dotnet tool restore

...we can add the libraries to our Server project like so:

dotnet paket add OpenTelemetry.Extensions.Hosting -p Server
dotnet paket add OpenTelemetry.Instrumentation.AspNetCore -p Server
dotnet paket add Azure.Monitor.OpenTelemetry.Exporter -p Server

2. Add our Application Insights key to the app's settings

We will need a connection string in order to export to Application Insights. This could be obtained by hand from the Azure Portal after deployment, but the SAFE template comes with Farmer which makes it easy to handle settings in a declarative way.

If you open Build.fs in the Build project, you will find a FAKE script with an Azure target. This defines a simple web app deployment.

The web app comes with an Application Insights resource configured by default - an example of Farmer's approach of picking sensible starting points for you.

The Application Insights key will be loaded into our configuration at runtime with the lookup key of APPINSIGHTS_INSTRUMENTATIONKEY

Target.create "Azure" (fun _ ->
    let web = webApp {
        name "Open_Telemetry_Example"
        zip_deploy "deploy"
    let deployment = arm {
        location Location.WestEurope
        add_resource web

    |> Deploy.execute "Open_Telemetry_Example" Deploy.NoParameters
    |> ignore

You may have different values for your app name, depending on your solution title. If your deployment in the following step fails, carefully read the output error as this will guide you as to the reason, which is often an incompatible resource name.

3. Deploying to Azure App Service

Providing you have installed the prerequisites listed in the SAFE docs quickstart, including the Azure CLI with an Azure subscription set up, you can deploy your SAFE template Todo app to Azure by simply calling

dotnet run azure

4. Create an ActivitySource

Open up Server.fs in the Server project.

Firstly, we are going to need to define a couple of constants at the top of the module.

let APP_INSIGHTS_KEY = "APPINSIGHTS_INSTRUMENTATIONKEY" // has to be the key used by Farmer to store the setting when deploying the app to Azure

let SOURCE = "OTel.AzureMonitor.SAFE.Demo" // This can be anything you like

Secondly, we need an 'activity source' directly underneath which will be used by our application to manually add data to the OpenTelemetry pipeline.

This and related classes are all available out of the box from the .NET System.Diagnostics namespace.

open System.Diagnostics

let activitySource = new ActivitySource(SOURCE)

5. Configure services

For the next step we need a few more namespaces opened:

open OpenTelemetry.Trace
open Azure.Monitor.OpenTelemetry.Exporter
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection

We want to get a handle to our app settings and service collection in order to

  • Add our ASP.NET Core instrumentation, our ActivitySource and our Azure Monitor exporter to the OpenTelemetry Tracing service builder

  • Register the resulting Tracing service with our web application.

At the bottom of Server.fs, find the default application builder.

Replace it with this:

let configureServices (services : IServiceCollection) =
    let config = services.BuildServiceProvider().GetService<IConfiguration>()
    services.AddOpenTelemetryTracing(fun builder ->
            .AddAzureMonitorTraceExporter(fun options ->
                options.ConnectionString <- $"InstrumentationKey={config.Item APP_INSIGHTS_KEY};IngestionEndpoint=https://westeurope-2.in.applicationinsights.azure.com/" 
        |> ignore

let app =
    application {
        url ""
        use_router webApp
        service_config configureServices
        use_static "public"

6. Trace activity

Finally, let's use the ActivitySource to trace some behaviour.

An activity is disposable and represents an OpenTelemetry span, which is analogous to a unit of work. It can also have child and parent spans. These spans combine to form a trace.

In the following code, I start the Validating Todo activity from the Adding Todo activity, so they have a parent-child relationship. I have added some small delays to make this easier to see in the exported data.

  • Delete the stubbed Todos that come with the template, as they would be added before the OpenTelemetry services are ready.

  • Replace the entire Storage class with the following:

type Storage() =
    let todos = ResizeArray<_>()

    let getTodos = async {
        use activity = activitySource.StartActivity "Loading Todos"
        do! Async.Sleep 300
        return List.ofSeq todos

    let validateTodo todo = async {
        use activity = activitySource.StartActivity "Validating Todo"
        do! Async.Sleep 300
        return Todo.isValid todo

    member __.GetTodos() = async {
        use activity = activitySource.StartActivity "Getting Todos"
        do! Async.Sleep 200
        return! getTodos

    member __.AddTodo(todo: Todo) = async {

        use activity = activitySource.StartActivity "Adding Todo" 
        let activityOption = activity |> Option.ofObj

        do! Async.Sleep 200

        let! todoIsValid = validateTodo todo.Description

        if todoIsValid then

            todos.Add todo

            |> Option.map (fun act -> act.AddTag("Success", todo.Id))
            |> ignore

            return Ok()
            |> Option.map (fun act -> act.AddTag("Failure", todo.Id))
            |> ignore

            return Error "Invalid todo"

7. Redeploy

That's it! If you now redeploy your app using

dotnet run azure

as before, you will have OpenTelemetry tracing running and active in your application.

You can test this by visiting the Azure Portal, opening Application Insights, selecting the Performance menu option and exploring the calls to getTodos and addTodo.

You should see the activity logs that we put in place, including their run time with respect to the overall request.


The full code for this demo is available at our Github.

I hope this was a useful introduction into the world of OpenTelemetry and has encouraged you to explore its benefits. I am sure we are all going to be hearing a lot more about it in the near future!

As I mentioned at the top, this is really the bare minimum and there is a whole lot more to explore.

As a next step, perhaps check out this great demo from the recent dotnet conf where Marco Minerva and Andrea Tosato demonstrate the OpenTelemetry Collector with export to Zipkin and Prometheus.

Also check out these blogs from Sourabh Shirhatti at Microsoft and Gérald Barré for more ideas and information.

Those resources use C#, however with the knowledge from this article you will hopefully be able to transfer the ideas across to F# pretty easily.