Skip to content

Commit

Permalink
Add request.path if route description is missing. (#24)
Browse files Browse the repository at this point in the history
* Add path if route description is missing

* Add TryGetMeasuredOperationLatency
  • Loading branch information
xavierjohn authored Nov 10, 2023
1 parent 026b600 commit 0453d1b
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 31 deletions.
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ By default, an instrument named `LatencySLI` is added to the service metrics and

## Usage


1. Create and register a metrics meter with the dependency injection.

Example.
Expand All @@ -75,10 +74,11 @@ By default, an instrument named `LatencySLI` is added to the service metrics and
}
builder.Services.AddSingleton<SampleApiMeters>();
```

2. Add a class to configure SLI

Example.

```csharp
internal sealed class ConfigureServiceLevelIndicatorOptions : IConfigureOptions<ServiceLevelIndicatorOptions>
{
Expand Down Expand Up @@ -107,20 +107,16 @@ By default, an instrument named `LatencySLI` is added to the service metrics and
});
```

4. Add the middleware to the pipeline.

If API versioning is used and want http.api.version as a SLI metric dimension, Use `app.UseServiceLevelIndicatorWithApiVersioning();`

Otherwise, `app.UseServiceLevelIndicator();`

4. Add the middleware to the pipeline.
If API versioning is used and want http.api.version as a SLI metric dimension, Use `app.UseServiceLevelIndicatorWithApiVersioning();` else, `app.UseServiceLevelIndicator();`

### Customizations

Once the Prerequisites are done, all controllers will emit SLI information.
The default operation name is in the format &lt;HTTP Method&gt; &lt;Controller&gt;/&lt;Action&gt;.
eg GET WeatherForecast/Action1

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

Example.

Expand All @@ -130,14 +126,15 @@ eg GET WeatherForecast/Action1
public IEnumerable<WeatherForecast> GetOperation() => GetWeather();
```

* To override the `CustomerResourceId` within an API method, mark the parameter with the attribute `[CustomerResourceId]`
- To override 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 `GetMeasuredOperationLatency` extension method.

``` csharp
[HttpGet("{customerResourceId}")]
public IEnumerable<WeatherForecast> Get(string customerResourceId)
Expand All @@ -147,18 +144,30 @@ eg GET WeatherForecast/Action1
}
```

