diff --git a/src/Amazon.CloudWatch.EMF/Constants.cs b/src/Amazon.CloudWatch.EMF/Constants.cs index 1d50e2f..807471a 100644 --- a/src/Amazon.CloudWatch.EMF/Constants.cs +++ b/src/Amazon.CloudWatch.EMF/Constants.cs @@ -2,12 +2,17 @@ namespace Amazon.CloudWatch.EMF { public class Constants { + public const int MAX_DIMENSION_SET_SIZE = 30; + public const int MAX_DIMENSION_NAME_LENGTH = 250; + public const int MAX_DIMENSION_VALUE_LENGTH = 1024; + public const int MAX_METRIC_NAME_LENGTH = 1024; + public const int MAX_NAMESPACE_LENGTH = 256; + public const string VALID_NAMESPACE_REGEX = "^[a-zA-Z0-9._#:/-]+$"; + public const int DEFAULT_AGENT_PORT = 25888; public const string UNKNOWN = "Unknown"; - public const int MAX_DIMENSION_SET_SIZE = 30; - public const int MAX_METRICS_PER_EVENT = 100; public const string DEFAULT_NAMESPACE = "aws-embedded-metrics"; diff --git a/src/Amazon.CloudWatch.EMF/Exception/DimensionSetExceededException.cs b/src/Amazon.CloudWatch.EMF/Exception/DimensionSetExceededException.cs index 4b89fcf..86e774a 100644 --- a/src/Amazon.CloudWatch.EMF/Exception/DimensionSetExceededException.cs +++ b/src/Amazon.CloudWatch.EMF/Exception/DimensionSetExceededException.cs @@ -9,15 +9,5 @@ public DimensionSetExceededException() ". Account for default dimensions if not using SetDimensions.") { } - - public DimensionSetExceededException(string message) - : base(message) - { - } - - public DimensionSetExceededException(string message, Exception inner) - : base(message, inner) - { - } } -} \ No newline at end of file +} diff --git a/src/Amazon.CloudWatch.EMF/Exception/EMFClientException.cs b/src/Amazon.CloudWatch.EMF/Exception/EMFClientException.cs index 8f9bc8c..a4fc9eb 100644 --- a/src/Amazon.CloudWatch.EMF/Exception/EMFClientException.cs +++ b/src/Amazon.CloudWatch.EMF/Exception/EMFClientException.cs @@ -18,4 +18,4 @@ public EMFClientException(string message, Exception inner) { } } -} \ No newline at end of file +} diff --git a/src/Amazon.CloudWatch.EMF/Exception/InvalidDimensionException.cs b/src/Amazon.CloudWatch.EMF/Exception/InvalidDimensionException.cs new file mode 100644 index 0000000..e62467d --- /dev/null +++ b/src/Amazon.CloudWatch.EMF/Exception/InvalidDimensionException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Amazon.CloudWatch.EMF +{ + public class InvalidDimensionException : Exception + { + public InvalidDimensionException(string message) + : base(message) + { + } + } +} diff --git a/src/Amazon.CloudWatch.EMF/Exception/InvalidMetricException.cs b/src/Amazon.CloudWatch.EMF/Exception/InvalidMetricException.cs new file mode 100644 index 0000000..da4fa75 --- /dev/null +++ b/src/Amazon.CloudWatch.EMF/Exception/InvalidMetricException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Amazon.CloudWatch.EMF +{ + public class InvalidMetricException : Exception + { + public InvalidMetricException(string message) + : base(message) + { + } + } +} diff --git a/src/Amazon.CloudWatch.EMF/Exception/InvalidNamespaceException.cs b/src/Amazon.CloudWatch.EMF/Exception/InvalidNamespaceException.cs new file mode 100644 index 0000000..d90a001 --- /dev/null +++ b/src/Amazon.CloudWatch.EMF/Exception/InvalidNamespaceException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Amazon.CloudWatch.EMF +{ + public class InvalidNamespaceException : Exception + { + public InvalidNamespaceException(string message) + : base(message) + { + } + } +} diff --git a/src/Amazon.CloudWatch.EMF/Logger/MetricsLogger.cs b/src/Amazon.CloudWatch.EMF/Logger/MetricsLogger.cs index f021f7b..fa4263b 100644 --- a/src/Amazon.CloudWatch.EMF/Logger/MetricsLogger.cs +++ b/src/Amazon.CloudWatch.EMF/Logger/MetricsLogger.cs @@ -3,6 +3,7 @@ using Amazon.CloudWatch.EMF.Config; using Amazon.CloudWatch.EMF.Environment; using Amazon.CloudWatch.EMF.Model; +using Amazon.CloudWatch.EMF.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -181,6 +182,7 @@ public MetricsLogger PutMetadata(string key, object value) /// the current logger. public MetricsLogger SetNamespace(string logNamespace) { + Validator.ValidateNamespace(logNamespace); _context.Namespace = logNamespace; return this; } diff --git a/src/Amazon.CloudWatch.EMF/Model/DimensionSet.cs b/src/Amazon.CloudWatch.EMF/Model/DimensionSet.cs index a5c2021..bcfd79d 100644 --- a/src/Amazon.CloudWatch.EMF/Model/DimensionSet.cs +++ b/src/Amazon.CloudWatch.EMF/Model/DimensionSet.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Amazon.CloudWatch.EMF.Utils; namespace Amazon.CloudWatch.EMF.Model { @@ -19,6 +20,7 @@ public DimensionSet() /// the value for the dimension public DimensionSet(string key, string value) { + Validator.ValidateDimensionSet(key, value); Dimensions[key] = value; } @@ -31,6 +33,7 @@ public DimensionSet(string key, string value) /// the dimension value public void AddDimension(string key, string value) { + Validator.ValidateDimensionSet(key, value); if (Dimensions.Count >= Constants.MAX_DIMENSION_SET_SIZE) throw new DimensionSetExceededException(); diff --git a/src/Amazon.CloudWatch.EMF/Model/MetricsContext.cs b/src/Amazon.CloudWatch.EMF/Model/MetricsContext.cs index 44ab4a3..112799b 100644 --- a/src/Amazon.CloudWatch.EMF/Model/MetricsContext.cs +++ b/src/Amazon.CloudWatch.EMF/Model/MetricsContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Amazon.CloudWatch.EMF.Utils; namespace Amazon.CloudWatch.EMF.Model { @@ -102,6 +103,7 @@ public bool HasDefaultDimensions /// the units of the metric public void PutMetric(string key, double value, Unit unit) { + Validator.ValidateMetric(key, value); _metricDirective.PutMetric(key, value, unit); } diff --git a/src/Amazon.CloudWatch.EMF/Utils/Validator.cs b/src/Amazon.CloudWatch.EMF/Utils/Validator.cs new file mode 100644 index 0000000..bd9f665 --- /dev/null +++ b/src/Amazon.CloudWatch.EMF/Utils/Validator.cs @@ -0,0 +1,87 @@ +using System; +using System.Text; + +namespace Amazon.CloudWatch.EMF.Utils +{ + public class Validator + { + internal static void ValidateDimensionSet(in string dimensionName, in string dimensionValue) + { + if (dimensionName == null || dimensionName.Trim().Length == 0) + { + throw new InvalidDimensionException("Dimension name must include at least one non-whitespace character"); + } + + if (dimensionValue == null || dimensionValue.Trim().Length == 0) + { + throw new InvalidDimensionException("Dimension value must include at least one non-whitespace character"); + } + + if (dimensionName.Length > Constants.MAX_DIMENSION_NAME_LENGTH) + { + throw new InvalidDimensionException($"Dimension name cannot be longer than {Constants.MAX_DIMENSION_NAME_LENGTH} characters: {dimensionName}"); + } + + if (dimensionValue.Length > Constants.MAX_DIMENSION_VALUE_LENGTH) + { + throw new InvalidDimensionException($"Dimension value cannot be longer than {Constants.MAX_DIMENSION_VALUE_LENGTH} characters: {dimensionValue}"); + } + + if (!IsAscii(dimensionName)) + { + throw new InvalidDimensionException($"Dimension name contains invalid characters: {dimensionName}"); + } + + if (!IsAscii(dimensionValue)) + { + throw new InvalidDimensionException($"Dimension value contains invalid characters: {dimensionValue}"); + } + + if (dimensionName.StartsWith(":")) + { + throw new InvalidDimensionException("Dimension name cannot start with ':'"); + } + } + + internal static void ValidateMetric(in string name, in double value) + { + if (name == null || name.Trim().Length == 0) + { + throw new InvalidMetricException($"Metric name {name} must include at least one non-whitespace character"); + } + + if (name.Length > Constants.MAX_METRIC_NAME_LENGTH) + { + throw new InvalidMetricException($"Metric name {name} cannot be longer than {Constants.MAX_METRIC_NAME_LENGTH} characters"); + } + + if (!Double.IsFinite(value)) + { + throw new InvalidMetricException($"Metric value {value} must be a finite number"); + } + } + + internal static void ValidateNamespace(in string @namespace) + { + if (@namespace == null || @namespace.Trim().Length == 0) + { + throw new InvalidNamespaceException($"Namespace {@namespace} must include at least one non-whitespace character"); + } + + if (@namespace.Length > Constants.MAX_NAMESPACE_LENGTH) + { + throw new InvalidNamespaceException($"Namespace {@namespace} cannot be longer than {Constants.MAX_NAMESPACE_LENGTH} characters"); + } + + if (!System.Text.RegularExpressions.Regex.IsMatch(@namespace, Constants.VALID_NAMESPACE_REGEX)) + { + throw new InvalidNamespaceException($"Namespace {@namespace} contains invalid characters"); + } + } + + private static bool IsAscii(in string str) + { + return Encoding.UTF8.GetByteCount(str) == str.Length; + } + } +} diff --git a/tests/Amazon.CloudWatch.EMF.Tests/Logger/MetricsLoggerTests.cs b/tests/Amazon.CloudWatch.EMF.Tests/Logger/MetricsLoggerTests.cs index dd1efc0..e9936a9 100644 --- a/tests/Amazon.CloudWatch.EMF.Tests/Logger/MetricsLoggerTests.cs +++ b/tests/Amazon.CloudWatch.EMF.Tests/Logger/MetricsLoggerTests.cs @@ -31,6 +31,9 @@ public MetricsLoggerTests() _sink = new MockSink(); _environment.Sink.Returns(_sink); + _environment.LogGroupName.Returns("LogGroup"); + _environment.Name.Returns("Environment"); + _environment.Type.Returns("Type"); _environmentProvider.ResolveEnvironment().Returns(_environment); _metricsLogger = new MetricsLogger(_environmentProvider, _logger); @@ -146,6 +149,25 @@ public void TestSetNameSpace() Assert.Equal(namespaceValue, _sink.MetricsContext.Namespace); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("namespace ")] + [InlineData("ǹẚḿⱸṥṕấćē")] + [InlineData("name$pace")] + public void SetNamespace_WithInvalidNamespace_ThrowsInvalidNamespaceException(string namespaceValue) + { + Assert.Throws(() => _metricsLogger.SetNamespace(namespaceValue)); + } + + [Fact] + public void SetNamespace_WithNameTooLong_ThrowsInvalidNamespaceException() + { + string namespaceValue = new string('a', Constants.MAX_NAMESPACE_LENGTH + 1); + Assert.Throws(() => _metricsLogger.SetNamespace(namespaceValue)); + } + [Fact] public void TestFlushWithConfiguredServiceName() { @@ -216,6 +238,25 @@ public void TestPutMetricWithUnit() Assert.Equal(Unit.MILLISECONDS, metricDefinition.Unit); } + [Theory] + [InlineData(null, 1)] + [InlineData("", 1)] + [InlineData(" ", 1)] + [InlineData("metric", Double.PositiveInfinity)] + [InlineData("metric", Double.NegativeInfinity)] + [InlineData("metric", Double.NaN)] + public void PutMetric_WithInvalidMetric_ThrowsInvalidMetricException(string metricName, double metricValue) + { + Assert.Throws(() => _metricsLogger.PutMetric(metricName, metricValue, Unit.NONE)); + } + + [Fact] + public void PutMetric_WithNameTooLong_ThrowsInvalidMetricException() + { + string metricName = new string('a', Constants.MAX_METRIC_NAME_LENGTH + 1); + Assert.Throws(() => _metricsLogger.PutMetric(metricName, 1, Unit.NONE)); + } + [Fact] public void TestPutMetaData() { diff --git a/tests/Amazon.CloudWatch.EMF.Tests/Model/DimensionSetTests.cs b/tests/Amazon.CloudWatch.EMF.Tests/Model/DimensionSetTests.cs index 7da20b7..e8255bf 100644 --- a/tests/Amazon.CloudWatch.EMF.Tests/Model/DimensionSetTests.cs +++ b/tests/Amazon.CloudWatch.EMF.Tests/Model/DimensionSetTests.cs @@ -1,4 +1,5 @@ using Amazon.CloudWatch.EMF.Model; +using Amazon.CloudWatch.EMF.Utils; using Xunit; namespace Amazon.CloudWatch.EMF.Tests.Model @@ -25,6 +26,45 @@ public void AddDimension_30_Dimensions() Assert.Equal(dimensionSetSize, dimensionSet.DimensionKeys.Count); } + [Theory] + [InlineData(null, "value")] + [InlineData(" ", "value")] + [InlineData("ďïɱ", "value")] + [InlineData("dim", null)] + [InlineData("dim", " ")] + [InlineData("dim", "ⱱẵĺ")] + [InlineData(":dim", "val")] + public void AddDimension_WithInvalidValues_ThrowsInvalidDimensionException(string key, string value) + { + Assert.Throws(() => + { + var dimensionSet = new DimensionSet(); + dimensionSet.AddDimension(key, value); + }); + } + + [Fact] + public void AddDimension_WithNameTooLong_ThrowsInvalidDimensionException() + { + Assert.Throws(() => + { + var dimensionSet = new DimensionSet(); + string dimensionName = new string('a', Constants.MAX_DIMENSION_NAME_LENGTH + 1); + dimensionSet.AddDimension(dimensionName, "value"); + }); + } + + [Fact] + public void AddDimension_WithValueTooLong_ThrowsInvalidDimensionException() + { + Assert.Throws(() => + { + var dimensionSet = new DimensionSet(); + string dimensionValue = new string('a', Constants.MAX_DIMENSION_VALUE_LENGTH + 1); + dimensionSet.AddDimension("name", dimensionValue); + }); + } + [Fact] public void AddDimension_Limit_Exceeded_Error() {