diff --git a/config/promitor/scraper/runtime.yaml b/config/promitor/scraper/runtime.yaml index 13bf42f00..7df2000ee 100644 --- a/config/promitor/scraper/runtime.yaml +++ b/config/promitor/scraper/runtime.yaml @@ -28,4 +28,7 @@ telemetry: azureMonitor: logging: informationLevel: Headers - isEnabled: true \ No newline at end of file + isEnabled: true +resourceDiscovery: + host: promitor.agents.resourcediscovery + port: 88 \ No newline at end of file diff --git a/src/Promitor.Agents.Core/Extensions/IServiceCollectionExtensions.cs b/src/Promitor.Agents.Core/Extensions/IServiceCollectionExtensions.cs index e20e5dc1c..8ffe58f0b 100644 --- a/src/Promitor.Agents.Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/Promitor.Agents.Core/Extensions/IServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Converters; using Swashbuckle.AspNetCore.Filters; // ReSharper disable once CheckNamespace @@ -31,14 +32,17 @@ public static IServiceCollection UseWebApi(this IServiceCollection services) options.RespectBrowserAcceptHeader = true; RestrictToJsonContentType(options); - AddEnumAsStringRepresentation(options); }) .AddJsonOptions(jsonOptions => { jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); jsonOptions.JsonSerializerOptions.IgnoreNullValues = true; }) - .AddNewtonsoftJson(); + .AddNewtonsoftJson(jsonOptions => + { + jsonOptions.SerializerSettings.Converters.Add(new StringEnumConverter()); + jsonOptions.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + }); return services; } @@ -100,28 +104,13 @@ private static void RestrictToJsonContentType(MvcOptions options) options.OutputFormatters.RemoveType(); } - private static void AddEnumAsStringRepresentation(MvcOptions options) - { - var onlyJsonInputFormatters = options.InputFormatters.OfType(); - foreach (SystemTextJsonInputFormatter inputFormatter in onlyJsonInputFormatters) - { - inputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - } - - var onlyJsonOutputFormatters = options.OutputFormatters.OfType(); - foreach (SystemTextJsonOutputFormatter outputFormatter in onlyJsonOutputFormatters) - { - outputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - } - } - private static string GetXmlDocumentationPath(IServiceCollection services, string docFileName = "Open-Api.xml") { var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IWebHostEnvironment)); if (hostingEnvironment == null) return string.Empty; - var contentRootPath = ((IWebHostEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath; + var contentRootPath = ((IWebHostEnvironment) hostingEnvironment.ImplementationInstance).ContentRootPath; var xmlDocumentationPath = $"{contentRootPath}/Docs/{docFileName}"; return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty; diff --git a/src/Promitor.Agents.Core/Serialization/HealthReportEntryConverter.cs b/src/Promitor.Agents.Core/Serialization/HealthReportEntryConverter.cs new file mode 100644 index 000000000..70bb8389b --- /dev/null +++ b/src/Promitor.Agents.Core/Serialization/HealthReportEntryConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Promitor.Agents.Core.Serialization +{ + /// + /// JSON converter to deserialize struct. + /// The structs is not correctly deserialized since it was not especially made to be deserialized; + /// also structs are naturally not made for deserialization using Newtonsoft.Json. + /// + public class HealthReportEntryConverter : JsonConverter + { + /// Writes the JSON representation of the object. + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// Reads the JSON representation of the object. + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + + var healthStatus = token["status"].ToObject(); + var description = token["description"]?.ToObject(); + var duration = token["duration"].ToObject(); + var exception = token["exception"]?.ToObject(); + var data = token["data"]?.ToObject>(); + var readOnlyDictionary = new ReadOnlyDictionary(data ?? new Dictionary()); + var tags = token["tags"]?.ToObject(); + + return new HealthReportEntry(healthStatus, description, duration, exception, readOnlyDictionary, tags); + } + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return typeof(HealthReportEntry) == objectType; + } + } +} diff --git a/src/Promitor.Agents.Scraper/Configuration/ResourceDiscoveryConfiguration.cs b/src/Promitor.Agents.Scraper/Configuration/ResourceDiscoveryConfiguration.cs new file mode 100644 index 000000000..b73900cea --- /dev/null +++ b/src/Promitor.Agents.Scraper/Configuration/ResourceDiscoveryConfiguration.cs @@ -0,0 +1,10 @@ +namespace Promitor.Agents.Scraper.Configuration +{ + public class ResourceDiscoveryConfiguration + { + public string Host { get; set; } + public int? Port { get; set; } = 80; + + public bool IsConfigured => string.IsNullOrWhiteSpace(Host) == false; + } +} diff --git a/src/Promitor.Agents.Scraper/Configuration/ScraperRuntimeConfiguration.cs b/src/Promitor.Agents.Scraper/Configuration/ScraperRuntimeConfiguration.cs index 7f8557434..d2205ed3e 100644 --- a/src/Promitor.Agents.Scraper/Configuration/ScraperRuntimeConfiguration.cs +++ b/src/Promitor.Agents.Scraper/Configuration/ScraperRuntimeConfiguration.cs @@ -8,9 +8,10 @@ namespace Promitor.Agents.Scraper.Configuration { public class ScraperRuntimeConfiguration : RuntimeConfiguration { - public PrometheusLegacyConfiguration Prometheus { get; set; } = new PrometheusLegacyConfiguration(); - public MetricSinkConfiguration MetricSinks { get; set; } = new MetricSinkConfiguration(); - public MetricsConfiguration MetricsConfiguration { get; set; } = new MetricsConfiguration(); public AzureMonitorConfiguration AzureMonitor { get; set; } = new AzureMonitorConfiguration(); + public MetricsConfiguration MetricsConfiguration { get; set; } = new MetricsConfiguration(); + public MetricSinkConfiguration MetricSinks { get; set; } = new MetricSinkConfiguration(); + public PrometheusLegacyConfiguration Prometheus { get; set; } = new PrometheusLegacyConfiguration(); + public ResourceDiscoveryConfiguration ResourceDiscovery { get; set; } } } \ No newline at end of file diff --git a/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs b/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs new file mode 100644 index 000000000..5352ebf09 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Arcus.Observability.Telemetry.Core; +using GuardNet; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Promitor.Agents.Core.Serialization; +using Promitor.Agents.Scraper.Configuration; + +namespace Promitor.Agents.Scraper.Discovery +{ + public class ResourceDiscoveryClient + { + private readonly IOptionsMonitor _configuration; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public ResourceDiscoveryClient(IHttpClientFactory httpClientFactory, IOptionsMonitor configuration, ILogger logger) + { + Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(logger, nameof(logger)); + Guard.For(() => configuration.CurrentValue.IsConfigured == false, "Resource Discovery is not configured"); + + _logger = logger; + _configuration = configuration; + _httpClientFactory = httpClientFactory; + } + + public async Task> GetAsync(string resourceCollectionName) + { + var uri = $"/api/v1/resources/collections/{resourceCollectionName}/discovery"; + var rawResponse = await SendGetRequestAsync(uri); + var foundResources = JsonConvert.DeserializeObject>(rawResponse); + return foundResources; + } + + public async Task GetHealthAsync() + { + var rawResponse = await SendGetRequestAsync("/api/v1/health"); + var healthReport = JsonConvert.DeserializeObject(rawResponse, new HealthReportEntryConverter()); + return healthReport; + } + + private async Task SendGetRequestAsync(string uri) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri); + + var response = await SendRequestToApiAsync(request); + response.EnsureSuccessStatusCode(); + + var rawResponse = await response.Content.ReadAsStringAsync(); + return rawResponse; + } + + private async Task SendRequestToApiAsync(HttpRequestMessage request) + { + var client = CreateHttpClient(); + using (var dependencyMeasurement = DependencyMeasurement.Start()) + { + HttpResponseMessage response = null; + try + { + response = await client.SendAsync(request); + _logger.LogRequest(request, response, dependencyMeasurement.Elapsed); + + return response; + } + finally + { + var statusCode = response?.StatusCode ?? HttpStatusCode.InternalServerError; + _logger.LogHttpDependency(request, statusCode, dependencyMeasurement); + } + } + } + + private HttpClient CreateHttpClient() + { + var httpClient = _httpClientFactory.CreateClient("Promitor Resource Discovery"); + httpClient.BaseAddress = new Uri($"http://{_configuration.CurrentValue.Host}:{_configuration.CurrentValue.Port}"); + return httpClient; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryRepository.cs b/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryRepository.cs new file mode 100644 index 000000000..f0e9fda53 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryRepository.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GuardNet; + +namespace Promitor.Agents.Scraper.Discovery +{ + public class ResourceDiscoveryRepository + { + private readonly ResourceDiscoveryClient _resourceDiscoveryClient; + + public ResourceDiscoveryRepository(ResourceDiscoveryClient resourceDiscoveryClient) + { + Guard.NotNull(resourceDiscoveryClient, nameof(resourceDiscoveryClient)); + + _resourceDiscoveryClient = resourceDiscoveryClient; + } + + public async Task> GetResourceCollectionAsync(string resourceCollectionName) + { + Guard.NotNullOrWhitespace(resourceCollectionName,nameof(resourceCollectionName)); + + var resources = await _resourceDiscoveryClient.GetAsync(resourceCollectionName); + return resources; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs b/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs index 6fba9521d..0d523e05e 100644 --- a/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs +++ b/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Promitor.Agents.Core.Configuration.Telemetry.Sinks; using Promitor.Agents.Scraper.Configuration; using Promitor.Agents.Scraper.Configuration.Sinks; +using Promitor.Agents.Scraper.Discovery; using Promitor.Core.Scraping.Configuration.Providers; using Promitor.Core.Scraping.Configuration.Providers.Interfaces; using Promitor.Core.Scraping.Configuration.Serialization; @@ -92,6 +93,8 @@ public static IServiceCollection ScheduleMetricScraping(this IServiceCollection /// Collections of services in application public static IServiceCollection DefineDependencies(this IServiceCollection services) { + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -175,6 +178,7 @@ public static IServiceCollection ConfigureYamlConfiguration(this IServiceCollect { services.Configure(configuration); services.Configure(configuration.GetSection("metricsConfiguration")); + services.Configure(configuration.GetSection("resourceDiscovery")); services.Configure(configuration.GetSection("telemetry")); services.Configure(configuration.GetSection("server")); services.Configure(configuration.GetSection("prometheus")); diff --git a/src/Promitor.Agents.Scraper/Health/ResourceDiscoveryHealthCheck.cs b/src/Promitor.Agents.Scraper/Health/ResourceDiscoveryHealthCheck.cs new file mode 100644 index 000000000..fb389f6e6 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Health/ResourceDiscoveryHealthCheck.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using GuardNet; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Promitor.Agents.Scraper.Discovery; + +namespace Promitor.Agents.Scraper.Health +{ + public class ResourceDiscoveryHealthCheck : IHealthCheck + { + private readonly ResourceDiscoveryClient _resourceDiscoveryClient; + + public ResourceDiscoveryHealthCheck(ResourceDiscoveryClient resourceDiscoveryClient) + { + Guard.NotNull(resourceDiscoveryClient, nameof(resourceDiscoveryClient)); + + _resourceDiscoveryClient = resourceDiscoveryClient; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + { + try + { + var resourceDiscoveryHealthReport = await _resourceDiscoveryClient.GetHealthAsync(); + + if (resourceDiscoveryHealthReport.Status == HealthStatus.Healthy) + { + return HealthCheckResult.Healthy("Successfully contacted Promitor Resource Discovery"); + } + + return HealthCheckResult.Degraded("Successfully contacted Promitor Resource Discovery but it's not in healthy state."); + } + catch (Exception exception) + { + return HealthCheckResult.Unhealthy("Unable to contacted Promitor Resource Discovery.", exception: exception); + } + } + } +} \ No newline at end of file diff --git a/src/Promitor.Agents.Scraper/Startup.cs b/src/Promitor.Agents.Scraper/Startup.cs index ff05df8d8..637d106a0 100644 --- a/src/Promitor.Agents.Scraper/Startup.cs +++ b/src/Promitor.Agents.Scraper/Startup.cs @@ -11,6 +11,7 @@ using Promitor.Agents.Scraper.Configuration; using Promitor.Agents.Scraper.Configuration.Sinks; using Promitor.Agents.Scraper.Extensions; +using Promitor.Agents.Scraper.Health; using Promitor.Agents.Scraper.Validation; using Promitor.Core.Scraping.Configuration.Serialization.v1.Mapping; using Promitor.Integrations.AzureMonitor.Logging; @@ -26,7 +27,7 @@ public class Startup : AgentStartup private readonly string _legacyPrometheusUriPath; public Startup(IConfiguration configuration) - : base(configuration) + : base(configuration) { var runtimeConfiguration = configuration.Get(); _legacyPrometheusUriPath = runtimeConfiguration?.Prometheus?.ScrapeEndpoint?.BaseUriPath; @@ -36,14 +37,25 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { string openApiDescription = BuildOpenApiDescription(Configuration, _legacyPrometheusUriPath); + services.AddHttpClient("Promitor Resource Discovery", client => + { + // Provide Promitor User-Agent + client.DefaultRequestHeaders.Add("User-Agent", "Promitor Scraper"); + }); + services.UseWebApi() .AddHttpCorrelation() .AddAutoMapper(typeof(V1MappingProfile).Assembly) .DefineDependencies() .ConfigureYamlConfiguration(Configuration) - .UseOpenApiSpecifications("Promitor - Scraper API v1", openApiDescription, 1) - .AddHealthChecks() - .AddCheck("self", () => HealthCheckResult.Healthy()); + .UseOpenApiSpecifications("Promitor - Scraper API v1", openApiDescription, 1); + + var healthCheckBuilder = services.AddHealthChecks(); + var resourceDiscoveryConfiguration = Configuration.GetSection("resourceDiscovery").Get(); + if (resourceDiscoveryConfiguration?.IsConfigured == true) + { + healthCheckBuilder.AddCheck("Promitor Resource Discovery", HealthStatus.Degraded); + } ValidateRuntimeConfiguration(services); @@ -60,20 +72,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } app.UseExceptionHandling() - .UseRequestTracking() - .UseHttpCorrelation() - .UseRouting() - .UseMetricSinks(Configuration) - .AddPrometheusScraperMetricSink(_legacyPrometheusUriPath) // Deprecated and will be gone in 2.0 - .ExposeOpenApiUi() // New Swagger UI - .ExposeOpenApiUi(ApiName, swaggerUiOptions => - { - swaggerUiOptions.SwaggerEndpoint("/swagger/v1/swagger.json", ApiName); - swaggerUiOptions.SwaggerEndpoint("/api/v1/docs.json", "Promitor - Scraper API (OpenAPI 3.0)"); - swaggerUiOptions.ConfigureDefaultOptions(ApiName); - }, openApiOptions => openApiOptions.SerializeAsV2 = true) // Deprecated Swagger UI - .UseEndpoints(endpoints => endpoints.MapControllers()); - + .UseRequestTracking() + .UseHttpCorrelation() + .UseRouting() + .UseMetricSinks(Configuration) + .AddPrometheusScraperMetricSink(_legacyPrometheusUriPath) // Deprecated and will be gone in 2.0 + .ExposeOpenApiUi() // New Swagger UI + .ExposeOpenApiUi(ApiName, swaggerUiOptions => + { + swaggerUiOptions.SwaggerEndpoint("/swagger/v1/swagger.json", ApiName); + swaggerUiOptions.SwaggerEndpoint("/api/v1/docs.json", "Promitor - Scraper API (OpenAPI 3.0)"); + swaggerUiOptions.ConfigureDefaultOptions(ApiName); + }, openApiOptions => openApiOptions.SerializeAsV2 = true) // Deprecated Swagger UI + .UseEndpoints(endpoints => endpoints.MapControllers()); UseSerilog(ComponentName, app.ApplicationServices); } @@ -95,18 +106,15 @@ protected override LoggerConfiguration FilterTelemetry(LoggerConfiguration logge } standardConfiguration.Filter.With(new AzureMonitorLoggingFilter(azureMonitorConfiguration)); - return standardConfiguration; } private string BuildOpenApiDescription(IConfiguration configuration, string legacyPrometheusUriPath) { var metricSinkConfiguration = configuration.GetSection("metricSinks").Get(); - var openApiDescriptionBuilder = new StringBuilder(); openApiDescriptionBuilder.Append("Collection of APIs to manage the Promitor Scraper.\r\n\r\n"); openApiDescriptionBuilder.AppendLine("Configured metric sinks are:\r\n"); - if (string.IsNullOrWhiteSpace(legacyPrometheusUriPath) == false) { openApiDescriptionBuilder.AppendLine($"
  • Legacy Prometheus scrape endpoint is exposed at {legacyPrometheusUriPath}
  • "); diff --git a/src/Promitor.Agents.Scraper/Validation/RuntimeValidator.cs b/src/Promitor.Agents.Scraper/Validation/RuntimeValidator.cs index c954e555a..bee9f71df 100644 --- a/src/Promitor.Agents.Scraper/Validation/RuntimeValidator.cs +++ b/src/Promitor.Agents.Scraper/Validation/RuntimeValidator.cs @@ -33,6 +33,7 @@ public RuntimeValidator( new ConfigurationPathValidationStep(metricsConfiguration, _validationLogger), new AzureAuthenticationValidationStep(configuration, _validationLogger), new MetricsDeclarationValidationStep(scrapeConfigurationProvider, _validationLogger), + new ResourceDiscoveryValidationStep(runtimeConfiguration.Value.ResourceDiscovery, _validationLogger), new StatsDMetricSinkValidationStep(runtimeConfiguration, _validationLogger), new PrometheusScrapingEndpointMetricSinkValidationStep(runtimeConfiguration, _validationLogger) }; diff --git a/src/Promitor.Agents.Scraper/Validation/Steps/ResourceDiscoveryValidationStep.cs b/src/Promitor.Agents.Scraper/Validation/Steps/ResourceDiscoveryValidationStep.cs new file mode 100644 index 000000000..cd5c4caab --- /dev/null +++ b/src/Promitor.Agents.Scraper/Validation/Steps/ResourceDiscoveryValidationStep.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Promitor.Agents.Scraper.Configuration; +using Promitor.Agents.Scraper.Validation.Interfaces; + +namespace Promitor.Agents.Scraper.Validation.Steps +{ + public class ResourceDiscoveryValidationStep : ValidationStep, IValidationStep + { + private readonly ResourceDiscoveryConfiguration _configuration; + + public ResourceDiscoveryValidationStep(ResourceDiscoveryConfiguration configuration) : this(configuration, NullLogger.Instance) + { + } + + public ResourceDiscoveryValidationStep(ResourceDiscoveryConfiguration configuration, ILogger logger) : base( logger) + { + _configuration = configuration; + } + + public string ComponentName { get; } = "Resource Discovery"; + + public ValidationResult Run() + { + if (_configuration == null) + { + return ValidationResult.Successful(ComponentName); + } + + var errorMessages = new List(); + if (string.IsNullOrWhiteSpace(_configuration.Host)) + { + errorMessages.Add( "No host name for resource discovery was configured"); + } + + if (_configuration.Port <= 0) + { + errorMessages.Add($"No valid port ({_configuration.Port}) for resource discovery was configured"); + } + + return errorMessages.Any() ? ValidationResult.Failure(ComponentName, errorMessages) : ValidationResult.Successful(ComponentName); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/StatsDMetricSinkValidationStep.cs b/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/StatsDMetricSinkValidationStep.cs index 7bd53ba41..5b9d1ac95 100644 --- a/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/StatsDMetricSinkValidationStep.cs +++ b/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/StatsDMetricSinkValidationStep.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Promitor.Agents.Scraper.Configuration; @@ -6,7 +8,8 @@ namespace Promitor.Agents.Scraper.Validation.Steps.Sinks { - public class StatsDMetricSinkValidationStep : ValidationStep, IValidationStep + public class StatsDMetricSinkValidationStep : ValidationStep, + IValidationStep { private readonly IOptions _runtimeConfiguration; @@ -32,19 +35,18 @@ public ValidationResult Run() return ValidationResult.Successful(ComponentName); } + var errorMessages = new List(); if (string.IsNullOrWhiteSpace(statsDConfiguration.Host)) { - var errorMessage = "No host of StatsD server is configured"; - return ValidationResult.Failure(ComponentName, errorMessage); + errorMessages.Add("No host of StatsD server is configured"); } if (statsDConfiguration.Port <= 0) { - var errorMessage = $"StatsD port {statsDConfiguration.Port} is not allowed"; - return ValidationResult.Failure(ComponentName, errorMessage); + errorMessages.Add($"StatsD port {statsDConfiguration.Port} is not allowed"); } - return ValidationResult.Successful(ComponentName); + return errorMessages.Any() ? ValidationResult.Failure(ComponentName, errorMessages) : ValidationResult.Successful(ComponentName); } } -} +} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs b/src/Promitor.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs index ce25edb63..6adfe6492 100644 --- a/src/Promitor.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs +++ b/src/Promitor.Tests.Unit/Configuration/RuntimeConfigurationUnitTest.cs @@ -495,6 +495,24 @@ public async Task RuntimeConfiguration_HasNoPrometheusScrapeEndpointConfigured_U Assert.NotEqual(Defaults.Prometheus.MetricUnavailableValue, runtimeConfiguration.Prometheus.MetricUnavailableValue); } + [Fact] + public async Task RuntimeConfiguration_HasNoResourceDiscoveryPortConfigured_UsesDefault() + { + // Arrange + var configuration = await RuntimeConfigurationGenerator.WithServerConfiguration() + .WithResourceDiscovery(port: null) + .GenerateAsync(); + + // Act + var runtimeConfiguration = configuration.Get(); + + // Assert + Assert.NotNull(runtimeConfiguration); + Assert.NotNull(runtimeConfiguration.ResourceDiscovery); + Assert.NotNull(runtimeConfiguration.ResourceDiscovery.Port); + Assert.Equal(Defaults.Prometheus.ScrapeEndpointBaseUri, runtimeConfiguration.Prometheus.ScrapeEndpoint.BaseUriPath); + } + [Fact] public async Task RuntimeConfiguration_IsFullyConfigured_UsesCorrectValues() { @@ -517,7 +535,10 @@ public async Task RuntimeConfiguration_IsFullyConfigured_UsesCorrectValues() Assert.NotNull(runtimeConfiguration.Prometheus); Assert.NotNull(runtimeConfiguration.Prometheus.ScrapeEndpoint); Assert.NotNull(runtimeConfiguration.MetricsConfiguration); + Assert.NotNull(runtimeConfiguration.ResourceDiscovery); Assert.Equal(bogusRuntimeConfiguration.Server.HttpPort, runtimeConfiguration.Server.HttpPort); + Assert.Equal(bogusRuntimeConfiguration.ResourceDiscovery.Host, runtimeConfiguration.ResourceDiscovery.Host); + Assert.Equal(bogusRuntimeConfiguration.ResourceDiscovery.Port, runtimeConfiguration.ResourceDiscovery.Port); Assert.Equal(bogusRuntimeConfiguration.Telemetry.DefaultVerbosity, runtimeConfiguration.Telemetry.DefaultVerbosity); Assert.Equal(bogusRuntimeConfiguration.Telemetry.ApplicationInsights.Verbosity, runtimeConfiguration.Telemetry.ApplicationInsights.Verbosity); Assert.Equal(bogusRuntimeConfiguration.Telemetry.ApplicationInsights.InstrumentationKey, runtimeConfiguration.Telemetry.ApplicationInsights.InstrumentationKey); diff --git a/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs b/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs index e8147b115..afc45cf13 100644 --- a/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs +++ b/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs @@ -70,14 +70,20 @@ internal static ScraperRuntimeConfiguration Generate() .RuleFor(sinkConfiguration => sinkConfiguration.Statsd, statsDConfiguration) .RuleFor(sinkConfiguration => sinkConfiguration.PrometheusScrapingEndpoint, prometheusScrapingEndpointSinkConfiguration) .Generate(); + var resourceDiscovery = new Faker() + .StrictMode(true) + .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Host, faker => faker.Person.FirstName) + .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Port, faker => faker.Random.Int(min: 1)) + .Generate(); var runtimeConfiguration = new ScraperRuntimeConfiguration { - Server = serverConfiguration, MetricsConfiguration = metricsConfiguration, + MetricSinks = metricSinkConfiguration, + ResourceDiscovery = resourceDiscovery, Prometheus = prometheusConfiguration, - Telemetry = telemetryConfiguration, - MetricSinks = metricSinkConfiguration + Server = serverConfiguration, + Telemetry = telemetryConfiguration }; return runtimeConfiguration; diff --git a/src/Promitor.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs b/src/Promitor.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs index ecc4d7178..f496c3979 100644 --- a/src/Promitor.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs +++ b/src/Promitor.Tests.Unit/Generators/Config/RuntimeConfigurationGenerator.cs @@ -113,6 +113,33 @@ public RuntimeConfigurationGenerator WithStatsDMetricSink(int? port = 1234, stri return this; } + public RuntimeConfigurationGenerator WithResourceDiscovery(int? port = 1234, string host = "promitor.resource.discovery.host") + { + ResourceDiscoveryConfiguration resourceDiscoveryConfiguration; + if (string.IsNullOrWhiteSpace(host) && port == null) + { + resourceDiscoveryConfiguration = null; + } + else + { + resourceDiscoveryConfiguration = new ResourceDiscoveryConfiguration(); + + if (string.IsNullOrWhiteSpace(host) == false) + { + resourceDiscoveryConfiguration.Host = host; + } + + if (port != null) + { + resourceDiscoveryConfiguration.Port = port.Value; + } + } + + _runtimeConfiguration.ResourceDiscovery = resourceDiscoveryConfiguration; + + return this; + } + public RuntimeConfigurationGenerator WithPrometheusLegacyConfiguration(double? metricUnavailableValue = -1, bool? enableMetricsTimestamp = false, string scrapeEndpointBaseUri = "/scrape-endpoint") { PrometheusLegacyConfiguration prometheusLegacyConfiguration; @@ -228,6 +255,13 @@ public async Task GenerateAsync() configurationBuilder.AppendLine($" httpPort: {_runtimeConfiguration?.Server.HttpPort}"); } + if (_runtimeConfiguration?.ResourceDiscovery != null) + { + configurationBuilder.AppendLine("resourceDiscovery:"); + configurationBuilder.AppendLine($" host: {_runtimeConfiguration?.ResourceDiscovery.Host}"); + configurationBuilder.AppendLine($" port: {_runtimeConfiguration?.ResourceDiscovery.Port}"); + } + if (_runtimeConfiguration?.Prometheus != null) { configurationBuilder.AppendLine("prometheus:"); diff --git a/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/StatsDMetricSinkValidationStepTests.cs b/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/StatsDMetricSinkValidationStepTests.cs index 5b8897546..4f79b0219 100644 --- a/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/StatsDMetricSinkValidationStepTests.cs +++ b/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/StatsDMetricSinkValidationStepTests.cs @@ -70,6 +70,7 @@ public void Validate_StatsDWithNegativePort_Fails() [Theory] [InlineData("")] + [InlineData(" ")] [InlineData(null)] public void Validate_StatsDWithoutHost_Fails(string host) { diff --git a/src/Promitor.Tests.Unit/Validation/Misc/ResourceDiscoveryValidationStepTests.cs b/src/Promitor.Tests.Unit/Validation/Misc/ResourceDiscoveryValidationStepTests.cs new file mode 100644 index 000000000..193d59db2 --- /dev/null +++ b/src/Promitor.Tests.Unit/Validation/Misc/ResourceDiscoveryValidationStepTests.cs @@ -0,0 +1,96 @@ +using System.ComponentModel; +using Promitor.Agents.Scraper.Configuration; +using Promitor.Agents.Scraper.Validation.Steps; +using Promitor.Tests.Unit.Generators.Config; +using Xunit; + +namespace Promitor.Tests.Unit.Validation.Misc +{ + [Category("Unit")] + public class ResourceDiscoveryValidationStepTests + { + [Fact] + public void Validate_ResourceDiscoveryIsFullyConfigured_Success() + { + // Arrange + var resourceDiscoveryConfiguration = CreateRuntimeConfiguration(); + + // Act + var azureAuthenticationValidationStep = new ResourceDiscoveryValidationStep(resourceDiscoveryConfiguration); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_ResourceDiscoveryIsNotConfigured_Success() + { + // Arrange + ResourceDiscoveryConfiguration resourceDiscoveryConfiguration = null; + + // Act + // ReSharper disable once ExpressionIsAlwaysNull + var azureAuthenticationValidationStep = new ResourceDiscoveryValidationStep(resourceDiscoveryConfiguration); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_StatsDWithNegativePort_Fails() + { + // Arrange + var resourceDiscoveryConfiguration = CreateRuntimeConfiguration(); + resourceDiscoveryConfiguration.Port = -1; + + // Act + var azureAuthenticationValidationStep = new ResourceDiscoveryValidationStep(resourceDiscoveryConfiguration); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_StatsDWithPortZero_Fails() + { + // Arrange + var resourceDiscoveryConfiguration = CreateRuntimeConfiguration(); + resourceDiscoveryConfiguration.Port = 0; + + // Act + var azureAuthenticationValidationStep = new ResourceDiscoveryValidationStep(resourceDiscoveryConfiguration); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Validate_NoHostIsConfigured_Fails(string host) + { + // Arrange + var resourceDiscoveryConfiguration = CreateRuntimeConfiguration(); + resourceDiscoveryConfiguration.Host = host; + + // Act + var azureAuthenticationValidationStep = new ResourceDiscoveryValidationStep(resourceDiscoveryConfiguration); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + private ResourceDiscoveryConfiguration CreateRuntimeConfiguration() + { + var bogusRuntimeConfiguration = BogusScraperRuntimeConfigurationGenerator.Generate(); + + return bogusRuntimeConfiguration.ResourceDiscovery; + } + } +} \ No newline at end of file