Skip to content

Commit 706541d

Browse files
sverdelEraYaN
sverdel
authored andcommitted
add histogram to grpc
1 parent 60e9106 commit 706541d

9 files changed

+173
-1
lines changed

Benchmark.NetCore/Benchmark.NetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35+
<ProjectReference Include="..\Prometheus.AspNetCore.Grpc\Prometheus.AspNetCore.Grpc.csproj" />
3536
<ProjectReference Include="..\Prometheus.AspNetCore\Prometheus.AspNetCore.csproj" />
3637
</ItemGroup>
3738

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Microsoft.AspNetCore.Http;
3+
using Prometheus;
4+
using System.Threading.Tasks;
5+
using Grpc.AspNetCore.Server;
6+
using Grpc.Core;
7+
8+
namespace Benchmark.NetCore
9+
{
10+
[MemoryDiagnoser]
11+
public class GrpcExporterBenchmarks
12+
{
13+
private CollectorRegistry _registry;
14+
private MetricFactory _factory;
15+
private GrpcRequestCountMiddleware _countMiddleware;
16+
private GrpcRequestDurationMiddleware _durationMiddleware;
17+
private DefaultHttpContext _ctx;
18+
19+
[Params(1000, 10000)]
20+
public int RequestCount { get; set; }
21+
22+
[GlobalSetup]
23+
public void Setup()
24+
{
25+
_ctx = new DefaultHttpContext();
26+
_ctx.SetEndpoint(new Endpoint(
27+
ctx => Task.CompletedTask,
28+
new EndpointMetadataCollection(new GrpcMethodMetadata(typeof(int),
29+
new Method<object, object>(MethodType.Unary,
30+
"test",
31+
"test",
32+
new Marshaller<object>(o => new byte[0], c => null),
33+
new Marshaller<object>(o => new byte[0], c => null)))),
34+
"test"));
35+
_registry = Metrics.NewCustomRegistry();
36+
_factory = Metrics.WithCustomRegistry(_registry);
37+
38+
_countMiddleware = new GrpcRequestCountMiddleware(next => Task.CompletedTask, new GrpcRequestCountOptions
39+
{
40+
Counter = _factory.CreateCounter("count", "help")
41+
});
42+
_durationMiddleware = new GrpcRequestDurationMiddleware(next => Task.CompletedTask, new GrpcRequestDurationOptions
43+
{
44+
Histogram = _factory.CreateHistogram("duration", "help")
45+
});
46+
}
47+
48+
[Benchmark]
49+
public async Task GrpcRequestCount()
50+
{
51+
for (var i = 0; i < RequestCount; i++)
52+
await _countMiddleware.Invoke(_ctx);
53+
}
54+
55+
[Benchmark]
56+
public async Task GrpcRequestDuration()
57+
{
58+
for (var i = 0; i < RequestCount; i++)
59+
await _durationMiddleware.Invoke(_ctx);
60+
}
61+
}
62+
}

Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app,
2929
app.UseMiddleware<GrpcRequestCountMiddleware>(options.RequestCount);
3030
}
3131

32+
if (options.RequestDuration.Enabled)
33+
{
34+
app.UseMiddleware<GrpcRequestDurationMiddleware>(options.RequestDuration);
35+
}
36+
3237
return app;
3338
}
3439
}

Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
public sealed class GrpcMiddlewareExporterOptions
44
{
55
public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions();
6+
7+
public GrpcRequestDurationOptions RequestDuration { get; set; } = new GrpcRequestDurationOptions();
68
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http;
5+
using Prometheus.HttpMetrics;
6+
7+
namespace Prometheus
8+
{
9+
internal sealed class GrpcRequestDurationMiddleware : GrpcRequestMiddlewareBase<ICollector<IHistogram>, IHistogram>
10+
{
11+
private readonly RequestDelegate _next;
12+
13+
public GrpcRequestDurationMiddleware(RequestDelegate next, GrpcRequestDurationOptions? options)
14+
: base(options, options?.Histogram)
15+
{
16+
_next = next ?? throw new ArgumentNullException(nameof(next));
17+
}
18+
19+
public async Task Invoke(HttpContext context)
20+
{
21+
var stopWatch = Stopwatch.StartNew();
22+
23+
// We need to write this out in long form instead of using a timer because routing data in
24+
// ASP.NET Core 2 is only available *after* executing the next request delegate.
25+
// So we would not have the right labels if we tried to create the child early on.
26+
try
27+
{
28+
await _next(context);
29+
}
30+
finally
31+
{
32+
stopWatch.Stop();
33+
34+
CreateChild(context)?.Observe(stopWatch.Elapsed.TotalSeconds);
35+
}
36+
}
37+
38+
protected override string[] DefaultLabels => GrpcRequestLabelNames.NoStatusSpecific;
39+
40+
protected override ICollector<IHistogram> CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
41+
"grpc_request_duration_seconds",
42+
"The duration of gRPC requests processed by an ASP.NET Core application.",
43+
new HistogramConfiguration
44+
{
45+
// 1 ms to 32K ms buckets
46+
Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
47+
LabelNames = labelNames
48+
});
49+
}
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Prometheus
2+
{
3+
public sealed class GrpcRequestDurationOptions : GrpcMetricsOptionsBase
4+
{
5+
/// <summary>
6+
/// Set this to use a custom metric instead of the default.
7+
/// </summary>
8+
public ICollector<IHistogram>? Histogram { get; set; }
9+
}
10+
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Prometheus;
1+
namespace Prometheus;
22

33
/// <summary>
44
/// Reserved label names used in gRPC metrics.
@@ -7,10 +7,19 @@ public static class GrpcRequestLabelNames
77
{
88
public const string Service = "service";
99
public const string Method = "method";
10+
public const string Status = "status";
1011

1112
public static readonly string[] All =
13+
{
14+
Service,
15+
Method,
16+
Status,
17+
};
18+
19+
public static readonly string[] NoStatusSpecific =
1220
{
1321
Service,
1422
Method,
1523
};
1624
}
25+

Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Grpc.AspNetCore.Server;
2+
using Grpc.Core;
3+
24
using Microsoft.AspNetCore.Http;
35

46
namespace Prometheus;
@@ -76,6 +78,9 @@ protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata)
7678
case GrpcRequestLabelNames.Method:
7779
labelValues[i] = metadata.Method.Name;
7880
break;
81+
case GrpcRequestLabelNames.Status:
82+
labelValues[i] = context.Response?.GetStatusCode().ToString() ?? StatusCode.OK.ToString();
83+
break;
7984
default:
8085
// Should never reach this point because we validate in ctor.
8186
throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}");
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Linq;
2+
using Grpc.Core;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace Prometheus
6+
{
7+
internal static class HttpResponseExtensions
8+
{
9+
private const string _grpcStatus = "grpc-status";
10+
11+
public static StatusCode GetStatusCode(this HttpResponse response)
12+
{
13+
var headerExists = response.Headers.TryGetValue(_grpcStatus, out var header);
14+
15+
if (!headerExists && response.StatusCode == StatusCodes.Status200OK)
16+
{
17+
return StatusCode.OK;
18+
}
19+
20+
if (header.Any() && int.TryParse(header.FirstOrDefault(), out var status))
21+
{
22+
return (StatusCode)status;
23+
}
24+
25+
return StatusCode.OK;
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)