From 3322f01f0b21b71c8396303e25c8976534ce79fe Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 10 Nov 2023 10:33:22 -0800 Subject: [PATCH 1/6] Make sure all logging calls use Pascal casing and add the analyzer warning as an error to enforce consistency in the future. (#69) --- src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj | 1 + src/AWS.Messaging.Lambda/DefaultLambdaMessaging.cs | 2 +- src/AWS.Messaging/AWS.Messaging.csproj | 2 ++ src/AWS.Messaging/Services/DefaultMessageManager.cs | 8 ++++---- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj index 84ca597..7e8be4d 100644 --- a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj +++ b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj @@ -8,6 +8,7 @@ true Major README.md + CA1727 diff --git a/src/AWS.Messaging.Lambda/DefaultLambdaMessaging.cs b/src/AWS.Messaging.Lambda/DefaultLambdaMessaging.cs index 3f62054..d38a080 100644 --- a/src/AWS.Messaging.Lambda/DefaultLambdaMessaging.cs +++ b/src/AWS.Messaging.Lambda/DefaultLambdaMessaging.cs @@ -138,7 +138,7 @@ private string GetSQSQueueUrl(string queueArn) Arn arn; if (!Arn.TryParse(queueArn, out arn)) { - _logger.LogError("{queueArn} is not a valid SQS queue ARN", queueArn); + _logger.LogError("{QueueArn} is not a valid SQS queue ARN", queueArn); throw new InvalidSQSQueueArnException($"{queueArn} is not a valid SQS queue ARN"); } diff --git a/src/AWS.Messaging/AWS.Messaging.csproj b/src/AWS.Messaging/AWS.Messaging.csproj index a92eb5f..e78a7f2 100644 --- a/src/AWS.Messaging/AWS.Messaging.csproj +++ b/src/AWS.Messaging/AWS.Messaging.csproj @@ -8,6 +8,8 @@ true Major README.md + True + CA1727 diff --git a/src/AWS.Messaging/Services/DefaultMessageManager.cs b/src/AWS.Messaging/Services/DefaultMessageManager.cs index 317d47d..fb58c6b 100644 --- a/src/AWS.Messaging/Services/DefaultMessageManager.cs +++ b/src/AWS.Messaging/Services/DefaultMessageManager.cs @@ -139,7 +139,7 @@ public async Task ProcessMessageGroupAsync(List message if (!isSuccessful) { // If the handler invocation fails for any message, skip processing subsequent messages in the group. - _logger.LogError("Handler invocation failed for a message belonging to message group '{groupdId}' having message ID '{messageID}'. Skipping processing of {remaining} messages from the same group.", groupId, message.Envelope.Id, remaining); + _logger.LogError("Handler invocation failed for a message belonging to message group '{GroupdId}' having message ID '{MessageID}'. Skipping processing of {Remaining} messages from the same group.", groupId, message.Envelope.Id, remaining); break; } remaining -= 1; @@ -181,7 +181,7 @@ private async Task InvokeHandler(MessageEnvelope messageEnvelope, Subscrib } catch (Exception ex) { - _logger.LogError(ex, "An exception has been thrown from handler '{handlerType}' while processing message with ID '{messageId}'", subscriberMapping.HandlerType.Name, messageEnvelope.Id); + _logger.LogError(ex, "An exception has been thrown from handler '{HandlerType}' while processing message with ID '{MessageId}'", subscriberMapping.HandlerType.Name, messageEnvelope.Id); } _inFlightMessageMetadata.Remove(messageEnvelope, out _); @@ -202,7 +202,7 @@ private async Task InvokeHandler(MessageEnvelope messageEnvelope, Subscrib } else if (handlerTask.IsFaulted) { - _logger.LogError(handlerTask.Exception, "An exception has been thrown from handler '{handlerType}' while processing message with ID '{messageId}'", subscriberMapping.HandlerType.Name, messageEnvelope.Id); + _logger.LogError(handlerTask.Exception, "An exception has been thrown from handler '{HandlerType}' while processing message with ID '{MessageId}'", subscriberMapping.HandlerType.Name, messageEnvelope.Id); await _sqsMessageCommunication.ReportMessageFailureAsync(messageEnvelope); } @@ -224,7 +224,7 @@ private void StartMessageVisibilityExtensionTaskIfNotRunning(CancellationToken t { _visibilityTimeoutExtensionTask = ExtendUnfinishedMessageVisibilityTimeoutBatch(token); - _logger.LogTrace("Started task with id {id} to extend the visibility timeout of in flight messages", _visibilityTimeoutExtensionTask.Id); + _logger.LogTrace("Started task with id {Id} to extend the visibility timeout of in flight messages", _visibilityTimeoutExtensionTask.Id); } } } From 88e46a22de1f06325e344bc22d72763162fb0d6b Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria <96malhar@gmail.com> Date: Fri, 10 Nov 2023 13:18:13 -0800 Subject: [PATCH 2/6] chore: Enable code scanning via Semgrep --- .github/workflows/semgrep-analysis.yml | 41 ++++++++++++++++++++++++++ .semgrepignore | 2 ++ 2 files changed, 43 insertions(+) create mode 100644 .github/workflows/semgrep-analysis.yml create mode 100644 .semgrepignore diff --git a/.github/workflows/semgrep-analysis.yml b/.github/workflows/semgrep-analysis.yml new file mode 100644 index 0000000..1ae557e --- /dev/null +++ b/.github/workflows/semgrep-analysis.yml @@ -0,0 +1,41 @@ +name: Semgrep + +on: + # Scan changed files in PRs, block on new issues only (existing issues ignored) + pull_request: + + push: + branches: ["dev", "main"] + + schedule: + - cron: '23 20 * * 1' + + # Manually trigger the workflow + workflow_dispatch: + +jobs: + semgrep: + name: Scan + permissions: + security-events: write + runs-on: ubuntu-latest + container: + image: returntocorp/semgrep + # Skip any PR created by dependabot to avoid permission issues + if: (github.actor != 'dependabot[bot]') + steps: + # Fetch project source + - uses: actions/checkout@v4 + + - run: semgrep ci --sarif > semgrep.sarif + env: + SEMGREP_RULES: >- # more at semgrep.dev/explore + p/security-audit + p/secrets + p/owasp-top-ten + + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: semgrep.sarif + if: always() \ No newline at end of file diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..9c0d548 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,2 @@ +/sampleapps +/test From 8bbd7797effb43abd5f700321479c6cdf8e04b30 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria <96malhar@gmail.com> Date: Tue, 14 Nov 2023 16:48:40 -0800 Subject: [PATCH 3/6] fix: Remove 'Dns.GetHostName' from the message source resolution chain and set '/aws/messaging' as the fallback source --- .../Configuration/MessageBusBuilder.cs | 1 - src/AWS.Messaging/Services/DnsManager.cs | 15 --- src/AWS.Messaging/Services/IDnsManager.cs | 18 --- .../Services/MessageSourceHandler.cs | 30 +---- .../MessageSourceHandlerTests.cs | 123 +++--------------- 5 files changed, 23 insertions(+), 164 deletions(-) delete mode 100644 src/AWS.Messaging/Services/DnsManager.cs delete mode 100644 src/AWS.Messaging/Services/IDnsManager.cs diff --git a/src/AWS.Messaging/Configuration/MessageBusBuilder.cs b/src/AWS.Messaging/Configuration/MessageBusBuilder.cs index ce821ba..9325739 100644 --- a/src/AWS.Messaging/Configuration/MessageBusBuilder.cs +++ b/src/AWS.Messaging/Configuration/MessageBusBuilder.cs @@ -276,7 +276,6 @@ internal void Build(IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.AddHttpClient("ECSMetadataClient"); diff --git a/src/AWS.Messaging/Services/DnsManager.cs b/src/AWS.Messaging/Services/DnsManager.cs deleted file mode 100644 index f2d3f9a..0000000 --- a/src/AWS.Messaging/Services/DnsManager.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Net; - -namespace AWS.Messaging.Services; - -/// -/// A wrapper around . -/// -internal class DnsManager : IDnsManager -{ - /// - public string GetHostName() => Dns.GetHostName(); -} diff --git a/src/AWS.Messaging/Services/IDnsManager.cs b/src/AWS.Messaging/Services/IDnsManager.cs deleted file mode 100644 index 07c01f0..0000000 --- a/src/AWS.Messaging/Services/IDnsManager.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Net; - -namespace AWS.Messaging.Services; - -/// -/// A wrapper around . -/// -internal interface IDnsManager -{ - /// - /// Gets the host name of the local computer. - /// - /// A string that contains the host name of the local computer. - string GetHostName(); -} diff --git a/src/AWS.Messaging/Services/MessageSourceHandler.cs b/src/AWS.Messaging/Services/MessageSourceHandler.cs index 3734af6..d5e1bae 100644 --- a/src/AWS.Messaging/Services/MessageSourceHandler.cs +++ b/src/AWS.Messaging/Services/MessageSourceHandler.cs @@ -15,7 +15,6 @@ namespace AWS.Messaging.Services; internal class MessageSourceHandler : IMessageSourceHandler { private readonly IEnvironmentManager _environmentManager; - private readonly IDnsManager _dnsManager; private readonly IECSContainerMetadataManager _ecsContainerMetadataManager; private readonly IEC2InstanceMetadataManager _ec2InstanceMetadataHandler; private readonly IMessageConfiguration _messageConfiguration; @@ -23,14 +22,12 @@ internal class MessageSourceHandler : IMessageSourceHandler public MessageSourceHandler( IEnvironmentManager environmentManager, - IDnsManager dnsManager, IECSContainerMetadataManager ecsContainerMetadataManager, IEC2InstanceMetadataManager ec2InstanceMetadataHandler, IMessageConfiguration messageConfiguration, ILogger logger) { _environmentManager = environmentManager; - _dnsManager = dnsManager; _ecsContainerMetadataManager = ecsContainerMetadataManager; _ec2InstanceMetadataHandler = ec2InstanceMetadataHandler; _messageConfiguration = messageConfiguration; @@ -56,7 +53,7 @@ public MessageSourceHandler( /// /// /// If the source cannot be resolved from the compute environment, - /// we fallback to using + /// we fallback to using "/aws/messaging" as the source identifier /// /// /// After a source is computed, the message source suffix is appended if one is set. @@ -68,7 +65,7 @@ public async Task ComputeMessageSource() return GetFullSourceUri(_messageConfiguration.Source, _messageConfiguration.SourceSuffix); _logger.LogTrace("Attempting to compute message source based on the current environment..."); - string? messageSource = GetSourceFromLambda(); + var messageSource = GetSourceFromLambda(); if (string.IsNullOrEmpty(messageSource)) { messageSource = await GetSourceFromECS(); @@ -78,10 +75,6 @@ public async Task ComputeMessageSource() messageSource = GetSourceFromEC2(); } if (string.IsNullOrEmpty(messageSource)) - { - messageSource = GetSourceFromDnsHostName(); - } - if (string.IsNullOrEmpty(messageSource)) { messageSource = "/aws/messaging"; } @@ -172,23 +165,4 @@ private Uri GetFullSourceUri(string source, string? suffix) $"/AmazonEC2/{instanceID}" : null; } - - /// - /// Retrieve the DNS host name using . - /// - /// Message source from DNS host name - private string? GetSourceFromDnsHostName() - { - _logger.LogTrace("Retrieving the DNS host name..."); - - try - { - return $"/DNSHostName/{_dnsManager.GetHostName()}"; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to retrieve the DNS host name."); - return null; - } - } } diff --git a/test/AWS.Messaging.UnitTests/MessageSourceHandlerTests.cs b/test/AWS.Messaging.UnitTests/MessageSourceHandlerTests.cs index 6413f70..6c98e4c 100644 --- a/test/AWS.Messaging.UnitTests/MessageSourceHandlerTests.cs +++ b/test/AWS.Messaging.UnitTests/MessageSourceHandlerTests.cs @@ -15,7 +15,6 @@ namespace AWS.Messaging.UnitTests; public class MessageSourceHandlerTests { private readonly Mock _environmentManager; - private readonly Mock _dnsManager; private readonly Mock _ec2InstanceMetadataManager; private readonly Mock _ecsContainerMetadataManager; private readonly IMessageConfiguration _messageConfiguration; @@ -24,7 +23,6 @@ public class MessageSourceHandlerTests public MessageSourceHandlerTests() { _environmentManager = new Mock(); - _dnsManager = new Mock(); _ec2InstanceMetadataManager = new Mock(); _ecsContainerMetadataManager = new Mock(); _messageConfiguration = new MessageConfiguration(); @@ -36,14 +34,7 @@ public async Task MessageSourceIsSet() { _messageConfiguration.Source = "/aws/messaging"; - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -56,14 +47,7 @@ public async Task MessageSourceAndSuffixIsSet() _messageConfiguration.Source = "/aws/messaging"; _messageConfiguration.SourceSuffix = "/suffix"; - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -76,14 +60,7 @@ public async Task MessageSourceAndSuffixIsSet_SourceDoesntEndInSlash_SuffixDoesn _messageConfiguration.Source = "/aws/messaging"; _messageConfiguration.SourceSuffix = "suffix"; - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -96,14 +73,7 @@ public async Task MessageSourceAndSuffixIsSet_SourceEndsInSlash_SuffixStartsWith _messageConfiguration.Source = "/aws/messaging/"; _messageConfiguration.SourceSuffix = "/suffix"; - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -116,14 +86,7 @@ public async Task MessageSourceAndSuffixIsSet_SourceAndSuffixHaveWhitespace() _messageConfiguration.Source = " /aws/messaging/ "; _messageConfiguration.SourceSuffix = " /suffix "; - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -134,20 +97,12 @@ public async Task MessageSourceAndSuffixIsSet_SourceAndSuffixHaveWhitespace() public async Task MessageSourceNotSet_RunningLocally() { _ecsContainerMetadataManager.Setup(x => x.GetContainerTaskMetadata()).ReturnsAsync(new Dictionary()); - _dnsManager.Setup(x => x.GetHostName()).Returns("local"); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); - Assert.Equal("/DNSHostName/local", messageSource.ToString()); + Assert.Equal("/aws/messaging", messageSource.ToString()); } [Fact] @@ -155,20 +110,12 @@ public async Task MessageSourceNotSet_SuffixSet_RunningLocally() { _messageConfiguration.SourceSuffix = "/suffix"; _ecsContainerMetadataManager.Setup(x => x.GetContainerTaskMetadata()).ReturnsAsync(new Dictionary()); - _dnsManager.Setup(x => x.GetHostName()).Returns("local"); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); - Assert.Equal("/DNSHostName/local/suffix", messageSource.ToString()); + Assert.Equal("/aws/messaging/suffix", messageSource.ToString()); } [Fact] @@ -176,14 +123,7 @@ public async Task MessageSourceNotSet_RunningInLambda() { _environmentManager.Setup(x => x.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME")).Returns("lambda"); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -199,14 +139,7 @@ public async Task MessageSourceNotSet_RunningInECS() { "TaskARN", "taskArn" } }); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); @@ -219,37 +152,23 @@ public async Task MessageSourceNotSet_RunningInEC2() _ecsContainerMetadataManager.Setup(x => x.GetContainerTaskMetadata()).ReturnsAsync(new Dictionary()); _ec2InstanceMetadataManager.Setup(x => x.InstanceId).Returns("instanceId"); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); + var messageSourceHandler = GetMessageSourceHandler(); var messageSource = await messageSourceHandler.ComputeMessageSource(); Assert.Equal("/AmazonEC2/instanceId", messageSource.ToString()); } - [Fact] - public async Task MessageSourceNotSet_DnsThrowsException() + private IMessageSourceHandler GetMessageSourceHandler() { - _ecsContainerMetadataManager.Setup(x => x.GetContainerTaskMetadata()).ReturnsAsync(new Dictionary()); - _dnsManager.Setup(x => x.GetHostName()).Throws(); - var messageSourceHandler = new MessageSourceHandler( - _environmentManager.Object, - _dnsManager.Object, - _ecsContainerMetadataManager.Object, - _ec2InstanceMetadataManager.Object, - _messageConfiguration, - _logger.Object - ); - - var messageSource = await messageSourceHandler.ComputeMessageSource(); - - Assert.Equal("/aws/messaging", messageSource.ToString()); + _environmentManager.Object, + _ecsContainerMetadataManager.Object, + _ec2InstanceMetadataManager.Object, + _messageConfiguration, + _logger.Object + ); + + return messageSourceHandler; } } From 3ac714cf09eb4b3b40f74a775b91e7abbf99562b Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 15 Nov 2023 12:45:56 -0500 Subject: [PATCH 4/6] ci: add SNK file to the repo for strong name signing --- public.snk | Bin 0 -> 596 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public.snk diff --git a/public.snk b/public.snk new file mode 100644 index 0000000000000000000000000000000000000000..709adb3d6ac9f5fd24010a99a3550ff07e26bc91 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098oUs>>&(sFjkgE)1jCOSOFO0j#Xsox7y zuG{A&Cq>(Gbk!#x*PI^+_Gs+-j;gg{Z>MV9fgL=L#aLOe(49NLs7~zAx``hviZ8`c zLCwrJ!Ua83NgibiN112CVkcskc>wL(@g*+MiX;>83MliSbt-`%KLgCS_JrFvH5jxl zika3W+aJWDM^0Rw((O=;e0`yFr!?>?ClfBFG4^-4)==RJ>spg`dl>MGa#*em5~QP0 zIF&MBa}%pD1i-g=;q2y&!|MkZ-)1;T5#lDRBvi>r_;_o(_ac z!D@NH2lYG=;?G04G7CgFPhj!xQ5h;j-mGpkl}T(XKpvAtc=-y$fomO zAD@iAxuf~JKKc}bL9MjjLKErlH{_mgTfh`PI`IKh=99+F0?V&=vDBuRCc8-J#xGtsqzW4!)gYmQaW%Osj~9Q)>&5_#_Dc literal 0 HcmV?d00001 From 7a38d826e8b9ac5dea8f0a731679b7d7bff88696 Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Thu, 16 Nov 2023 11:55:33 -0600 Subject: [PATCH 5/6] feat: Add OpenTelemetry Instrumentation Provider (#68) Add OpenTelemetry Instrumentation Provider, and MessageEnvelope.Metadata is now serialized and uses JsonElements explicitly --- AWS.Messaging.sln | 7 + sampleapps/PublisherAPI/Program.cs | 8 + sampleapps/PublisherAPI/PublisherAPI.csproj | 3 + sampleapps/SubscriberService/Program.cs | 11 +- .../SubscriberService.csproj | 3 + ...S.Messaging.Telemetry.OpenTelemetry.csproj | 35 +++ .../Constants.cs | 15 ++ .../OpenTelemetryProvider.cs | 83 +++++++ .../OpenTelemetryTrace.cs | 105 +++++++++ .../README.md | 55 +++++ .../TracerProviderBuilderExtensions.cs | 30 +++ .../Configuration/AWSClientProvider.cs | 4 +- src/AWS.Messaging/MessageEnvelope.cs | 5 +- .../Serialization/EnvelopeSerializer.cs | 11 + src/AWS.Messaging/Services/HandlerInvoker.cs | 4 + src/AWS.Messaging/Telemetry/TelemetryKeys.cs | 13 +- .../AWS.Messaging.UnitTests.csproj | 2 + .../OpenTelemetryTests.cs | 213 ++++++++++++++++++ .../EnvelopeSerializerTests.cs | 8 +- 19 files changed, 605 insertions(+), 10 deletions(-) create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/Constants.cs create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryProvider.cs create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/README.md create mode 100644 src/AWS.Messaging.Telemetry.OpenTelemetry/TracerProviderBuilderExtensions.cs create mode 100644 test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs diff --git a/AWS.Messaging.sln b/AWS.Messaging.sln index 2ffc190..a51efaa 100644 --- a/AWS.Messaging.sln +++ b/AWS.Messaging.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Messaging.Lambda", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaMessaging", "sampleapps\LambdaMessaging\LambdaMessaging.csproj", "{F74A4CF0-D814-426E-8149-46758E86AFE3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Messaging.Telemetry.OpenTelemetry", "src\AWS.Messaging.Telemetry.OpenTelemetry\AWS.Messaging.Telemetry.OpenTelemetry.csproj", "{C529DC6E-72DA-49ED-908A-21DBC40F26C0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {F74A4CF0-D814-426E-8149-46758E86AFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU {F74A4CF0-D814-426E-8149-46758E86AFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU {F74A4CF0-D814-426E-8149-46758E86AFE3}.Release|Any CPU.Build.0 = Release|Any CPU + {C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,6 +88,7 @@ Global {A174942B-AF9C-4935-AD7B-AF651BACE63C} = {80DB2C77-6ADD-4A60-B27D-763BDF9659D3} {24FA3671-8C2B-4B64-865C-68FB6237E34D} = {2D0A561B-0B97-4259-8603-3AF5437BB652} {F74A4CF0-D814-426E-8149-46758E86AFE3} = {1AA8985B-897C-4BD5-9735-FD8B33FEBFFB} + {C529DC6E-72DA-49ED-908A-21DBC40F26C0} = {2D0A561B-0B97-4259-8603-3AF5437BB652} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B2B759D-6455-4089-8173-3F1619567B36} diff --git a/sampleapps/PublisherAPI/Program.cs b/sampleapps/PublisherAPI/Program.cs index 554193c..a6b5b57 100644 --- a/sampleapps/PublisherAPI/Program.cs +++ b/sampleapps/PublisherAPI/Program.cs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System.Text.Json; +using AWS.Messaging.Telemetry.OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using PublisherAPI.Models; var builder = WebApplication.CreateBuilder(args); @@ -33,6 +36,11 @@ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("PublisherAPI")) + .WithTracing(tracing => tracing + .AddAWSMessagingInstrumentation() + .AddConsoleExporter()); var app = builder.Build(); diff --git a/sampleapps/PublisherAPI/PublisherAPI.csproj b/sampleapps/PublisherAPI/PublisherAPI.csproj index e930d3e..439008d 100644 --- a/sampleapps/PublisherAPI/PublisherAPI.csproj +++ b/sampleapps/PublisherAPI/PublisherAPI.csproj @@ -7,10 +7,13 @@ + + + diff --git a/sampleapps/SubscriberService/Program.cs b/sampleapps/SubscriberService/Program.cs index dcfdc32..d5d456d 100644 --- a/sampleapps/SubscriberService/Program.cs +++ b/sampleapps/SubscriberService/Program.cs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 using System.Text.Json; +using AWS.Messaging.Telemetry.OpenTelemetry; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using SubscriberService.MessageHandlers; using SubscriberService.Models; @@ -37,7 +39,12 @@ await Host.CreateDefaultBuilder(args) PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; }); - }); + }) + .AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("SubscriberService")) + .WithTracing(tracing => tracing + .AddAWSMessagingInstrumentation() + .AddConsoleExporter()); }) .Build() .RunAsync(); diff --git a/sampleapps/SubscriberService/SubscriberService.csproj b/sampleapps/SubscriberService/SubscriberService.csproj index 1e522ad..7215a0c 100644 --- a/sampleapps/SubscriberService/SubscriberService.csproj +++ b/sampleapps/SubscriberService/SubscriberService.csproj @@ -20,9 +20,12 @@ + + + diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj new file mode 100644 index 0000000..001c2fd --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + true + AWS Message Processing Framework Instrumention for OpenTelemetry + https://github.com/awslabs/aws-dotnet-messaging + true + Major + README.md + CA1727 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/Constants.cs b/src/AWS.Messaging.Telemetry.OpenTelemetry/Constants.cs new file mode 100644 index 0000000..0f1a587 --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/Constants.cs @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Messaging.Telemetry.OpenTelemetry; + +/// +/// Constants related to the OpenTelemetry instrumentation for AWS.Messaging +/// +public class Constants +{ + /// + /// OpenTelemetry activity source name + /// + public const string SourceName = "AWS.Messaging"; +} diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryProvider.cs b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryProvider.cs new file mode 100644 index 0000000..ead3ed2 --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryProvider.cs @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; + +namespace AWS.Messaging.Telemetry.OpenTelemetry; + +/// +/// Creates OpenTelemetry traces +/// +public class OpenTelemetryProvider : ITelemetryProvider +{ + private static readonly ActivitySource _activitySource = new ActivitySource(Constants.SourceName, TelemetryKeys.AWSMessagingAssemblyVersion); + + /// + public ITelemetryTrace Trace(string traceName) + { + var activity = _activitySource.StartActivity(traceName, ActivityKind.Producer); + if (activity != null) + { + return new OpenTelemetryTrace(activity); + } + + // If we initially failed to create an activity, attempt to force creation with + // a link to the current activity, see https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities + var parentActivity = Activity.Current; + Activity.Current = null; + ActivityLink[]? links = null; + if (parentActivity != null) + { + links = new[] { new ActivityLink(parentActivity.Context) }; + } + + activity = _activitySource.StartActivity(traceName, ActivityKind.Producer, parentContext: default, links: links); + + return new OpenTelemetryTrace(activity, parentActivity); + } + + /// + public ITelemetryTrace Trace(string traceName, MessageEnvelope envelope) + { + var propogatedContext = Propagators.DefaultTextMapPropagator.Extract(default, envelope, ExtractTraceContextFromEnvelope); + Baggage.Current = propogatedContext.Baggage; + + var activity = _activitySource.StartActivity(traceName, ActivityKind.Consumer, parentContext: propogatedContext.ActivityContext); + if (activity != null) + { + return new OpenTelemetryTrace(activity); + } + + // If we initially failed to create an activity, attempt to force creation with + // a link to the current activity, see https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities + var parentActivity = Activity.Current; + Activity.Current = null; + ActivityLink[]? links = null; + if (parentActivity != null) + { + links = new[] { new ActivityLink(parentActivity.Context) }; + } + + activity = _activitySource.StartActivity(traceName, ActivityKind.Consumer, parentContext: propogatedContext.ActivityContext, links: links); + + return new OpenTelemetryTrace(activity, parentActivity); + } + + /// + /// Extracts propagation context from a , meant to be used with + /// + /// Inbound message envelope + /// Context key + /// Context value + private IEnumerable ExtractTraceContextFromEnvelope(MessageEnvelope envelope, string key) + { + if (envelope.Metadata.TryGetValue(key, out var jsonElement)) + { + return new string[] { jsonElement.ToString() }; + } + + return Enumerable.Empty(); + } +} diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs new file mode 100644 index 0000000..dd38f79 --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Text.Json; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Trace; + +namespace AWS.Messaging.Telemetry.OpenTelemetry; + +/// +/// An OpenTelemetry trace (wrapper around a ) +/// +public class OpenTelemetryTrace : ITelemetryTrace +{ + private readonly Activity? _activity; + private readonly Activity? _parentToRestore; + + /// + /// Creates a new trace + /// + /// New trace + /// Optional parent activity that will be set as when this trace is disposed + public OpenTelemetryTrace(Activity? activity, Activity? parentToRestore = null) + { + _activity = activity; + _parentToRestore = parentToRestore; + } + + /// + public void AddException(Exception exception, bool fatal = true) + { + _activity?.RecordException(exception); + + if (fatal) + { + _activity?.SetStatus(ActivityStatusCode.Error, exception.Message); + } + } + + /// + public void AddMetadata(string key, object value) + { + if (_activity != null && _activity.IsAllDataRequested) + { + _activity.SetTag(key, value); + } + } + + /// + public void RecordTelemetryContext(MessageEnvelope envelope) + { + ActivityContext contextToInject = default; + if (_activity != null) + { + contextToInject = _activity.Context; + } + // Even if an "AWS.Messaging" activity was not created, we still + // propogate the current activity (if it exists) through the message envelope + else if (Activity.Current != null) + { + contextToInject = Activity.Current.Context; + } + + Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(contextToInject, Baggage.Current), envelope, InjectTraceContextIntoEnvelope); + } + + /// + /// Stores propagation context in the , meant to be used with + /// + /// Outbound message envelope + /// Context key + /// Context value + private void InjectTraceContextIntoEnvelope(MessageEnvelope envelope, string key, string value) + { + envelope.Metadata[key] = JsonSerializer.SerializeToElement(value); + } + + private bool _disposed; + + /// + /// Disposes the inner , and also restores the parent activity if set + /// + /// Indicates whether the call comes from Dispose (true) or a finalizer (false) + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _activity?.Dispose(); + if (_parentToRestore != null) + { + Activity.Current = _parentToRestore; + } + _disposed = true; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/README.md b/src/AWS.Messaging.Telemetry.OpenTelemetry/README.md new file mode 100644 index 0000000..879d3c6 --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/README.md @@ -0,0 +1,55 @@ +# OpenTelemetry plugin for AWS Message Processing Framework for .NET + +**Notice:** *This library is still in early active development and is not ready for use beyond experimentation.* + +This package is an [Instrumentation +Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), which instruments the [AWS Message Processing Framework for .NET](https://github.com/awslabs/aws-dotnet-messaging) to collect traces about +messages that are sent and received. + +## Configuration + +### 1. Install Packages +Add a reference to [`AWS.Messaging.Telemetry.OpenTelemetry`](https://www.nuget.org/packages/AWS.Messaging.Telemetry.OpenTelemetry) and [`OpenTelemetry.Extensions.Hosting`](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). + +In this example, we're going to configure OpenTelemetry on our `IServiceCollection`, so also add a reference to [`OpenTelemetry.Extensions.Hosting`](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). This is not required if starting and stopping tracing via `CreateTracerProviderBuilder`. + +You may also add a reference to one or more [exporters](https://opentelemetry.io/docs/instrumentation/net/exporters/) to visualize your telemetry data. + +```shell +dotnet add package --prerelease AWS.Messaging.Telemetry.OpenTelemetry +dotnet add package OpenTelemetry.Extensions.Hosting +``` + +### 2. Enable Instrumentation +In the `Startup` class add a call to `AddOpenTelemetry` to configure OpenTelemetry. On the `TracerProviderBuilder`, call `AddAWSMessagingInstrumentation` to begin capturing traces for the AWS Message Processing Framework for .NET. + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddAWSMessageBus(builder => + { + builder.AddSQSPoller("https://sqs.us-west-2.amazonaws.com/012345678910/MPF"); + builder.AddMessageHandler("chatMessage"); + }); + + services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("myApplication")) + .WithTracing(tracing => tracing + .AddAWSMessagingInstrumentation() + .AddConsoleExporter()); + } +} +``` + +# Useful Links +* [AWS Message Processing Framework for .NET Design Document](../../docs/design/message-processing-framework-design.md) + +# Security + +See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. + +# License + +This project is licensed under the Apache-2.0 License. diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/AWS.Messaging.Telemetry.OpenTelemetry/TracerProviderBuilderExtensions.cs new file mode 100644 index 0000000..40f61e2 --- /dev/null +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Trace; + +namespace AWS.Messaging.Telemetry.OpenTelemetry; + +/// +/// Extensions for a to enable instrumentation for AWS Messaging +/// +public static class TracerProviderBuilderExtensions +{ + /// + /// Enables AWS Messaging Instrumentation for OpenTelemetry + /// + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAWSMessagingInstrumentation(this TracerProviderBuilder builder) + { + builder.ConfigureServices(services => + { + services.AddSingleton(); + }); + + builder.AddSource(Constants.SourceName); + + return builder; + } +} diff --git a/src/AWS.Messaging/Configuration/AWSClientProvider.cs b/src/AWS.Messaging/Configuration/AWSClientProvider.cs index 4655591..e83755c 100644 --- a/src/AWS.Messaging/Configuration/AWSClientProvider.cs +++ b/src/AWS.Messaging/Configuration/AWSClientProvider.cs @@ -3,6 +3,7 @@ using System.Reflection; using Amazon.Runtime; +using AWS.Messaging.Telemetry; namespace AWS.Messaging.Configuration; @@ -12,8 +13,7 @@ namespace AWS.Messaging.Configuration; internal class AWSClientProvider : IAWSClientProvider { private const string _userAgentHeader = "User-Agent"; - private static readonly string _assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; - private static readonly string _userAgentString = $"lib/aws-dotnet-messaging_{_assemblyVersion}"; + private static readonly string _userAgentString = $"lib/aws-dotnet-messaging_{TelemetryKeys.AWSMessagingAssemblyVersion}"; private readonly IServiceProvider _serviceProvider; diff --git a/src/AWS.Messaging/MessageEnvelope.cs b/src/AWS.Messaging/MessageEnvelope.cs index 794bb78..557acc7 100644 --- a/src/AWS.Messaging/MessageEnvelope.cs +++ b/src/AWS.Messaging/MessageEnvelope.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Text.Json; using System.Text.Json.Serialization; namespace AWS.Messaging; @@ -45,9 +46,11 @@ public abstract class MessageEnvelope /// /// This stores different metadata that is not modeled as a top-level property in MessageEnvelope class. + /// These entries will also be serialized as top-level properties when sending the message, which + /// can be used for CloudEvents Extension Attributes. /// [JsonExtensionData] - public Dictionary Metadata { get; set; } = new Dictionary(); + public Dictionary Metadata { get; set; } = new Dictionary(); /// /// Stores metadata related to Amazon SQS. diff --git a/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs b/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs index 49f26b8..d5e7781 100644 --- a/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs +++ b/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs @@ -95,6 +95,17 @@ public async ValueTask SerializeAsync(MessageEnvelope envelope) ["data"] = _messageSerializer.Serialize(message) }; + // Write any Metadata as top-level keys + // This may be useful for any extensions defined in + // https://github.com/cloudevents/spec/tree/main/cloudevents/extensions + foreach (var key in envelope.Metadata.Keys) + { + if (!blob.ContainsKey(key)) // don't overwrite any reserved keys + { + blob[key] = JsonSerializer.SerializeToNode(envelope.Metadata[key]); + } + } + var jsonString = blob.ToJsonString(); var serializedMessage = await InvokePostSerializationCallback(jsonString); diff --git a/src/AWS.Messaging/Services/HandlerInvoker.cs b/src/AWS.Messaging/Services/HandlerInvoker.cs index 574c03c..b7667d5 100644 --- a/src/AWS.Messaging/Services/HandlerInvoker.cs +++ b/src/AWS.Messaging/Services/HandlerInvoker.cs @@ -49,6 +49,10 @@ public async Task InvokeAsync(MessageEnvelope messageEnvel trace.AddMetadata(TelemetryKeys.MessageId, messageEnvelope.Id); trace.AddMetadata(TelemetryKeys.MessageType, messageEnvelope.MessageTypeIdentifier); trace.AddMetadata(TelemetryKeys.HandlerType, subscriberMapping.HandlerType.FullName!); + if (!string.IsNullOrEmpty(messageEnvelope.SQSMetadata?.MessageID)) + { + trace.AddMetadata(TelemetryKeys.SqsMessageId, messageEnvelope.SQSMetadata.MessageID); + } using (var scope = _serviceProvider.CreateScope()) { diff --git a/src/AWS.Messaging/Telemetry/TelemetryKeys.cs b/src/AWS.Messaging/Telemetry/TelemetryKeys.cs index 2234c22..315c49a 100644 --- a/src/AWS.Messaging/Telemetry/TelemetryKeys.cs +++ b/src/AWS.Messaging/Telemetry/TelemetryKeys.cs @@ -1,11 +1,22 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Reflection; + namespace AWS.Messaging.Telemetry; -internal static class TelemetryKeys +/// +/// Constants related to telemetry +/// +public static class TelemetryKeys { + /// + /// Current version of the AWS.Messaging package + /// + public static string AWSMessagingAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; + internal const string QueueUrl = "aws.messaging.sqs.queueurl"; + internal const string SqsMessageId = "aws.messaging.sqs.messageId"; internal const string TopicUrl = "aws.messaging.sns.topicUrl"; internal const string EventBusName = "aws.messaging.eventBridge.eventBusName"; internal const string ObjectType = "aws.messaging.objectType"; diff --git a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj index e011800..7a17fc0 100644 --- a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj +++ b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj @@ -7,6 +7,7 @@ + @@ -20,6 +21,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs b/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs new file mode 100644 index 0000000..075ea18 --- /dev/null +++ b/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs @@ -0,0 +1,213 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using AWS.Messaging.Configuration; +using AWS.Messaging.Publishers; +using AWS.Messaging.Serialization; +using AWS.Messaging.Services; +using AWS.Messaging.Telemetry; +using AWS.Messaging.Telemetry.OpenTelemetry; +using AWS.Messaging.UnitTests.MessageHandlers; +using AWS.Messaging.UnitTests.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Xunit; + +namespace AWS.Messaging.UnitTests; + +public class OpenTelemetryTests +{ + private readonly ServiceProvider _serviceProvider; + private readonly MessageRoutingPublisher _publisher; + private readonly HandlerInvoker _handler; + private readonly SubscriberMapping _subscriberMapping; + + /// + /// Initializes all the services needed to publish and handle + /// messages without actually using SQS + /// + public OpenTelemetryTests() + { + var envelopeSerializer = new Mock(); + var messageConfiguration = new Mock(); + var logger = new Mock>(); + var publisherMapping = new PublisherMapping(typeof(ChatMessage), new SQSPublisherConfiguration("endpoint"), PublisherTargetType.SQS_PUBLISHER); + _subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + + envelopeSerializer.SetReturnsDefault(ValueTask.FromResult(new MessageEnvelope() + { + Id = "1234", + Source = new Uri("/aws/messaging/unittest", UriKind.Relative) + })); + + messageConfiguration.Setup(x => x.GetPublisherMapping(typeof(ChatMessage))).Returns(publisherMapping); + messageConfiguration.Setup(x => x.GetSubscriberMapping(typeof(ChatMessage))).Returns(_subscriberMapping); + + var services = new ServiceCollection(); + services.AddSingleton(new Mock().Object); + services.AddSingleton(logger.Object); + services.AddSingleton(messageConfiguration.Object); + services.AddSingleton(envelopeSerializer.Object); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + _serviceProvider = services.BuildServiceProvider(); + + _publisher = new MessageRoutingPublisher( + _serviceProvider, + messageConfiguration.Object, + logger.Object, + new DefaultTelemetryFactory(_serviceProvider)); + + _handler = new HandlerInvoker( + _serviceProvider, + new NullLogger(), + new DefaultTelemetryFactory(_serviceProvider)); + } + + /// + /// Verifies that the expected traces and tags are created when publishing a message + /// + [Fact] + public async Task OpenTelemetry_Publisher_ExpectedTracesAndTags() + { + var activities = new List(); + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(Constants.SourceName) + .ConfigureResource(resource => resource.AddService("unittest")) + .AddInMemoryExporter(activities).Build()) + { + + await _publisher.PublishAsync(new ChatMessage { MessageDescription = "Test Description" }); + } + + Assert.Equal(2, activities.Count); + Assert.Equal("AWS.Messaging: Publish to AWS SQS", activities[0].OperationName); + Assert.Equal(4, activities[0].Tags.Count()); + Assert.Contains(new KeyValuePair("aws.messaging.objectType", "AWS.Messaging.UnitTests.Models.ChatMessage"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.messageType", "AWS.Messaging.UnitTests.Models.ChatMessage"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.sqs.queueurl", "endpoint"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.messageId", "1234"), activities[0].Tags); + + Assert.Equal("AWS.Messaging: Routing message to AWS service", activities[1].OperationName); + Assert.Equal(2, activities[1].Tags.Count()); + Assert.Contains(new KeyValuePair("aws.messaging.objectType", "AWS.Messaging.UnitTests.Models.ChatMessage"), activities[1].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.publishTargetType", "SQS"), activities[1].Tags); + } + + /// + /// Verifies that when we need to manipulate in order + /// to force creation of our activity, that we reset the original activity at the end. + /// + [Fact] + public async Task OpenTelemetry_Publisher_ResetsParentActivity() + { + var activities = new List(); + + // Start a non-MPF activity + var existingActivity = new Activity("current").Start(); + Activity.Current = existingActivity; + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(Constants.SourceName) + .ConfigureResource(resource => resource.AddService("unittest")) + .AddInMemoryExporter(activities).Build()) + { + await _publisher.PublishAsync(new ChatMessage { MessageDescription = "Test Description" }); + } + + // We expect the top-level MPF activity to reset Activity.Current once disposed + Assert.Equal(existingActivity, Activity.Current); + existingActivity.Stop(); + } + + /// + /// Verifies that the expected traces and tags are created when handling a message + /// + [Fact] + public async Task OpenTelemetry_Handler_ExpectedTracesAndTags() + { + var activities = new List(); + var envelope = new MessageEnvelope() + { + MessageTypeIdentifier = "AWS.Messaging.UnitTests.Models.ChatMessage", + Id = "1234", + SQSMetadata = new SQSMetadata() + { + MessageID = "4567" + }, + Message = new ChatMessage { MessageDescription = "Test Description" } + }; + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(Constants.SourceName) + .ConfigureResource(resource => resource.AddService("unittest")) + .AddInMemoryExporter(activities).Build()) + { + + await _handler.InvokeAsync(envelope, _subscriberMapping); + } + + Assert.Single(activities); + Assert.Equal("AWS.Messaging: Processing message", activities[0].OperationName); + Assert.Equal(4, activities[0].Tags.Count()); + Assert.Contains(new KeyValuePair("aws.messaging.messageId", "1234"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.messageType", "AWS.Messaging.UnitTests.Models.ChatMessage"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.handlerType", "AWS.Messaging.UnitTests.MessageHandlers.ChatMessageHandler"), activities[0].Tags); + Assert.Contains(new KeyValuePair("aws.messaging.sqs.messageId", "4567"), activities[0].Tags); + } + + /// + /// Verifies that the handler trace has the correct parent when included + /// in the message envelope + /// + [Fact] + public async Task OpenTelemetry_Handler_ParentFromEnvelope() + { + var activities = new List(); + var envelope = new MessageEnvelope() + { + MessageTypeIdentifier = "AWS.Messaging.UnitTests.Models.ChatMessage", + Id = "1234", + SQSMetadata = new SQSMetadata() + { + MessageID = "4567" + }, + Metadata = new Dictionary + { + { "traceparent", JsonDocument.Parse("\"00-d2d8865217873923d2d74cf680a30ac3-d63e320582f9ff94-01\"").RootElement } + }, + Message = new ChatMessage { MessageDescription = "Test Description" } + }; + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(Constants.SourceName) + .ConfigureResource(resource => resource.AddService("unittest")) + .AddInMemoryExporter(activities).Build()) + { + + await _handler.InvokeAsync(envelope, _subscriberMapping); + } + + Assert.Single(activities); + Assert.Equal("AWS.Messaging: Processing message", activities[0].OperationName); + + // The MPF activity's parent should be the one specified in envelope.Metadata above + Assert.Equal("00-d2d8865217873923d2d74cf680a30ac3-d63e320582f9ff94-01", activities[0].ParentId); + } +} diff --git a/test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs b/test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs index 7a77fc1..83fd740 100644 --- a/test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs +++ b/test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs @@ -343,7 +343,7 @@ public async Task SerializationCallbacks_AreCorrectlyInvoked() var serializedMessage = await envelopeSerializer.SerializeAsync(messageEnvelope); // ASSERT - Check expected base 64 encoded string - var expectedserializedMessage = "eyJpZCI6IjEyMyIsInNvdXJjZSI6Ii9hd3MvbWVzc2FnaW5nIiwic3BlY3ZlcnNpb24iOiIxLjAiLCJ0eXBlIjoiYWRkcmVzc0luZm8iLCJ0aW1lIjoiMjAwMC0xMi0wNVQxMDozMDo1NSswMDowMCIsImRhdGEiOiJ7XHUwMDIyVW5pdFx1MDAyMjoxMjMsXHUwMDIyU3RyZWV0XHUwMDIyOlx1MDAyMlByaW5jZSBTdFx1MDAyMixcdTAwMjJaaXBDb2RlXHUwMDIyOlx1MDAyMjAwMDAxXHUwMDIyfSJ9"; + var expectedserializedMessage = "eyJpZCI6IjEyMyIsInNvdXJjZSI6Ii9hd3MvbWVzc2FnaW5nIiwic3BlY3ZlcnNpb24iOiIxLjAiLCJ0eXBlIjoiYWRkcmVzc0luZm8iLCJ0aW1lIjoiMjAwMC0xMi0wNVQxMDozMDo1NSswMDowMCIsImRhdGEiOiJ7XHUwMDIyVW5pdFx1MDAyMjoxMjMsXHUwMDIyU3RyZWV0XHUwMDIyOlx1MDAyMlByaW5jZSBTdFx1MDAyMixcdTAwMjJaaXBDb2RlXHUwMDIyOlx1MDAyMjAwMDAxXHUwMDIyfSIsIklzLURlbGl2ZXJlZCI6ZmFsc2V9"; Assert.Equal(expectedserializedMessage, serializedMessage); // ACT - Convert To Envelope from base 64 Encoded Message @@ -362,7 +362,7 @@ public async Task SerializationCallbacks_AreCorrectlyInvoked() Assert.Equal("1.0", envelope.Version); Assert.Equal("/aws/messaging", envelope.Source?.ToString()); Assert.Equal("addressInfo", envelope.MessageTypeIdentifier); - Assert.Equal(true, envelope.Metadata["Is-Delivered"]); + Assert.True(envelope.Metadata["Is-Delivered"].GetBoolean()); var subscribeMapping = conversionResult.Mapping; Assert.NotNull(subscribeMapping); @@ -376,7 +376,7 @@ public class MockSerializationCallback : ISerializationCallback { public ValueTask PreSerializationAsync(MessageEnvelope messageEnvelope) { - messageEnvelope.Metadata["Is-Delivered"] = false; + messageEnvelope.Metadata["Is-Delivered"] = JsonSerializer.SerializeToElement(false); return ValueTask.CompletedTask; } @@ -396,7 +396,7 @@ public ValueTask PreDeserializationAsync(string message) public ValueTask PostDeserializationAsync(MessageEnvelope messageEnvelope) { - messageEnvelope.Metadata["Is-Delivered"] = true; + messageEnvelope.Metadata["Is-Delivered"] = JsonSerializer.SerializeToElement(true); return ValueTask.CompletedTask; } } From 4724c9f6e076dfa4768e47cdfec2c74ebb398733 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 17 Nov 2023 16:47:40 -0500 Subject: [PATCH 6/6] chore: add strong naming, version and changelog --- CHANGELOG.md | 1 + src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj | 3 +++ .../AWS.Messaging.Telemetry.OpenTelemetry.csproj | 3 +++ src/AWS.Messaging/AWS.Messaging.csproj | 3 +++ src/AWS.Messaging/Properties/AssemblyInfo.cs | 8 ++++---- .../AWS.Messaging.IntegrationTests.csproj | 2 ++ .../AWS.Messaging.Tests.Common.csproj | 2 ++ .../AWS.Messaging.UnitTests.csproj | 2 ++ 8 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5ddad42 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog \ No newline at end of file diff --git a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj index 7e8be4d..1eae68c 100644 --- a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj +++ b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj @@ -9,6 +9,9 @@ Major README.md CA1727 + true + ..\..\public.snk + 0.1.0 diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj index 001c2fd..6bfd4bb 100644 --- a/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj @@ -11,6 +11,9 @@ Major README.md CA1727 + true + ..\..\public.snk + 0.1.0 diff --git a/src/AWS.Messaging/AWS.Messaging.csproj b/src/AWS.Messaging/AWS.Messaging.csproj index e78a7f2..c0cc827 100644 --- a/src/AWS.Messaging/AWS.Messaging.csproj +++ b/src/AWS.Messaging/AWS.Messaging.csproj @@ -10,6 +10,9 @@ README.md True CA1727 + true + ..\..\public.snk + 0.1.0 diff --git a/src/AWS.Messaging/Properties/AssemblyInfo.cs b/src/AWS.Messaging/Properties/AssemblyInfo.cs index c6eb5b4..e929977 100644 --- a/src/AWS.Messaging/Properties/AssemblyInfo.cs +++ b/src/AWS.Messaging/Properties/AssemblyInfo.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("AWS.Messaging.Lambda")] -[assembly: InternalsVisibleTo("AWS.Messaging.UnitTests")] -[assembly: InternalsVisibleTo("AWS.Messaging.IntegrationTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("AWS.Messaging.Lambda, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4")] +[assembly: InternalsVisibleTo("AWS.Messaging.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4")] +[assembly: InternalsVisibleTo("AWS.Messaging.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj b/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj index 689af5b..d072bae 100644 --- a/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj +++ b/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj @@ -4,6 +4,8 @@ net6.0 false enable + true + ..\..\public.snk diff --git a/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj b/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj index fdd6a3c..be0bba8 100644 --- a/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj +++ b/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj @@ -4,6 +4,8 @@ net6.0 enable enable + true + ..\..\public.snk diff --git a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj index 7a17fc0..f9a2f09 100644 --- a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj +++ b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj @@ -4,6 +4,8 @@ net6.0 false enable + true + ..\..\public.snk