diff --git a/src/Promitor.Core.Configuration/Defaults.cs b/src/Promitor.Core.Configuration/Defaults.cs index ec2b881e6..2fe691fb3 100644 --- a/src/Promitor.Core.Configuration/Defaults.cs +++ b/src/Promitor.Core.Configuration/Defaults.cs @@ -12,6 +12,7 @@ public static class Server public static class Prometheus { public static string ScrapeEndpointBaseUri { get; } = "/metrics"; + public static double MetricUnavailableValue { get; } = double.NaN; } public static class MetricsConfiguration diff --git a/src/Promitor.Core.Configuration/Model/Prometheus/PrometheusConfiguration.cs b/src/Promitor.Core.Configuration/Model/Prometheus/PrometheusConfiguration.cs index ca9bc6571..a9a85e7a1 100644 --- a/src/Promitor.Core.Configuration/Model/Prometheus/PrometheusConfiguration.cs +++ b/src/Promitor.Core.Configuration/Model/Prometheus/PrometheusConfiguration.cs @@ -3,5 +3,6 @@ public class PrometheusConfiguration { public ScrapeEndpointConfiguration ScrapeEndpoint { get; set; } = new ScrapeEndpointConfiguration(); + public double? MetricUnavailableValue { get; set; } = Defaults.Prometheus.MetricUnavailableValue; } } diff --git a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs index b4716c272..925d21c40 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -6,6 +6,7 @@ using Promitor.Core.Scraping.Configuration.Model; using Promitor.Core.Scraping.Configuration.Model.Metrics; using Promitor.Core.Scraping.Interfaces; +using Promitor.Core.Scraping.Prometheus.Interfaces; using Promitor.Core.Scraping.ResourceTypes; using Promitor.Core.Telemetry.Interfaces; using Promitor.Core.Telemetry.Metrics.Interfaces; @@ -38,12 +39,13 @@ public MetricScraperFactory(IConfiguration configuration, FeatureToggleClient fe /// /// Metadata concerning the Azure resources /// Resource type to scrape + /// Metrics collector for our Prometheus scraping endpoint /// Metrics collector for our runtime public IScraper CreateScraper(ResourceType metricDefinitionResourceType, AzureMetadata azureMetadata, - IRuntimeMetricsCollector runtimeMetricsCollector) + IPrometheusMetricWriter prometheusMetricWriter, IRuntimeMetricsCollector runtimeMetricsCollector) { var azureMonitorClient = CreateAzureMonitorClient(azureMetadata, runtimeMetricsCollector); - var scraperConfiguration = new ScraperConfiguration(azureMetadata, azureMonitorClient, _featureToggleClient, _logger, _exceptionTracker); + var scraperConfiguration = new ScraperConfiguration(azureMetadata, azureMonitorClient, prometheusMetricWriter, _featureToggleClient, _logger, _exceptionTracker); switch (metricDefinitionResourceType) { diff --git a/src/Promitor.Core.Scraping/Prometheus/Interfaces/IPrometheusMetricWriter.cs b/src/Promitor.Core.Scraping/Prometheus/Interfaces/IPrometheusMetricWriter.cs new file mode 100644 index 000000000..100700965 --- /dev/null +++ b/src/Promitor.Core.Scraping/Prometheus/Interfaces/IPrometheusMetricWriter.cs @@ -0,0 +1,9 @@ +using Promitor.Core.Scraping.Configuration.Model.Metrics; + +namespace Promitor.Core.Scraping.Prometheus.Interfaces +{ + public interface IPrometheusMetricWriter + { + void ReportMetric(MetricDefinition metricDefinition, ScrapeResult scrapedMetricResult); + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Prometheus/PrometheusMetricWriter.cs b/src/Promitor.Core.Scraping/Prometheus/PrometheusMetricWriter.cs new file mode 100644 index 000000000..d64252121 --- /dev/null +++ b/src/Promitor.Core.Scraping/Prometheus/PrometheusMetricWriter.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using GuardNet; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Prometheus.Client; +using Promitor.Core.Configuration; +using Promitor.Core.Configuration.FeatureFlags; +using Promitor.Core.Configuration.Model.Prometheus; +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Core.Scraping.Prometheus.Interfaces; + +namespace Promitor.Core.Scraping.Prometheus +{ + public class PrometheusMetricWriter : IPrometheusMetricWriter + { + private readonly FeatureToggleClient _featureToggleClient; + private readonly IOptionsMonitor _prometheusConfiguration; + private readonly ILogger _logger; + + public PrometheusMetricWriter(FeatureToggleClient featureToggleClient, IOptionsMonitor prometheusConfiguration, ILogger logger) + { + Guard.NotNull(featureToggleClient, nameof(featureToggleClient)); + Guard.NotNull(prometheusConfiguration, nameof(prometheusConfiguration)); + Guard.NotNull(logger, nameof(logger)); + + _featureToggleClient = featureToggleClient; + _prometheusConfiguration = prometheusConfiguration; + _logger = logger; + } + + public void ReportMetric(MetricDefinition metricDefinition, ScrapeResult scrapedMetricResult) + { + var metricsTimestampFeatureFlag = _featureToggleClient.IsActive(ToggleNames.DisableMetricTimestamps, defaultFlagState: true); + + var labels = DetermineLabels(metricDefinition, scrapedMetricResult); + + var gauge = Metrics.CreateGauge(metricDefinition.Name, metricDefinition.Description, includeTimestamp: metricsTimestampFeatureFlag, labelNames: labels.Names); + var metricValue = DetermineMetricMeasurement(scrapedMetricResult); + gauge.WithLabels(labels.Values).Set(metricValue); + } + + private double DetermineMetricMeasurement(ScrapeResult scrapedMetricResult) + { + var metricUnavailableValue = _prometheusConfiguration.CurrentValue?.MetricUnavailableValue ?? Defaults.Prometheus.MetricUnavailableValue; + return scrapedMetricResult.MetricValue ?? metricUnavailableValue; + } + + private (string[] Names, string[] Values) DetermineLabels(MetricDefinition metricDefinition, ScrapeResult scrapeResult) + { + var labels = new Dictionary(scrapeResult.Labels); + + if (metricDefinition?.Labels?.Any() == true) + { + foreach (var customLabel in metricDefinition.Labels) + { + if (labels.ContainsKey(customLabel.Key)) + { + _logger.LogWarning("Custom label '{CustomLabelName}' was already specified with value 'LabelValue' instead of 'CustomLabelValue'. Ignoring...", customLabel.Key, labels[customLabel.Key], customLabel.Value); + continue; + } + + labels.Add(customLabel.Key, customLabel.Value); + } + } + + return (labels.Keys.ToArray(), labels.Values.ToArray()); + } + } +} diff --git a/src/Promitor.Core.Scraping/ScrapeResult.cs b/src/Promitor.Core.Scraping/ScrapeResult.cs index 371c0e27d..d556e799e 100644 --- a/src/Promitor.Core.Scraping/ScrapeResult.cs +++ b/src/Promitor.Core.Scraping/ScrapeResult.cs @@ -12,7 +12,7 @@ public class ScrapeResult /// Resource group name that contains the resource that was scraped /// Uri of the resource that was scraped /// Value of the metric that was found - public ScrapeResult(string subscriptionId, string resourceGroupName, string resourceUri, double metricValue) : this(subscriptionId, resourceGroupName, string.Empty, resourceUri, metricValue, new Dictionary()) + public ScrapeResult(string subscriptionId, string resourceGroupName, string resourceUri, double? metricValue) : this(subscriptionId, resourceGroupName, string.Empty, resourceUri, metricValue, new Dictionary()) { } @@ -25,7 +25,7 @@ public class ScrapeResult /// Uri of the resource that was scraped /// Value of the metric that was found /// A collection of custom labels to add to the scraping result - public ScrapeResult(string subscriptionId, string resourceGroupName, string instanceName, string resourceUri, double metricValue, Dictionary customLabels) + public ScrapeResult(string subscriptionId, string resourceGroupName, string instanceName, string resourceUri, double? metricValue, Dictionary customLabels) { Guard.NotNullOrEmpty(subscriptionId, nameof(subscriptionId)); Guard.NotNullOrEmpty(resourceGroupName, nameof(resourceGroupName)); @@ -61,7 +61,7 @@ public ScrapeResult(string subscriptionId, string resourceGroupName, string inst /// Name of the resource that is being scraped /// Uri of the resource that was scraped /// Value of the metric that was found - public ScrapeResult(string subscriptionId, string resourceGroupName, string instanceName, string resourceUri, double metricValue) : this(subscriptionId, resourceGroupName, instanceName,resourceUri,metricValue, new Dictionary()) + public ScrapeResult(string subscriptionId, string resourceGroupName, string instanceName, string resourceUri, double? metricValue) : this(subscriptionId, resourceGroupName, instanceName,resourceUri,metricValue, new Dictionary()) { } @@ -88,7 +88,7 @@ public ScrapeResult(string subscriptionId, string resourceGroupName, string inst /// /// Value of the metric that was found /// - public double MetricValue { get; } + public double? MetricValue { get; } public Dictionary Labels { get; } = new Dictionary(); } diff --git a/src/Promitor.Core.Scraping/Scraper.cs b/src/Promitor.Core.Scraping/Scraper.cs index 03c511f71..457075e05 100644 --- a/src/Promitor.Core.Scraping/Scraper.cs +++ b/src/Promitor.Core.Scraping/Scraper.cs @@ -1,18 +1,16 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using GuardNet; using Microsoft.Azure.Management.Monitor.Fluent.Models; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Prometheus.Client; -using Promitor.Core.Configuration.FeatureFlags; using Promitor.Core.Scraping.Configuration.Model; using Promitor.Core.Scraping.Interfaces; +using Promitor.Core.Scraping.Prometheus.Interfaces; using Promitor.Core.Telemetry.Interfaces; using Promitor.Integrations.AzureMonitor; using MetricDefinition = Promitor.Core.Scraping.Configuration.Model.Metrics.MetricDefinition; + // ReSharper disable All namespace Promitor.Core.Scraping @@ -22,12 +20,12 @@ namespace Promitor.Core.Scraping /// /// Type of metric definition that is being used public abstract class Scraper : IScraper - where TMetricDefinition : MetricDefinition, new() + where TMetricDefinition : MetricDefinition, new() { - private readonly ScraperConfiguration _scraperConfiguration; private readonly IExceptionTracker _exceptionTracker; - private readonly FeatureToggleClient _featureToggleClient; private readonly ILogger _logger; + private readonly IPrometheusMetricWriter _prometheusMetricWriter; + private readonly ScraperConfiguration _scraperConfiguration; /// /// Constructor @@ -37,9 +35,9 @@ protected Scraper(ScraperConfiguration scraperConfiguration) Guard.NotNull(scraperConfiguration, nameof(scraperConfiguration)); _logger = scraperConfiguration.Logger; - _featureToggleClient = scraperConfiguration.FeatureToggleClient; _exceptionTracker = scraperConfiguration.ExceptionTracker; _scraperConfiguration = scraperConfiguration; + _prometheusMetricWriter = scraperConfiguration.PrometheusMetricWriter; AzureMetadata = scraperConfiguration.AzureMetadata; AzureMonitorClient = scraperConfiguration.AzureMonitorClient; @@ -81,7 +79,7 @@ public async Task ScrapeAsync(MetricDefinition metricDefinition) _logger.LogInformation("Found value '{MetricValue}' for metric '{MetricName}' with aggregation interval '{AggregationInterval}'", scrapedMetricResult, metricDefinition.Name, aggregationInterval); - ReportMetric(metricDefinition, scrapedMetricResult); + _prometheusMetricWriter.ReportMetric(metricDefinition, scrapedMetricResult); } catch (ErrorResponseException errorResponseException) { @@ -93,16 +91,6 @@ public async Task ScrapeAsync(MetricDefinition metricDefinition) } } - private void ReportMetric(MetricDefinition metricDefinition, ScrapeResult scrapedMetricResult) - { - var metricsTimestampFeatureFlag = _featureToggleClient.IsActive(ToggleNames.DisableMetricTimestamps, defaultFlagState: true); - - var labels = DetermineLabels(metricDefinition, scrapedMetricResult); - - var gauge = Metrics.CreateGauge(metricDefinition.Name, metricDefinition.Description, includeTimestamp: metricsTimestampFeatureFlag, labelNames: labels.Names); - gauge.WithLabels(labels.Values).Set(scrapedMetricResult.MetricValue); - } - private void HandleErrorResponseException(ErrorResponseException errorResponseException) { string reason = string.Empty; @@ -141,27 +129,6 @@ private void HandleErrorResponseException(ErrorResponseException errorResponseEx _exceptionTracker.Track(new Exception(reason)); } - private (string[] Names, string[] Values) DetermineLabels(MetricDefinition metricDefinition, ScrapeResult scrapeResult) - { - var labels = new Dictionary(scrapeResult.Labels); - - if (metricDefinition?.Labels?.Any() == true) - { - foreach (var customLabel in metricDefinition.Labels) - { - if (labels.ContainsKey(customLabel.Key)) - { - _logger.LogWarning("Custom label '{CustomLabelName}' was already specified with value 'LabelValue' instead of 'CustomLabelValue'. Ignoring...", customLabel.Key, labels[customLabel.Key], customLabel.Value); - continue; - } - - labels.Add(customLabel.Key, customLabel.Value); - } - } - - return (labels.Keys.ToArray(), labels.Values.ToArray()); - } - /// /// Scrapes the configured resource /// @@ -170,7 +137,6 @@ private void HandleErrorResponseException(ErrorResponseException errorResponseEx /// Definition of the metric to scrape /// Aggregation for the metric to use /// Interval that is used to aggregate metrics - /// protected abstract Task ScrapeResourceAsync(string subscriptionId, string resourceGroupName, TMetricDefinition metricDefinition, AggregationType aggregationType, TimeSpan aggregationInterval); } -} +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/ScraperConfiguration.cs b/src/Promitor.Core.Scraping/ScraperConfiguration.cs index 7dc7b8a0a..2122d59c6 100644 --- a/src/Promitor.Core.Scraping/ScraperConfiguration.cs +++ b/src/Promitor.Core.Scraping/ScraperConfiguration.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Configuration.FeatureFlags; using Promitor.Core.Scraping.Configuration.Model; +using Promitor.Core.Scraping.Prometheus.Interfaces; using Promitor.Core.Telemetry.Interfaces; using Promitor.Integrations.AzureMonitor; @@ -19,6 +20,11 @@ public class ScraperConfiguration /// public AzureMonitorClient AzureMonitorClient { get; } + /// + /// Metrics collector for our Prometheus scraping endpoint + /// + public IPrometheusMetricWriter PrometheusMetricWriter { get; } + /// /// Interaction with feature flags /// @@ -39,19 +45,22 @@ public class ScraperConfiguration /// /// Metadata concerning the Azure resources /// Client to communicate with Azure Monitor + /// Metrics collector for our Prometheus scraping endpoint /// Interaction with feature flags /// General logger /// Exception tracker - public ScraperConfiguration(AzureMetadata azureMetadata, AzureMonitorClient azureMonitorClient, FeatureToggleClient featureToggleClient, ILogger logger, IExceptionTracker exceptionTracker) + public ScraperConfiguration(AzureMetadata azureMetadata, AzureMonitorClient azureMonitorClient, IPrometheusMetricWriter prometheusMetricWriter, FeatureToggleClient featureToggleClient, ILogger logger, IExceptionTracker exceptionTracker) { Guard.NotNull(azureMetadata, nameof(azureMetadata)); Guard.NotNull(azureMonitorClient, nameof(azureMonitorClient)); + Guard.NotNull(prometheusMetricWriter, nameof(prometheusMetricWriter)); Guard.NotNull(featureToggleClient, nameof(featureToggleClient)); Guard.NotNull(logger, nameof(logger)); Guard.NotNull(exceptionTracker, nameof(exceptionTracker)); AzureMetadata = azureMetadata; AzureMonitorClient = azureMonitorClient; + PrometheusMetricWriter = prometheusMetricWriter; FeatureToggleClient = featureToggleClient; Logger = logger; ExceptionTracker = exceptionTracker; diff --git a/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs b/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs index 096d37839..3b6fcdebf 100644 --- a/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs +++ b/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs @@ -53,7 +53,7 @@ public AzureMonitorClient(string tenantId, string subscriptionId, string applica /// Id of the resource to query /// Optional filter to filter out metrics /// Latest representation of the metric - public async Task QueryMetricAsync(string metricName, AggregationType aggregationType, TimeSpan aggregationInterval, + public async Task QueryMetricAsync(string metricName, AggregationType aggregationType, TimeSpan aggregationInterval, string resourceId, string metricFilter = null) { Guard.NotNullOrWhitespace(metricName, nameof(metricName)); @@ -159,20 +159,20 @@ private MetricValue GetMostRecentMetricValue(string metricName, IReadOnlyList new MetricScrapingJob(metric, metricsProvider, + serviceProvider.GetService(), serviceProvider.GetService(), serviceProvider.GetService(), serviceProvider.GetService(), @@ -70,6 +73,7 @@ public static IServiceCollection DefineDependencies(this IServiceCollection serv services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/src/Promitor.Scraper.Host/Scheduling/MetricScrapingJob.cs b/src/Promitor.Scraper.Host/Scheduling/MetricScrapingJob.cs index 7c58f5c2c..25d4e4412 100644 --- a/src/Promitor.Scraper.Host/Scheduling/MetricScrapingJob.cs +++ b/src/Promitor.Scraper.Host/Scheduling/MetricScrapingJob.cs @@ -8,6 +8,7 @@ using Promitor.Core.Scraping.Configuration.Model.Metrics; using Promitor.Core.Scraping.Configuration.Providers.Interfaces; using Promitor.Core.Scraping.Factories; +using Promitor.Core.Scraping.Prometheus.Interfaces; using Promitor.Core.Telemetry.Interfaces; using Promitor.Core.Telemetry.Metrics.Interfaces; @@ -17,6 +18,7 @@ public class MetricScrapingJob : IScheduledJob { private readonly MetricDefinition _metric; private readonly IMetricsDeclarationProvider _metricsDeclarationProvider; + private readonly IPrometheusMetricWriter _prometheusMetricWriter; private readonly IRuntimeMetricsCollector _runtimeMetricsCollector; private readonly IExceptionTracker _exceptionTracker; private readonly ILogger _logger; @@ -25,17 +27,22 @@ public class MetricScrapingJob : IScheduledJob public MetricScrapingJob(MetricDefinition metric, IMetricsDeclarationProvider metricsDeclarationProvider, + IPrometheusMetricWriter prometheusMetricWriter, IRuntimeMetricsCollector runtimeMetricsCollector, MetricScraperFactory metricScraperFactory, ILogger logger, IExceptionTracker exceptionTracker) { Guard.NotNull(metric, nameof(metric)); - Guard.NotNull(exceptionTracker, nameof(exceptionTracker)); - Guard.NotNull(logger, nameof(logger)); + Guard.NotNull(metricsDeclarationProvider, nameof(metricsDeclarationProvider)); + Guard.NotNull(prometheusMetricWriter, nameof(prometheusMetricWriter)); + Guard.NotNull(runtimeMetricsCollector, nameof(runtimeMetricsCollector)); + Guard.NotNull(metricScraperFactory, nameof(metricScraperFactory)); Guard.NotNull(logger, nameof(logger)); + Guard.NotNull(exceptionTracker, nameof(exceptionTracker)); _metric = metric; _metricsDeclarationProvider = metricsDeclarationProvider; + _prometheusMetricWriter = prometheusMetricWriter; _runtimeMetricsCollector = runtimeMetricsCollector; _exceptionTracker = exceptionTracker; _logger = logger; @@ -76,7 +83,7 @@ private async Task ScrapeMetric(AzureMetadata azureMetadata, MetricDefinition me { _logger.LogInformation("Scraping '{MetricName}' for resource type '{ResourceType}'", metricDefinitionDefinition.Name, metricDefinitionDefinition.ResourceType); - var scraper = _metricScraperFactory.CreateScraper(metricDefinitionDefinition.ResourceType, azureMetadata, _runtimeMetricsCollector); + var scraper = _metricScraperFactory.CreateScraper(metricDefinitionDefinition.ResourceType, azureMetadata, _prometheusMetricWriter, _runtimeMetricsCollector); await scraper.ScrapeAsync(metricDefinitionDefinition); } } diff --git a/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs b/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs index 7c23eb2a1..a3bfc182b 100644 --- a/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs +++ b/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs @@ -61,8 +61,7 @@ private void LogMetricsDeclaration(MetricsDeclaration metricsDeclaration) private void SanitizeStorageQueueDeclaration(MetricDefinition metricDefinition) { - var storageQueueDeclaration = metricDefinition as StorageQueueMetricDefinition; - if (storageQueueDeclaration != null && string.IsNullOrWhiteSpace(storageQueueDeclaration.SasToken.RawValue) == false) + if (metricDefinition is StorageQueueMetricDefinition storageQueueDeclaration && string.IsNullOrWhiteSpace(storageQueueDeclaration.SasToken.RawValue) == false) { storageQueueDeclaration.SasToken.RawValue = "***"; } diff --git a/src/Promitor.Scraper.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs b/src/Promitor.Scraper.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs index df2219bcc..2626712ef 100644 --- a/src/Promitor.Scraper.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs +++ b/src/Promitor.Scraper.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs @@ -167,7 +167,7 @@ public async Task RuntimeConfiguration_HasConfiguredMetricsConfigurationBasePath // Arrange var metricsDeclarationBasePath = _faker.System.DirectoryPath(); var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() - .WithMetricsConfiguration(metricsDeclarationBasePath) + .WithMetricsConfiguration(absolutePath: metricsDeclarationBasePath) .GenerateAsync(); // Act @@ -180,13 +180,31 @@ public async Task RuntimeConfiguration_HasConfiguredMetricsConfigurationBasePath Assert.Equal(metricsDeclarationBasePath, runtimeConfiguration.MetricsConfiguration.AbsolutePath); } + [Fact] + public async Task RuntimeConfiguration_HasConfiguredMetricUnavailableValue_UsesConfigured() + { + // Arrange + var metricUnavailableValue = _faker.Random.Double(min: 1); + var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() + .WithPrometheusConfiguration(metricUnavailableValue: metricUnavailableValue) + .GenerateAsync(); + + // Act + var runtimeConfiguration = configuration.Get(); + + // Assert + Assert.NotNull(runtimeConfiguration); + Assert.NotNull(runtimeConfiguration.Prometheus); + Assert.Equal(metricUnavailableValue, runtimeConfiguration.Prometheus.MetricUnavailableValue); + } + [Fact] public async Task RuntimeConfiguration_HasConfiguredPrometheusScrapeEndpointConfigured_UsesConfigured() { // Arrange var scrapeEndpointBaseUri = _faker.System.DirectoryPath(); var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() - .WithPrometheusConfiguration(scrapeEndpointBaseUri) + .WithPrometheusConfiguration(scrapeEndpointBaseUri: scrapeEndpointBaseUri) .GenerateAsync(); // Act @@ -293,7 +311,24 @@ public async Task RuntimeConfiguration_HasNoMetricsConfigurationBasePathConfigur { // Arrange var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() - .WithMetricsConfiguration(null) + .WithMetricsConfiguration(absolutePath: null) + .GenerateAsync(); + + // Act + var runtimeConfiguration = configuration.Get(); + + // Assert + Assert.NotNull(runtimeConfiguration); + Assert.NotNull(runtimeConfiguration.MetricsConfiguration); + Assert.Equal(Defaults.MetricsConfiguration.AbsolutePath, runtimeConfiguration.MetricsConfiguration.AbsolutePath); + } + + [Fact] + public async Task RuntimeConfiguration_HasNoMetricUnavailableValuePathConfigured_UsesDefault() + { + // Arrange + var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() + .WithPrometheusConfiguration(metricUnavailableValue: null) .GenerateAsync(); // Act @@ -303,7 +338,27 @@ public async Task RuntimeConfiguration_HasNoMetricsConfigurationBasePathConfigur Assert.NotNull(runtimeConfiguration); Assert.NotNull(runtimeConfiguration.Prometheus); Assert.NotNull(runtimeConfiguration.Prometheus.ScrapeEndpoint); - Assert.Equal(Defaults.MetricsConfiguration.AbsolutePath, runtimeConfiguration.MetricsConfiguration.AbsolutePath); + Assert.NotEqual(Defaults.Prometheus.ScrapeEndpointBaseUri, runtimeConfiguration.Prometheus.ScrapeEndpoint.BaseUriPath); + Assert.Equal(Defaults.Prometheus.MetricUnavailableValue, runtimeConfiguration.Prometheus.MetricUnavailableValue); + } + + [Fact] + public async Task RuntimeConfiguration_HasNoPrometheusConfigurationConfigured_UsesDefault() + { + // Arrange + var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() + .WithPrometheusConfiguration(metricUnavailableValue: null, scrapeEndpointBaseUri: null) + .GenerateAsync(); + + // Act + var runtimeConfiguration = configuration.Get(); + + // Assert + Assert.NotNull(runtimeConfiguration); + Assert.NotNull(runtimeConfiguration.Prometheus); + Assert.NotNull(runtimeConfiguration.Prometheus.ScrapeEndpoint); + Assert.Equal(Defaults.Prometheus.ScrapeEndpointBaseUri, runtimeConfiguration.Prometheus.ScrapeEndpoint.BaseUriPath); + Assert.Equal(Defaults.Prometheus.MetricUnavailableValue, runtimeConfiguration.Prometheus.MetricUnavailableValue); } [Fact] @@ -311,7 +366,7 @@ public async Task RuntimeConfiguration_HasNoPrometheusScrapeEndpointConfigured_U { // Arrange var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() - .WithPrometheusConfiguration(null) + .WithPrometheusConfiguration(scrapeEndpointBaseUri:null) .GenerateAsync(); // Act @@ -322,6 +377,7 @@ public async Task RuntimeConfiguration_HasNoPrometheusScrapeEndpointConfigured_U Assert.NotNull(runtimeConfiguration.Prometheus); Assert.NotNull(runtimeConfiguration.Prometheus.ScrapeEndpoint); Assert.Equal(Defaults.Prometheus.ScrapeEndpointBaseUri, runtimeConfiguration.Prometheus.ScrapeEndpoint.BaseUriPath); + Assert.NotEqual(Defaults.Prometheus.MetricUnavailableValue, runtimeConfiguration.Prometheus.MetricUnavailableValue); } [Fact] diff --git a/src/Promitor.Scraper.Tests.Unit/Generators/Config/BogusRuntimeConfigurationGenerator.cs b/src/Promitor.Scraper.Tests.Unit/Generators/Config/BogusRuntimeConfigurationGenerator.cs index a81a023fe..053bce1a7 100644 --- a/src/Promitor.Scraper.Tests.Unit/Generators/Config/BogusRuntimeConfigurationGenerator.cs +++ b/src/Promitor.Scraper.Tests.Unit/Generators/Config/BogusRuntimeConfigurationGenerator.cs @@ -32,7 +32,8 @@ internal static RuntimeConfiguration Generate() .Generate(); var prometheusConfiguration = new Faker() .StrictMode(true) - .RuleFor(flagsConfiguration => flagsConfiguration.ScrapeEndpoint, scrapeEndpointConfiguration) + .RuleFor(promConfiguration => promConfiguration.ScrapeEndpoint, scrapeEndpointConfiguration) + .RuleFor(promConfiguration => promConfiguration.MetricUnavailableValue, faker => faker.Random.Double(min: 1)) .Generate(); var containerLogConfiguration = new Faker() diff --git a/src/Promitor.Scraper.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs b/src/Promitor.Scraper.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs index 4d14e2b77..552a401a9 100644 --- a/src/Promitor.Scraper.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs +++ b/src/Promitor.Scraper.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs @@ -46,17 +46,30 @@ public static RuntimeConfigurationGenerator WithRuntimeConfiguration(RuntimeConf return new RuntimeConfigurationGenerator(runtimeConfiguration); } - public RuntimeConfigurationGenerator WithPrometheusConfiguration(string scrapeEndpointBaseUri = "/scrape-endpoint") + public RuntimeConfigurationGenerator WithPrometheusConfiguration(double? metricUnavailableValue = -1, string scrapeEndpointBaseUri = "/scrape-endpoint") { - var prometheusConfiguration = scrapeEndpointBaseUri == null - ? null - : new PrometheusConfiguration + PrometheusConfiguration prometheusConfiguration; + if (string.IsNullOrWhiteSpace(scrapeEndpointBaseUri) && metricUnavailableValue == null) + { + prometheusConfiguration = null; + } + else + { + prometheusConfiguration = new PrometheusConfiguration(); + + if (string.IsNullOrWhiteSpace(scrapeEndpointBaseUri) == false) { - ScrapeEndpoint = new ScrapeEndpointConfiguration + prometheusConfiguration.ScrapeEndpoint = new ScrapeEndpointConfiguration { BaseUriPath = scrapeEndpointBaseUri - } - }; + }; + } + + if (metricUnavailableValue != null) + { + prometheusConfiguration.MetricUnavailableValue = (double)metricUnavailableValue; + } + } _runtimeConfiguration.Prometheus = prometheusConfiguration; @@ -164,12 +177,20 @@ public async Task GenerateAsync() configurationBuilder.AppendLine(" scrapeEndpoint:"); configurationBuilder.AppendLine($" baseUriPath: {_runtimeConfiguration?.Prometheus.ScrapeEndpoint.BaseUriPath}"); } + + if (_runtimeConfiguration?.Prometheus.MetricUnavailableValue != null) + { + configurationBuilder.AppendLine($" metricUnavailableValue: {_runtimeConfiguration?.Prometheus.MetricUnavailableValue}"); + } } if (_runtimeConfiguration?.MetricsConfiguration != null) { configurationBuilder.AppendLine("metricsConfiguration:"); - configurationBuilder.AppendLine($" absolutePath: {_runtimeConfiguration?.MetricsConfiguration.AbsolutePath}"); + if (_runtimeConfiguration?.MetricsConfiguration.AbsolutePath != null) + { + configurationBuilder.AppendLine($" absolutePath: {_runtimeConfiguration?.MetricsConfiguration.AbsolutePath}"); + } } if (_runtimeConfiguration?.Telemetry != null) diff --git a/src/Promitor.sln.DotSettings b/src/Promitor.sln.DotSettings index 5f5cb44b9..357b73ca6 100644 --- a/src/Promitor.sln.DotSettings +++ b/src/Promitor.sln.DotSettings @@ -3,6 +3,8 @@ SUGGESTION SUGGESTION SUGGESTION + SUGGESTION + SUGGESTION SUGGESTION SUGGESTION False diff --git a/src/runtime-config.yaml b/src/runtime-config.yaml index c69538af7..006955511 100644 --- a/src/runtime-config.yaml +++ b/src/runtime-config.yaml @@ -1,6 +1,7 @@ server: httpPort: 88 prometheus: + metricUnavailableValue: NaN scrapeEndpoint: baseUriPath: /scrape metricsConfiguration: