diff --git a/changelog/content/experimental/unreleased.md b/changelog/content/experimental/unreleased.md index 9e7de578d..41ae7d210 100644 --- a/changelog/content/experimental/unreleased.md +++ b/changelog/content/experimental/unreleased.md @@ -8,8 +8,10 @@ version: - {{% tag added %}} Provide support for all label scenarios in StatsD & OpenTelemetry metric sink. This includes dimensions, customer & default labels. +- {{% tag added %}} Provide support for scraping multiple metrics dimensions. - {{% tag changed %}} Switch to Mariner distroless base images - {{% tag security %}} Patch for [CVE-2023-29331](https://github.com/advisories/GHSA-555c-2p6r-68mm) (High) +- {{% tag deprecated %}} Support for scraping single metric dimension by using `metrics[x].azureMetricConfiguration.dimension`. #### Resource Discovery diff --git a/config/promitor/scraper/metrics.yaml b/config/promitor/scraper/metrics.yaml index ed63baabf..fae8f91e8 100644 --- a/config/promitor/scraper/metrics.yaml +++ b/config/promitor/scraper/metrics.yaml @@ -173,7 +173,7 @@ metrics: resources: - accountName: promitor-testing-resource-eu-automation-1 - name: promitor_demo_frontdoor_backend_health_per_backend_pool - description: "Health percentage for a backed in Azure Front Door" + description: "Health percentage for a backend in Azure Front Door" resourceType: FrontDoor labels: app: promitor @@ -243,7 +243,7 @@ metrics: type: Total resourceDiscoveryGroups: - name: data-factory-landscape - - name: promitor_demo_application_insights_availability + - name: application_insights_availability description: "Availability (%) of promitor.io measured in Azure Application Insights" resourceType: ApplicationInsights azureMetricConfiguration: @@ -257,6 +257,39 @@ metrics: # Application Insights with data in Log Analytics - name: promitor-testing-resource-eu-telemetry resourceGroupName: promitor-testing-infrastructure-europe + - name: application_insights_availability_per_name + description: "Availability (%) of promitor.io measured in Azure Application Insights per name" + resourceType: ApplicationInsights + azureMetricConfiguration: + metricName: availabilityResults/availabilityPercentage + aggregation: + type: Average + dimension: + name: availabilityResult/name + resources: + # Application Insights with data in the service itself (classic, deprecated) + - name: promitor-testing-resource-eu-telemetry-classic + resourceGroupName: promitor-testing-infrastructure-europe + # Application Insights with data in Log Analytics + - name: promitor-testing-resource-eu-telemetry + resourceGroupName: promitor-testing-infrastructure-europe + - name: application_insights_availability_per_name_and_location + description: "Availability (%) of promitor.io measured in Azure Application Insights per name and location" + resourceType: ApplicationInsights + azureMetricConfiguration: + metricName: availabilityResults/availabilityPercentage + aggregation: + type: Average + dimensions: + - name: availabilityResult/name + - name: availabilityResult/location + resources: + # Application Insights with data in the service itself (classic, deprecated) + - name: promitor-testing-resource-eu-telemetry-classic + resourceGroupName: promitor-testing-infrastructure-europe + # Application Insights with data in Log Analytics + - name: promitor-testing-resource-eu-telemetry + resourceGroupName: promitor-testing-infrastructure-europe - name: promitor_demo_cdn_requests_discovered description: "Amount of requests sent to Azure CDN" resourceType: Cdn diff --git a/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj b/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj index 7a6ed9db4..c83b51d61 100644 --- a/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj +++ b/src/Promitor.Agents.Scraper/Promitor.Agents.Scraper.csproj @@ -12,11 +12,11 @@ - 1701;1702;1591 + 1701;1702;1591;618 - 1701;1702;1591 + 1701;1702;1591;618 diff --git a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/AzureMetricConfigurationValidator.cs b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/AzureMetricConfigurationValidator.cs index c2aeb65b8..bdf760d8f 100644 --- a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/AzureMetricConfigurationValidator.cs +++ b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/AzureMetricConfigurationValidator.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Kusto.Language; +using System.Linq; using Promitor.Core.Scraping.Configuration.Model; using Promitor.Core.Scraping.Configuration.Model.Metrics; using ResourceType = Promitor.Core.Contracts.ResourceType; @@ -54,6 +55,11 @@ private IEnumerable ValidateAzureMetricConfiguration(AzureMetricConfigur } } + if (azureMetricConfiguration.Dimension != null && azureMetricConfiguration.Dimensions.Any()) + { + errorMessages.Add("Only one of 'dimensions' and 'dimension' is allowed. Please use 'dimensions'."); + } + var metricAggregationValidator = new MetricAggregationValidator(_metricDefaults); var metricsAggregationErrorMessages = metricAggregationValidator.Validate(azureMetricConfiguration.Aggregation); errorMessages.AddRange(metricsAggregationErrorMessages); diff --git a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/EventHubsMetricValidator.cs b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/EventHubsMetricValidator.cs index bb03f5f01..48d670b91 100644 --- a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/EventHubsMetricValidator.cs +++ b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/EventHubsMetricValidator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using GuardNet; using Promitor.Core.Scraping.Configuration.Model.Metrics; @@ -18,8 +17,12 @@ public IEnumerable Validate(MetricDefinition metricDefinition) var errorMessages = new List(); - var configuredDimension = metricDefinition.AzureMetricConfiguration?.Dimension?.Name; - var isEntityNameDimensionConfigured = string.IsNullOrWhiteSpace(configuredDimension) == false && configuredDimension.Equals(EntityNameDimension, StringComparison.InvariantCultureIgnoreCase); + if (metricDefinition.AzureMetricConfiguration?.Dimensions?.Count > 1) + { + errorMessages.Add("At least one Dimension other than EntityName is defined."); + } + + var isEntityNameDimensionConfigured = metricDefinition.AzureMetricConfiguration?.HasDimension(EntityNameDimension) ?? false; foreach (var resourceDefinition in metricDefinition.Resources.Cast()) { diff --git a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/ServiceBusNamespaceMetricValidator.cs b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/ServiceBusNamespaceMetricValidator.cs index 570cd70c8..60602d6dd 100644 --- a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/ServiceBusNamespaceMetricValidator.cs +++ b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/ServiceBusNamespaceMetricValidator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using GuardNet; using Promitor.Core.Scraping.Configuration.Model.Metrics; @@ -18,8 +17,12 @@ public IEnumerable Validate(MetricDefinition metricDefinition) var errorMessages = new List(); - var configuredDimension = metricDefinition.AzureMetricConfiguration?.Dimension?.Name; - var isEntityNameDimensionConfigured = string.IsNullOrWhiteSpace(configuredDimension) == false && configuredDimension.Equals(EntityNameDimension, StringComparison.InvariantCultureIgnoreCase); + if (metricDefinition.AzureMetricConfiguration?.Dimensions?.Count > 1) + { + errorMessages.Add("At least one Dimension other than EntityName is defined."); + } + + var isEntityNameDimensionConfigured = metricDefinition.AzureMetricConfiguration?.HasDimension(EntityNameDimension) ?? false; foreach (var resourceDefinition in metricDefinition.Resources.Cast()) { diff --git a/src/Promitor.Core.Scraping/AzureMonitorScraper.cs b/src/Promitor.Core.Scraping/AzureMonitorScraper.cs index a7ff55496..edae805f0 100644 --- a/src/Promitor.Core.Scraping/AzureMonitorScraper.cs +++ b/src/Promitor.Core.Scraping/AzureMonitorScraper.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using GuardNet; using Microsoft.Azure.Management.Monitor.Fluent.Models; using Microsoft.Extensions.Logging; using Promitor.Core.Contracts; using Promitor.Core.Metrics; +using Promitor.Core.Scraping.Configuration.Model; using Promitor.Core.Scraping.Configuration.Model.Metrics; using Promitor.Integrations.AzureMonitor.Exceptions; -using MetricDimension = Promitor.Core.Scraping.Configuration.Model.MetricDimension; namespace Promitor.Core.Scraping { @@ -45,19 +46,19 @@ protected override async Task ScrapeResourceAsync(string subscript var metricLimit = DetermineMetricLimit(scrapeDefinition); // Determine the metric dimension to use, if any - var dimensionName = DetermineMetricDimension(metricName, resourceDefinition, scrapeDefinition.AzureMetricConfiguration?.Dimension); + var dimensionNames = DetermineMetricDimensions(metricName, resourceDefinition, scrapeDefinition.AzureMetricConfiguration); - List measuredMetrics = new List(); + var measuredMetrics = new List(); try { // Query Azure Monitor for metrics - measuredMetrics = await AzureMonitorClient.QueryMetricAsync(metricName, dimensionName, aggregationType, aggregationInterval, resourceUri, metricFilter, metricLimit); + measuredMetrics = await AzureMonitorClient.QueryMetricAsync(metricName, dimensionNames, aggregationType, aggregationInterval, resourceUri, metricFilter, metricLimit); } catch (MetricInformationNotFoundException metricsNotFoundException) { - Logger.LogWarning("No metric information found for metric {MetricName} with dimension {MetricDimension}. Details: {Details}", metricsNotFoundException.Name, metricsNotFoundException.Dimension, metricsNotFoundException.Details); + Logger.LogWarning("No metric information found for metric {MetricName} with dimensions {MetricDimensions}. Details: {Details}", metricsNotFoundException.Name, metricsNotFoundException.Dimensions, metricsNotFoundException.Details); - var measuredMetric = string.IsNullOrWhiteSpace(dimensionName) ? MeasuredMetric.CreateWithoutDimension(null) : MeasuredMetric.CreateForDimension(null, dimensionName, "unknown"); + var measuredMetric = dimensionNames.Any() ? MeasuredMetric.CreateForDimensions(dimensionNames) : MeasuredMetric.CreateWithoutDimensions(null); measuredMetrics.Add(measuredMetric); } @@ -65,7 +66,7 @@ protected override async Task ScrapeResourceAsync(string subscript var metricLabels = DetermineMetricLabels(resourceDefinition); // Enrich measured metrics, in case we need to - var finalMetricValues = EnrichMeasuredMetrics(resourceDefinition, dimensionName, measuredMetrics); + var finalMetricValues = EnrichMeasuredMetrics(resourceDefinition, dimensionNames, measuredMetrics); // We're done! return new ScrapeResult(subscriptionId, scrapeDefinition.ResourceGroupName, resourceDefinition.ResourceName, resourceUri, finalMetricValues, metricLabels); @@ -84,10 +85,10 @@ protected override async Task ScrapeResourceAsync(string subscript /// metrics to align with others /// /// Contains the resource cast to the specific resource type. - /// Name of the specified dimension provided by the scraper + /// List of names of the specified dimensions provided by the scraper. /// Measured metric values that were found /// - protected virtual List EnrichMeasuredMetrics(TResourceDefinition resourceDefinition, string dimensionName, List metricValues) + protected virtual List EnrichMeasuredMetrics(TResourceDefinition resourceDefinition, List dimensionNames, List metricValues) { return metricValues; } @@ -107,10 +108,16 @@ protected virtual string DetermineMetricFilter(string metricName, TResourceDefin /// /// Name of the metric being queried /// Contains the resource cast to the specific resource type. - /// Provides information concerning the configured metric dimension. - protected virtual string DetermineMetricDimension(string metricName, TResourceDefinition resourceDefinition, MetricDimension dimension) + /// + protected virtual List DetermineMetricDimensions(string metricName, TResourceDefinition resourceDefinition, AzureMetricConfiguration configuration) { - return dimension?.Name; + if (configuration.Dimension != null) + { + Logger.LogWarning("AzureMetricConfiguration property 'dimension' has been deprecated and will be removed in a future update. Please use 'dimensions' instead."); + return string.IsNullOrWhiteSpace(configuration.Dimension.Name) ? new List() : new List{ configuration.Dimension.Name }; + } + + return configuration.Dimensions?.Select(dimension => dimension.Name).Where(dimensionName => !string.IsNullOrWhiteSpace(dimensionName)).Distinct().ToList(); } /// diff --git a/src/Promitor.Core.Scraping/Configuration/Model/AzureMetricConfiguration.cs b/src/Promitor.Core.Scraping/Configuration/Model/AzureMetricConfiguration.cs index 577db9a8d..f1bac2974 100644 --- a/src/Promitor.Core.Scraping/Configuration/Model/AzureMetricConfiguration.cs +++ b/src/Promitor.Core.Scraping/Configuration/Model/AzureMetricConfiguration.cs @@ -1,4 +1,8 @@ -namespace Promitor.Core.Scraping.Configuration.Model +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Promitor.Core.Scraping.Configuration.Model { public class AzureMetricConfiguration { @@ -12,14 +16,34 @@ public class AzureMetricConfiguration /// public int? Limit { get; set; } + /// + /// Information about the dimensions of an Azure Monitor metric + /// + public IReadOnlyCollection Dimensions { get; set; } + /// /// Information about the dimension of an Azure Monitor metric /// + [Obsolete("Dimension is deprecated, please use Dimensions instead.")] public MetricDimension Dimension { get; set; } /// /// Configuration on how to aggregate the metric /// public MetricAggregation Aggregation { get; set; } + + /// + /// Checks whether the configuration contains a dimension with the given name. + /// + /// Dimension name to be checked for. + /// true if the dimension name was found, false otherwise + public bool? HasDimension(string dimensionName) + { + if (Dimension != null) + { + return Dimension?.Name?.Equals(dimensionName, StringComparison.InvariantCultureIgnoreCase); + } + return Dimensions?.Any(dimension => dimension.Name.Equals(dimensionName, StringComparison.InvariantCultureIgnoreCase)); + } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs index 65f1e74e6..07463f3d3 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs @@ -116,7 +116,12 @@ private static object GetFieldValue( if (fieldDeserializationInfo.Deserializer != null) { - return fieldDeserializationInfo.Deserializer.DeserializeObject((YamlMappingNode)fieldNodePair.Value, errorReporter); + return fieldNodePair.Value switch + { + YamlMappingNode node => fieldDeserializationInfo.Deserializer.Deserialize(node, errorReporter), + YamlSequenceNode node => fieldDeserializationInfo.Deserializer.Deserialize(node, errorReporter), + _ => null + }; } var propertyType = Nullable.GetUnderlyingType(fieldDeserializationInfo.PropertyInfo.PropertyType) ?? fieldDeserializationInfo.PropertyInfo.PropertyType; diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs index 653aa39c2..8ffe4ba8e 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs @@ -27,7 +27,7 @@ public FieldDeserializationInfo( bool isRequired, object defaultValue, Func, IErrorReporter, object> customMapperFunc, - IDeserializer deserializer, + IDeserializer deserializer, IReadOnlyCollection validators) { YamlFieldName = GetName(propertyInfo); @@ -67,7 +67,7 @@ public FieldDeserializationInfo( /// /// Gets a deserializer to use when deserializing the field. /// - public IDeserializer Deserializer { get; } + public IDeserializer Deserializer { get; } /// /// Gets the custom validators for the field. diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfoBuilder.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfoBuilder.cs index d3cd42e66..aa1373d2b 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfoBuilder.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfoBuilder.cs @@ -17,7 +17,7 @@ public class FieldDeserializationInfoBuilder : IFieldDeseriali private bool _isRequired; private object _defaultValue; private Func, IErrorReporter, object> _customMapperFunc; - private IDeserializer _deserializer; + private IDeserializer _deserializer; /// /// Sets the expression that defines the property to map. @@ -71,12 +71,13 @@ public FieldDeserializationInfoBuilder MapUsing(Func - /// Specifies an to use to map the field. + /// Specifies an to use to map the field. /// /// The deserializer. /// The builder. - public FieldDeserializationInfoBuilder MapUsingDeserializer(IDeserializer deserializer) + public FieldDeserializationInfoBuilder MapUsingDeserializer(IDeserializer deserializer) { _deserializer = deserializer; diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs index 6096f159c..3d2296377 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs @@ -3,25 +3,11 @@ namespace Promitor.Core.Scraping.Configuration.Serialization { - /// - /// An object that can deserialize a yaml node into an object. - /// - public interface IDeserializer - { - /// - /// Deserializes the specified node. - /// - /// The node to deserialize. - /// Used to report deserialization errors. - /// The deserialized object. - object DeserializeObject(YamlMappingNode node, IErrorReporter errorReporter); - } - /// /// An object that can deserialize a yaml node into an object. /// /// The type of object that can be deserialized. - public interface IDeserializer : IDeserializer where TObject: new() + public interface IDeserializer { /// /// Deserializes the specified node. diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs index bb9467c1d..f476db65a 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs @@ -14,11 +14,13 @@ public AzureMetricConfigurationDeserializer(IDeserializer dim .IsRequired(); Map(config => config.Limit) .MapUsing(DetermineLimit); - Map(config => config.Dimension) - .MapUsingDeserializer(dimensionDeserializer); Map(config => config.Aggregation) .IsRequired() .MapUsingDeserializer(aggregationDeserializer); + Map(config => config.Dimensions) + .MapUsingDeserializer(dimensionDeserializer); + Map(config => config.Dimension) + .MapUsingDeserializer(dimensionDeserializer); } private object DetermineLimit(string rawLimit, KeyValuePair nodePair, IErrorReporter errorReporter) diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/AzureMetricConfigurationV1.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/AzureMetricConfigurationV1.cs index 2f33068ef..c6c0cf2f7 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/AzureMetricConfigurationV1.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/AzureMetricConfigurationV1.cs @@ -1,4 +1,7 @@ -namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Model +using System; +using System.Collections.Generic; + +namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Model { public class AzureMetricConfigurationV1 { @@ -12,9 +15,15 @@ public class AzureMetricConfigurationV1 /// public int? Limit { get; set; } + /// + /// Information about the dimensions of an Azure Monitor metric + /// + public IReadOnlyCollection Dimensions { get; set; } + /// /// Information about the dimension of an Azure Monitor metric /// + [Obsolete("Dimension is deprecated, please use Dimensions instead.")] public MetricDimensionV1 Dimension { get; set; } /// diff --git a/src/Promitor.Core.Scraping/LogAnalyticsScraper.cs b/src/Promitor.Core.Scraping/LogAnalyticsScraper.cs index eaae858ce..15ba7e508 100644 --- a/src/Promitor.Core.Scraping/LogAnalyticsScraper.cs +++ b/src/Promitor.Core.Scraping/LogAnalyticsScraper.cs @@ -34,7 +34,7 @@ protected override async Task ScrapeResourceAsync(string subscript // Query Azure Log Analytics for result var result = await _logAnalyticsClient.RunKustoQueryAsync(workspaceId, query, aggregationInterval); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(result); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(result); measuredMetrics.Add(measuredMetric); var metricLabels = DetermineMetricLabels(resourceDefinition); diff --git a/src/Promitor.Core.Scraping/Promitor.Core.Scraping.csproj b/src/Promitor.Core.Scraping/Promitor.Core.Scraping.csproj index 40f8c596f..5c74f5bd6 100644 --- a/src/Promitor.Core.Scraping/Promitor.Core.Scraping.csproj +++ b/src/Promitor.Core.Scraping/Promitor.Core.Scraping.csproj @@ -7,11 +7,11 @@ - 1701;1702;1591 + 1701;1702;1591;618 - 1701;1702;1591 + 1701;1702;1591;618 diff --git a/src/Promitor.Core.Scraping/ResourceTypes/AzureMessagingScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/AzureMessagingScraper.cs index 80db3ad01..88bb8acac 100644 --- a/src/Promitor.Core.Scraping/ResourceTypes/AzureMessagingScraper.cs +++ b/src/Promitor.Core.Scraping/ResourceTypes/AzureMessagingScraper.cs @@ -16,12 +16,12 @@ protected AzureMessagingScraper(ScraperConfiguration scraperConfiguration) { } - protected override List EnrichMeasuredMetrics(TResourceDefinition resourceDefinition, string dimensionName, List metricValues) + protected override List EnrichMeasuredMetrics(TResourceDefinition resourceDefinition, List dimensionNames, List metricValues) { // Change Azure Monitor Dimension name to more representable value - foreach (var measuredMetric in metricValues.Where(metricValue => string.IsNullOrWhiteSpace(metricValue.DimensionName) == false)) + foreach (var measuredMetric in metricValues.Where(metricValue => metricValue.Dimensions.Any())) { - measuredMetric.DimensionName = EntityNameLabel; + measuredMetric.Dimensions[0].Name = EntityNameLabel; } return metricValues; @@ -40,16 +40,16 @@ protected override Dictionary DetermineMetricLabels(TResourceDef return metricLabels; } - protected override string DetermineMetricDimension(string metricName, TResourceDefinition resourceDefinition, MetricDimension dimension) + protected override List DetermineMetricDimensions(string metricName, TResourceDefinition resourceDefinition, AzureMetricConfiguration configuration) { if (IsEntityDeclared(resourceDefinition)) { - return base.DetermineMetricDimension(metricName, resourceDefinition, dimension); + return base.DetermineMetricDimensions(metricName, resourceDefinition, configuration); } Logger.LogTrace("Using 'EntityName' dimension since no topic was configured."); - return "EntityName"; + return new List { "EntityName" }; } protected override string DetermineMetricFilter(string metricName, TResourceDefinition resourceDefinition) diff --git a/src/Promitor.Core.Scraping/ResourceTypes/DataFactoryScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/DataFactoryScraper.cs index a684b5058..9b2d6fb96 100644 --- a/src/Promitor.Core.Scraping/ResourceTypes/DataFactoryScraper.cs +++ b/src/Promitor.Core.Scraping/ResourceTypes/DataFactoryScraper.cs @@ -48,17 +48,17 @@ protected override Dictionary DetermineMetricLabels(DataFactoryR return metricLabels; } - protected override string DetermineMetricDimension(string metricName, DataFactoryResourceDefinition resourceDefinition, MetricDimension dimension) + protected override List DetermineMetricDimensions(string metricName, DataFactoryResourceDefinition resourceDefinition, AzureMetricConfiguration configuration) { if (IsPipelineNameConfigured(resourceDefinition)) { - return base.DetermineMetricDimension(metricName, resourceDefinition, dimension); + return base.DetermineMetricDimensions(metricName, resourceDefinition, configuration); } var dimensionName = GetMetricFilterFieldName(metricName); Logger.LogTrace($"Using '{dimensionName}' dimension since no pipeline name was configured."); - return dimensionName; + return new List { dimensionName }; } private static bool IsPipelineNameConfigured(DataFactoryResourceDefinition resourceDefinition) diff --git a/src/Promitor.Core.Scraping/ResourceTypes/DataShareScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/DataShareScraper.cs index 5ac73c34b..c5b26abbf 100644 --- a/src/Promitor.Core.Scraping/ResourceTypes/DataShareScraper.cs +++ b/src/Promitor.Core.Scraping/ResourceTypes/DataShareScraper.cs @@ -38,26 +38,25 @@ protected override string DetermineMetricFilter(string metricName, DataShareReso return $"{fieldName} eq '{entityName}'"; } - protected override string DetermineMetricDimension(string metricName, DataShareResourceDefinition resourceDefinition, MetricDimension dimension) + protected override List DetermineMetricDimensions(string metricName, DataShareResourceDefinition resourceDefinition, AzureMetricConfiguration configuration) { if (IsShareNameConfigured(resourceDefinition)) { - return base.DetermineMetricDimension(metricName, resourceDefinition, dimension); + return base.DetermineMetricDimensions(metricName, resourceDefinition, configuration); } var dimensionName = GetMetricFilterFieldName(metricName); Logger.LogTrace($"Using '{dimensionName}' dimension since no share name was configured."); - return dimensionName; + return new List { dimensionName }; } - protected override List EnrichMeasuredMetrics(DataShareResourceDefinition resourceDefinition, string dimensionName, List metricValues) + protected override List EnrichMeasuredMetrics(DataShareResourceDefinition resourceDefinition, List dimensionNames, List metricValues) { // Change Azure Monitor dimension name to more representable value - foreach (var measuredMetric in metricValues.Where(metricValue => metricValue.DimensionName == "ShareName" - || metricValue.DimensionName == "ShareSubscriptionName")) + foreach (var dimension in metricValues.SelectMany(measuredMetric => measuredMetric.Dimensions.Where(dimension => (dimension.Name == "ShareName" || dimension.Name == "ShareSubscriptionName")))) { - measuredMetric.DimensionName = "share_name"; + dimension.Name = "share_name"; } return metricValues; diff --git a/src/Promitor.Core.Scraping/ResourceTypes/StorageQueueScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/StorageQueueScraper.cs index 81b3d2b53..eb3929b8a 100644 --- a/src/Promitor.Core.Scraping/ResourceTypes/StorageQueueScraper.cs +++ b/src/Promitor.Core.Scraping/ResourceTypes/StorageQueueScraper.cs @@ -51,7 +51,7 @@ protected override async Task ScrapeResourceAsync(string subscript var measuredMetrics = new List { - MeasuredMetric.CreateWithoutDimension(foundMetricValue) + MeasuredMetric.CreateWithoutDimensions(foundMetricValue) }; return new ScrapeResult(subscriptionId, scrapeDefinition.ResourceGroupName, resource.AccountName, resourceUri, measuredMetrics, labels); diff --git a/src/Promitor.Core.Scraping/Scraper.cs b/src/Promitor.Core.Scraping/Scraper.cs index 6a8ffb9d6..09be2c757 100644 --- a/src/Promitor.Core.Scraping/Scraper.cs +++ b/src/Promitor.Core.Scraping/Scraper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using GuardNet; using Microsoft.Azure.Management.Monitor.Fluent.Models; @@ -142,7 +143,7 @@ private void LogMeasuredMetrics(ScrapeDefinition scrap { if (measuredMetric.IsDimensional) { - Logger.LogInformation("Found value {MetricValue} for metric {MetricName} with dimension {DimensionValue} as part of {DimensionName} dimension with aggregation interval {AggregationInterval}", measuredMetric.Value, scrapeDefinition.PrometheusMetricDefinition.Name, measuredMetric.DimensionValue, measuredMetric.DimensionName, aggregationInterval); + Logger.LogInformation("Found value {MetricValue} for metric {MetricName} with dimension values {DimensionValues} as part of the dimensions {DimensionNames} with aggregation interval {AggregationInterval}", measuredMetric.Value, scrapeDefinition.PrometheusMetricDefinition.Name, measuredMetric.Dimensions.Select(dimension => dimension.Value).ToList(), measuredMetric.Dimensions.Select(dimension => dimension.Name).ToList(), aggregationInterval); } else { diff --git a/src/Promitor.Core/Metrics/MeasuredMetric.cs b/src/Promitor.Core/Metrics/MeasuredMetric.cs index 52ccc4a2b..21634f194 100644 --- a/src/Promitor.Core/Metrics/MeasuredMetric.cs +++ b/src/Promitor.Core/Metrics/MeasuredMetric.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using GuardNet; using Microsoft.Azure.Management.Monitor.Fluent.Models; @@ -13,14 +14,9 @@ public class MeasuredMetric public double? Value { get; } /// - /// Name of dimension for a metric + /// Measured dimensions. /// - public string DimensionName { get; set; } - - /// - /// Name of dimension for a metric - /// - public string DimensionValue { get; } + public List Dimensions { get; } /// /// Indication whether or not the metric represents a dimension @@ -32,55 +28,58 @@ private MeasuredMetric(double? value) Value = value; } - private MeasuredMetric(double? value, string dimensionName, string dimensionValue) + private MeasuredMetric(double? value, List dimensions) { - Guard.NotNullOrWhitespace(dimensionName, nameof(dimensionName)); - Guard.NotNullOrWhitespace(dimensionValue, nameof(dimensionValue)); + Guard.NotAny(dimensions, nameof(dimensions)); Value = value; IsDimensional = true; - DimensionName = dimensionName; - DimensionValue = dimensionValue; + Dimensions = dimensions; } /// - /// Create a measured metric without dimension + /// Create a measured metric without dimensions /// /// Measured metric value - public static MeasuredMetric CreateWithoutDimension(double? value) + public static MeasuredMetric CreateWithoutDimensions(double? value) { return new MeasuredMetric(value); } /// - /// Create a measured metric for a given dimension + /// Create a measured metric for given dimensions /// /// Measured metric value - /// Name of dimension that is being scraped + /// List of names of dimensions that are being scraped /// Timeseries representing one of the dimensions - public static MeasuredMetric CreateForDimension(double? value, string dimensionName, TimeSeriesElement timeseries) + public static MeasuredMetric CreateForDimensions(double? value, List dimensionNames, TimeSeriesElement timeseries) { - Guard.NotNullOrWhitespace(dimensionName, nameof(dimensionName)); + Guard.NotAny(dimensionNames, nameof(dimensionNames)); Guard.NotNull(timeseries, nameof(timeseries)); Guard.For(() => timeseries.Metadatavalues.Any() == false); - var dimensionValue = timeseries.Metadatavalues.Single(metadataValue => metadataValue.Name?.Value.Equals(dimensionName, StringComparison.InvariantCultureIgnoreCase) == true); - return CreateForDimension(value, dimensionName, dimensionValue.Value); + var dimensions = new List(); + foreach (var dimensionName in dimensionNames) + { + var dimensionValue = timeseries.Metadatavalues.Single(metadataValue => metadataValue.Name?.Value.Equals(dimensionName, StringComparison.InvariantCultureIgnoreCase) == true).Value; + dimensions.Add(new MeasuredMetricDimension(dimensionName, dimensionValue)); + } + + return new MeasuredMetric(value, dimensions); } /// - /// Create a measured metric for a given dimension + /// Create a measured metric for given dimensions when no metric information was found /// - /// Measured metric value - /// Name of dimension that is being scraped - /// Value of the dimension that is being scraped - public static MeasuredMetric CreateForDimension(double? value, string dimensionName, string dimensionValue) + /// List of names of dimensions that are being scraped + public static MeasuredMetric CreateForDimensions(List dimensionNames) { - Guard.NotNullOrWhitespace(dimensionName, nameof(dimensionName)); - Guard.NotNullOrWhitespace(dimensionValue, nameof(dimensionValue)); + Guard.NotAny(dimensionNames, nameof(dimensionNames)); + + var dimensions = dimensionNames.Select(name => new MeasuredMetricDimension(name, "unknown")).ToList(); - return new MeasuredMetric(value, dimensionName, dimensionValue); + return new MeasuredMetric(null, dimensions); } } } diff --git a/src/Promitor.Core/Metrics/MeasuredMetricDimension.cs b/src/Promitor.Core/Metrics/MeasuredMetricDimension.cs new file mode 100644 index 000000000..80ddcfb34 --- /dev/null +++ b/src/Promitor.Core/Metrics/MeasuredMetricDimension.cs @@ -0,0 +1,25 @@ +using GuardNet; + +namespace Promitor.Core.Metrics; + +public class MeasuredMetricDimension +{ + /// + /// Name of dimension + /// + public string Name { get; set; } + + /// + /// Value of dimension + /// + public string Value { get; } + + public MeasuredMetricDimension(string dimensionName, string dimensionValue) + { + Guard.NotNullOrWhitespace(dimensionName, nameof(dimensionName)); + Guard.NotNullOrWhitespace(dimensionValue, nameof(dimensionValue)); + + Value = dimensionValue; + Name = dimensionName; + } +} \ No newline at end of file diff --git a/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs b/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs index 74c80fe87..5dcd3672d 100644 --- a/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs +++ b/src/Promitor.Integrations.AzureMonitor/AzureMonitorClient.cs @@ -66,14 +66,14 @@ public AzureMonitorClient(AzureEnvironment azureCloud, string tenantId, string s /// Queries Azure Monitor to get the latest value for a specific metric /// /// Name of the metric - /// Name of dimension to split metric on + /// List of names of dimensions to split metric on /// Aggregation for the metric to use /// Interval that is used to aggregate metrics /// Id of the resource to query /// Optional filter to filter out metrics /// Limit of resources to query metrics for when using filtering /// Latest representation of the metric - public async Task> QueryMetricAsync(string metricName, string metricDimension, AggregationType aggregationType, TimeSpan aggregationInterval, + public async Task> QueryMetricAsync(string metricName, List metricDimensions, AggregationType aggregationType, TimeSpan aggregationInterval, string resourceId, string metricFilter = null, int? metricLimit = null) { Guard.NotNullOrWhitespace(metricName, nameof(metricName)); @@ -91,10 +91,10 @@ public async Task> QueryMetricAsync(string metricName, stri var closestAggregationInterval = DetermineAggregationInterval(metricName, aggregationInterval, metricDefinition.MetricAvailabilities); // Get the most recent metric - var relevantMetric = await GetRelevantMetric(metricName, aggregationType, closestAggregationInterval, metricFilter, metricDimension, metricDefinition, metricLimit, startQueryingTime); + var relevantMetric = await GetRelevantMetric(metricName, aggregationType, closestAggregationInterval, metricFilter, metricDimensions, metricDefinition, metricLimit, startQueryingTime); if (relevantMetric.Timeseries.Count < 1) { - throw new MetricInformationNotFoundException(metricName, "No time series was found", metricDimension); + throw new MetricInformationNotFoundException(metricName, "No time series was found", metricDimensions); } var measuredMetrics = new List(); @@ -109,7 +109,7 @@ public async Task> QueryMetricAsync(string metricName, stri // Get the metric value according to the requested aggregation type var requestedMetricAggregate = InterpretMetricValue(aggregationType, mostRecentMetricValue); - var measuredMetric = string.IsNullOrWhiteSpace(metricDimension) ? MeasuredMetric.CreateWithoutDimension(requestedMetricAggregate) : MeasuredMetric.CreateForDimension(requestedMetricAggregate, metricDimension, timeseries); + var measuredMetric = metricDimensions.Any() ? MeasuredMetric.CreateForDimensions(requestedMetricAggregate, metricDimensions, timeseries) : MeasuredMetric.CreateWithoutDimensions(requestedMetricAggregate); measuredMetrics.Add(measuredMetric); } @@ -171,9 +171,9 @@ private static TimeSpan GetClosestAggregationInterval(TimeSpan requestedAggregat } private async Task GetRelevantMetric(string metricName, AggregationType metricAggregation, TimeSpan metricInterval, - string metricFilter, string metricDimension, IMetricDefinition metricDefinition, int? metricLimit, DateTime recordDateTime) + string metricFilter, List metricDimensions, IMetricDefinition metricDefinition, int? metricLimit, DateTime recordDateTime) { - var metricQuery = CreateMetricsQuery(metricAggregation, metricInterval, metricFilter, metricDimension, metricLimit, metricDefinition, recordDateTime); + var metricQuery = CreateMetricsQuery(metricAggregation, metricInterval, metricFilter, metricDimensions, metricLimit, metricDefinition, recordDateTime); var metrics = await metricQuery.ExecuteAsync(); // We already filtered this out so only expect to have one @@ -220,7 +220,7 @@ private MetricValue GetMostRecentMetricValue(string metricName, TimeSeriesElemen } } - private IWithMetricsQueryExecute CreateMetricsQuery(AggregationType metricAggregation, TimeSpan metricsInterval, string metricFilter, string metricDimension, + private IWithMetricsQueryExecute CreateMetricsQuery(AggregationType metricAggregation, TimeSpan metricsInterval, string metricFilter, List metricDimensions, int? metricLimit, IMetricDefinition metricDefinition, DateTime recordDateTime) { var historyStartingFromInHours = _azureMonitorIntegrationConfiguration.Value.History.StartingFromInHours; @@ -238,9 +238,10 @@ private IWithMetricsQueryExecute CreateMetricsQuery(AggregationType metricAggreg metricQuery.SelectTop(queryLimit); } - if (string.IsNullOrWhiteSpace(metricDimension) == false) + if (metricDimensions.Any()) { - metricQuery.WithOdataFilter($"{metricDimension} eq '*'"); + string metricDimensionsFilter = string.Join(" and ", metricDimensions.Select(metricDimension => $"{metricDimension} eq '*'")); + metricQuery.WithOdataFilter(metricDimensionsFilter); metricQuery.SelectTop(queryLimit); } @@ -255,7 +256,7 @@ private IAzure CreateAzureClient(AzureEnvironment azureCloud, string tenantId, s var azureClientConfiguration = Microsoft.Azure.Management.Fluent.Azure.Configure() .WithDelegatingHandler(monitorHandler); - + var azureMonitorLogging = azureMonitorLoggingConfiguration.Value; if (azureMonitorLogging.IsEnabled) { diff --git a/src/Promitor.Integrations.AzureMonitor/Exceptions/MetricInformationNotFoundException.cs b/src/Promitor.Integrations.AzureMonitor/Exceptions/MetricInformationNotFoundException.cs index 246b3273f..3f4683f4b 100644 --- a/src/Promitor.Integrations.AzureMonitor/Exceptions/MetricInformationNotFoundException.cs +++ b/src/Promitor.Integrations.AzureMonitor/Exceptions/MetricInformationNotFoundException.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GuardNet; namespace Promitor.Integrations.AzureMonitor.Exceptions @@ -23,15 +24,15 @@ public class MetricInformationNotFoundException : Exception /// Constructor /// /// Name of the metric - /// Dimension of the metric /// Details that provide more context about the scenario - public MetricInformationNotFoundException(string name, string details, string dimension) : base($"No metric information was found for '{name}' with dimension '{dimension}'. Reason: '{details}'") + /// Dimensions of the metric + public MetricInformationNotFoundException(string name, string details, List dimensions) : base($"No metric information was found for '{name}' with dimensions '{dimensions}'. Reason: '{details}'") { Guard.NotNullOrWhitespace(name, nameof(name)); Guard.NotNullOrWhitespace(details, nameof(details)); Name = name; - Dimension = dimension; + Dimensions = dimensions; Details = details; } @@ -43,7 +44,7 @@ public class MetricInformationNotFoundException : Exception /// /// Dimension of the metric /// - public string Dimension { get; } + public List Dimensions { get; } /// /// Details that provide more context about the scenario diff --git a/src/Promitor.Integrations.Sinks.Core/MetricSink.cs b/src/Promitor.Integrations.Sinks.Core/MetricSink.cs index e200c2704..a86846bf4 100644 --- a/src/Promitor.Integrations.Sinks.Core/MetricSink.cs +++ b/src/Promitor.Integrations.Sinks.Core/MetricSink.cs @@ -36,7 +36,10 @@ public Dictionary DetermineLabels(string metricName, ScrapeResul if (measuredMetric.IsDimensional) { - labels.Add(DetermineLabelName(measuredMetric.DimensionName, mutateLabelName), measuredMetric.DimensionValue); + foreach (var dimension in measuredMetric.Dimensions) + { + labels.Add(DetermineLabelName(dimension.Name, mutateLabelName), dimension.Value); + } } if (metricDefinition?.Labels?.Any() == true) diff --git a/src/Promitor.Integrations.Sinks.Statsd/StatsdMetricSink.cs b/src/Promitor.Integrations.Sinks.Statsd/StatsdMetricSink.cs index 74ba52ed8..66f43fe83 100644 --- a/src/Promitor.Integrations.Sinks.Statsd/StatsdMetricSink.cs +++ b/src/Promitor.Integrations.Sinks.Statsd/StatsdMetricSink.cs @@ -79,7 +79,7 @@ private Task ReportMetricWithGenevaFormattingAsync(string metricName, string met Guard.NotNullOrEmpty(metricName, nameof(metricName)); Guard.NotNull(_statsDConfiguration.CurrentValue, nameof(_statsDConfiguration.CurrentValue)); Guard.NotNull(_statsDConfiguration.CurrentValue.Geneva, new ArgumentException ("Geneva formatter settings are required", nameof(_statsDConfiguration.CurrentValue.Geneva))); - + var bucket = JsonConvert.SerializeObject(new { _statsDConfiguration.CurrentValue.Geneva?.Account, diff --git a/src/Promitor.Tests.Integration/Services/Scraper/MetricSinks/PrometheusScrapeMetricSinkTests.cs b/src/Promitor.Tests.Integration/Services/Scraper/MetricSinks/PrometheusScrapeMetricSinkTests.cs index fc7f8dcb2..ad9a28f8e 100644 --- a/src/Promitor.Tests.Integration/Services/Scraper/MetricSinks/PrometheusScrapeMetricSinkTests.cs +++ b/src/Promitor.Tests.Integration/Services/Scraper/MetricSinks/PrometheusScrapeMetricSinkTests.cs @@ -1,4 +1,6 @@ -using System.Net; +using System; +using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using Promitor.Agents.Core; using Promitor.Tests.Integration.Data; @@ -91,5 +93,34 @@ public async Task Prometheus_Scrape_Get_ReturnsVersionHeader() Assert.True(response.Headers.Contains(HttpHeaders.AgentVersion)); Assert.Equal(ExpectedVersion, response.Headers.GetFirstOrDefaultHeaderValue(HttpHeaders.AgentVersion)); } + + [Theory] + [MemberData(nameof(DimensionsData))] + public async Task Prometheus_Scrape_ExpectedDimensionsAreAvailable(string expectedMetricName, IReadOnlyCollection expectedDimensionNames) + { + // Arrange + var prometheusClient = PrometheusClientFactory.CreateForPrometheusScrapingEndpointInScraperAgent(Configuration); + + // Act + var gaugeMetric = await prometheusClient.WaitForPrometheusMetricAsync(expectedMetricName); + + // Assert + Assert.NotNull(gaugeMetric); + Assert.Equal(expectedMetricName, gaugeMetric.Name); + Assert.NotNull(gaugeMetric.Measurements); + Assert.False(gaugeMetric.Measurements.Count < 1); + + foreach (var expectedDimensionName in expectedDimensionNames) + { + var sanitizedDimensionName = expectedDimensionName.SanitizeForPrometheusLabelKey(); + Assert.True(gaugeMetric.Measurements[0].Labels.ContainsKey(sanitizedDimensionName)); + Assert.NotEqual("unknown", gaugeMetric.Measurements[0].Labels[sanitizedDimensionName]); + } + } + + public static IEnumerable DimensionsData(){ + yield return new object[] { "application_insights_availability_per_name", new List{ "availabilityResult/name" } }; + yield return new object[] { "application_insights_availability_per_name_and_location", new List{ "availabilityResult/name", "availabilityResult/location" } }; + } } } diff --git a/src/Promitor.Tests.Integration/appsettings.json b/src/Promitor.Tests.Integration/appsettings.json index 43dd7fc3c..a33028a74 100644 --- a/src/Promitor.Tests.Integration/appsettings.json +++ b/src/Promitor.Tests.Integration/appsettings.json @@ -22,7 +22,7 @@ "OpenTelemetry": { "Collector": { "Uri": "#{OpenTelemetry.Collector.Uri}#", - "MetricNamespace": "otel" - } - } + "MetricNamespace": "otel" + } + } } \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs index ac2278ff7..9f821f1e3 100644 --- a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using AutoMapper; using Microsoft.Azure.Management.Monitor.Fluent.Models; using Microsoft.Extensions.Logging.Abstractions; @@ -334,7 +335,7 @@ public MetricsDeclarationBuilder WithDeviceProvisioningServiceMetric(string metr public MetricsDeclarationBuilder WithEventHubsMetric(string metricName = "promitor-event-hubs", string metricDescription = "Description for a metric", - string metricDimension = "", + IReadOnlyCollection metricDimensions = null, string topicName = "promitor-queue", string eventHubsNamespace = "promitor-namespace", string azureMetricName = "Total", @@ -348,7 +349,7 @@ public MetricsDeclarationBuilder WithEventHubsMetric(string metricName = "promit Namespace = eventHubsNamespace }; - CreateAndAddMetricDefinition(ResourceType.EventHubs, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, resource, metricDimension); + CreateAndAddMetricDefinition(ResourceType.EventHubs, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, resource, metricDimensions); return this; } @@ -525,7 +526,7 @@ public MetricsDeclarationBuilder WithLogAnalytics(string metricName = "promitor" string resourceDiscoveryGroupName = "", int? azureMetricLimit = null, bool omitResource = false, - string metricDimension = null, + IReadOnlyCollection metricDimensions = null, Dictionary labels = null, string query = "Usage | take 1 | extend result = Quantity | project result", string interval = "10:00:00:00") @@ -537,7 +538,7 @@ public MetricsDeclarationBuilder WithLogAnalytics(string metricName = "promitor" }; CreateAndAddMetricDefinition(ResourceType.LogAnalytics, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, - azureMetricName, azureMetricLimit, new List { resource }, metricDimension, labels, query, interval); + azureMetricName, azureMetricLimit, new List { resource }, metricDimensions, labels, query, interval); return this; } @@ -746,7 +747,7 @@ public MetricsDeclarationBuilder WithRedisEnterpriseCacheMetric(string metricNam public MetricsDeclarationBuilder WithServiceBusMetric(string metricName = "promitor-service-bus", string metricDescription = "Description for a metric", - string metricDimension = "", + IReadOnlyCollection metricDimensions = null, string queueName = "promitor-queue", string topicName = "", string serviceBusNamespace = "promitor-namespace", @@ -782,7 +783,7 @@ public MetricsDeclarationBuilder WithServiceBusMetric(string metricName = "promi serviceBusQueueResources.Add(resource); } - CreateAndAddMetricDefinition(ResourceType.ServiceBusNamespace, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, serviceBusQueueResources, metricDimension, labels); + CreateAndAddMetricDefinition(ResourceType.ServiceBusNamespace, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, serviceBusQueueResources, metricDimensions, labels); return this; } @@ -1065,16 +1066,17 @@ public MetricsDeclarationBuilder WithWebAppMetric(string metricName = "promitor- return this; } - private void CreateAndAddMetricDefinition(ResourceType resourceType, string metricName, string metricDescription, string resourceDiscoveryGroupName, bool omitResource, string azureMetricName, int? azureMetricLimit, AzureResourceDefinitionV1 resource, string metricDimension = null) + private void CreateAndAddMetricDefinition(ResourceType resourceType, string metricName, string metricDescription, string resourceDiscoveryGroupName, bool omitResource, string azureMetricName, int? azureMetricLimit, AzureResourceDefinitionV1 resource, IReadOnlyCollection metricDimensions = null) { - CreateAndAddMetricDefinition(resourceType, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, new List { resource }, metricDimension); + CreateAndAddMetricDefinition(resourceType, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, new List { resource }, metricDimensions); } private void CreateAndAddMetricDefinition(ResourceType resourceType, string metricName, string metricDescription, string resourceDiscoveryGroupName, bool omitResource, - string azureMetricName, int? azureMetricLimit, List resources, string metricDimension = null, Dictionary labels = null, string query = "", string interval = "10:00:00:00") + string azureMetricName, int? azureMetricLimit, List resources, IReadOnlyCollection metricDimensions = null, Dictionary labels = null, string query = "", string interval = "10:00:00:00") { - var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName, azureMetricLimit, metricDimension); + var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName, azureMetricLimit, metricDimensions); var logAnalyticsConfiguration = CreateLogAnalyticsConfiguration(query, interval); + var metric = new MetricDefinitionV1 { Name = metricName, @@ -1102,7 +1104,7 @@ private void CreateAndAddMetricDefinition(ResourceType resourceType, string metr _metrics.Add(metric); } - private AzureMetricConfigurationV1 CreateAzureMetricConfiguration(string azureMetricName, int? azureMetricLimit, string metricDimension = "") + private AzureMetricConfigurationV1 CreateAzureMetricConfiguration(string azureMetricName, int? azureMetricLimit, IReadOnlyCollection metricDimensions = null) { var metricConfig = new AzureMetricConfigurationV1 { @@ -1114,13 +1116,7 @@ private AzureMetricConfigurationV1 CreateAzureMetricConfiguration(string azureMe } }; - if (string.IsNullOrWhiteSpace(metricDimension) == false) - { - metricConfig.Dimension = new MetricDimensionV1 - { - Name = metricDimension - }; - } + metricConfig.Dimensions = metricDimensions != null ? metricDimensions.Select(name => new MetricDimensionV1{ Name = name }).ToList() : new List(); return metricConfig; } diff --git a/src/Promitor.Tests.Unit/Generators/ScrapeResultGenerator.cs b/src/Promitor.Tests.Unit/Generators/ScrapeResultGenerator.cs index 97b3f4aa4..9248c0723 100644 --- a/src/Promitor.Tests.Unit/Generators/ScrapeResultGenerator.cs +++ b/src/Promitor.Tests.Unit/Generators/ScrapeResultGenerator.cs @@ -21,7 +21,7 @@ public static ScrapeResult GenerateFromMetric(MeasuredMetric measuredMetric) public static ScrapeResult Generate(double metricValue) { - return GenerateFromMetric(MeasuredMetric.CreateWithoutDimension(metricValue)); + return GenerateFromMetric(MeasuredMetric.CreateWithoutDimensions(metricValue)); } } } \ 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 index 00f8e8490..35407f823 100644 --- a/src/Promitor.Tests.Unit/Metrics/Sinks/AtlassianStatuspageMetricSinkTests.cs +++ b/src/Promitor.Tests.Unit/Metrics/Sinks/AtlassianStatuspageMetricSinkTests.cs @@ -41,7 +41,7 @@ public async Task ReportMetricAsync_InputDoesNotContainMetricDescription_Succeed // Arrange var metricName = BogusGenerator.Name.FirstName(); var metricValue = BogusGenerator.Random.Double(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(); var atlassianStatuspageClientMock = new Mock(); @@ -78,7 +78,7 @@ public async Task ReportMetricAsync_GetsValidInputWithMetricValueAndPromitorToSy var metricDescription = BogusGenerator.Lorem.Sentence(); var metricValue = BogusGenerator.Random.Double(); var metricName = BogusGenerator.Name.FirstName(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(systemMetricId: systemMetricId, promitorMetricName: promitorMetricName); var atlassianStatuspageClientMock = new Mock(); @@ -103,7 +103,7 @@ public async Task ReportMetricAsync_GetsValidInputWithoutMetricValueButWithPromi var metricName = BogusGenerator.Name.FirstName(); double? metricValue = null; // ReSharper disable once ExpressionIsAlwaysNull - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(systemMetricId: systemMetricId, promitorMetricName: promitorMetricName); var atlassianStatuspageClientMock = new Mock(); @@ -128,7 +128,7 @@ public async Task ReportMetricAsync_GetsValidInputWithPromitorMetricThatIsNotMap var metricName = BogusGenerator.Name.FirstName(); double? metricValue = null; // ReSharper disable once ExpressionIsAlwaysNull - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var systemMetricConfigOptions = BogusAtlassianStatuspageMetricSinkConfigurationGenerator.GetSinkConfiguration(promitorMetricName: promitorMetricName); var atlassianStatuspageClientMock = new Mock(); diff --git a/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTest.cs b/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTest.cs index 876465429..ee621e100 100644 --- a/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTest.cs +++ b/src/Promitor.Tests.Unit/Metrics/Sinks/PrometheusScrapingEndpointMetricSinkTest.cs @@ -108,8 +108,8 @@ public async Task ReportMetricAsync_GetsValidInputWithTwoMetricValues_Successful var metricDescription = BogusGenerator.Lorem.Sentence(); var firstMetricValue = BogusGenerator.Random.Double(); var secondMetricValue = BogusGenerator.Random.Double(); - var firstMetric = MeasuredMetric.CreateWithoutDimension(firstMetricValue); - var secondMetric = MeasuredMetric.CreateWithoutDimension(secondMetricValue); + var firstMetric = MeasuredMetric.CreateWithoutDimensions(firstMetricValue); + var secondMetric = MeasuredMetric.CreateWithoutDimensions(secondMetricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(firstMetric); scrapeResult.MetricValues.Add(secondMetric); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); @@ -142,8 +142,8 @@ public async Task ReportMetricAsync_GetsValidInputWithTwoMetricValuesOfWhichOneI { Metadatavalues = new List { new(name: new LocalizableString(dimensionName), value: dimensionValue) } }; - var firstMetric = MeasuredMetric.CreateForDimension(firstMetricValue, dimensionName.ToUpper(), timeSeries); - var secondMetric = MeasuredMetric.CreateWithoutDimension(secondMetricValue); + var firstMetric = MeasuredMetric.CreateForDimensions(firstMetricValue, new List{ dimensionName.ToUpper() }, timeSeries); + var secondMetric = MeasuredMetric.CreateWithoutDimensions(secondMetricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(firstMetric); scrapeResult.MetricValues.Add(secondMetric); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); @@ -224,7 +224,7 @@ public async Task ReportMetricAsync_GetsValidInputWithoutMetricValue_Successfull var metricDescription = BogusGenerator.Lorem.Sentence(); double? metricValue = null; // ReSharper disable once ExpressionIsAlwaysNull - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); var prometheusConfiguration = CreatePrometheusConfiguration(metricUnavailableValue: expectedDefaultValue); @@ -254,7 +254,7 @@ public async Task ReportMetricAsync_GetsValidInputWithOneDimensions_Successfully { Metadatavalues = new List { new(name: new LocalizableString(dimensionName), value: dimensionValue) } }; - var measuredMetric = MeasuredMetric.CreateForDimension(metricValue, dimensionName.ToUpper(), timeSeries); + var measuredMetric = MeasuredMetric.CreateForDimensions(metricValue, new List{ dimensionName.ToUpper() }, timeSeries); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); var prometheusConfiguration = CreatePrometheusConfiguration(); diff --git a/src/Promitor.Tests.Unit/Metrics/Sinks/StatsDMetricSinkTests.cs b/src/Promitor.Tests.Unit/Metrics/Sinks/StatsDMetricSinkTests.cs index 1be427d4a..670fc3239 100644 --- a/src/Promitor.Tests.Unit/Metrics/Sinks/StatsDMetricSinkTests.cs +++ b/src/Promitor.Tests.Unit/Metrics/Sinks/StatsDMetricSinkTests.cs @@ -45,7 +45,7 @@ public async Task ReportMetricAsync_InputDoesNotContainMetricDescription_Succeed // Arrange var metricName = BogusGenerator.Name.FirstName(); var metricValue = BogusGenerator.Random.Double(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var statsDPublisherMock = new Mock(); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); @@ -80,7 +80,7 @@ public async Task ReportMetricAsync_GetsValidInputWithMetricValue_SuccessfullyWr var metricName = BogusGenerator.Name.FirstName(); var metricDescription = BogusGenerator.Lorem.Sentence(); var metricValue = BogusGenerator.Random.Double(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var statsDPublisherMock = new Mock(); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); @@ -103,7 +103,7 @@ public async Task ReportMetricAsync_GetsValidInputWithoutMetricValue_Successfull var metricDescription = BogusGenerator.Lorem.Sentence(); double? metricValue = null; // ReSharper disable once ExpressionIsAlwaysNull - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var statsDPublisherMock = new Mock(); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); @@ -145,7 +145,7 @@ public async Task ReportMetricAsync_UsesValidInputWithGenevaFormat_SuccessfullyW var metricValue = BogusGenerator.Random.Double(); var metricFormat = StatsdFormatterTypesEnum.Geneva; var genevaConfiguration = GenerateGenevaConfiguration(); - var measuredMetric = MeasuredMetric.CreateWithoutDimension(metricValue); + var measuredMetric = MeasuredMetric.CreateWithoutDimensions(metricValue); var scrapeResult = ScrapeResultGenerator.GenerateFromMetric(measuredMetric); var statsDPublisherMock = new Mock(); var metricsDeclarationProvider = CreateMetricsDeclarationProvider(metricName); diff --git a/src/Promitor.Tests.Unit/Promitor.Tests.Unit.csproj b/src/Promitor.Tests.Unit/Promitor.Tests.Unit.csproj index f11ca5d0d..91f186f8b 100644 --- a/src/Promitor.Tests.Unit/Promitor.Tests.Unit.csproj +++ b/src/Promitor.Tests.Unit/Promitor.Tests.Unit.csproj @@ -7,11 +7,11 @@ - 1701;1702;1591 + 1701;1702;1591;618 - 1701;1702;1591 + 1701;1702;1591;618 diff --git a/src/Promitor.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs b/src/Promitor.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs index 2bf6961be..14c3e9869 100644 --- a/src/Promitor.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs @@ -13,7 +13,7 @@ public class DeserializationTests : UnitTest private static readonly TimeSpan defaultInterval = TimeSpan.FromMinutes(5); private readonly Mock _errorReporter = new(); - private readonly Mock _childDeserializer = new(); + private readonly Mock> _childDeserializer = new(); private readonly RegistrationConfigDeserializer _deserializer; public DeserializationTests() @@ -209,7 +209,7 @@ public void Deserialize_RequiredChildObject_CanUseChildDeserializer() childProperty: 123"); var child = new ChildConfig(); _childDeserializer.Setup( - d => d.DeserializeObject((YamlMappingNode)node.Children["child"], _errorReporter.Object)).Returns(child); + d => d.Deserialize((YamlMappingNode)node.Children["child"], _errorReporter.Object)).Returns(child); // Act var result = _deserializer.Deserialize(node, _errorReporter.Object); @@ -227,7 +227,7 @@ public void Deserialize_OptionalChildObject_CanUseChildDeserializer() childProperty: 123"); var child = new ChildConfig(); _childDeserializer.Setup( - d => d.DeserializeObject((YamlMappingNode)node.Children["optionalChild"], _errorReporter.Object)).Returns(child); + d => d.Deserialize((YamlMappingNode)node.Children["optionalChild"], _errorReporter.Object)).Returns(child); // Act var result = _deserializer.Deserialize(node, _errorReporter.Object); @@ -259,7 +259,7 @@ public class ChildConfig private class RegistrationConfigDeserializer: Deserializer { - public RegistrationConfigDeserializer(IDeserializer childDeserializer) : base(NullLogger.Instance) + public RegistrationConfigDeserializer(IDeserializer childDeserializer) : base(NullLogger.Instance) { Map(t => t.Name).IsRequired(); Map(t => t.Age).IsRequired(); diff --git a/src/Promitor.Tests.Unit/Serialization/FieldDeserializationInfoBuilderTests/MapUsingDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/FieldDeserializationInfoBuilderTests/MapUsingDeserializerTests.cs index e53313807..a7ed0f377 100644 --- a/src/Promitor.Tests.Unit/Serialization/FieldDeserializationInfoBuilderTests/MapUsingDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/FieldDeserializationInfoBuilderTests/MapUsingDeserializerTests.cs @@ -17,7 +17,7 @@ public MapUsingDeserializerTests() public void MapUsingDeserializer_SetsDeserializer() { // Arrange - var deserializer = Mock.Of(); + var deserializer = Mock.Of>(); // Act _builder.MapUsingDeserializer(deserializer); @@ -31,7 +31,7 @@ public void MapUsingDeserializer_SetsDeserializer() public void MapUsingDeserializer_ReturnsBuilder() { // Arrange - var deserializer = Mock.Of(); + var deserializer = Mock.Of>(); // Act var result = _builder.MapUsingDeserializer(deserializer); diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs index 6c24f4e43..23bc8417c 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Promitor.Core.Scraping.Configuration.Serialization; @@ -88,7 +89,7 @@ public void Deserialize_AggregationSupplied_UsesDeserializer() var aggregation = new MetricAggregationV1(); _aggregationDeserializer.Setup( - d => d.DeserializeObject(aggregationNode, _errorReporter.Object)).Returns(aggregation); + d => d.Deserialize(aggregationNode, _errorReporter.Object)).Returns(aggregation); // Act var config = _deserializer.Deserialize(node, _errorReporter.Object); @@ -108,13 +109,62 @@ public void Deserialize_DimensionSupplied_UsesDeserializer() var dimensionNode = (YamlMappingNode)node.Children["dimension"]; var dimension = new MetricDimensionV1(); - _dimensionDeserializer.Setup(d => d.DeserializeObject(dimensionNode, _errorReporter.Object)).Returns(dimension); + _dimensionDeserializer.Setup(d => d.Deserialize(dimensionNode, _errorReporter.Object)).Returns(dimension); // Act var config = _deserializer.Deserialize(node, _errorReporter.Object); // Assert - Assert.Same(dimension, config.Dimension); + Assert.Equal(dimension, config.Dimension); + } + + [Fact] + public void Deserialize_DimensionsSupplied_UsesDeserializer() + { + // Arrange + const string yamlText = + @"dimensions: + - name: EntityPath + - name: Test"; + var node = YamlUtils.CreateYamlNode(yamlText); + var dimensionsNode = (YamlSequenceNode)node.Children["dimensions"]; + + var dimensions = new List { new(), new() }; + _dimensionDeserializer.Setup(d => d.Deserialize(dimensionsNode, _errorReporter.Object)).Returns(dimensions); + + // Act + var config = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Same(dimensions, config.Dimensions); + } + + [Fact] + public void Deserialize_DimensionAndDimensionsSupplied_UsesDeserializer() + { + // Arrange + const string yamlText = + @"dimension: + name: EntityPath +dimensions: + - name: EntityPath + - name: EntityName"; + var node = YamlUtils.CreateYamlNode(yamlText); + var dimensionNode = (YamlMappingNode)node.Children["dimension"]; + var dimensionsNode = (YamlSequenceNode)node.Children["dimensions"]; + + var dimension = new MetricDimensionV1(); + _dimensionDeserializer.Setup(d => d.Deserialize(dimensionNode, _errorReporter.Object)).Returns(dimension); + + var dimensions = new List { new(), new() }; + _dimensionDeserializer.Setup(d => d.Deserialize(dimensionsNode, _errorReporter.Object)).Returns(dimensions); + + // Act + var config = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(dimension, config.Dimension); + Assert.Same(dimensions, config.Dimensions); } [Fact] @@ -123,7 +173,7 @@ public void Deserialize_DimensionNotSupplied_Null() YamlAssert.PropertyNull( _deserializer, "metricName: ActiveMessages", - c => c.Dimension); + c => c.Dimensions); } [Fact] diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Core/LogAnalyticsConfigurationDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Core/LogAnalyticsConfigurationDeserializerTests.cs index e4ea58bea..a75f1b3e7 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Core/LogAnalyticsConfigurationDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Core/LogAnalyticsConfigurationDeserializerTests.cs @@ -51,7 +51,7 @@ public void Deserialize_AggregationSupplied_UsesDeserializer() var aggregation = new AggregationV1(); _aggregationDeserializer.Setup( - d => d.DeserializeObject(aggregationNode, _errorReporter.Object)).Returns(aggregation); + d => d.Deserialize(aggregationNode, _errorReporter.Object)).Returns(aggregation); // Act var config = _deserializer.Deserialize(node, _errorReporter.Object); diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs index b46fc1939..ff92bf820 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs @@ -38,7 +38,7 @@ public void Deserialize_AggregationPresent_UsesAggregationDeserializer() var aggregationNode = (YamlMappingNode)node.Children["aggregation"]; var aggregation = new AggregationV1(); - _aggregationDeserializer.Setup(d => d.DeserializeObject(aggregationNode, _errorReporter.Object)).Returns(aggregation); + _aggregationDeserializer.Setup(d => d.Deserialize(aggregationNode, _errorReporter.Object)).Returns(aggregation); // Act var defaults = _deserializer.Deserialize(node, _errorReporter.Object); @@ -76,7 +76,7 @@ public void Deserialize_ScrapingPresent_UsesScrapingDeserializer() var scrapingNode = (YamlMappingNode)node.Children["scraping"]; var scraping = new ScrapingV1(); - _scrapingDeserializer.Setup(d => d.DeserializeObject(scrapingNode, _errorReporter.Object)).Returns(scraping); + _scrapingDeserializer.Setup(d => d.Deserialize(scrapingNode, _errorReporter.Object)).Returns(scraping); // Act var defaults = _deserializer.Deserialize(node, _errorReporter.Object); diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs index 5daa5a13e..d7f81a650 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs @@ -175,7 +175,7 @@ public void Deserialize_AzureMetricConfigurationSupplied_UsesDeserializer() var configurationNode = (YamlMappingNode)node.Children["azureMetricConfiguration"]; var configuration = new AzureMetricConfigurationV1(); - _azureMetricConfigurationDeserializer.Setup(d => d.DeserializeObject(configurationNode, _errorReporter.Object)).Returns(configuration); + _azureMetricConfigurationDeserializer.Setup(d => d.Deserialize(configurationNode, _errorReporter.Object)).Returns(configuration); // Act var definition = _deserializer.Deserialize(node, _errorReporter.Object); @@ -269,7 +269,7 @@ public void Deserialize_ScrapingSupplied_UsesDeserializer() var scrapingNode = (YamlMappingNode)node.Children["scraping"]; var scraping = new ScrapingV1(); - _scrapingDeserializer.Setup(d => d.DeserializeObject(scrapingNode, _errorReporter.Object)).Returns(scraping); + _scrapingDeserializer.Setup(d => d.Deserialize(scrapingNode, _errorReporter.Object)).Returns(scraping); // Act var definition = _deserializer.Deserialize(node, _errorReporter.Object); diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs index b34233e2c..3eee06a70 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs @@ -89,7 +89,7 @@ public void Deserialize_AzureMetadata_UsesMetadataDeserializer() var yamlNode = YamlUtils.CreateYamlNode(config); var azureMetadata = new AzureMetadataV1(); _metadataDeserializer.Setup( - d => d.DeserializeObject(It.IsAny(), It.IsAny())).Returns(azureMetadata); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(azureMetadata); // Act var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); @@ -104,7 +104,7 @@ public void Deserialize_AzureMetadataNotSupplied_SetsMetadataNull() // Arrange var yamlNode = YamlUtils.CreateYamlNode("version: v1"); _metadataDeserializer.Setup( - d => d.DeserializeObject(It.IsAny(), It.IsAny())).Returns(new AzureMetadataV1()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new AzureMetadataV1()); // Act var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); @@ -125,7 +125,7 @@ public void Deserialize_MetricDefaults_UsesDefaultsDeserializer() var yamlNode = YamlUtils.CreateYamlNode(config); var metricDefaults = new MetricDefaultsV1(); _defaultsDeserializer.Setup( - d => d.DeserializeObject(It.IsAny(), It.IsAny())).Returns(metricDefaults); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(metricDefaults); // Act var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); @@ -142,7 +142,7 @@ public void Deserialize_MetricDefaultsNotSupplied_SetsDefaultsNull() @"version: v1"; var yamlNode = YamlUtils.CreateYamlNode(config); _defaultsDeserializer.Setup( - d => d.DeserializeObject(It.IsAny(), It.IsAny())).Returns(new MetricDefaultsV1()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new MetricDefaultsV1()); // Act var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs index c5b13676e..4dd63acea 100644 --- a/src/Promitor.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs +++ b/src/Promitor.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs @@ -98,7 +98,7 @@ public void Deserialize_SasTokenSupplied_UsesDeserializer() var sasTokenNode = (YamlMappingNode)node.Children["sasToken"]; var secret = new SecretV1(); - _secretDeserializer.Setup(d => d.DeserializeObject(sasTokenNode, _errorReporter.Object)).Returns(secret); + _secretDeserializer.Setup(d => d.Deserialize(sasTokenNode, _errorReporter.Object)).Returns(secret); // Act var resource = _deserializer.Deserialize(node, _errorReporter.Object); diff --git a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/AzureMetricConfigurationValidatorTest.cs b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/AzureMetricConfigurationValidatorTest.cs new file mode 100644 index 000000000..b785ba8ef --- /dev/null +++ b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/AzureMetricConfigurationValidatorTest.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using Promitor.Agents.Scraper.Validation.MetricDefinitions; +using Promitor.Core.Scraping.Configuration.Model; +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Xunit; +using MetricDimension = Promitor.Core.Scraping.Configuration.Model.MetricDimension; + +namespace Promitor.Tests.Unit.Validation.Scraper.Metrics; + +public class AzureMetricConfigurationValidatorTest +{ + [Fact] + public void DimensionsAndDimension_Fails() + { + // Arrange + var metricConfig = new AzureMetricConfiguration + { + MetricName = "testMetric", + Dimension = new MetricDimension { Name = "testDimension1" }, + Dimensions = new List { new() {Name = "testDimension2"}, new() { Name = "testDimension3" } } + }; + + var metricDefinition = new MetricDefinition + { + AzureMetricConfiguration = metricConfig + }; + + // Act + var azureMetricConfigurationValidator = new AzureMetricConfigurationValidator( new MetricDefaults()); + var validationErrors = azureMetricConfigurationValidator.Validate(metricDefinition); + + // Assert + Assert.NotEmpty(validationErrors); + } + + [Fact] + public void OnlyDimension_Succeeds() + { + // Arrange + var metricConfig = new AzureMetricConfiguration + { + MetricName = "testMetric", + Dimension = new MetricDimension { Name = "testDimension1" }, + Dimensions = new List() + }; + + var metricDefinition = new MetricDefinition + { + AzureMetricConfiguration = metricConfig + }; + + // Act + var azureMetricConfigurationValidator = new AzureMetricConfigurationValidator( new MetricDefaults()); + var validationErrors = azureMetricConfigurationValidator.Validate(metricDefinition); + + // Assert + Assert.Empty(validationErrors); + } + + [Fact] + public void OnlyDimensions_Succeeds() + { + // Arrange + var metricConfig = new AzureMetricConfiguration + { + MetricName = "testMetric", + Dimensions = new List { new() {Name = "testDimension1"}, new() { Name = "testDimension2" } } + }; + + var metricDefinition = new MetricDefinition + { + AzureMetricConfiguration = metricConfig + }; + + // Act + var azureMetricConfigurationValidator = new AzureMetricConfigurationValidator( new MetricDefaults()); + var validationErrors = azureMetricConfigurationValidator.Validate(metricDefinition); + + // Assert + Assert.Empty(validationErrors); + } +} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/EventHubsMetricsDeclarationValidationStepTests.cs b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/EventHubsMetricsDeclarationValidationStepTests.cs index 40710ec81..0d0480aab 100644 --- a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/EventHubsMetricsDeclarationValidationStepTests.cs +++ b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/EventHubsMetricsDeclarationValidationStepTests.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using Microsoft.Extensions.Logging.Abstractions; using Promitor.Agents.Scraper.Validation.Steps; using Promitor.Tests.Unit.Builders.Metrics.v1; @@ -52,7 +53,7 @@ public void EventHubsMetricsDeclaration_UseEntityNameDimensionWithoutTopicName_S { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithEventHubsMetric(metricDimension: "EntityName", topicName: string.Empty) + .WithEventHubsMetric(metricDimensions: new List{"EntityName"}, topicName: string.Empty) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); @@ -69,7 +70,7 @@ public void EventHubsMetricsDeclaration_UseEntityNameDimensionWithTopicName_Fail { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithEventHubsMetric(metricDimension: "EntityName") + .WithEventHubsMetric(metricDimensions: new List{"EntityName"}) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); @@ -86,7 +87,7 @@ public void EventHubsMetricsDeclaration_UseAllowedDimension_Succeeded() { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithEventHubsMetric(metricDimension: "OperationResult") + .WithEventHubsMetric(metricDimensions: new List{"OperationResult"}) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); diff --git a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/ServiceBusNamespaceMetricsDeclarationValidationStepTests.cs b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/ServiceBusNamespaceMetricsDeclarationValidationStepTests.cs index aa7caacc4..674745c55 100644 --- a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/ServiceBusNamespaceMetricsDeclarationValidationStepTests.cs +++ b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/ServiceBusNamespaceMetricsDeclarationValidationStepTests.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using Microsoft.Extensions.Logging.Abstractions; using Promitor.Agents.Scraper.Validation.Steps; using Promitor.Tests.Unit.Builders.Metrics.v1; @@ -52,7 +53,7 @@ public void ServiceBusNamespaceMetricsDeclaration_UseEntityNameDimensionAndQueue { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithServiceBusMetric(metricDimension: "EntityName") + .WithServiceBusMetric(metricDimensions: new List{"EntityName"}) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); @@ -69,7 +70,7 @@ public void ServiceBusNamespaceMetricsDeclaration_UseEntityNameDimensionAndTopic { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithServiceBusMetric(queueName: string.Empty, topicName: "test-topic", metricDimension: "EntityName") + .WithServiceBusMetric(queueName: string.Empty, topicName: "test-topic", metricDimensions: new List{"EntityName"}) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); @@ -86,7 +87,7 @@ public void ServiceBusNamespaceMetricsDeclaration_UseAllowedDimension_Succeeded( { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithServiceBusMetric(metricDimension: "OperationResult") + .WithServiceBusMetric(metricDimensions: new List{"OperationResult"}) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); @@ -103,7 +104,7 @@ public void ServiceBusNamespaceMetricsDeclaration_UseEntityNameDimension_Succeed { // Arrange var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() - .WithServiceBusMetric(metricDimension: "EntityName", queueName: string.Empty) + .WithServiceBusMetric(metricDimensions: new List{"EntityName"}, queueName: string.Empty) .Build(Mapper); var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper);