How to use OpenTelemetry with F#
source link: https://phillipcarter.dev/posts/how-to-use-opentelemetry-with-fsharp/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
How to use OpenTelemetry with F#
Updated on Dec 3, 2021
9 min read
In this post I’ll skip ahead to the fun stuff and teach you how you can use OpenTelemetry with F#.
How to install OpenTelemetry
First, make sure you’re using the latest .NET SDK and create a new F# web app:
dotnet new webapp -lang F# -o FSharpOpenTelemetry
Now, install the OpenTelemetry package in the FSharpOpenTelemetry
directory:
dotnet add package OpenTelemetry
You can install a preview by providing an explicit version, but the stable version will work fine here.
Full sample
There’s a bit of code to wire up so that you can export to a backend without using a vendor SDK distribution. Here it is, all in Program.fs
:
open System
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open OpenTelemetry.Resources
open OpenTelemetry.Trace
let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs())
// substitute your backend config data here, I'm using Honeycomb
let honeycombEndpoint = "https://api.honeycomb.io:443"
let honeycombApiKey = "lol use an environment variable"
let honeycombDataset = "this can also live in appsettings if you prefer"
let serviceName = "FSharpOpenTelemetry"
// Configure an exporter with some important info:
//
// - endpoint stuff you might need (e.g., headers)
// - make sure the service name is set up
// - configure some automatic instrumentation
builder.Services.AddOpenTelemetryTracing(fun builder ->
builder
.AddSource(serviceName)
.AddOtlpExporter(fun otlpOptions ->
// this config code will depend on the backend you choose.
// it may require more stuff, less stuff, or just as much stuff!
otlpOptions.Endpoint <- Uri(honeycombEndpoint)
otlpOptions.Headers <-
$"x-honeycomb-team={honeycombApiKey},x-honeycomb-dataset={honeycombDataset}"
)
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName))
.AddAspNetCoreInstrumentation(fun opts ->
opts.RecordException <- true
)
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(fun o ->
o.SetDbStatementForText <- true
o.RecordException<- true
)
|> ignore
) |> ignore
// Start a tracer scoped to the service
let tracer = TracerProvider.Default.GetTracer(serviceName)
let rootHandler () =
task {
// Track the work done in the root HTTP handler
use span = tracer.StartActiveSpan("sleep span")
span.SetAttribute("duration_ms", 100) |> ignore
do! Task.Delay(100)
return "Hello World!"
}
// Add the handler to the root route using .NET 6 APIs!
let app = builder.Build()
app.MapGet("/", Func<unit, Task<string>>(rootHandler)) |> ignore
app.Run()
There’s a lot more you can fiddle around with:
- Loading configuration from
appsettings.json
file - Configuring the right API keys
- Configuring more automatic instrumentation
But in general, this is what you can expect some of your code to look like.
This code will generate a trace with two spans:
- A span generated by ASP.NET Core automatic instrumentation for the inbound request
- A child span of the automatically-created one that tracks the
Task.Delay
call
Not exactly a bunch of data, but it should be enough to get you familiar with tracking work in your own codebase.
Using a vendor SDK distribution (Honeycomb!)
I work for Honeycomb, an observability product. We have an SDK distributions for OTel that make config easier and lights up some more automatic instrumentation. Here’s what the same code can look like:
open System
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open OpenTelemetry.Trace
open Honeycomb.OpenTelemetry
let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs())
// Config values stored in appsettings.json, with environment variable overrides
// this includes the service name, which is injected for you
builder.Services.AddHoneycomb(builder.Configuration) |> ignore
let rootHandler (tracer: Tracer) = // this is injected by the honeycomb SDK distro
task {
use span = tracer.StartActiveSpan("sleep span")
span.SetAttribute("duration_ms", 100) |> ignore
do! Task.Delay(100)
return "Hello World!"
}
let app = builder.Build()
app.MapGet("/", Func<Tracer, Task<string>>(rootHandler)) |> ignore
app.Run()
Much nicer! This will more or less generate the same data, but things like redis instrumentation and a few others will also get automatically generated for me. I can also turn off specific pieces of automatic instrumentation if I prefer.
What about System.Diagnostics.DiagnosticsSource?
Oh boy.
In the early days of OpenTelemetry and .NET, a few things happened:
- The .NET team started baking OpenTelemetry concepts into System.Diagnostics.DiagnosticsSource APIs
- Microsoft engineers decided that this could be a solid foundation for tracing with OpenTelemetry
- It was decided that, for the sake familiarity with existing APIs, guidance would be that .NET developers should prefer to use things like
ActivitySource
andActivity
as a noun instead ofTracer
andSpan
That would make the code sample above look like this:
open System
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open OpenTelemetry.Trace
open Honeycomb.OpenTelemetry
let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs())
// Config values stored in appsettings.json, with environment variable overrides
builder.Services.AddHoneycomb(builder.Configuration) |> ignore
// This is the same thing as a Tracer in OTel
let activitySource = new ActivitySource(serviceName)
let rootHandler () =
task {
// Activities and Tags are Spans and Attributes
use activity = activitySource.StartActivity("sleepy span")
activity.SetTag("sleepy", "true") |> ignore
do! Task.Delay(100)
return "Hello World!"
}
let app = builder.Build()
// Update the Func<> wrapper's type here
app.MapGet("/", Func<unit, Task<string>>(rootHandler)) |> ignore
app.Run()
I personally don’t like this decision for a few reasons:
- It effectively violates to OpenTelemetry spec, since nouns and verbs are using an older .NET API that came before OpenTelemetry (even if it’s all compliant under the hood)
- There is no shared terminology with other languages, making it harder to collaborate with others on a team
- It feels like the decision was to optimize for legacy codebases and users
Additionally, System.Diagnostics.DiagnosticsSource
also has the concept of Baggage (a kind of metadata you can propagate across a trace), but the System.Diagnostics Baggage API today is not OpenTelemetry spec compliant. And so you must use the OpenTelemetry Baggage
API instead, otherwise things won’t work.
Will this all get resolved? Time will tell, but I would personally recommend that you use the OpenTelemetry nouns and verbs over System.Diagnostics.DiagnosticsSource
stuff if you can:
- This ensures you can look at anything else online related to OpenTelemetry and have a shared vocabulary
- Working in a polyglot codebase is easier since there’s no need to mentally translate nouns and verbs
- It feels good to be spec-compliant
If you stick with the official guidance, which is to use System.Diagnostics.DiagnosticsSource
, then we can’t be friends. I’m kidding; we can be friends.
Misc Q & A
Is OpenTelemetry stable?
Yes! Tracing with OTel is 1.0 and most SDKs are also 1.0 now with tracing. Traces are the most important data type, so go forth and instrument with it now!
Metrics are stable in the spec, but SDK support is still experimental-to-beta depending on the language.
Logs are still experimental, but that’s fine for now I guess.
.NET in particular is very good with OpenTelemetry. The .NET team and some other folks working at Microsoft have taken great care to bake in OTel concepts to .NET itself, and the packages are stable, well-tested, etc.
You can rely on OpenTelemetry for your production services.
What about using something other than Honeycomb?
Honeycomb isn’t the only vendor that supports OpenTelemetry data.
Some vendors still don’t support native OTLP ingest, so you might need to run the data through a proxy or exporter component that they provide to make it work.
And there are OSS exporters like for Zipkin that work out of the box if you want to self-host your telemetry data.
But you should really not be in the business of building out your own observability stack. It’s really hard tech.
Scoping with use
One gotcha with OpenTelemetry and span lifecycles is that when they’re disposed of (i.e., go out of scope), they end. THis is super convenient if you have one span tracking work for one function or method.
But if you create child spans in the same scope as a parent span, you might need to explicitly end them, otherwise they’ll continue to track work until they go out of scope, which you might not have intended!
let doSomeWork (tracer: Trace) =
task {
// track the work here
use parentSpan = tracer.StartActiveSpan("parent")
// do some work
do! Task.Delay(100)
// track some more work with a child span
use childSpan = tracer.StartActiveSpan("child")
doMoreWork(childSpan)
// If you don't call this, then childSpan will still track work in
// this function! That might be a bug, so end it here, or create the
// child span in the 'doMoreWork' function.
childSpan.End()
// do more work for the parent to track
do! Task.Delay
// the parent span goes out of scope and will then end now
}
Why the |> ignore
? Is there not an F#-friendly version to use instead?
I spent some time playing around with some little F# helpers to see if it was possible to make the API a little nicer to use from F#. Unfortunately, I couldn’t come up with anything I liked:
- Adding nicer overloads: Most routines return a type for fluent-style usage (even though that’s rarely done), and .NET can’t overload based on return type, so this isn’t possible.
- Adding
FSharp
-branded overloads: An alternative is to make overloads for everything (e.g.,SetAttributeFSharp
)that do nothing except ignore the return value, but this quickly felt silly, and I think it would raise a lot more eyebrows than make F# developers happier. - Module-bound function helpers: Something like
Span.setAttribute key value span
function call, with maybe some way to chain calls with|>
pipelines ended up being kind of silly too. Chaining calls is actually quite rare, so it ends up being a wash in terms of number of characters typed - Wrapping core types to be F#-friendly: This is certainly possible (e.g.,
FSharpTelemetrySpan
and perhaps someFSharp
-branded new overloads for various things) – but it sucks. OpenTelemetry is designed to be as low-footprint as possible, and wrapping structs to re-create an API surface area all in the name of getting rid of some|> ignore
calls isn’t that great. - Computation expressions: I also experimented with a computation expression with some custom operations so that you could do something like this:
use span = tracer.StartActiveSpan("parent")
span {
set_attribute "key" "value"
add_event "some-event" DateTimeOffset.Now
} |> ignore
It was kinda neat, but also ended up being a wash in terms of the amount of code you need to type.
One thing that you could consider adding to a prelude somewhere in your codebase is this little type extension:
type Link with
static member New(spanContext: SpanContext) =
Link(&spanContext)
static member New(spanContext: SpanContext, spanAttributes: SpanAttributes) =
Link(&spanContext, spanAttributes)
This just makes some annoying inref<>
stuff easier to deal with. You might find little things like this to do too. Perhaps someone more creative than me will figure out a brilliant OpenTelemetry.FSharp
helper package.
Unfortunately, the .NET OpenTelemetry API is a little annoying to use from F#, and there aren’t any awesome solutions to make that any better. But it’s not the end of the world by any means.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK