< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Metrics.MetricsOptions
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Metrics/MetricsOptions.cs
Line coverage
100%
Covered lines: 13
Uncovered lines: 0
Coverable lines: 13
Total lines: 97
Line coverage: 100%
Branch coverage
100%
Covered branches: 12
Total branches: 12
Branch coverage: 100%
Method coverage
100%
Covered methods: 7
Fully covered methods: 7
Total methods: 7
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Disabled()100%11100%
get_Port()100%11100%
get_Enabled()100%11100%
get_InvalidValue()100%11100%
FromEnvironment()100%11100%
Parse(...)100%88100%
IsScrapeRequest(...)100%44100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Metrics/MetricsOptions.cs

#LineLine coverage
 1namespace ArturRios.Heimdall.WebApi.Metrics;
 2
 3/// <summary>
 4///     Where the Prometheus scrape endpoint is served, read once at start-up from
 5///     <c>HEIMDALL_METRICS_PORT</c>. Decides both whether the exporter is registered at all and which
 6///     requests it answers — see <see cref="IsScrapeRequest" />.
 7/// </summary>
 8/// <remarks>
 9///     <para>
 10///         The endpoint is told apart from the public API by the <em>port the connection arrived
 11///         on</em>, and by nothing a caller can influence. The API runs behind Traefik, which
 12///         forwards the client's own <c>Host</c> header, so a check on the host name would be a
 13///         check on a value the attacker chooses. The local port is decided by the socket instead:
 14///         Traefik reaches the container on the public port, Prometheus reaches it directly on this
 15///         one over a private Docker network, and no request can arrive on one while claiming to
 16///         be on the other.
 17///     </para>
 18///     <para>
 19///         That only holds while this port is never published. Kestrel listens on it because the
 20///         image's <c>ASPNETCORE_HTTP_PORTS</c> says so, and Compose deliberately maps only the
 21///         public port to the host — see the Dockerfile and docker-compose.yml.
 22///     </para>
 23/// </remarks>
 24public sealed class MetricsOptions
 25{
 26    /// <summary>The port Prometheus scrapes on. <c>0</c> switches the exporter off.</summary>
 27    public const string PortVariable = "HEIMDALL_METRICS_PORT";
 28
 29    /// <summary>
 30    ///     The exporter's conventional port, the one Prometheus's own default-port allocations list for
 31    ///     the OpenTelemetry Prometheus exporter — so a scrape configuration written from the
 32    ///     convention needs no change.
 33    /// </summary>
 34    public const int DefaultPort = 9464;
 35
 36    /// <summary>The scrape path. Fixed: Prometheus assumes it, so making it configurable buys nothing.</summary>
 37    public const string ScrapePath = "/metrics";
 38
 39    /// <summary>Metrics switched off: no exporter registered, no request ever treated as a scrape.</summary>
 90040    public static MetricsOptions Disabled { get; } = new() { Port = 0 };
 41
 42    /// <summary>The port the scrape endpoint answers on, or <c>0</c> when metrics are switched off.</summary>
 761743    public int Port { get; private init; }
 44
 45    /// <summary>Whether the exporter is registered and the scrape endpoint served.</summary>
 425446    public bool Enabled => Port > 0;
 47
 48    /// <summary>
 49    ///     The configured value when it was unusable and <see cref="DefaultPort" /> was applied instead;
 50    ///     <c>null</c> otherwise. Kept so start-up can say so, instead of silently scraping somewhere
 51    ///     the operator did not ask for.
 52    /// </summary>
 91053    public string? InvalidValue { get; private init; }
 54
 89955    public static MetricsOptions FromEnvironment() => Parse(Environment.GetEnvironmentVariable(PortVariable));
 56
 57    /// <summary>
 58    ///     Interprets a <see cref="PortVariable" /> value. Split from <see cref="FromEnvironment" /> so
 59    ///     it can be tested without mutating the process environment the functional suite's hosts read.
 60    /// </summary>
 61    /// <remarks>
 62    ///     A blank value means "the default", as it does for every other optional variable here:
 63    ///     Compose hands the container an empty string for anything the env file leaves unset, and
 64    ///     Windows cannot hold an empty variable at all, so reading blank as "off" would make the
 65    ///     same env file mean different things on different hosts. Switching metrics off is the
 66    ///     explicit <c>0</c>.
 67    /// </remarks>
 68    public static MetricsOptions Parse(string? value)
 69    {
 91470        if (string.IsNullOrWhiteSpace(value))
 71        {
 90772            return new MetricsOptions { Port = DefaultPort };
 73        }
 74
 775        if (int.TryParse(value.Trim(), out var port) && port is >= 0 and <= 65535)
 76        {
 477            return new MetricsOptions { Port = port };
 78        }
 79
 380        return new MetricsOptions { Port = DefaultPort, InvalidValue = value };
 81    }
 82
 83    /// <summary>
 84    ///     The predicate the scrape endpoint is mounted behind: the metrics path, arriving on the
 85    ///     metrics port. Anything else — the same path on the public port included — falls through
 86    ///     to the rest of the pipeline, which has no such route and answers 404.
 87    /// </summary>
 88    /// <remarks>
 89    ///     <see cref="Enabled" /> is checked first and is not redundant. An in-memory test host
 90    ///     reports a local port of <c>0</c>, so without it a disabled configuration (port <c>0</c>)
 91    ///     would match every request for <c>/metrics</c> there.
 92    /// </remarks>
 93    public bool IsScrapeRequest(HttpContext context) =>
 154394        Enabled
 154395        && context.Connection.LocalPort == Port
 154396        && context.Request.Path.Equals(ScrapePath, StringComparison.OrdinalIgnoreCase);
 97}