Skip to content
Open
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="FSharp.Core" Version="10.0.102" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.1" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.67.0" />
Expand Down
1 change: 1 addition & 0 deletions OpenTelemetry.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
<Folder Name="/examples/">
<Project Path="examples/AspNetCore/Examples.AspNetCore.csproj" />
<Project Path="examples/Console/Examples.Console.csproj" />
<Project Path="examples/FSharp/Examples.FSharp.fsproj" />
<Project Path="examples/GrpcService/Examples.GrpcService.csproj" />
</Folder>
<Folder Name="/examples/MicroserviceExample/">
Expand Down
60 changes: 60 additions & 0 deletions examples/FSharp/Controllers/WeatherForecastController.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

namespace Examples.AspNetCore.Controllers

open System
open System.Net.Http
open System.Security.Cryptography
open Examples.AspNetCore
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging

[<ApiController>]
[<Route("[controller]")>]
type WeatherForecastController(
httpClient: HttpClient,
instrumentationSource: InstrumentationSource,
logger: ILogger<WeatherForecastController>) =
inherit ControllerBase()

static let requestUri = Uri("http://example.com")
static let summaries =
[| "Freezing"; "Bracing"; "Chilly"; "Cool"; "Mild"; "Warm"; "Balmy"; "Hot"; "Sweltering"; "Scorching" |]

let activitySource = instrumentationSource.ActivitySource
let freezingDaysCounter = instrumentationSource.FreezingDaysCounter

[<HttpGet>]
member this.Get() = task {

// Making a http call here to serve as an example of
// how dependency calls will be captured and treated
// automatically as child of incoming request.
let _ = httpClient.GetStringAsync(requestUri) |> Async.AwaitTask

// Optional: Manually create an activity. This will become a child of
// the activity created from the instrumentation library for AspNetCore.
// Manually created activities are useful when there is a desire to track
// a specific subset of the request. In this example one could imagine
// that calculating the forecast is an expensive operation and therefore
// something to be distinguished from the overall request.
// Note: Tags can be added to the current activity without the need for
// a manual activity using Activity.Current?.SetTag()
use _ = activitySource.StartActivity("calculate forecast")

let forecast =
[| 1 .. 5 |]
|> Array.map (fun index ->
{ Date = DateTime.Now.AddDays(float index)
TemperatureC = RandomNumberGenerator.GetInt32(-20, 55)
Summary = summaries.[RandomNumberGenerator.GetInt32(summaries.Length)] })

// Optional: Count the freezing days
let freezingDays = forecast |> Array.filter (fun f -> f.TemperatureC < 0) |> Array.length
freezingDaysCounter.Add(int64 freezingDays)

logger.LogInformation("WeatherForecasts generated {Count}: {Forecasts}", forecast.Length, forecast)

return forecast :> WeatherForecast seq
}
29 changes: 29 additions & 0 deletions examples/FSharp/Examples.FSharp.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<Nullable>disable</Nullable>
<TargetFramework>$(DefaultTargetFrameworkForExampleApps)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="InstrumentationSource.fs" />
<Compile Include="WeatherForecast.fs" />
<Compile Include="Controllers/WeatherForecastController.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Core" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.AspNetCore\OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions examples/FSharp/InstrumentationSource.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

namespace Examples.AspNetCore

open System
open System.Diagnostics.Metrics
open System.Diagnostics

// It is recommended to use a custom type to hold references for
// ActivitySource and Instruments. This avoids possible type collisions
// with other components in the DI container.
type InstrumentationSource() =

static let activitySourceName = "Examples.AspNetCore"
static let meterName = "Examples.AspNetCore"

let version =
typeof<InstrumentationSource>.Assembly.GetName().Version
|> Option.ofObj
|> Option.map string
|> Option.toObj

let activitySource = new ActivitySource(activitySourceName, version)
let meter = new Meter(meterName, version)
let freezingDaysCounter =
meter.CreateCounter<int64>("weather.days.freezing", description = "The number of days where the temperature is below freezing")

member _.ActivitySource = activitySource
member _.FreezingDaysCounter = freezingDaysCounter
member _.MeterName = meterName

