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