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.
-
OpenTelemetry.Extensions.Hosting provides extensions which allow us to easily register services that hook our ASP.NET application into to the OpenTelemetry pipeline.
-
OpenTelemetry.Instrumentation.AspNetCore provides instrumentation which automatically collects relevant data from the ASP.NET Core framework.
-
Azure.Monitor.OpenTelemetry.Exporter allows us to export data from our OpenTelemetry pipeline directly to Application Insights.
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
}
deployment
|> 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.
[<Literal>]
let APP_INSIGHTS_KEY = "APPINSIGHTS_INSTRUMENTATIONKEY" // has to be the key used by Farmer to store the setting when deploying the app to Azure
[<Literal>]
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 ->
builder
.AddAspNetCoreInstrumentation()
.AddSource(SOURCE)
.AddAzureMonitorTraceExporter(fun options ->
options.ConnectionString <- $"InstrumentationKey={config.Item APP_INSIGHTS_KEY};IngestionEndpoint=https://westeurope-2.in.applicationinsights.azure.com/"
)
|> ignore
)
let app =
application {
url "http://0.0.0.0:8085"
use_router webApp
memory_cache
service_config configureServices
use_static "public"
use_gzip
}
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
Todo
s 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
activityOption
|> Option.map (fun act -> act.AddTag("Success", todo.Id))
|> ignore
return Ok()
else
activityOption
|> 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.
Conclusion
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.