Skip to content

Commit

Permalink
add histogram to grpc
Browse files Browse the repository at this point in the history
  • Loading branch information
sverdel authored and EraYaN committed Jan 12, 2024
1 parent 60e9106 commit 706541d
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 1 deletion.
1 change: 1 addition & 0 deletions Benchmark.NetCore/Benchmark.NetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Prometheus.AspNetCore.Grpc\Prometheus.AspNetCore.Grpc.csproj" />
<ProjectReference Include="..\Prometheus.AspNetCore\Prometheus.AspNetCore.csproj" />
</ItemGroup>

Expand Down
62 changes: 62 additions & 0 deletions Benchmark.NetCore/GrpcExporterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Prometheus;
using System.Threading.Tasks;
using Grpc.AspNetCore.Server;
using Grpc.Core;

namespace Benchmark.NetCore
{
[MemoryDiagnoser]
public class GrpcExporterBenchmarks
{
private CollectorRegistry _registry;
private MetricFactory _factory;
private GrpcRequestCountMiddleware _countMiddleware;
private GrpcRequestDurationMiddleware _durationMiddleware;
private DefaultHttpContext _ctx;

[Params(1000, 10000)]
public int RequestCount { get; set; }

[GlobalSetup]
public void Setup()
{
_ctx = new DefaultHttpContext();
_ctx.SetEndpoint(new Endpoint(
ctx => Task.CompletedTask,
new EndpointMetadataCollection(new GrpcMethodMetadata(typeof(int),
new Method<object, object>(MethodType.Unary,
"test",
"test",
new Marshaller<object>(o => new byte[0], c => null),
new Marshaller<object>(o => new byte[0], c => null)))),
"test"));
_registry = Metrics.NewCustomRegistry();
_factory = Metrics.WithCustomRegistry(_registry);

_countMiddleware = new GrpcRequestCountMiddleware(next => Task.CompletedTask, new GrpcRequestCountOptions
{
Counter = _factory.CreateCounter("count", "help")
});
_durationMiddleware = new GrpcRequestDurationMiddleware(next => Task.CompletedTask, new GrpcRequestDurationOptions
{
Histogram = _factory.CreateHistogram("duration", "help")
});
}

[Benchmark]
public async Task GrpcRequestCount()
{
for (var i = 0; i < RequestCount; i++)
await _countMiddleware.Invoke(_ctx);
}

[Benchmark]
public async Task GrpcRequestDuration()
{
for (var i = 0; i < RequestCount; i++)
await _durationMiddleware.Invoke(_ctx);
}
}
}
5 changes: 5 additions & 0 deletions Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public static class GrpcMetricsMiddlewareExtensions
app.UseMiddleware<GrpcRequestCountMiddleware>(options.RequestCount);
}

if (options.RequestDuration.Enabled)
{
app.UseMiddleware<GrpcRequestDurationMiddleware>(options.RequestDuration);
}

return app;
}
}
2 changes: 2 additions & 0 deletions Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public sealed class GrpcMiddlewareExporterOptions
{
public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions();

public GrpcRequestDurationOptions RequestDuration { get; set; } = new GrpcRequestDurationOptions();
}
50 changes: 50 additions & 0 deletions Prometheus.AspNetCore.Grpc/GrpcRequestDurationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Prometheus.HttpMetrics;

namespace Prometheus
{
internal sealed class GrpcRequestDurationMiddleware : GrpcRequestMiddlewareBase<ICollector<IHistogram>, IHistogram>
{
private readonly RequestDelegate _next;

public GrpcRequestDurationMiddleware(RequestDelegate next, GrpcRequestDurationOptions? options)
: base(options, options?.Histogram)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}

public async Task Invoke(HttpContext context)
{
var stopWatch = Stopwatch.StartNew();

// We need to write this out in long form instead of using a timer because routing data in
// ASP.NET Core 2 is only available *after* executing the next request delegate.
// So we would not have the right labels if we tried to create the child early on.
try
{
await _next(context);
}
finally
{
stopWatch.Stop();

CreateChild(context)?.Observe(stopWatch.Elapsed.TotalSeconds);
}
}

protected override string[] DefaultLabels => GrpcRequestLabelNames.NoStatusSpecific;

protected override ICollector<IHistogram> CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
"grpc_request_duration_seconds",
"The duration of gRPC requests processed by an ASP.NET Core application.",
new HistogramConfiguration
{
// 1 ms to 32K ms buckets
Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
LabelNames = labelNames
});
}
}
10 changes: 10 additions & 0 deletions Prometheus.AspNetCore.Grpc/GrpcRequestDurationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Prometheus
{
public sealed class GrpcRequestDurationOptions : GrpcMetricsOptionsBase
{
/// <summary>
/// Set this to use a custom metric instead of the default.
/// </summary>
public ICollector<IHistogram>? Histogram { get; set; }
}
}
11 changes: 10 additions & 1 deletion Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Prometheus;
namespace Prometheus;

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

public static readonly string[] All =
{
Service,
Method,
Status,
};

public static readonly string[] NoStatusSpecific =
{
Service,
Method,
};
}

5 changes: 5 additions & 0 deletions Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Grpc.AspNetCore.Server;
using Grpc.Core;

using Microsoft.AspNetCore.Http;

namespace Prometheus;
Expand Down Expand Up @@ -76,6 +78,9 @@ protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata)
case GrpcRequestLabelNames.Method:
labelValues[i] = metadata.Method.Name;
break;
case GrpcRequestLabelNames.Status:
labelValues[i] = context.Response?.GetStatusCode().ToString() ?? StatusCode.OK.ToString();
break;
default:
// Should never reach this point because we validate in ctor.
throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}");
Expand Down
28 changes: 28 additions & 0 deletions Prometheus.AspNetCore.Grpc/HttpResponseExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Linq;
using Grpc.Core;
using Microsoft.AspNetCore.Http;

namespace Prometheus
{
internal static class HttpResponseExtensions
{
private const string _grpcStatus = "grpc-status";

public static StatusCode GetStatusCode(this HttpResponse response)
{
var headerExists = response.Headers.TryGetValue(_grpcStatus, out var header);

if (!headerExists && response.StatusCode == StatusCodes.Status200OK)
{
return StatusCode.OK;
}

if (header.Any() && int.TryParse(header.FirstOrDefault(), out var status))
{
return (StatusCode)status;
}

return StatusCode.OK;
}
}
}

0 comments on commit 706541d

Please sign in to comment.