diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/ArnBuilder.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/ArnBuilder.cs index b067b5b06b..86ba2deab3 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/ArnBuilder.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/ArnBuilder.cs @@ -138,7 +138,7 @@ public override string ToString() string partition = string.IsNullOrEmpty(Partition) ? "[Missing]" : Partition; string region = string.IsNullOrEmpty(Region) ? "[Missing]" : Region; string idPresent = string.IsNullOrEmpty(AccountId) ? "[Missing]" : "[Present]"; - + return $"Partition: {partition}, Region: {region}, AccountId: {idPresent}"; } @@ -155,6 +155,5 @@ private string ConstructArn(string partition, string service, string region, str } return "arn:" + partition + ":" + service + ":" + region + ":" + accountId + ":" + resource; } - } } diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/AwsAccountIdDecoder.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/AwsAccountIdDecoder.cs index a00be1f478..a6e3dea2fb 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/AwsAccountIdDecoder.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/AwsAccountIdDecoder.cs @@ -31,13 +31,20 @@ public static string GetAccountId(string awsAccessKeyId) /// /// Performs a Base-32 decode of the specified input string. /// Allowed character range is a-z and 2-7. 'a' being 0 and '7' is 31. + /// + /// public to allow for unit testing /// /// The string to be decoded. Must be at least 10 characters. /// A long containing first 6 bytes of the base 32 decoded data. /// If src has less than 10 characters. /// If src contains invalid characters for Base-32 - private static long Base32Decode(string src) + public static long Base32Decode(string src) { + if (string.IsNullOrEmpty(src)) + { + throw new ArgumentNullException(nameof(src), "The input string cannot be null or empty."); + } + if (src.Length < 10) { throw new ArgumentException("The input string must be at least 10 characters long.", nameof(src)); diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs index 615a9ea8e5..62e2c72aec 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs @@ -39,6 +39,9 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins // convert the access key to an account id AwsAccountId = AwsAccountIdDecoder.GetAccountId(accessKey); + + // TODO: TESTING ONLY + agent.Logger.Info($"Successfully parsed AWS AccountId: {AwsAccountId}"); } catch (Exception e) { diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs index e473c89968..a77591ed98 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs @@ -33,10 +33,13 @@ private ArnBuilder CreateArnBuilder(IAgent agent, dynamic requestContext) try { accountId = GetAccountId(agent); - var clientconfig = requestContext.ClientConfig; - var regionEndpoint = clientconfig.RegionEndpoint; - systemName = regionEndpoint.SystemName; - partition = regionEndpoint.PartitionName; + var clientConfig = requestContext.ClientConfig; + if (clientConfig.RegionEndpoint != null) + { + var regionEndpoint = clientConfig.RegionEndpoint; + systemName = regionEndpoint.SystemName; + partition = regionEndpoint.PartitionName; + } } catch (Exception e) { @@ -46,8 +49,10 @@ private ArnBuilder CreateArnBuilder(IAgent agent, dynamic requestContext) _reportBadArnBuilder = false; } } - - return new ArnBuilder(partition, systemName, accountId); + agent.Logger.Debug($"AwsSdkPipelineWrapper: Creating ArnBuilder with partition: {partition}, systemName: {systemName}, accountId: {accountId}"); + var arnBuilder = new ArnBuilder(partition, systemName, accountId); + agent.Logger.Debug($"AwsSdkPipelineWrapper: ArnBuilder created: {arnBuilder}"); + return arnBuilder; } private string GetAccountId(IAgent agent) @@ -66,6 +71,8 @@ private string GetAccountId(IAgent agent) } } + // TODO: testing only + agent.Logger.Debug($"AwsSdkPipelineWrapper: Using accountId: {accountId}"); return accountId; } @@ -104,17 +111,19 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins { return SQSRequestHandler.HandleSQSRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext); } - else if (requestType == "Amazon.Lambda.Model.InvokeRequest") + + if (requestType == "Amazon.Lambda.Model.InvokeRequest") { return LambdaInvokeRequestHandler.HandleInvokeRequest(instrumentedMethodCall, agent, transaction, request, isAsync, builder); - } - else if (requestType.StartsWith("Amazon.DynamoDBv2")) + } + + if (requestType.StartsWith("Amazon.DynamoDBv2")) { - return DynamoDbRequestHandler.HandleDynamoDbRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext, builder); + return DynamoDbRequestHandler.HandleDynamoDbRequest(instrumentedMethodCall, agent, transaction, request, isAsync, builder); } if (!_unsupportedRequestTypes.Contains(requestType)) // log once per unsupported request type - { + { agent.Logger.Debug($"AwsSdkPipelineWrapper: Unsupported request type: {requestType}. Returning NoOp delegate."); _unsupportedRequestTypes.Add(requestType); } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/DynamoDbRequestHandler.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/DynamoDbRequestHandler.cs index a559fcc94e..4d3d2d220a 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/DynamoDbRequestHandler.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/DynamoDbRequestHandler.cs @@ -15,7 +15,7 @@ internal static class DynamoDbRequestHandler private static readonly ConcurrentDictionary _operationNameCache = new(); - public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, dynamic executionContext, ArnBuilder builder) + public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, ArnBuilder builder) { var requestType = ((object)request).GetType().Name; @@ -29,8 +29,12 @@ public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMetho // TODO: The entity relationship docs suggest cloud.resource_id should be a span attribute, so maybe we added it to the DataStore segment below instead?? var arn = builder.Build("dynamodb", $"table/{model}"); - if (string.IsNullOrEmpty(arn)) + if (!string.IsNullOrEmpty(arn)) transaction.AddCloudSdkAttribute("cloud.resource_id", arn); + else + { + agent.Logger.Debug($"Unable to build ARN for DynamoDB request. ArnBuilder reports: {builder}"); + } var segment = transaction.StartDatastoreSegment(instrumentedMethodCall.MethodCall, new ParsedSqlStatement(DatastoreVendor.DynamoDB, model, operation), isLeaf: true); diff --git a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs index e07fe0aa84..859b74c3aa 100644 --- a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs +++ b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs @@ -8,7 +8,7 @@ using System; using System.Collections.Generic; using Amazon.Runtime; -using System.Threading; +using Amazon; namespace AwsSdkTestApp.AwsSdkExercisers { @@ -25,12 +25,16 @@ public AwsSdkDynamoDBExerciser() private AmazonDynamoDBClient GetDynamoDBClient() { - AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig(); - // Set the endpoint URL - clientConfig.ServiceURL = "http://dynamodb:8000"; // port must match what is set in docker compose - clientConfig.AuthenticationRegion = "us-west-2"; - var creds = new BasicAWSCredentials("xxx", "xxx"); - AmazonDynamoDBClient client = new AmazonDynamoDBClient(creds, clientConfig); + AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig + { + // Set the endpoint URL + ServiceURL = "http://dynamodb:8000", // port must match what is set in docker compose + AuthenticationRegion = "us-west-2", + RegionEndpoint = RegionEndpoint.USWest2 + }; + + // use plausible (but fake) access key and fake secret key so account id parsing can be tested + AmazonDynamoDBClient client = new AmazonDynamoDBClient("FOOIHSFODNNAEXAMPLE", "MOREGIBBERISH", clientConfig); return client; } diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs index b70159f279..d8dad5675d 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs @@ -32,7 +32,7 @@ public class AwsSdkContainerSQSTestFixture : AwsSdkContainerTestFixtureBase { private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; - private const string DistroTag = "jammy"; + private const string DistroTag = "noble"; private readonly string BaseUrl; @@ -78,7 +78,7 @@ public class AwsSdkContainerDynamoDBTestFixture : AwsSdkContainerTestFixtureBase { private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; - private const string DistroTag = "jammy"; + private const string DistroTag = "noble"; private readonly string BaseUrl; diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs index c136e83bf7..ed9e7955be 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs @@ -23,6 +23,8 @@ protected AwsSdkDynamoDBTestBase(AwsSdkContainerDynamoDBTestFixture fixture, ITe _fixture = fixture; _fixture.TestLogger = output; + _fixture.SetAdditionalEnvironmentVariable("AWSSDK_INITCOLLECTIONS", "true"); + _fixture.Actions(setupConfiguration: () => { var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); @@ -54,9 +56,9 @@ protected AwsSdkDynamoDBTestBase(AwsSdkContainerDynamoDBTestFixture fixture, ITe _fixture.AgentLog.WaitForLogLine(AgentLogBase.MetricDataLogLineRegex, TimeSpan.FromMinutes(2)); _fixture.AgentLog.WaitForLogLine(AgentLogBase.TransactionTransformCompletedLogLineRegex, TimeSpan.FromMinutes(2)); - // shut down the container and wait for the agent log to see it - _fixture.ShutdownRemoteApplication(); - _fixture.AgentLog.WaitForLogLine(AgentLogBase.ShutdownLogLineRegex, TimeSpan.FromSeconds(10)); + //// shut down the container and wait for the agent log to see it + //_fixture.ShutdownRemoteApplication(); + //_fixture.AgentLog.WaitForLogLine(AgentLogBase.ShutdownLogLineRegex, TimeSpan.FromSeconds(10)); }); _fixture.Initialize(); diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs index edd06af3fb..9015497449 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs @@ -40,7 +40,7 @@ protected AwsSdkSQSTestBase(AwsSdkContainerSQSTestFixture fixture, ITestOutputHe configModifier.ConfigureFasterMetricsHarvestCycle(15); configModifier.ConfigureFasterSpanEventsHarvestCycle(15); configModifier.ConfigureFasterTransactionTracesHarvestCycle(15); - configModifier.LogToConsole(); + //configModifier.LogToConsole(); }, exerciseApplication: () => diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs index c665fe3052..bfb7db67fe 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs @@ -351,6 +351,7 @@ public virtual void Initialize() catch (Exception ex) { TestLogger?.WriteLine("Exception occurred in Initialize: " + ex.ToString()); + AgentLogExpected = false; throw; } finally diff --git a/tests/Agent/IntegrationTests/Shared/AwsBedrockConfiguration.cs b/tests/Agent/IntegrationTests/Shared/AwsConfiguration.cs similarity index 96% rename from tests/Agent/IntegrationTests/Shared/AwsBedrockConfiguration.cs rename to tests/Agent/IntegrationTests/Shared/AwsConfiguration.cs index fc446cb945..26158c52b0 100644 --- a/tests/Agent/IntegrationTests/Shared/AwsBedrockConfiguration.cs +++ b/tests/Agent/IntegrationTests/Shared/AwsConfiguration.cs @@ -3,7 +3,7 @@ namespace NewRelic.Agent.IntegrationTests.Shared { - public class AwsBedrockConfiguration + public class AwsConfiguration { public static string AwsAccessKeyId { diff --git a/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/LLM/BedrockModels.cs b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/LLM/BedrockModels.cs index f7d16f8a9e..d2731d06d5 100644 --- a/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/LLM/BedrockModels.cs +++ b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/LLM/BedrockModels.cs @@ -20,7 +20,7 @@ namespace MultiFunctionApplicationHelpers.NetStandardLibraries.LLM internal class BedrockModels { private static readonly AmazonBedrockRuntimeClient _amazonBedrockRuntimeClient = - new AmazonBedrockRuntimeClient(AwsBedrockConfiguration.AwsAccessKeyId, AwsBedrockConfiguration.AwsSecretAccessKey, AwsBedrockConfiguration.AwsRegion.ToRegionId()); + new AmazonBedrockRuntimeClient(AwsConfiguration.AwsAccessKeyId, AwsConfiguration.AwsSecretAccessKey, AwsConfiguration.AwsRegion.ToRegionId()); [MethodImpl(MethodImplOptions.NoInlining)] public static async Task InvokeAmazonEmbedAsync(string prompt, bool generateError) => await InvokeTitanAsync(prompt, true, generateError); diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsAccountIdDecoderTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsAccountIdDecoderTests.cs new file mode 100644 index 0000000000..c884ed0f16 --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsAccountIdDecoderTests.cs @@ -0,0 +1,102 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using NewRelic.Agent.Extensions.AwsSdk; +using NUnit.Framework; +using Telerik.JustMock; + +namespace Agent.Extensions.Tests.Helpers +{ + [TestFixture] + internal class AwsAccountIdDecoderTests + { + [Test] + public void GetAccountId_ValidAwsAccessKeyId_ReturnsExpectedAccountId() + { + // Arrange + string awsAccessKeyId = "AKIAIOSFODNN7EXAMPLE"; // not a real AWS access key! + string expectedAccountId = "581039954779"; + + // Act + string actualAccountId = AwsAccountIdDecoder.GetAccountId(awsAccessKeyId); + + // Assert + Assert.That(expectedAccountId, Is.EqualTo(actualAccountId)); + } + + [Test] + public void GetAccountId_NullOrEmptyAwsAccessKeyId_ThrowsArgumentNullException() + { + // Arrange + string awsAccessKeyId = null; + + // Act & Assert + var ex = Assert.Throws(() => AwsAccountIdDecoder.GetAccountId(awsAccessKeyId)); + Assert.That(ex.ParamName, Is.EqualTo("awsAccessKeyId")); + } + + [Test] + public void GetAccountId_ShortAwsAccessKeyId_ThrowsArgumentOutOfRangeException() + { + // Arrange + string awsAccessKeyId = "AKIAIOSFODN"; + + // Act & Assert + var ex = Assert.Throws(() => AwsAccountIdDecoder.GetAccountId(awsAccessKeyId)); + Assert.That(ex.ParamName, Is.EqualTo("awsAccessKeyId")); + } + + [Test] + public void Base32Decode_ShortString_ThrowsArgumentException() + { + // Arrange + string shortString = "shortstr"; + + // Act & Assert + var ex = Assert.Throws(() => AwsAccountIdDecoder.Base32Decode(shortString)); + Assert.That(ex.ParamName, Is.EqualTo("src")); + } + + [Test] + public void Base32Decode_InvalidCharacters_ThrowsArgumentOutOfRangeException() + { + // Arrange + string invalidBase32String = "someBogusbase32string"; + + // Act & Assert + var ex = Assert.Throws(() => AwsAccountIdDecoder.Base32Decode(invalidBase32String)); + Assert.That(ex.ParamName, Is.EqualTo("src")); + } + + [Test] + public void Base32Decode_NullOrEmptyString_ThrowsArgumentNullException() + { + // Arrange + string nullString = null; + string emptyString = string.Empty; + + // Act & Assert + var exNull = Assert.Throws(() => AwsAccountIdDecoder.Base32Decode(nullString)); + Assert.That(exNull.ParamName, Is.EqualTo("src")); + + var exEmpty = Assert.Throws(() => AwsAccountIdDecoder.Base32Decode(emptyString)); + Assert.That(exEmpty.ParamName, Is.EqualTo("src")); + } + + [Test] + public void Base32Decode_ValidBase32String_ReturnsDecodedLong() + { + // Arrange + string validBase32String = "iosfodnn7example"; // Example valid Base32 string (10 characters) + long expectedDecodedValue = 74373114211833L; + + // Act + long decodedValue = AwsAccountIdDecoder.Base32Decode(validBase32String); + + // Assert + Assert.That(decodedValue, Is.EqualTo(expectedDecodedValue)); // Adjust expected value based on actual decoding logic + } + + } +}