From dc2a1930772abb522a2b6e2b8981e0c7569709cc Mon Sep 17 00:00:00 2001 From: XinRan Zhang Date: Tue, 28 May 2024 14:07:15 -0700 Subject: [PATCH] Implementation of Unit Test for AwsSpanMetricProcessor (#19) --- ...S.OpenTelemetry.AutoInstrumentation.csproj | 3 + .../AwsMetricAttributeGenerator.cs | 6 +- ...Telemetry.AutoInstrumentation.Tests.csproj | 1 + .../AwsSpanMetricsProcessorTest.cs | 543 +++++++++++++++++- 4 files changed, 542 insertions(+), 11 deletions(-) diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj index 01fe31d0..2ef29a31 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj @@ -22,6 +22,9 @@ <_Parameter1>AWS.OpenTelemetry.AutoInstrumentation.Tests + + <_Parameter1>DynamicProxyGenAssembly2 + diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs index 4c9a15cb..b8851f3d 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs @@ -21,7 +21,7 @@ namespace AWS.OpenTelemetry.AutoInstrumentation; /// represent "incoming" traffic, { and spans /// represent "outgoing" traffic, and spans are ignored. /// -internal sealed class AwsMetricAttributeGenerator : IMetricAttributeGenerator +internal class AwsMetricAttributeGenerator : IMetricAttributeGenerator { private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddConsole()); private static readonly ILogger Logger = Factory.CreateLogger(); @@ -46,7 +46,7 @@ internal sealed class AwsMetricAttributeGenerator : IMetricAttributeGenerator private static readonly string AttributeServiceName = "service.name"; /// - public Dictionary GenerateMetricAttributeMapFromSpan(Activity span, Resource resource) + public virtual Dictionary GenerateMetricAttributeMapFromSpan(Activity span, Resource resource) { Dictionary attributesMap = new Dictionary(); if (ShouldGenerateServiceMetricAttributes(span)) @@ -89,7 +89,7 @@ private ActivityTagsCollection GenerateDependencyMetricAttributes(Activity span, private static void SetService(Resource resource, Activity span, ActivityTagsCollection attributes) #pragma warning restore SA1204 // Static elements should appear before instance elements { - string? service = (string?)resource.Attributes.First(attribute => attribute.Key == AttributeServiceName).Value; + string? service = (string?)resource.Attributes.FirstOrDefault(attribute => attribute.Key == AttributeServiceName).Value; // In practice the service name is never null, but we can be defensive here. if (service == null || service.StartsWith(OtelUnknownServicePrefix)) diff --git a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AWS.OpenTelemetry.AutoInstrumentation.Tests.csproj b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AWS.OpenTelemetry.AutoInstrumentation.Tests.csproj index e3884120..4b2f2729 100644 --- a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AWS.OpenTelemetry.AutoInstrumentation.Tests.csproj +++ b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AWS.OpenTelemetry.AutoInstrumentation.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs index 6e1943a1..bd1ea744 100644 --- a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs +++ b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs @@ -1,18 +1,545 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Reflection; +using HarmonyLib; +using Moq; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using static AWS.OpenTelemetry.AutoInstrumentation.AwsAttributeKeys; + + + namespace AWS.OpenTelemetry.AutoInstrumentation.Tests; -/// -/// TODO: Add documentation here -/// -public class AwsSpanMetricsProcessorTest +// There is two test that is not implemented in this Class, comparing with Java: + +// 1. testIsRequired() +// Implementation of AwsSpanMetricsProcessor.isStartRequired() and isEndRequired() do not exist + +// 2. testsOnEndMetricsGenerationLocalRootServerSpan() +// This test cannot be done here because there is no difference (or cannot set difference) in dotnet for +// a null parent information and a invalid parent information +// Found no way to setup a Activity.Parent to a default/invalid value, +// It either valid (set by passing a parent ID and automatically matching Activity.Parent field) +// or just Null + + + + +public class AwsSpanMetricsProcessorTest: IDisposable { - /// - /// Test documentation - /// + public static int count; + private AwsSpanMetricsProcessor awsSpanMetricsProcessor; + private Mock Generator = new Mock(); + private Resource resource = Resource.Empty; + private Meter meter = new Meter("test"); + private Histogram errorHistogram; + private Histogram faultHistogram; + private Histogram latencyHistogram; + private ActivitySource activitySource = new ActivitySource("test"); + private Activity spanDataMock; + private readonly double testLatencyMillis = 150; + + public void Dispose() + { + GlobalCallbackData.Clear(); + } + + public AwsSpanMetricsProcessorTest() + { + var listener = new ActivityListener + { + ShouldListenTo = (activitySource) => true, + Sample = ((ref ActivityCreationOptions options) => ActivitySamplingResult.AllData) + }; + ActivitySource.AddActivityListener(listener); + + errorHistogram = meter.CreateHistogram("error"); + faultHistogram = meter.CreateHistogram("fault"); + latencyHistogram = meter.CreateHistogram("latency"); + var meterListener = new MeterListener(); + meterListener.EnableMeasurementEvents(errorHistogram); + meterListener.EnableMeasurementEvents(faultHistogram); + meterListener.EnableMeasurementEvents(latencyHistogram); + meter.Tags.AddItem(new KeyValuePair("test", "test")); + meterListener.SetMeasurementEventCallback(((instrument, measurement, tags, state) => + { + var list = GlobalCallbackData.CallList is null ? [] : GlobalCallbackData.CallList; + list.Add(new KeyValuePair(instrument.Name, new KeyValuePair(measurement, tags[0]))); + GlobalCallbackData.CallList = list; + } + )); + meterListener.SetMeasurementEventCallback(((instrument, measurement, tags, state) => + { + var list = GlobalCallbackData.CallList is null ? [] : GlobalCallbackData.CallList; + list.Add(new KeyValuePair(instrument.Name, new KeyValuePair(measurement, tags[0]))); + GlobalCallbackData.CallList = list; + } + )); + awsSpanMetricsProcessor = AwsSpanMetricsProcessor.Create(errorHistogram, faultHistogram, latencyHistogram, Generator.Object, resource); + } + + [Fact] + public void testStartDoesNothingToSpan() + { + spanDataMock = activitySource.StartActivity("test"); + var parentInfo = spanDataMock.ParentSpanId; + awsSpanMetricsProcessor.OnStart(spanDataMock); + Assert.Equal(parentInfo, spanDataMock.ParentSpanId); + } + + [Fact] + public void testTearDown() + { + Assert.True(awsSpanMetricsProcessor.Shutdown()); + Assert.True(awsSpanMetricsProcessor.ForceFlush()); + } + + /** + * Tests starting with testOnEndMetricsGeneration are testing the logic in + * AwsSpanMetricsProcessor's onEnd method pertaining to metrics generation. + */ + [Fact] + public void testOnEndMetricsGenerationWithoutSpanAttributes() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + spanDataMock.SetEndTime(DateTime.Now); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 1,0); + } + [Fact] - public void Test1() + public void testOnEndMetricsGenerationWithoutMetricAttributes() { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); + setLatency(); + spanDataMock.SetTag(AwsAttributeKeys.AttributeHttpStatusCode, (long)500); + Dictionary expectAttributes = buildMetricAttributes(false, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 0,0); } + + [Fact] + public void testsOnEndMetricsGenerationLocalRootConsumerSpan() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Consumer); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 1,1); + } + + [Fact] + public void testsOnEndMetricsGenerationLocalRootClientSpan() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Client); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 1,1); + } + + [Fact] + public void testsOnEndMetricsGenerationLocalRootProducerSpan() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 1,1); + } + + [Fact] + public void testsOnEndMetricsGenerationLocalRootInternalSpan() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Internal); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 1,0); + } + + [Fact] + public void testsOnEndMetricsGenerationLocalRootProducerSpanWithoutMetricAttributes() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(false, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 0,0); + } + + [Fact] + public void testsOnEndMetricsGenerationClientSpan() + { + Activity parentSpan = activitySource.StartActivity("test parent"); + using (spanDataMock = activitySource.StartActivity("test Child", ActivityKind.Client)) + { + spanDataMock.SetParentId(parentSpan.TraceId, parentSpan.SpanId); + spanDataMock.Start(); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 0,1); + } + } + + [Fact] + public void testsOnEndMetricsGenerationProducerSpan() + { + Activity parentSpan = activitySource.StartActivity("test parent"); + using (spanDataMock = activitySource.StartActivity("test Child", ActivityKind.Producer)) + { + spanDataMock.SetParentId(parentSpan.TraceId, parentSpan.SpanId); + spanDataMock.Start(); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + verifyHistogramRecords(expectAttributes, 0,1); + } + } + + [Fact] + public void testOnEndMetricsGenerationWithoutEndRequired() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); + spanDataMock.SetTag(AttributeHttpStatusCode, (long)500); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + var serviceMetrics = expectAttributes[IMetricAttributeGenerator.ServiceMetric]; + var serviceKVP = new KeyValuePair(serviceMetrics.Keys.FirstOrDefault(), + serviceMetrics.Values.FirstOrDefault()); + List> expectedService = []; + expectedService.Add(new KeyValuePair("error", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(1,serviceKVP))); + expectedService.Add(new KeyValuePair("latency", new KeyValuePair(testLatencyMillis,serviceKVP))); + + Assert.True(GlobalCallbackData.CallList.OrderBy(kvp => kvp.Key).SequenceEqual(expectedService.OrderBy(kvp => kvp.Key))); + } + + [Fact] + public void testOnEndMetricsGenerationWithLatency() + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); + spanDataMock.SetTag(AttributeHttpStatusCode, (long)200); + setLatency(5.5); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + var serviceMetrics = expectAttributes[IMetricAttributeGenerator.ServiceMetric]; + var serviceKVP = new KeyValuePair(serviceMetrics.Keys.FirstOrDefault(), + serviceMetrics.Values.FirstOrDefault()); + List> expectedService = []; + expectedService.Add(new KeyValuePair("error", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("latency", new KeyValuePair(5.5,serviceKVP))); + + Assert.True(GlobalCallbackData.CallList.OrderBy(kvp => kvp.Key).SequenceEqual(expectedService.OrderBy(kvp => kvp.Key))); + } + + [Fact] + public void testOnEndMetricsGenerationWithStatusCodes() + { + validateMetricsGeneratedForHttpStatusCode(null, ExpectedStatusMetric.NEITHER); + + // Valid HTTP status codes + validateMetricsGeneratedForHttpStatusCode(200L, ExpectedStatusMetric.NEITHER); + validateMetricsGeneratedForHttpStatusCode(399L, ExpectedStatusMetric.NEITHER); + validateMetricsGeneratedForHttpStatusCode(400L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForHttpStatusCode(499L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForHttpStatusCode(500L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForHttpStatusCode(599L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForHttpStatusCode(600L, ExpectedStatusMetric.NEITHER); + } + + [Fact] + public void testOnEndMetricsGenerationWithStatusDataError() { + // Empty Status and HTTP with Error Status + validateMetricsGeneratedForStatusDataError(null, ExpectedStatusMetric.FAULT); + + // Valid HTTP with Error Status + validateMetricsGeneratedForStatusDataError(200L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataError(399L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataError(400L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForStatusDataError(499L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForStatusDataError(500L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataError(599L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataError(600L, ExpectedStatusMetric.FAULT); + } + + [Fact] + public void testOnEndMetricsGenerationWithAwsStatusCodes() { + // Invalid HTTP status codes + validateMetricsGeneratedForAttributeStatusCode(null, ExpectedStatusMetric.NEITHER); + + // Valid HTTP status codes + validateMetricsGeneratedForAttributeStatusCode(399L, ExpectedStatusMetric.NEITHER); + validateMetricsGeneratedForAttributeStatusCode(400L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForAttributeStatusCode(499L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForAttributeStatusCode(500L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForAttributeStatusCode(599L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForAttributeStatusCode(600L, ExpectedStatusMetric.NEITHER); + } + + [Fact] + public void testOnEndMetricsGenerationWithStatusDataOk() { + // Empty Status and HTTP with Ok Status + validateMetricsGeneratedForStatusDataOk(null, ExpectedStatusMetric.NEITHER); + + // Valid HTTP with Ok Status + validateMetricsGeneratedForStatusDataOk(200L, ExpectedStatusMetric.NEITHER); + validateMetricsGeneratedForStatusDataOk(399L, ExpectedStatusMetric.NEITHER); + validateMetricsGeneratedForStatusDataOk(400L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForStatusDataOk(499L, ExpectedStatusMetric.ERROR); + validateMetricsGeneratedForStatusDataOk(500L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataOk(599L, ExpectedStatusMetric.FAULT); + validateMetricsGeneratedForStatusDataOk(600L, ExpectedStatusMetric.NEITHER); + } + + private void validateMetricsGeneratedForAttributeStatusCode( + long? awsStatusCode, ExpectedStatusMetric expectedStatusMetric) + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + spanDataMock.SetTag("new key", "new value"); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + if (awsStatusCode != null) + { + expectAttributes[IMetricAttributeGenerator.ServiceMetric]["new service key"] = "new service value"; + + expectAttributes[IMetricAttributeGenerator.ServiceMetric] + .Add(new KeyValuePair(AttributeHttpStatusCode, awsStatusCode)); + + expectAttributes[IMetricAttributeGenerator.DependencyMetric]["new dependency key"] = "new dependency value"; + + expectAttributes[IMetricAttributeGenerator.DependencyMetric] + .Add(new KeyValuePair(AttributeHttpStatusCode, awsStatusCode)); + } + + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + + awsSpanMetricsProcessor.OnEnd(spanDataMock); + validMetrics(expectAttributes, expectedStatusMetric); + spanDataMock.Dispose(); + } + + private void validateMetricsGeneratedForStatusDataOk( + long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + spanDataMock.SetStatus(ActivityStatusCode.Ok); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + validMetrics(expectAttributes, expectedStatusMetric); + spanDataMock.Dispose(); + } + + + private void validateMetricsGeneratedForStatusDataError( + long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + spanDataMock.SetStatus(ActivityStatusCode.Error); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + validMetrics(expectAttributes, expectedStatusMetric); + spanDataMock.Dispose(); + } + + private void validateMetricsGeneratedForHttpStatusCode( + long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) + { + spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); + spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + setLatency(); + Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); + Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) + .Returns(expectAttributes); + awsSpanMetricsProcessor.OnEnd(spanDataMock); + validMetrics(expectAttributes, expectedStatusMetric); + spanDataMock.Dispose(); + } + + private void validMetrics(Dictionary metricAttributesMap, + ExpectedStatusMetric expectedStatusMetric) + { + var expectedList = new List>(); + var serviceMetrics = metricAttributesMap[IMetricAttributeGenerator.ServiceMetric]; + var serviceKVP = new KeyValuePair(serviceMetrics.Keys.FirstOrDefault(), + serviceMetrics.Values.FirstOrDefault()); + List> expectedService = []; + var dependencyMetrics = metricAttributesMap[IMetricAttributeGenerator.DependencyMetric]; + var dependencyKVP = new KeyValuePair(dependencyMetrics.Keys.FirstOrDefault(), + dependencyMetrics.Values.FirstOrDefault()); + List> expectedDependency = []; + + switch (expectedStatusMetric) + { + case ExpectedStatusMetric.ERROR: + expectedService.Add(new KeyValuePair("error", new KeyValuePair(1,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(0,serviceKVP))); + expectedDependency.Add(new KeyValuePair("error", new KeyValuePair(1,dependencyKVP))); + expectedDependency.Add(new KeyValuePair("fault", new KeyValuePair(0,dependencyKVP))); + break; + case ExpectedStatusMetric.FAULT: + expectedService.Add(new KeyValuePair("error", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(1,serviceKVP))); + expectedDependency.Add(new KeyValuePair("error", new KeyValuePair(0,dependencyKVP))); + expectedDependency.Add(new KeyValuePair("fault", new KeyValuePair(1,dependencyKVP))); + break; + case ExpectedStatusMetric.NEITHER: + expectedService.Add(new KeyValuePair("error", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(0,serviceKVP))); + expectedDependency.Add(new KeyValuePair("error", new KeyValuePair(0,dependencyKVP))); + expectedDependency.Add(new KeyValuePair("fault", new KeyValuePair(0,dependencyKVP))); + break; + } + + expectedList.AddRange(expectedService); + expectedList.AddRange(expectedDependency); + + var expectDict = new Dictionary, int>(); + var actualDict = new Dictionary, int>(); + GlobalCallbackData.CallList.RemoveAll(kvp => kvp.Key == "latency"); + if (expectedList.Count > 0) + { + expectDict = expectedList.GroupBy(kvp => kvp).ToDictionary(g => g.Key, g => g.Count()); + } + if (GlobalCallbackData.CallList is not null) + { + actualDict = GlobalCallbackData.CallList.GroupBy(kvp => kvp).ToDictionary(g => g.Key, g => g.Count()); + } + Assert.Equal(expectDict, actualDict); + + GlobalCallbackData.Clear(); + + } + + private void verifyHistogramRecords(Dictionary metricAttributesMap, + int wantedServiceMetricInvocation, + int wantedDependencyMetricInvocation) + { + var expectedList = new List>(); + if (wantedServiceMetricInvocation > 0) + { + var serviceMetrics = metricAttributesMap[IMetricAttributeGenerator.ServiceMetric]; + var serviceKVP = new KeyValuePair(serviceMetrics.Keys.FirstOrDefault(), + serviceMetrics.Values.FirstOrDefault()); + List> expectedService = []; + expectedService.Add(new KeyValuePair("error", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("fault", new KeyValuePair(0,serviceKVP))); + expectedService.Add(new KeyValuePair("latency", new KeyValuePair(testLatencyMillis,serviceKVP))); + expectedService = expectedService.SelectMany(item => Enumerable.Repeat(item, wantedServiceMetricInvocation)).ToList(); + expectedList.AddRange(expectedService); + } + + if (wantedDependencyMetricInvocation > 0) + { + var dependencyMetrics = metricAttributesMap[IMetricAttributeGenerator.DependencyMetric]; + var dependencyKVP = new KeyValuePair(dependencyMetrics.Keys.FirstOrDefault(), + dependencyMetrics.Values.FirstOrDefault()); + List> expectedDependency = []; + expectedDependency.Add(new KeyValuePair("error", new KeyValuePair(0,dependencyKVP))); + expectedDependency.Add(new KeyValuePair("fault", new KeyValuePair(0,dependencyKVP))); + expectedDependency.Add(new KeyValuePair("latency", new KeyValuePair(testLatencyMillis,dependencyKVP))); + expectedDependency = expectedDependency.SelectMany(item => Enumerable.Repeat(item, wantedDependencyMetricInvocation)).ToList(); + expectedList.AddRange(expectedDependency); + } + + var expectDict = new Dictionary, int>(); + var actualDict = new Dictionary, int>(); + if (expectedList.Count > 0) + { + expectDict = expectedList.GroupBy(kvp => kvp).ToDictionary(g => g.Key, g => g.Count()); + } + if (GlobalCallbackData.CallList is not null) + { + actualDict = GlobalCallbackData.CallList.GroupBy(kvp => kvp).ToDictionary(g => g.Key, g => g.Count()); + } + + Assert.Equal(expectDict, actualDict); + + } + + private Dictionary buildMetricAttributes(bool containAttributes, Activity span) + { + Dictionary attributes = new Dictionary(); + if (containAttributes) + { + if (AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(span)) + { + attributes.Add(IMetricAttributeGenerator.DependencyMetric, new ActivityTagsCollection([new KeyValuePair("new dependency key", "new dependency value")])); + } + + if (AwsSpanProcessingUtil.ShouldGenerateServiceMetricAttributes(span)) + { + attributes.Add(IMetricAttributeGenerator.ServiceMetric, new ActivityTagsCollection([new KeyValuePair("new service key", "new service value")])); + } + } + return attributes; + } + + // Configure latency + private void setLatency(double latency = -1) + { + if (latency == -1) + { + latency = testLatencyMillis; + } + PropertyInfo spanDuration = typeof(Activity).GetProperty("Duration"); + TimeSpan timeSpan = TimeSpan.FromMilliseconds(latency); + MethodInfo durationSetMethod = spanDuration?.GetSetMethod(nonPublic: true); + durationSetMethod.Invoke(spanDataMock, new object?[] { timeSpan }); + } + } + +public static class GlobalCallbackData +{ + public static List> CallList { get; set; } + + public static void Clear() + { + CallList = null; + } +} +public enum ExpectedStatusMetric { + ERROR = 0, + FAULT = 1, + NEITHER = 2 +} +