From a2f45699bc2519d69989b0825ec2bbd41f2c5148 Mon Sep 17 00:00:00 2001 From: Adam Connelly Date: Mon, 24 Feb 2020 16:22:21 +0000 Subject: [PATCH] Improve Promitor's metric configuration validation (#893) * Initial implementation of new validation approach I've altered the way that the deserializers work and are defined so that they are simpler to create, but also so that we can record validation failures during the deserialization process. This allows us to provide much better validation messages than we can if we take the approach of validating based on the deserialized objects. In this initial change I haven't gone ahead and updated all of the deserializers because I wanted the change to be kept small. I've also not actually hooked up the validation messages in any way - instead I've just provided a stub ErrorReporter object that doesn't actually do anything yet. Part of #592 * Converted AzureMetricConfigurationDeserializer Converted AzureMetricConfigurationDeserializer to the new way of defining a deserializer, and added support in the base Deserializer for using another deserializer to deserialize child properties. I had to add a new `IDeserializer` interface that isn't generic to allow us to deserialize a child property at runtime without knowing its type. Part of #592 * Converted MetricAggregationDeserializer Part of #592 * Converted MetricDefaultsDeserializer Part of #592 * Converted MetricDefinitionDeserializer Also: - Fixed the metrics definition docs to indicate that 'labels' is optional. Part of #592 * Converted ScrapingDeserializer Part of #592 * Converted SecretDeserializer Part of #592 * Converted all of the resource type deserializers In order to do this I had to alter the type parameter in `IDeserializer` to be covariant so that I could define the resource deserializers as their specific type (for example `IDeserializer`), but still allow them to be returned as an `IDeserializer` from the resource deserializer factory. I also ended up altering a few of the newer deserializers that are using inheritance (for example, SqlServer / SqlDatabase deserializers) to inherit directly from `ResourceDeserializer` again. The reason I did this is that the `Deserializer` class needs to know the specific type of object to construct, and I decided that the extra complication of adding more generics to get it to work wasn't worth the hassle to save one or two lines of code. Part of #592 * Hook up the new validation to Promitor This change implements the ErrorReporter, and connects it to Promitor's startup validation so that the errors are actually reported. In addition: - Report error when a deserializer cannot be found. - Ignore the 'resources' field on metrics because we are manually deserializing it. - Make `AzureMetricConfiguration.Dimension` optional. - Fix a test in `ServiceBusQueueMetricsDeclarationValidationStepTests` that was asserting that validation was successful, when the test was meant to test for a validation failure. Part of #592 * Don't reuse context when deserializing Because the deserializers are singletons, the set of FieldContext objects was being reused when deserializing multiple nodes of the same type. This lead to a bug where if we had multiple metric definitions, and the first one to be deserialized set a required field, an error would not be reported if that required field was missing from subsequent definitions. To solve this I've pulled the FieldContext class out of the Deserializer, and I've split it into two separate objects: - `FieldDeserializationInfo` - contains immutable information about the field to be deserialized, and can be reused between multiple deserializations. - `FieldDeserializationContext` - contains any information about a specific deserialization attempt, and must be created each time we deserialize a node using a deserializer. In addition, I've also altered the way the errors / warnings are output to avoid issues I was seeing where the messages weren't always output in the correct order because of the way the logger works. Part of #592 * Add validation improvements to changelog Part of #592 * Refactor the validation error tests I've added some new methods to the `YamlAssert` class to make it easier to test that errors are reported when required properties are not supplied. I've also removed some TODOs for functionality that can be added later after this first set of validation changes are merged. Part of #592 * Fixing code-quality issues * fixup! Fixing code-quality issues --- changelog/content/experimental/unreleased.md | 1 + docs/configuration/v1.x/metrics/index.md | 2 +- .../Interfaces/IMetricsDeclarationProvider.cs | 4 +- .../Providers/MetricsDeclarationProvider.cs | 5 +- .../Serialization/ConfigurationSerializer.cs | 9 +- .../Serialization/DeserializationContext.cs | 61 ++++ .../Serialization/DeserializationMessage.cs | 48 +++ .../Serialization/Deserializer.cs | 149 ++++++++- .../Serialization/ErrorReporter.cs | 36 +++ .../FieldDeserializationContext.cs | 53 ++++ .../Serialization/FieldDeserializationInfo.cs | 68 +++++ .../Serialization/IDeserializer.cs | 22 +- .../Serialization/IErrorReporter.cs | 35 +++ .../Serialization/MessageType.cs | 18 ++ .../YamlMappingNodeExtensions.cs | 33 +- .../v1/Core/AggregationDeserializer.cs | 23 +- .../v1/Core/AzureMetadataDeserializer.cs | 58 ++-- .../AzureMetricConfigurationDeserializer.cs | 42 +-- .../v1/Core/MetricAggregationDeserializer.cs | 22 +- .../v1/Core/MetricDefaultsDeserializer.cs | 21 +- .../v1/Core/MetricDefinitionDeserializer.cs | 74 ++--- .../v1/Core/MetricDimensionDeserializer.cs | 2 +- .../v1/Core/ScrapingDeserializer.cs | 18 +- .../v1/Core/SecretDeserializer.cs | 19 +- .../Serialization/v1/Core/V1Deserializer.cs | 20 +- .../v1/Model/MetricDefinitionV1.cs | 2 +- .../v1/Model/MetricsDeclarationV1.cs | 2 +- .../v1/Providers/ApiManagementDeserializer.cs | 15 +- .../v1/Providers/AppPlanDeserializer.cs | 14 +- .../v1/Providers/BlobStorageDeserializer.cs | 11 +- .../ContainerInstanceDeserializer.cs | 14 +- .../ContainerRegistryDeserializer.cs | 14 +- .../v1/Providers/CosmosDbDeserializer.cs | 14 +- .../v1/Providers/FileStorageDeserializer.cs | 11 +- .../v1/Providers/FunctionAppDeserializer.cs | 18 +- .../Providers/GenericResourceDeserializer.cs | 18 +- .../Providers/NetworkInterfaceDeserializer.cs | 14 +- .../v1/Providers/PostgreSqlDeserializer.cs | 14 +- .../v1/Providers/RedisCacheDeserializer.cs | 14 +- .../v1/Providers/ResourceDeserializer.cs | 25 +- .../Providers/ServiceBusQueueDeserializer.cs | 18 +- .../v1/Providers/SqlDatabaseDeserializer.cs | 15 +- .../SqlManagedInstanceDeserializer.cs | 12 +- .../v1/Providers/SqlServerDeserializer.cs | 12 +- .../Providers/StorageAccountDeserializer.cs | 14 +- .../v1/Providers/StorageQueueDeserializer.cs | 28 +- .../Providers/VirtualMachineDeserializer.cs | 14 +- .../VirtualMachineScaleSetDeserializer.cs | 14 +- .../v1/Providers/WebAppDeserializer.cs | 18 +- .../Steps/MetricsDeclarationValidationStep.cs | 33 +- .../DeserializerTests/DeserializationTests.cs | 286 ++++++++++++++++++ .../DeserializerTests/ValidationTests.cs | 176 +++++++++++ .../Serialization/ErrorReporterTests.cs | 142 +++++++++ .../YamlMappingNodeExtensionTests.cs | 7 +- .../v1/Core/AzureMetadataDeserializerTests.cs | 72 ++++- ...ureMetricConfigurationDeserializerTests.cs | 36 ++- .../MetricAggregationDeserializerTests.cs | 13 + .../Core/MetricDefaultsDeserializerTests.cs | 35 ++- .../Core/MetricDefinitionDeserializerTests.cs | 161 +++++++++- .../v1/Core/ScrapingDeserializerTests.cs | 13 + .../v1/Core/SecretDeserializerTests.cs | 30 ++ .../v1/Core/V1DeserializerTests.cs | 38 +-- .../Providers/BlobStorageDeserializerTests.cs | 4 +- .../ContainerInstanceDeserializerTests.cs | 13 + .../ContainerRegistryDeserializerTests.cs | 13 + .../v1/Providers/CosmosDbDeserializerTests.cs | 13 + .../Providers/FileStorageDeserializerTests.cs | 2 +- .../GenericResourceDeserializerTests.cs | 13 + .../NetworkInterfaceDeserializerTests.cs | 13 + .../Providers/PostgreSqlDeserializerTests.cs | 13 + .../Providers/RedisCacheDeserializerTests.cs | 13 + .../ServiceBusQueueDeserializerTests.cs | 26 ++ .../Providers/SqlDatabaseDeserializerTests.cs | 33 +- .../StorageQueueDeserializerTests.cs | 44 ++- .../VirtualMachineDeserializerTests.cs | 13 + .../Serialization/v1/V1SerializationTests.cs | 5 +- .../Serialization/v1/YamlAssert.cs | 106 ++++++- ...ueMetricsDeclarationValidationStepTests.cs | 2 +- 78 files changed, 1927 insertions(+), 629 deletions(-) create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationContext.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationMessage.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/ErrorReporter.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationContext.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/IErrorReporter.cs create mode 100644 src/Promitor.Core.Scraping/Configuration/Serialization/MessageType.cs create mode 100644 src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs create mode 100644 src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/ValidationTests.cs create mode 100644 src/Promitor.Scraper.Tests.Unit/Serialization/ErrorReporterTests.cs diff --git a/changelog/content/experimental/unreleased.md b/changelog/content/experimental/unreleased.md index 04dc1306c..4ed9f89b1 100644 --- a/changelog/content/experimental/unreleased.md +++ b/changelog/content/experimental/unreleased.md @@ -7,3 +7,4 @@ version: - {{% tag added %}} Support Prometheus service discovery in Helm chart ([#861](https://github.com/tomkerkhove/promitor/issues/861)) - {{% tag added %}} Capability to gain insights on Azure Monitor integration ([docs](http://promitor.io/operations/#azure-monitor-integration) | [#848](https://github.com/tomkerkhove/promitor/issues/848)) +- {{% tag added %}} Improve metrics configuration validation ([#592](https://github.com/tomkerkhove/promitor/issues/592)) diff --git a/docs/configuration/v1.x/metrics/index.md b/docs/configuration/v1.x/metrics/index.md index 3deff1f21..30d2e8867 100644 --- a/docs/configuration/v1.x/metrics/index.md +++ b/docs/configuration/v1.x/metrics/index.md @@ -38,7 +38,6 @@ Every metric that is being declared needs to define the following fields: - `description` - Description for the metric that will be exposed in the scrape endpoint for Prometheus. - `resourceType` - Defines what type of resource needs to be queried. -- `labels` - Defines a set of custom labels to include for a given metric. - `azureMetricConfiguration.metricName` - The name of the metric in Azure Monitor to query - `azureMetricConfiguration.aggregation.type` - The aggregation that needs to be @@ -57,6 +56,7 @@ Additionally, the following fields are optional: - ☝ *Promitor simply acts as a proxy and will not validate if it's supported or not, we recommend verifying that the dimension is supported in the [official documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported)* +- `labels` - Defines a set of custom labels to include for a given metric. - `scraping.schedule` - A scraping schedule for the individual metric; overrides the the one specified in `metricDefaults` diff --git a/src/Promitor.Core.Scraping/Configuration/Providers/Interfaces/IMetricsDeclarationProvider.cs b/src/Promitor.Core.Scraping/Configuration/Providers/Interfaces/IMetricsDeclarationProvider.cs index f3134d261..4cef7ea1b 100644 --- a/src/Promitor.Core.Scraping/Configuration/Providers/Interfaces/IMetricsDeclarationProvider.cs +++ b/src/Promitor.Core.Scraping/Configuration/Providers/Interfaces/IMetricsDeclarationProvider.cs @@ -1,4 +1,5 @@ using Promitor.Core.Scraping.Configuration.Model; +using Promitor.Core.Scraping.Configuration.Serialization; namespace Promitor.Core.Scraping.Configuration.Providers.Interfaces { @@ -9,7 +10,8 @@ public interface IMetricsDeclarationProvider /// /// true if the provider should apply default values from top-level /// configuration elements to metrics where those values aren't specified. false otherwise - MetricsDeclaration Get(bool applyDefaults = false); + /// Used to report errors during the deserialization process. + MetricsDeclaration Get(bool applyDefaults = false, IErrorReporter errorReporter = null); /// /// Gets the serialized metrics declaration diff --git a/src/Promitor.Core.Scraping/Configuration/Providers/MetricsDeclarationProvider.cs b/src/Promitor.Core.Scraping/Configuration/Providers/MetricsDeclarationProvider.cs index f37badd76..da1d4ab98 100644 --- a/src/Promitor.Core.Scraping/Configuration/Providers/MetricsDeclarationProvider.cs +++ b/src/Promitor.Core.Scraping/Configuration/Providers/MetricsDeclarationProvider.cs @@ -19,11 +19,12 @@ public MetricsDeclarationProvider(IConfiguration configuration, ConfigurationSer _configuration = configuration; } - public virtual MetricsDeclaration Get(bool applyDefaults = false) + public virtual MetricsDeclaration Get(bool applyDefaults = false, IErrorReporter errorReporter = null) { var rawMetricsDeclaration = ReadRawDeclaration(); + errorReporter ??= new ErrorReporter(); - var config = _configurationSerializer.Deserialize(rawMetricsDeclaration); + var config = _configurationSerializer.Deserialize(rawMetricsDeclaration, errorReporter); if (applyDefaults) { diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/ConfigurationSerializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/ConfigurationSerializer.cs index 2addb78b2..f3bf50219 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/ConfigurationSerializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/ConfigurationSerializer.cs @@ -26,9 +26,10 @@ public ConfigurationSerializer(ILogger logger, IMapper _v1Deserializer = v1Deserializer; } - public MetricsDeclaration Deserialize(string rawMetricsDeclaration) + public MetricsDeclaration Deserialize(string rawMetricsDeclaration, IErrorReporter errorReporter) { Guard.NotNullOrWhitespace(rawMetricsDeclaration, nameof(rawMetricsDeclaration)); + Guard.NotNull(errorReporter, nameof(errorReporter)); var input = new StringReader(rawMetricsDeclaration); try @@ -36,7 +37,7 @@ public MetricsDeclaration Deserialize(string rawMetricsDeclaration) var metricsDeclarationYamlStream = new YamlStream(); metricsDeclarationYamlStream.Load(input); - var metricsDeclaration = InterpretYamlStream(metricsDeclarationYamlStream); + var metricsDeclaration = InterpretYamlStream(metricsDeclarationYamlStream, errorReporter); return metricsDeclaration; } @@ -46,7 +47,7 @@ public MetricsDeclaration Deserialize(string rawMetricsDeclaration) } } - private MetricsDeclaration InterpretYamlStream(YamlStream metricsDeclarationYamlStream) + private MetricsDeclaration InterpretYamlStream(YamlStream metricsDeclarationYamlStream, IErrorReporter errorReporter) { var document = metricsDeclarationYamlStream.Documents.First(); var rootNode = (YamlMappingNode)document.RootNode; @@ -57,7 +58,7 @@ private MetricsDeclaration InterpretYamlStream(YamlStream metricsDeclarationYaml switch (specVersion) { case SpecVersion.v1: - var v1Config = _v1Deserializer.Deserialize(rootNode); + var v1Config = _v1Deserializer.Deserialize(rootNode, errorReporter); return _mapper.Map(v1Config); default: diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationContext.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationContext.cs new file mode 100644 index 000000000..b62580aa9 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationContext.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Contains the information needed to deserialize a node. + /// + public class DeserializationContext + { + private readonly ISet _ignoredFields; + private readonly Dictionary> _fields; + + /// + /// Initializes a new instance of the type. + /// + /// The fields that should be ignored. + /// The fields that can be deserialized. + public DeserializationContext(ISet ignoredFields, IReadOnlyCollection fields) + { + _ignoredFields = ignoredFields; + _fields = fields.ToDictionary( + fieldInfo => fieldInfo.YamlFieldName, fieldInfo => new FieldDeserializationContext(fieldInfo)); + } + + /// + /// The fields that have not been set during deserialization. + /// + public IReadOnlyCollection> UnsetFields + { + get + { + return _fields + .Where(f => !f.Value.HasBeenSet) + .Select(f => f.Value) + .ToList(); + } + } + + /// + /// Returns whether the specified field is ignored. + /// + /// The field name. + /// true if the field is ignored, false otherwise. + public bool IsIgnored(string fieldName) + { + return _ignoredFields.Contains(fieldName); + } + + /// + /// Tries to find the specified field. + /// + /// The field name. + /// Set to the field if found, otherwise null. + /// true if the field was found, false otherwise. + public bool TryGetField(string fieldName, out FieldDeserializationContext field) + { + return _fields.TryGetValue(fieldName, out field); + } + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationMessage.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationMessage.cs new file mode 100644 index 000000000..bb3711637 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/DeserializationMessage.cs @@ -0,0 +1,48 @@ +using YamlDotNet.RepresentationModel; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Represents a message reported during the deserialization process. + /// + public class DeserializationMessage + { + /// + /// Initializes a new instance of the type. + /// + /// The type of message reported. + /// The node the message is associated with. + /// The message. + public DeserializationMessage(MessageType messageType, YamlNode node, string message) + { + MessageType = messageType; + Node = node; + Message = message; + } + + /// + /// Gets the type of message. + /// + public MessageType MessageType { get; } + + /// + /// Gets the node the message has been reported against. + /// + public YamlNode Node { get; } + + /// + /// Gets the message. + /// + public string Message { get; } + + /// + /// Gets the message formatted for output to users. + /// + public string FormattedMessage => $"{MessageType} {Node.Start.Line}:{Node.Start.Column}: {Message}"; + + public override string ToString() + { + return FormattedMessage; + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs index c48bb0c52..a8c926821 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/Deserializer.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using System.Runtime.Serialization; using GuardNet; using Microsoft.Extensions.Logging; @@ -7,7 +10,11 @@ namespace Promitor.Core.Scraping.Configuration.Serialization { public abstract class Deserializer : IDeserializer + where TObject: new() { + private readonly HashSet _ignoredFields = new HashSet(); + private readonly List _fields = new List(); + protected ILogger Logger { get; } protected Deserializer(ILogger logger) @@ -17,9 +24,47 @@ protected Deserializer(ILogger logger) Logger = logger; } - public abstract TObject Deserialize(YamlMappingNode node); + /// + public virtual TObject Deserialize(YamlMappingNode node, IErrorReporter errorReporter) + { + var result = new TObject(); + var deserializationContext = new DeserializationContext(_ignoredFields, _fields); + + foreach (var child in node.Children) + { + if (deserializationContext.IsIgnored(child.Key.ToString())) + { + continue; + } + + if (deserializationContext.TryGetField(child.Key.ToString(), out var fieldContext)) + { + fieldContext.SetValue( + result, GetFieldValue(child, fieldContext.DeserializationInfo, errorReporter)); + } + else + { + errorReporter.ReportWarning(child.Key, $"Unknown field '{child.Key}'."); + } + } + + foreach (var unsetField in deserializationContext.UnsetFields) + { + if (unsetField.DeserializationInfo.IsRequired) + { + errorReporter.ReportError(node, $"'{unsetField.DeserializationInfo.YamlFieldName}' is a required field but was not found."); + } + else + { + unsetField.SetDefaultValue(result); + } + } + + return result; + } - public List Deserialize(YamlSequenceNode nodes) + /// + public IReadOnlyCollection Deserialize(YamlSequenceNode nodes, IErrorReporter errorReporter) { Guard.NotNull(nodes, nameof(nodes)); @@ -31,11 +76,107 @@ public List Deserialize(YamlSequenceNode nodes) throw new SerializationException($"Failed parsing metrics because we couldn't cast an item to {nameof(YamlMappingNode)}"); } - var deserializedObject = Deserialize(metricNode); + var deserializedObject = Deserialize(metricNode, errorReporter); deserializedObjects.Add(deserializedObject); } return deserializedObjects; } + + public object DeserializeObject(YamlMappingNode node, IErrorReporter errorReporter) + { + return Deserialize(node, errorReporter); + } + + protected void MapRequired(Expression> accessorExpression, Func, IErrorReporter, object> customMapperFunc = null) + { + var memberExpression = (MemberExpression)accessorExpression.Body; + _fields.Add(new FieldDeserializationInfo(memberExpression.Member as PropertyInfo, true, default(TReturn), customMapperFunc)); + } + + protected void MapRequired( + Expression> accessorExpression, IDeserializer deserializer) + where TReturn: new() + { + var memberExpression = (MemberExpression)accessorExpression.Body; + _fields.Add(new FieldDeserializationInfo( + memberExpression.Member as PropertyInfo, true, default(TReturn), null, deserializer)); + } + + protected void MapOptional( + Expression> accessorExpression, TReturn defaultValue = default, Func, IErrorReporter, object> customMapperFunc = null) + { + var memberExpression = (MemberExpression)accessorExpression.Body; + _fields.Add(new FieldDeserializationInfo(memberExpression.Member as PropertyInfo, false, defaultValue, customMapperFunc)); + } + + protected void MapOptional( + Expression> accessorExpression, IDeserializer deserializer) + where TReturn: new() + { + var memberExpression = (MemberExpression)accessorExpression.Body; + _fields.Add(new FieldDeserializationInfo( + memberExpression.Member as PropertyInfo, false, default(TReturn), null, deserializer)); + } + + protected void IgnoreField(string fieldName) + { + _ignoredFields.Add(fieldName); + } + + private static object GetFieldValue( + KeyValuePair fieldNodePair, FieldDeserializationInfo fieldDeserializationInfo, IErrorReporter errorReporter) + { + if (fieldDeserializationInfo.CustomMapperFunc != null) + { + return fieldDeserializationInfo.CustomMapperFunc(fieldNodePair.Value.ToString(), fieldNodePair, errorReporter); + } + + if (fieldDeserializationInfo.Deserializer != null) + { + return fieldDeserializationInfo.Deserializer.DeserializeObject((YamlMappingNode)fieldNodePair.Value, errorReporter); + } + + var propertyType = Nullable.GetUnderlyingType(fieldDeserializationInfo.PropertyInfo.PropertyType) ?? fieldDeserializationInfo.PropertyInfo.PropertyType; + if (propertyType.IsEnum) + { + var parseSucceeded = System.Enum.TryParse( + propertyType, fieldNodePair.Value.ToString(), out var enumValue); + + if (!parseSucceeded) + { + errorReporter.ReportError(fieldNodePair.Value, $"'{fieldNodePair.Value}' is not a valid value for '{fieldNodePair.Key}'."); + } + + return enumValue; + } + + if (typeof(IDictionary).IsAssignableFrom(propertyType)) + { + return ((YamlMappingNode)fieldNodePair.Value).GetDictionary(); + } + + if (propertyType == typeof(TimeSpan)) + { + var parseSucceeded = TimeSpan.TryParse(fieldNodePair.Value.ToString(), out var timeSpanValue); + if (!parseSucceeded) + { + errorReporter.ReportError(fieldNodePair.Value, $"'{fieldNodePair.Value}' is not a valid value for '{fieldNodePair.Key}'. The value must be in the format 'hh:mm:ss'."); + } + + return timeSpanValue; + } + + try + { + return Convert.ChangeType(fieldNodePair.Value.ToString(), fieldDeserializationInfo.PropertyInfo.PropertyType); + } + catch (FormatException) + { + errorReporter.ReportError(fieldNodePair.Value, $"'{fieldNodePair.Value}' is not a valid value for '{fieldNodePair.Key}'. The value must be of type {fieldDeserializationInfo.PropertyInfo.PropertyType}."); + } + + return null; + } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/ErrorReporter.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/ErrorReporter.cs new file mode 100644 index 000000000..94dae86f4 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/ErrorReporter.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using GuardNet; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + public class ErrorReporter : IErrorReporter + { + private readonly List _messages = new List(); + + /// + public IReadOnlyCollection Messages => _messages.OrderBy(m => m.Node.Start.Line).ToList(); + + /// + public bool HasErrors => Messages.Any(m => m.MessageType == MessageType.Error); + + /// + public void ReportError(YamlNode node, string message) + { + Guard.NotNull(node, nameof(node)); + Guard.NotNullOrEmpty(message, nameof(message)); + + _messages.Add(new DeserializationMessage(MessageType.Error, node, message)); + } + + /// + public void ReportWarning(YamlNode node, string message) + { + Guard.NotNull(node, nameof(node)); + Guard.NotNullOrEmpty(message, nameof(message)); + + _messages.Add(new DeserializationMessage(MessageType.Warning, node, message)); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationContext.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationContext.cs new file mode 100644 index 000000000..29cf85eec --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationContext.cs @@ -0,0 +1,53 @@ +using GuardNet; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Used to store information about a field while it is being deserialized. + /// This object is not designed to be reused when deserializing multiple Yaml + /// nodes, and a new copy should be created each time an object is deserialized. + /// + public class FieldDeserializationContext + { + /// + /// Initializes a new instance of the type. + /// + /// The information about the field to deserialize. + public FieldDeserializationContext(FieldDeserializationInfo deserializationInfo) + { + Guard.NotNull(deserializationInfo, nameof(deserializationInfo)); + + DeserializationInfo = deserializationInfo; + } + + /// + /// Gets the information about the field to deserialize. + /// + public FieldDeserializationInfo DeserializationInfo { get; } + + /// + /// Gets a value indicating whether the field has been set. + /// + public bool HasBeenSet { get; private set; } + + /// + /// Sets the field on the specified target object. + /// + /// The object being deserialized. + /// The value of the field. + public void SetValue(TObject target, object value) + { + DeserializationInfo.PropertyInfo.SetValue(target, value); + HasBeenSet = true; + } + + /// + /// Sets the field to its default value. + /// + /// The object being deserialized. + public void SetDefaultValue(TObject target) + { + DeserializationInfo.PropertyInfo.SetValue(target, DeserializationInfo.DefaultValue); + } + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs new file mode 100644 index 000000000..34f317028 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/FieldDeserializationInfo.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Contains information and helper methods needed to deserialize a particular field. + /// This class should be immutable, and should be able to be reused when deserializing + /// multiple Yaml nodes. + /// + public class FieldDeserializationInfo + { + /// + /// Initializes a new instance of the type. + /// + /// The property being deserialized. + /// Whether the field is required or not. + /// The default value to use for the field if not specified. + /// A custom function to use for getting the value of the field. + /// A deserializer to use to deserialize the field. + public FieldDeserializationInfo(PropertyInfo propertyInfo, bool isRequired, object defaultValue, Func, IErrorReporter, object> customMapperFunc, IDeserializer deserializer = null) + { + YamlFieldName = GetName(propertyInfo); + CustomMapperFunc = customMapperFunc; + PropertyInfo = propertyInfo; + IsRequired = isRequired; + DefaultValue = defaultValue; + Deserializer = deserializer; + } + + /// + /// Gets the Yaml field name. + /// + public string YamlFieldName { get; } + + /// + /// Gets information about the property that the field should be deserialized into. + /// + public PropertyInfo PropertyInfo { get; } + + /// + /// Gets a value indicating whether the field is required or not. + /// + public bool IsRequired { get; } + + /// + /// Gets the default value that should be used for the field if none is supplied. + /// + public object DefaultValue { get; } + + /// + /// Gets a custom function that can be used to deserialize the field. + /// + public Func, IErrorReporter, object> CustomMapperFunc { get; } + + /// + /// Gets a deserializer to use when deserializing the field. + /// + public IDeserializer Deserializer { get; } + + private static string GetName(MemberInfo propertyInfo) + { + return char.ToLowerInvariant(propertyInfo.Name[0]) + propertyInfo.Name.Substring(1); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs index cb51130e4..6096f159c 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/IDeserializer.cs @@ -3,24 +3,40 @@ namespace Promitor.Core.Scraping.Configuration.Serialization { + /// + /// An object that can deserialize a yaml node into an object. + /// + public interface IDeserializer + { + /// + /// Deserializes the specified node. + /// + /// The node to deserialize. + /// Used to report deserialization errors. + /// The deserialized object. + object DeserializeObject(YamlMappingNode node, IErrorReporter errorReporter); + } + /// /// An object that can deserialize a yaml node into an object. /// /// The type of object that can be deserialized. - public interface IDeserializer + public interface IDeserializer : IDeserializer where TObject: new() { /// /// Deserializes the specified node. /// /// The node to deserialize. + /// Used to report deserialization errors. /// The deserialized object. - TObject Deserialize(YamlMappingNode node); + TObject Deserialize(YamlMappingNode node, IErrorReporter errorReporter); /// /// Deserializes an array of elements. /// /// The node to deserialize. + /// Used to report deserialization errors. /// The deserialized objects. - List Deserialize(YamlSequenceNode node); + IReadOnlyCollection Deserialize(YamlSequenceNode node, IErrorReporter errorReporter); } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/IErrorReporter.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/IErrorReporter.cs new file mode 100644 index 000000000..05e19e00e --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/IErrorReporter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Used to report errors discovered during the deserialization process. + /// + public interface IErrorReporter + { + /// + /// Contains the list of messages reported, in order of line number. + /// + IReadOnlyCollection Messages { get; } + + /// + /// Indicates if any errors were reported. + /// + bool HasErrors { get; } + + /// + /// Reports an error. + /// + /// The node containing the error / that should be highlighted to the user. + /// The error message. + void ReportError(YamlNode node, string message); + + /// + /// Reports a warning (i.e. something that doesn't prevent Promitor from functioning). + /// + /// The node containing the error / that should be highlighted to the user. + /// The warning message. + void ReportWarning(YamlNode node, string message); + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/MessageType.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/MessageType.cs new file mode 100644 index 000000000..377cf7628 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/MessageType.cs @@ -0,0 +1,18 @@ +namespace Promitor.Core.Scraping.Configuration.Serialization +{ + /// + /// Defines the type of message reported during deserialization. + /// + public enum MessageType + { + /// + /// The message is a warning, and doesn't prevent Promitor from functioning. + /// + Warning, + + /// + /// The message is an error which means that Promitor cannot use the configuration. + /// + Error + } +} \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/YamlMappingNodeExtensions.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/YamlMappingNodeExtensions.cs index f9a2e1c67..657767c4d 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/YamlMappingNodeExtensions.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/YamlMappingNodeExtensions.cs @@ -40,6 +40,23 @@ public static string GetString(this YamlMappingNode node, string propertyName) return null; } + /// + /// Gets the contents of the node. + /// + /// The node containing the property. + /// The child items of the property as a dictionary. + public static Dictionary GetDictionary(this YamlMappingNode node) + { + var result = new Dictionary(); + + foreach (var (key, value) in node.Children) + { + result[key.ToString()] = value.ToString(); + } + + return result; + } + /// /// Gets the contents of the specified property as a dictionary. /// @@ -50,14 +67,7 @@ public static Dictionary GetDictionary(this YamlMappingNode node { if (node.Children.TryGetValue(propertyName, out var propertyNode)) { - var result = new Dictionary(); - - foreach (var (key, value) in ((YamlMappingNode) propertyNode).Children) - { - result[key.ToString()] = value.ToString(); - } - - return result; + return GetDictionary(((YamlMappingNode)propertyNode)); } return null; @@ -86,14 +96,15 @@ public static Dictionary GetDictionary(this YamlMappingNode node /// The yaml node. /// The name of the property to deserialize. /// The deserializer to use. + /// Used to report information about the deserialization process. /// The deserialized property, or null if the property does not exist. public static TObject DeserializeChild( - this YamlMappingNode node, string propertyName, IDeserializer deserializer) - where TObject: class + this YamlMappingNode node, string propertyName, IDeserializer deserializer, IErrorReporter errorReporter) + where TObject: class, new() { if (node.Children.TryGetValue(propertyName, out var propertyNode)) { - return deserializer.Deserialize((YamlMappingNode)propertyNode); + return deserializer.Deserialize((YamlMappingNode)propertyNode, errorReporter); } return null; diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AggregationDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AggregationDeserializer.cs index d135f5b01..a6bd6b1c1 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AggregationDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AggregationDeserializer.cs @@ -1,35 +1,16 @@ using System; using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class AggregationDeserializer : Deserializer { - private const string IntervalTag = "interval"; - - private readonly TimeSpan _defaultAggregationInterval = TimeSpan.FromMinutes(5); + private static readonly TimeSpan defaultAggregationInterval = TimeSpan.FromMinutes(5); public AggregationDeserializer(ILogger logger) : base(logger) { - } - - public override AggregationV1 Deserialize(YamlMappingNode node) - { - var interval = node.GetTimeSpan(IntervalTag); - - var aggregation = new AggregationV1 {Interval = interval}; - - if (aggregation.Interval == null) - { - aggregation.Interval = _defaultAggregationInterval; - Logger.LogWarning( - "No default aggregation was configured, falling back to {AggregationInterval}", - aggregation.Interval?.ToString("g")); - } - - return aggregation; + MapOptional(aggregation => aggregation.Interval, defaultAggregationInterval); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetadataDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetadataDeserializer.cs index ccea0a97f..10f1d17ea 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetadataDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetadataDeserializer.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Generic; using Microsoft.Azure.Management.ResourceManager.Fluent; using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; @@ -8,50 +8,36 @@ namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class AzureMetadataDeserializer : Deserializer { - private const string CloudTag = "cloud"; - private const string TenantIdTag = "tenantId"; - private const string SubscriptionIdTag = "subscriptionId"; - private const string ResourceGroupNameTag = "resourceGroupName"; - public AzureMetadataDeserializer(ILogger logger) : base(logger) { + MapRequired(metadata => metadata.TenantId); + MapRequired(metadata => metadata.SubscriptionId); + MapRequired(metadata => metadata.ResourceGroupName); + MapOptional(metadata => metadata.Cloud, AzureEnvironment.AzureGlobalCloud, DetermineAzureCloud); } - public override AzureMetadataV1 Deserialize(YamlMappingNode node) + private object DetermineAzureCloud(string rawAzureCloud, KeyValuePair nodePair, IErrorReporter errorReporter) { - var metadata = new AzureMetadataV1(); - - var azureCloud = node.GetEnum(CloudTag); - var cloud = DetermineAzureCloud(azureCloud); - - metadata.TenantId = node.GetString(TenantIdTag); - metadata.SubscriptionId = node.GetString(SubscriptionIdTag); - metadata.ResourceGroupName = node.GetString(ResourceGroupNameTag); - metadata.Cloud = cloud; - - return metadata; - } - - private AzureEnvironment DetermineAzureCloud(AzureCloudsV1? azureCloud) - { - if (azureCloud == null) + if (System.Enum.TryParse(rawAzureCloud, out var azureCloud)) { - return AzureEnvironment.AzureGlobalCloud; + switch (azureCloud) + { + case AzureCloudsV1.Global: + return AzureEnvironment.AzureGlobalCloud; + case AzureCloudsV1.China: + return AzureEnvironment.AzureChinaCloud; + case AzureCloudsV1.Germany: + return AzureEnvironment.AzureGermanCloud; + case AzureCloudsV1.UsGov: + return AzureEnvironment.AzureUSGovernment; + } } - - switch (azureCloud) + else { - case AzureCloudsV1.Global: - return AzureEnvironment.AzureGlobalCloud; - case AzureCloudsV1.China: - return AzureEnvironment.AzureChinaCloud; - case AzureCloudsV1.Germany: - return AzureEnvironment.AzureGermanCloud; - case AzureCloudsV1.UsGov: - return AzureEnvironment.AzureUSGovernment; - default: - throw new ArgumentOutOfRangeException(nameof(azureCloud), azureCloud, $"{azureCloud} is not supported yet"); + errorReporter.ReportError(nodePair.Value, $"'{rawAzureCloud}' is not a valid value for 'cloud'."); } + + return null; } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs index b37b71e1e..130199f81 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureMetricConfigurationDeserializer.cs @@ -1,52 +1,16 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class AzureMetricConfigurationDeserializer : Deserializer { - private const string MetricNameTag = "metricName"; - private const string DimensionTag = "dimension"; - private const string AggregationTag = "aggregation"; - private readonly IDeserializer _dimensionDeserializer; - private readonly IDeserializer _aggregationDeserializer; - public AzureMetricConfigurationDeserializer(IDeserializer dimensionDeserializer, IDeserializer aggregationDeserializer, ILogger logger) : base(logger) { - _dimensionDeserializer = dimensionDeserializer; - _aggregationDeserializer = aggregationDeserializer; - } - - public override AzureMetricConfigurationV1 Deserialize(YamlMappingNode node) - { - return new AzureMetricConfigurationV1 - { - MetricName = node.GetString(MetricNameTag), - Dimension = DeserializeDimension(node), - Aggregation = DeserializeAggregation(node) - }; - } - - private MetricAggregationV1 DeserializeAggregation(YamlMappingNode node) - { - if (node.Children.TryGetValue(AggregationTag, out var aggregationNode)) - { - return _aggregationDeserializer.Deserialize((YamlMappingNode)aggregationNode); - } - - return null; - } - - private MetricDimensionV1 DeserializeDimension(YamlMappingNode node) - { - if (node.Children.TryGetValue(DimensionTag, out var aggregationNode)) - { - return _dimensionDeserializer.Deserialize((YamlMappingNode)aggregationNode); - } - - return null; + MapRequired(config => config.MetricName); + MapOptional(config => config.Dimension, dimensionDeserializer); + MapRequired(config => config.Aggregation, aggregationDeserializer); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricAggregationDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricAggregationDeserializer.cs index 870890263..ed05637fa 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricAggregationDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricAggregationDeserializer.cs @@ -1,30 +1,14 @@ -using Microsoft.Azure.Management.Monitor.Fluent.Models; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class MetricAggregationDeserializer : Deserializer { - private const string TypeTag = "type"; - private const string IntervalTag = "interval"; - public MetricAggregationDeserializer(ILogger logger) : base(logger) { - } - - public override MetricAggregationV1 Deserialize(YamlMappingNode node) - { - var aggregationType = node.GetEnum(TypeTag); - - var interval = node.GetTimeSpan(IntervalTag); - - return new MetricAggregationV1 - { - Type = aggregationType, - Interval = interval - }; + MapRequired(aggregation => aggregation.Type); + MapOptional(aggregation => aggregation.Interval); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefaultsDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefaultsDeserializer.cs index e754cdf8d..68e3fcfd3 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefaultsDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefaultsDeserializer.cs @@ -1,34 +1,17 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class MetricDefaultsDeserializer : Deserializer { - private const string AggregationTag = "aggregation"; - private const string ScrapingTag = "scraping"; - - private readonly IDeserializer _aggregationDeserializer; - private readonly IDeserializer _scrapingDeserializer; - public MetricDefaultsDeserializer( IDeserializer aggregationDeserializer, IDeserializer scrapingDeserializer, ILogger logger) : base(logger) { - _aggregationDeserializer = aggregationDeserializer; - _scrapingDeserializer = scrapingDeserializer; - } - - public override MetricDefaultsV1 Deserialize(YamlMappingNode node) - { - var defaults = new MetricDefaultsV1(); - - defaults.Aggregation = node.DeserializeChild(AggregationTag, _aggregationDeserializer); - defaults.Scraping = node.DeserializeChild(ScrapingTag, _scrapingDeserializer); - - return defaults; + MapOptional(defaults => defaults.Aggregation, aggregationDeserializer); + MapRequired(defaults => defaults.Scraping, scrapingDeserializer); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefinitionDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefinitionDeserializer.cs index 6c75c0c3a..7b316661d 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefinitionDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDefinitionDeserializer.cs @@ -7,16 +7,7 @@ namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class MetricDefinitionDeserializer : Deserializer { - private const string NameTag = "name"; - private const string DescriptionTag = "description"; - private const string ResourceTypeTag = "resourceType"; - private const string LabelsTag = "labels"; - private const string AzureMetricConfigurationTag = "azureMetricConfiguration"; - private const string ScrapingTag = "scraping"; private const string ResourcesTag = "resources"; - - private readonly IDeserializer _azureMetricConfigurationDeserializer; - private readonly IDeserializer _scrapingDeserializer; private readonly IAzureResourceDeserializerFactory _azureResourceDeserializerFactory; public MetricDefinitionDeserializer(IDeserializer azureMetricConfigurationDeserializer, @@ -24,58 +15,55 @@ public MetricDefinitionDeserializer(IDeserializer az IAzureResourceDeserializerFactory azureResourceDeserializerFactory, ILogger logger) : base(logger) { - _azureMetricConfigurationDeserializer = azureMetricConfigurationDeserializer; - _scrapingDeserializer = scrapingDeserializer; _azureResourceDeserializerFactory = azureResourceDeserializerFactory; + + MapRequired(definition => definition.Name); + MapRequired(definition => definition.Description); + MapRequired(definition => definition.ResourceType); + MapOptional(definition => definition.Labels); + MapRequired(definition => definition.AzureMetricConfiguration, azureMetricConfigurationDeserializer); + MapOptional(definition => definition.Scraping, scrapingDeserializer); + IgnoreField(ResourcesTag); } - public override MetricDefinitionV1 Deserialize(YamlMappingNode node) + public override MetricDefinitionV1 Deserialize(YamlMappingNode node, IErrorReporter errorReporter) { - var name = node.GetString(NameTag); - var description = node.GetString(DescriptionTag); - var resourceType = node.GetEnum(ResourceTypeTag); - var labels = node.GetDictionary(LabelsTag); + var metricDefinition = base.Deserialize(node, errorReporter); - var metricDefinition = new MetricDefinitionV1 - { - Name = name, - Description = description, - ResourceType = resourceType, - Labels = labels - }; - - DeserializeAzureMetricConfiguration(node, metricDefinition); - DeserializeScraping(node, metricDefinition); - DeserializeMetrics(node, metricDefinition); + DeserializeMetrics(node, metricDefinition, errorReporter); return metricDefinition; } - private void DeserializeAzureMetricConfiguration(YamlMappingNode node, MetricDefinitionV1 metricDefinition) + private void DeserializeMetrics(YamlMappingNode node, MetricDefinitionV1 metricDefinition, IErrorReporter errorReporter) { - if (node.Children.TryGetValue(AzureMetricConfigurationTag, out var configurationNode)) + if (metricDefinition.ResourceType == null) { - metricDefinition.AzureMetricConfiguration = - _azureMetricConfigurationDeserializer.Deserialize((YamlMappingNode) configurationNode); + return; } - } - private void DeserializeScraping(YamlMappingNode node, MetricDefinitionV1 metricDefinition) - { - if (node.Children.TryGetValue(ScrapingTag, out var scrapingNode)) + var resourceTypeNode = node.Children["resourceType"]; + if (metricDefinition.ResourceType == ResourceType.NotSpecified) { - metricDefinition.Scraping = _scrapingDeserializer.Deserialize((YamlMappingNode)scrapingNode); + errorReporter.ReportError(resourceTypeNode, "'resourceType' must not be set to 'NotSpecified'."); + return; } - } - private void DeserializeMetrics(YamlMappingNode node, MetricDefinitionV1 metricDefinition) - { - if (metricDefinition.ResourceType != null && - metricDefinition.ResourceType != ResourceType.NotSpecified && - node.Children.TryGetValue(ResourcesTag, out var metricsNode)) + if (node.Children.TryGetValue(ResourcesTag, out var metricsNode)) { var resourceDeserializer = _azureResourceDeserializerFactory.GetDeserializerFor(metricDefinition.ResourceType.Value); - metricDefinition.Resources = resourceDeserializer.Deserialize((YamlSequenceNode)metricsNode); + if (resourceDeserializer != null) + { + metricDefinition.Resources = resourceDeserializer.Deserialize((YamlSequenceNode)metricsNode, errorReporter); + } + else + { + errorReporter.ReportError(resourceTypeNode, $"Could not find a deserializer for resource type '{metricDefinition.ResourceType}'."); + } + } + else + { + errorReporter.ReportError(node, "'resources' is a required field but was not found."); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDimensionDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDimensionDeserializer.cs index 1870a9362..93bfe2cd1 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDimensionDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/MetricDimensionDeserializer.cs @@ -13,7 +13,7 @@ public MetricDimensionDeserializer(ILogger logger) { } - public override MetricDimensionV1 Deserialize(YamlMappingNode node) + public override MetricDimensionV1 Deserialize(YamlMappingNode node, IErrorReporter errorReporter) { return new MetricDimensionV1 { diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/ScrapingDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/ScrapingDeserializer.cs index 5b94c4290..475cbde47 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/ScrapingDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/ScrapingDeserializer.cs @@ -1,29 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class ScrapingDeserializer : Deserializer { - private const string ScheduleTag = "schedule"; - public ScrapingDeserializer(ILogger logger) : base(logger) { - } - - public override ScrapingV1 Deserialize(YamlMappingNode node) - { - var scraping = new ScrapingV1(); - - scraping.Schedule = node.GetString(ScheduleTag); - - if (scraping.Schedule == null) - { - Logger.LogError("No default metric scraping schedule was configured!"); - } - - return scraping; + MapRequired(scraping => scraping.Schedule); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/SecretDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/SecretDeserializer.cs index d55482700..260d9328d 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/SecretDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/SecretDeserializer.cs @@ -6,27 +6,24 @@ namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Core { public class SecretDeserializer : Deserializer { - private const string RawValueTag = "rawValue"; - private const string EnvironmentVariableTag = "environmentVariable"; - public SecretDeserializer(ILogger logger) : base(logger) { + MapOptional(secret => secret.RawValue); + MapOptional(secret => secret.EnvironmentVariable); } - public override SecretV1 Deserialize(YamlMappingNode node) + public override SecretV1 Deserialize(YamlMappingNode node, IErrorReporter errorReporter) { - var rawValue = node.GetString(RawValueTag); - var environmentVariable = node.GetString(EnvironmentVariableTag); + var secret = base.Deserialize(node, errorReporter); - var secret = new SecretV1 + if (string.IsNullOrEmpty(secret.EnvironmentVariable) && string.IsNullOrEmpty(secret.RawValue)) { - RawValue = rawValue, - EnvironmentVariable = environmentVariable - }; + errorReporter.ReportError(node, "Either 'environmentVariable' or 'rawValue' must be supplied for a secret."); + } if (!string.IsNullOrEmpty(secret.RawValue) && !string.IsNullOrEmpty(secret.EnvironmentVariable)) { - Logger.LogWarning("Secret with environment variable '{EnvironmentVariable}' also has a rawValue provided.", secret.EnvironmentVariable); + errorReporter.ReportWarning(node, $"Secret with environment variable '{secret.EnvironmentVariable}' also has a rawValue provided."); } return secret; diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/V1Deserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/V1Deserializer.cs index 67df4e73e..401e7857f 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/V1Deserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/V1Deserializer.cs @@ -22,13 +22,13 @@ public V1Deserializer(IDeserializer azureMetadataDeserializer, _metricsDeserializer = metricsDeserializer; } - public override MetricsDeclarationV1 Deserialize(YamlMappingNode rootNode) + public override MetricsDeclarationV1 Deserialize(YamlMappingNode rootNode, IErrorReporter errorReporter) { ValidateVersion(rootNode); - var azureMetadata = DeserializeAzureMetadata(rootNode); - var metricDefaults = DeserializeMetricDefaults(rootNode); - var metrics = DeserializeMetrics(rootNode); + var azureMetadata = DeserializeAzureMetadata(rootNode, errorReporter); + var metricDefaults = DeserializeMetricDefaults(rootNode, errorReporter); + var metrics = DeserializeMetrics(rootNode, errorReporter); return new MetricsDeclarationV1 { @@ -53,31 +53,31 @@ private static void ValidateVersion(YamlMappingNode rootNode) } } - private AzureMetadataV1 DeserializeAzureMetadata(YamlMappingNode rootNode) + private AzureMetadataV1 DeserializeAzureMetadata(YamlMappingNode rootNode, IErrorReporter errorReporter) { if (rootNode.Children.TryGetValue("azureMetadata", out var azureMetadataNode)) { - return _azureMetadataDeserializer.Deserialize((YamlMappingNode)azureMetadataNode); + return _azureMetadataDeserializer.Deserialize((YamlMappingNode)azureMetadataNode, errorReporter); } return null; } - private MetricDefaultsV1 DeserializeMetricDefaults(YamlMappingNode rootNode) + private MetricDefaultsV1 DeserializeMetricDefaults(YamlMappingNode rootNode, IErrorReporter errorReporter) { if (rootNode.Children.TryGetValue("metricDefaults", out var defaultsNode)) { - return _defaultsDeserializer.Deserialize((YamlMappingNode)defaultsNode); + return _defaultsDeserializer.Deserialize((YamlMappingNode)defaultsNode, errorReporter); } return null; } - private List DeserializeMetrics(YamlMappingNode rootNode) + private IReadOnlyCollection DeserializeMetrics(YamlMappingNode rootNode, IErrorReporter errorReporter) { if (rootNode.Children.TryGetValue("metrics", out var metricsNode)) { - return _metricsDeserializer.Deserialize((YamlSequenceNode)metricsNode); + return _metricsDeserializer.Deserialize((YamlSequenceNode)metricsNode, errorReporter); } return null; diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricDefinitionV1.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricDefinitionV1.cs index 87f3eecf3..b8025377c 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricDefinitionV1.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricDefinitionV1.cs @@ -42,6 +42,6 @@ public class MetricDefinitionV1 /// /// The resources to be scraped. /// - public List Resources { get; set; } + public IReadOnlyCollection Resources { get; set; } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricsDeclarationV1.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricsDeclarationV1.cs index e1dc6891d..8aa31d018 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricsDeclarationV1.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/MetricsDeclarationV1.cs @@ -11,6 +11,6 @@ public class MetricsDeclarationV1 public string Version { get; set; } = SpecVersion.v1.ToString(); public AzureMetadataV1 AzureMetadata { get; set; } public MetricDefaultsV1 MetricDefaults { get; set; } - public List Metrics { get; set; } + public IReadOnlyCollection Metrics { get; set; } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ApiManagementDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ApiManagementDeserializer.cs index a264120c4..bdddcd7b5 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ApiManagementDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ApiManagementDeserializer.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { @@ -15,18 +14,8 @@ public class ApiManagementDeserializer : ResourceDeserializerThe logger. public ApiManagementDeserializer(ILogger logger) : base(logger) { - } - - protected override ApiManagementResourceV1 DeserializeResource(YamlMappingNode node) - { - var instanceName = node.GetString("instanceName"); - var locationName = node.GetString("locationName"); - - return new ApiManagementResourceV1 - { - InstanceName = instanceName, - LocationName = locationName - }; + MapRequired(resource => resource.InstanceName); + MapOptional(resource => resource.LocationName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AppPlanDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AppPlanDeserializer.cs index cb30fd583..1b0cf70a3 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AppPlanDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AppPlanDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class AppPlanDeserializer : ResourceDeserializer { - private const string AppPlanNameTag = "appPlanName"; - public AppPlanDeserializer(ILogger logger) : base(logger) { - } - - protected override AppPlanResourceV1 DeserializeResource(YamlMappingNode node) - { - var appPlanName = node.GetString(AppPlanNameTag); - - return new AppPlanResourceV1 - { - AppPlanName= appPlanName - }; + MapRequired(resource => resource.AppPlanName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/BlobStorageDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/BlobStorageDeserializer.cs index 4370d6bf7..252f3244c 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/BlobStorageDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/BlobStorageDeserializer.cs @@ -1,20 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { - public class BlobStorageDeserializer : StorageAccountDeserializer + public class BlobStorageDeserializer : ResourceDeserializer { public BlobStorageDeserializer(ILogger logger) : base(logger) { - } - - protected override StorageAccountResourceV1 DeserializeResource(YamlMappingNode node) - { - var storageAccountResource = base.DeserializeResource(node); - - return new BlobStorageResourceV1(storageAccountResource); + MapRequired(resource => resource.AccountName); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerInstanceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerInstanceDeserializer.cs index a0e414dad..fb5dac005 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerInstanceDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerInstanceDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class ContainerInstanceDeserializer : ResourceDeserializer { - private const string ContainerGroupTag = "containerGroup"; - public ContainerInstanceDeserializer(ILogger logger) : base(logger) { - } - - protected override ContainerInstanceResourceV1 DeserializeResource(YamlMappingNode node) - { - var containerGroup = node.GetString(ContainerGroupTag); - - return new ContainerInstanceResourceV1 - { - ContainerGroup = containerGroup - }; + MapRequired(resource => resource.ContainerGroup); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerRegistryDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerRegistryDeserializer.cs index 06d8b8be4..072246514 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerRegistryDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ContainerRegistryDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class ContainerRegistryDeserializer : ResourceDeserializer { - private const string RegistryNameTag = "registryName"; - public ContainerRegistryDeserializer(ILogger logger) : base(logger) { - } - - protected override ContainerRegistryResourceV1 DeserializeResource(YamlMappingNode node) - { - var registryName = node.GetString(RegistryNameTag); - - return new ContainerRegistryResourceV1 - { - RegistryName = registryName - }; + MapRequired(resource => resource.RegistryName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/CosmosDbDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/CosmosDbDeserializer.cs index f621717f3..3a72b9dbd 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/CosmosDbDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/CosmosDbDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class CosmosDbDeserializer : ResourceDeserializer { - private const string DatabaseNameTag = "dbName"; - public CosmosDbDeserializer(ILogger logger) : base(logger) { - } - - protected override CosmosDbResourceV1 DeserializeResource(YamlMappingNode node) - { - var databaseName = node.GetString(DatabaseNameTag); - - return new CosmosDbResourceV1 - { - DbName = databaseName - }; + MapRequired(resource => resource.DbName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FileStorageDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FileStorageDeserializer.cs index 1248fcf9c..8b3657313 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FileStorageDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FileStorageDeserializer.cs @@ -1,20 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { - public class FileStorageDeserializer : StorageAccountDeserializer + public class FileStorageDeserializer : ResourceDeserializer { public FileStorageDeserializer(ILogger logger) : base(logger) { - } - - protected override StorageAccountResourceV1 DeserializeResource(YamlMappingNode node) - { - var storageAccountResource = base.DeserializeResource(node); - - return new FileStorageResourceV1(storageAccountResource); + MapRequired(resource => resource.AccountName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FunctionAppDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FunctionAppDeserializer.cs index 9898f9c88..922fae8e8 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FunctionAppDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/FunctionAppDeserializer.cs @@ -1,28 +1,14 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class FunctionAppDeserializer : ResourceDeserializer { - private const string FunctionAppNameTag = "functionAppName"; - private const string SlotNameTag = "slotName"; - public FunctionAppDeserializer(ILogger logger) : base(logger) { - } - - protected override FunctionAppResourceV1 DeserializeResource(YamlMappingNode node) - { - var functionAppName = node.GetString(FunctionAppNameTag); - var slotName = node.GetString(SlotNameTag); - - return new FunctionAppResourceV1 - { - FunctionAppName = functionAppName, - SlotName = slotName - }; + MapRequired(resource => resource.FunctionAppName); + MapOptional(resource => resource.SlotName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/GenericResourceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/GenericResourceDeserializer.cs index 588445d5c..241300d88 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/GenericResourceDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/GenericResourceDeserializer.cs @@ -1,28 +1,14 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class GenericResourceDeserializer : ResourceDeserializer { - private const string FilterTag = "filter"; - private const string ResourceUriTag = "resourceUri"; - public GenericResourceDeserializer(ILogger logger) : base(logger) { - } - - protected override GenericResourceV1 DeserializeResource(YamlMappingNode node) - { - var filter = node.GetString(FilterTag); - var resourceUri = node.GetString(ResourceUriTag); - - return new GenericResourceV1 - { - Filter = filter, - ResourceUri = resourceUri - }; + MapRequired(resource => resource.ResourceUri); + MapOptional(resource => resource.Filter); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/NetworkInterfaceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/NetworkInterfaceDeserializer.cs index 51b5ecdfd..883f09ba9 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/NetworkInterfaceDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/NetworkInterfaceDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class NetworkInterfaceDeserializer : ResourceDeserializer { - private const string NetworkInterfaceNameTag = "networkInterfaceName"; - public NetworkInterfaceDeserializer(ILogger logger) : base(logger) { - } - - protected override NetworkInterfaceResourceV1 DeserializeResource(YamlMappingNode node) - { - var networkInterfaceName = node.GetString(NetworkInterfaceNameTag); - - return new NetworkInterfaceResourceV1 - { - NetworkInterfaceName = networkInterfaceName - }; + MapRequired(resource => resource.NetworkInterfaceName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/PostgreSqlDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/PostgreSqlDeserializer.cs index d0e9a9cd4..1034e6051 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/PostgreSqlDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/PostgreSqlDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class PostgreSqlDeserializer : ResourceDeserializer { - private const string ServerNameTag = "serverName"; - public PostgreSqlDeserializer(ILogger logger) : base(logger) { - } - - protected override PostgreSqlResourceV1 DeserializeResource(YamlMappingNode node) - { - var serverName = node.GetString(ServerNameTag); - - return new PostgreSqlResourceV1 - { - ServerName = serverName - }; + MapRequired(resource => resource.ServerName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/RedisCacheDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/RedisCacheDeserializer.cs index 1c90c3e6d..6c1593a94 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/RedisCacheDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/RedisCacheDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class RedisCacheDeserializer : ResourceDeserializer { - private const string CacheNameTag = "cacheName"; - public RedisCacheDeserializer(ILogger logger) : base(logger) { - } - - protected override RedisCacheResourceV1 DeserializeResource(YamlMappingNode node) - { - var cacheName = node.GetString(CacheNameTag); - - return new RedisCacheResourceV1 - { - CacheName = cacheName - }; + MapRequired(resource => resource.CacheName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ResourceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ResourceDeserializer.cs index 3e0405def..ea166c700 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ResourceDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ResourceDeserializer.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { @@ -8,30 +7,12 @@ namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers /// A base class for azure resource deserializers that makes sure that any shared /// properties are deserialized correctly for all resources. /// - public abstract class ResourceDeserializer : Deserializer - where TResourceDefinition : AzureResourceDefinitionV1 + public abstract class ResourceDeserializer : Deserializer + where TResourceDefinition : AzureResourceDefinitionV1, new() { - private const string ResourceGroupNameTag = "resourceGroupName"; - protected ResourceDeserializer(ILogger logger) : base(logger) { + MapOptional(resource => resource.ResourceGroupName); } - - public override AzureResourceDefinitionV1 Deserialize(YamlMappingNode node) - { - var resource = DeserializeResource(node); - - resource.ResourceGroupName = node.GetString(ResourceGroupNameTag); - - return resource; - } - - /// - /// Implement on subclasses to return the correct type of - /// object with all its custom properties populated. - /// - /// The yaml node. - /// The deserialized object. - protected abstract TResourceDefinition DeserializeResource(YamlMappingNode node); } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ServiceBusQueueDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ServiceBusQueueDeserializer.cs index acbfb4c35..2193e3eeb 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ServiceBusQueueDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/ServiceBusQueueDeserializer.cs @@ -1,28 +1,14 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class ServiceBusQueueDeserializer : ResourceDeserializer { - private const string QueueNameTag = "queueName"; - private const string NamespaceTag = "namespace"; - public ServiceBusQueueDeserializer(ILogger logger) : base(logger) { - } - - protected override ServiceBusQueueResourceV1 DeserializeResource(YamlMappingNode node) - { - var queueName = node.GetString(QueueNameTag); - var @namespace = node.GetString(NamespaceTag); - - return new ServiceBusQueueResourceV1 - { - QueueName = queueName, - Namespace = @namespace - }; + MapRequired(resource => resource.QueueName); + MapRequired(resource => resource.Namespace); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlDatabaseDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlDatabaseDeserializer.cs index 597797570..b39c2b927 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlDatabaseDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlDatabaseDeserializer.cs @@ -1,13 +1,12 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { /// /// Used to deserialize a resource. /// - public class SqlDatabaseDeserializer : SqlServerDeserializer + public class SqlDatabaseDeserializer : ResourceDeserializer { /// /// Initializes a new instance of the class. @@ -15,16 +14,8 @@ public class SqlDatabaseDeserializer : SqlServerDeserializer /// The logger. public SqlDatabaseDeserializer(ILogger logger) : base(logger) { - } - - protected override SqlServerResourceV1 DeserializeResource(YamlMappingNode node) - { - var sqlServerResource = base.DeserializeResource(node); - - return new SqlDatabaseResourceV1(sqlServerResource) - { - DatabaseName = node.GetString("databaseName") - }; + MapRequired(resource => resource.ServerName); + MapRequired(resource => resource.DatabaseName); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlManagedInstanceDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlManagedInstanceDeserializer.cs index e9743c278..eea1c21ac 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlManagedInstanceDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlManagedInstanceDeserializer.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { @@ -15,16 +14,7 @@ public class SqlManagedInstanceDeserializer : ResourceDeserializerThe logger. public SqlManagedInstanceDeserializer(ILogger logger) : base(logger) { - } - - protected override SqlManagedInstanceResourceV1 DeserializeResource(YamlMappingNode node) - { - var instanceName = node.GetString("instanceName"); - - return new SqlManagedInstanceResourceV1 - { - InstanceName = instanceName - }; + MapRequired(resource => resource.InstanceName); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlServerDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlServerDeserializer.cs index 9a9fe51fa..f69f5a257 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlServerDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/SqlServerDeserializer.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { @@ -15,16 +14,7 @@ public class SqlServerDeserializer : ResourceDeserializer /// The logger. public SqlServerDeserializer(ILogger logger) : base(logger) { - } - - protected override SqlServerResourceV1 DeserializeResource(YamlMappingNode node) - { - var serverName = node.GetString("serverName"); - - return new SqlServerResourceV1 - { - ServerName = serverName - }; + MapRequired(resource => resource.ServerName); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageAccountDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageAccountDeserializer.cs index 6ce04f3d1..c0007eb5e 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageAccountDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageAccountDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class StorageAccountDeserializer : ResourceDeserializer { - private const string AccountNameTag = "accountName"; - public StorageAccountDeserializer(ILogger logger) : base(logger) { - } - - protected override StorageAccountResourceV1 DeserializeResource(YamlMappingNode node) - { - var accountName = node.GetString(AccountNameTag); - - return new StorageAccountResourceV1 - { - AccountName = accountName - }; + MapRequired(resource => resource.AccountName); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageQueueDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageQueueDeserializer.cs index 3773193a1..ebd8c80ca 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageQueueDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/StorageQueueDeserializer.cs @@ -1,36 +1,16 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { - public class StorageQueueDeserializer : StorageAccountDeserializer + public class StorageQueueDeserializer : ResourceDeserializer { - private const string QueueNameTag = "queueName"; - private const string SasTokenTag = "sasToken"; - - private readonly IDeserializer _secretDeserializer; - public StorageQueueDeserializer(IDeserializer secretDeserializer, ILogger logger) : base(logger) { - _secretDeserializer = secretDeserializer; - } - - protected override StorageAccountResourceV1 DeserializeResource(YamlMappingNode node) - { - var storageAccountResource = base.DeserializeResource(node); - - var queueName = node.GetString(QueueNameTag); - var sasToken = node.DeserializeChild(SasTokenTag, _secretDeserializer); - - var storageQueueResource = new StorageQueueResourceV1(storageAccountResource) - { - QueueName = queueName, - SasToken = sasToken - }; - - return storageQueueResource; + MapRequired(resource => resource.AccountName); + MapRequired(resource => resource.QueueName); + MapRequired(resource => resource.SasToken, secretDeserializer); } } } \ No newline at end of file diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineDeserializer.cs index bd4ae2c43..927b22c08 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class VirtualMachineDeserializer : ResourceDeserializer { - private const string VirtualMachineNameTag = "virtualMachineName"; - public VirtualMachineDeserializer(ILogger logger) : base(logger) { - } - - protected override VirtualMachineResourceV1 DeserializeResource(YamlMappingNode node) - { - var virtualMachineName = node.GetString(VirtualMachineNameTag); - - return new VirtualMachineResourceV1 - { - VirtualMachineName = virtualMachineName - }; + MapRequired(resource => resource.VirtualMachineName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineScaleSetDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineScaleSetDeserializer.cs index a87aa5b29..a270c1a77 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineScaleSetDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/VirtualMachineScaleSetDeserializer.cs @@ -1,25 +1,13 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class VirtualMachineScaleSetDeserializer : ResourceDeserializer { - private const string ScaleSetNameTag = "scaleSetName"; - public VirtualMachineScaleSetDeserializer(ILogger logger) : base(logger) { - } - - protected override VirtualMachineScaleSetResourceV1 DeserializeResource(YamlMappingNode node) - { - var scaleSetName = node.GetString(ScaleSetNameTag); - - return new VirtualMachineScaleSetResourceV1 - { - ScaleSetName = scaleSetName - }; + MapRequired(resource => resource.ScaleSetName); } } } diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/WebAppDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/WebAppDeserializer.cs index 221f60caf..fb9fe3632 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/WebAppDeserializer.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/WebAppDeserializer.cs @@ -1,28 +1,14 @@ using Microsoft.Extensions.Logging; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; -using YamlDotNet.RepresentationModel; namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers { public class WebAppDeserializer : ResourceDeserializer { - private const string WebAppNameTag = "webAppName"; - private const string SlotNameTag = "slotName"; - public WebAppDeserializer(ILogger logger) : base(logger) { - } - - protected override WebAppResourceV1 DeserializeResource(YamlMappingNode node) - { - var webAppName = node.GetString(WebAppNameTag); - var slotName = node.GetString(SlotNameTag); - - return new WebAppResourceV1 - { - WebAppName = webAppName, - SlotName = slotName, - }; + MapRequired(resource => resource.WebAppName); + MapOptional(resource => resource.SlotName); } } } diff --git a/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs b/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs index dff34f3fd..8e2bafcc1 100644 --- a/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs +++ b/src/Promitor.Scraper.Host/Validation/Steps/MetricsDeclarationValidationStep.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -6,6 +7,7 @@ using Promitor.Core.Scraping.Configuration.Model.Metrics; using Promitor.Core.Scraping.Configuration.Model.Metrics.ResourceTypes; using Promitor.Core.Scraping.Configuration.Providers.Interfaces; +using Promitor.Core.Scraping.Configuration.Serialization; using Promitor.Core.Serialization.Yaml; using Promitor.Scraper.Host.Validation.Interfaces; using Promitor.Scraper.Host.Validation.MetricDefinitions; @@ -29,12 +31,20 @@ public MetricsDeclarationValidationStep(IMetricsDeclarationProvider metricsDecla public ValidationResult Run() { - var metricsDeclaration = _metricsDeclarationProvider.Get(applyDefaults: true); + var errorReporter = new ErrorReporter(); + var metricsDeclaration = _metricsDeclarationProvider.Get(applyDefaults: true, errorReporter: errorReporter); if (metricsDeclaration == null) { return ValidationResult.Failure(ComponentName, "Unable to deserialize configured metrics declaration"); } + LogDeserializationMessages(errorReporter); + + if (errorReporter.HasErrors) + { + return ValidationResult.Failure(ComponentName, "Errors were found while deserializing the metric configuration."); + } + LogMetricsDeclaration(metricsDeclaration); var validationErrors = new List(); @@ -50,6 +60,25 @@ public ValidationResult Run() return validationErrors.Any() ? ValidationResult.Failure(ComponentName, validationErrors) : ValidationResult.Successful(ComponentName); } + private void LogDeserializationMessages(IErrorReporter errorReporter) + { + if (errorReporter.Messages.Any()) + { + var combinedMessages = string.Join( + Environment.NewLine, errorReporter.Messages.Select(message => message.FormattedMessage)); + + var deserializationProblemsMessage = $"The following problems were found with the metric configuration:{Environment.NewLine}{combinedMessages}"; + if (errorReporter.HasErrors) + { + Logger.LogError(deserializationProblemsMessage); + } + else + { + Logger.LogWarning(deserializationProblemsMessage); + } + } + } + private void LogMetricsDeclaration(MetricsDeclaration metricsDeclaration) { metricsDeclaration.Metrics.ForEach(SanitizeStorageQueueDeclaration); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs new file mode 100644 index 000000000..8ed363e41 --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/DeserializationTests.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Promitor.Core.Scraping.Configuration.Serialization; +using Xunit; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Scraper.Tests.Unit.Serialization.DeserializerTests +{ + public class DeserializationTests + { + private static readonly TimeSpan defaultInterval = TimeSpan.FromMinutes(5); + + private readonly Mock _errorReporter = new Mock(); + private readonly Mock _childDeserializer = new Mock(); + private readonly RegistrationConfigDeserializer _deserializer; + + public DeserializationTests() + { + _deserializer = new RegistrationConfigDeserializer(_childDeserializer.Object); + } + + [Fact] + public void Deserialize_RequiredFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal("Promitor", result.Name); + } + + [Fact] + public void Deserialize_OptionalFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("town: Glasgow"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal("Glasgow", result.Town); + } + + [Fact] + public void Deserialize_StringFieldNotSupplied_Null() + { + // Arrange + var node = YamlUtils.CreateYamlNode("age: 17"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Null(result.Name); + } + + [Fact] + public void Deserialize_IntFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("age: 22"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(22, result.Age); + } + + [Fact] + public void Deserialize_IntFieldNotSupplied_SetsFieldTo0() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(0, result.Age); + } + + [Fact] + public void Deserialize_EnumFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("day: Monday"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(DayOfWeek.Monday, result.Day); + } + + [Fact] + public void Deserialize_NullableEnumFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("nullableDay: Monday"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(DayOfWeek.Monday, result.NullableDay); + } + + [Fact] + public void Deserialize_EnumFieldNotSupplied_SetsDefault() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(default(DayOfWeek), result.Day); + } + + [Fact] + public void Deserialize_DictionarySupplied_DeserializesDictionary() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"classes: + first: maths + second: chemistry + third: art"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + var expectedClasses = new Dictionary + { + { "first", "maths" }, + { "second", "chemistry" }, + { "third", "art" } + }; + Assert.Equal(expectedClasses, result.Classes); + } + + [Fact] + public void Deserialize_TimeSpanFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("interval: 01:02:03"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(new TimeSpan(1, 2, 3), result.Interval); + } + + [Fact] + public void Deserialize_MapOptional_CanSpecifyDefaultValue() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(defaultInterval, result.DefaultedInterval); + } + + [Fact] + public void Deserialize_NullableTimeSpanFieldSupplied_SetsField() + { + // Arrange + var node = YamlUtils.CreateYamlNode("nullableInterval: 01:02:03"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Equal(new TimeSpan(1, 2, 3), result.NullableInterval); + } + + [Fact] + public void Deserialize_CustomMappingFunctionSupplied_UsesCustomMappingFunction() + { + // Arrange + var node = YamlUtils.CreateYamlNode("invertedProperty: false"); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.True(result.InvertedProperty); + } + + [Fact] + public void Deserialize_RequiredChildObject_CanUseChildDeserializer() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"child: + childProperty: 123"); + var child = new ChildConfig(); + _childDeserializer.Setup( + d => d.DeserializeObject((YamlMappingNode)node.Children["child"], _errorReporter.Object)).Returns(child); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Same(child, result.Child); + } + + [Fact] + public void Deserialize_OptionalChildObject_CanUseChildDeserializer() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"optionalChild: + childProperty: 123"); + var child = new ChildConfig(); + _childDeserializer.Setup( + d => d.DeserializeObject((YamlMappingNode)node.Children["optionalChild"], _errorReporter.Object)).Returns(child); + + // Act + var result = _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + Assert.Same(child, result.OptionalChild); + } + + public class RegistrationConfig + { + public string Name { get; set; } + public int Age { get; set; } + public DayOfWeek Day { get; set; } + public DayOfWeek? NullableDay { get; set; } + public Dictionary Classes { get; set; } + public string Town { get; set; } + public TimeSpan Interval { get; set; } + public TimeSpan DefaultedInterval { get; set; } + public TimeSpan? NullableInterval { get; set; } + public bool InvertedProperty { get; set; } + public ChildConfig Child { get; set; } + public ChildConfig OptionalChild { get; set; } + } + + public class ChildConfig + { + public string ChildProperty { get; set; } + } + + private class RegistrationConfigDeserializer: Deserializer + { + public RegistrationConfigDeserializer(IDeserializer childDeserializer) : base(NullLogger.Instance) + { + MapRequired(t => t.Name); + MapRequired(t => t.Age); + MapRequired(t => t.Day); + MapOptional(t => t.NullableDay); + MapRequired(t => t.Classes); + MapOptional(t => t.Town); + MapOptional(t => t.Interval); + MapOptional(t => t.DefaultedInterval, defaultInterval); + MapOptional(t => t.NullableInterval); + MapOptional(t => t.InvertedProperty, false, InvertBooleanString); + MapRequired(t => t.Child, childDeserializer); + MapOptional(t => t.OptionalChild, childDeserializer); + } + + private static object InvertBooleanString(string value, KeyValuePair nodePair, IErrorReporter errorReporter) + { + var boolValue = bool.Parse(value); + + return !boolValue; + } + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/ValidationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/ValidationTests.cs new file mode 100644 index 000000000..7d33b472e --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/DeserializerTests/ValidationTests.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Promitor.Core.Scraping.Configuration.Serialization; +using Promitor.Scraper.Tests.Unit.Serialization.v1; +using Xunit; +using YamlDotNet.RepresentationModel; + +namespace Promitor.Scraper.Tests.Unit.Serialization.DeserializerTests +{ + public class ValidationTests + { + private readonly Mock _errorReporter = new Mock(); + private readonly TestDeserializer _deserializer = new TestDeserializer(); + + [Fact] + public void Deserialize_RequiredFieldMissing_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("age: 20"); + + // Act + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "name"); + } + + [Fact] + public void Deserialize_RequiredFieldProvided_DoesNotReportError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + _errorReporter.Verify( + r => r.ReportError(node, It.Is(message => message.Contains("name"))), Times.Never); + } + + [Fact] + public void Deserialize_OptionalFieldMissing_DoesNotReportError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: Promitor"); + + // Act + _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + _errorReporter.Verify( + r => r.ReportError(It.IsAny(), It.Is(message => message.Contains("age"))), Times.Never); + } + + [Fact] + public void Deserialize_UnknownFields_ReportsWarnings() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"city: Glasgow +country: Scotland"); + + // Act + _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + var cityTagNode = node.Children.Single(c => c.Key.ToString() == "city").Key; + var countryTagNode = node.Children.Single(c => c.Key.ToString() == "country").Key; + + _errorReporter.Verify(r => r.ReportWarning(cityTagNode, "Unknown field 'city'.")); + _errorReporter.Verify(r => r.ReportWarning(countryTagNode, "Unknown field 'country'.")); + } + + [Fact] + public void Deserialize_EnumValueInvalid_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("day: Sundag"); + var dayValueNode = node.Children.Single(c => c.Key.ToString() == "day").Value; + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + dayValueNode, + "'Sundag' is not a valid value for 'day'."); + } + + [Fact] + public void Deserialize_IntValueInvalid_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("age: twenty"); + var dayValueNode = node.Children.Single(c => c.Key.ToString() == "age").Value; + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + dayValueNode, + $"'twenty' is not a valid value for 'age'. The value must be of type {typeof(int)}."); + } + + [Fact] + public void Deserialize_TimeSpanValueInvalid_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("interval: twenty"); + var dayValueNode = node.Children.Single(c => c.Key.ToString() == "interval").Value; + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + dayValueNode, + $"'twenty' is not a valid value for 'interval'. The value must be in the format 'hh:mm:ss'."); + } + + [Fact] + public void Deserialize_IgnoreField_DoesNotReportErrorIfFieldFound() + { + // Arrange + var node = YamlUtils.CreateYamlNode("customField: 1234"); + + // Act + _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + _errorReporter.Verify( + r => r.ReportWarning(It.IsAny(), It.Is(s => s.Contains("customField"))), Times.Never); + } + + [Fact] + public void Deserialize_CalledMultipleTimes_DoesNotReusePreviousState() + { + // Since the deserializers are created once during startup, this test is to ensure + // that if the same deserializer is used to deserialize multiple Yaml nodes, we don't + // reuse the state from the previous time. + + // Arrange + var node1 = YamlUtils.CreateYamlNode("name: Promitor"); + var node2 = YamlUtils.CreateYamlNode("age: 20"); + + // Act + _deserializer.Deserialize(node1, _errorReporter.Object); + _deserializer.Deserialize(node2, _errorReporter.Object); + + // Assert + _errorReporter.Verify( + r => r.ReportError(It.IsAny(), It.Is(s => s.Contains("name")))); + } + + private class TestConfigObject + { + public string Name { get; set; } + public int Age { get; set; } + public DayOfWeek Day { get; set; } + public TimeSpan Interval { get; set; } + } + + private class TestDeserializer: Deserializer + { + public TestDeserializer() : base(NullLogger.Instance) + { + MapRequired(t => t.Name); + MapOptional(t => t.Age); + MapOptional(t => t.Day); + MapOptional(t => t.Interval); + IgnoreField("customField"); + } + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/ErrorReporterTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/ErrorReporterTests.cs new file mode 100644 index 000000000..3669eb3dd --- /dev/null +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/ErrorReporterTests.cs @@ -0,0 +1,142 @@ +using System; +using System.ComponentModel; +using Promitor.Core.Scraping.Configuration.Serialization; +using Xunit; + +namespace Promitor.Scraper.Tests.Unit.Serialization +{ + [Category("Unit")] + public class ErrorReporterTests + { + private readonly ErrorReporter _errorReporter = new ErrorReporter(); + + [Fact] + public void ReportError_AfterErrorReported_AddsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: promitor"); + + // Act + _errorReporter.ReportError(node, "Test error message"); + + // Assert + Assert.Collection(_errorReporter.Messages, + m => + { + Assert.Equal(node, m.Node); + Assert.Equal(MessageType.Error, m.MessageType); + Assert.Equal("Test error message", m.Message); + }); + } + + [Fact] + public void ReportError_NullNode_ThrowsException() + { + Assert.Throws(() => _errorReporter.ReportError(null, "Test error")); + } + + [Fact] + public void ReportError_NullMessage_ThrowsException() + { + Assert.Throws( + () => _errorReporter.ReportError(YamlUtils.CreateYamlNode("name: promitor"), null)); + } + + [Fact] + public void ReportError_EmptyMessage_ThrowsException() + { + Assert.Throws( + () => _errorReporter.ReportError(YamlUtils.CreateYamlNode("name: promitor"), string.Empty)); + } + + [Fact] + public void ReportWarning_NullNode_ThrowsException() + { + Assert.Throws(() => _errorReporter.ReportWarning(null, "Test warning")); + } + + [Fact] + public void ReportWarning_NullMessage_ThrowsException() + { + Assert.Throws( + () => _errorReporter.ReportWarning(YamlUtils.CreateYamlNode("name: promitor"), null)); + } + + [Fact] + public void ReportWarning_EmptyMessage_ThrowsException() + { + Assert.Throws( + () => _errorReporter.ReportWarning(YamlUtils.CreateYamlNode("name: promitor"), string.Empty)); + } + + [Fact] + public void ReportWarning_AfterWarningReported_AddsWarning() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: promitor"); + + // Act + _errorReporter.ReportWarning(node, "Test warning message"); + + // Assert + Assert.Collection(_errorReporter.Messages, + m => + { + Assert.Equal(node, m.Node); + Assert.Equal(MessageType.Warning, m.MessageType); + Assert.Equal("Test warning message", m.Message); + }); + } + + [Fact] + public void HasErrors_AfterErrorReported_True() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: promitor"); + _errorReporter.ReportError(node, "Test error message"); + + // Act + var hasErrors = _errorReporter.HasErrors; + + // Assert + Assert.True(hasErrors); + } + + [Fact] + public void HasErrors_AfterWarningReported_False() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: promitor"); + _errorReporter.ReportWarning(node, "Test warning"); + + // Act + var hasErrors = _errorReporter.HasErrors; + + // Assert + Assert.False(hasErrors); + } + + [Fact] + public void Messages_MultipleMessagesAdded_ReturnedInOrderOfLineNumber() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"firstNode: one +secondNode: two +thirdNode: three"); + + _errorReporter.ReportWarning(node.Children["secondNode"], "second"); + _errorReporter.ReportError(node.Children["firstNode"], "first"); + _errorReporter.ReportWarning(node.Children["thirdNode"], "third"); + + // Act + var messages = _errorReporter.Messages; + + // Assert + Assert.Collection(messages, + m => Assert.Equal("first", m.Message), + m => Assert.Equal("second", m.Message), + m => Assert.Equal("third", m.Message)); + } + } +} \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/YamlMappingNodeExtensionTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/YamlMappingNodeExtensionTests.cs index 10f2bd5fc..37167ede6 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/YamlMappingNodeExtensionTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/YamlMappingNodeExtensionTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Promitor.Core.Scraping.Configuration.Serialization; using Promitor.Core.Scraping.Configuration.Serialization.v1.Core; using Xunit; @@ -139,6 +140,7 @@ public void GetTimeSpan_PropertyNotSpecified_Null() public void DeserializeChild_PropertySpecified_DeserializesChild() { // Arrange + var errorReporter = new Mock(); const string yamlText = @"aggregation: interval: 00:05:00"; @@ -146,7 +148,7 @@ public void DeserializeChild_PropertySpecified_DeserializesChild() var deserializer = new AggregationDeserializer(NullLogger.Instance); // Act - var aggregation = node.DeserializeChild("aggregation", deserializer); + var aggregation = node.DeserializeChild("aggregation", deserializer, errorReporter.Object); // Assert Assert.Equal(TimeSpan.FromMinutes(5), aggregation.Interval); @@ -156,11 +158,12 @@ public void DeserializeChild_PropertySpecified_DeserializesChild() public void DeserializeChild_PropertyNotSpecified_Null() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(@"time: 00:05:30"); var deserializer = new AggregationDeserializer(NullLogger.Instance); // Act - var aggregation = node.DeserializeChild("aggregation", deserializer); + var aggregation = node.DeserializeChild("aggregation", deserializer, errorReporter.Object); // Assert Assert.Null(aggregation); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetadataDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetadataDeserializerTests.cs index 3825c5e82..83737b56f 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetadataDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetadataDeserializerTests.cs @@ -1,6 +1,6 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; using Microsoft.Azure.Management.ResourceManager.Fluent; +using System.Linq; using Microsoft.Extensions.Logging.Abstractions; using Promitor.Core.Scraping.Configuration.Serialization.v1.Core; using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; @@ -54,17 +54,19 @@ public void Deserialize_AzureCloudNotSupplied_SetsGlobalAzureCloud() } [Fact] - public void Deserialize_InvalidAzureCloudSupplied_ThrowsException() + public void Deserialize_InvalidAzureCloudSupplied_ReportsError() { - var yamlText = - @"azureMetadata: - cloud: invalid"; - - // Arrange - var node = YamlUtils.CreateYamlNode(yamlText).Children["azureMetadata"]; + var yamlNode = YamlUtils.CreateYamlNode( +@"azureMetadata: + cloud: invalid"); + var azureMetadataNode = (YamlMappingNode)yamlNode.Children["azureMetadata"]; + var errorNode = azureMetadataNode.Children["cloud"]; - // Act - Assert.Throws(() => _deserializer.Deserialize((YamlMappingNode) node)); + YamlAssert.ReportsError( + _deserializer, + azureMetadataNode, + errorNode, + "'invalid' is not a valid value for 'cloud'."); } [Fact] @@ -98,6 +100,22 @@ public void Deserialize_TenantIdNotSupplied_Null() a => a.TenantId); } + [Fact] + public void Deserialize_TenantIdNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"azureMetadata: + subscriptionId: '0f9d7fea-99e8-4768-8672-06a28514f77e'"); + var metaDataNode = (YamlMappingNode)node.Children.Single(c => c.Key.ToString() == "azureMetadata").Value; + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + metaDataNode, + "tenantId"); + } + [Fact] public void Deserialize_SubscriptionIdSupplied_SetsSubscriptionId() { @@ -129,6 +147,22 @@ public void Deserialize_SubscriptionIdNotSupplied_Null() a => a.SubscriptionId); } + [Fact] + public void Deserialize_SubscriptionIdNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"azureMetadata: + tenantId: 'c8819874-9e56-4e3f-b1a8-1c0325138f27'"); + var metaDataNode = (YamlMappingNode)node.Children.Single(c => c.Key.ToString() == "azureMetadata").Value; + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + metaDataNode, + "subscriptionId"); + } + [Fact] public void Deserialize_ResourceGroupNameSupplied_SetsResourceGroupName() { @@ -159,5 +193,21 @@ public void Deserialize_ResourceGroupNameNotSupplied_Null() "azureMetadata", a => a.ResourceGroupName); } + + [Fact] + public void Deserialize_ResourceGroupNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"azureMetadata: + tenantId: 'c8819874-9e56-4e3f-b1a8-1c0325138f27'"); + var metaDataNode = (YamlMappingNode)node.Children.Single(c => c.Key.ToString() == "azureMetadata").Value; + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + metaDataNode, + "resourceGroupName"); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs index 055f7b051..c07131cce 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/AzureMetricConfigurationDeserializerTests.cs @@ -15,6 +15,7 @@ public class AzureMetricConfigurationDeserializerTests private readonly AzureMetricConfigurationDeserializer _deserializer; private readonly Mock> _dimensionDeserializer; private readonly Mock> _aggregationDeserializer; + private readonly Mock _errorReporter = new Mock(); public AzureMetricConfigurationDeserializerTests() { @@ -43,6 +44,19 @@ public void Deserialize_MetricNameNotSupplied_Null() a => a.MetricName); } + [Fact] + public void Deserialize_MetricNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "metricName"); + } + [Fact] public void Deserialize_AggregationSupplied_UsesDeserializer() { @@ -54,10 +68,11 @@ public void Deserialize_AggregationSupplied_UsesDeserializer() var aggregationNode = (YamlMappingNode)node.Children["aggregation"]; var aggregation = new MetricAggregationV1(); - _aggregationDeserializer.Setup(d => d.Deserialize(aggregationNode)).Returns(aggregation); + _aggregationDeserializer.Setup( + d => d.DeserializeObject(aggregationNode, _errorReporter.Object)).Returns(aggregation); // Act - var config = _deserializer.Deserialize(node); + var config = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(aggregation, config.Aggregation); @@ -74,10 +89,10 @@ public void Deserialize_DimensionSupplied_UsesDeserializer() var dimensionNode = (YamlMappingNode)node.Children["dimension"]; var dimension = new MetricDimensionV1(); - _dimensionDeserializer.Setup(d => d.Deserialize(dimensionNode)).Returns(dimension); + _dimensionDeserializer.Setup(d => d.DeserializeObject(dimensionNode, _errorReporter.Object)).Returns(dimension); // Act - var config = _deserializer.Deserialize(node); + var config = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(dimension, config.Dimension); @@ -100,5 +115,18 @@ public void Deserialize_AggregationNotSupplied_Null() "metricName: ActiveMessages", c => c.Aggregation); } + + [Fact] + public void Deserialize_AggregationNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "aggregation"); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricAggregationDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricAggregationDeserializerTests.cs index a40150870..df660aaab 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricAggregationDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricAggregationDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_TypeNotSupplied_Null() a => a.Type); } + [Fact] + public void Deserialize_TypeNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "type"); + } + [Fact] public void Deserialize_IntervalSupplied_SetsInterval() { diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs index e963e17f1..6302b27bc 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefaultsDeserializerTests.cs @@ -15,6 +15,7 @@ public class MetricDefaultsDeserializerTests private readonly MetricDefaultsDeserializer _deserializer; private readonly Mock> _aggregationDeserializer; private readonly Mock> _scrapingDeserializer; + private readonly Mock _errorReporter = new Mock(); public MetricDefaultsDeserializerTests() { @@ -37,10 +38,10 @@ public void Deserialize_AggregationPresent_UsesAggregationDeserializer() var aggregationNode = (YamlMappingNode)node.Children["aggregation"]; var aggregation = new AggregationV1(); - _aggregationDeserializer.Setup(d => d.Deserialize(aggregationNode)).Returns(aggregation); + _aggregationDeserializer.Setup(d => d.DeserializeObject(aggregationNode, _errorReporter.Object)).Returns(aggregation); // Act - var defaults = _deserializer.Deserialize(node); + var defaults = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(aggregation, defaults.Aggregation); @@ -57,10 +58,10 @@ public void Deserialize_AggregationNotPresent_DoesNotUseDeserializer() var node = (YamlMappingNode)YamlUtils.CreateYamlNode(yamlText).Children["metricDefaults"]; // Act - _deserializer.Deserialize(node); + _deserializer.Deserialize(node, _errorReporter.Object); // Assert - _aggregationDeserializer.Verify(d => d.Deserialize(It.IsAny()), Times.Never); + _aggregationDeserializer.Verify(d => d.Deserialize(It.IsAny(), It.IsAny()), Times.Never); } [Fact] @@ -75,10 +76,10 @@ public void Deserialize_ScrapingPresent_UsesScrapingDeserializer() var scrapingNode = (YamlMappingNode)node.Children["scraping"]; var scraping = new ScrapingV1(); - _scrapingDeserializer.Setup(d => d.Deserialize(scrapingNode)).Returns(scraping); + _scrapingDeserializer.Setup(d => d.DeserializeObject(scrapingNode, _errorReporter.Object)).Returns(scraping); // Act - var defaults = _deserializer.Deserialize(node); + var defaults = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(scraping, defaults.Scraping); @@ -95,10 +96,28 @@ public void Deserialize_ScrapingNotPresent_DoesNotUseDeserializer() var node = (YamlMappingNode)YamlUtils.CreateYamlNode(yamlText).Children["metricDefaults"]; // Act - _deserializer.Deserialize(node); + _deserializer.Deserialize(node, _errorReporter.Object); // Assert - _scrapingDeserializer.Verify(d => d.Deserialize(It.IsAny()), Times.Never); + _scrapingDeserializer.Verify( + d => d.Deserialize(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void Deserialize_ScrapingNotPresent_ReportsError() + { + // Arrange + const string yamlText = +@"metricDefaults: + aggregation: + interval: '00:05:00'"; + var node = (YamlMappingNode)YamlUtils.CreateYamlNode(yamlText).Children["metricDefaults"]; + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "scraping"); } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs index a5a33a4dc..cabc88901 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/MetricDefinitionDeserializerTests.cs @@ -17,6 +17,7 @@ public class MetricDefinitionDeserializerTests private readonly Mock> _azureMetricConfigurationDeserializer; private readonly Mock> _scrapingDeserializer; private readonly Mock _resourceDeserializerFactory; + private readonly Mock _errorReporter = new Mock(); private readonly MetricDefinitionDeserializer _deserializer; @@ -49,6 +50,19 @@ public void Deserialize_NameNotSupplied_Null() YamlAssert.PropertyNull(_deserializer, "description: 'Test metric'", d => d.Name); } + [Fact] + public void Deserialize_NameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("description: 'Test metric'"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "name"); + } + [Fact] public void Deserialize_DescriptionSupplied_SetsDescription() { @@ -65,6 +79,19 @@ public void Deserialize_DescriptionNotSupplied_Null() YamlAssert.PropertyNull(_deserializer, "name: metric", d => d.Description); } + [Fact] + public void Deserialize_DescriptionNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: 'test_metric'"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "description"); + } + [Fact] public void Deserialize_ResourceTypeSupplied_SetsResourceType() { @@ -84,6 +111,33 @@ public void Deserialize_ResourceTypeNotSupplied_Null() d => d.ResourceType); } + [Fact] + public void Deserialize_ResourceTypeNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: 'test_metric'"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "resourceType"); + } + + [Fact] + public void Deserialize_ResourceType_NotSpecified_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceType: 'NotSpecified'"); + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + node["resourceType"], + "'resourceType' must not be set to 'NotSpecified'."); + } + [Fact] public void Deserialize_LabelsSupplied_SetsLabels() { @@ -116,10 +170,10 @@ public void Deserialize_AzureMetricConfigurationSupplied_UsesDeserializer() var configurationNode = (YamlMappingNode)node.Children["azureMetricConfiguration"]; var configuration = new AzureMetricConfigurationV1(); - _azureMetricConfigurationDeserializer.Setup(d => d.Deserialize(configurationNode)).Returns(configuration); + _azureMetricConfigurationDeserializer.Setup(d => d.DeserializeObject(configurationNode, _errorReporter.Object)).Returns(configuration); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(configuration, definition.AzureMetricConfiguration); @@ -133,15 +187,28 @@ public void Deserialize_AzureMetricConfigurationNotSupplied_Null() var node = YamlUtils.CreateYamlNode(yamlText); _azureMetricConfigurationDeserializer.Setup( - d => d.Deserialize(It.IsAny())).Returns(new AzureMetricConfigurationV1()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new AzureMetricConfigurationV1()); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Null(definition.AzureMetricConfiguration); } + [Fact] + public void Deserialize_AzureMetricConfigurationNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: 'test_metric'"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "azureMetricConfiguration"); + } + [Fact] public void Deserialize_ScrapingSupplied_UsesDeserializer() { @@ -153,10 +220,10 @@ public void Deserialize_ScrapingSupplied_UsesDeserializer() var scrapingNode = (YamlMappingNode)node.Children["scraping"]; var scraping = new ScrapingV1(); - _scrapingDeserializer.Setup(d => d.Deserialize(scrapingNode)).Returns(scraping); + _scrapingDeserializer.Setup(d => d.DeserializeObject(scrapingNode, _errorReporter.Object)).Returns(scraping); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(scraping, definition.Scraping); @@ -169,10 +236,11 @@ public void Deserialize_ScrapingNotSupplied_Null() const string yamlText = "name: promitor_test_metric"; var node = YamlUtils.CreateYamlNode(yamlText); - _scrapingDeserializer.Setup(d => d.Deserialize(It.IsAny())).Returns(new ScrapingV1()); + _scrapingDeserializer.Setup( + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new ScrapingV1()); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Null(definition.Scraping); @@ -193,15 +261,43 @@ public void Deserialize_ResourcesSupplied_UsesDeserializer() _resourceDeserializerFactory.Setup( f => f.GetDeserializerFor(ResourceType.Generic)).Returns(resourceDeserializer.Object); - var resources = new List(); + var resources = new List + { + new AzureResourceDefinitionV1 { ResourceGroupName = "promitor-group" } + }; resourceDeserializer.Setup( - d => d.Deserialize((YamlSequenceNode)node.Children["resources"])).Returns(resources); + d => d.Deserialize((YamlSequenceNode)node.Children["resources"], _errorReporter.Object)) + .Returns(resources); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert - Assert.Same(resources, definition.Resources); + Assert.Collection(definition.Resources, + resource => Assert.Equal("promitor-group", resource.ResourceGroupName)); + } + + [Fact] + public void Deserialize_ResourcesSupplied_DoesNotReportWarning() + { + // Because we're handling deserializing the resources manually, we + // need to explicitly ignore the field to stop a warning being reported + // about an unknown field + + // Arrange + const string yamlText = +@"resourceType: Generic +resources: +- resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging +- resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging-2"; + var node = YamlUtils.CreateYamlNode(yamlText); + + // Act + _deserializer.Deserialize(node, _errorReporter.Object); + + // Assert + _errorReporter.Verify( + r => r.ReportWarning(It.IsAny(), It.Is(s => s.Contains("resources"))), Times.Never); } [Fact] @@ -218,15 +314,50 @@ public void Deserialize_ResourcesWithUnspecifiedResourceType_Null() _resourceDeserializerFactory.Setup( f => f.GetDeserializerFor(It.IsAny())).Returns(resourceDeserializer.Object); - var resources = new List(); resourceDeserializer.Setup( - d => d.Deserialize((YamlSequenceNode)node.Children["resources"])).Returns(resources); + d => d.Deserialize((YamlSequenceNode)node.Children["resources"], _errorReporter.Object)) + .Returns(new List()); // Act - var definition = _deserializer.Deserialize(node); + var definition = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Null(definition.Resources); } + + [Fact] + public void Deserialize_ResourcesNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceType: Generic"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "resources"); + } + + [Fact] + public void Deserialize_NoDeserializerForResourceType_ReportsError() + { + // Arrange + const string yamlText = +@"resourceType: Generic +resources: +- resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging +- resourceUri: Microsoft.ServiceBus/namespaces/promitor-messaging-2"; + var node = YamlUtils.CreateYamlNode(yamlText); + + _resourceDeserializerFactory.Setup( + f => f.GetDeserializerFor(It.IsAny())).Returns((IDeserializer)null); + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + node.Children["resourceType"], + "Could not find a deserializer for resource type 'Generic'."); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/ScrapingDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/ScrapingDeserializerTests.cs index 494204f4a..ef4ba1221 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/ScrapingDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/ScrapingDeserializerTests.cs @@ -43,5 +43,18 @@ public void Deserialize_ScheduleNotSupplied_SetsScheduleNull() "scraping", s => s.Schedule); } + + [Fact] + public void Deserialize_ScheduleNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: promitor"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "schedule"); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/SecretDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/SecretDeserializerTests.cs index 01cb36495..c59ef81f8 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/SecretDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/SecretDeserializerTests.cs @@ -52,5 +52,35 @@ public void Deserialize_EnvironmentVariableNotSupplied_Null() "rawValue: abc123", s => s.EnvironmentVariable); } + + [Fact] + public void Deserialize_EnvironmentVariableAndRawValueNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("name: 123"); + + // Act / Assert + YamlAssert.ReportsError( + _deserializer, + node, + node, + "Either 'environmentVariable' or 'rawValue' must be supplied for a secret."); + } + + [Fact] + public void Deserialize_EnvironmentVariableAndRawValueBothSupplied_ReportsWarning() + { + // Arrange + var node = YamlUtils.CreateYamlNode( +@"rawValue: 123 +environmentVariable: PROMITOR_SECRET"); + + // Act / Assert + YamlAssert.ReportsWarning( + _deserializer, + node, + node, + "Secret with environment variable 'PROMITOR_SECRET' also has a rawValue provided."); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs index 6d8b0bdab..afa67a672 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Core/V1DeserializerTests.cs @@ -17,6 +17,7 @@ public class V1DeserializerTests private readonly Mock> _metadataDeserializer; private readonly Mock> _defaultsDeserializer; private readonly Mock> _metricsDeserializer; + private readonly Mock _errorReporter = new Mock(); private readonly V1Deserializer _deserializer; public V1DeserializerTests() @@ -39,7 +40,7 @@ public void Deserialize_NoVersionSpecified_ThrowsException() var yamlNode = YamlUtils.CreateYamlNode("azureMetadata:"); // Act - var exception = Assert.Throws(() => _deserializer.Deserialize(yamlNode)); + var exception = Assert.Throws(() => _deserializer.Deserialize(yamlNode, _errorReporter.Object)); // Assert Assert.Equal("No 'version' element was found in the metrics config", exception.Message); @@ -52,7 +53,7 @@ public void Deserialize_VersionSpecified_SetsCorrectVersion() var yamlNode = YamlUtils.CreateYamlNode("version: v1"); // Act - var builder = _deserializer.Deserialize(yamlNode); + var builder = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Equal("v1", builder.Version); @@ -65,7 +66,7 @@ public void Deserialize_WrongVersionSpecified_ThrowsException() var yamlNode = YamlUtils.CreateYamlNode("version: v2"); // Act - var exception = Assert.Throws(() => _deserializer.Deserialize(yamlNode)); + var exception = Assert.Throws(() => _deserializer.Deserialize(yamlNode, _errorReporter.Object)); // Assert Assert.Equal("A 'version' element with a value of 'v1' was expected but the value 'v2' was found", exception.Message); @@ -81,10 +82,11 @@ public void Deserialize_AzureMetadata_UsesMetadataDeserializer() tenantId: 'abc-123'"; var yamlNode = YamlUtils.CreateYamlNode(config); var azureMetadata = new AzureMetadataV1(); - _metadataDeserializer.Setup(d => d.Deserialize(It.IsAny())).Returns(azureMetadata); + _metadataDeserializer.Setup( + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(azureMetadata); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Same(azureMetadata, declaration.AzureMetadata); @@ -96,10 +98,10 @@ public void Deserialize_AzureMetadataNotSupplied_SetsMetadataNull() // Arrange var yamlNode = YamlUtils.CreateYamlNode("version: v1"); _metadataDeserializer.Setup( - d => d.Deserialize(It.IsAny())).Returns(new AzureMetadataV1()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new AzureMetadataV1()); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Null(declaration.AzureMetadata); @@ -116,10 +118,11 @@ public void Deserialize_MetricDefaults_UsesDefaultsDeserializer() interval: '00:05:00'"; var yamlNode = YamlUtils.CreateYamlNode(config); var metricDefaults = new MetricDefaultsV1(); - _defaultsDeserializer.Setup(d => d.Deserialize(It.IsAny())).Returns(metricDefaults); + _defaultsDeserializer.Setup( + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(metricDefaults); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Same(metricDefaults, declaration.MetricDefaults); @@ -133,10 +136,10 @@ public void Deserialize_MetricDefaultsNotSupplied_SetsDefaultsNull() @"version: v1"; var yamlNode = YamlUtils.CreateYamlNode(config); _defaultsDeserializer.Setup( - d => d.Deserialize(It.IsAny())).Returns(new MetricDefaultsV1()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new MetricDefaultsV1()); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Null(declaration.MetricDefaults); @@ -151,14 +154,15 @@ public void Deserialize_Metrics_UsesMetricsDeserializer() metrics: - name: promitor_metrics_total"; var yamlNode = YamlUtils.CreateYamlNode(config); - var metrics = new List(); - _metricsDeserializer.Setup(d => d.Deserialize(It.IsAny())).Returns(metrics); + var metrics = new List { new MetricDefinitionV1 { Name = "test_metric" } }; + _metricsDeserializer.Setup( + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(metrics); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert - Assert.Same(metrics, declaration.Metrics); + Assert.Collection(declaration.Metrics, metric => Assert.Equal("test_metric", metric.Name)); } [Fact] @@ -169,10 +173,10 @@ public void Deserialize_Metric_SetsMetricsNull() @"version: v1"; var yamlNode = YamlUtils.CreateYamlNode(config); _metricsDeserializer.Setup( - d => d.Deserialize(It.IsAny())).Returns(new List()); + d => d.Deserialize(It.IsAny(), It.IsAny())).Returns(new List()); // Act - var declaration = _deserializer.Deserialize(yamlNode); + var declaration = _deserializer.Deserialize(yamlNode, _errorReporter.Object); // Assert Assert.Null(declaration.Metrics); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/BlobStorageDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/BlobStorageDeserializerTests.cs index bc29c707d..585974d59 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/BlobStorageDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/BlobStorageDeserializerTests.cs @@ -8,7 +8,7 @@ namespace Promitor.Scraper.Tests.Unit.Serialization.v1.Providers { public class BlobStorageDeserializerTests : ResourceDeserializerTest { - private readonly StorageAccountDeserializer _deserializer; + private readonly BlobStorageDeserializer _deserializer; public BlobStorageDeserializerTests() { @@ -37,7 +37,7 @@ public void Deserialize_AccountNameNotSupplied_Null() protected override IDeserializer CreateDeserializer() { - return new StorageAccountDeserializer(Logger); + return new BlobStorageDeserializer(Logger); } } } \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerInstanceDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerInstanceDeserializerTests.cs index 9bd113523..325588311 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerInstanceDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerInstanceDeserializerTests.cs @@ -40,5 +40,18 @@ public void Deserialize_ContainerGroupNotSupplied_Null() "resourceGroupName: promitor-resource-group", c => c.ContainerGroup); } + + [Fact] + public void Deserialize_ContainerGroupNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "containerGroup"); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerRegistryDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerRegistryDeserializerTests.cs index 75d0ac1b4..b4fe83d54 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerRegistryDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ContainerRegistryDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_RegistryNameNotSupplied_Null() c => c.RegistryName); } + [Fact] + public void Deserialize_RegistryNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "registryName"); + } + protected override IDeserializer CreateDeserializer() { return new ContainerRegistryDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/CosmosDbDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/CosmosDbDeserializerTests.cs index 377851c33..6627571a7 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/CosmosDbDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/CosmosDbDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_DbNameNotSupplied_Null() c => c.DbName); } + [Fact] + public void Deserialize_DbNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "dbName"); + } + protected override IDeserializer CreateDeserializer() { return new CosmosDbDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/FileStorageDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/FileStorageDeserializerTests.cs index e2e074701..c0a89fd34 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/FileStorageDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/FileStorageDeserializerTests.cs @@ -37,7 +37,7 @@ public void Deserialize_AccountNameNotSupplied_Null() protected override IDeserializer CreateDeserializer() { - return new StorageAccountDeserializer(Logger); + return new FileStorageDeserializer(Logger); } } } \ No newline at end of file diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/GenericResourceDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/GenericResourceDeserializerTests.cs index 056aa5feb..7d8fa9012 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/GenericResourceDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/GenericResourceDeserializerTests.cs @@ -53,6 +53,19 @@ public void Deserialize_ResourceUriNotSupplied_Null() r => r.ResourceUri); } + [Fact] + public void Deserialize_ResourceUriNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "resourceUri"); + } + protected override IDeserializer CreateDeserializer() { return new GenericResourceDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/NetworkInterfaceDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/NetworkInterfaceDeserializerTests.cs index 58377ef81..6209db6ca 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/NetworkInterfaceDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/NetworkInterfaceDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_NetworkInterfaceNameNotSupplied_Null() r => r.NetworkInterfaceName); } + [Fact] + public void Deserialize_NetworkInterfaceNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "networkInterfaceName"); + } + protected override IDeserializer CreateDeserializer() { return new NetworkInterfaceDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/PostgreSqlDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/PostgreSqlDeserializerTests.cs index 8981e216e..09fb0e98e 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/PostgreSqlDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/PostgreSqlDeserializerTests.cs @@ -34,6 +34,19 @@ public void Deserialize_ServerNameNotSupplied_Null() r => r.ServerName); } + [Fact] + public void Deserialize_ServerNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "serverName"); + } + protected override IDeserializer CreateDeserializer() { return new PostgreSqlDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/RedisCacheDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/RedisCacheDeserializerTests.cs index f8d2e5225..9faf31d0f 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/RedisCacheDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/RedisCacheDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_CacheNameNotSupplied_Null() r => r.CacheName); } + [Fact] + public void Deserialize_CacheNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "cacheName"); + } + protected override IDeserializer CreateDeserializer() { return new RedisCacheDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ServiceBusQueueDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ServiceBusQueueDeserializerTests.cs index 89ab19a22..00c19aa98 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ServiceBusQueueDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/ServiceBusQueueDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_QueueNameNotSupplied_Null() r => r.QueueName); } + [Fact] + public void Deserialize_QueueNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "queueName"); + } + [Fact] public void Deserialize_NamespaceSupplied_SetsNamespace() { @@ -55,6 +68,19 @@ public void Deserialize_NamespaceNotSupplied_Null() r => r.Namespace); } + [Fact] + public void Deserialize_NamespaceNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "namespace"); + } + protected override IDeserializer CreateDeserializer() { return new ServiceBusQueueDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/SqlDatabaseDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/SqlDatabaseDeserializerTests.cs index 586591510..7b14898cf 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/SqlDatabaseDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/SqlDatabaseDeserializerTests.cs @@ -11,12 +11,7 @@ namespace Promitor.Scraper.Tests.Unit.Serialization.v1.Providers [Category("Unit")] public class SqlDatabaseDeserializerTests : ResourceDeserializerTest { - private readonly SqlDatabaseDeserializer _deserializer; - - public SqlDatabaseDeserializerTests() - { - _deserializer = new SqlDatabaseDeserializer(NullLogger.Instance); - } + private readonly SqlDatabaseDeserializer _deserializer = new SqlDatabaseDeserializer(NullLogger.Instance); [Fact] public void Deserialize_ServerNameSupplied_SetsServerName() @@ -37,6 +32,19 @@ public void Deserialize_ServerNameNotSupplied_Null() c => c.ServerName); } + [Fact] + public void Deserialize_ServerNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "serverName"); + } + [Fact] public void Deserialize_DatabaseNameSupplied_SetsDatabaseName() { @@ -56,6 +64,19 @@ public void Deserialize_DatabaseNameNotSupplied_Null() c => c.DatabaseName); } + [Fact] + public void Deserialize_DatabaseNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "databaseName"); + } + protected override IDeserializer CreateDeserializer() { return new SqlDatabaseDeserializer(NullLogger.Instance); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs index 275aabf68..87cb412e8 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/StorageQueueDeserializerTests.cs @@ -11,6 +11,7 @@ namespace Promitor.Scraper.Tests.Unit.Serialization.v1.Providers public class StorageQueueDeserializerTests : ResourceDeserializerTest { private readonly Mock> _secretDeserializer; + private readonly Mock _errorReporter = new Mock(); private readonly StorageQueueDeserializer _deserializer; @@ -41,6 +42,19 @@ public void Deserialize_AccountNameNotSupplied_Null() r => r.AccountName); } + [Fact] + public void Deserialize_AccountNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-resource-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "accountName"); + } + [Fact] public void Deserialize_QueueNameSupplied_SetsQueueName() { @@ -60,6 +74,19 @@ public void Deserialize_QueueNameNotSupplied_Null() r => r.QueueName); } + [Fact] + public void Deserialize_QueueNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "queueName"); + } + [Fact] public void Deserialize_SasTokenSupplied_UsesDeserializer() { @@ -71,10 +98,10 @@ public void Deserialize_SasTokenSupplied_UsesDeserializer() var sasTokenNode = (YamlMappingNode)node.Children["sasToken"]; var secret = new SecretV1(); - _secretDeserializer.Setup(d => d.Deserialize(sasTokenNode)).Returns(secret); + _secretDeserializer.Setup(d => d.DeserializeObject(sasTokenNode, _errorReporter.Object)).Returns(secret); // Act - var resource = (StorageQueueResourceV1)_deserializer.Deserialize(node); + var resource = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(secret, resource.SasToken); @@ -89,6 +116,19 @@ public void Deserialize_SasTokenNotSupplied_Null() r => r.SasToken); } + [Fact] + public void Deserialize_SasTokenNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "sasToken"); + } + protected override IDeserializer CreateDeserializer() { return new StorageQueueDeserializer(new Mock>().Object, Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/VirtualMachineDeserializerTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/VirtualMachineDeserializerTests.cs index 98360eee8..16ed377bc 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/VirtualMachineDeserializerTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/Providers/VirtualMachineDeserializerTests.cs @@ -36,6 +36,19 @@ public void Deserialize_VirtualMachineNameNotSupplied_Null() r => r.VirtualMachineName); } + [Fact] + public void Deserialize_VirtualMachineNameNotSupplied_ReportsError() + { + // Arrange + var node = YamlUtils.CreateYamlNode("resourceGroupName: promitor-group"); + + // Act / Assert + YamlAssert.ReportsErrorForProperty( + _deserializer, + node, + "virtualMachineName"); + } + protected override IDeserializer CreateDeserializer() { return new VirtualMachineDeserializer(Logger); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/V1SerializationTests.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/V1SerializationTests.cs index 4a7ca8268..8a0197ce3 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/V1SerializationTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/V1SerializationTests.cs @@ -27,6 +27,7 @@ public class V1SerializationTests private readonly V1Deserializer _v1Deserializer; private readonly ConfigurationSerializer _configurationSerializer; private readonly MetricsDeclarationV1 _metricsDeclaration; + private readonly IErrorReporter _errorReporter = new ErrorReporter(); public V1SerializationTests() { @@ -130,7 +131,7 @@ public void Deserialize_SerializedModel_CanDeserialize() var yaml = _configurationSerializer.Serialize(_metricsDeclaration); // Act - var deserializedModel = _v1Deserializer.Deserialize(YamlUtils.CreateYamlNode(yaml)); + var deserializedModel = _v1Deserializer.Deserialize(YamlUtils.CreateYamlNode(yaml), _errorReporter); // Assert Assert.NotNull(deserializedModel); @@ -179,7 +180,7 @@ public void Deserialize_SerializedYaml_CanDeserializeToRuntimeModel() var yaml = _configurationSerializer.Serialize(_metricsDeclaration); // Act - var runtimeModel = _configurationSerializer.Deserialize(yaml); + var runtimeModel = _configurationSerializer.Deserialize(yaml, _errorReporter); // Assert Assert.NotNull(runtimeModel); diff --git a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/YamlAssert.cs b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/YamlAssert.cs index 491aa4bbc..324535ea6 100644 --- a/src/Promitor.Scraper.Tests.Unit/Serialization/v1/YamlAssert.cs +++ b/src/Promitor.Scraper.Tests.Unit/Serialization/v1/YamlAssert.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using Moq; using Promitor.Core.Scraping.Configuration.Serialization; using Xunit; using YamlDotNet.RepresentationModel; @@ -19,17 +21,19 @@ public static class YamlAssert /// The property to check. public static void PropertySet( IDeserializer deserializer, string yamlText, TResult expected, Func propertyAccessor) + where TObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText); // Act - var definition = deserializer.Deserialize(node); + var definition = deserializer.Deserialize(node, errorReporter.Object); // Assert Assert.Equal(expected, propertyAccessor(definition)); } - + /// /// Deserializes the yaml and asserts that the specified property has been set. /// Use this overload where the deserializer actually returns a subclass of . @@ -44,12 +48,14 @@ public static void PropertySet( public static void PropertySet( IDeserializer deserializer, string yamlText, TResult expected, Func propertyAccessor) where TObject: TBaseObject + where TBaseObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText); // Act - var definition = deserializer.Deserialize(node); + var definition = deserializer.Deserialize(node, errorReporter.Object); // Assert Assert.Equal(expected, propertyAccessor((TObject)definition)); @@ -68,12 +74,14 @@ public static void PropertySet( /// The property to check. public static void PropertySet( IDeserializer deserializer, string yamlText, string yamlElement, TResult expected, Func propertyAccessor) + where TObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText).Children[yamlElement]; // Act - var definition = deserializer.Deserialize((YamlMappingNode)node); + var definition = deserializer.Deserialize((YamlMappingNode)node, errorReporter.Object); // Assert Assert.Equal(expected, propertyAccessor(definition)); @@ -88,12 +96,14 @@ public static void PropertySet( /// The property to check. public static void PropertyNull( IDeserializer deserializer, string yamlText, Func propertyAccessor) + where TObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText); // Act - var definition = deserializer.Deserialize(node); + var definition = deserializer.Deserialize(node, errorReporter.Object); // Assert Assert.Null(propertyAccessor(definition)); @@ -110,12 +120,14 @@ public static void PropertyNull( public static void PropertyNull( IDeserializer deserializer, string yamlText, Func propertyAccessor) where TObject: TBaseObject + where TBaseObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText); // Act - var definition = deserializer.Deserialize(node); + var definition = deserializer.Deserialize(node, errorReporter.Object); // Assert Assert.Null(propertyAccessor((TObject)definition)); @@ -132,15 +144,95 @@ public static void PropertyNull( /// The element to look for the property under. public static void PropertyNull( IDeserializer deserializer, string yamlText, string yamlElement, Func propertyAccessor) + where TObject: new() { // Arrange + var errorReporter = new Mock(); var node = YamlUtils.CreateYamlNode(yamlText).Children[yamlElement]; // Act - var definition = deserializer.Deserialize((YamlMappingNode)node); + var definition = deserializer.Deserialize((YamlMappingNode)node, errorReporter.Object); // Assert Assert.Null(propertyAccessor(definition)); } + + /// + /// Checks that the specified error message is reported while deserializing the yaml. + /// + /// The deserializer to use. + /// The Yaml to deserialize. + /// The node that should have the error. + /// The message that should be reported. + /// The type of object being deserialized. + public static void ReportsError( + IDeserializer deserializer, YamlMappingNode yamlNode, YamlNode errorNode, string expectedMessage) + where TObject: new() + { + ReportsMessage(deserializer, yamlNode, errorNode, expectedMessage, MessageType.Error); + } + + /// + /// Checks that the specified warning message is reported while deserializing the yaml. + /// + /// The deserializer to use. + /// The Yaml to deserialize. + /// The node that should have the warning. + /// The message that should be reported. + /// The type of object being deserialized. + public static void ReportsWarning( + IDeserializer deserializer, YamlMappingNode yamlNode, YamlNode errorNode, string expectedMessage) + where TObject: new() + { + ReportsMessage(deserializer, yamlNode, errorNode, expectedMessage, MessageType.Warning); + } + + /// + /// Checks that the specified error message is reported while deserializing the yaml. + /// + /// The deserializer to use. + /// The Yaml to deserialize. + /// The node that should have the error. + /// The message that should be reported. + /// The type of message that should be reported. + /// The type of object being deserialized. + public static void ReportsMessage( + IDeserializer deserializer, YamlMappingNode yamlNode, YamlNode errorNode, string expectedMessage, MessageType expectedMessageType) + where TObject: new() + { + // Arrange + var errorReporter = new ErrorReporter(); + + // Act + deserializer.Deserialize(yamlNode, errorReporter); + + // Assert + var message = errorReporter.Messages.FirstOrDefault(m => m.Node == errorNode && m.Message == expectedMessage); + Assert.True(message != null, "Error message not found against specified yaml element."); + Assert.Equal(expectedMessageType, message.MessageType); + } + + /// + /// Checks that an error is reported for the specified property while deserializing the yaml. + /// + /// The deserializer to use. + /// The Yaml to deserialize. + /// The property that should have an error. + /// The type of object being deserialized. + public static void ReportsErrorForProperty( + IDeserializer deserializer, YamlMappingNode yamlNode, string propertyName) + where TObject: new() + { + // Arrange + var errorReporter = new ErrorReporter(); + + // Act + deserializer.Deserialize(yamlNode, errorReporter); + + // Assert + var message = errorReporter.Messages.FirstOrDefault(m => ReferenceEquals(m.Node, yamlNode) && m.Message.Contains(propertyName)); + Assert.True(message != null, "Error message not found against specified yaml element."); + Assert.Equal(MessageType.Error, message.MessageType); + } } } diff --git a/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/ServiceBusQueueMetricsDeclarationValidationStepTests.cs b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/ServiceBusQueueMetricsDeclarationValidationStepTests.cs index cbb7076d1..1106b46bc 100644 --- a/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/ServiceBusQueueMetricsDeclarationValidationStepTests.cs +++ b/src/Promitor.Scraper.Tests.Unit/Validation/Metrics/ResourceTypes/ServiceBusQueueMetricsDeclarationValidationStepTests.cs @@ -74,7 +74,7 @@ public void ServiceBusQueuesMetricsDeclaration_DeclarationWithoutResourceInfo_Fa var validationResult = scrapingScheduleValidationStep.Run(); // Assert - Assert.True(validationResult.IsSuccessful, "Validation was successful"); + Assert.False(validationResult.IsSuccessful, "Validation was successful but should have failed"); } [Fact]