diff --git a/changelog/content/experimental/unreleased.md b/changelog/content/experimental/unreleased.md index c50da55dc..ce872da98 100644 --- a/changelog/content/experimental/unreleased.md +++ b/changelog/content/experimental/unreleased.md @@ -10,3 +10,4 @@ version: v1.6.0 - {{% tag added %}} Support for Kubernetes RBAC in Helm chart ([Helm Hub](https://hub.helm.sh/charts/promitor/promitor-agent-scraper) | [#951](https://github.com/tomkerkhove/promitor/issues/951)) - {{% tag added %}} Support for configuring Pod Security Policy in Helm Chart ([Helm Hub](https://hub.helm.sh/charts/promitor/promitor-agent-scraper) | [#952](https://github.com/tomkerkhove/promitor/issues/952)) - {{% tag added %}} Support for scraping Azure IoT Hub metrics ([docs](https://promitor.io/configuration/v1.x/metrics/iot-hub) | [#372](https://github.com/tomkerkhove/promitor/issues/372)) +- {{% tag added %}} Support for scraping Azure IoT Hub Device Provisioning Service (DPS) metrics ([docs](https://promitor.io/configuration/v1.x/metrics/iot-hub-device-provisioning-service) | [#1014](https://github.com/tomkerkhove/promitor/issues/1014)) diff --git a/docs/configuration/v1.x/metrics/index.md b/docs/configuration/v1.x/metrics/index.md index ccc3ed05a..7d9ef63b7 100644 --- a/docs/configuration/v1.x/metrics/index.md +++ b/docs/configuration/v1.x/metrics/index.md @@ -129,6 +129,7 @@ We also provide a simplified way to scrape the following Azure resources: - [Azure Database for PostgreSQL](postgresql) - [Azure Function App](function-app) - [Azure IoT Hub](iot-hub) +- [Azure IoT Hub Device Provisioning Service (DPS)](iot-hub-device-provisioning-service) - [Azure Network Interface](network-interface) - [Azure Service Bus Queue](service-bus-queue) - [Azure SQL Database](sql-database) diff --git a/docs/configuration/v1.x/metrics/iot-hub-device-provisioning-service.md b/docs/configuration/v1.x/metrics/iot-hub-device-provisioning-service.md new file mode 100644 index 000000000..32f1c260d --- /dev/null +++ b/docs/configuration/v1.x/metrics/iot-hub-device-provisioning-service.md @@ -0,0 +1,35 @@ +--- +layout: default +title: Azure IoT Hub Device Provisioning Service (DPS) Declaration +--- + +## Azure IoT Hub Device Provisioning Service (DPS) - ![Availability Badge](https://img.shields.io/badge/Available%20Starting-v1.6-green.svg) + +You can declare to scrape an Azure IoT Hub Device Provisioning Service (DPS) +via the `DeviceProvisioningService` resource type. + +The following fields need to be provided: + +- `deviceProvisioningServiceName` - The name of the Azure IoT Hub Device Provisioning Service (DPS) + +All supported metrics are documented in the official [Azure Monitor documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported#microsoftdevicesprovisioningservices). + +Example: + +```yaml +name: azure_dps_attestation_attempts +description: "The number of device attestations attempted" +resourceType: DeviceProvisioningService +azureMetricConfiguration: + metricName: AttestationAttempts + aggregation: + type: Total +resources: +- deviceProvisioningServiceName: promitor-1 +- deviceProvisioningServiceName: promitor-2 +``` + + +[← back to metrics declarations](/configuration/v1.x/metrics)
+[← back to introduction](/) + diff --git a/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs b/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs index 700397e12..ae9e81752 100644 --- a/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs +++ b/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs @@ -55,6 +55,8 @@ internal static IMetricValidator GetValidatorFor(ResourceType resourceType) return new SqlServerMetricValidator(); case ResourceType.IoTHub: return new IoTHubMetricValidator(); + case ResourceType.DeviceProvisioningService: + return new DeviceProvisioningServiceMetricValidator(); } throw new ArgumentOutOfRangeException(nameof(resourceType), $"No validation rules are defined for metric type '{resourceType}'"); diff --git a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/DeviceProvisioningServiceMetricValidator.cs b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/DeviceProvisioningServiceMetricValidator.cs new file mode 100644 index 000000000..7d8980122 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/DeviceProvisioningServiceMetricValidator.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using GuardNet; +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using Promitor.Agents.Scraper.Validation.MetricDefinitions.Interfaces; + +namespace Promitor.Agents.Scraper.Validation.MetricDefinitions.ResourceTypes +{ + internal class DeviceProvisioningServiceMetricValidator : IMetricValidator + { + public IEnumerable Validate(MetricDefinition metricDefinition) + { + Guard.NotNull(metricDefinition, nameof(metricDefinition)); + + foreach (var resourceDefinition in metricDefinition.Resources.Cast()) + { + if (string.IsNullOrWhiteSpace(resourceDefinition.DeviceProvisioningServiceName)) + { + yield return "No Azure IoT Hub Device Provisioning Service (DPS) name is configured"; + } + } + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/DeviceProvisioningServiceResourceDefinition.cs b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/DeviceProvisioningServiceResourceDefinition.cs new file mode 100644 index 000000000..0ac7d80d9 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/DeviceProvisioningServiceResourceDefinition.cs @@ -0,0 +1,16 @@ +namespace Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes +{ + public class DeviceProvisioningServiceResourceDefinition : AzureResourceDefinition + { + public DeviceProvisioningServiceResourceDefinition(string subscriptionId, string resourceGroupName, string deviceProvisioningServiceName) + : base(ResourceType.DeviceProvisioningService, subscriptionId, resourceGroupName) + { + DeviceProvisioningServiceName = deviceProvisioningServiceName; + } + + public string DeviceProvisioningServiceName { get; } + + /// + public override string GetResourceName() => DeviceProvisioningServiceName; + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs index cd6baa702..c0f06a898 100644 --- a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs +++ b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs @@ -25,5 +25,6 @@ public enum ResourceType StorageAccount = 20, ApiManagement = 21, IoTHub = 22, + DeviceProvisioningService = 23 } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs index 2d421aa7c..076271d33 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs @@ -87,6 +87,9 @@ public IDeserializer GetDeserializerFor(ResourceType case ResourceType.IoTHub: var iotHubLogger = _loggerFactory.CreateLogger(); return new IoTHubDeserializer(iotHubLogger); + case ResourceType.DeviceProvisioningService: + var deviceProvisioningServiceLogger = _loggerFactory.CreateLogger(); + return new DeviceProvisioningServiceDeserializer(deviceProvisioningServiceLogger); default: throw new ArgumentOutOfRangeException($"Resource Type {resourceType} not supported."); } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs index 1982a0061..0ab39fddd 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs @@ -44,6 +44,7 @@ public V1MappingProfile() CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); @@ -72,7 +73,8 @@ public V1MappingProfile() .Include() .Include() .Include() - .Include(); + .Include() + .Include(); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/DeviceProvisioningServiceResourceV1.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/DeviceProvisioningServiceResourceV1.cs new file mode 100644 index 000000000..b9f169bd7 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/DeviceProvisioningServiceResourceV1.cs @@ -0,0 +1,13 @@ +namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes +{ + /// + /// Contains the configuration required to scrape an Azure IoT Hub Device Provisioning Service (DPS). + /// + public class DeviceProvisioningServiceResourceV1 : AzureResourceDefinitionV1 + { + /// + /// The name of the Azure IoT Hub Device Provisioning Service (DPS) to get metrics for. + /// + public string DeviceProvisioningServiceName { get; set; } + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/DeviceProvisioningServiceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/DeviceProvisioningServiceDeserializer.cs new file mode 100644 index 000000000..e84de8861 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/DeviceProvisioningServiceDeserializer.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; + +namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers +{ + public class DeviceProvisioningServiceDeserializer : ResourceDeserializer + { + public DeviceProvisioningServiceDeserializer(ILogger logger) : base(logger) + { + MapRequired(resource => resource.DeviceProvisioningServiceName); + } + } +} diff --git a/src/Promitor.Core.Scraping/DeviceProvisioningServiceScraper.cs b/src/Promitor.Core.Scraping/DeviceProvisioningServiceScraper.cs new file mode 100644 index 000000000..dfdea4b27 --- /dev/null +++ b/src/Promitor.Core.Scraping/DeviceProvisioningServiceScraper.cs @@ -0,0 +1,26 @@ +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; + +// ReSharper disable All + +namespace Promitor.Core.Scraping +{ + /// + /// Scrapes an Azure IoT Hub Device Provisioning Service (DPS) + /// + public class DeviceProvisioningServiceScraper : AzureMonitorScraper + { + private const string ResourceUriTemplate = "subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Devices/provisioningServices/{2}"; + + public DeviceProvisioningServiceScraper(ScraperConfiguration scraperConfiguration) : + base(scraperConfiguration) + { + } + + /// + protected override string BuildResourceUri(string subscriptionId, ScrapeDefinition scrapeDefinition, DeviceProvisioningServiceResourceDefinition resource) + { + return string.Format(ResourceUriTemplate, subscriptionId, scrapeDefinition.ResourceGroupName, resource.DeviceProvisioningServiceName); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs index e8e07b3e6..57068f948 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -79,6 +79,8 @@ public IScraper CreateScraper(ResourceType metricDefin return new FileStorageScraper(scraperConfiguration); case ResourceType.IoTHub: return new IoTHubScraper(scraperConfiguration); + case ResourceType.DeviceProvisioningService: + return new DeviceProvisioningServiceScraper(scraperConfiguration); default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs index 97d891bd7..21cc1af85 100644 --- a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs @@ -603,5 +603,27 @@ public MetricsDeclarationBuilder WithSqlManagedInstanceMetric( return this; } + + public MetricsDeclarationBuilder WithDeviceProvisioningServiceMetric(string metricName = "promitor-dps", string metricDescription = "Description for a metric", string deviceProvisioningServiceName = "promitor-dps", string azureMetricName = "AttestationAttempts") + { + var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); + var resource = new DeviceProvisioningServiceResourceV1 + { + DeviceProvisioningServiceName = deviceProvisioningServiceName + }; + + var metric = new MetricDefinitionV1 + { + Name = metricName, + Description = metricDescription, + AzureMetricConfiguration = azureMetricConfiguration, + Resources = new List { resource }, + ResourceType = ResourceType.DeviceProvisioningService + }; + + _metrics.Add(metric); + + return this; + } } } \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Providers/DeviceProvisioningServiceDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Providers/DeviceProvisioningServiceDeserializerTests.cs new file mode 100644 index 000000000..db77e6c56 --- /dev/null +++ b/src/Promitor.Tests.Unit/Serialization/v1/Providers/DeviceProvisioningServiceDeserializerTests.cs @@ -0,0 +1,45 @@ +using Promitor.Core.Scraping.Configuration.Serialization; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Providers; +using System.ComponentModel; +using Xunit; + +namespace Promitor.Tests.Unit.Serialization.v1.Providers +{ + [Category("Unit")] + public class DeviceProvisioningServiceDeserializerTests : ResourceDeserializerTest + { + private readonly DeviceProvisioningServiceDeserializer _deserializer; + + public DeviceProvisioningServiceDeserializerTests() + { + _deserializer = new DeviceProvisioningServiceDeserializer(Logger); + } + + [Fact] + public void Deserialize_DeviceProvisioningServiceNameSupplied_SetsDeviceProvisioningServiceName() + { + const string deviceProvisioningServiceName = "promitor-dps"; + YamlAssert.PropertySet( + _deserializer, + $"deviceProvisioningServiceName: {deviceProvisioningServiceName}", + deviceProvisioningServiceName, + r => r.DeviceProvisioningServiceName); + } + + [Fact] + public void Deserialize_DeviceProvisioningServiceNameNotSupplied_Null() + { + YamlAssert.PropertyNull( + _deserializer, + "resourceGroupName: promitor-group", + r => r.DeviceProvisioningServiceName); + } + + protected override IDeserializer CreateDeserializer() + { + return new DeviceProvisioningServiceDeserializer(Logger); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Tests.Unit/Validation/Metrics/ResourceTypes/DeviceProvisioningServiceMetricsDeclarationValidationStepsTests.cs b/src/Promitor.Tests.Unit/Validation/Metrics/ResourceTypes/DeviceProvisioningServiceMetricsDeclarationValidationStepsTests.cs new file mode 100644 index 000000000..57eff0d94 --- /dev/null +++ b/src/Promitor.Tests.Unit/Validation/Metrics/ResourceTypes/DeviceProvisioningServiceMetricsDeclarationValidationStepsTests.cs @@ -0,0 +1,97 @@ +using System.ComponentModel; +using Promitor.Agents.Scraper.Validation.Steps; +using Promitor.Tests.Unit.Builders.Metrics.v1; +using Promitor.Tests.Unit.Stubs; +using Xunit; + +namespace Promitor.Tests.Unit.Validation.Metrics.ResourceTypes +{ + [Category("Unit")] + public class DeviceProvisioningServiceMetricsDeclarationValidationStepsTests : MetricsDeclarationValidationStepsTests + { + [Fact] + public void DeviceProvisioningServiceMetricsDeclaration_DeclarationWithoutAzureMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithDeviceProvisioningServiceMetric(azureMetricName: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void DeviceProvisioningServiceMetricsDeclaration_DeclarationWithoutMetricDescription_Succeeded() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithDeviceProvisioningServiceMetric(metricDescription: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful, "Validation was not successful"); + } + + [Fact] + public void DeviceProvisioningServiceMetricsDeclaration_DeclarationWithoutMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithDeviceProvisioningServiceMetric(string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void DeviceProvisioningServiceMetricsDeclaration_DeclarationWithoutDeviceProvisioningServiceName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithDeviceProvisioningServiceMetric(deviceProvisioningServiceName: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void DeviceProvisioningServiceMetricsDeclaration_ValidDeclaration_Succeeds() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithDeviceProvisioningServiceMetric() + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawMetricsDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful, "Validation was not successful"); + } + } +}