diff --git a/docs/configuration/metrics/index.md b/docs/configuration/metrics/index.md index 0d9fb58cf..fd8b0b64f 100644 --- a/docs/configuration/metrics/index.md +++ b/docs/configuration/metrics/index.md @@ -56,6 +56,7 @@ We also provide a simplified way to configure the following Azure resources: - [Azure Container Instances](container-instances) - [Azure Service Bus Queue](service-bus-queue) - [Azure Storage Queue](storage-queue) +- [Azure Virtual Machine](virtual-machine) 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/virtual-machine.md b/docs/configuration/metrics/virtual-machine.md new file mode 100644 index 000000000..c36a0f0c6 --- /dev/null +++ b/docs/configuration/metrics/virtual-machine.md @@ -0,0 +1,27 @@ +--- +layout: default +title: Azure Virtual Machine Declaration +--- + +## Azure Virtual Machine +You can declare to scrape an Azure Virtual Machine via the `VirtualMachine` resource type. + +The following fields need to be provided: +- `virtualMachineName` - The name of the virtual machine + +All supported metrics are documented in the official [Azure Monitor documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported#microsoftcomputevirtualmachines). + +Example: +```yaml +name: demo_virtualmachine_percentage_cpu +description: "Average percentage cpu usage of our 'promitor-virtual-machine' virtual machine" +resourceType: VirtualMachine +virtualMachineName: promitor-virtual-machine +azureMetricConfiguration: + metricName: Percentage CPU + 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/VirtualMachineMetricDefinition.cs b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/VirtualMachineMetricDefinition.cs new file mode 100644 index 000000000..685d736ee --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/VirtualMachineMetricDefinition.cs @@ -0,0 +1,8 @@ +namespace Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes +{ + public class VirtualMachineMetricDefinition : MetricDefinition + { + public string VirtualMachineName { get; set; } + public override ResourceType ResourceType { get; } = ResourceType.VirtualMachine; + } +} \ 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 dc42804bf..705fe56fb 100644 --- a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs +++ b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs @@ -6,6 +6,7 @@ public enum ResourceType ServiceBusQueue = 1, Generic = 2, StorageQueue = 3, - ContainerInstance = 4 + ContainerInstance = 4, + VirtualMachine = 5 } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/VirtualMachineMetricDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/VirtualMachineMetricDeserializer.cs new file mode 100644 index 000000000..2fb520433 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/VirtualMachineMetricDeserializer.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 VirtualMachineMetricDeserializer : GenericAzureMetricDeserializer + { + /// Deserializes the specified Virtual Machine metric node from the YAML configuration file. + /// The metric node containing 'virtualMachineName' parameter pointing to an instance of a Virtual Machine + /// A new object (strongly typed as a ) + internal override MetricDefinition Deserialize(YamlMappingNode metricNode) + { + var metricDefinition = base.DeserializeMetricDefinition(metricNode); + var virtualMachineName = metricNode.Children[new YamlScalarNode("virtualMachineName")]; + + metricDefinition.VirtualMachineName = virtualMachineName?.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 9cf6b0763..1293299ee 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs @@ -17,6 +17,8 @@ internal static GenericAzureMetricDeserializer GetDeserializerFor(Configuration. return new StorageQueueMetricDeserializer(); case Configuration.Model.ResourceType.ContainerInstance: return new ContainerInstanceMetricDeserializer(); + case Configuration.Model.ResourceType.VirtualMachine: + return new VirtualMachineMetricDeserializer(); } 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 a87dc645f..3b09da6b3 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -35,6 +35,8 @@ public static IScraper CreateScraper(ResourceType metricDefini return new StorageQueueScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); case ResourceType.ContainerInstance: return new ContainerInstanceScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); + case ResourceType.VirtualMachine: + return new VirtualMachineScraper(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker); default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Promitor.Core.Scraping/ResourceTypes/VirtualMachineScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/VirtualMachineScraper.cs new file mode 100644 index 000000000..11193eea1 --- /dev/null +++ b/src/Promitor.Core.Scraping/ResourceTypes/VirtualMachineScraper.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 VirtualMachineScraper : Scraper + { + private const string ResourceUriTemplate = "subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Compute/virtualMachines/{2}"; + + public VirtualMachineScraper(AzureMetadata azureMetadata, MetricDefaults metricDefaults, AzureMonitorClient azureMonitorClient, ILogger logger, IExceptionTracker exceptionTracker) + : base(azureMetadata, metricDefaults, azureMonitorClient, logger, exceptionTracker) + { + } + + protected override async Task ScrapeResourceAsync(VirtualMachineMetricDefinition metricDefinition, AggregationType aggregationType, TimeSpan aggregationInterval) + { + var resourceUri = string.Format(ResourceUriTemplate, AzureMetadata.SubscriptionId, AzureMetadata.ResourceGroupName, metricDefinition.VirtualMachineName); + + 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 7fa7ab346..4b2ed565e 100644 --- a/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs +++ b/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs @@ -19,6 +19,8 @@ internal static IMetricValidator GetValidatorFor(ResourceType resourceType) return new StorageQueueMetricValidator(); case ResourceType.ContainerInstance: return new ContainerInstanceMetricValidator(); + case ResourceType.VirtualMachine: + return new VirtualMachineMetricValidator(); } throw new ArgumentOutOfRangeException(nameof(resourceType), $"No validation rules are defined for metric type '{resourceType}'"); diff --git a/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/VirtualMachineMetricValidator.cs b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/VirtualMachineMetricValidator.cs new file mode 100644 index 000000000..cfa13a7c1 --- /dev/null +++ b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/VirtualMachineMetricValidator.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 VirtualMachineMetricValidator : MetricValidator + { + protected override IEnumerable Validate(VirtualMachineMetricDefinition virtualMachineMetricDefinition) + { + Guard.NotNull(virtualMachineMetricDefinition, nameof(virtualMachineMetricDefinition)); + + var errorMessages = new List(); + + if (string.IsNullOrWhiteSpace(virtualMachineMetricDefinition.VirtualMachineName)) + { + errorMessages.Add("No virtual machine 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 b6b35f7fe..8ec64a881 100644 --- a/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs @@ -96,6 +96,22 @@ public string Build() return this; } + public MetricsDeclarationBuilder WithVirtualMachineMetric(string metricName = "promitor-virtual-machine", string metricDescription = "Description for a metric", string virtualMachineName = "promitor-virtual-machine-name", string azureMetricName = "Total") + { + var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); + var metric = new VirtualMachineMetricDefinition + { + Name = metricName, + Description = metricDescription, + VirtualMachineName = virtualMachineName, + 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/MetricsDeclarationWithVirtualMachineYamlSerializationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithVirtualMachineYamlSerializationTests.cs new file mode 100644 index 000000000..2b513939f --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithVirtualMachineYamlSerializationTests.cs @@ -0,0 +1,75 @@ +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.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Serialization.Core; +using Xunit; +using MetricDefinition = Promitor.Core.Scraping.Configuration.Model.Metrics.MetricDefinition; + +namespace Promitor.Scraper.Tests.Unit.Serialization.MetricsDeclaration +{ + [Category("Unit")] + public class MetricsDeclarationWithVirtualMachineYamlSerializationTests : YamlSerializationTests + { + [Fact] + public void YamlSerialization_SerializeAndDeserializeValidConfigForVirtualMachine_SucceedsWithIdenticalOutput() + { + // Arrange + var azureMetadata = GenerateBogusAzureMetadata(); + var virtualMachineMetricDefinition = GenerateBogusVirtualMachineMetricDefinition(); + var metricDefaults = GenerateBogusMetricDefaults(); + var scrapingConfiguration = new Core.Scraping.Configuration.Model.MetricsDeclaration + { + AzureMetadata = azureMetadata, + MetricDefaults = metricDefaults, + Metrics = new List + { + virtualMachineMetricDefinition + } + }; + 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, virtualMachineMetricDefinition); + var deserializedVirtualMachineMetricDefinition = deserializedMetricDefinition as VirtualMachineMetricDefinition; + AssertVirtualMachineMetricDefinition(deserializedVirtualMachineMetricDefinition, virtualMachineMetricDefinition, deserializedMetricDefinition); + } + + private static void AssertVirtualMachineMetricDefinition(VirtualMachineMetricDefinition deserializedVirtualMachineMetricDefinition, VirtualMachineMetricDefinition virtualMachineMetricDefinition, MetricDefinition deserializedMetricDefinition) + { + Assert.NotNull(deserializedVirtualMachineMetricDefinition); + Assert.Equal(virtualMachineMetricDefinition.VirtualMachineName, deserializedVirtualMachineMetricDefinition.VirtualMachineName); + Assert.NotNull(deserializedMetricDefinition.AzureMetricConfiguration); + Assert.Equal(virtualMachineMetricDefinition.AzureMetricConfiguration.MetricName, deserializedMetricDefinition.AzureMetricConfiguration.MetricName); + Assert.NotNull(deserializedMetricDefinition.AzureMetricConfiguration.Aggregation); + Assert.Equal(virtualMachineMetricDefinition.AzureMetricConfiguration.Aggregation.Type, deserializedMetricDefinition.AzureMetricConfiguration.Aggregation.Type); + Assert.Equal(virtualMachineMetricDefinition.AzureMetricConfiguration.Aggregation.Interval, deserializedMetricDefinition.AzureMetricConfiguration.Aggregation.Interval); + } + private VirtualMachineMetricDefinition GenerateBogusVirtualMachineMetricDefinition() + { + var bogusAzureMetricConfiguration = GenerateBogusAzureMetricConfiguration(); + var 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.VirtualMachine) + .RuleFor(metricDefinition => metricDefinition.VirtualMachineName, faker => faker.Name.LastName()) + .RuleFor(metricDefinition => metricDefinition.AzureMetricConfiguration, faker => bogusAzureMetricConfiguration); + + return bogusGenerator.Generate(); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/VirtualMachineMetricsDeclarationValidationStepsTests.cs b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/VirtualMachineMetricsDeclarationValidationStepsTests.cs new file mode 100644 index 000000000..f5c7e288b --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/VirtualMachineMetricsDeclarationValidationStepsTests.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 VirtualMachineMetricsDeclarationValidationStepTests + { + [Fact] + public void VirtualMachineMetricsDeclaration_DeclarationWithoutAzureMetricName_Succeeds() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithVirtualMachineMetric(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 VirtualMachineMetricsDeclaration_DeclarationWithoutMetricDescription_Succeeded() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithVirtualMachineMetric(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 VirtualMachineMetricsDeclaration_DeclarationWithoutMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithVirtualMachineMetric(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 VirtualMachineMetricsDeclaration_DeclarationWithoutVirtualMachineName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithVirtualMachineMetric(virtualMachineName: 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 VirtualMachineMetricsDeclaration_ValidDeclaration_Succeeds() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithVirtualMachineMetric() + .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