diff --git a/docs/configuration/metrics/index.md b/docs/configuration/metrics/index.md index 7566883f5..67624258d 100644 --- a/docs/configuration/metrics/index.md +++ b/docs/configuration/metrics/index.md @@ -86,6 +86,7 @@ We also provide a simplified way to scrape the following Azure resources: - [Azure Container Instances](container-instances) - [Azure Container Registry](container-registry) - [Azure Cosmos DB](cosmos-db) +- [Azure Database for PostgreSQL](postgresql) - [Azure Network Interface](network-interface) - [Azure Service Bus Queue](service-bus-queue) - [Azure Storage Queue](storage-queue) diff --git a/docs/configuration/metrics/postgresql.md b/docs/configuration/metrics/postgresql.md new file mode 100644 index 000000000..508404e68 --- /dev/null +++ b/docs/configuration/metrics/postgresql.md @@ -0,0 +1,30 @@ +--- +layout: default +title: Azure Database for PostgreSQL +--- + +## Azure Database for PostgreSQL - ![Availability Badge](https://img.shields.io/badge/Available%20Starting-v1.0.0-green.svg) +You can declare to scrape an Azure Database for PostgreSQL server via the `PostgreSql` resource type. + +The following fields need to be provided: +- `serverName` - The name of the PostgreSQL server + +All supported metrics are documented in the official [Azure Monitor documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported#microsoftdbforpostgresqlservers). + +Example: +```yaml +name: postgre_sql_cpu_percent +description: "The CPU percentage on the server" +resourceType: PostgreSql +serverName: Promitor +scraping: + schedule: "0 */2 * ? * *" +azureMetricConfiguration: + metricName: cpu_percent + aggregation: + type: Average + interval: 00:01:00 +``` + +[← 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/PostgreSqlMetricDefinition.cs b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/PostgreSqlMetricDefinition.cs new file mode 100644 index 000000000..ad5e1641f --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Model/Metrics/ResourceTypes/PostgreSqlMetricDefinition.cs @@ -0,0 +1,8 @@ +namespace Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes +{ + public class PostgreSqlMetricDefinition : MetricDefinition + { + public string ServerName { get; set; } + public override ResourceType ResourceType { get; } = ResourceType.PostgreSql; + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs index 7c76376e2..194bd88bf 100644 --- a/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs +++ b/src/Promitor.Core.Scraping/Configuration/Model/ResourceType.cs @@ -12,5 +12,6 @@ public enum ResourceType NetworkInterface = 7, CosmosDb = 8, RedisCache = 9, + PostgreSql = 10, } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/PostgreSqlMetricDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/PostgreSqlMetricDeserializer.cs new file mode 100644 index 000000000..b0e55185d --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializers/PostgreSqlMetricDeserializer.cs @@ -0,0 +1,27 @@ +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 +{ + /// + /// Defines a deserializer for the PostgreSQL Server resource type + /// + internal class PostgreSqlMetricDeserializer : GenericAzureMetricDeserializer + { + /// + /// Deserializes the specified PostgreSQL Server metric node from the YAML configuration file. + /// + /// The metric node to deserialize to PostgreSQL Server configuration + /// A new object (strongly typed as a ) + internal override MetricDefinition Deserialize(YamlMappingNode metricNode) + { + var metricDefinition = base.DeserializeMetricDefinition(metricNode); + + var serverName = metricNode.Children[new YamlScalarNode("serverName")]; + metricDefinition.ServerName = serverName?.ToString(); + + return metricDefinition; + } + } +} diff --git a/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs index ad9e8561c..f64be9906 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricDeserializerFactory.cs @@ -26,7 +26,9 @@ internal static GenericAzureMetricDeserializer GetDeserializerFor(Configuration. case Configuration.Model.ResourceType.CosmosDb: return new CosmosDbMetricDeserializer(); case Configuration.Model.ResourceType.RedisCache: - return new RedisCacheMetricDeserializer(); + return new RedisCacheMetricDeserializer(); + case Configuration.Model.ResourceType.PostgreSql: + return new PostgreSqlMetricDeserializer(); } 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 1177477d4..2bef8494b 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -45,7 +45,9 @@ public static IScraper CreateScraper(ResourceType metricDefini case ResourceType.CosmosDb: return new CosmosDbScraper(azureMetadata, azureMonitorClient, logger, exceptionTracker); case ResourceType.RedisCache: - return new RedisCacheScraper(azureMetadata, azureMonitorClient, logger, exceptionTracker); + return new RedisCacheScraper(azureMetadata, azureMonitorClient, logger, exceptionTracker); + case ResourceType.PostgreSql: + return new PostgreSqlScraper(azureMetadata, azureMonitorClient, logger, exceptionTracker); default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Promitor.Core.Scraping/ResourceTypes/PostgreSqlScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/PostgreSqlScraper.cs new file mode 100644 index 000000000..ecbf32145 --- /dev/null +++ b/src/Promitor.Core.Scraping/ResourceTypes/PostgreSqlScraper.cs @@ -0,0 +1,31 @@ +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; +using System; +using System.Threading.Tasks; + +namespace Promitor.Core.Scraping.ResourceTypes +{ + public class PostgreSqlScraper : Scraper + { + private const string ResourceUriTemplate = "subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DBforPostgreSQL/servers/{2}"; + + public PostgreSqlScraper(AzureMetadata azureMetadata, AzureMonitorClient azureMonitorClient, ILogger logger, IExceptionTracker exceptionTracker) + : base(azureMetadata, azureMonitorClient, logger, exceptionTracker) + { + } + + protected override async Task ScrapeResourceAsync(string subscriptionId, string resourceGroupName, PostgreSqlMetricDefinition metricDefinition, AggregationType aggregationType, TimeSpan aggregationInterval) + { + var resourceUri = string.Format(ResourceUriTemplate, subscriptionId, resourceGroupName, metricDefinition.ServerName); + + var metricName = metricDefinition.AzureMetricConfiguration.MetricName; + var foundMetricValue = await AzureMonitorClient.QueryMetricAsync(metricName, aggregationType, aggregationInterval, resourceUri); + + return new ScrapeResult(resourceUri, foundMetricValue); + } + } +} diff --git a/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs b/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs index a5a192493..47182abc6 100644 --- a/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs +++ b/src/Promitor.Scraper.Host/Validation/Factories/MetricValidatorFactory.cs @@ -28,7 +28,9 @@ internal static IMetricValidator GetValidatorFor(ResourceType resourceType) case ResourceType.CosmosDb: return new CosmosDbMetricValidator(); case ResourceType.RedisCache: - return new RedisCacheMetricValidator(); + return new RedisCacheMetricValidator(); + case ResourceType.PostgreSql: + return new PostgreSqlMetricValidator(); } throw new ArgumentOutOfRangeException(nameof(resourceType), $"No validation rules are defined for metric type '{resourceType}'"); diff --git a/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/PostgreSqlMetricValidator.cs b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/PostgreSqlMetricValidator.cs new file mode 100644 index 000000000..b3a1a44c6 --- /dev/null +++ b/src/Promitor.Scraper.Host/Validation/MetricDefinitions/ResourceTypes/PostgreSqlMetricValidator.cs @@ -0,0 +1,19 @@ +using GuardNet; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using System.Collections.Generic; + +namespace Promitor.Scraper.Host.Validation.MetricDefinitions.ResourceTypes +{ + internal class PostgreSqlMetricValidator : MetricValidator + { + protected override IEnumerable Validate(PostgreSqlMetricDefinition postgreSqlMetricDefinition) + { + Guard.NotNull(postgreSqlMetricDefinition, nameof(postgreSqlMetricDefinition)); + + if (string.IsNullOrWhiteSpace(postgreSqlMetricDefinition.ServerName)) + { + yield return "No server name is configured"; + } + } + } +} diff --git a/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs b/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs index a8e3bce9f..b16a28b7b 100644 --- a/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Scraper.Tests.Unit/Builders/MetricsDeclarationBuilder.cs @@ -218,5 +218,20 @@ private AzureMetricConfiguration CreateAzureMetricConfiguration(string azureMetr return this; } + + public MetricsDeclarationBuilder WithPostgreSqlMetric(string metricName = "promitor-postgresql", string metricDescription = "Description for a metric", string serverName = "promitor-postgresql", string azureMetricName = "cpu_percent") + { + var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); + var metric = new PostgreSqlMetricDefinition + { + Name = metricName, + Description = metricDescription, + ServerName = serverName, + AzureMetricConfiguration = azureMetricConfiguration + }; + _metrics.Add(metric); + + return this; + } } } \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithPostgreSqlYamlSerializationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithPostgreSqlYamlSerializationTests.cs new file mode 100644 index 000000000..667224052 --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/MetricsDeclaration/MetricsDeclarationWithPostgreSqlYamlSerializationTests.cs @@ -0,0 +1,77 @@ +using Bogus; +using Microsoft.Extensions.Logging.Abstractions; +using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Serialization.Core; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Xunit; +using MetricDefinition = Promitor.Core.Scraping.Configuration.Model.Metrics.MetricDefinition; + +namespace Promitor.Scraper.Tests.Unit.Serialization.MetricsDeclaration +{ + [Category("Unit")] + public class MetricsDeclarationWithPostgreSqlYamlSerializationTests : YamlSerializationTests + { + [Theory] + [InlineData("promitor1", @"* */1 * * * *", @"* */2 * * * *")] + [InlineData(null, null, null)] + public void YamlSerialization_SerializeAndDeserializeConfigForPostgreSql_SucceedsWithIdenticalOutput(string resourceGroupName, string defaultScrapingInterval, string metricScrapingInterval) + { + // Arrange + var azureMetadata = GenerateBogusAzureMetadata(); + var postgreSqlMetricDefinition = GenerateBogusPostgreSqlMetricDefinition(resourceGroupName, metricScrapingInterval); + var metricDefaults = GenerateBogusMetricDefaults(defaultScrapingInterval); + var scrapingConfiguration = new Core.Scraping.Configuration.Model.MetricsDeclaration() + { + AzureMetadata = azureMetadata, + MetricDefaults = metricDefaults, + Metrics = new List() + { + postgreSqlMetricDefinition + } + }; + 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, postgreSqlMetricDefinition); + var deserializedPostgreSqlMetricDefinition = deserializedMetricDefinition as PostgreSqlMetricDefinition; + AssertPostgreSqlMetricDefinition(deserializedPostgreSqlMetricDefinition, postgreSqlMetricDefinition); + } + + private static void AssertPostgreSqlMetricDefinition(PostgreSqlMetricDefinition deserializedPostgreSqlMetricDefinition, PostgreSqlMetricDefinition postgreSqlMetricDefinition) + { + Assert.NotNull(deserializedPostgreSqlMetricDefinition); + Assert.Equal(postgreSqlMetricDefinition.ServerName, deserializedPostgreSqlMetricDefinition.ServerName); + } + + private PostgreSqlMetricDefinition GenerateBogusPostgreSqlMetricDefinition(string resourceGroupName, string metricScrapingInterval) + { + var bogusScrapingInterval = GenerateBogusScrapingInterval(metricScrapingInterval); + var bogusAzureMetricConfiguration = GenerateBogusAzureMetricConfiguration(); + + var bogusGenerator = new Faker() + .StrictMode(ensureRulesForAllProperties: true) + .RuleFor(metricDefinition => metricDefinition.Name, faker => faker.Lorem.Word()) + .RuleFor(metricDefinition => metricDefinition.Description, faker => faker.Lorem.Sentence(wordCount: 6)) + .RuleFor(metricDefinition => metricDefinition.ResourceType, faker => Core.Scraping.Configuration.Model.ResourceType.PostgreSql) + .RuleFor(metricDefinition => metricDefinition.ServerName, faker => faker.Lorem.Word()) + .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(); + } + } +} diff --git a/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/PostgreSqlMetricsDeclarationValidationStepTests.cs b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/PostgreSqlMetricsDeclarationValidationStepTests.cs new file mode 100644 index 000000000..a309ecfe1 --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/PostgreSqlMetricsDeclarationValidationStepTests.cs @@ -0,0 +1,63 @@ +using Promitor.Scraper.Host.Validation.Steps; +using Promitor.Scraper.Tests.Unit.Builders; +using Promitor.Scraper.Tests.Unit.Stubs; +using System.ComponentModel; +using Xunit; + +namespace Promitor.Scraper.Tests.Unit.Validation.Metrics.ResourceTypes +{ + [Category("Unit")] + public class PostgreSqlMetricsDeclarationValidationStepTests + { + [Fact] + public void PostgreSqlMetricsDeclaration_DeclarationWithoutAzureMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithPostgreSqlMetric(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 not successful"); + } + + [Fact] + public void PostgreSqlMetricsDeclaration_DeclarationWithoutAzureMetricDescription_Succeeds() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithPostgreSqlMetric(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 is successful"); + } + + [Fact] + public void PostgreSqlMetricsDeclaration_DeclarationWithoutServerName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithPostgreSqlMetric(serverName: 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 not successful"); + } + } +}