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");
+ }
+ }
+}