Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement KestrelMetricServer #6429

Merged
merged 1 commit into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}