From 0a40a13aebef0051fe69d3cb5b3ed4d0324ca5d3 Mon Sep 17 00:00:00 2001 From: Richard Guthrie Date: Wed, 27 Mar 2019 13:19:29 -0700 Subject: [PATCH] Network interface metrics (#466) * Adjust .gitignore to exclude .DS_Store * Add config doc for network interfaces. * Add Network Interface Resource Definition * Add Network interface deserializer * Adjust index * Add Network Interface to Deserializer Factory * Add Network Interface Scraper support * Add validator. * Bugfix to Network interface scraper. * Add unit test for Network Interface. * Fix doc typo * Add link to Microsoft docs for azure network interface. * I hate you git --- .gitignore | 6 +- docs/configuration/metrics/index.md | 3 +- .../metrics/network-interfaces.md | 27 ++++++ .../NetworkInterfaceMetricDefinition.cs | 8 ++ .../Configuration/Model/ResourceType.cs | 3 +- .../NetworkInterfaceMetricDeserializer.cs | 22 +++++ .../Factories/MetricDeserializerFactory.cs | 2 + .../Factories/MetricScraperFactory.cs | 2 + .../ResourceTypes/NetworkInterfaceScraper.cs | 31 ++++++ .../Factories/MetricValidatorFactory.cs | 2 + .../NetworkInterfaceMetricValidator.cs | 23 +++++ .../Builders/MetricsDeclarationBuilder.cs | 16 +++ ...hNetworkInterfaceYamlSerializationTests.cs | 77 +++++++++++++++ ...eMetricsDeclarationValidationStepsTests.cs | 97 +++++++++++++++++++ 14 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 docs/configuration/metrics/network-interfaces.md create mode 100644 src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/NetworkInterfaceMetricDefinition.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/NetworkInterfaceMetricDeserializer.cs create mode 100644 src/Promitor.Core.Scraping/ResourceTypes/NetworkInterfaceScraper.cs create mode 100644 src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/NetworkInterfaceMetricValidator.cs create mode 100644 src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithNetworkInterfaceYamlSerializationTests.cs create mode 100644 src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/NetworkInterfaceMetricsDeclarationValidationStepsTests.cs diff --git a/.gitignore b/.gitignore index 605c6d654..e5d5bccaa 100644 --- a/.gitignore +++ b/.gitignore @@ -293,4 +293,8 @@ __pycache__/ # Local Jekyll output _site/* docs/_site/* -*.orig + +#MAC +.DS_Store + +*.orig \ No newline at end of file diff --git a/docs/configuration/metrics/index.md b/docs/configuration/metrics/index.md index d9b53f14f..486e08304 100644 --- a/docs/configuration/metrics/index.md +++ b/docs/configuration/metrics/index.md @@ -16,7 +16,7 @@ azureMetadata: metricDefaults: aggregation: interval: 00:05:00 -metrics: +metrics: - name: demo_queue_size description: "Amount of active messages of the 'myqueue' queue" resourceType: ServiceBusQueue @@ -74,6 +74,7 @@ We also provide a simplified way to configure the following Azure resources: - [Azure Container Registry](container-registry) - [Azure Service Bus Queue](service-bus-queue) - [Azure Virtual Machine](virtual-machine) +- [Azure Network Interface](network-interface) - [Azure Storage Queue](storage-queue) Want to help out? Create an issue and [contribute a new scraper](https://github.com/tomkerkhove/promitor/blob/master/adding-a-new-scraper.md). diff --git a/docs/configuration/metrics/network-interfaces.md b/docs/configuration/metrics/network-interfaces.md new file mode 100644 index 000000000..ad6b00e3c --- /dev/null +++ b/docs/configuration/metrics/network-interfaces.md @@ -0,0 +1,27 @@ +--- +layout: default +title: Azure Network Interface Declaration +--- + +## Azure Network Interface +You can declare to scrape an [Azure Network Interface](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-network-interface) via the `NetworkInterface` resource type. + +The following fields need to be provided: +- `networkInterfaceName` - The name of the network interface + +All supported metrics are documented in the official [Azure Monitor documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported#microsoftnetworknetworkinterfaces). + +Example: +```yaml + - name: demo_azuresnetworkinterface_bytesreceivedrate + description: "Number of bytes the Network Interface sent" + resourceType: NetworkInterface + networkInterfaceName: promitor-network-interface + azureMetricConfiguration: + metricName: BytesReceivedRate + aggregation: + type: Average +``` + +[← back to metrics declarations](/configuration/metrics)
+[← back to introduction](/) \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/NetworkInterfaceMetricDefinition.cs b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/NetworkInterfaceMetricDefinition.cs new file mode 100644 index 000000000..163b272b0 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/NetworkInterfaceMetricDefinition.cs @@ -0,0 +1,8 @@ +namespace Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes +{ + public class NetworkInterfaceMetricDefinition : MetricDefinition + { + public string NetworkInterfaceName { get; set; } + public override ResourceType ResourceType { get; } = ResourceType.NetworkInterface; + } +} \ 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 8405684ab..d4d0a8010 100644 --- a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs +++ b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs @@ -8,6 +8,7 @@ public enum ResourceType StorageQueue = 3, ContainerInstance = 4, VirtualMachine = 5, - ContainerRegistry = 6 + ContainerRegistry = 6, + NetworkInterface = 7, } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/NetworkInterfaceMetricDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/NetworkInterfaceMetricDeserializer.cs new file mode 100644 index 000000000..b854756a0 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/NetworkInterfaceMetricDeserializer.cs @@ -0,0 +1,22 @@ +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Core.Scraping.Configuration.Serialization.Deserializers +{ + internal class NetworkInterfaceMetricDeserializer : GenericAzureMetricDeserializer + { + /// Deserializes the specified Network Interface metric node from the YAML configuration file. + /// The metric node containing 'networkInterfaceName' parameter pointing to an instance of a Network Interface + /// A new object (strongly typed as a ) + internal override MetricDefinition Deserialize(YamlMappingNode metricNode) + { + var metricDefinition = base.DeserializeMetricDefinition(metricNode); + var networkInterfaceName = metricNode.Children[new YamlScalarNode("networkInterfaceName")]; + + metricDefinition.NetworkInterfaceName = networkInterfaceName?.ToString(); + + return metricDefinition; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs index 1102ba66c..301c83735 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs @@ -21,6 +21,8 @@ internal static GenericAzureMetricDeserializer GetDeserializerFor(Configuration. return new VirtualMachineMetricDeserializer(); case Configuration.Model.ResourceType.ContainerRegistry: return new ContainerRegistryMetricDeserializer(); + case Configuration.Model.ResourceType.NetworkInterface: + return new NetworkInterfaceMetricDeserializer(); } throw new ArgumentOutOfRangeException($@"Resource Type {resource} not supported."); diff --git a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs index 34438a7d8..e8e5220a9 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -37,6 +37,8 @@ public static IScraper CreateScraper(ResourceType metricDefini return new ContainerInstanceScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); case ResourceType.VirtualMachine: return new VirtualMachineScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); + case ResourceType.NetworkInterface: + return new NetworkInterfaceScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); case ResourceType.ContainerRegistry: return new ContainerRegistryScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); default: diff --git a/src/Promitor.Core.Scraping/ResourceTypes/NetworkInterfaceScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/NetworkInterfaceScraper.cs new file mode 100644 index 000000000..139278285 --- /dev/null +++ b/src/Promitor.Core.Scraping/ResourceTypes/NetworkInterfaceScraper.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Azure.Management.Monitor.Fluent.Models; +using Microsoft.Extensions.Logging; +using Promitor.Core.Scraping.Configuration.Model; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using Promitor.Core.Telemetry.Interfaces; +using Promitor.Integrations.AzureMonitor; + +namespace Promitor.Core.Scraping.ResourceTypes +{ + internal class NetworkInterfaceScraper : Scraper + { + private const string ResourceUriTemplate = "subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Network/networkInterfaces/{2}"; + + public NetworkInterfaceScraper(AzureMetadata azureMetadata, MetricDefaults metricDefaults, AzureMonitorClient azureMonitorClient, ILogger logger, IExceptionTracker exceptionTracker) + : base(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker) + { + } + + protected override async Task ScrapeResourceAsync(string subscriptionId, string resourceGroupName, NetworkInterfaceMetricDefinition metricDefinition, AggregationType aggregationType, TimeSpan aggregationInterval) + { + var resourceUri = string.Format(ResourceUriTemplate, AzureMetadata.SubscriptionId, AzureMetadata.ResourceGroupName, metricDefinition.NetworkInterfaceName); + + var metricName = metricDefinition.AzureMetricConfiguration.MetricName; + var foundMetricValue = await AzureMonitorClient.QueryMetricAsync(metricName, aggregationType, aggregationInterval, resourceUri); + + return foundMetricValue; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs b/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs index 6b640d189..195d9e8f1 100644 --- a/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs +++ b/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs @@ -21,6 +21,8 @@ internal static IMetricValidator GetValidatorFor(ResourceType resourceType) return new ContainerInstanceMetricValidator(); case ResourceType.VirtualMachine: return new VirtualMachineMetricValidator(); + case ResourceType.NetworkInterface: + return new NetworkInterfaceMetricValidator(); case ResourceType.ContainerRegistry: return new ContainerRegistryMetricValidator(); } diff --git a/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/NetworkInterfaceMetricValidator.cs b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/NetworkInterfaceMetricValidator.cs new file mode 100644 index 000000000..a18d1dbf7 --- /dev/null +++ b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/NetworkInterfaceMetricValidator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using GuardNet; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; + +namespace Promitor.Scraper.Host.Validation.MetricDefinitions.ResourceTypes +{ + internal class NetworkInterfaceMetricValidator : MetricValidator + { + protected override IEnumerable Validate(NetworkInterfaceMetricDefinition networkInterfaceMetricDefinition) + { + Guard.NotNull(networkInterfaceMetricDefinition, nameof(networkInterfaceMetricDefinition)); + + var errorMessages = new List(); + + if (string.IsNullOrWhiteSpace(networkInterfaceMetricDefinition.NetworkInterfaceName)) + { + errorMessages.Add("No network interface name is configured"); + } + + return errorMessages; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs b/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs index 539c678e4..6c8a8d368 100644 --- a/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs @@ -127,6 +127,22 @@ public string Build() return this; } + public MetricsDeclarationBuilder WithNetworkInterfaceMetric(string metricName = "promitor-network-interface", string metricDescription = "Description for a metric", string networkInterfaceName = "promitor-network-interface-name", string azureMetricName = "Total") + { + var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); + var metric = new NetworkInterfaceMetricDefinition + { + Name = metricName, + Description = metricDescription, + NetworkInterfaceName = networkInterfaceName, + AzureMetricConfiguration = azureMetricConfiguration + }; + + _metrics.Add(metric); + + return this; + } + public MetricsDeclarationBuilder WithGenericMetric(string metricName = "foo", string metricDescription = "Description for a metric", string resourceUri = "Microsoft.ServiceBus/namespaces/promitor-messaging", string filter = "EntityName eq \'orders\'", string azureMetricName = "Total") { var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithNetworkInterfaceYamlSerializationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithNetworkInterfaceYamlSerializationTests.cs new file mode 100644 index 000000000..525531dff --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithNetworkInterfaceYamlSerializationTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Bogus; +using Microsoft.Extensions.Logging.Abstractions; +using Promitor.Core.Scraping.Configuration.Model; +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Serialization.Core; +using Xunit; + +namespace Promitor.Scraper.Tests.Unit.Serialization.MetricsDeclaration +{ + [Category(category: "Unit")] + public class MetricsDeclarationWithNetworkInterfaceYamlSerializationTests : YamlSerializationTests + { + [Theory] + [InlineData("promitor1", @"01:00", @"2:00")] + [InlineData(null, null, null)] + public void YamlSerialization_SerializeAndDeserializeValidConfigForNetworkInterface_SucceedsWithIdenticalOutput(string resourceGroupName, string defaultScrapingInterval, string metricScrapingInterval) + { + // Arrange + var azureMetadata = GenerateBogusAzureMetadata(); + var networkInterfaceMetricDefinition = GenerateBogusNetworkInterfaceMetricDefinition(resourceGroupName, metricScrapingInterval); + var metricDefaults = GenerateBogusMetricDefaults(defaultScrapingInterval); + var scrapingConfiguration = new Core.Scraping.Configuration.Model.MetricsDeclaration + { + AzureMetadata = azureMetadata, + MetricDefaults = metricDefaults, + Metrics = new List + { + networkInterfaceMetricDefinition + } + }; + var configurationSerializer = new ConfigurationSerializer(NullLogger.Instance); + + // Act + var serializedConfiguration = configurationSerializer.Serialize(scrapingConfiguration); + var deserializedConfiguration = configurationSerializer.Deserialize(serializedConfiguration); + + // Assert + Assert.NotNull(deserializedConfiguration); + AssertAzureMetadata(deserializedConfiguration, azureMetadata); + AssertMetricDefaults(deserializedConfiguration, metricDefaults); + Assert.NotNull(deserializedConfiguration.Metrics); + Assert.Single(deserializedConfiguration.Metrics); + var deserializedMetricDefinition = deserializedConfiguration.Metrics.FirstOrDefault(); + AssertMetricDefinition(deserializedMetricDefinition, networkInterfaceMetricDefinition); + var deserializedNetworkInterfaceMetricDefinition = deserializedMetricDefinition as NetworkInterfaceMetricDefinition; + AssertNetworkInterfaceMetricDefinition(deserializedNetworkInterfaceMetricDefinition, networkInterfaceMetricDefinition); + } + + private static void AssertNetworkInterfaceMetricDefinition(NetworkInterfaceMetricDefinition deserializedNetworkInterfaceMetricDefinition, NetworkInterfaceMetricDefinition networkInterfaceMetricDefinition) + { + Assert.NotNull(deserializedNetworkInterfaceMetricDefinition); + Assert.Equal(networkInterfaceMetricDefinition.NetworkInterfaceName, deserializedNetworkInterfaceMetricDefinition.NetworkInterfaceName); + } + + private NetworkInterfaceMetricDefinition GenerateBogusNetworkInterfaceMetricDefinition(string resourceGroupName, string metricScrapingInterval) + { + var bogusScrapingInterval = GenerateBogusScrapingInterval(metricScrapingInterval); + var bogusAzureMetricConfiguration = GenerateBogusAzureMetricConfiguration(); + Faker bogusGenerator = new Faker() + .StrictMode(ensureRulesForAllProperties: true) + .RuleFor(metricDefinition => metricDefinition.Name, faker => faker.Name.FirstName()) + .RuleFor(metricDefinition => metricDefinition.Description, faker => faker.Lorem.Sentence(wordCount: 6)) + .RuleFor(metricDefinition => metricDefinition.ResourceType, faker => ResourceType.NetworkInterface) + .RuleFor(metricDefinition => metricDefinition.NetworkInterfaceName, faker => faker.Name.LastName()) + .RuleFor(metricDefinition => metricDefinition.AzureMetricConfiguration, faker => bogusAzureMetricConfiguration) + .RuleFor(metricDefinition => metricDefinition.ResourceGroupName, faker => resourceGroupName) + .RuleFor(metricDefinition => metricDefinition.Scraping, faker => bogusScrapingInterval) + .Ignore(metricDefinition => metricDefinition.ResourceGroupName); + + return bogusGenerator.Generate(); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/NetworkInterfaceMetricsDeclarationValidationStepsTests.cs b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/NetworkInterfaceMetricsDeclarationValidationStepsTests.cs new file mode 100644 index 000000000..4871965df --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/NetworkInterfaceMetricsDeclarationValidationStepsTests.cs @@ -0,0 +1,97 @@ +using System.ComponentModel; +using Promitor.Scraper.Host.Validation.Steps; +using Promitor.Scraper.Tests.Unit.Builders; +using Promitor.Scraper.Tests.Unit.Stubs; +using Xunit; + +namespace Promitor.Scraper.Tests.Unit.Validation.Metrics.ResourceTypes +{ + [Category("Unit")] + public class NetworkInterfaceMetricsDeclarationValidationStepTests + { + [Fact] + public void NetworkInterfaceMetricsDeclaration_DeclarationWithoutAzureMetricName_Succeeds() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithNetworkInterfaceMetric(azureMetricName: string.Empty) + .Build(); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void NetworkInterfaceMetricsDeclaration_DeclarationWithoutMetricDescription_Succeeded() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithNetworkInterfaceMetric(metricDescription: string.Empty) + .Build(); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful, "Validation was not successful"); + } + + [Fact] + public void NetworkInterfaceMetricsDeclaration_DeclarationWithoutMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithNetworkInterfaceMetric(string.Empty) + .Build(); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void NetworkInterfaceMetricsDeclaration_DeclarationWithoutNetworkInterfaceName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithNetworkInterfaceMetric(networkInterfaceName: string.Empty) + .Build(); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.False(validationResult.IsSuccessful, "Validation is successful"); + } + + [Fact] + public void NetworkInterfaceMetricsDeclaration_ValidDeclaration_Succeeds() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithNetworkInterfaceMetric() + .Build(); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawMetricsDeclaration); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + Assert.True(validationResult.IsSuccessful, "Validation was not successful"); + } + } +} \ No newline at end of file