diff --git a/.gitignore b/.gitignore index 9b45898f8..9c41410f4 100644 --- a/.gitignore +++ b/.gitignore @@ -301,6 +301,8 @@ docs/_site/* #MAC .DS_Store +# Custom *.orig changelog/public/ changelog/resources/_gen/ +src/docker-compose.vs.debug.yml diff --git a/changelog/content/experimental/unreleased.md b/changelog/content/experimental/unreleased.md index e81e4c0b8..6b86b1822 100644 --- a/changelog/content/experimental/unreleased.md +++ b/changelog/content/experimental/unreleased.md @@ -14,3 +14,8 @@ version: - {{% tag added %}} Provide suggestions when unknown fields are found in the metrics config. [#1105](https://github.com/tomkerkhove/promitor/issues/1105). - {{% tag added %}} Add validation to ensure the scraping schedule is a valid Cron expression. [#1103](https://github.com/tomkerkhove/promitor/issues/1103). - {{% tag changed %}} Handle validation failures on startup more gracefully. [#1113](https://github.com/tomkerkhove/promitor/issues/1113). +- {{% tag added %}} Provide support for pushing metrics to Atlassian Statuspage + ([docs](https://promitor.io/configuration/v2.x/runtime#atlassian-statuspage) | [#1152](https://github.com/tomkerkhove/promitor/issues/1152)) +- {{% tag added %}} Provide suggestions when unknown fields are found in the metrics config. [#1105](https://github.com/tomkerkhove/promitor/issues/1105). +- {{% tag added %}} New validation rule to ensure the scraping schedule is a valid Cron expression. [#1103](https://github.com/tomkerkhove/promitor/issues/1103). +- {{% tag added %}} New validation rule to ensure declarative or dynamic discovery for metrics to scrape are configured diff --git a/config/promitor/scraper/metrics.yaml b/config/promitor/scraper/metrics.yaml index f00c44c71..b87c0012a 100644 --- a/config/promitor/scraper/metrics.yaml +++ b/config/promitor/scraper/metrics.yaml @@ -10,132 +10,6 @@ metricDefaults: # Every minute schedule: "0 * * ? * *" metrics: - - name: promitor_demo_container_cpu - description: "The CPU of our containers in a container group." - resourceType: ContainerInstance - azureMetricConfiguration: - metricName: CpuUsage - dimension: - name: containerName - aggregation: - type: Average - resources: - - containerGroup: promitor-container-group - resourceGroupName: promitor-sources - - name: promitor_demo_generic_queue_size - description: "Amount of active messages of the 'orders' queue (determined with Generic provider)" - resourceType: Generic - labels: - app: promitor - azureMetricConfiguration: - metricName: ActiveMessages - aggregation: - type: Average - resources: - - resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging - filter: EntityName eq 'orders' - - name: promitor_demo_generic_namespace_size - description: "Size of all queues in our Azure Service Bus namespace (determined with Generic provider)" - resourceType: Generic - scraping: - # Every 2 minutes - schedule: "0 */2 * ? * *" - azureMetricConfiguration: - metricName: ActiveMessages - aggregation: - type: Average - resources: - # filter is deliberately omitted given filter is optional - - resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging - - name: promitor_demo_servicebusqueue_queue_size_discovered - description: "Amount of active messages of the Service Bus queues" - resourceType: ServiceBusQueue - azureMetricConfiguration: - metricName: ActiveMessages - aggregation: - type: Average - # Optionally override the default aggregation interval (metricDefaults.aggregation.interval) - interval: 00:15:00 - resourceDiscoveryGroups: - - name: service-bus-landscape - - name: promitor_demo_servicebusqueue_queue_size - description: "Amount of active messages of the 'orders' queue (determined with ServiceBusQueue provider)" - resourceType: ServiceBusQueue - azureMetricConfiguration: - metricName: ActiveMessages - aggregation: - type: Average - # Optionally override the default aggregation interval (metricDefaults.aggregation.interval) - interval: 00:15:00 - resources: - - namespace: promitor-messaging - queueName: orders - - namespace: promitor-messaging - queueName: sales - - namespace: promitor-messaging-other-subscription - queueName: orders - subscriptionId: 0329dd2a-59dc-4493-aa54-cb01cb027dc2 - resourceGroupName: promitor-sources - - name: promitor_demo_azurestoragequeue_queue_size - description: "Approximate amount of messages in 'orders' queue (determined with StorageQueue provider)" - resourceType: StorageQueue - scraping: - # Every 2 minutes - schedule: "0 */2 * ? * *" - azureMetricConfiguration: - metricName: MessageCount - aggregation: - type: Total - resources: - - accountName: promitor - queueName: orders - sasToken: - rawValue: "?sv=2018-03-28&ss=bfqt&srt=sco&sp=rwla&se=2022-08-07T00:16:01Z&st=2019-08-06T16:16:01Z&spr=https&sig=Ik4jprS89kGIFRM0qaQpXrv0ttP3pnlhmGQuYVQ7cbA%3D" - - name: promitor_demo_azurestoragequeue_queue_timespentinqueue - description: "Approximate amount of time that the oldest message has been in 'orders' queue (determined with StorageQueue provider)" - resourceType: StorageQueue - azureMetricConfiguration: - metricName: TimeSpentInQueue - aggregation: - type: Total - resources: - - accountName: promitor - queueName: orders - sasToken: - environmentVariable: SECRETS_STORAGEQUEUE_SAS - - name: promitor_demo_azuresqldb_dtu - description: "The DTU consumption percentage used by an Azure SQL Database." - resourceType: SqlDatabase - azureMetricConfiguration: - metricName: dtu_consumption_percent - aggregation: - type: Average - resources: - - serverName: promitor - databaseName: promitor-db-1 - - name: promitor_demo_webapp_cpu - description: "Amount of CPU time used for an Azure Web App" - resourceType: WebApp - azureMetricConfiguration: - metricName: CpuTime - aggregation: - type: Total - resources: - - webAppName: promitor-web-app - resourceGroupName: promitor-sources - - webAppName: promitor-web-app - resourceGroupName: promitor-sources - slotName: staging - - name: promitor_demo_function_requests - description: "Amount of requests for an Azure Function App" - resourceType: FunctionApp - azureMetricConfiguration: - metricName: Requests - aggregation: - type: Total - resources: - - functionAppName: promitor-function-app - resourceGroupName: promitor-sources - name: promitor_demo_appplan_percentage_cpu description: "Average percentage of memory usage on an Azure App Plan" resourceType: AppPlan @@ -199,4 +73,4 @@ metrics: aggregation: type: Total resourceDiscoveryGroups: - - name: logic-apps-unfiltered \ No newline at end of file + - name: logic-apps-unfiltered diff --git a/config/promitor/scraper/runtime.yaml b/config/promitor/scraper/runtime.yaml index f00bc00fe..833b30fe7 100644 --- a/config/promitor/scraper/runtime.yaml +++ b/config/promitor/scraper/runtime.yaml @@ -9,6 +9,11 @@ metricSinks: host: graphite port: 8125 metricPrefix: promitor. + atlassianStatuspage: + pageId: y79z9b78ybgs + systemMetricMapping: + - id: nfkgnrwpn545 + promitorMetricName: promitor_demo_appplan_percentage_cpu metricsConfiguration: absolutePath: /config/metrics-declaration.yaml telemetry: diff --git a/docs/configuration/v2.x/runtime.md b/docs/configuration/v2.x/runtime.md index 3d2abeba3..22d3a54bd 100644 --- a/docs/configuration/v2.x/runtime.md +++ b/docs/configuration/v2.x/runtime.md @@ -66,9 +66,39 @@ by providing the metric information to the configured sinks. As of today, we support the follow sinks: +- **Atlassian Statuspage** - **Prometheus Scraping Endpoint** - **StatsD** +### Atlassian Statuspage + +![Availability Badge](https://img.shields.io/badge/Available%20Starting-v2.0-green.svg) + +In order to expose a Prometheus Scraping endpoint, you'll need to configure the sink: + +- `atlassianStatuspage.pageId` - Defines the id of the Atlassian Statuspage to report to. +- `atlassianStatuspage.systemMetricMapping` - Defines a mapping of the scraped metric by Promitor and to which + Atlassian Statuspage system metric it should be reported to. Here's what we expect: + - `id` - Id of the Atlassian Statuspage system metric + - `promitorMetricName` - Name of the Promitor metric which needs to be reported + +Next to that, `PROMITOR_ATLASSIAN_STATUSPAGE_APIKEY` environment variable is required which contains the API Key + for Atlassian Statuspage. + +```yaml +metricSinks: + atlassianStatuspage: + pageId: XXX # Mandatory + systemMetricMapping: # Mandatory to have at least one mapping + - id: ABC + promitorMetricName: promitor_demo_appplan_percentage_cpu +``` + +> :warning: **As of today, metric labels, resource discovery and multi-resource scraping are not supported.** +> +> This is because Promitor will report the different resource metrics to the same Atlassian metric which will mix metrics +> which becomes confusing. + ### Prometheus Scraping Endpoint ![Availability Badge](https://img.shields.io/badge/Available%20Starting-v1.6-green.svg) @@ -110,6 +140,10 @@ metricSinks: metricPrefix: promitor. ``` +> :warning: **As of today, metric labels are not supported.** +> +> Unfortunately, this is not supported in the specifiaction. + ## Metric Configuration Promitor will scrape the Azure Monitor metrics that are configured via a metric diff --git a/docs/index.md b/docs/index.md index 931f33064..c50ab88b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,6 +56,7 @@ and vote for features! - [What labels do we provide?](metrics/labels) - **Configuration** - [Overview of metric sinks](configuration/v1.x/runtime#metric-sinks) + - [Atlassian Statuspage](configuration/v2.x/runtime#atlassian-statuspage) - [Prometheus Scraping Endpoint](configuration/v1.x/runtime#prometheus-scraping-endpoint) - [StatsD](configuration/v1.x/runtime#statsd) - [Authentication with Azure Monitor](configuration/v1.x/azure-monitor) diff --git a/src/Promitor.Agents.Scraper/Configuration/Sinks/MetricSinkConfiguration.cs b/src/Promitor.Agents.Scraper/Configuration/Sinks/MetricSinkConfiguration.cs index 64a69021f..d33c68e47 100644 --- a/src/Promitor.Agents.Scraper/Configuration/Sinks/MetricSinkConfiguration.cs +++ b/src/Promitor.Agents.Scraper/Configuration/Sinks/MetricSinkConfiguration.cs @@ -1,4 +1,5 @@ -using Promitor.Integrations.Sinks.Prometheus.Configuration; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; +using Promitor.Integrations.Sinks.Prometheus.Configuration; using Promitor.Integrations.Sinks.Statsd.Configuration; namespace Promitor.Agents.Scraper.Configuration.Sinks @@ -7,5 +8,6 @@ public class MetricSinkConfiguration { public StatsdSinkConfiguration Statsd { get; set; } public PrometheusScrapingEndpointSinkConfiguration PrometheusScrapingEndpoint { get; set; } + public AtlassianStatusPageSinkConfiguration AtlassianStatuspage { 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 index c6aeb8c9a..3cb966715 100644 --- a/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs +++ b/src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs @@ -20,18 +20,18 @@ public class ResourceDiscoveryClient private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects }; private readonly IOptionsMonitor _configuration; private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; + private readonly HttpClient _httpClient; - public ResourceDiscoveryClient(IHttpClientFactory httpClientFactory, IOptionsMonitor configuration, ILogger logger) + public ResourceDiscoveryClient(HttpClient httpClient, IOptionsMonitor configuration, ILogger logger) { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); + Guard.NotNull(httpClient, nameof(httpClient)); Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(logger, nameof(logger)); Guard.For(() => configuration.CurrentValue.IsConfigured == false, "Resource Discovery is not configured"); _logger = logger; + _httpClient = httpClient; _configuration = configuration; - _httpClientFactory = httpClientFactory; } public async Task> GetAsync(string resourceDiscoveryGroupName) @@ -63,13 +63,13 @@ private async Task SendGetRequestAsync(string uri) private async Task SendRequestToApiAsync(HttpRequestMessage request) { - var client = CreateHttpClient(); using (var dependencyMeasurement = DependencyMeasurement.Start()) { HttpResponseMessage response = null; try { - response = await client.SendAsync(request); + _httpClient.BaseAddress = new Uri($"http://{_configuration.CurrentValue.Host}:{_configuration.CurrentValue.Port}"); + response = await _httpClient.SendAsync(request); _logger.LogRequest(request, response, dependencyMeasurement.Elapsed); return response; @@ -81,12 +81,5 @@ private async Task SendRequestToApiAsync(HttpRequestMessage } } } - - 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/Dockerfile b/src/Promitor.Agents.Scraper/Dockerfile index 4385b6788..d1f58e071 100644 --- a/src/Promitor.Agents.Scraper/Dockerfile +++ b/src/Promitor.Agents.Scraper/Dockerfile @@ -8,6 +8,7 @@ COPY Promitor.Core.Telemetry/* Promitor.Core.Telemetry/ COPY Promitor.Integrations.AzureMonitor/* Promitor.Integrations.AzureMonitor/ COPY Promitor.Integrations.AzureStorage/* Promitor.Integrations.AzureStorage/ COPY Promitor.Integrations.Sinks.Statsd/* Promitor.Integrations.Sinks.Statsd/ +COPY Promitor.Integrations.Sinks.Atlassian.Statuspage/* Promitor.Integrations.Sinks.Atlassian.Statuspage/ COPY Promitor.Integrations.Sinks.Prometheus/* Promitor.Integrations.Sinks.Prometheus/ COPY Promitor.Agents.Scraper/* Promitor.Agents.Scraper/ RUN dotnet publish Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj --configuration release --output app diff --git a/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs b/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs index fe0d1af74..070c0be04 100644 --- a/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs +++ b/src/Promitor.Agents.Scraper/Extensions/IServiceCollectionExtensions.cs @@ -23,6 +23,8 @@ using Promitor.Core.Metrics.Sinks; using Promitor.Core.Scraping.Configuration.Runtime; using Promitor.Integrations.AzureMonitor.Configuration; +using Promitor.Integrations.Sinks.Atlassian.Statuspage; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; using Promitor.Integrations.Sinks.Prometheus; using Promitor.Integrations.Sinks.Prometheus.Configuration; using Promitor.Integrations.Sinks.Statsd; @@ -40,7 +42,6 @@ public static class IServiceCollectionExtensions /// Collections of services in application public static IServiceCollection DefineDependencies(this IServiceCollection services) { - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -98,6 +99,11 @@ public static IServiceCollection UseMetricSinks(this IServiceCollection services AddPrometheusMetricSink(services); } + if (metricSinkConfiguration?.AtlassianStatuspage != null) + { + AddAtlassianStatuspageMetricSink(services); + } + services.TryAddSingleton(); return services; @@ -108,6 +114,11 @@ private static void AddPrometheusMetricSink(IServiceCollection services) services.AddTransient(); } + private static void AddAtlassianStatuspageMetricSink(IServiceCollection services) + { + services.AddTransient(); + } + private static void AddStatsdMetricSink(IServiceCollection services, StatsdSinkConfiguration statsdConfiguration) { services.AddTransient(); @@ -143,7 +154,9 @@ public static IServiceCollection ConfigureYamlConfiguration(this IServiceCollect services.Configure(configuration.GetSection("resourceDiscovery")); services.Configure(configuration.GetSection("telemetry")); services.Configure(configuration.GetSection("server")); - services.Configure(configuration.GetSection("prometheus")); + services.Configure(configuration.GetSection("metricSinks:prometheus")); + services.Configure(configuration.GetSection("metricSinks:statsd")); + services.Configure(configuration.GetSection("metricSinks:atlassianStatuspage")); services.Configure(configuration.GetSection("telemetry:applicationInsights")); services.Configure(configuration.GetSection("telemetry:containerLogs")); services.Configure(configuration.GetSection("prometheus:scrapeEndpoint")); diff --git a/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj b/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj index f2528624e..1b8161bad 100644 --- a/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj +++ b/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj @@ -53,6 +53,7 @@ + diff --git a/src/Promitor.Agents.Scraper/Startup.cs b/src/Promitor.Agents.Scraper/Startup.cs index 99bdccf99..c7d91b364 100644 --- a/src/Promitor.Agents.Scraper/Startup.cs +++ b/src/Promitor.Agents.Scraper/Startup.cs @@ -10,10 +10,13 @@ using Promitor.Agents.Core; using Promitor.Agents.Scraper.Configuration; using Promitor.Agents.Scraper.Configuration.Sinks; +using Promitor.Agents.Scraper.Discovery; using Promitor.Agents.Scraper.Extensions; using Promitor.Agents.Scraper.Health; +using Promitor.Core; using Promitor.Core.Scraping.Configuration.Serialization.v1.Mapping; using Promitor.Integrations.AzureMonitor.Logging; +using Promitor.Integrations.Sinks.Atlassian.Statuspage; using Serilog; namespace Promitor.Agents.Scraper @@ -31,10 +34,19 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { string openApiDescription = BuildOpenApiDescription(Configuration); - services.AddHttpClient("Promitor Resource Discovery", client => + services.AddHttpClient(client => { // Provide Promitor User-Agent - client.DefaultRequestHeaders.Add("User-Agent", "Promitor Scraper"); + client.DefaultRequestHeaders.UserAgent.TryParseAdd(Http.Headers.UserAgents.Scraper); + }); + services.AddHttpClient(client => + { + // Provide Promitor User-Agent + client.DefaultRequestHeaders.UserAgent.TryParseAdd(Http.Headers.UserAgents.Scraper); + + // Auth all requests + var apiKey = Configuration[EnvironmentVariables.Integrations.AtlassianStatuspage.ApiKey]; + client.DefaultRequestHeaders.Add("Authorization", $"OAuth {apiKey}"); }); services.UseWebApi() diff --git a/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/AtlassianStatuspageMetricSinkValidationStep.cs b/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/AtlassianStatuspageMetricSinkValidationStep.cs new file mode 100644 index 000000000..cfdd93513 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Validation/Steps/Sinks/AtlassianStatuspageMetricSinkValidationStep.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Promitor.Agents.Core.Validation; +using Promitor.Agents.Core.Validation.Interfaces; +using Promitor.Agents.Core.Validation.Steps; +using Promitor.Agents.Scraper.Configuration; +using Promitor.Core.Scraping.Configuration.Providers.Interfaces; + +namespace Promitor.Agents.Scraper.Validation.Steps.Sinks +{ + public class AtlassianStatuspageMetricSinkValidationStep : ValidationStep, + IValidationStep + { + private readonly IMetricsDeclarationProvider _metricsDeclarationProvider; + private readonly IOptions _runtimeConfiguration; + + public AtlassianStatuspageMetricSinkValidationStep(IOptions runtimeConfiguration, IMetricsDeclarationProvider metricsDeclarationProvider, ILogger validationLogger) + : base(validationLogger) + { + _runtimeConfiguration = runtimeConfiguration; + _metricsDeclarationProvider = metricsDeclarationProvider; + } + + public string ComponentName { get; } = "Atlassian Statuspage Metric Sink"; + + public ValidationResult Run() + { + var currentRuntimeConfiguration = _runtimeConfiguration.Value; + var atlassianStatuspageConfiguration = currentRuntimeConfiguration?.MetricSinks?.AtlassianStatuspage; + if (atlassianStatuspageConfiguration == null) + { + return ValidationResult.Successful(ComponentName); + } + + var errorMessages = new List(); + if (string.IsNullOrWhiteSpace(atlassianStatuspageConfiguration.PageId)) + { + errorMessages.Add("No page id of Atlassian Status page is configured"); + } + + if (atlassianStatuspageConfiguration.SystemMetricMapping?.Any() != true) + { + errorMessages.Add("No system metrics mappings are configured which means no metrics can be reported"); + } + else + { + if (atlassianStatuspageConfiguration.SystemMetricMapping.Select(map => map.Id).Distinct().Count() != atlassianStatuspageConfiguration.SystemMetricMapping?.Count) + { + errorMessages.Add("System metric with duplicate id(s) mappings are configured"); + } + + var metricsDeclaration = _metricsDeclarationProvider.Get(true); + + foreach (var systemMetric in atlassianStatuspageConfiguration.SystemMetricMapping) + { + if (string.IsNullOrWhiteSpace(systemMetric.Id)) + { + errorMessages.Add($"System metric mapping defined without specifying a system metric id (Promitor metric name: {systemMetric.PromitorMetricName})"); + } + if (string.IsNullOrWhiteSpace(systemMetric.PromitorMetricName)) + { + errorMessages.Add($"System metric mapping defined without specifying a Promitor metric name (System metric id: {systemMetric.Id})"); + } + + var matchingPromitorMetric = metricsDeclaration.Metrics.FirstOrDefault(metricDefinition => metricDefinition.PrometheusMetricDefinition.Name.Equals(systemMetric.PromitorMetricName)); + if (matchingPromitorMetric == null) + { + errorMessages.Add($"Statuspage metric Id '{systemMetric.Id}' is mapped to a metric called '{systemMetric.PromitorMetricName}', but no metric was found with that name"); + } + else + { + if (matchingPromitorMetric.ResourceDiscoveryGroups?.Any() == true) + { + errorMessages.Add("Scraping with resource discovery is not supported"); + } + + if (matchingPromitorMetric.Resources?.Count > 1) + { + errorMessages.Add("Scraping multiple resources for one metric is not supported"); + } + } + } + } + + return errorMessages.Any() ? ValidationResult.Failure(ComponentName, errorMessages) : ValidationResult.Successful(ComponentName); + } + } +} diff --git a/src/Promitor.Core/EnvironmentVariables.cs b/src/Promitor.Core/EnvironmentVariables.cs index f0027aa50..e33acbc37 100644 --- a/src/Promitor.Core/EnvironmentVariables.cs +++ b/src/Promitor.Core/EnvironmentVariables.cs @@ -12,5 +12,13 @@ public class Authentication public const string ApplicationId = "AUTH_APPID"; public const string ApplicationKey = "AUTH_APPKEY"; } + + public class Integrations + { + public class AtlassianStatuspage + { + public const string ApiKey = "ATLASSIAN_STATUSPAGE_APIKEY"; + } + } } } \ No newline at end of file diff --git a/src/Promitor.Core/Http.cs b/src/Promitor.Core/Http.cs new file mode 100644 index 000000000..29a22be70 --- /dev/null +++ b/src/Promitor.Core/Http.cs @@ -0,0 +1,13 @@ +namespace Promitor.Core +{ + public class Http + { + public class Headers + { + public class UserAgents + { + public const string Scraper = "App/Promitor Agent/Scraper"; + } + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core/Metrics/Sinks/MetricSinkType.cs b/src/Promitor.Core/Metrics/Sinks/MetricSinkType.cs index d195d9efd..36ac23846 100644 --- a/src/Promitor.Core/Metrics/Sinks/MetricSinkType.cs +++ b/src/Promitor.Core/Metrics/Sinks/MetricSinkType.cs @@ -3,6 +3,7 @@ public enum MetricSinkType { PrometheusScrapingEndpoint, - StatsD + StatsD, + AtlassianStatuspage } } \ No newline at end of file diff --git a/src/Promitor.Docker.dcproj b/src/Promitor.Docker.dcproj index 27396a4ad..b0c92062c 100644 --- a/src/Promitor.Docker.dcproj +++ b/src/Promitor.Docker.dcproj @@ -10,6 +10,9 @@ promitor.scraper + + docker-compose.yml + docker-compose.yml diff --git a/src/Promitor.Integrations.AzureMonitor/RequestHandlers/AzureResourceManagerThrottlingRequestHandler.cs b/src/Promitor.Integrations.AzureMonitor/RequestHandlers/AzureResourceManagerThrottlingRequestHandler.cs index d13388bdc..4d572a6a5 100644 --- a/src/Promitor.Integrations.AzureMonitor/RequestHandlers/AzureResourceManagerThrottlingRequestHandler.cs +++ b/src/Promitor.Integrations.AzureMonitor/RequestHandlers/AzureResourceManagerThrottlingRequestHandler.cs @@ -55,7 +55,7 @@ public AzureResourceManagerThrottlingRequestHandler(string tenantId, string subs protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - request.Headers.UserAgent.TryParseAdd("App/Promitor.Scraper"); + request.Headers.UserAgent.TryParseAdd(Http.Headers.UserAgents.Scraper); var response = await base.SendAsync(request, cancellationToken); diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageClient.cs b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageClient.cs new file mode 100644 index 000000000..ecb355036 --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageClient.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Flurl; +using GuardNet; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; + +namespace Promitor.Integrations.Sinks.Atlassian.Statuspage +{ + public class AtlassianStatuspageClient : IAtlassianStatuspageClient + { + private const string MetricRequestFormat = "{{\"data\": {{\"timestamp\": {0},\"value\": {1}}}}}"; + private const string ApiUrl = "https://api.statuspage.io/v1"; + + private readonly IOptionsMonitor _sinkConfiguration; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public AtlassianStatuspageClient(HttpClient httpClient, IOptionsMonitor sinkConfiguration, ILogger logger) + { + Guard.NotNull(httpClient, nameof(httpClient)); + Guard.NotNull(logger, nameof(logger)); + Guard.NotNull(sinkConfiguration, nameof(sinkConfiguration)); + Guard.NotNull(sinkConfiguration.CurrentValue, nameof(sinkConfiguration.CurrentValue)); + + _sinkConfiguration = sinkConfiguration; + _httpClient = httpClient; + _logger = logger; + } + + public async Task ReportMetricAsync(string id, double value) + { + Guard.NotNullOrWhitespace(id, nameof(id)); + + var pageId = _sinkConfiguration.CurrentValue.PageId; + + // Docs: https://developer.statuspage.io/#operation/postPagesPageIdMetricsMetricIdData + var requestUri = ApiUrl.AppendPathSegment("pages") + .AppendPathSegment(pageId) + .AppendPathSegment("metrics") + .AppendPathSegment(id) + .AppendPathSegment("data"); + + var measurementTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri) + { + Content = new StringContent(string.Format(MetricRequestFormat, measurementTime, value), Encoding.UTF8, MediaTypeNames.Application.Json) + }; + + var response = await _httpClient.SendAsync(request); + + if (response.IsSuccessStatusCode == false) + { + var message = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to report metric. Details: {message}"); + } + } + } +} diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageMetricSink.cs b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageMetricSink.cs new file mode 100644 index 000000000..8a59ec0ff --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/AtlassianStatusPageMetricSink.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GuardNet; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Promitor.Core; +using Promitor.Core.Metrics.Sinks; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; + +namespace Promitor.Integrations.Sinks.Atlassian.Statuspage +{ + public class AtlassianStatuspageMetricSink : IMetricSink + { + private readonly ILogger _logger; + private readonly IAtlassianStatuspageClient _atlassianStatusPageClient; + private readonly IOptionsMonitor _sinkConfiguration; + + public MetricSinkType Type { get; } = MetricSinkType.AtlassianStatuspage; + + public AtlassianStatuspageMetricSink(IAtlassianStatuspageClient atlassianStatusPageClient, IOptionsMonitor sinkConfiguration, ILogger logger) + { + Guard.NotNull(atlassianStatusPageClient, nameof(atlassianStatusPageClient)); + Guard.NotNull(sinkConfiguration, nameof(sinkConfiguration)); + Guard.NotNull(sinkConfiguration.CurrentValue, nameof(sinkConfiguration.CurrentValue)); + Guard.NotNull(logger, nameof(logger)); + + _atlassianStatusPageClient = atlassianStatusPageClient; + _sinkConfiguration = sinkConfiguration; + _logger = logger; + } + + public async Task ReportMetricAsync(string metricName, string metricDescription, ScrapeResult scrapeResult) + { + Guard.NotNullOrEmpty(metricName, nameof(metricName)); + Guard.NotNull(scrapeResult, nameof(scrapeResult)); + Guard.NotNull(scrapeResult.MetricValues, nameof(scrapeResult.MetricValues)); + + var reportMetricTasks = new List(); + + foreach (var measuredMetric in scrapeResult.MetricValues) + { + var metricValue = measuredMetric.Value ?? 0; + + var reportMetricTask = ReportMetricAsync(metricName, metricDescription, metricValue, new Dictionary()); + reportMetricTasks.Add(reportMetricTask); + } + + await Task.WhenAll(reportMetricTasks); + } + + public async Task ReportMetricAsync(string metricName, string metricDescription, double metricValue, Dictionary labels) + { + Guard.NotNullOrEmpty(metricName, nameof(metricName)); + + var systemMetricMapping = _sinkConfiguration.CurrentValue.SystemMetricMapping.SingleOrDefault(metricMapping => metricMapping.PromitorMetricName.Equals(metricName)); + if(systemMetricMapping != null) + { + await _atlassianStatusPageClient.ReportMetricAsync(systemMetricMapping.Id, metricValue); + } + + _logger.LogTrace("Metric {MetricName} with value {MetricValue} was written to Atlassian Statuspage", metricName, metricValue); + } + } +} diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/AtlassianStatusPageSinkConfiguration.cs b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/AtlassianStatusPageSinkConfiguration.cs new file mode 100644 index 000000000..ecebf8930 --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/AtlassianStatusPageSinkConfiguration.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration +{ + public class AtlassianStatusPageSinkConfiguration + { + public string PageId { get; set; } + public List SystemMetricMapping { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/SystemMetricMapping.cs b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/SystemMetricMapping.cs new file mode 100644 index 000000000..bf5f25b86 --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Configuration/SystemMetricMapping.cs @@ -0,0 +1,8 @@ +namespace Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration +{ + public class SystemMetricMapping + { + public string Id { get; set; } + public string PromitorMetricName { get; set; } + } +} diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/IAtlassianStatuspageClient.cs b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/IAtlassianStatuspageClient.cs new file mode 100644 index 000000000..7a2ad15f6 --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/IAtlassianStatuspageClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Promitor.Integrations.Sinks.Atlassian.Statuspage +{ + public interface IAtlassianStatuspageClient + { + Task ReportMetricAsync(string id, double value); + } +} \ No newline at end of file diff --git a/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Promitor.Integrations.Sinks.Atlassian.Statuspage.csproj b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Promitor.Integrations.Sinks.Atlassian.Statuspage.csproj new file mode 100644 index 000000000..3238d906e --- /dev/null +++ b/src/Promitor.Integrations.Sinks.Atlassian.Statuspage/Promitor.Integrations.Sinks.Atlassian.Statuspage.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + 3.1.3 + + + + 1701;1702;1591 + + + + 1701;1702;1591 + + + + + + + + + + + + diff --git a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs index efbf53c1f..cc6937316 100644 --- a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs @@ -76,15 +76,34 @@ public MetricsDeclarationBuilder WithServiceBusMetric(string metricName = "promi string serviceBusNamespace = "promitor-namespace", string azureMetricName = "Total", string resourceDiscoveryGroupName = "", - bool omitResource = false) + bool omitResource = false, + List queueNames = null) { - var resource = new ServiceBusQueueResourceV1 + var serviceBusQueueResources = new List(); + + if (queueNames != null) { - QueueName = queueName, - Namespace = serviceBusNamespace - }; + foreach (string queue in queueNames) + { + var resource = new ServiceBusQueueResourceV1 + { + QueueName = queue, + Namespace = serviceBusNamespace + }; + serviceBusQueueResources.Add(resource); + } + } + else + { + var resource = new ServiceBusQueueResourceV1 + { + QueueName = queueName, + Namespace = serviceBusNamespace + }; + serviceBusQueueResources.Add(resource); + } - CreateAndAddMetricDefinition(ResourceType.ServiceBusQueue, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, resource, metricDimension); + CreateAndAddMetricDefinition(ResourceType.ServiceBusQueue, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, serviceBusQueueResources, metricDimension); return this; } @@ -518,6 +537,11 @@ public MetricsDeclarationBuilder WithKeyVaultMetric(string metricName = "promito } private void CreateAndAddMetricDefinition(ResourceType resourceType, string metricName, string metricDescription, string resourceDiscoveryGroupName, bool omitResource, string azureMetricName, AzureResourceDefinitionV1 resource, string metricDimension = null) + { + CreateAndAddMetricDefinition(resourceType, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, new List {resource}, metricDimension); + } + + private void CreateAndAddMetricDefinition(ResourceType resourceType, string metricName, string metricDescription, string resourceDiscoveryGroupName, bool omitResource, string azureMetricName, List resources, string metricDimension = null) { var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName, metricDimension); var metric = new MetricDefinitionV1 @@ -530,7 +554,7 @@ private void CreateAndAddMetricDefinition(ResourceType resourceType, string metr if (omitResource == false) { - metric.Resources = new List { resource }; + metric.Resources = resources; } if (string.IsNullOrWhiteSpace(resourceDiscoveryGroupName) == false) diff --git a/src/Promitor.Tests.Unit/Clients/AtlassianStatuspageClientTests.cs b/src/Promitor.Tests.Unit/Clients/AtlassianStatuspageClientTests.cs new file mode 100644 index 000000000..df8efbc90 --- /dev/null +++ b/src/Promitor.Tests.Unit/Clients/AtlassianStatuspageClientTests.cs @@ -0,0 +1,93 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; +using Bogus; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Newtonsoft.Json.Linq; +using Promitor.Integrations.Sinks.Atlassian.Statuspage; +using Promitor.Tests.Unit.Generators.Config; +using Promitor.Tests.Unit.Stubs; +using Xunit; + +namespace Promitor.Tests.Unit.Clients +{ + [Category("Unit")] + public class AtlassianStatuspageClientTests + { + private readonly Faker _bogus = new Faker(); + + [Fact] + public async Task ReportMetricAsync_MetricIdAndValueAreProvided_Succeeds() + { + // Arrange + var pageId = _bogus.Name.FirstName(); + var metricId = _bogus.Name.FirstName(); + var metricValue = _bogus.Random.Double(); + var fakeHttpMessageHandler = new HttpMessageHandlerStub(); + var sinkConfiguration = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(pageId: pageId); + var atlassianStatuspageClient = new AtlassianStatuspageClient(httpClient: new HttpClient(fakeHttpMessageHandler), sinkConfiguration, NullLogger.Instance); + + // Act + await atlassianStatuspageClient.ReportMetricAsync(metricId, metricValue); + + // Assert + var request = fakeHttpMessageHandler.LastRequest; + Assert.NotNull(request); + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal($"https://api.statuspage.io/v1/pages/{pageId}/metrics/{metricId}/data", request.RequestUri.ToString()); + var requestBody = await request.Content.ReadAsStringAsync(); + var requestToken = JToken.Parse(requestBody); + Assert.Equal(metricValue.ToString(CultureInfo.InvariantCulture), requestToken["data"]?["value"]?.ToString()); + } + + [Fact] + public async Task ReportMetricAsync_NoMetricIdIsProvided_ThrowsException() + { + // Arrange + var metricValue = _bogus.Random.Double(); + var httpClientMock = new Mock(); + var sinkConfiguration = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + var atlassianStatuspageClient = new AtlassianStatuspageClient(httpClient: httpClientMock.Object, sinkConfiguration, NullLogger.Instance); + + // Act & Assert + await Assert.ThrowsAsync(() => atlassianStatuspageClient.ReportMetricAsync(id: null, metricValue)); + } + + [Fact] + public void Constructor_NoHttpClientIsProvided_ThrowsException() + { + // Arrange + var sinkConfiguration = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + Assert.Throws(() => new AtlassianStatuspageClient(httpClient: null, sinkConfiguration, NullLogger.Instance)); + } + + [Fact] + public void Constructor_NoSinkConfigurationIsProvided_ThrowsException() + { + // Arrange + var httpClientMock = new Mock(); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + Assert.Throws(() => new AtlassianStatuspageClient(httpClientMock.Object, sinkConfiguration: null, NullLogger.Instance)); + } + + [Fact] + public void Constructor_NoLoggerIsProvided_ThrowsException() + { + // Arrange + var httpClientMock = new Mock(); + var sinkConfiguration = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + Assert.Throws(() => new AtlassianStatuspageClient(httpClientMock.Object, sinkConfiguration, logger: null)); + } + } +} diff --git a/src/Promitor.Tests.Unit/Generators/Config/BogusAtlassianStatuspageMetricSinkConfigurationGenerator.cs b/src/Promitor.Tests.Unit/Generators/Config/BogusAtlassianStatuspageMetricSinkConfigurationGenerator.cs new file mode 100644 index 000000000..85e3b70a5 --- /dev/null +++ b/src/Promitor.Tests.Unit/Generators/Config/BogusAtlassianStatuspageMetricSinkConfigurationGenerator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Bogus; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; +using Promitor.Tests.Unit.Stubs; + +namespace Promitor.Tests.Unit.Generators.Config +{ + internal static class BogusAtlassianStatuspageMetricSinkConfigurationGenerator + { + internal static AtlassianStatusPageSinkConfiguration Generate(string pageId = null, string systemMetricId = null, string promitorMetricName = null) + { + var systemMetricMapping = new Faker() + .RuleFor(config => config.Id, fake => systemMetricId ?? fake.Name.FirstName()) + .RuleFor(config => config.PromitorMetricName, fake => promitorMetricName ?? fake.Name.FirstName()) + .Generate(); + + var sinkConfiguration = new Faker() + .RuleFor(config => config.PageId, fake => pageId ?? fake.Name.FirstName()) + .RuleFor(config => config.SystemMetricMapping, fake => new List { systemMetricMapping }) + .Generate(); + + return sinkConfiguration; + } + + internal static OptionsMonitorStub GetSinkConfiguration(string pageId = null, string systemMetricId = null, string promitorMetricName = null) + { + var sinkConfiguration = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.Generate(pageId, systemMetricId, promitorMetricName); + + return new OptionsMonitorStub(sinkConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs b/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs index a36f12ad5..805998edb 100644 --- a/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs +++ b/src/Promitor.Tests.Unit/Generators/Config/BogusScraperRuntimeConfigurationGenerator.cs @@ -1,4 +1,5 @@ -using Bogus; +using System.Collections.Generic; +using Bogus; using Microsoft.Extensions.Logging; using Promitor.Agents.Core.Configuration.Server; using Promitor.Agents.Core.Configuration.Telemetry; @@ -6,6 +7,7 @@ using Promitor.Agents.Scraper.Configuration; using Promitor.Agents.Scraper.Configuration.Sinks; using Promitor.Core.Scraping.Configuration.Runtime; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; using Promitor.Integrations.Sinks.Prometheus.Configuration; using Promitor.Integrations.Sinks.Statsd.Configuration; @@ -15,67 +17,140 @@ internal static class BogusScraperRuntimeConfigurationGenerator { internal static ScraperRuntimeConfiguration Generate() { - var serverConfiguration = new Faker() + var serverConfiguration = GenerateServerConfiguration(); + var metricsConfiguration = GenerateMetricsConfiguration(); + var containerLogConfiguration = GenerateContainerLogConfiguration(); + var applicationInsightsConfiguration = GenerateApplicationInsightsConfiguration(); + var telemetryConfiguration = GenerateTelemetryConfiguration(containerLogConfiguration, applicationInsightsConfiguration); + var metricSinkConfiguration = GenerateMetricSinkConfiguration(); + var resourceDiscovery = GenerateResourceDiscoveryConfiguration(); + + var runtimeConfiguration = new ScraperRuntimeConfiguration + { + MetricsConfiguration = metricsConfiguration, + MetricSinks = metricSinkConfiguration, + ResourceDiscovery = resourceDiscovery, + Server = serverConfiguration, + Telemetry = telemetryConfiguration + }; + + return runtimeConfiguration; + } + + private static ResourceDiscoveryConfiguration GenerateResourceDiscoveryConfiguration() + { + var resourceDiscovery = new Faker() .StrictMode(true) - .RuleFor(srvConfig => srvConfig.HttpPort, faker => faker.Random.Int()) + .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Host, faker => faker.Person.FirstName) + .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Port, faker => faker.Random.Int(min: 1)) .Generate(); - var metricsConfiguration = new Faker() + return resourceDiscovery; + } + + private static MetricSinkConfiguration GenerateMetricSinkConfiguration() + { + var statsDConfiguration = GenerateStatsdSinkConfiguration(); + var prometheusScrapingEndpointSinkConfiguration = GeneratePrometheusScrapingEndpointSinkConfiguration(); + var atlassianStatusPageSinkConfiguration = GenerateAtlassianStatusPageSinkConfiguration(); + var metricSinkConfiguration = new Faker() .StrictMode(true) - .RuleFor(metricConfiguration => metricConfiguration.AbsolutePath, faker => faker.System.DirectoryPath()) + .RuleFor(sinkConfiguration => sinkConfiguration.Statsd, statsDConfiguration) + .RuleFor(sinkConfiguration => sinkConfiguration.AtlassianStatuspage, atlassianStatusPageSinkConfiguration) + .RuleFor(sinkConfiguration => sinkConfiguration.PrometheusScrapingEndpoint, prometheusScrapingEndpointSinkConfiguration) .Generate(); + return metricSinkConfiguration; + } - var containerLogConfiguration = new Faker() + private static StatsdSinkConfiguration GenerateStatsdSinkConfiguration() + { + var statsDConfiguration = new Faker() .StrictMode(true) - .RuleFor(containerConfiguration => containerConfiguration.Verbosity, faker => LogLevel.Error) - .RuleFor(containerConfiguration => containerConfiguration.IsEnabled, faker => faker.Random.Bool()) + .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.Host, faker => faker.Person.FirstName) + .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.Port, faker => faker.Random.Int(min: 0)) + .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.MetricPrefix, faker => faker.Person.FirstName) + .Generate(); + return statsDConfiguration; + } + + private static TelemetryConfiguration GenerateTelemetryConfiguration(ContainerLogConfiguration containerLogConfiguration, ApplicationInsightsConfiguration applicationInsightsConfiguration) + { + var telemetryConfiguration = new Faker() + .StrictMode(true) + .RuleFor(telemetry => telemetry.DefaultVerbosity, faker => LogLevel.Error) + .RuleFor(telemetry => telemetry.ContainerLogs, faker => containerLogConfiguration) + .RuleFor(telemetry => telemetry.ApplicationInsights, faker => applicationInsightsConfiguration) .Generate(); + return telemetryConfiguration; + } + private static ApplicationInsightsConfiguration GenerateApplicationInsightsConfiguration() + { var applicationInsightsConfiguration = new Faker() .StrictMode(true) .RuleFor(containerConfiguration => containerConfiguration.Verbosity, faker => LogLevel.Error) .RuleFor(containerConfiguration => containerConfiguration.IsEnabled, faker => faker.Random.Bool()) .RuleFor(containerConfiguration => containerConfiguration.InstrumentationKey, faker => faker.Random.Guid().ToString()) .Generate(); + return applicationInsightsConfiguration; + } - var telemetryConfiguration = new Faker() + private static MetricsConfiguration GenerateMetricsConfiguration() + { + var metricsConfiguration = new Faker() .StrictMode(true) - .RuleFor(telemetry => telemetry.DefaultVerbosity, faker => LogLevel.Error) - .RuleFor(telemetry => telemetry.ContainerLogs, faker => containerLogConfiguration) - .RuleFor(telemetry => telemetry.ApplicationInsights, faker => applicationInsightsConfiguration) + .RuleFor(metricConfiguration => metricConfiguration.AbsolutePath, faker => faker.System.DirectoryPath()) .Generate(); - var statsDConfiguration = new Faker() + return metricsConfiguration; + } + + private static ContainerLogConfiguration GenerateContainerLogConfiguration() + { + var containerLogConfiguration = new Faker() .StrictMode(true) - .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.Host, faker => faker.Person.FirstName) - .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.Port, faker => faker.Random.Int(min: 0)) - .RuleFor(statsdSinkConfiguration => statsdSinkConfiguration.MetricPrefix, faker => faker.Person.FirstName) + .RuleFor(containerConfiguration => containerConfiguration.Verbosity, faker => LogLevel.Error) + .RuleFor(containerConfiguration => containerConfiguration.IsEnabled, faker => faker.Random.Bool()) .Generate(); - var prometheusScrapingEndpointSinkConfiguration = new Faker() + return containerLogConfiguration; + } + + private static ServerConfiguration GenerateServerConfiguration() + { + var serverConfiguration = new Faker() .StrictMode(true) - .RuleFor(promConfiguration => promConfiguration.BaseUriPath, faker => faker.System.DirectoryPath()) - .RuleFor(promConfiguration => promConfiguration.MetricUnavailableValue, faker => faker.Random.Double(min: 1)) - .RuleFor(promConfiguration => promConfiguration.EnableMetricTimestamps, faker => faker.Random.Bool()) + .RuleFor(srvConfig => srvConfig.HttpPort, faker => faker.Random.Int()) .Generate(); - var metricSinkConfiguration = new Faker() + return serverConfiguration; + } + + private static AtlassianStatusPageSinkConfiguration GenerateAtlassianStatusPageSinkConfiguration() + { + var atlassianStatusPageSinkConfiguration = new Faker() .StrictMode(true) - .RuleFor(sinkConfiguration => sinkConfiguration.Statsd, statsDConfiguration) - .RuleFor(sinkConfiguration => sinkConfiguration.PrometheusScrapingEndpoint, prometheusScrapingEndpointSinkConfiguration) + .RuleFor(promConfiguration => promConfiguration.PageId, faker => faker.Person.FirstName) + .RuleFor(promConfiguration => promConfiguration.SystemMetricMapping, faker => new List { GenerateAtlassianStatuspageSystemMetricMapping() }) .Generate(); - var resourceDiscovery = new Faker() + return atlassianStatusPageSinkConfiguration; + } + + private static SystemMetricMapping GenerateAtlassianStatuspageSystemMetricMapping() + { + var systemMetricMapping = new Faker() .StrictMode(true) - .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Host, faker => faker.Person.FirstName) - .RuleFor(resourceDiscoveryConfiguration => resourceDiscoveryConfiguration.Port, faker => faker.Random.Int(min: 1)) + .RuleFor(promConfiguration => promConfiguration.Id, faker => faker.Person.FirstName) + .RuleFor(promConfiguration => promConfiguration.PromitorMetricName, faker => faker.Person.FirstName) .Generate(); + return systemMetricMapping; + } - var runtimeConfiguration = new ScraperRuntimeConfiguration - { - MetricsConfiguration = metricsConfiguration, - MetricSinks = metricSinkConfiguration, - ResourceDiscovery = resourceDiscovery, - Server = serverConfiguration, - Telemetry = telemetryConfiguration - }; - - return runtimeConfiguration; + private static PrometheusScrapingEndpointSinkConfiguration GeneratePrometheusScrapingEndpointSinkConfiguration() + { + var prometheusScrapingEndpointSinkConfiguration = new Faker() + .StrictMode(true) + .RuleFor(promConfiguration => promConfiguration.BaseUriPath, faker => faker.System.DirectoryPath()) + .RuleFor(promConfiguration => promConfiguration.MetricUnavailableValue, faker => faker.Random.Double(min: 1)) + .RuleFor(promConfiguration => promConfiguration.EnableMetricTimestamps, faker => faker.Random.Bool()) + .Generate(); + return prometheusScrapingEndpointSinkConfiguration; } } } \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Metrics/Sinks/AtlassianStatuspageMetricSinkTests.cs b/src/Promitor.Tests.Unit/Metrics/Sinks/AtlassianStatuspageMetricSinkTests.cs new file mode 100644 index 000000000..b3686d0c3 --- /dev/null +++ b/src/Promitor.Tests.Unit/Metrics/Sinks/AtlassianStatuspageMetricSinkTests.cs @@ -0,0 +1,139 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Bogus; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Promitor.Core.Metrics; +using Promitor.Integrations.Sinks.Atlassian.Statuspage; +using Promitor.Tests.Unit.Generators; +using Promitor.Tests.Unit.Generators.Config; +using Xunit; + +namespace Promitor.Tests.Unit.Metrics.Sinks +{ + [Category("Unit")] + public class AtlassianStatuspageMetricSinkTests + { + private readonly Faker _bogus = new Faker(); + + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task ReportMetricAsync_InputDoesNotContainMetricName_ThrowsException(string metricName) + { + // Arrange + var metricDescription = _bogus.Lorem.Sentence(); + var metricValue = _bogus.Random.Double(); + var scrapeResult = ScrapeResultGenerator.Generate(metricValue); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + await Assert.ThrowsAsync(() => metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult)); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task ReportMetricAsync_InputDoesNotContainMetricDescription_Succeeds(string metricDescription) + { + // Arrange + var metricName = _bogus.Name.FirstName(); + var metricValue = _bogus.Random.Double(); + var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + await metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult); + } + + [Fact] + public async Task ReportMetricAsync_InputDoesNotContainMeasuredMetric_ThrowsException() + { + // Arrange + var metricName = _bogus.Name.FirstName(); + var metricDescription = _bogus.Lorem.Sentence(); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act & Assert + // ReSharper disable once ExpressionIsAlwaysNull + await Assert.ThrowsAsync(() => metricSink.ReportMetricAsync(metricName, metricDescription, null)); + } + + [Fact] + public async Task ReportMetricAsync_GetsValidInputWithMetricValueAndPromitorToSystemMetricMapping_SuccessfullyWritesMetric() + { + // Arrange + var promitorMetricName = _bogus.Name.FirstName(); + var systemMetricId = _bogus.Name.FirstName(); + var metricDescription = _bogus.Lorem.Sentence(); + var metricValue = _bogus.Random.Double(); + var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(systemMetricId: systemMetricId, promitorMetricName: promitorMetricName); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act + await metricSink.ReportMetricAsync(promitorMetricName, metricDescription, scrapeResult); + + // Assert + atlassianStatuspageClientMock.Verify(mock => mock.ReportMetricAsync(systemMetricId, metricValue), Times.Once()); + } + + [Fact] + public async Task ReportMetricAsync_GetsValidInputWithoutMetricValueButWithPromitorToSystemMetricMapping_SuccessfullyWritesMetricWithDefault() + { + // Arrange + const double expectedDefaultValue = 0; + var promitorMetricName = _bogus.Name.FirstName(); + var systemMetricId = _bogus.Name.FirstName(); + var metricDescription = _bogus.Lorem.Sentence(); + double? metricValue = null; + // ReSharper disable once ExpressionIsAlwaysNull + var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(systemMetricId: systemMetricId, promitorMetricName: promitorMetricName); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act + await metricSink.ReportMetricAsync(promitorMetricName, metricDescription, scrapeResult); + + // Assert + atlassianStatuspageClientMock.Verify(mock => mock.ReportMetricAsync(systemMetricId, expectedDefaultValue), Times.Once()); + } + + [Fact] + public async Task ReportMetricAsync_GetsValidInputWithPromitorMetricThatIsNotMappedToSystemMetricId_DoesNotWriteMetric() + { + // Arrange + const double expectedDefaultValue = 0; + var promitorMetricName = _bogus.Name.FirstName(); + var systemMetricId = _bogus.Name.FirstName(); + var metricDescription = _bogus.Lorem.Sentence(); + double? metricValue = null; + // ReSharper disable once ExpressionIsAlwaysNull + var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); + var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(promitorMetricName: promitorMetricName); + var atlassianStatuspageClientMock = new Mock(); + var metricSink = new AtlassianStatuspageMetricSink(atlassianStatuspageClientMock.Object, systemMetricConfigOptions, NullLogger.Instance); + + // Act + await metricSink.ReportMetricAsync(promitorMetricName, metricDescription, scrapeResult); + + // Assert + atlassianStatuspageClientMock.Verify(mock => mock.ReportMetricAsync(systemMetricId, expectedDefaultValue), Times.Never); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTests.cs b/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTests.cs deleted file mode 100644 index b4cb0f6a3..000000000 --- a/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.ComponentModel; -using System.Threading.Tasks; -using Bogus; -using JustEat.StatsD; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Promitor.Core.Metrics; -using Promitor.Integrations.Sinks.Statsd; -using Promitor.Tests.Unit.Generators; -using Xunit; - -namespace Promitor.Tests.Unit.Metrics.Sinks -{ - [Category("Unit")] - public class PrometheusScrapingEndpointMetricSinkTests - { - private readonly Faker _bogus = new Faker(); - - [Theory] - [InlineData("")] - [InlineData(null)] - public async Task ReportMetricAsync_InputDoesNotContainMetricName_ThrowsException(string metricName) - { - // Arrange - var metricDescription = _bogus.Lorem.Sentence(); - var metricValue = _bogus.Random.Double(); - var scrapeResult = ScrapeResultGenerator.Generate(metricValue); - var statsDPublisherMock = new Mock(); - var metricSink = new StatsdMetricSink(statsDPublisherMock.Object, NullLogger.Instance); - - // Act & Assert - // ReSharper disable once ExpressionIsAlwaysNull - await Assert.ThrowsAsync(() => metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult)); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - public async Task ReportMetricAsync_InputDoesNotContainMetricDescription_Succeeds(string metricDescription) - { - // Arrange - var metricName = _bogus.Name.FirstName(); - var metricValue = _bogus.Random.Double(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); - var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); - var statsDPublisherMock = new Mock(); - var metricSink = new StatsdMetricSink(statsDPublisherMock.Object, NullLogger.Instance); - - // Act & Assert - // ReSharper disable once ExpressionIsAlwaysNull - await metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult); - } - - [Fact] - public async Task ReportMetricAsync_InputDoesNotContainMeasuredMetric_ThrowsException() - { - // Arrange - var metricName = _bogus.Name.FirstName(); - var metricDescription = _bogus.Lorem.Sentence(); - var statsDPublisherMock = new Mock(); - var metricSink = new StatsdMetricSink(statsDPublisherMock.Object, NullLogger.Instance); - - // Act & Assert - // ReSharper disable once ExpressionIsAlwaysNull - await Assert.ThrowsAsync(() => metricSink.ReportMetricAsync(metricName, metricDescription, null)); - } - - [Fact] - public async Task ReportMetricAsync_GetsValidInputWithMetricValue_SuccessfullyWritesMetric() - { - // Arrange - var metricName = _bogus.Name.FirstName(); - var metricDescription = _bogus.Lorem.Sentence(); - var metricValue = _bogus.Random.Double(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); - var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); - var statsDPublisherMock = new Mock(); - var metricSink = new StatsdMetricSink(statsDPublisherMock.Object, NullLogger.Instance); - - // Act - await metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult); - - // Assert - statsDPublisherMock.Verify(mock => mock.Gauge(metricValue, metricName), Times.Once()); - } - - [Fact] - public async Task ReportMetricAsync_GetsValidInputWithoutMetricValue_SuccessfullyWritesMetricWithDefault() - { - // Arrange - const double expectedDefaultValue = 0; - var metricName = _bogus.Name.FirstName(); - var metricDescription = _bogus.Lorem.Sentence(); - double? metricValue = null; - // ReSharper disable once ExpressionIsAlwaysNull - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); - var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); - var statsDPublisherMock = new Mock(); - var metricSink = new StatsdMetricSink(statsDPublisherMock.Object, NullLogger.Instance); - - // Act - await metricSink.ReportMetricAsync(metricName, metricDescription, scrapeResult); - - // Assert - statsDPublisherMock.Verify(mock => mock.Gauge(expectedDefaultValue, metricName), Times.Once()); - } - } -} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Stubs/HttpMessageHandlerStub.cs b/src/Promitor.Tests.Unit/Stubs/HttpMessageHandlerStub.cs new file mode 100644 index 000000000..53a65932e --- /dev/null +++ b/src/Promitor.Tests.Unit/Stubs/HttpMessageHandlerStub.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Promitor.Tests.Unit.Stubs +{ + public class HttpMessageHandlerStub : HttpMessageHandler + { + public HttpRequestMessage LastRequest { get; set; } + + public virtual HttpResponseMessage Send(HttpRequestMessage request) + { + LastRequest = request; + return new HttpResponseMessage(HttpStatusCode.OK); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(Send(request)); + } + } +} diff --git a/src/Promitor.Tests.Unit/Stubs/OptionsMonitorStub.cs b/src/Promitor.Tests.Unit/Stubs/OptionsMonitorStub.cs index 5fa500c18..4fb0ffb95 100644 --- a/src/Promitor.Tests.Unit/Stubs/OptionsMonitorStub.cs +++ b/src/Promitor.Tests.Unit/Stubs/OptionsMonitorStub.cs @@ -5,6 +5,11 @@ namespace Promitor.Tests.Unit.Stubs { public class OptionsMonitorStub : IOptionsMonitor { + public OptionsMonitorStub(TConfig currentValue) + { + CurrentValue = currentValue; + } + public TConfig Get(string name) { return CurrentValue; diff --git a/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/AtlassianStatuspageMetricSinkValidationStepTests.cs b/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/AtlassianStatuspageMetricSinkValidationStepTests.cs new file mode 100644 index 000000000..cc43445b7 --- /dev/null +++ b/src/Promitor.Tests.Unit/Validation/Metrics/Sinks/AtlassianStatuspageMetricSinkValidationStepTests.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using AutoMapper; +using Bogus; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Promitor.Agents.Scraper.Configuration; +using Promitor.Agents.Scraper.Validation.Steps.Sinks; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Mapping; +using Promitor.Integrations.Sinks.Atlassian.Statuspage.Configuration; +using Promitor.Tests.Unit.Builders.Metrics.v1; +using Promitor.Tests.Unit.Generators.Config; +using Promitor.Tests.Unit.Stubs; +using Xunit; + +namespace Promitor.Tests.Unit.Validation.Metrics.Sinks +{ + [Category("Unit")] + public class AtlassianStatuspageMetricSinkValidationStepTests + { + private readonly IMapper _mapper; + + public AtlassianStatuspageMetricSinkValidationStepTests() + { + var mapperConfiguration = new MapperConfiguration(c => c.AddProfile()); + _mapper = mapperConfiguration.CreateMapper(); + } + + [Fact] + public void Validate_AtlassianStatuspageIsFullyConfigured_Success() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageIsNotConfigured_Success() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful); + } + [Fact] + public void Validate_NoSinksConfigured_Success() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + runtimeConfiguration.Value.MetricSinks = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithEmptyPageId_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.PageId = string.Empty; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithoutPageId_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.PageId = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithoutSystemMetricId_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping[0].Id = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithEmptySystemMetricId_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping[0].Id = string.Empty; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithoutPromitorMetricName_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping[0].PromitorMetricName = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithEmptyPromitorMetricName_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping[0].PromitorMetricName = string.Empty; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithoutSystemMetricMapping_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping = null; + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithDuplicateIdsInSystemMetricMapping_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var systemMetricMapping = new SystemMetricMapping + { + Id = Guid.NewGuid().ToString(), + PromitorMetricName = metricName + }; + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping.Clear(); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping.Add(systemMetricMapping); + runtimeConfiguration.Value.MetricSinks.AtlassianStatuspage.SystemMetricMapping.Add(systemMetricMapping); + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithUnmappedSystemMetricMapping_Fails() + { + // Arrange + const string metricName = "my_metric"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(metricName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName: "other_metric"); + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspageWithPromitorMetricUsingResourceDiscovery_Fails() + { + // Arrange + const string metricName = "my_metric"; + const string resourceDiscoveryGroupName = "my_discovery_group"; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(resourceDiscoveryGroupName: resourceDiscoveryGroupName) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + [Fact] + public void Validate_AtlassianStatuspagePromitorMetricScrapingMultipleResources_Fails() + { + // Arrange + const string metricName = "my_metric"; + List queueNames = new List { "queue-1", "queue-2"}; + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithServiceBusMetric(queueNames: queueNames) + .Build(_mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, _mapper); + var runtimeConfiguration = CreateRuntimeConfiguration(metricName); + + // Act + var azureAuthenticationValidationStep = new AtlassianStatuspageMetricSinkValidationStep(runtimeConfiguration, metricsDeclarationProvider, NullLogger.Instance); + var validationResult = azureAuthenticationValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful); + } + + private IOptions CreateRuntimeConfiguration(string metricName, string pageId = null, string systemMetricId = null) + { + var bogusRuntimeConfiguration = BogusScraperRuntimeConfigurationGenerator.Generate(); + bogusRuntimeConfiguration.MetricSinks.AtlassianStatuspage.SystemMetricMapping.Clear(); + + if (string.IsNullOrWhiteSpace(pageId) == false) + { + bogusRuntimeConfiguration.MetricSinks.AtlassianStatuspage.PageId = pageId; + } + + var systemMetricMapping = new Faker() + .StrictMode(true) + .RuleFor(map => map.Id, faker => systemMetricId ?? faker.Person.FirstName) + .RuleFor(map => map.PromitorMetricName, faker => metricName?? faker.Person.FirstName) + .Generate(); + + bogusRuntimeConfiguration.MetricSinks.AtlassianStatuspage.SystemMetricMapping.Add(systemMetricMapping); + + return Options.Create(bogusRuntimeConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Promitor.sln b/src/Promitor.sln index 04c5a4590..7813cbe9e 100644 --- a/src/Promitor.sln +++ b/src/Promitor.sln @@ -58,6 +58,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Discovery", "Discovery", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Promitor.Core.Contracts", "Promitor.Core.Contracts\Promitor.Core.Contracts.csproj", "{EE3568F2-BB20-4BD2-BD21-AE43E5B1EB77}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Promitor.Integrations.Sinks.Atlassian.Statuspage", "Promitor.Integrations.Sinks.Atlassian.Statuspage\Promitor.Integrations.Sinks.Atlassian.Statuspage.csproj", "{B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,10 @@ Global {EE3568F2-BB20-4BD2-BD21-AE43E5B1EB77}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE3568F2-BB20-4BD2-BD21-AE43E5B1EB77}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE3568F2-BB20-4BD2-BD21-AE43E5B1EB77}.Release|Any CPU.Build.0 = Release|Any CPU + {B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,6 +143,7 @@ Global {EE3BDE48-B756-481F-B5BB-857569568878} = {1C184DB7-C5E5-41F4-9E0C-8A798B9ED0CA} {33D4263D-DDDC-4ECC-BB25-834DE9979C7C} = {1C184DB7-C5E5-41F4-9E0C-8A798B9ED0CA} {EE3568F2-BB20-4BD2-BD21-AE43E5B1EB77} = {05524B1B-9965-4E36-9626-A2823B8588AA} + {B30E58FF-47AD-41FC-94D2-DE4B9B6AD06B} = {59D7FFBC-3929-48AD-8C9C-242280D01CBD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B47A777A-C793-453F-8F4F-7703551DFC4C} diff --git a/src/Promitor.sln.DotSettings b/src/Promitor.sln.DotSettings index af5d5918a..03e320853 100644 --- a/src/Promitor.sln.DotSettings +++ b/src/Promitor.sln.DotSettings @@ -7,6 +7,10 @@ SUGGESTION SUGGESTION SUGGESTION + Required + Required + Required + Required False NEVER True @@ -18,6 +22,61 @@ False <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + DO_NOTHING + LIVE_MONITOR + LIVE_MONITOR + DO_NOTHING + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + LIVE_MONITOR + DO_NOTHING + LIVE_MONITOR True True True @@ -25,6 +84,7 @@ True True True + True True True True @@ -32,4 +92,5 @@ True True True + True True \ No newline at end of file diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml index 028ab01b1..167b4dabb 100644 --- a/src/docker-compose.override.yml +++ b/src/docker-compose.override.yml @@ -6,7 +6,7 @@ services: - "ASPNETCORE_ENVIRONMENT=Development" - "PROMITOR_INTERNAL_CONFIG_FOLDER=/config/" - "PROMITOR_DISCOVERY_APPID=67882a00-21d3-4ee7-b32a-430ea0768cd3" - - "PROMITOR_DISCOVERY_APPSECRET=_M6e4CNu]x2SSN0hUFobamWttPyVXl?-" + - "PROMITOR_DISCOVERY_APPSECRET=" ports: - "777:88" promitor.agents.scraper: @@ -14,7 +14,8 @@ services: - ASPNETCORE_ENVIRONMENT=Development - PROMITOR_INTERNAL_CONFIG_FOLDER=/config/ - PROMITOR_AUTH_APPID=ceb249a3-44ce-4c90-8863-6776336f5b7e - - PROMITOR_AUTH_APPKEY=ZgLy6zYNh9SEmIl0B+rv+ZuQQ2wJyQi/tTXnp2Wp9PM= - - "SECRETS_STORAGEQUEUE_SAS=?sv=2018-03-28&ss=bfqt&srt=sco&sp=rwla&se=2022-08-07T00:16:01Z&st=2019-08-06T16:16:01Z&spr=https&sig=Ik4jprS89kGIFRM0qaQpXrv0ttP3pnlhmGQuYVQ7cbA%3D" + - PROMITOR_AUTH_APPKEY= + - PROMITOR_ATLASSIAN_STATUSPAGE_APIKEY= + - SECRETS_STORAGEQUEUE_SAS= ports: - "888:88" \ No newline at end of file diff --git a/src/docker-compose.vs.debug.yml b/src/docker-compose.vs.debug.yml new file mode 100644 index 000000000..aa671e66c --- /dev/null +++ b/src/docker-compose.vs.debug.yml @@ -0,0 +1,11 @@ +version: '3.4' + +services: + promitor.agents.resourcediscovery: + environment: + - "PROMITOR_DISCOVERY_APPSECRET=" + promitor.agents.scraper: + environment: + - PROMITOR_AUTH_APPKEY= + - PROMITOR_ATLASSIAN_STATUSPAGE_APIKEY= + - "SECRETS_STORAGEQUEUE_SAS=" \ No newline at end of file