interface IDisposable with
member _.Dispose() =
activitySource.Dispose()
meter.Dispose()
155 changes: 155 additions & 0 deletions examples/FSharp/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

module Examples.AspNetCore.FSharp

open System
open System.Diagnostics.Metrics
open Examples.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open OpenTelemetry.Instrumentation.AspNetCore
open OpenTelemetry.Logs
open OpenTelemetry.Metrics
open OpenTelemetry.Resources
open OpenTelemetry.Trace

[<EntryPoint>]
let main args =
let instrumentationSource = new InstrumentationSource()
let appBuilder = WebApplication.CreateBuilder(args)

// Note: Switch between OTLP/Console by setting UseTracingExporter in appsettings.json.
let tracingExporter =
appBuilder.Configuration.GetValue("UseTracingExporter", defaultValue = "CONSOLE")
|> fun s -> s.ToUpperInvariant()

// Note: Switch between Prometheus/OTLP/Console by setting UseMetricsExporter in appsettings.json.
let metricsExporter =
appBuilder.Configuration.GetValue("UseMetricsExporter", defaultValue = "CONSOLE")
|> fun s -> s.ToUpperInvariant()

// Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json.
let logExporter =
appBuilder.Configuration.GetValue("UseLogExporter", defaultValue = "CONSOLE")
|> fun s -> s.ToUpperInvariant()

// Note: Switch between Explicit/Exponential by setting HistogramAggregation in appsettings.json
let histogramAggregation =
appBuilder.Configuration.GetValue("HistogramAggregation", defaultValue = "EXPLICIT")
|> fun s -> s.ToUpperInvariant()

// Create a service to expose ActivitySource, and Metric Instruments
// for manual instrumentation
appBuilder.Services.AddSingleton<InstrumentationSource>() |> ignore

// Add HttpClient to the service provider for dependency injection.
appBuilder.Services.AddHttpClient() |> ignore

// Clear default logging providers used by WebApplication host.
appBuilder.Logging.ClearProviders() |> ignore

// Configure OpenTelemetry logging, metrics, & tracing with auto-start using the
// AddOpenTelemetry extension from OpenTelemetry.Extensions.Hosting.
appBuilder.Services.AddOpenTelemetry()
.ConfigureResource(fun r ->
r.AddService(
serviceName = appBuilder.Configuration.GetValue("ServiceName", defaultValue = "otel-test"),
serviceVersion = instrumentationSource.ActivitySource.Version,
serviceInstanceId = Environment.MachineName)
|> ignore)
.WithTracing(fun builder ->
// Tracing

// Ensure the TracerProvider subscribes to any custom ActivitySources.
builder
.AddSource(instrumentationSource.ActivitySource.Name)
.SetSampler(AlwaysOnSampler())
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
|> ignore

// Use IConfiguration binding for AspNetCore instrumentation options.
appBuilder.Services.Configure<AspNetCoreTraceInstrumentationOptions>(
appBuilder.Configuration.GetSection("AspNetCoreInstrumentation"))
|> ignore

match tracingExporter with
| "OTLP" ->
builder.AddOtlpExporter(fun otlpOptions ->
// Use IConfiguration directly for Otlp exporter endpoint option.
otlpOptions.Endpoint <-
Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue = "http://localhost:4317")))
|> ignore
| _ ->
builder.AddConsoleExporter() |> ignore)
.WithMetrics(fun builder ->
// Metrics

// Ensure the MeterProvider subscribes to any custom Meters.
builder
.AddMeter(instrumentationSource.MeterName)
.SetExemplarFilter(ExemplarFilterType.TraceBased)
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
|> ignore

match histogramAggregation with
| "EXPONENTIAL" ->
builder.AddView(fun instrument ->
if instrument.GetType().GetGenericTypeDefinition() = typedefof<Histogram<_>> then
Base2ExponentialBucketHistogramConfiguration() :> MetricStreamConfiguration
else
null)
|> ignore
| _ ->
// Explicit bounds histogram is the default.
// No additional configuration necessary.
()

match metricsExporter with
| "PROMETHEUS" ->
builder.AddPrometheusExporter() |> ignore
| "OTLP" ->
builder.AddOtlpExporter(fun otlpOptions ->
// Use IConfiguration directly for Otlp exporter endpoint option.
otlpOptions.Endpoint <-
Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue = "http://localhost:4317")))
|> ignore
| _ ->
builder.AddConsoleExporter() |> ignore)
.WithLogging(fun builder ->
// Note: See appsettings.json Logging:OpenTelemetry section for configuration.

match logExporter with
| "OTLP" ->
builder.AddOtlpExporter(fun otlpOptions ->
// Use IConfiguration directly for Otlp exporter endpoint option.
otlpOptions.Endpoint <-
Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue = "http://localhost:4317")))
|> ignore
| _ ->
builder.AddConsoleExporter() |> ignore)
|> ignore

