Skip to content

Commit 0453d1b

Browse files
authored
Add request.path if route description is missing. (#24)
* Add path if route description is missing * Add TryGetMeasuredOperationLatency
1 parent 026b600 commit 0453d1b

File tree

7 files changed

+168
-31
lines changed

7 files changed

+168
-31
lines changed

README.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ By default, an instrument named `LatencySLI` is added to the service metrics and
6262

6363
## Usage
6464

65-
6665
1. Create and register a metrics meter with the dependency injection.
6766

6867
Example.
@@ -75,10 +74,11 @@ By default, an instrument named `LatencySLI` is added to the service metrics and
7574
}
7675
builder.Services.AddSingleton<SampleApiMeters>();
7776
```
78-
77+
7978
2. Add a class to configure SLI
8079

8180
Example.
81+
8282
```csharp
8383
internal sealed class ConfigureServiceLevelIndicatorOptions : IConfigureOptions<ServiceLevelIndicatorOptions>
8484
{
@@ -107,20 +107,16 @@ By default, an instrument named `LatencySLI` is added to the service metrics and
107107
});
108108
```
109109

110-
4. Add the middleware to the pipeline.
111-
112-
If API versioning is used and want http.api.version as a SLI metric dimension, Use `app.UseServiceLevelIndicatorWithApiVersioning();`
113-
114-
Otherwise, `app.UseServiceLevelIndicator();`
115-
110+
4. Add the middleware to the pipeline.
111+
If API versioning is used and want http.api.version as a SLI metric dimension, Use `app.UseServiceLevelIndicatorWithApiVersioning();` else, `app.UseServiceLevelIndicator();`
116112

117113
### Customizations
118114

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

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

125121
Example.
126122

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

133-
* To override the `CustomerResourceId` within an API method, mark the parameter with the attribute `[CustomerResourceId]`
129+
- To override the `CustomerResourceId` within an API method, mark the parameter with the attribute `[CustomerResourceId]`
130+
134131
```csharp
135132
[HttpGet("get-by-zip-code/{zipCode}")]
136133
public IEnumerable<WeatherForecast> GetByZipcode([CustomerResourceId] string zipCode) => GetWeather();
137134
```
138-
135+
139136
Or use `GetMeasuredOperationLatency` extension method.
140-
137+
141138
``` csharp
142139
[HttpGet("{customerResourceId}")]
143140
public IEnumerable<WeatherForecast> Get(string customerResourceId)
@@ -147,18 +144,30 @@ eg GET WeatherForecast/Action1
147144
}
148145
```
149146

150-
* To add custom Open Telemetry attributes.
151-
``` csharp
147+
- To add custom Open Telemetry attributes.
148+
149+
``` csharp
152150
HttpContext.GetMeasuredOperationLatency().AddAttribute(attribute, value);
153151
```
154152

