Skip to content

Commit

Permalink
Reimplement KestrelMetricServer (#6429)
Browse files Browse the repository at this point in the history
  • Loading branch information
asdacap committed Dec 28, 2023
1 parent be00812 commit ada8cc9
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 7 deletions.
8 changes: 1 addition & 7 deletions src/Nethermind/Nethermind.Monitoring/MonitoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,7 @@ public async Task StartAsync()
}
if (_exposePort is not null)
{
IMetricServer metricServer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
// MetricServer uses HttpListener which on Windows either needs
// permissions at OS or Admin mode, whereas KestrelMetricServer doesn't need those
new KestrelMetricServer(_exposePort.Value) :
// KestrelMetricServer intercept SIGTERM causing exitcode to be incorrect
new MetricServer(_exposePort.Value);
metricServer.Start();
new NethermindKestrelMetricServer(_exposePort.Value).Start();
}
await Task.Factory.StartNew(() => _metricsController.StartUpdating(), TaskCreationOptions.LongRunning);
if (_logger.IsInfo) _logger.Info($"Started monitoring for the group: {_options.Group}, instance: {_options.Instance}");
Expand Down
124 changes: 124 additions & 0 deletions src/Nethermind/Nethermind.Monitoring/NethermindKestrelMetricServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

#nullable enable
using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Prometheus;

namespace Nethermind.Monitoring;

/// <summary>
/// Copy of KestrelMetricServer but does not wait for Ctrl-C so that it does not intercept exit code
/// </summary>
public sealed class NethermindKestrelMetricServer : MetricHandler
{
public NethermindKestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate)
{
}

public NethermindKestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this(LegacyOptions(hostname, port, url, registry, certificate))
{
}

private static KestrelMetricServerOptions LegacyOptions(string hostname, int port, string url, CollectorRegistry? registry, X509Certificate2? certificate) =>
new KestrelMetricServerOptions
{
Hostname = hostname,
Port = (ushort)port,
Url = url,
Registry = registry,
TlsCertificate = certificate,
};

public NethermindKestrelMetricServer(KestrelMetricServerOptions options)
{
_hostname = options.Hostname;
_port = options.Port;
_url = options.Url;
_certificate = options.TlsCertificate;

// We use one callback to apply the legacy settings, and from within this we call the real callback.
_configureExporter = settings =>
{
// Legacy setting, may be overridden by ConfigureExporter.
settings.Registry = options.Registry;
if (options.ConfigureExporter != null)
options.ConfigureExporter(settings);
};
}

private readonly string _hostname;
private readonly int _port;
private readonly string _url;

private readonly X509Certificate2? _certificate;

private readonly Action<MetricServerMiddleware.Settings> _configureExporter;

protected override Task StartServer(CancellationToken cancel)
{
var s = _certificate != null ? "s" : "";
var hostAddress = $"http{s}://{_hostname}:{_port}";

// If the caller needs to customize any of this, they can just set up their own web host and inject the middleware.
var builder = new WebHostBuilder()
.UseKestrel()
.UseIISIntegration()
.Configure(app =>
{
app.UseMetricServer(_configureExporter, _url);
// If there is any URL prefix, we just redirect people going to root URL to our prefix.
if (!string.IsNullOrWhiteSpace(_url.Trim('/')))
{
app.MapWhen(context => context.Request.Path.Value?.Trim('/') == "",
configuration =>
{
configuration.Use((HttpContext context, RequestDelegate next) =>
{
context.Response.Redirect(_url);
return Task.CompletedTask;
});
});
}
});

if (_certificate != null)
{
builder = builder.ConfigureServices(services =>
{
Action<ListenOptions> configureEndpoint = options =>
{
options.UseHttps(_certificate);
};
services.Configure<KestrelServerOptions>(options =>
{
options.Listen(IPAddress.Any, _port, configureEndpoint);
});
});
}
else
{
builder = builder.UseUrls(hostAddress);
}

var webHost = builder.Build();
webHost.Start();

// This is what changed
// return webHost.WaitForShutdownAsync(cancel);

return webHost.RunAsync(cancel);
}
}

0 comments on commit ada8cc9

Please sign in to comment.