diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessageAttributeHelper.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessageAttributeHelper.cs new file mode 100644 index 0000000000..5d2b76539a --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessageAttributeHelper.cs @@ -0,0 +1,82 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Amazon.Runtime; +using Amazon.Runtime.Internal; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; + +internal class AWSMessageAttributeHelper +{ + // SQS/SNS message attributes collection size limit according to + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html and + // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html + private const int MaxMessageAttributes = 10; + + private readonly IAWSMessageAttributeFormatter attributeFormatter; + + internal AWSMessageAttributeHelper(IAWSMessageAttributeFormatter attributeFormatter) + { + this.attributeFormatter = attributeFormatter ?? throw new ArgumentNullException(nameof(attributeFormatter)); + } + + internal bool TryAddParameter(ParameterCollection parameters, string name, string value) + { + var index = this.GetNextAttributeIndex(parameters, name); + if (!index.HasValue) + { + return false; + } + else if (index >= MaxMessageAttributes) + { + // TODO: Add logging (event source). + return false; + } + + var attributePrefix = this.attributeFormatter.AttributeNamePrefix + $".{index.Value}"; + parameters.Add(attributePrefix + ".Name", name.Trim()); + parameters.Add(attributePrefix + ".Value.DataType", "String"); + parameters.Add(attributePrefix + ".Value.StringValue", value.Trim()); + + return true; + } + + private int? GetNextAttributeIndex(ParameterCollection parameters, string name) + { + var names = parameters.Where(a => this.attributeFormatter.AttributeNameRegex.IsMatch(a.Key)); + if (!names.Any()) + { + return 1; + } + + int? index = 0; + foreach (var nameAttribute in names) + { + if (nameAttribute.Value is StringParameterValue param && param.Value == name) + { + index = null; + break; + } + + var currentIndex = this.attributeFormatter.GetAttributeIndex(nameAttribute.Key); + index = (currentIndex ?? 0) > index ? currentIndex : index; + } + + return ++index; + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessagingUtils.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessagingUtils.cs new file mode 100644 index 0000000000..b46ec0fa8b --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSMessagingUtils.cs @@ -0,0 +1,74 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Amazon.Runtime; +using SNS = Amazon.SimpleNotificationService.Model; +using SQS = Amazon.SQS.Model; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; + +internal static class AWSMessagingUtils +{ + private static readonly AWSMessageAttributeHelper SqsAttributeHelper = new(new SqsMessageAttributeFormatter()); + private static readonly AWSMessageAttributeHelper SnsAttributeHelper = new(new SnsMessageAttributeFormatter()); + + internal static void SqsMessageAttributeSetter(IRequestContext context, string name, string value) + { + var parameters = context.Request?.ParameterCollection; + if (parameters == null || + !parameters.ContainsKey("MessageBody") || + context.OriginalRequest is not SQS::SendMessageRequest originalRequest) + { + return; + } + + // Add trace data to parameters collection of the request. + if (SqsAttributeHelper.TryAddParameter(parameters, name, value)) + { + // Add trace data to message attributes dictionary of the original request. + // This dictionary must be in sync with parameters collection to pass through the MD5 hash matching check. + if (!originalRequest.MessageAttributes.ContainsKey(name)) + { + originalRequest.MessageAttributes.Add( + name, new SQS::MessageAttributeValue + { DataType = "String", StringValue = value }); + } + } + } + + internal static void SnsMessageAttributeSetter(IRequestContext context, string name, string value) + { + var parameters = context.Request?.ParameterCollection; + if (parameters == null || + !parameters.ContainsKey("Message") || + context.OriginalRequest is not SNS::PublishRequest originalRequest) + { + return; + } + + if (SnsAttributeHelper.TryAddParameter(parameters, name, value)) + { + // Add trace data to message attributes dictionary of the original request. + // This dictionary must be in sync with parameters collection to pass through the MD5 hash matching check. + if (!originalRequest.MessageAttributes.ContainsKey(name)) + { + originalRequest.MessageAttributes.Add( + name, new SNS::MessageAttributeValue + { DataType = "String", StringValue = value }); + } + } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceHelper.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceHelper.cs index b36964b4ea..8c11883331 100644 --- a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceHelper.cs +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceHelper.cs @@ -14,7 +14,6 @@ // limitations under the License. // -using System; using System.Collections.Generic; using Amazon.Runtime; @@ -24,8 +23,8 @@ internal class AWSServiceHelper { internal static IReadOnlyDictionary ServiceParameterMap = new Dictionary() { - { DynamoDbService, "TableName" }, - { SQSService, "QueueUrl" }, + { AWSServiceType.DynamoDbService, "TableName" }, + { AWSServiceType.SQSService, "QueueUrl" }, }; internal static IReadOnlyDictionary ParameterAttributeMap = new Dictionary() @@ -34,9 +33,6 @@ internal class AWSServiceHelper { "QueueUrl", AWSSemanticConventions.AttributeAWSSQSQueueUrl }, }; - private const string DynamoDbService = "DynamoDBv2"; - private const string SQSService = "SQS"; - internal static string GetAWSServiceName(IRequestContext requestContext) => Utils.RemoveAmazonPrefixFromServiceName(requestContext.Request.ServiceName); @@ -47,7 +43,4 @@ internal static string GetAWSOperationName(IRequestContext requestContext) var operationName = Utils.RemoveSuffix(completeRequestName, suffix); return operationName; } - - internal static bool IsDynamoDbService(string service) - => DynamoDbService.Equals(service, StringComparison.OrdinalIgnoreCase); } diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceType.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceType.cs new file mode 100644 index 0000000000..24223f6f4b --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSServiceType.cs @@ -0,0 +1,35 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; + +internal class AWSServiceType +{ + internal const string DynamoDbService = "DynamoDBv2"; + internal const string SQSService = "SQS"; + internal const string SNSService = "SimpleNotificationService"; + + internal static bool IsDynamoDbService(string service) + => DynamoDbService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsSqsService(string service) + => SQSService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsSnsService(string service) + => SNSService.Equals(service, StringComparison.OrdinalIgnoreCase); +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs index bbce612cf6..427e043c6b 100644 --- a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs @@ -196,10 +196,18 @@ private void AddRequestSpecificInformation(Activity activity, IRequestContext re } } - if (AWSServiceHelper.IsDynamoDbService(service)) + if (AWSServiceType.IsDynamoDbService(service)) { activity.SetTag(SemanticConventions.AttributeDbSystem, AWSSemanticConventions.AttributeValueDynamoDb); } + else if (AWSServiceType.IsSqsService(service)) + { + Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), requestContext, AWSMessagingUtils.SqsMessageAttributeSetter); + } + else if (AWSServiceType.IsSnsService(service)) + { + Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), requestContext, AWSMessagingUtils.SnsMessageAttributeSetter); + } } private void AddStatusCodeToActivity(Activity activity, int status_code) diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/IAWSMessageAttributeFormatter.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/IAWSMessageAttributeFormatter.cs new file mode 100644 index 0000000000..536dbe8b50 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/IAWSMessageAttributeFormatter.cs @@ -0,0 +1,27 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Text.RegularExpressions; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; +internal interface IAWSMessageAttributeFormatter +{ + Regex AttributeNameRegex { get; } + + string AttributeNamePrefix { get; } + + int? GetAttributeIndex(string attributeName); +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SnsMessageAttributeFormatter.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SnsMessageAttributeFormatter.cs new file mode 100644 index 0000000000..439dc469c8 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SnsMessageAttributeFormatter.cs @@ -0,0 +1,34 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Text.RegularExpressions; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; + +internal class SnsMessageAttributeFormatter : IAWSMessageAttributeFormatter +{ + Regex IAWSMessageAttributeFormatter.AttributeNameRegex => new(@"MessageAttributes\.entry\.\d+\.Name"); + + string IAWSMessageAttributeFormatter.AttributeNamePrefix => "MessageAttributes.entry"; + + int? IAWSMessageAttributeFormatter.GetAttributeIndex(string attributeName) + { + var parts = attributeName.Split('.'); + return (parts.Length >= 3 && int.TryParse(parts[2], out int index)) ? + index : + null; + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SqsMessageAttributeFormatter.cs b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SqsMessageAttributeFormatter.cs new file mode 100644 index 0000000000..d3bfc9e4e8 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/Implementation/SqsMessageAttributeFormatter.cs @@ -0,0 +1,34 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Text.RegularExpressions; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; + +internal class SqsMessageAttributeFormatter : IAWSMessageAttributeFormatter +{ + Regex IAWSMessageAttributeFormatter.AttributeNameRegex => new(@"MessageAttribute\.\d+\.Name"); + + string IAWSMessageAttributeFormatter.AttributeNamePrefix => "MessageAttribute"; + + int? IAWSMessageAttributeFormatter.GetAttributeIndex(string attributeName) + { + var parts = attributeName.Split('.'); + return (parts.Length >= 2 && int.TryParse(parts[1], out int index)) ? + index : + null; + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.AWS/OpenTelemetry.Contrib.Instrumentation.AWS.csproj b/src/OpenTelemetry.Contrib.Instrumentation.AWS/OpenTelemetry.Contrib.Instrumentation.AWS.csproj index 1a7b34718c..38b0be96b9 100644 --- a/src/OpenTelemetry.Contrib.Instrumentation.AWS/OpenTelemetry.Contrib.Instrumentation.AWS.csproj +++ b/src/OpenTelemetry.Contrib.Instrumentation.AWS/OpenTelemetry.Contrib.Instrumentation.AWS.csproj @@ -8,12 +8,14 @@ + + - - + + diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs index bda6d7677b..c5b794ffd0 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs @@ -20,6 +20,8 @@ using System.Linq; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; +using Amazon.Lambda.SNSEvents; +using Amazon.Lambda.SQSEvents; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Contrib.Extensions.AWSXRay.Trace; @@ -74,6 +76,18 @@ internal static ActivityContext ExtractParentContext(TInput input) case APIGatewayHttpApiV2ProxyRequest apiGatewayHttpApiV2ProxyRequest: propagationContext = Propagators.DefaultTextMapPropagator.Extract(default, apiGatewayHttpApiV2ProxyRequest, GetHeaderValues); break; + case SQSEvent sqsEvent: + propagationContext = AWSMessagingUtils.ExtractParentContext(sqsEvent); + break; + case SQSEvent.SQSMessage sqsMessage: + propagationContext = AWSMessagingUtils.ExtractParentContext(sqsMessage); + break; + case SNSEvent snsEvent: + propagationContext = AWSMessagingUtils.ExtractParentContext(snsEvent); + break; + case SNSEvent.SNSRecord snsRecord: + propagationContext = AWSMessagingUtils.ExtractParentContext(snsRecord); + break; } return propagationContext.ActivityContext; diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSMessagingUtils.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSMessagingUtils.cs new file mode 100644 index 0000000000..f9663b9f88 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSMessagingUtils.cs @@ -0,0 +1,141 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Linq; +using Amazon.Lambda.SNSEvents; +using Amazon.Lambda.SQSEvents; +using Newtonsoft.Json; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Instrumentation.AWSLambda.Implementation; + +internal class AWSMessagingUtils +{ + // SNS attribute types: https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html + private const string SnsAttributeTypeString = "String"; + private const string SnsAttributeTypeStringArray = "String.Array"; + private const string SnsMessageAttributes = "MessageAttributes"; + + internal static PropagationContext ExtractParentContext(SQSEvent sqsEvent) + { + if (sqsEvent == null) + { + return default; + } + + // We assume there can be only one parent that's why we consider only a single (the last) record as the carrier. + var message = sqsEvent.Records.LastOrDefault(); + return ExtractParentContext(message); + } + + internal static PropagationContext ExtractParentContext(SQSEvent.SQSMessage sqsMessage) + { + if (sqsMessage == null) + { + return default; + } + + // SQS subscribed to SNS topic with raw delivery disabled case, i.e. SNS record serialized into SQS body. + // https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html + SNSEvent.SNSMessage snsMessage = GetSnsMessage(sqsMessage); + return snsMessage != null ? + ExtractParentContext(snsMessage) : + Propagators.DefaultTextMapPropagator.Extract(default, sqsMessage.MessageAttributes, SqsMessageAttributeGetter); + } + + internal static PropagationContext ExtractParentContext(SNSEvent snsEvent) + { + if (snsEvent == null) + { + return default; + } + + // We assume there can be only one parent that's why we consider only a single (the last) record as the carrier. + var record = snsEvent.Records.LastOrDefault(); + return ExtractParentContext(record); + } + + internal static PropagationContext ExtractParentContext(SNSEvent.SNSRecord record) + { + return (record?.Sns != null) ? + Propagators.DefaultTextMapPropagator.Extract(default, record.Sns.MessageAttributes, SnsMessageAttributeGetter) : + default; + } + + internal static PropagationContext ExtractParentContext(SNSEvent.SNSMessage message) + { + return (message != null) ? + Propagators.DefaultTextMapPropagator.Extract(default, message.MessageAttributes, SnsMessageAttributeGetter) : + default; + } + + private static IEnumerable SqsMessageAttributeGetter(IDictionary attributes, string attributeName) + { + SQSEvent.MessageAttribute attribute = attributes.GetValueByKeyIgnoringCase(attributeName); + if (attribute == null) + { + return null; + } + + return attribute.StringValue != null ? + new[] { attribute.StringValue } : + attribute.StringListValues; + } + + private static IEnumerable SnsMessageAttributeGetter(IDictionary attributes, string attributeName) + { + SNSEvent.MessageAttribute attribute = attributes.GetValueByKeyIgnoringCase(attributeName); + if (attribute == null) + { + return null; + } + + switch (attribute.Type) + { + case SnsAttributeTypeString when attribute.Value != null: + return new[] { attribute.Value }; + case SnsAttributeTypeStringArray when attribute.Value != null: + // Multiple values are stored as CSV (https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html). + return attribute.Value.Split(','); + default: + return null; + } + } + + private static SNSEvent.SNSMessage GetSnsMessage(SQSEvent.SQSMessage sqsMessage) + { + SNSEvent.SNSMessage snsMessage = null; + + var body = sqsMessage.Body; + if (body != null && + body.TrimStart().StartsWith("{") && + body.Contains(SnsMessageAttributes)) + { + try + { + snsMessage = JsonConvert.DeserializeObject(body); + } + catch (JsonException) + { + // TODO: log exception. + return null; + } + } + + return snsMessage; + } +} diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj b/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj index 14a98b2675..e2ae6b6736 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj @@ -10,13 +10,15 @@ + + - + diff --git a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/AWSMessageAttributeHelperTests.cs b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/AWSMessageAttributeHelperTests.cs new file mode 100644 index 0000000000..adc4c70167 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/AWSMessageAttributeHelperTests.cs @@ -0,0 +1,94 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using Amazon.Runtime.Internal; +using OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; +using Xunit; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Tests.Implementation; + +public class AWSMessageAttributeHelperTests +{ + [Theory] + [InlineData(AWSServiceType.SQSService)] + [InlineData(AWSServiceType.SNSService)] + public void TryAddParameter_CollectionSizeReachesLimit_ParameterNotAdded(string serviceType) + { + var helper = TestsHelper.CreateAttributeHelper(serviceType); + var parameters = new ParameterCollection(); + parameters.AddStringParameters(TestsHelper.GetNamePrefix(serviceType), 10); + + var added = helper.TryAddParameter(parameters, "testName", "testValue"); + + Assert.False(added, "Expected parameter not to be added."); + Assert.Equal(30, parameters.Count); + } + + [Theory] + [InlineData(AWSServiceType.SQSService)] + [InlineData(AWSServiceType.SNSService)] + public void TryAddParameter_EmptyCollection_ParameterAdded(string serviceType) + { + var helper = TestsHelper.CreateAttributeHelper(serviceType); + var parameters = new ParameterCollection(); + var expectedParameters = new List>() + { + new KeyValuePair("testName", "testValue"), + }; + + var added = helper.TryAddParameter(parameters, "testName", "testValue"); + + Assert.True(added, "Expected parameter to be added."); + TestsHelper.AssertStringParameters(expectedParameters, parameters, TestsHelper.GetNamePrefix(serviceType)); + } + + [Theory] + [InlineData(AWSServiceType.SQSService)] + [InlineData(AWSServiceType.SNSService)] + public void TryAddParameter_CollectionWithSingleParameter_SecondParameterAdded(string serviceType) + { + var helper = TestsHelper.CreateAttributeHelper(serviceType); + var parameters = new ParameterCollection(); + parameters.AddStringParameter("testNameA", "testValueA", TestsHelper.GetNamePrefix(serviceType), 1); + + var expectedParameters = new List>() + { + new KeyValuePair("testNameA", "testValueA"), + new KeyValuePair("testNameB", "testValueB"), + }; + + var added = helper.TryAddParameter(parameters, "testNameB", "testValueB"); + + Assert.True(added, "Expected parameter to be added."); + TestsHelper.AssertStringParameters(expectedParameters, parameters, TestsHelper.GetNamePrefix(serviceType)); + } + + [Theory] + [InlineData(AWSServiceType.SQSService)] + [InlineData(AWSServiceType.SNSService)] + public void TryAddParameter_ParameterPresentInCollection_ParameterNotAdded(string serviceType) + { + var helper = TestsHelper.CreateAttributeHelper(serviceType); + var parameters = new ParameterCollection(); + parameters.AddStringParameter("testNameA", "testValueA", TestsHelper.GetNamePrefix(serviceType), 1); + + var added = helper.TryAddParameter(parameters, "testNameA", "testValueA"); + + Assert.False(added, "Expected parameter not to be added."); + Assert.Equal(3, parameters.Count); + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/TestsHelper.cs b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/TestsHelper.cs new file mode 100644 index 0000000000..b41f2acbf5 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Implementation/TestsHelper.cs @@ -0,0 +1,82 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using Amazon.Runtime; +using Amazon.Runtime.Internal; +using OpenTelemetry.Contrib.Instrumentation.AWS.Implementation; +using Xunit; + +namespace OpenTelemetry.Contrib.Instrumentation.AWS.Tests.Implementation; +internal static class TestsHelper +{ + internal static AWSMessageAttributeHelper CreateAttributeHelper(string serviceType) + { + return serviceType switch + { + AWSServiceType.SQSService => new(new SqsMessageAttributeFormatter()), + AWSServiceType.SNSService => new(new SnsMessageAttributeFormatter()), + _ => throw new NotSupportedException($"Tests for service type {serviceType} not supported."), + }; + } + + internal static string GetNamePrefix(string serviceType) + { + return serviceType switch + { + AWSServiceType.SQSService => "MessageAttribute", + AWSServiceType.SNSService => "MessageAttributes.entry", + _ => throw new NotSupportedException($"Tests for service type {serviceType} not supported."), + }; + } + + internal static void AddStringParameter(this ParameterCollection parameters, string name, string value, string namePrefix, int index) + { + var prefix = $"{namePrefix}.{index}"; + parameters.Add($"{prefix}.Name", name); + parameters.Add($"{prefix}.Value.DataType", "String"); + parameters.Add($"{prefix}.Value.StringValue", value); + } + + internal static void AddStringParameters(this ParameterCollection parameters, string namePrefix, int count) + { + for (int i = 1; i <= count; i++) + { + AddStringParameter(parameters, $"name{i}", $"value{i}", namePrefix, i); + } + } + + internal static void AssertStringParameters(List> expectedParameters, ParameterCollection actualParameters, string namePrefix) + { + Assert.Equal(expectedParameters.Count * 3, actualParameters.Count); + + for (int i = 0; i < expectedParameters.Count; i++) + { + var prefix = $"{namePrefix}.{i + 1}"; + static string Value(ParameterValue p) => (p as StringParameterValue).Value; + + Assert.True(actualParameters.ContainsKey($"{prefix}.Name")); + Assert.Equal(expectedParameters[i].Key, Value(actualParameters[$"{prefix}.Name"])); + + Assert.True(actualParameters.ContainsKey($"{prefix}.Value.DataType")); + Assert.Equal("String", Value(actualParameters[$"{prefix}.Value.DataType"])); + + Assert.True(actualParameters.ContainsKey($"{prefix}.Value.StringValue")); + Assert.Equal(expectedParameters[i].Value, Value(actualParameters[$"{prefix}.Value.StringValue"])); + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/OpenTelemetry.Contrib.Instrumentation.AWS.Tests.csproj b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/OpenTelemetry.Contrib.Instrumentation.AWS.Tests.csproj index e8fa57bb14..a30b9e351f 100644 --- a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/OpenTelemetry.Contrib.Instrumentation.AWS.Tests.csproj +++ b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/OpenTelemetry.Contrib.Instrumentation.AWS.Tests.csproj @@ -8,11 +8,10 @@ + - - - + all diff --git a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Tools/Utils.cs b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Tools/Utils.cs index 635f5efabf..65fd24ff65 100644 --- a/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Tools/Utils.cs +++ b/test/OpenTelemetry.Contrib.Instrumentation.AWS.Tests/Tools/Utils.cs @@ -71,6 +71,6 @@ public static IEnumerable FindResourceName(Predicate match) public static object GetTagValue(Activity activity, string tagName) { - return Implementation.Utils.GetTagValue(activity, tagName); + return AWS.Implementation.Utils.GetTagValue(activity, tagName); } }