Skip to content

Commit

Permalink
AddServiceLevelIndicatorInstrumentation (#43)
Browse files Browse the repository at this point in the history
Add an extension method to simplify the registration of Service Level Indicator (SLI) instrumentation.

This code creates a default meter, allowing users the flexibility to use the default meter or specify a custom meter if desired.
  • Loading branch information
xavierjohn authored Nov 20, 2024
1 parent 5ea6237 commit 36fc878
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 115 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.143" />
<PackageVersion Include="OpenTelemetry" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.8.1" />
Expand Down
94 changes: 20 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,79 +80,53 @@ Difference between ServiceLevelIndicator and http.server.request.duration

## Usage for Web API MVC

1. Create and register a metrics meter with the dependency injection.
1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`.

Example.

``` csharp

public class SampleApiMeters
{
public const string MeterName = "SampleMeter";
public Meter Meter { get; } = new Meter(MeterName);
}
builder.Services.AddSingleton<SampleApiMeters>();

builder.Services.AddOpenTelemetry()
.ConfigureResource(configureResource)
.WithMetrics(builder =>
{
builder.AddServiceLevelIndicatorInstrumentation();
builder.AddOtlpExporter();
});
```

2. Add a class to configure SLI

Example.

```csharp

internal sealed class ConfigureServiceLevelIndicatorOptions
: IConfigureOptions<ServiceLevelIndicatorOptions>
{
public ConfigureServiceLevelIndicatorOptions(SampleApiMeters meters)
=> this.meters = meters;
public void Configure(ServiceLevelIndicatorOptions options)
=> options.Meter = meters.Meter;

private readonly SampleApiMeters meters;
}

builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IConfigureOptions<ServiceLevelIndicatorOptions>,
ConfigureServiceLevelIndicatorOptions>());

```

3. Add ServiceLevelIndicator, into the dependency injection. AddMvc() is required for overrides present in SLI attributes to take effect.
2. Add ServiceLevelIndicator, into the dependency injection. AddMvc() is required for overrides present in SLI attributes to take effect.

Example.

``` csharp

builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name);
})
.AddMvc();

```

4. Add the middleware to the pipeline.
3. Add the middleware to the pipeline.

``` csharp

app.UseServiceLevelIndicator();

```

## Usage for Minimal API

1. Create the metrics meter.

Example.
1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`.

```csharp
Example.

internal sealed class Sample
{
public static Meter Meter { get; } = new(nameof(Sample));
}

``` csharp
builder.Services.AddOpenTelemetry()
.ConfigureResource(configureResource)
.WithMetrics(builder =>
{
builder.AddServiceLevelIndicatorInstrumentation();
builder.AddOtlpExporter();
});
```

2. Add ServiceLevelIndicator into the dependency injection.
Expand All @@ -173,20 +147,16 @@ Difference between ServiceLevelIndicator and http.server.request.duration
Example.

``` csharp

app.UseServiceLevelIndicator();

```

4. To each API route mapping, add `AddServiceLevelIndicator()`

Example.

``` csharp

app.MapGet("/hello", () => "Hello World!")
.AddServiceLevelIndicator();

```

### Usage for background jobs
Expand All @@ -195,14 +165,12 @@ You can measure a block of code by boxing it in a using clause of MeasuredOperat
Example.

```csharp

async Task MeasureCodeBlock(ServiceLevelIndicator serviceLevelIndicator)
{
using var measuredOperation = serviceLevelIndicator.StartMeasuring("OperationName");
// Do Work.
measuredOperation.SetActivityStatusCode(System.Diagnostics.ActivityStatusCode.Ok);
}

```

### Customizations
Expand All @@ -216,29 +184,25 @@ eg GET WeatherForecast/Action1
Example.

``` csharp

builder.Services.AddServiceLevelIndicator(options =>
{
/// Options
})
.AddMvc()
.AddApiVersion();

```

- To add HTTP method as a dimension, add `AddHttpMethod` to Service Level Indicator.

Example.

``` csharp

builder.Services.AddServiceLevelIndicator(options =>
{
/// Options
})
.AddMvc()
.AddHttpMethod();

```

- Enrich SLI with `Enrich` callback. The callback receives a `MeasuredOperation` as context that can be used to set to `CustomerResourceId` or additional attributes.
Expand All @@ -247,7 +211,6 @@ An async version `EnrichAsync` is also available.
Example.

``` csharp

builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId(Cloud, Region);
Expand All @@ -259,81 +222,66 @@ An async version `EnrichAsync` is also available.
.FirstOrDefault(c => c.Type == "upn")?.Value ?? "Unknown";
context.SetCustomerResourceId(upn);
});

```

- To override the default operation name add the attribute `[ServiceLevelIndicator]` and specify the operation name.

Example.

``` csharp

[HttpGet("MyAction2")]
[ServiceLevelIndicator(Operation = "MyNewOperationName")]
public IEnumerable<WeatherForecast> GetOperation() => GetWeather();

```

- To set the `CustomerResourceId` within an API method, mark the parameter with the attribute `[CustomerResourceId]`

```csharp

[HttpGet("get-by-zip-code/{zipCode}")]
public IEnumerable<WeatherForecast> GetByZipcode([CustomerResourceId] string zipCode)
=> GetWeather();

```

Or use `GetMeasuredOperation` extension method.

``` csharp

[HttpGet("{customerResourceId}")]
public IEnumerable<WeatherForecast> Get(string customerResourceId)
{
HttpContext.GetMeasuredOperation().CustomerResourceId = customerResourceId;
return GetWeather();
}

```

- To add custom Open Telemetry attributes.

``` csharp

