diff --git a/src/Nethermind/Nethermind.Monitoring/MonitoringService.cs b/src/Nethermind/Nethermind.Monitoring/MonitoringService.cs index f7d9e87423a..08203506e87 100644 --- a/src/Nethermind/Nethermind.Monitoring/MonitoringService.cs +++ b/src/Nethermind/Nethermind.Monitoring/MonitoringService.cs @@ -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}"); diff --git a/src/Nethermind/Nethermind.Monitoring/NethermindKestrelMetricServer.cs b/src/Nethermind/Nethermind.Monitoring/NethermindKestrelMetricServer.cs new file mode 100644 index 00000000000..1b6e5b4c50e --- /dev/null +++ b/src/Nethermind/Nethermind.Monitoring/NethermindKestrelMetricServer.cs @@ -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; + +/// +/// Copy of KestrelMetricServer but does not wait for Ctrl-C so that it does not intercept exit code +/// +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 _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 configureEndpoint = options => + { + options.UseHttps(_certificate); + }; + + services.Configure(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); + } +}