diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f69847b59..7cb5064c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ This changelog will be used to generate documentation on [release notes page](http://azure.microsoft.com/en-us/documentation/articles/app-insights-release-notes-dotnet/). +## Version 2.3.0-beta1 +- Added metric aggregation functionality via MetricManager and Metric classes. +- Exposed a source field on RequestTelemetry. This can be used to store a representation of the component that issued the incoming http request. + ## Version 2.2.0 - Includes all changes since 2.1.0 stable release. diff --git a/GlobalStaticVersion.props b/GlobalStaticVersion.props index f5fb43f0aa..7fd2fea53c 100644 --- a/GlobalStaticVersion.props +++ b/GlobalStaticVersion.props @@ -6,14 +6,14 @@ Update for every public release. --> 2 - 2 + 3 0 - + beta1 - 2016-06-02 + 2016-11-11 .PreReleaseVersion $(MSBuildThisFileDirectory)$(PreReleaseVersionFileName) diff --git a/Test/CoreSDK.Test/Net46/Extensibility/Implementation/RichPayloadEventSourceTest.cs b/Test/CoreSDK.Test/Net46/Extensibility/Implementation/RichPayloadEventSourceTest.cs index 746def208e..d1d944cb5c 100644 --- a/Test/CoreSDK.Test/Net46/Extensibility/Implementation/RichPayloadEventSourceTest.cs +++ b/Test/CoreSDK.Test/Net46/Extensibility/Implementation/RichPayloadEventSourceTest.cs @@ -84,9 +84,11 @@ public void RichPayloadEventSourceMetricSentTest() { this.DoTracking( RichPayloadEventSource.Keywords.Metrics, +#pragma warning disable CS0618 new MetricTelemetry("TestMetric", 1), typeof(External.MetricData), (client, item) => { client.TrackMetric((MetricTelemetry)item); }); +#pragma warning restore CS0618 } /// diff --git a/Test/CoreSDK.Test/Shared/Core.Shared.Tests.projitems b/Test/CoreSDK.Test/Shared/Core.Shared.Tests.projitems index d722eba6ea..6905615c8e 100644 --- a/Test/CoreSDK.Test/Shared/Core.Shared.Tests.projitems +++ b/Test/CoreSDK.Test/Shared/Core.Shared.Tests.projitems @@ -27,10 +27,14 @@ + + + + @@ -65,6 +69,7 @@ + diff --git a/Test/CoreSDK.Test/Shared/DataContracts/MetricTelemetryTest.cs b/Test/CoreSDK.Test/Shared/DataContracts/MetricTelemetryTest.cs index f0c0519fd4..72b6621882 100644 --- a/Test/CoreSDK.Test/Shared/DataContracts/MetricTelemetryTest.cs +++ b/Test/CoreSDK.Test/Shared/DataContracts/MetricTelemetryTest.cs @@ -7,7 +7,6 @@ using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.VisualStudio.TestTools.UnitTesting; using Assert = Xunit.Assert; - [TestClass] public class MetricTelemetryTest @@ -32,18 +31,24 @@ public void EventTelemetryReturnsNonNullContext() Assert.NotNull(item.Context); } +#pragma warning disable CS0618 [TestMethod] public void MetricTelemetrySuppliesConstructorThatTakesNameAndValueToSimplifyAdvancedScenarios() { var instance = new MetricTelemetry("Test Metric", 4.2); + Assert.Equal("Test Metric", instance.Name); Assert.Equal(4.2, instance.Value); } +#pragma warning restore CS0618 [TestMethod] public void MetricTelemetrySuppliesPropertiesForCustomerToSendAggregatedMetric() { +#pragma warning disable CS0618 var instance = new MetricTelemetry("Test Metric", 4.2); +#pragma warning restore CS0618 + instance.Count = 5; instance.Min = 1.2; instance.Max = 6.4; @@ -54,38 +59,15 @@ public void MetricTelemetrySuppliesPropertiesForCustomerToSendAggregatedMetric() Assert.Equal(0.5, instance.StandardDeviation); } - [TestMethod] - public void MeasurementMetricTelemetrySerializesToJsonCorrectly() - { - var expected = new MetricTelemetry(); - expected.Name = "My Page"; - expected.Value = 42; - expected.Properties.Add("Property1", "Value1"); - - var item = TelemetryItemTestHelper.SerializeDeserializeTelemetryItem(expected); - - // NOTE: It's correct that we use the v1 name here, and therefore we test against it. - Assert.Equal(item.name, AI.ItemType.Metric); - - Assert.Equal(typeof(AI.MetricData).Name, item.data.baseType); - Assert.Equal(2, item.data.baseData.ver); - Assert.Equal(1, item.data.baseData.metrics.Count); - Assert.Equal(expected.Name, item.data.baseData.metrics[0].name); - Assert.Equal(AI.DataPointType.Measurement, item.data.baseData.metrics[0].kind); - Assert.Equal(expected.Value, item.data.baseData.metrics[0].value); - Assert.False(item.data.baseData.metrics[0].count.HasValue); - Assert.False(item.data.baseData.metrics[0].min.HasValue); - Assert.False(item.data.baseData.metrics[0].max.HasValue); - Assert.False(item.data.baseData.metrics[0].stdDev.HasValue); - Assert.Equal(expected.Properties.ToArray(), item.data.baseData.properties.ToArray()); - } - [TestMethod] public void AggregateMetricTelemetrySerializesToJsonCorrectly() { var expected = new MetricTelemetry(); + expected.Name = "My Page"; +#pragma warning disable CS0618 expected.Value = 42; +#pragma warning restore CS0618 expected.Count = 5; expected.Min = 1.2; expected.Max = 6.4; @@ -100,7 +82,9 @@ public void AggregateMetricTelemetrySerializesToJsonCorrectly() Assert.Equal(1, item.data.baseData.metrics.Count); Assert.Equal(expected.Name, item.data.baseData.metrics[0].name); Assert.Equal(AI.DataPointType.Aggregation, item.data.baseData.metrics[0].kind); +#pragma warning disable CS0618 Assert.Equal(expected.Value, item.data.baseData.metrics[0].value); +#pragma warning restore CS0618 Assert.Equal(expected.Count.Value, item.data.baseData.metrics[0].count.Value); Assert.Equal(expected.Min.Value, item.data.baseData.metrics[0].min.Value); Assert.Equal(expected.Max.Value, item.data.baseData.metrics[0].max.Value); @@ -109,6 +93,45 @@ public void AggregateMetricTelemetrySerializesToJsonCorrectly() Assert.Equal(expected.Properties.ToArray(), item.data.baseData.properties.ToArray()); } + [TestMethod] + public void MetricTelemetrySuppliesConstructorThatAllowsToFullyPopulateAggregationData() + { + var instance = new MetricTelemetry( + name: "Test Metric", + count: 4, + sum: 40, + min: 5, + max: 15, + standardDeviation: 4.2); + + Assert.Equal("Test Metric", instance.Name); + Assert.Equal(4, instance.Count); + Assert.Equal(40, instance.Sum); + Assert.Equal(5, instance.Min); + Assert.Equal(15, instance.Max); + Assert.Equal(4.2, instance.StandardDeviation); + } + + [TestMethod] + public void MetricTelemetrySuppliesPropertiesForCustomerToSendAggregionData() + { + var instance = new MetricTelemetry(); + + instance.Name = "Test Metric"; + instance.Count = 4; + instance.Sum = 40; + instance.Min = 5.0; + instance.Max = 15.0; + instance.StandardDeviation = 4.2; + + Assert.Equal("Test Metric", instance.Name); + Assert.Equal(4, instance.Count); + Assert.Equal(40, instance.Sum); + Assert.Equal(5, instance.Min); + Assert.Equal(15, instance.Max); + Assert.Equal(4.2, instance.StandardDeviation); + } + [TestMethod] public void MetricTelemetrySerializesStructuredIKeyCorrectlyPreservingCaseOfPrefix() { @@ -165,17 +188,19 @@ public void SerializeWritesNullValuesAsExpectedByEndpoint() Assert.Equal(2, item.data.baseData.ver); } +#pragma warning disable CS0618 [TestMethod] - public void SerializeReplacesNaNValueOn0() + public void SanitizeReplacesNaNValueOn0() { MetricTelemetry original = new MetricTelemetry("test", double.NaN); ((ITelemetry)original).Sanitize(); Assert.Equal(0, original.Value); } +#pragma warning restore CS0618 [TestMethod] - public void SerializeReplacesNaNMinOn0() + public void SanitizeReplacesNaNMinOn0() { MetricTelemetry original = new MetricTelemetry { Min = double.NaN }; ((ITelemetry)original).Sanitize(); @@ -184,7 +209,7 @@ public void SerializeReplacesNaNMinOn0() } [TestMethod] - public void SerializeReplacesNaNMaxOn0() + public void SanitizeReplacesNaNMaxOn0() { MetricTelemetry original = new MetricTelemetry { Max = double.NaN }; ((ITelemetry)original).Sanitize(); @@ -193,12 +218,55 @@ public void SerializeReplacesNaNMaxOn0() } [TestMethod] - public void SerializeReplacesNaNStandardDeviationOn0() + public void SanitizeReplacesNaNStandardDeviationOn0() { MetricTelemetry original = new MetricTelemetry { StandardDeviation = double.NaN }; ((ITelemetry)original).Sanitize(); Assert.Equal(0, original.StandardDeviation.Value); } + + [TestMethod] + public void SanitizeReplacesNaNSumOn0() + { + MetricTelemetry original = new MetricTelemetry(); + original.Name = "Test"; + original.Sum = double.NaN; + + ((ITelemetry)original).Sanitize(); + + Assert.Equal(0, original.Sum); + } + + [TestMethod] + public void SanitizeReplacesNegativeCountOn1() + { + MetricTelemetry original = new MetricTelemetry(); + original.Name = "Test"; + original.Count = -5; ; + + ((ITelemetry)original).Sanitize(); + + Assert.Equal(1, original.Count); + } + + [TestMethod] + public void SanitizeReplacesZeroCountOn1() + { + MetricTelemetry original = new MetricTelemetry(); + original.Name = "Test"; + + ((ITelemetry)original).Sanitize(); + + Assert.Equal(1, original.Count); + } + + [TestMethod] + public void CountPropertyGetterReturnsOneIfNoValueIsSet() + { + MetricTelemetry telemetry = new MetricTelemetry(); + + Assert.Equal(1, telemetry.Count); + } } } diff --git a/Test/CoreSDK.Test/Shared/EnumerableExtensions.cs b/Test/CoreSDK.Test/Shared/EnumerableExtensions.cs new file mode 100644 index 0000000000..492b83d87a --- /dev/null +++ b/Test/CoreSDK.Test/Shared/EnumerableExtensions.cs @@ -0,0 +1,28 @@ + +namespace Microsoft.ApplicationInsights +{ + using System; + using System.Collections.Generic; + using System.Linq; + + internal static class EnumerableExtensions + { + public static double StdDev(this IEnumerable sequence) + { + return StdDev(sequence, (e) => e); + } + + public static double StdDev(this IEnumerable sequence, Func selector) + { + if (sequence.Count() <= 0) + { + return 0; + } + + double avg = sequence.Average(selector); + double sum = sequence.Sum(e => Math.Pow(selector(e) - avg, 2)); + + return Math.Sqrt(sum / sequence.Count()); + } + } +} diff --git a/Test/CoreSDK.Test/Shared/Extensibility/MetricManagerTest.cs b/Test/CoreSDK.Test/Shared/Extensibility/MetricManagerTest.cs new file mode 100644 index 0000000000..a9ce7ddd00 --- /dev/null +++ b/Test/CoreSDK.Test/Shared/Extensibility/MetricManagerTest.cs @@ -0,0 +1,271 @@ +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Assert = Xunit.Assert; + + [TestClass] + public class MetricManagerTest + { + [TestMethod] + public void CanCreateMetricHavingNoDimensions() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + using (MetricManager manager = new MetricManager(client)) + { + // Act + Metric metric = manager.CreateMetric("Test Metric"); + metric.Track(42); + } + + // Assert (single metric aggregation exists in the output) + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", aggregatedMetric.Name); + + Assert.Equal(1, aggregatedMetric.Count); + Assert.Equal(42, aggregatedMetric.Sum); + + // note: interval duration property is auto-generated + Assert.Equal(1, aggregatedMetric.Properties.Count); + } + + [TestMethod] + public void CanCreateMetricExplicitlySettingDimensionsToNull() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + + using (MetricManager manager = new MetricManager(client)) + { + // Act + Metric metric = manager.CreateMetric("Test Metric", null); + metric.Track(42); + } + + // Assert + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", aggregatedMetric.Name); + + Assert.Equal(1, aggregatedMetric.Count); + Assert.Equal(42, aggregatedMetric.Sum); + + // note: interval duration property is auto-generated + Assert.Equal(1, aggregatedMetric.Properties.Count); + } + + [TestMethod] + public void CanCreateMetricWithASetOfDimensions() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + + var dimensions = new Dictionary { + { "Dim1", "Value1"}, + { "Dim2", "Value2"} + }; + + using (MetricManager manager = new MetricManager(client)) + { + // Act + Metric metric = manager.CreateMetric("Test Metric", dimensions); + metric.Track(42); + } + + // Assert + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", aggregatedMetric.Name); + + Assert.Equal(1, aggregatedMetric.Count); + Assert.Equal(42, aggregatedMetric.Sum); + + // note: interval duration property is auto-generated + Assert.Equal(3, aggregatedMetric.Properties.Count); + + Assert.Equal("Value1", aggregatedMetric.Properties["Dim1"]); + Assert.Equal("Value2", aggregatedMetric.Properties["Dim2"]); + } + + [TestMethod] + public void AggregatedMetricTelemetryHasIntervalDurationProperty() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + metric.Track(42); + } + + // Assert + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", aggregatedMetric.Name); + + Assert.Equal(1, aggregatedMetric.Count); + Assert.Equal(1, aggregatedMetric.Properties.Count); + + Assert.True(aggregatedMetric.Properties.ContainsKey("IntervalDurationMs")); + } + + [TestMethod] + public void AggregatedMetricTelemetryIntervalDurationPropertyIsPositiveInteger() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + metric.Track(42); + } + + // Assert + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", aggregatedMetric.Name); + + Assert.Equal(1, aggregatedMetric.Count); + Assert.Equal(1, aggregatedMetric.Properties.Count); + + Assert.True(aggregatedMetric.Properties.ContainsKey("IntervalDurationMs")); + Assert.True(long.Parse(aggregatedMetric.Properties["IntervalDurationMs"]) > 0); + } + + [TestMethod] + public void EqualMetricsAreCombinedIntoSignleAggregatedStatsStructure() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + + Metric metric1 = null; + Metric metric2 = null; + + using (MetricManager manager = new MetricManager(client)) + { + // note: on first go aggregators may be different because manager may + // snapshot after first got created but before the second + for (int i = 0; i < 2; i++) + { + metric1 = manager.CreateMetric("Test Metric"); + metric2 = manager.CreateMetric("Test Metric"); + + // Act + metric1.Track(10); + metric2.Track(5); + + manager.Flush(); + + if (sentTelemetry.Count == 1) + { + break; + } + else + { + sentTelemetry.Clear(); + } + } + } + + // Assert + Assert.Equal(1, sentTelemetry.Count); + + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal(2, aggregatedMetric.Count); + Assert.Equal(15, aggregatedMetric.Sum); + } + + [TestMethod] + public void CanDisposeMetricManagerMultipleTimes() + { + MetricManager manager = null; + + using (manager = new MetricManager()) { } + + Assert.DoesNotThrow(() => { manager.Dispose(); }); + } + + [TestMethod] + public void FlushCreatesAggregatedMetricTelemetry() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + metric.Track(42); + + // Act + manager.Flush(); + + // Assert + Assert.Equal(1, sentTelemetry.Count); + + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + Assert.NotNull(aggregatedMetric); + } + } + + [TestMethod] + public void DisposingManagerCreatesAggregatedMetricTelemetry() + { + // Arrange + var sentTelemetry = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry); + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + metric.Track(42); + + // Act + manager.Dispose(); + + // Assert + Assert.Equal(1, sentTelemetry.Count); + + var aggregatedMetric = (MetricTelemetry)sentTelemetry.Single(); + Assert.NotNull(aggregatedMetric); + } + } + + private TelemetryClient InitializeTelemetryClient(List sentTelemetry) + { + var channel = new StubTelemetryChannel { OnSend = t => sentTelemetry.Add(t) }; + var telemetryConfiguration = new TelemetryConfiguration { InstrumentationKey = Guid.NewGuid().ToString(), TelemetryChannel = channel }; + + var client = new TelemetryClient(telemetryConfiguration); + + return client; + } + } +} diff --git a/Test/CoreSDK.Test/Shared/Extensibility/MetricSample.cs b/Test/CoreSDK.Test/Shared/Extensibility/MetricSample.cs new file mode 100644 index 0000000000..c2e925bcab --- /dev/null +++ b/Test/CoreSDK.Test/Shared/Extensibility/MetricSample.cs @@ -0,0 +1,15 @@ + +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + + internal class MetricSample + { + public string Name { get; set; } + + public IDictionary Dimensions { get; set; } + + public double Value { get; set; } + } +} diff --git a/Test/CoreSDK.Test/Shared/Extensibility/MetricTest.cs b/Test/CoreSDK.Test/Shared/Extensibility/MetricTest.cs new file mode 100644 index 0000000000..53c784b510 --- /dev/null +++ b/Test/CoreSDK.Test/Shared/Extensibility/MetricTest.cs @@ -0,0 +1,421 @@ + +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Assert = Xunit.Assert; + + [TestClass] + public class MetricTest + { + [TestMethod] + public void MetricInvokesMetricProcessorsForEachValueTracked() + { + // Arrange + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + var dimensions = new Dictionary { + { "Dim1", "Value1"}, + { "Dim2", "Value2"} + }; + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric", dimensions); + + // Act + metric.Track(42); + } + + // Assert + var sample = (MetricSample)sentSamples.Single(); + + Assert.Equal("Test Metric", sample.Name); + + Assert.Equal(42, sample.Value); + + Assert.Equal("Value1", sample.Dimensions["Dim1"]); + Assert.Equal("Value2", sample.Dimensions["Dim2"]); + } + + [TestMethod] + public void MetricAggregatorCalculatesSampleCountCorrectly() + { + // Arrange + double[] testValues = { 4.45, 8, 29.21, 78.43, 0 }; + + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + for (int i = 0; i < testValues.Length; i++) + { + metric.Track(testValues[i]); + } + } + + // Assert + int sentSampleCount = sentTelemetry.Sum( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return (metric == null) || (!metric.Count.HasValue) ? 0 : metric.Count.Value; + }); + + Assert.Equal(testValues.Length, sentSampleCount); + } + + [TestMethod] + public void MetricAggregatorCalculatesSumCorrectly() + { + // Arrange + double[] testValues = { 4.45, 8, 29.21, 78.43, 0 }; + + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + for (int i = 0; i < testValues.Length; i++) + { + metric.Track(testValues[i]); + } + } + + // Assert + double sentSampleSum = sentTelemetry.Sum( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return metric == null ? 0 : metric.Sum; + }); + + Assert.Equal(testValues.Sum(), sentSampleSum); + } + + [TestMethod] + public void MetricAggregatorCalculatesMinCorrectly() + { + // Arrange + double[] testValues = { 4.45, 8, 29.21, 78.43, 1.4 }; + + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + for (int i = 0; i < testValues.Length; i++) + { + metric.Track(testValues[i]); + } + } + + // Assert + double sentSampleSum = sentTelemetry.Min( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return (metric == null) || (!metric.Min.HasValue) ? 0 : metric.Min.Value; + }); + + Assert.Equal(testValues.Min(), sentSampleSum); + } + + [TestMethod] + public void MetricAggregatorCalculatesMaxCorrectly() + { + // Arrange + double[] testValues = { 4.45, 8, 29.21, 78.43, 1.4 }; + + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + for (int i = 0; i < testValues.Length; i++) + { + metric.Track(testValues[i]); + } + } + + // Assert + double sentSampleMax = sentTelemetry.Max( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return (metric == null) || (!metric.Max.HasValue) ? 0 : metric.Max.Value; + }); + + Assert.Equal(testValues.Max(), sentSampleMax); + } + + [TestMethod] + public void MetricAggregatorCalculatesStandardDeviationCorrectly() + { + // Arrange + double[] testValues = { 1, 2, 3, 4, 5 }; + + var sentTelemetry = new List(); + var sentSamples = new List(); + + var client = this.InitializeTelemetryClient(sentTelemetry, sentSamples); + + using (MetricManager manager = new MetricManager(client)) + { + Metric metric = manager.CreateMetric("Test Metric"); + + // Act + for (int i = 0; i < testValues.Length; i++) + { + metric.Track(testValues[i]); + } + } + + // Assert + double sumOfSquares = sentTelemetry.Sum( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return + metric == null + ? 0 + : Math.Pow(metric.StandardDeviation.Value, 2) * metric.Count.Value + Math.Pow(metric.Sum, 2) / metric.Count.Value; + }); + + int count = sentTelemetry.Sum( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return metric == null ? 0 : metric.Count.Value; + }); + + double sum = sentTelemetry.Sum( + (telemetry) => { + var metric = telemetry as MetricTelemetry; + return metric == null ? 0 : metric.Sum; + }); + + double stddev = Math.Sqrt(sumOfSquares / count - Math.Pow(sum / count, 2)); + + Assert.Equal(testValues.StdDev(), stddev); + } + + #region Equitable implementation tests + + [TestMethod] + public void MetricNeverEqualsNull() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + object other = null; + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricEqualsItself() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + + Assert.True(metric.Equals(metric)); + } + } + + [TestMethod] + public void MetricNotEqualsOtherObject() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + var other = new object(); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricsAreEqualForTheSameMetricNameWithoutDimensions() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + Metric other = manager.CreateMetric("My metric"); + + Assert.True(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricNameIsCaseSensitive() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + Metric other = manager.CreateMetric("My Metric"); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricNameIsAccentSensitive() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric"); + Metric other = manager.CreateMetric("My métric"); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricsAreEqualIfDimensionsSetToNothingImplicitlyAndExplicitly() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric", null); + Metric other = manager.CreateMetric("My metric"); + + Assert.True(metric.Equals(other)); + } + } + + [TestMethod] + public void MetricsAreEqualIfDimensionsSetToNothingImplicitlyAndExplicitlyAsEmptySet() + { + using (var manager = new MetricManager()) + { + Metric metric = manager.CreateMetric("My metric", new Dictionary()); + Metric other = manager.CreateMetric("My metric"); + + Assert.True(metric.Equals(other)); + } + } + + [TestMethod] + public void DimensionsAreOrderInsensitive() + { + using (var manager = new MetricManager()) + { + var dimensionSet1 = new Dictionary() { + { "Dim1", "Value1"}, + { "Dim2", "Value2"}, + }; + + var dimensionSet2 = new Dictionary() { + { "Dim2", "Value2"}, + { "Dim1", "Value1"}, + }; + + Metric metric = manager.CreateMetric("My metric", dimensionSet1); + Metric other = manager.CreateMetric("My metric", dimensionSet2); + + Assert.True(metric.Equals(other)); + } + } + + [TestMethod] + public void DimensionNamesAreCaseSensitive() + { + using (var manager = new MetricManager()) + { + var dimensionSet1 = new Dictionary() { { "Dim1", "Value1" } }; + var dimensionSet2 = new Dictionary() { { "dim1", "Value1" } }; + + Metric metric = manager.CreateMetric("My metric", dimensionSet1); + Metric other = manager.CreateMetric("My metric", dimensionSet2); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void DimensionNamesAreAccentSensitive() + { + using (var manager = new MetricManager()) + { + var dimensionSet1 = new Dictionary() { { "Dim1", "Value1" } }; + var dimensionSet2 = new Dictionary() { { "Dím1", "Value1" } }; + + Metric metric = manager.CreateMetric("My metric", dimensionSet1); + Metric other = manager.CreateMetric("My metric", dimensionSet2); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void DimensionValuesAreCaseSensitive() + { + using (var manager = new MetricManager()) + { + var dimensionSet1 = new Dictionary() { { "Dim1", "Value1" } }; + var dimensionSet2 = new Dictionary() { { "Dim1", "value1" } }; + + Metric metric = manager.CreateMetric("My metric", dimensionSet1); + Metric other = manager.CreateMetric("My metric", dimensionSet2); + + Assert.False(metric.Equals(other)); + } + } + + [TestMethod] + public void DimensionValuesAreAccentSensitive() + { + using (var manager = new MetricManager()) + { + var dimensionSet1 = new Dictionary() { { "Dim1", "Value1" } }; + var dimensionSet2 = new Dictionary() { { "Dim1", "Válue1" } }; + + Metric metric = manager.CreateMetric("My metric", dimensionSet1); + Metric other = manager.CreateMetric("My metric", dimensionSet2); + + Assert.False(metric.Equals(other)); + } + } + + #endregion + + private TelemetryClient InitializeTelemetryClient(List sentTelemetry, List sentSamples) + { + var channel = new StubTelemetryChannel { OnSend = t => sentTelemetry.Add(t) }; + + var telemetryConfiguration = new TelemetryConfiguration { InstrumentationKey = Guid.NewGuid().ToString(), TelemetryChannel = channel }; + telemetryConfiguration.MetricProcessors.Add(new StubMetricProcessor(sentSamples)); + + var client = new TelemetryClient(telemetryConfiguration); + + return client; + } + } +} + \ No newline at end of file diff --git a/Test/CoreSDK.Test/Shared/Extensibility/StubMetricProcessor.cs b/Test/CoreSDK.Test/Shared/Extensibility/StubMetricProcessor.cs new file mode 100644 index 0000000000..5bc1eb6024 --- /dev/null +++ b/Test/CoreSDK.Test/Shared/Extensibility/StubMetricProcessor.cs @@ -0,0 +1,31 @@ +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + + internal class StubMetricProcessor : IMetricProcessor + { + private IList sampleList; + + public StubMetricProcessor(IList sampleList) + { + if (sampleList == null) + { + throw new ArgumentNullException("sampleList"); + } + + this.sampleList = sampleList; + } + + public void Track(Metric metric, double value) + { + this.sampleList.Add( + new MetricSample() + { + Name = metric.Name, + Dimensions = metric.Dimensions, + Value = value + }); + } + } +} diff --git a/Test/CoreSDK.Test/Shared/Extensibility/TelemetryConfigurationTest.cs b/Test/CoreSDK.Test/Shared/Extensibility/TelemetryConfigurationTest.cs index 5fa6894769..40c4e7d537 100644 --- a/Test/CoreSDK.Test/Shared/Extensibility/TelemetryConfigurationTest.cs +++ b/Test/CoreSDK.Test/Shared/Extensibility/TelemetryConfigurationTest.cs @@ -277,6 +277,24 @@ public void TelemetryProcessorsCollectionIsReadOnly() #endregion + #region MetricProcessors + + [TestMethod] + public void MetricProcessorsReturnsAnEmptyListByDefaultToAvoidNullReferenceExceptionsInUserCode() + { + var configuration = new TelemetryConfiguration(); + Assert.Equal(0, configuration.MetricProcessors.Count); + } + + [TestMethod] + public void MetricPrcessorsReturnsThreadSafeList() + { + var configuration = new TelemetryConfiguration(); + Assert.Equal(typeof(SnapshottingList), configuration.MetricProcessors.GetType()); + } + + #endregion + #region Serialized Configuration [TestMethod] public void TelemetryConfigThrowsIfSerializedConfigIsNull() diff --git a/Test/CoreSDK.Test/Shared/TelemetryClientTest.cs b/Test/CoreSDK.Test/Shared/TelemetryClientTest.cs index 58b8ad2ea9..78c17263c2 100644 --- a/Test/CoreSDK.Test/Shared/TelemetryClientTest.cs +++ b/Test/CoreSDK.Test/Shared/TelemetryClientTest.cs @@ -143,17 +143,50 @@ public void InitializeDoesNotOverrideNodeName() #region TrackMetric + [TestMethod] + public void TrackMetricSendsSpecifiedAggregatedMetricTelemetry() + { + var sentTelemetry = new List(); + var client = this.InitializeTelemetryClient(sentTelemetry); + + client.TrackMetric( + new MetricTelemetry() + { + Name = "Test Metric", + Count = 5, + Sum = 40, + Min = 3.0, + Max = 4.0, + StandardDeviation = 1.0 + }); + + var metric = (MetricTelemetry)sentTelemetry.Single(); + + Assert.Equal("Test Metric", metric.Name); + Assert.Equal(5, metric.Count); + Assert.Equal(40, metric.Sum); + Assert.Equal(3.0, metric.Min); + Assert.Equal(4.0, metric.Max); + Assert.Equal(1.0, metric.StandardDeviation); + } + [TestMethod] public void TrackMetricSendsMetricTelemetryWithSpecifiedNameAndValue() { var sentTelemetry = new List(); var client = this.InitializeTelemetryClient(sentTelemetry); +#pragma warning disable CS0618 client.TrackMetric("TestMetric", 42); +#pragma warning restore CS0618 var metric = (MetricTelemetry)sentTelemetry.Single(); + Assert.Equal("TestMetric", metric.Name); + +#pragma warning disable CS0618 Assert.Equal(42, metric.Value); +#pragma warning restore CS0618 } [TestMethod] @@ -162,11 +195,17 @@ public void TrackMetricSendsSpecifiedMetricTelemetry() var sentTelemetry = new List(); var client = this.InitializeTelemetryClient(sentTelemetry); +#pragma warning disable CS0618 client.TrackMetric(new MetricTelemetry("TestMetric", 42)); +#pragma warning restore CS0618 var metric = (MetricTelemetry)sentTelemetry.Single(); + Assert.Equal("TestMetric", metric.Name); + +#pragma warning disable CS0618 Assert.Equal(42, metric.Value); +#pragma warning restore CS0618 } [TestMethod] @@ -175,11 +214,18 @@ public void TrackMetricSendsMetricTelemetryWithGivenNameValueAndProperties() var sentTelemetry = new List(); var client = this.InitializeTelemetryClient(sentTelemetry); +#pragma warning disable CS0618 client.TrackMetric("TestMetric", 4.2, new Dictionary { { "blah", "yoyo" } }); +#pragma warning restore CS0618 var metric = (MetricTelemetry)sentTelemetry.Single(); + Assert.Equal("TestMetric", metric.Name); + +#pragma warning disable CS0618 Assert.Equal(4.2, metric.Value); +#pragma warning restore CS0618 + Assert.Equal("yoyo", metric.Properties["blah"]); } @@ -189,11 +235,16 @@ public void TrackMetricIgnoresNullPropertiesArgumentToAvoidCrashingUserApp() var sentTelemetry = new List(); var client = this.InitializeTelemetryClient(sentTelemetry); +#pragma warning disable CS0618 client.TrackMetric("TestMetric", 4.2, null); +#pragma warning restore CS0618 var metric = (MetricTelemetry)sentTelemetry.Single(); + Assert.Equal("TestMetric", metric.Name); +#pragma warning disable CS0618 Assert.Equal(4.2, metric.Value); +#pragma warning restore CS0618 Assert.Empty(metric.Properties); } diff --git a/Test/ServerTelemetryChannel.Test/Shared.Tests/Implementation/TransmissionSenderTest.cs b/Test/ServerTelemetryChannel.Test/Shared.Tests/Implementation/TransmissionSenderTest.cs index c592d25899..dc66dc5075 100644 --- a/Test/ServerTelemetryChannel.Test/Shared.Tests/Implementation/TransmissionSenderTest.cs +++ b/Test/ServerTelemetryChannel.Test/Shared.Tests/Implementation/TransmissionSenderTest.cs @@ -257,7 +257,7 @@ public void IsRaisedWhenTransmissionIsThrottledLocallyWithItems() var telemetryItems = new List(); for (var i=0; i(); for (var i = 0; i < sender.ThrottleLimit + 10; i++) { - telemetryItems.Add(new DataContracts.MetricTelemetry()); + telemetryItems.Add(new DataContracts.EventTelemetry()); } var wrapper = new HttpWebResponseWrapper(); diff --git a/Test/ServerTelemetryChannel.Test/Shared.Tests/SamplingTelemetryProcessorTest.cs b/Test/ServerTelemetryChannel.Test/Shared.Tests/SamplingTelemetryProcessorTest.cs index ec57e279ee..af1648df44 100644 --- a/Test/ServerTelemetryChannel.Test/Shared.Tests/SamplingTelemetryProcessorTest.cs +++ b/Test/ServerTelemetryChannel.Test/Shared.Tests/SamplingTelemetryProcessorTest.cs @@ -95,17 +95,17 @@ public void ExceptionTelemetryIsSubjectToSampling() { TelemetryTypeSupportsSampling(telemetryProcessors => telemetryProcessors.Process(new ExceptionTelemetry(new Exception("exception")))); } - + [TestMethod] public void MetricTelemetryIsNotSubjectToSampling() { TelemetryTypeDoesNotSupportSampling(telemetryProcessors => { - telemetryProcessors.Process(new MetricTelemetry("metric", 1.0)); + telemetryProcessors.Process(new MetricTelemetry() { Count = 1, Sum = 1.0 } ); return 1; }); } - + [TestMethod] public void PageViewTelemetryIsSubjectToSampling() { diff --git a/src/Core/Managed/Net45/Extensibility/Implementation/RichPayloadEventSource.TelemetryHandler.cs b/src/Core/Managed/Net45/Extensibility/Implementation/RichPayloadEventSource.TelemetryHandler.cs index 6f1371fe7a..d8d8e3cbee 100644 --- a/src/Core/Managed/Net45/Extensibility/Implementation/RichPayloadEventSource.TelemetryHandler.cs +++ b/src/Core/Managed/Net45/Extensibility/Implementation/RichPayloadEventSource.TelemetryHandler.cs @@ -117,6 +117,7 @@ private Action CreateHandlerForRequestTelemetry(EventSource eventSou { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as RequestTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -171,6 +172,7 @@ private Action CreateHandlerForTraceTelemetry(EventSource eventSourc { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as TraceTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -219,6 +221,7 @@ private Action CreateHandlerForEventTelemetry(EventSource eventSourc { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as EventTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -274,6 +277,7 @@ private Action CreateHandlerForDependencyTelemetry(EventSource event { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as DependencyTelemetry; var data = telemetryItem.InternalData; var extendedData = new @@ -342,6 +346,7 @@ private Action CreateHandlerForMetricTelemetry(EventSource eventSour { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as MetricTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -426,6 +431,7 @@ private Action CreateHandlerForExceptionTelemetry(EventSource eventS { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as ExceptionTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -505,6 +511,7 @@ private Action CreateHandlerForPerformanceCounterTelemetry(EventSour { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); #pragma warning disable 618 var telemetryItem = (item as PerformanceCounterTelemetry).Data; #pragma warning restore 618 @@ -565,6 +572,7 @@ private Action CreateHandlerForPageViewTelemetry(EventSource eventSo { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); var telemetryItem = item as PageViewTelemetry; var data = telemetryItem.Data; var extendedData = new @@ -615,6 +623,7 @@ private Action CreateHandlerForSessionStateTelemetry(EventSource eve { if (this.EventSourceInternal.IsEnabled(EventLevel.Verbose, keywords)) { + item.Sanitize(); #pragma warning disable 618 var telemetryItem = (item as SessionStateTelemetry).Data; #pragma warning restore 618 diff --git a/src/Core/Managed/Net46/Extensibility/Implementation/RichPayloadEventSource.cs b/src/Core/Managed/Net46/Extensibility/Implementation/RichPayloadEventSource.cs index e27b7acb49..d56be7e04a 100644 --- a/src/Core/Managed/Net46/Extensibility/Implementation/RichPayloadEventSource.cs +++ b/src/Core/Managed/Net46/Extensibility/Implementation/RichPayloadEventSource.cs @@ -53,6 +53,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as RequestTelemetry; this.WriteEvent( RequestTelemetry.TelemetryName, @@ -68,6 +69,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as TraceTelemetry; this.WriteEvent( TraceTelemetry.TelemetryName, @@ -83,6 +85,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as EventTelemetry; this.WriteEvent( EventTelemetry.TelemetryName, @@ -98,6 +101,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as DependencyTelemetry; this.WriteEvent( DependencyTelemetry.TelemetryName, @@ -113,6 +117,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as MetricTelemetry; this.WriteEvent( MetricTelemetry.TelemetryName, @@ -128,6 +133,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as ExceptionTelemetry; this.WriteEvent( ExceptionTelemetry.TelemetryName, @@ -144,6 +150,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = (item as PerformanceCounterTelemetry).Data; this.WriteEvent( MetricTelemetry.TelemetryName, @@ -160,6 +167,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as PageViewTelemetry; this.WriteEvent( PageViewTelemetry.TelemetryName, @@ -176,6 +184,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = (item as SessionStateTelemetry).Data; this.WriteEvent( EventTelemetry.TelemetryName, @@ -191,6 +200,7 @@ public void Process(ITelemetry item) return; } + item.Sanitize(); var telemetryItem = item as AvailabilityTelemetry; this.WriteEvent( AvailabilityTelemetry.TelemetryName, diff --git a/src/Core/Managed/Shared/DataContracts/MetricTelemetry.cs b/src/Core/Managed/Shared/DataContracts/MetricTelemetry.cs index 805f77ee5a..b937e77c54 100644 --- a/src/Core/Managed/Shared/DataContracts/MetricTelemetry.cs +++ b/src/Core/Managed/Shared/DataContracts/MetricTelemetry.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.ComponentModel; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Extensibility.Implementation.External; @@ -18,8 +19,6 @@ public sealed class MetricTelemetry : ITelemetry, ISupportProperties internal readonly MetricData Data; internal readonly DataPoint Metric; - private bool isAggregation = false; - /// /// Initializes a new instance of the class with empty /// properties. @@ -30,6 +29,8 @@ public MetricTelemetry() this.Metric = new DataPoint(); this.Context = new TelemetryContext(this.Data.properties); + this.Metric.kind = DataPointType.Aggregation; + // We always have a single 'metric'. this.Data.metrics.Add(this.Metric); } @@ -39,12 +40,42 @@ public MetricTelemetry() /// specified and . /// /// The is null or empty string. + [Obsolete("This constructor is obsolete. Use different constructor of this class to represent aggregated metric data or use EventTelemetry type to represent individual events.")] public MetricTelemetry(string metricName, double metricValue) : this() { this.Name = metricName; this.Value = metricValue; } + /// + /// Initializes a new instance of the class with properties provided. + /// + /// + /// Metric statistics provided are assumed to be calculated over a period of time equaling 1 minute. + /// + /// Metric name. + /// Count of values taken during aggregation interval. + /// Sum of values taken during aggregation interval. + /// Minimum value taken during aggregation interval. + /// Maximum of values taken during aggregation interval. + /// Standard deviation of values taken during aggregation interval. + public MetricTelemetry( + string name, + int count, + double sum, + double min, + double max, + double standardDeviation) + : this() + { + this.Name = name; + this.Count = count; + this.Sum = sum; + this.Min = min; + this.Max = max; + this.StandardDeviation = standardDeviation; + } + /// /// Gets or sets date and time when event was recorded. /// @@ -68,31 +99,33 @@ public string Name get { return this.Metric.name; } set { this.Metric.name = value; } } - + /// /// Gets or sets the value of this metric. /// + [Obsolete("This property is obsolete. Use Sum property instead.")] public double Value { get { return this.Metric.value; } set { this.Metric.value = value; } } + /// + /// Gets or sets sum of the values of the metric samples. + /// + public double Sum + { + get { return this.Metric.value; } + set { this.Metric.value = value; } + } + /// /// Gets or sets the number of samples for this metric. /// public int? Count { - get - { - return this.Metric.count; - } - - set - { - this.Metric.count = value; - this.UpdateKind(); - } + get { return this.Metric.count.HasValue ? this.Metric.count : 1; } + set { this.Metric.count = value; } } /// @@ -100,16 +133,8 @@ public double Value /// public double? Min { - get - { - return this.Metric.min; - } - - set - { - this.Metric.min = value; - this.UpdateKind(); - } + get { return this.Metric.min; } + set { this.Metric.min = value; } } /// @@ -117,16 +142,8 @@ public double Value /// public double? Max { - get - { - return this.Metric.max; - } - - set - { - this.Metric.max = value; - this.UpdateKind(); - } + get { return this.Metric.max; } + set { this.Metric.max = value; } } /// @@ -134,16 +151,8 @@ public double Value /// public double? StandardDeviation { - get - { - return this.Metric.stdDev; - } - - set - { - this.Metric.stdDev = value; - this.UpdateKind(); - } + get { return this.Metric.stdDev; } + set { this.Metric.stdDev = value; } } /// @@ -162,7 +171,12 @@ void ITelemetry.Sanitize() this.Name = this.Name.SanitizeName(); this.Name = Utils.PopulateRequiredStringValue(this.Name, "name", typeof(MetricTelemetry).FullName); this.Properties.SanitizeProperties(); - this.Value = Utils.SanitizeNanAndInfinity(this.Value); + this.Sum = Utils.SanitizeNanAndInfinity(this.Sum); + + // note: we set count to 1 if it isn't a postitive integer + // thinking that if it is zero (negative case is clearly broken) + // that most likely means somebody created instance but forgot to set count + this.Count = (!this.Count.HasValue) || (this.Count <= 0) ? 1 : this.Count; if (this.Min.HasValue) { @@ -181,17 +195,5 @@ void ITelemetry.Sanitize() this.Context.SanitizeTelemetryContext(); } - - private void UpdateKind() - { - bool isAggregation = this.Metric.count != null || this.Metric.min != null || this.Metric.max != null || this.Metric.stdDev != null; - - if (this.isAggregation != isAggregation) - { - this.Metric.kind = isAggregation ? DataPointType.Aggregation : DataPointType.Measurement; - } - - this.isAggregation = isAggregation; - } } } diff --git a/src/Core/Managed/Shared/Extensibility/IMetricProcessor.cs b/src/Core/Managed/Shared/Extensibility/IMetricProcessor.cs new file mode 100644 index 0000000000..39205e027c --- /dev/null +++ b/src/Core/Managed/Shared/Extensibility/IMetricProcessor.cs @@ -0,0 +1,18 @@ +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + + /// + /// Provides functionality to process metric values prior to aggregation. + /// + public interface IMetricProcessor + { + /// + /// Process metric value. + /// + /// Metric definition. + /// Metric value. + void Track(Metric metric, double value); + } +} diff --git a/src/Core/Managed/Shared/Extensibility/Implementation/SimpleMetricStatisticsAggregator.cs b/src/Core/Managed/Shared/Extensibility/Implementation/SimpleMetricStatisticsAggregator.cs new file mode 100644 index 0000000000..6d6439292e --- /dev/null +++ b/src/Core/Managed/Shared/Extensibility/Implementation/SimpleMetricStatisticsAggregator.cs @@ -0,0 +1,116 @@ +namespace Microsoft.ApplicationInsights.Extensibility.Implementation +{ + using System; + using System.Threading; + + /// + /// Represents mechanism to calculate basic statistical parameters of a series of numeric values. + /// + internal class SimpleMetricStatisticsAggregator + { + /// + /// Lock to make Track() method thread-safe. + /// + private SpinLock trackLock = new SpinLock(); + + /// + /// Initializes a new instance of the class. + /// + internal SimpleMetricStatisticsAggregator() + { + } + + /// + /// Gets sample count. + /// + internal int Count { get; private set; } + + /// + /// Gets sum of the samples. + /// + internal double Sum { get; private set; } + + /// + /// Gets sum of squares of the samples. + /// + internal double SumOfSquares { get; private set; } + + /// + /// Gets minimum sample value. + /// + internal double Min { get; private set; } + + /// + /// Gets maximum sample value. + /// + internal double Max { get; private set; } + + /// + /// Gets arithmetic average value in the population. + /// + internal double Average + { + get + { + return this.Count == 0 ? 0 : this.Sum / this.Count; + } + } + + /// + /// Gets variance of the values in the population. + /// + internal double Variance + { + get + { + return this.Count == 0 ? 0 : (this.SumOfSquares / this.Count) - (this.Average * this.Average); + } + } + + /// + /// Gets standard deviation of the values in the population. + /// + internal double StandardDeviation + { + get + { + return Math.Sqrt(this.Variance); + } + } + + /// + /// Adds a value to the time series. + /// + /// Metric value. + public void Track(double value) + { + bool lockAcquired = false; + + try + { + this.trackLock.Enter(ref lockAcquired); + + if ((this.Count == 0) || (value < this.Min)) + { + this.Min = value; + } + + if ((this.Count == 0) || (value > this.Max)) + { + this.Max = value; + } + + this.Count++; + this.Sum += value; + this.SumOfSquares += value * value; + } + finally + { + if (lockAcquired) + { + this.trackLock.Exit(); + } + } + } + } +} diff --git a/src/Core/Managed/Shared/Extensibility/Implementation/Tracing/CoreEventSource.cs b/src/Core/Managed/Shared/Extensibility/Implementation/Tracing/CoreEventSource.cs index 47fdaa0484..4a9c6b98a5 100644 --- a/src/Core/Managed/Shared/Extensibility/Implementation/Tracing/CoreEventSource.cs +++ b/src/Core/Managed/Shared/Extensibility/Implementation/Tracing/CoreEventSource.cs @@ -324,6 +324,43 @@ public void FailedToGetMachineName(string error, string appDomainName = "Incorre this.nameProvider.Name); } + [Event( + 26, + Message = "Failed to flush aggregated metrics. Exception: {0}.", + Level = EventLevel.Error)] + public void FailedToFlushMetricAggregators(string ex, string appDomainName = "Incorrect") + { + this.WriteEvent( + 26, + ex ?? string.Empty, + this.nameProvider.Name); + } + + [Event( + 27, + Message = "Failed to snapshot aggregated metrics. Exception: {0}.", + Level = EventLevel.Error)] + public void FailedToSnapshotMetricAggregators(string ex, string appDomainName = "Incorrect") + { + this.WriteEvent( + 27, + ex ?? string.Empty, + this.nameProvider.Name); + } + + [Event( + 28, + Message = "Failed to invoke metric processor '{0}'. If the issue persists, remove the processor. Exception: {1}.", + Level = EventLevel.Error)] + public void FailedToRunMetricProcessor(string processorName, string ex, string appDomainName = "Incorrect") + { + this.WriteEvent( + 28, + processorName ?? string.Empty, + ex ?? string.Empty, + this.nameProvider.Name); + } + /// /// Keywords for the PlatformEventSource. /// diff --git a/src/Core/Managed/Shared/Extensibility/Metric.cs b/src/Core/Managed/Shared/Extensibility/Metric.cs new file mode 100644 index 0000000000..5aa9a0d3b4 --- /dev/null +++ b/src/Core/Managed/Shared/Extensibility/Metric.cs @@ -0,0 +1,175 @@ +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + + /// + /// Represents aggregator for a single time series of a given metric. + /// + public class Metric : IEquatable + { + /// + /// Aggregator manager for the aggregator. + /// + private readonly MetricManager manager; + + /// + /// Metric aggregator id to look for in the aggregator dictionary. + /// + private readonly string aggregatorId; + + /// + /// Aggregator hash code. + /// + private readonly int hashCode; + + /// + /// Initializes a new instance of the class. + /// + /// Aggregator manager handling this instance. + /// Metric name. + /// Metric dimensions. + internal Metric( + MetricManager manager, + string name, + IDictionary dimensions = null) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + + this.manager = manager; + this.Name = name; + this.Dimensions = dimensions; + + this.aggregatorId = Metric.GetAggregatorId(name, dimensions); + this.hashCode = this.aggregatorId.GetHashCode(); + } + + /// + /// Gets metric name. + /// + public string Name { get; private set; } + + /// + /// Gets a set of metric dimensions and their values. + /// + public IDictionary Dimensions { get; private set; } + + /// + /// Adds a value to the time series. + /// + /// Metric value. + public void Track(double value) + { + SimpleMetricStatisticsAggregator aggregator = this.manager.GetStatisticsAggregator(this); + aggregator.Track(value); + + this.ForwardToProcessors(value); + } + + /// + /// Returns the hash code for this object. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return this.hashCode; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// True if the specified object is equal to the current object; otherwise, false. + public bool Equals(Metric other) + { + if (other == null) + { + return false; + } + + return this.aggregatorId.Equals(other.aggregatorId, StringComparison.Ordinal); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// True if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return this.Equals(obj as Metric); + } + + /// + /// Generates id of the aggregator serving time series specified in the parameters. + /// + /// Metric name. + /// Optional metric dimensions. + /// Aggregator id that can be used to get aggregator. + private static string GetAggregatorId(string name, IDictionary dimensions = null) + { + StringBuilder aggregatorIdBuilder = new StringBuilder(name ?? "n/a"); + + if (dimensions != null) + { + var sortedDimensions = dimensions.OrderBy((pair) => { return pair.Key; }); + + foreach (KeyValuePair pair in sortedDimensions) + { + aggregatorIdBuilder.AppendFormat(CultureInfo.InvariantCulture, "\n{0}\t{1}", pair.Key ?? string.Empty, pair.Value ?? string.Empty); + } + } + + return aggregatorIdBuilder.ToString(); + } + + /// + /// Forwards value to metric processors. + /// + /// Value tracked on time series. + private void ForwardToProcessors(double value) + { + // create a local reference to metric processor collection + // if collection changes after that - it will be copied not affecting local reference + IList metricProcessors = this.manager.MetricProcessors; + + if (metricProcessors != null) + { + int processorCount = metricProcessors.Count; + + for (int i = 0; i < processorCount; i++) + { + IMetricProcessor processor = metricProcessors[i]; + + try + { + processor.Track(this, value); + } + catch (Exception ex) + { + CoreEventSource.Log.FailedToRunMetricProcessor(processor.GetType().FullName, ex.ToString()); + } + } + } + } + } +} diff --git a/src/Core/Managed/Shared/Extensibility/MetricManager.cs b/src/Core/Managed/Shared/Extensibility/MetricManager.cs new file mode 100644 index 0000000000..3fa46837ad --- /dev/null +++ b/src/Core/Managed/Shared/Extensibility/MetricManager.cs @@ -0,0 +1,256 @@ +namespace Microsoft.ApplicationInsights.Extensibility +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + +#if CORE_PCL || NET45 || NET46 + using TaskEx = System.Threading.Tasks.Task; +#endif + + /// + /// Metric factory and controller. + /// + public sealed class MetricManager : IDisposable + { + /// + /// Name of the property added to aggregation results to indicate duration of the aggregation interval. + /// + private static string intervalDurationPropertyName = "IntervalDurationMs"; + + /// + /// Reporting frequency. + /// + private static TimeSpan aggregationPeriod = TimeSpan.FromMinutes(1); + + /// + /// Telemetry client used to track resulting aggregated metrics. + /// + private readonly TelemetryClient telemetryClient; + + /// + /// Metric aggregation snapshot task. + /// + private TaskTimer snapshotTimer; + + /// + /// Last time snapshot was initiated. + /// + private DateTimeOffset lastSnapshotStartDateTime; + + /// + /// A dictionary of all metrics instantiated via this manager. + /// + private ConcurrentDictionary metricDictionary; + + /// + /// Initializes a new instance of the class. + /// + public MetricManager() + : this(new TelemetryClient()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Telemetry client to use to output aggregated metric data. + public MetricManager(TelemetryClient client) + { + this.telemetryClient = client ?? new TelemetryClient(); + this.metricDictionary = new ConcurrentDictionary(); + + this.lastSnapshotStartDateTime = DateTimeOffset.UtcNow; + + this.snapshotTimer = new TaskTimer() { Delay = GetWaitTime() }; + this.snapshotTimer.Start(this.SnapshotAndReschedule); + } + + /// + /// Gets a list of metric processors associated + /// with this instance of . + /// + internal IList MetricProcessors + { + get + { + TelemetryConfiguration config = this.telemetryClient.TelemetryConfiguration; + + return config.MetricProcessors; + } + } + + /// + /// Creates metric. + /// + /// Name of the metric. + /// Optional dimensions. + /// Metric instance. + public Metric CreateMetric(string name, IDictionary dimensions = null) + { + return new Metric(this, name, dimensions); + } + + /// + /// Flushes the in-memory aggregation buffers. + /// + public void Flush() + { + try + { + this.Snapshot(); + this.telemetryClient.Flush(); + } + catch (Exception ex) + { + CoreEventSource.Log.FailedToFlushMetricAggregators(ex.ToString()); + } + } + + /// + /// Disposes the object. + /// + public void Dispose() + { + if (this.snapshotTimer != null) + { + this.snapshotTimer.Dispose(); + this.snapshotTimer = null; + } + + this.Flush(); + } + + internal SimpleMetricStatisticsAggregator GetStatisticsAggregator(Metric metric) + { + return this.metricDictionary.GetOrAdd(metric, (m) => { return new SimpleMetricStatisticsAggregator(); }); + } + + /// + /// Calculates wait time until next snapshot of the aggregators. + /// + /// Wait time. + private static TimeSpan GetWaitTime() + { + DateTimeOffset currentTime = DateTimeOffset.UtcNow; + + double minutesFromZero = currentTime.Subtract(DateTimeOffset.MinValue).TotalMinutes; + + // we want to wake up exactly at 1 second past minute + // to make perceived system latency look smaller + var nextWakeTime = DateTimeOffset.MinValue + .AddMinutes((long)minutesFromZero) + .Add(aggregationPeriod) + .AddSeconds(1); + + TimeSpan sleepTime = nextWakeTime - DateTimeOffset.UtcNow; + + // adjust wait time to a bit longer than a minute if the wake up time is within few seconds from now + return sleepTime < TimeSpan.FromSeconds(3) ? sleepTime.Add(aggregationPeriod) : sleepTime; + } + + /// + /// Generates telemetry object based on the metric aggregator. + /// + /// Metric definition. + /// Metric aggregator statistics calculated for a period of time. + /// Metric telemetry object resulting from aggregation. + private static MetricTelemetry CreateAggergatedMetricTelemetry(Metric metric, SimpleMetricStatisticsAggregator statistics) + { + var telemetry = new MetricTelemetry( + metric.Name, + statistics.Count, + statistics.Sum, + statistics.Min, + statistics.Max, + statistics.StandardDeviation); + + if (metric.Dimensions != null) + { + foreach (KeyValuePair property in metric.Dimensions) + { + telemetry.Properties.Add(property); + } + } + + return telemetry; + } + + /// + /// Takes a snapshot of aggregators collected by this instance of the manager + /// and schedules the next snapshot. + /// + private Task SnapshotAndReschedule() + { + return Task.Factory.StartNew( + () => + { + try + { + this.Snapshot(); + } + catch (Exception ex) + { + CoreEventSource.Log.FailedToSnapshotMetricAggregators(ex.ToString()); + } + finally + { + this.snapshotTimer.Delay = GetWaitTime(); + this.snapshotTimer.Start(this.SnapshotAndReschedule); + } + }); + } + + /// + /// Takes snapshot of all active metric aggregators and turns results into metric telemetry. + /// + private void Snapshot() + { + ConcurrentDictionary aggregatorSnapshot = + Interlocked.Exchange(ref this.metricDictionary, new ConcurrentDictionary()); + + // calculate aggregation interval duration interval + TimeSpan aggregationIntervalDuation = DateTimeOffset.UtcNow - this.lastSnapshotStartDateTime; + this.lastSnapshotStartDateTime = DateTimeOffset.UtcNow; + + // prevent zero duration for interval + if (aggregationIntervalDuation.TotalMilliseconds < 1) + { + aggregationIntervalDuation = TimeSpan.FromMilliseconds(1); + } + + // adjust interval duration to exactly snapshot frequency if it is close (within 1%) + double difference = Math.Abs(aggregationIntervalDuation.TotalMilliseconds - aggregationPeriod.TotalMilliseconds); + + if (difference <= aggregationPeriod.TotalMilliseconds / 100) + { + aggregationIntervalDuation = aggregationPeriod; + } + + if (aggregatorSnapshot.Count > 0) + { + foreach (KeyValuePair aggregatorWithStats in aggregatorSnapshot) + { + if (aggregatorWithStats.Value.Count > 0) + { + MetricTelemetry aggergatedMetricTelemetry = CreateAggergatedMetricTelemetry(aggregatorWithStats.Key, aggregatorWithStats.Value); + + aggergatedMetricTelemetry.Properties.Add(intervalDurationPropertyName, ((long)aggregationIntervalDuation.TotalMilliseconds).ToString(CultureInfo.InvariantCulture)); + + // set the timestamp back by aggregation period + aggergatedMetricTelemetry.Timestamp = DateTimeOffset.Now - aggregationPeriod; + + this.telemetryClient.Track(aggergatedMetricTelemetry); + } + } + } + } + } +} diff --git a/src/Core/Managed/Shared/Extensibility/TelemetryConfiguration.cs b/src/Core/Managed/Shared/Extensibility/TelemetryConfiguration.cs index fb9b7daaf4..48c12ba97b 100644 --- a/src/Core/Managed/Shared/Extensibility/TelemetryConfiguration.cs +++ b/src/Core/Managed/Shared/Extensibility/TelemetryConfiguration.cs @@ -26,6 +26,7 @@ public sealed class TelemetryConfiguration : IDisposable private string instrumentationKey = string.Empty; private bool disableTelemetry = false; private TelemetryProcessorChainBuilder builder; + private SnapshottingList metricProcessors = new SnapshottingList(); /// /// Gets the active instance loaded from the ApplicationInsights.config file. @@ -132,6 +133,15 @@ public IList TelemetryInitializers get { return this.telemetryInitializers; } } + /// + /// Gets the list of objects used for custom metric data processing + /// before client-side metric aggregation process. + /// + public IList MetricProcessors + { + get { return this.metricProcessors; } + } + /// /// Gets a readonly collection of TelemetryProcessors. /// diff --git a/src/Core/Managed/Shared/Shared.projitems b/src/Core/Managed/Shared/Shared.projitems index fcd2aeaf4a..f72e4cedca 100644 --- a/src/Core/Managed/Shared/Shared.projitems +++ b/src/Core/Managed/Shared/Shared.projitems @@ -38,6 +38,7 @@ + @@ -58,6 +59,7 @@ + @@ -116,6 +118,8 @@ + + diff --git a/src/Core/Managed/Shared/TelemetryClient.cs b/src/Core/Managed/Shared/TelemetryClient.cs index 49251e426a..c9a9964e1a 100644 --- a/src/Core/Managed/Shared/TelemetryClient.cs +++ b/src/Core/Managed/Shared/TelemetryClient.cs @@ -189,6 +189,8 @@ public void TrackTrace(TraceTelemetry telemetry) /// Metric name. /// Metric value. /// Named string values you can use to classify and filter metrics. + [Obsolete("This method is obsolete. Use TrackMetric(metricTelemetry) method to send pre-aggregated metric data or MetricManager class to create metrics.")] + [EditorBrowsable(EditorBrowsableState.Never)] public void TrackMetric(string name, double value, IDictionary properties = null) { var telemetry = new MetricTelemetry(name, value); @@ -201,7 +203,7 @@ public void TrackMetric(string name, double value, IDictionary p } /// - /// Send a for aggregation in Metric Explorer. + /// Send a for representing aggregated metric data. /// Create a separate instance for each call to . /// public void TrackMetric(MetricTelemetry telemetry) @@ -359,8 +361,7 @@ public void Track(ITelemetry telemetry) this.configuration.TelemetryProcessorChain.Process(telemetry); #if !CORE_PCL - // logs rich payload ETW event for any partners to process it - telemetry.Sanitize(); + // logs rich payload ETW event for any partners to process it RichPayloadEventSource.Log.Process(telemetry); #endif }