155-
* To prevent automatically emitting SLI information on all controllers, set the option.
156-
``` csharp
153+
GetMeasuredOperationLatency will **throw** if the route is not configured to emit SLI.
154+
155+
When used in a middleware or scenarios where a route may not be configured to emit SLI.
156+
157+
``` csharp
158+
if (HttpContext.TryGetMeasuredOperationLatency(out var measuredOperationLatency))
159+
measuredOperationLatency.AddAttribute("CustomAttribute", value);
160+
```
161+
162+
- To prevent automatically emitting SLI information on all controllers, set the option,
163+
164+
``` csharp
157165
ServiceLevelIndicatorOptions.AutomaticallyEmitted = false;
158166
```
167+
159168
In this case, add the attribute `[ServiceLevelIndicator]` on the controllers that should emit SLI.
160-
161-
* To measure a process, run it withing a `StartLatencyMeasureOperation` using block.
169+
170+
- To measure a process, run it within a `using StartLatencyMeasureOperation` block.
162171

163172
Example.
164173

ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,25 +140,26 @@ public async Task SLI_Metrics_is_emitted_with_default_API_version()
140140
}
141141

142142
[Theory]
143-
[InlineData("testSingle?api-version=invalid")]
144-
[InlineData("testDouble?api-version=2023-08-29&api-version=2023-09-01")]
145-
public async Task SLI_Metrics_is_emitted_when_invalid_api_version(string route)
143+
[InlineData("testSingle", "api-version=invalid")]
144+
[InlineData("testDouble", "api-version=2023-08-29&api-version=2023-09-01")]
145+
public async Task SLI_Metrics_is_emitted_when_invalid_api_version(string route, string version)
146146
{
147147
// Arrange
148148
_expectedTags = new KeyValuePair<string, object?>[]
149149
{
150150
new KeyValuePair<string, object?>("http.api.version", string.Empty),
151151
new KeyValuePair<string, object?>("CustomerResourceId", "TestCustomerResourceId"),
152152
new KeyValuePair<string, object?>("LocationId", "ms-loc://az/public/West US 3"),
153-
new KeyValuePair<string, object?>("Operation", "GET "),
153+
new KeyValuePair<string, object?>("Operation", "GET /" + route),
154154
new KeyValuePair<string, object?>("activity.status_code", "Unset"),
155155
new KeyValuePair<string, object?>("http.request.method", "GET"),
156156
new KeyValuePair<string, object?>("http.response.status_code", 400),
157157
};
158+
var routeWithVersion = route + "?" + version;
158159
using var host = await CreateHost();
159160

160161
// Act
161-
var response = await host.GetTestClient().GetAsync(route);
162+
var response = await host.GetTestClient().GetAsync(routeWithVersion);
162163

163164
// Assert
164165
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,46 @@
11
namespace ServiceLevelIndicators;
22
using System;
3+
using System.Diagnostics.CodeAnalysis;
34
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Http.Features;
56

67
public static class HttpContextExtensions
78
{
9+
/// <summary>
10+
/// Gets the MeasuredOperationLatency from the IServiceLevelIndicatorFeature.
11+
/// The method will throw an exception if the route is not configured to emit SLI metrics.
12+
/// </summary>
13+
/// <param name="context"></param>
14+
/// <returns>MeasuredOperationLatency for the current API method.</returns>
15+
/// <exception cref="ArgumentNullException"></exception>
16+
/// <exception cref="InvalidOperationException">If the route does not emit SLI information and therefore MeasuredOperationLatency does not exist.</exception>
817
public static MeasuredOperationLatency GetMeasuredOperationLatency(this HttpContext context)
918
{
1019
if (context == null)
1120
throw new ArgumentNullException(nameof(context));
1221

1322
return context.Features.GetRequiredFeature<IServiceLevelIndicatorFeature>().MeasuredOperationLatency;
1423
}
24+
25+
/// <summary>
26+
/// Gets the MeasuredOperationLatency from the IServiceLevelIndicatorFeature.
27+
/// </summary>
28+
/// <param name="context"></param>
29+
/// <param name="measuredOperationLatency"></param>
30+
/// <returns>true if MeasuredOperationLatency exists.</returns>
31+
/// <exception cref="ArgumentNullException"></exception>
32+
public static bool TryGetMeasuredOperationLatency(this HttpContext context, [MaybeNullWhen(false)] out MeasuredOperationLatency measuredOperationLatency)
33+
{
34+
if (context == null)
35+
throw new ArgumentNullException(nameof(context));
36+
37+
if (context.Features.Get<IServiceLevelIndicatorFeature>() is IServiceLevelIndicatorFeature feature)
38+
{
39+
measuredOperationLatency = feature.MeasuredOperationLatency;
40+
return true;
41+
}
42+
43+
measuredOperationLatency = null;
44+
return false;
45+
}
1546
}

ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorMiddleware.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ private static string GetOperation(HttpContext context, EndpointMetadataCollecti
7171
if (attrib is null || string.IsNullOrEmpty(attrib.Operation))
7272
{
7373
var description = metadata.GetMetadata<ControllerActionDescriptor>();
74-
return context.Request.Method + " " + description?.AttributeRouteInfo?.Template;
74+
var path = description?.AttributeRouteInfo?.Template ?? context.Request.Path;
75+
return context.Request.Method + " " + path;
7576
}
7677

7778
return attrib.Operation;

ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public async Task Add_custom_SLI_attribute()
195195

196196
using var host = await CreateHostWithSli(_meter);
197197

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

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

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

226-
using var host = await CreateHostWithoutSli();
226+
using var host = await CreateHostWithoutAutomaticSli();
227227

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

244244
[Fact]
245-
public async Task When_automatically_emit_SLI_is_Off_send_SLI_using_attribute()
245+
public async Task When_automatically_emit_SLI_is_Off_X2C_send_SLI_using_attribute()
246246
{
247247
_meterListener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);
248248
_meterListener.Start();
249249

250-
using var host = await CreateHostWithoutSli();
250+
using var host = await CreateHostWithoutAutomaticSli();
251251

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

273+
[Fact]
274+
public async Task GetMeasuredOperationLatency_will_throw_if_route_does_not_emit_SLI()
275+
{
276+
using var host = await CreateHostWithoutSli();
277+
278+
var response = await host.GetTestClient().GetAsync("test");
279+
response.StatusCode.Should().Be(HttpStatusCode.OK);
280+
281+
Func<Task> getMeasuredOperationLatency = () => host.GetTestClient().GetAsync("test/custom_attribute/Mickey");
282+
283+
await getMeasuredOperationLatency.Should().ThrowAsync<InvalidOperationException>();
284+
}
285+
286+
[Fact]
287+
public async Task TryGetMeasuredOperationLatency_will_return_false_if_route_does_not_emit_SLI()
288+
{
289+
using var host = await CreateHostWithoutSli();
290+
291+
var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation_latency/Donald");
292+
response.StatusCode.Should().Be(HttpStatusCode.OK);
293+
var content = await response.Content.ReadAsStringAsync();
294+
295+
content.Should().Be("false");
296+
}
297+
298+
[Fact]
299+
public async Task TryGetMeasuredOperationLatency_will_return_true_if_route_emits_SLI()
300+
{
301+
_meterListener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);
302+
_meterListener.Start();
303+
304+
using var host = await CreateHostWithSli(_meter);
305+
306+
var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation_latency/Goofy");
307+
response.StatusCode.Should().Be(HttpStatusCode.OK);
308+
var content = await response.Content.ReadAsStringAsync();
309+
310+
content.Should().Be("true");
311+
312+
void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
313+
{
314+
var expectedTags = new KeyValuePair<string, object?>[]
315+
{
316+
new KeyValuePair<string, object?>("CustomerResourceId", "TestCustomerResourceId"),
317+
new KeyValuePair<string, object?>("LocationId", "ms-loc://az/public/West US 3"),
318+
new KeyValuePair<string, object?>("Operation", "GET Test/try_get_measured_operation_latency/{value}"),
319+
new KeyValuePair<string, object?>("activity.status_code", "Ok"),
320+
new KeyValuePair<string, object?>("http.request.method", "GET"),
321+
new KeyValuePair<string, object?>("http.response.status_code", 200),
322+
new KeyValuePair<string, object?>("CustomAttribute", "Goofy"),
323+
};
324+
325+
ValidateMetrics(instrument, measurement, tags, expectedTags);
326+
}
327+
328+
_callbackCalled.Should().BeTrue();
329+
}
330+
273331
private static async Task<IHost> CreateHostWithSli(Meter meter) =>
274332
await new HostBuilder()
275333
.ConfigureWebHost(webBuilder =>
@@ -299,7 +357,7 @@ private static async Task<IHost> CreateHostWithSli(Meter meter) =>
299357
});
300358
})
301359
.StartAsync();
302-
private async Task<IHost> CreateHostWithoutSli()
360+
private async Task<IHost> CreateHostWithoutAutomaticSli()
303361
{
304362
return await new HostBuilder()
305363
.ConfigureWebHost(webBuilder =>
@@ -331,6 +389,32 @@ private async Task<IHost> CreateHostWithoutSli()
331389
})
332390
.StartAsync();
333391
}
392+
393+
private static async Task<IHost> CreateHostWithoutSli()
394+
{
395+
return await new HostBuilder()
396+
.ConfigureWebHost(webBuilder =>
397+
{
398+
webBuilder
399+
.UseTestServer()
400+
.ConfigureServices(services =>
401+
{
402+
services.AddControllers();
403+
})
404+
.Configure(app =>
405+
{
406+
app.UseRouting()
407+
.Use(async (context, next) =>
408+
{
409+
await Task.Delay(MillisecondsDelay);
410+
await next(context);
411+
})
412+
.UseEndpoints(endpoints => endpoints.MapControllers());
413+
});
414+
})
415+
.StartAsync();
416+
}
417+
334418
protected virtual void Dispose(bool disposing)
335419
{
336420
if (!_disposedValue)

ServiceLevelIndicators.Asp/tests/TestController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ public IActionResult AddCustomAttribute(string value)
2828
return Ok(value);
2929
}
3030

31+
[HttpGet("try_get_measured_operation_latency/{value}")]
32+
public IActionResult TryGetMeasuredOperationLatency(string value)
33+
{
34+
if (HttpContext.TryGetMeasuredOperationLatency(out var measuredOperationLatency))
35+
{
36+
measuredOperationLatency.AddAttribute("CustomAttribute", value);
37+
return Ok(true);
38+
}
39+
return Ok(false);
40+
}
41+
3142
[HttpGet("send_sli")]
3243
[ServiceLevelIndicator]
3344
public IActionResult SendSLI() => Ok("Hello");

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "1.1-alpha.{height}",
3+
"version": "1.1",
44
"nuGetPackageVersion": {
55
"semVer": 2.0
66
},

0 commit comments

Comments
 (0)