* To add custom Open Telemetry attributes.
``` csharp
- To add custom Open Telemetry attributes.

``` csharp
HttpContext.GetMeasuredOperationLatency().AddAttribute(attribute, value);
```

* To prevent automatically emitting SLI information on all controllers, set the option.
``` csharp
GetMeasuredOperationLatency 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.TryGetMeasuredOperationLatency(out var measuredOperationLatency))
measuredOperationLatency.AddAttribute("CustomAttribute", value);
```

- 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.
* To measure a process, run it withing a `StartLatencyMeasureOperation` using block.

- To measure a process, run it within a `using StartLatencyMeasureOperation` block.

Example.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,25 +140,26 @@ public async Task SLI_Metrics_is_emitted_with_default_API_version()
}

[Theory]
[InlineData("testSingle?api-version=invalid")]
[InlineData("testDouble?api-version=2023-08-29&api-version=2023-09-01")]
public async Task SLI_Metrics_is_emitted_when_invalid_api_version(string route)
[InlineData("testSingle", "api-version=invalid")]
[InlineData("testDouble", "api-version=2023-08-29&api-version=2023-09-01")]
public async Task SLI_Metrics_is_emitted_when_invalid_api_version(string route, string version)
{
// Arrange
_expectedTags = new KeyValuePair<string, object?>[]
{
new KeyValuePair<string, object?>("http.api.version", string.Empty),
new KeyValuePair<string, object?>("CustomerResourceId", "TestCustomerResourceId"),
new KeyValuePair<string, object?>("LocationId", "ms-loc://az/public/West US 3"),
new KeyValuePair<string, object?>("Operation", "GET "),
new KeyValuePair<string, object?>("Operation", "GET /" + route),
new KeyValuePair<string, object?>("activity.status_code", "Unset"),
new KeyValuePair<string, object?>("http.request.method", "GET"),
new KeyValuePair<string, object?>("http.response.status_code", 400),
};
var routeWithVersion = route + "?" + version;
using var host = await CreateHost();

// Act
var response = await host.GetTestClient().GetAsync(route);
var response = await host.GetTestClient().GetAsync(routeWithVersion);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
Expand Down
31 changes: 31 additions & 0 deletions ServiceLevelIndicators.Asp/src/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
namespace ServiceLevelIndicators;
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

public static class HttpContextExtensions
{
/// <summary>
/// Gets the MeasuredOperationLatency from the IServiceLevelIndicatorFeature.
/// The method will throw an exception if the route is not configured to emit SLI metrics.
/// </summary>
/// <param name="context"></param>
/// <returns>MeasuredOperationLatency for the current API method.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException">If the route does not emit SLI information and therefore MeasuredOperationLatency does not exist.</exception>
public static MeasuredOperationLatency GetMeasuredOperationLatency(this HttpContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

return context.Features.GetRequiredFeature<IServiceLevelIndicatorFeature>().MeasuredOperationLatency;
}

/// <summary>
/// Gets the MeasuredOperationLatency from the IServiceLevelIndicatorFeature.
/// </summary>
/// <param name="context"></param>
/// <param name="measuredOperationLatency"></param>
/// <returns>true if MeasuredOperationLatency exists.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool TryGetMeasuredOperationLatency(this HttpContext context, [MaybeNullWhen(false)] out MeasuredOperationLatency measuredOperationLatency)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

if (context.Features.Get<IServiceLevelIndicatorFeature>() is IServiceLevelIndicatorFeature feature)
{
measuredOperationLatency = feature.MeasuredOperationLatency;
return true;
}

measuredOperationLatency = null;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ private static string GetOperation(HttpContext context, EndpointMetadataCollecti
if (attrib is null || string.IsNullOrEmpty(attrib.Operation))
{
var description = metadata.GetMetadata<ControllerActionDescriptor>();
return context.Request.Method + " " + description?.AttributeRouteInfo?.Template;
var path = description?.AttributeRouteInfo?.Template ?? context.Request.Path;
return context.Request.Method + " " + path;
}

return attrib.Operation;
Expand Down
96 changes: 90 additions & 6 deletions ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public async Task Add_custom_SLI_attribute()

using var host = await CreateHostWithSli(_meter);

var response = await host.GetTestClient().GetAsync("test/custom_attribute/mickey");
var response = await host.GetTestClient().GetAsync("test/custom_attribute/Mickey");
response.StatusCode.Should().Be(HttpStatusCode.OK);

void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
Expand All @@ -208,7 +208,7 @@ void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan
new KeyValuePair<string, object?>("activity.status_code", "Ok"),
new KeyValuePair<string, object?>("http.request.method", "GET"),
new KeyValuePair<string, object?>("http.response.status_code", 200),
new KeyValuePair<string, object?>("CustomAttribute", "mickey"),
new KeyValuePair<string, object?>("CustomAttribute", "Mickey"),
};

ValidateMetrics(instrument, measurement, tags, expectedTags);
Expand All @@ -223,7 +223,7 @@ public async Task When_automatically_emit_SLI_is_Off_do_not_send_SLI()
_meterListener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);
_meterListener.Start();

using var host = await CreateHostWithoutSli();
using var host = await CreateHostWithoutAutomaticSli();

var response = await host.GetTestClient().GetAsync("test");
response.StatusCode.Should().Be(HttpStatusCode.OK);
Expand All @@ -242,12 +242,12 @@ void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan
}

[Fact]
public async Task When_automatically_emit_SLI_is_Off_send_SLI_using_attribute()
public async Task When_automatically_emit_SLI_is_Off_X2C_send_SLI_using_attribute()
{
_meterListener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);
_meterListener.Start();

using var host = await CreateHostWithoutSli();
using var host = await CreateHostWithoutAutomaticSli();

var response = await host.GetTestClient().GetAsync("test/send_sli");
response.StatusCode.Should().Be(HttpStatusCode.OK);
Expand All @@ -270,6 +270,64 @@ void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan
_callbackCalled.Should().BeTrue();
}

[Fact]
public async Task GetMeasuredOperationLatency_will_throw_if_route_does_not_emit_SLI()
{
using var host = await CreateHostWithoutSli();

var response = await host.GetTestClient().GetAsync("test");
response.StatusCode.Should().Be(HttpStatusCode.OK);

Func<Task> getMeasuredOperationLatency = () => host.GetTestClient().GetAsync("test/custom_attribute/Mickey");

await getMeasuredOperationLatency.Should().ThrowAsync<InvalidOperationException>();
}

[Fact]
public async Task TryGetMeasuredOperationLatency_will_return_false_if_route_does_not_emit_SLI()
{
using var host = await CreateHostWithoutSli();

var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation_latency/Donald");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();

content.Should().Be("false");
}

[Fact]
public async Task TryGetMeasuredOperationLatency_will_return_true_if_route_emits_SLI()
{
_meterListener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);
_meterListener.Start();

using var host = await CreateHostWithSli(_meter);

var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation_latency/Goofy");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();

content.Should().Be("true");

void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
{
var expectedTags = new KeyValuePair<string, object?>[]
{
new KeyValuePair<string, object?>("CustomerResourceId", "TestCustomerResourceId"),
new KeyValuePair<string, object?>("LocationId", "ms-loc://az/public/West US 3"),
new KeyValuePair<string, object?>("Operation", "GET Test/try_get_measured_operation_latency/{value}"),
new KeyValuePair<string, object?>("activity.status_code", "Ok"),
new KeyValuePair<string, object?>("http.request.method", "GET"),
new KeyValuePair<string, object?>("http.response.status_code", 200),
new KeyValuePair<string, object?>("CustomAttribute", "Goofy"),
};

ValidateMetrics(instrument, measurement, tags, expectedTags);
}

_callbackCalled.Should().BeTrue();
}

private static async Task<IHost> CreateHostWithSli(Meter meter) =>
await new HostBuilder()
.ConfigureWebHost(webBuilder =>
Expand Down Expand Up @@ -299,7 +357,7 @@ private static async Task<IHost> CreateHostWithSli(Meter meter) =>
});
})
.StartAsync();
private async Task<IHost> CreateHostWithoutSli()
private async Task<IHost> CreateHostWithoutAutomaticSli()
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
Expand Down Expand Up @@ -331,6 +389,32 @@ private async Task<IHost> CreateHostWithoutSli()
})
.StartAsync();
}

private static async Task<IHost> CreateHostWithoutSli()
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddControllers();
})
.Configure(app =>
{
app.UseRouting()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
.UseEndpoints(endpoints => endpoints.MapControllers());
});
})
.StartAsync();
}

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
Expand Down
11 changes: 11 additions & 0 deletions ServiceLevelIndicators.Asp/tests/TestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ public IActionResult AddCustomAttribute(string value)
return Ok(value);
}

[HttpGet("try_get_measured_operation_latency/{value}")]
public IActionResult TryGetMeasuredOperationLatency(string value)
{
if (HttpContext.TryGetMeasuredOperationLatency(out var measuredOperationLatency))
{
measuredOperationLatency.AddAttribute("CustomAttribute", value);
return Ok(true);
}
return Ok(false);
}

[HttpGet("send_sli")]
[ServiceLevelIndicator]
public IActionResult SendSLI() => Ok("Hello");
Expand Down
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "1.1-alpha.{height}",
"version": "1.1",
"nuGetPackageVersion": {
"semVer": 2.0
},
Expand Down

0 comments on commit 0453d1b

Please sign in to comment.