HttpContext.GetMeasuredOperation().AddAttribute(attribute, value);

```

GetMeasuredOperation will **throw** if the route is not configured to emit SLI.

When used in a middleware or scenarios where a route may not be configured to emit SLI.

``` csharp

if (HttpContext.TryGetMeasuredOperation(out var measuredOperation))
measuredOperation.AddAttribute("CustomAttribute", value);

```

You can add additional dimensions to the SLI data by using the `Measure` attribute.

```csharp

[HttpGet("name/{first}/{surname}")]
public IActionResult GetCustomerResourceId(
[Measure] string first,
[CustomerResourceId] string surname)
=> Ok(first + " " + surname);

```

- To prevent automatically emitting SLI information on all controllers, set the option,

``` csharp

ServiceLevelIndicatorOptions.AutomaticallyEmitted = false;

```

In this case, add the attribute `[ServiceLevelIndicator]` on the controllers that should emit SLI.
Expand All @@ -343,14 +291,12 @@ An async version `EnrichAsync` is also available.
Example.

``` csharp

public void StoreItem(MyDomainEvent domainEvent)
{
var attribute = new KeyValuePair<string, object?>("Event", domainEvent.GetType().Name);
using var measuredOperation = _serviceLevelIndicator.StartMeasuring("StoreItem", attribute);
DoTheWork();
)

```

### Sample
Expand Down
10 changes: 8 additions & 2 deletions ServiceLevelIndicators/src/ServiceLevelIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Reflection;
using Microsoft.Extensions.Options;

public class ServiceLevelIndicator
{
public const string DefaultMeterName = nameof(ServiceLevelIndicator);
public ServiceLevelIndicatorOptions ServiceLevelIndicatorOptions { get; }

private readonly Histogram<long> _responseLatencyHistogram;
Expand All @@ -15,9 +17,13 @@ public ServiceLevelIndicator(IOptions<ServiceLevelIndicatorOptions> options)
{
ServiceLevelIndicatorOptions = options.Value;
if (ServiceLevelIndicatorOptions.Meter == null)
throw new ArgumentNullException(message: "Meter must be provided in options.", paramName: nameof(options));
{
AssemblyName AssemblyName = typeof(ServiceLevelIndicator).Assembly.GetName();
string InstrumentationVersion = AssemblyName.Version!.ToString();
ServiceLevelIndicatorOptions.Meter = new(DefaultMeterName, InstrumentationVersion);
}

_responseLatencyHistogram = ServiceLevelIndicatorOptions.Meter.CreateHistogram<long>(ServiceLevelIndicatorOptions.InstrumentName, "ms");
_responseLatencyHistogram = ServiceLevelIndicatorOptions.Meter.CreateHistogram<long>(ServiceLevelIndicatorOptions.InstrumentName, "ms", "Duration of the operation.");
}

public void Record(string operation, long elapsedTime, params KeyValuePair<string, object?>[] attributes) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ServiceLevelIndicators;
using OpenTelemetry.Metrics;

/// <summary>
/// Extension methods to simplify registering of Service Level Indicator instrumentation.
/// </summary>
public static class ServiceLevelIndicatorMeterProviderBuilderExtensions
{
/// <summary>
/// Enables Service Level Indicator instrumentation.
/// </summary>
/// <param name="builder"><see cref="MeterProviderBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns>
public static MeterProviderBuilder AddServiceLevelIndicatorInstrumentation(this MeterProviderBuilder builder)
=> builder.AddMeter(ServiceLevelIndicator.DefaultMeterName);
}
17 changes: 16 additions & 1 deletion ServiceLevelIndicators/src/ServiceLevelIndicatorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,25 @@
/// </summary>
public class ServiceLevelIndicatorOptions
{
private readonly object _meterLock = new();
private Meter _meter = null!;

/// <summary>
/// The meter that is used to create the histogram that reports the latency.
/// </summary>
public Meter Meter { get; set; } = null!;
public Meter Meter
{
get
{
lock (_meterLock)
return _meter;
}
set
{
lock (_meterLock)
_meter = value;
}
}

/// <summary>
/// CustomerResrouceId is the unique identifier for the customer like subscriptionId, tenantId, etc.
Expand Down
1 change: 1 addition & 0 deletions ServiceLevelIndicators/src/ServiceLevelIndicators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="OpenTelemetry" />
</ItemGroup>
</Project>
11 changes: 2 additions & 9 deletions sample/MinApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.Metrics;
using Azure.Core;
using Azure.Core;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using ServiceLevelIndicators;
Expand Down Expand Up @@ -32,7 +31,7 @@
.ConfigureResource(configureResource)
.WithMetrics(builder =>
{
builder.AddMeter(Sample.Meter.Name);
builder.AddServiceLevelIndicatorInstrumentation();
builder.AddOtlpExporter();
});

Expand All @@ -41,7 +40,6 @@
{
options.CustomerResourceId = "SampleCustomerResourceId";
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name);
options.Meter = Sample.Meter;
})
.AddHttpMethod();

Expand Down Expand Up @@ -77,8 +75,3 @@ internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

internal sealed class Sample
{
public static Meter Meter { get; } = new(nameof(Sample));
}
1 change: 0 additions & 1 deletion sample/WebApi/ConfigureServiceLevelIndicatorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace SampleWebApplicationSLI;

using Microsoft.Extensions.Options;
using SampleWebApplicationSLI;
using ServiceLevelIndicators;

internal sealed class ConfigureServiceLevelIndicatorOptions : IConfigureOptions<ServiceLevelIndicatorOptions>
Expand Down
Loading

0 comments on commit 36fc878

Please sign in to comment.