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