appBuilder.Services.AddControllers() |> ignore

let app = appBuilder.Build()

app.UseHttpsRedirection() |> ignore

app.UseAuthorization() |> ignore

app.MapControllers() |> ignore

// Configure OpenTelemetry Prometheus AspNetCore middleware scrape endpoint if enabled.
if metricsExporter.Equals("prometheus", StringComparison.OrdinalIgnoreCase) then
app.UseOpenTelemetryPrometheusScrapingEndpoint() |> ignore

app.Run()

0
42 changes: 42 additions & 0 deletions examples/FSharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# OpenTelemetry ASP.NET Core Web API Example

This example uses the new WebApplication host that ships with .NET
written using F# that shows how to setup:

1. OpenTelemetry logging
2. OpenTelemetry metrics
3. OpenTelemetry tracing

`ResourceBuilder` is associated with OpenTelemetry to associate the
service name, version and the machine on which this program is running.

The sample rate is set to emit all the traces using `AlwaysOnSampler`.
You can try out different samplers like `TraceIdRatioBasedSampler`.

## Running Dependencies via Docker

The example by default writes telemetry to stdout. To enable telemetry export
via OTLP, update the `appsettings.json` file to replace `"console"` with
`"otlp"`. Launching the application will then send telemetry data via OTLP.

Use the provided "docker-compose.yaml" file to spin up the
required dependencies, including:

- **OTel Collector** Accept telemetry and forwards them to Loki, Tempo,
and Prometheus
- **Prometheus** to store metrics
- **Grafana (UI)** UI to view logs, metrics and traces. (Exemplars can be used
to jump from metrics to traces)
- **Tempo** to store traces
- **Loki** to store logs

Once the Docker containers are running, you can access the **Grafana UI** at:
<http://localhost:3000>

## References

- [ASP.NET Core](https://learn.microsoft.com/aspnet/core/introduction-to-aspnet-core)
- [Docker](http://docker.com)
- [Prometheus](http://prometheus.io/docs)
- [Tempo](https://github.com/grafana/tempo)
- [Loki](https://github.com/grafana/loki)
14 changes: 14 additions & 0 deletions examples/FSharp/WeatherForecast.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

namespace Examples.AspNetCore

open System

type WeatherForecast =
{ Date: DateTime
TemperatureC: int
Summary: string }

member this.TemperatureF =
32.0 + (float this.TemperatureC / 0.5556)
37 changes: 37 additions & 0 deletions examples/FSharp/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"Logging": {
"LogLevel": {
"Default": "Information"
},
"OpenTelemetry": {
"IncludeFormattedMessage": true,
"IncludeScopes": true,
"ParseStateValues": true
}
},
"ServiceName": "otel-test",
"AllowedHosts": "*",
"UseTracingExporter": "console",
"UseMetricsExporter": "console",
"UseLogExporter": "console",
"HistogramAggregation": "explicit",
"Zipkin": {
"Endpoint": "http://localhost:9411/api/v2/spans"
},
"Otlp": {
"Endpoint": "http://localhost:4317"
},
"AspNetCoreInstrumentation": {
"RecordException": "true"
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
},
"Https": {
"Url": "https://localhost:5001"
}
}
}
}
Loading