diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..113da76ae0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,5 @@ +When generating new source code files, always include the copyright header. + +When writing unit tests, always use modern NUnit assertions. + +We use JustMock.Lite, so use only mock configurations that are valid for that package. \ No newline at end of file diff --git a/.github/workflows/all_solutions.yml b/.github/workflows/all_solutions.yml index a6cd12ab06..cd2ed221c5 100644 --- a/.github/workflows/all_solutions.yml +++ b/.github/workflows/all_solutions.yml @@ -254,6 +254,7 @@ jobs: AwsLambda.Sns, AwsLambda.Sqs, AwsLambda.WebRequest, + AwsSdk, AzureFunction, BasicInstrumentation, CatInbound, diff --git a/.github/workflows/build_buildtools.yml b/.github/workflows/build_buildtools.yml index 80b46e464f..2d50087e13 100644 --- a/.github/workflows/build_buildtools.yml +++ b/.github/workflows/build_buildtools.yml @@ -18,7 +18,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Add msbuild to PATH (required for MsiInstaller build) uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 diff --git a/.github/workflows/build_profiler.yml b/.github/workflows/build_profiler.yml index b6a3748b32..622bc84e75 100644 --- a/.github/workflows/build_profiler.yml +++ b/.github/workflows/build_profiler.yml @@ -7,6 +7,13 @@ on: - "feature/**" paths-ignore: - ".github/**" # skip changes to the .github folder (workflows, etc.) + + # needs to run on every push to main to keep CodeCov in sync + push: + branches: + - main + paths-ignore: + - ".github/**" # skip changes to the .github folder (workflows, etc.) # this workflow can be called from another workflow workflow_call: diff --git a/.github/workflows/post_deploy_agent.yml b/.github/workflows/post_deploy_agent.yml index 820664d00c..0036fe1110 100644 --- a/.github/workflows/post_deploy_agent.yml +++ b/.github/workflows/post_deploy_agent.yml @@ -189,3 +189,26 @@ jobs: env: BUILD_PATH: ${{ github.workspace }}/build/NugetVersionDeprecator/NugetVersionDeprecator.csproj PUBLISH_PATH: ${{ github.workspace }}/publish + + release-tags: + name: Create release tags + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + disable-sudo: true + egress-policy: audit + - name: Create release tags for Lambda and K8s Init Containers + run: | + RELEASE_TITLE="New Relic .NET Agent ${AGENT_VERSION}.0" + RELEASE_TAG="${AGENT_VERSION}.0_dotnet" + RELEASE_NOTES="Automated release for [.NET Agent ${AGENT_VERSION}](https://github.com/newrelic/newrelic-dotnet-agent/releases/tag/${AGENT_VERSION})" + gh auth login --with-token <<< $GH_RELEASE_TOKEN + echo "newrelic/newrelic-lambda-layers - Releasing ${RELEASE_TITLE} with tag ${RELEASE_TAG}" + gh release create "${RELEASE_TAG}" --title=${RELEASE_TITLE} --repo=newrelic/newrelic-lambda-layers --notes=${RELEASE_NOTES} + echo "newrelic/newrelic-agent-init-container - Releasing ${RELEASE_TITLE} with tag ${RELEASE_TAG}" + gh release create "${RELEASE_TAG}" --title=${RELEASE_TITLE} --repo=newrelic/newrelic-agent-init-container --notes=${RELEASE_NOTES} + env: + GH_RELEASE_TOKEN: ${{ secrets.DOTNET_AGENT_GH_TOKEN }} + AGENT_VERSION: "v${{ inputs.agent_version }}" diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 46d2b9a9a4..019d79a8e3 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -5,6 +5,11 @@ on: branches: - main - "feature/**" + + # needs to run on every push to main to keep CodeCov in sync + push: + branches: + - main workflow_dispatch: # allows for manual trigger diff --git a/.github/workflows/scripts/nugetSlackNotifications/CsprojHandler.cs b/.github/workflows/scripts/nugetSlackNotifications/CsprojHandler.cs index b4aa59eb55..ec4d67ad02 100644 --- a/.github/workflows/scripts/nugetSlackNotifications/CsprojHandler.cs +++ b/.github/workflows/scripts/nugetSlackNotifications/CsprojHandler.cs @@ -36,9 +36,9 @@ public static async Task> UpdatePackageReferences(string csprojPath foreach (var package in matchingPackages) { - if (package.VersionAsVersion < versionData.NewVersionAsVersion && package.Pin) + if (package.VersionAsVersion < versionData.NewVersionAsVersion && !string.IsNullOrEmpty(versionData.IgnoreTfMs) && versionData.IgnoreTfMs.Split(",").Contains(package.TargetFramework)) { - Log.Warning($"Not updating {package.Include} for {package.TargetFramework}, it is pinned to {package.Version}. Manual verification recommended."); + Log.Warning($"Not updating {package.Include} for {package.TargetFramework}, this TFM is ignored. Manual verification recommended."); continue; } diff --git a/.github/workflows/scripts/nugetSlackNotifications/NugetVersionData.cs b/.github/workflows/scripts/nugetSlackNotifications/NugetVersionData.cs index 1ac0928723..30b624fb88 100644 --- a/.github/workflows/scripts/nugetSlackNotifications/NugetVersionData.cs +++ b/.github/workflows/scripts/nugetSlackNotifications/NugetVersionData.cs @@ -10,8 +10,10 @@ public class NugetVersionData public Version NewVersionAsVersion { get; set; } public string Url { get; set; } public DateTime PublishDate { get; set; } + public string IgnoreTfMs { get; } - public NugetVersionData(string packageName, string oldVersion, string newVersion, string url, DateTime publishDate) + public NugetVersionData(string packageName, string oldVersion, string newVersion, string url, + DateTime publishDate, string ignoreTfMs) { PackageName = packageName; OldVersion = oldVersion; @@ -19,6 +21,7 @@ public NugetVersionData(string packageName, string oldVersion, string newVersion NewVersionAsVersion = new Version(newVersion); Url = url; PublishDate = publishDate; + IgnoreTfMs = ignoreTfMs; } } } diff --git a/.github/workflows/scripts/nugetSlackNotifications/PackageInfo.cs b/.github/workflows/scripts/nugetSlackNotifications/PackageInfo.cs index dc0c93556d..572a513f1f 100644 --- a/.github/workflows/scripts/nugetSlackNotifications/PackageInfo.cs +++ b/.github/workflows/scripts/nugetSlackNotifications/PackageInfo.cs @@ -14,5 +14,7 @@ public class PackageInfo public bool IgnoreMajor { get; set; } [JsonPropertyName("ignoreReason")] public string IgnoreReason {get; set;} + [JsonPropertyName("ignoreTFMs")] + public string IgnoreTFMs { get; set; } } } diff --git a/.github/workflows/scripts/nugetSlackNotifications/PackageReference.cs b/.github/workflows/scripts/nugetSlackNotifications/PackageReference.cs index 06235055f8..bdec525937 100644 --- a/.github/workflows/scripts/nugetSlackNotifications/PackageReference.cs +++ b/.github/workflows/scripts/nugetSlackNotifications/PackageReference.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.RegularExpressions; using System.Xml.Serialization; @@ -33,8 +33,5 @@ public string TargetFramework return match.Success ? match.Value : null; } } - - [XmlAttribute] - public bool Pin { get; set; } } } diff --git a/.github/workflows/scripts/nugetSlackNotifications/Program.cs b/.github/workflows/scripts/nugetSlackNotifications/Program.cs index dfacde9096..717b486616 100644 --- a/.github/workflows/scripts/nugetSlackNotifications/Program.cs +++ b/.github/workflows/scripts/nugetSlackNotifications/Program.cs @@ -168,7 +168,7 @@ static async Task CheckPackage(PackageInfo package, PackageMetadataResource meta var previousVersionDescription = previous?.Identity.Version.ToNormalizedString() ?? "Unknown"; var latestVersionDescription = latest.Identity.Version.ToNormalizedString(); Log.Information($"Package {packageName} was updated from {previousVersionDescription} to {latestVersionDescription}."); - _newVersions.Add(new NugetVersionData(packageName, previousVersionDescription, latestVersionDescription, latest.PackageDetailsUrl.ToString(), latest.Published.Value.Date)); + _newVersions.Add(new NugetVersionData(packageName, previousVersionDescription, latestVersionDescription, latest.PackageDetailsUrl.ToString(), latest.Published.Value.Date, package.IgnoreTFMs)); } else { @@ -238,11 +238,15 @@ static async Task CreateGithubPullRequestForNewVersions(IEnumerable CreateGithubPullRequestForNewVersions(IEnumerable GetLambdaAttribute(string name); AttributeDefinition GetFaasAttribute(string name); + AttributeDefinition GetCloudSdkAttribute(string name); AttributeDefinition GetRequestParameterAttribute(string paramName); @@ -190,6 +191,7 @@ public AttributeDefinitions(IAttributeFilter attribFilter) private readonly ConcurrentDictionary> _requestHeadersAttributes = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> _lambdaAttributes = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> _faasAttributes = new(); + private readonly ConcurrentDictionary> _cloudSdkAttributes = new(); private readonly ConcurrentDictionary> _typeAttributes = new ConcurrentDictionary>(); @@ -281,6 +283,20 @@ public AttributeDefinition GetFaasAttribute(string name) } + private AttributeDefinition CreateCloudSdkAttribute(string attribName) + { + return AttributeDefinitionBuilder + .Create(attribName, AttributeClassification.AgentAttributes) + .AppliesTo(AttributeDestinations.TransactionTrace) + .AppliesTo(AttributeDestinations.SpanEvent) + .WithConvert(x => x) + .Build(_attribFilter); + } + + public AttributeDefinition GetCloudSdkAttribute(string name) + { + return _cloudSdkAttributes.GetOrAdd(name, CreateCloudSdkAttribute); + } public AttributeDefinition GetCustomAttributeForTransaction(string name) { return _trxCustomAttributes.GetOrAdd(name, CreateCustomAttributeForTransaction); diff --git a/src/Agent/NewRelic/Agent/Core/Logging/LoggerBootstrapper.cs b/src/Agent/NewRelic/Agent/Core/Logging/LoggerBootstrapper.cs index 72eb290eb2..a4c6974c47 100644 --- a/src/Agent/NewRelic/Agent/Core/Logging/LoggerBootstrapper.cs +++ b/src/Agent/NewRelic/Agent/Core/Logging/LoggerBootstrapper.cs @@ -20,10 +20,6 @@ namespace NewRelic.Agent.Core public static class LoggerBootstrapper { - // Watch out! If you change the time format that the agent puts into its log files, other log parsers may fail. - //private static ILayout AuditLogLayout = new PatternLayout("%utcdate{yyyy-MM-dd HH:mm:ss,fff} NewRelic %level: %message\r\n"); - //private static ILayout FileLogLayout = new PatternLayout("%utcdate{yyyy-MM-dd HH:mm:ss,fff} NewRelic %6level: [pid: %property{pid}, tid: %property{threadid}] %message\r\n"); - private const string AuditLogLayout = "{UTCTimestamp} NewRelic Audit: {Message:l}\n"; private const string FileLogLayout = "{UTCTimestamp} NewRelic {NRLogLevel,6}: [pid: {pid}, tid: {tid}] {Message:l}\n{Exception:l}"; @@ -34,8 +30,16 @@ public static class LoggerBootstrapper public static void SetLoggingLevel(string newLogLevel) => _loggingLevelSwitch.MinimumLevel = newLogLevel.MapToSerilogLogLevel(); + private static bool _isWindows; + public static void Initialize() { +#if NETSTANDARD2_0 + _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#else + _isWindows = true; +#endif + var startupLoggerConfig = new LoggerConfiguration() .Enrich.With(new ThreadIdEnricher(), new ProcessIdEnricher(), new NrLogLevelEnricher(), new UTCTimestampEnricher()) .MinimumLevel.Information() @@ -52,22 +56,26 @@ public static void Initialize() /// This should only be called once, as soon as you have a valid config. public static void ConfigureLogger(ILogConfig config) { + // if logging is disabled, we don't log anywhere + if (!config.Enabled) + { + SetLoggingLevel("off"); // to short-circuit logging calls + Log.Logger = Serilog.Core.Logger.None; // a logger that does nothing + return; + } + SetLoggingLevel(config.LogLevel); - AuditLog.IsAuditLogEnabled = config.IsAuditLogEnabled; + AuditLog.IsAuditLogEnabled = config.IsAuditLogEnabled && !config.Console; var loggerConfig = new LoggerConfiguration() .MinimumLevel.ControlledBy(_loggingLevelSwitch) .Enrich.With(new ThreadIdEnricher(), new ProcessIdEnricher(), new NrLogLevelEnricher(), new UTCTimestampEnricher()) + .ConfigureConsoleSink(config) .ConfigureAuditLogSink(config) .ConfigureFileSink(config) .ConfigureDebugSink(); - if (config.Console) - { - loggerConfig = loggerConfig.ConfigureConsoleSink(); - } - // configure the global singleton logger instance (which remains specific to the Agent by way of ILRepack) var configuredLogger = loggerConfig.CreateLogger(); @@ -90,6 +98,9 @@ private static void EchoInMemoryLogsToConfiguredLogger(ILogger configuredLogger) _inMemorySink.Dispose(); } + /// + /// Configures the in-memory log sink used during bootstrapping. + /// private static LoggerConfiguration ConfigureInMemoryLogSink(this LoggerConfiguration loggerConfiguration) { // formatter not needed since this will be pushed to other sinks for output. @@ -116,12 +127,7 @@ private static LoggerConfiguration ConfigureInMemoryLogSink(this LoggerConfigura /// private static LoggerConfiguration ConfigureEventLogSink(this LoggerConfiguration loggerConfiguration) { -#if NETSTANDARD2_0 - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); -#else - var isWindows = true; -#endif - if (isWindows) + if (_isWindows) { const string eventLogName = "Application"; const string eventLogSourceName = "New Relic .NET Agent"; @@ -169,8 +175,10 @@ private static LoggerConfiguration ConfigureDebugSink(this LoggerConfiguration l /// /// Configure the console sink /// - private static LoggerConfiguration ConfigureConsoleSink(this LoggerConfiguration loggerConfiguration) + private static LoggerConfiguration ConfigureConsoleSink(this LoggerConfiguration loggerConfiguration, ILogConfig config) { + if (!config.Console) return loggerConfiguration; + return loggerConfiguration .WriteTo.Async(a => a.Logger(configuration => @@ -185,14 +193,11 @@ private static LoggerConfiguration ConfigureConsoleSink(this LoggerConfiguration /// /// Configure the file log sink /// - /// - /// The configuration for the appender. private static LoggerConfiguration ConfigureFileSink(this LoggerConfiguration loggerConfiguration, ILogConfig config) { - if (!config.Enabled) - { - return loggerConfiguration; - } + // console logging disables all file logging output. + if (config.Console) return loggerConfiguration; + string logFileName = config.GetFullLogFileName(); try @@ -201,32 +206,36 @@ private static LoggerConfiguration ConfigureFileSink(this LoggerConfiguration lo .WriteTo .Async(a => a.Logger(configuration => - { - configuration - .ExcludeAuditLog() - .ConfigureRollingLogSink(logFileName, FileLogLayout, config); - }) - ); + { + configuration + .ExcludeAuditLog() + .ConfigureRollingLogSink(logFileName, FileLogLayout, config); + }) + ); } catch (Exception ex) { Log.Logger.Warning(ex, "Unexpected exception when configuring file sink."); - // Fallback to the event log sink if we cannot setup a file logger. - Extensions.Logging.Log.FileLoggingHasFailed = true; - Log.Logger.Warning("Falling back to EventLog sink."); - loggerConfiguration.ConfigureEventLogSink(); + if (_isWindows) + { + // Fallback to the event log sink if we cannot setup a file logger. + Extensions.Logging.Log.FileLoggingHasFailed = true; + Log.Logger.Warning("Falling back to EventLog sink."); + loggerConfiguration.ConfigureEventLogSink(); + } } return loggerConfiguration; } /// - /// Setup the audit log file appender and attach it to a logger. + /// Configure the audit log sink /// private static LoggerConfiguration ConfigureAuditLogSink(this LoggerConfiguration loggerConfiguration, ILogConfig config) { - if (!config.IsAuditLogEnabled || !config.Enabled) return loggerConfiguration; + // console logging disables all file logging output, including audit logs + if (!config.IsAuditLogEnabled || config.Console) return loggerConfiguration; string logFileName = config.GetFullLogFileName().Replace(".log", "_audit.log"); @@ -242,13 +251,8 @@ private static LoggerConfiguration ConfigureAuditLogSink(this LoggerConfiguratio } /// - /// Sets up a rolling file appender using defaults shared for all our rolling file appenders. + /// Configure the rolling log sink /// - /// - /// The name of the file this appender will write to. - /// - /// - /// This does not call appender.ActivateOptions or add the appender to the logger. private static LoggerConfiguration ConfigureRollingLogSink(this LoggerConfiguration loggerConfiguration, string fileName, string outputFormat, ILogConfig config) { // check that the log file is accessible diff --git a/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs b/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs index 2f399603a7..adb0c82471 100644 --- a/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs +++ b/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs @@ -838,6 +838,7 @@ public static string GetSupportabilityInstallType(string installType) public const string SupportabilityIgnoredInstrumentation = SupportabilityDotnetPs + "IgnoredInstrumentation"; public const string SupportabilityGCSamplerV2Enabled = SupportabilityDotnetPs + "GCSamplerV2/Enabled"; + public const string SupportabilityAwsAccountIdProvided = SupportabilityDotnetPs + "AwsAccountId/Config"; #endregion Supportability diff --git a/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs b/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs index f38e5ddfb6..3b9ad1da87 100644 --- a/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs +++ b/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs @@ -61,6 +61,10 @@ public ISpan AddCustomAttribute(string key, object value) { return this; } + public ISpan AddCloudSdkAttribute(string key, object value) + { + return this; + } public ISpan SetName(string name) { diff --git a/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs b/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs index 7323b80277..968e7d654b 100644 --- a/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs +++ b/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs @@ -38,7 +38,7 @@ public class Segment : IInternalSpan, ISegmentDataState public IAttributeDefinitions AttribDefs => _transactionSegmentState.AttribDefs; public string TypeName => MethodCallData.TypeName; - private SpanAttributeValueCollection _customAttribValues; + private SpanAttributeValueCollection _attribValues; public Segment(ITransactionSegmentState transactionSegmentState, MethodCallData methodCallData) { @@ -318,7 +318,7 @@ public TimeSpan ExclusiveDurationOrZero public SpanAttributeValueCollection GetAttributeValues() { - var attribValues = _customAttribValues ?? new SpanAttributeValueCollection(); + var attribValues = _attribValues ?? new SpanAttributeValueCollection(); AttribDefs.Duration.TrySetValue(attribValues, DurationOrZero); AttribDefs.NameForSpan.TrySetValue(attribValues, GetTransactionTraceName()); @@ -434,14 +434,14 @@ public ISegmentExperimental MakeLeaf() return this; } - private readonly object _customAttribValuesSyncRoot = new object(); + private readonly object _attribValuesSyncRoot = new object(); public ISpan AddCustomAttribute(string key, object value) { SpanAttributeValueCollection customAttribValues; - lock (_customAttribValuesSyncRoot) + lock (_attribValuesSyncRoot) { - customAttribValues = _customAttribValues ?? (_customAttribValues = new SpanAttributeValueCollection()); + customAttribValues = _attribValues ?? (_attribValues = new SpanAttributeValueCollection()); } AttribDefs.GetCustomAttributeForSpan(key).TrySetValue(customAttribValues, value); @@ -449,6 +449,19 @@ public ISpan AddCustomAttribute(string key, object value) return this; } + public ISpan AddCloudSdkAttribute(string key, object value) + { + SpanAttributeValueCollection attribValues; + lock (_attribValuesSyncRoot) + { + attribValues = _attribValues ?? (_attribValues = new SpanAttributeValueCollection()); + } + + AttribDefs.GetCloudSdkAttribute(key).TrySetValue(attribValues, value); + + return this; + } + public ISpan SetName(string name) { SegmentNameOverride = name; diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs index 404cbac7f4..91c3723e52 100644 --- a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs +++ b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs @@ -331,5 +331,10 @@ public void AddFaasAttribute(string name, object value) { return; } + + public void AddCloudSdkAttribute(string name, object value) + { + return; + } } } diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs index 9cbe293901..46f82f827c 100644 --- a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs +++ b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs @@ -1374,7 +1374,7 @@ public void AddLambdaAttribute(string name, object value) { if (string.IsNullOrWhiteSpace(name)) { - Log.Debug($"AddLambdaAttribute - Unable to set Lambda value on transaction because the key is null/empty"); + Log.Debug($"AddLambdaAttribute - Name cannot be null/empty"); return; } @@ -1386,7 +1386,7 @@ public void AddFaasAttribute(string name, object value) { if (string.IsNullOrWhiteSpace(name)) { - Log.Debug($"AddFaasAttribute - Unable to set FaaS value on transaction because the key is null/empty"); + Log.Debug($"AddFaasAttribute - Name cannot be null/empty"); return; } diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ISpan.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ISpan.cs index 86bc4d6451..6bca5d9ab9 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ISpan.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ISpan.cs @@ -11,6 +11,8 @@ public interface ISpan { ISpan AddCustomAttribute(string key, object value); + ISpan AddCloudSdkAttribute(string key, object value); + ISpan SetName(string name); } } 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 new file mode 100644 index 0000000000..d40a53aaaa --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/ArnBuilder.cs @@ -0,0 +1,153 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Text.RegularExpressions; + +namespace NewRelic.Agent.Extensions.AwsSdk +{ + public class ArnBuilder + { + public readonly string Partition; + public readonly string Region; + public readonly string AccountId; + + public ArnBuilder(string partition, string region, string accountId) + { + Partition = string.IsNullOrEmpty(partition) ? "aws" : partition; + Region = string.IsNullOrEmpty(region) ? "(unknown)" : region; + AccountId = accountId ?? ""; + } + + public string Build(string service, string resource) => ConstructArn(Partition, service, Region, AccountId, resource); + + // This is the full regex pattern for a Lambda ARN: + // (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_\.]+)(:(\$LATEST|[a-zA-Z0-9-_]+))? + + // If it's a full ARN, it has to start with 'arn:' + // A partial ARN can contain up to 5 segments separated by ':' + // 1. Region + // 2. Account ID + // 3. 'function' (fixed string) + // 4. Function name + // 5. Alias or version + // Only the function name is required, the rest are all optional. e.g. you could have region and function name and nothing else + public string BuildFromPartialLambdaArn(string invocationName) + { + if (invocationName.StartsWith("arn:")) + { + return invocationName; + } + var segments = invocationName.Split(':'); + string functionName = null; + string alias = null; + string fallback = null; + string region = null; + string accountId = null; + + // If there's only one segment, assume it's the function name + if (segments.Length == 1) + { + functionName = segments[0]; + } + else + { + // All we should need is the function name, but if we find a region or account ID, we'll use it + // since it should be more accurate + foreach (var segment in segments) + { + // A string that looks like a region or account ID could also be the function name + // Assume it's the former, unless we never find a function name + if (LooksLikeARegion(segment)) + { + if (string.IsNullOrEmpty(region)) + { + region = segment; + } + else + { + fallback = segment; + } + continue; + } + else if (LooksLikeAnAccountId(segment)) + { + if (string.IsNullOrEmpty(accountId)) + { + accountId = segment; + } + else + { + fallback = segment; + } + continue; + } + else if (segment == "function") + { + continue; + } + else if (functionName == null) + { + functionName = segment; + } + else if (alias == null) + { + alias = segment; + } + else + { + return null; + } + } + } + + if (string.IsNullOrEmpty(functionName)) + { + if (!string.IsNullOrEmpty(fallback)) + { + functionName = fallback; + } + else + { + return null; + } + } + + accountId = !string.IsNullOrEmpty(accountId) ? accountId : AccountId; + if (string.IsNullOrEmpty(accountId)) + { + return null; + } + + // The member Region cannot be blank (it has a default) so we don't need to check it here + region = !string.IsNullOrEmpty(region) ? region : Region; + + if (!string.IsNullOrEmpty(alias)) + { + functionName += $":{alias}"; + } + return ConstructArn(Partition, "lambda", region, accountId, $"function:{functionName}"); + } + + public override string ToString() + { + string idPresent = string.IsNullOrEmpty(AccountId) ? "[Missing]" : "[Present]"; + + return $"Partition: {Partition}, Region: {Region}, AccountId: {idPresent}"; + } + + private static Regex RegionRegex = new Regex(@"^[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}$", RegexOptions.Compiled); + private static bool LooksLikeARegion(string text) => RegionRegex.IsMatch(text); + private static bool LooksLikeAnAccountId(string text) => (text.Length == 12) && text.All(c => c >= '0' && c <= '9'); + + private string ConstructArn(string partition, string service, string region, string accountId, string resource) + { + if (string.IsNullOrEmpty(partition) || string.IsNullOrEmpty(region) || string.IsNullOrEmpty(accountId) + || string.IsNullOrEmpty(service) || string.IsNullOrEmpty(resource)) + { + return null; + } + 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 new file mode 100644 index 0000000000..a6e3dea2fb --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/AwsAccountIdDecoder.cs @@ -0,0 +1,70 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace NewRelic.Agent.Extensions.AwsSdk +{ + public static class AwsAccountIdDecoder + { + // magic number + private const long Mask = 0x7FFFFFFFFF80; + + public static string GetAccountId(string awsAccessKeyId) + { + if (string.IsNullOrEmpty(awsAccessKeyId)) + { + throw new ArgumentNullException(nameof(awsAccessKeyId), "AWS Access Key ID cannot be null or empty."); + } + + if (awsAccessKeyId.Length < 14) + { + throw new ArgumentOutOfRangeException(nameof(awsAccessKeyId), "AWS Access Key ID must be at least 14 characters long."); + } + + string accessKeyWithoutPrefix = awsAccessKeyId.Substring(4).ToLowerInvariant(); + long encodedAccount = Base32Decode(accessKeyWithoutPrefix); + + return ((encodedAccount & Mask) >> 7).ToString(); + } + + /// + /// 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 + 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)); + } + + long baseValue = 0; + for (int i = 0; i < 10; i++) + { + baseValue <<= 5; + char c = src[i]; + baseValue += c switch + { + >= 'a' and <= 'z' => c - 'a', + >= '2' and <= '7' => c - '2' + 26, + _ => throw new ArgumentOutOfRangeException(nameof(src), + "The input string must contain only characters in the range a-z and 2-7.") + }; + } + + return baseValue >> 2; + } + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUCache.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUCache.cs new file mode 100644 index 0000000000..9dc81d1446 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUCache.cs @@ -0,0 +1,121 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace NewRelic.Agent.Extensions.Caching +{ + /// + /// A thread-safe LRU cache implementation. + /// + /// + /// + public class LRUCache + { + private readonly int _capacity; + private readonly Dictionary> _cacheMap; + private readonly LinkedList _lruList; + private readonly ReaderWriterLockSlim _lock = new(); + + public LRUCache(int capacity) + { + if (capacity <= 0) + { + throw new ArgumentException("Capacity must be greater than zero.", nameof(capacity)); + } + + _capacity = capacity; + _cacheMap = new Dictionary>(capacity); + _lruList = new LinkedList(); + } + + public TValue Get(TKey key) + { + _lock.EnterUpgradeableReadLock(); + try + { + if (_cacheMap.TryGetValue(key, out var node)) + { + // Move the accessed node to the front of the list + _lock.EnterWriteLock(); + try + { + _lruList.Remove(node); + _lruList.AddFirst(node); + } + finally + { + _lock.ExitWriteLock(); + } + return node.Value.Value; + } + throw new KeyNotFoundException("The given key was not present in the cache."); + } + finally + { + _lock.ExitUpgradeableReadLock(); + } + } + + public void Put(TKey key, TValue value) + { + _lock.EnterWriteLock(); + try + { + if (_cacheMap.TryGetValue(key, out var node)) + { + // Update the value and move the node to the front of the list + node.Value.Value = value; + _lruList.Remove(node); + _lruList.AddFirst(node); + } + else + { + if (_cacheMap.Count >= _capacity) + { + // Remove the least recently used item + var lruNode = _lruList.Last; + _cacheMap.Remove(lruNode.Value.Key); + _lruList.RemoveLast(); + } + + // Add the new item to the cache + var cacheItem = new CacheItem(key, value); + var newNode = new LinkedListNode(cacheItem); + _lruList.AddFirst(newNode); + _cacheMap[key] = newNode; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + public bool ContainsKey(TKey key) + { + _lock.EnterReadLock(); + try + { + return _cacheMap.ContainsKey(key); + } + finally + { + _lock.ExitReadLock(); + } + } + + private class CacheItem + { + public TKey Key { get; } + public TValue Value { get; set; } + + public CacheItem(TKey key, TValue value) + { + Key = key; + Value = value; + } + } + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUHashSet.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUHashSet.cs new file mode 100644 index 0000000000..33ee77a056 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/LRUHashSet.cs @@ -0,0 +1,114 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace NewRelic.Agent.Extensions.Caching +{ + /// + /// A thread-safe LRU HashSet implementation. + /// + /// + public class LRUHashSet + { + private readonly int _capacity; + private readonly HashSet _hashSet; + private readonly LinkedList _lruList; + private readonly ReaderWriterLockSlim _lock = new(); + + public LRUHashSet(int capacity) + { + if (capacity <= 0) + { + throw new ArgumentException("Capacity must be greater than zero.", nameof(capacity)); + } + + _capacity = capacity; + _hashSet = new HashSet(); + _lruList = new LinkedList(); + } + + public bool Add(T item) + { + _lock.EnterWriteLock(); + try + { + if (_hashSet.Contains(item)) + { + // Move the accessed item to the front of the list + _lruList.Remove(item); + _lruList.AddFirst(item); + return false; + } + else + { + if (_hashSet.Count >= _capacity) + { + // Remove the least recently used item + var lruItem = _lruList.Last.Value; + _hashSet.Remove(lruItem); + _lruList.RemoveLast(); + } + + // Add the new item to the set and list + _hashSet.Add(item); + _lruList.AddFirst(item); + return true; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool Contains(T item) + { + _lock.EnterReadLock(); + try + { + return _hashSet.Contains(item); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool Remove(T item) + { + _lock.EnterWriteLock(); + try + { + if (_hashSet.Remove(item)) + { + _lruList.Remove(item); + return true; + } + return false; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public int Count + { + get + { + _lock.EnterReadLock(); + try + { + return _hashSet.Count; + } + finally + { + _lock.ExitReadLock(); + } + } + } + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/WeakReferenceKey.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/WeakReferenceKey.cs new file mode 100644 index 0000000000..478498fe5e --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/WeakReferenceKey.cs @@ -0,0 +1,50 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace NewRelic.Agent.Extensions.Caching +{ + /// + /// Creates an object that can be used as a dictionary key, which holds a WeakReference<T> + /// + /// + public class WeakReferenceKey where T : class + { + private WeakReference WeakReference { get; } + + public WeakReferenceKey(T cacheKey) + { + WeakReference = new WeakReference(cacheKey); + } + + public override bool Equals(object obj) + { + if (obj is WeakReferenceKey otherKey) + { + if (WeakReference.TryGetTarget(out var thisTarget) && + otherKey.WeakReference.TryGetTarget(out var otherTarget)) + { + return ReferenceEquals(thisTarget, otherTarget); + } + } + + return false; + } + + public override int GetHashCode() + { + if (WeakReference.TryGetTarget(out var target)) + { + return target.GetHashCode(); + } + + return 0; + } + + /// + /// Gets the value from the WeakReference or null if the target has been garbage collected. + /// + public T Value => WeakReference.TryGetTarget(out var target) ? target : null; + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Collections/ConcurrentHashSet.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Collections/ConcurrentHashSet.cs index 28e8133cf2..145d4391be 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Collections/ConcurrentHashSet.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Collections/ConcurrentHashSet.cs @@ -47,7 +47,13 @@ public void Add(T item) _hashSet.Add(item); } } - + public bool TryAdd(T item) + { + using (_writeLock()) + { + return _hashSet.Add(item); + } + } public void Clear() { using (_writeLock()) diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs index 1fdc696c79..ac2a32c79b 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs @@ -193,10 +193,9 @@ private void InitLambdaData(InstrumentedMethodCall instrumentedMethodCall, IAgen { agent.Logger.Log(Agent.Extensions.Logging.Level.Debug, $"Supported Event Type found: {_functionDetails.EventType}"); } - else if (!_unsupportedInputTypes.Contains(name)) + else if (_unsupportedInputTypes.TryAdd(name)) { agent.Logger.Log(Agent.Extensions.Logging.Level.Warn, $"Unsupported input object type: {name}. Unable to provide additional instrumentation."); - _unsupportedInputTypes.Add(name); } } @@ -344,10 +343,9 @@ private void CaptureResponseData(ITransaction transaction, object response, IAge || (_functionDetails.EventType == AwsLambdaEventType.ApplicationLoadBalancerRequest && responseType != "Amazon.Lambda.ApplicationLoadBalancerEvents.ApplicationLoadBalancerResponse")) { - if (!_unexpectedResponseTypes.Contains(responseType)) + if (_unexpectedResponseTypes.TryAdd(responseType)) { agent.Logger.Log(Agent.Extensions.Logging.Level.Warn, $"Unexpected response type {responseType} for request event type {_functionDetails.EventType}. Not capturing any response data."); - _unexpectedResponseTypes.Add(responseType); } return; diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs new file mode 100644 index 0000000000..d61c7d642f --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AmazonServiceClientWrapper.cs @@ -0,0 +1,65 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.AwsSdk; +using NewRelic.Agent.Extensions.Caching; +using NewRelic.Agent.Extensions.Providers.Wrapper; + +namespace NewRelic.Providers.Wrapper.AwsSdk; + +public class AmazonServiceClientWrapper : IWrapper +{ + private const int LRUCapacity = 100; + // cache the account id per instance of AmazonServiceClient.Config + public static LRUCache, string> AwsAccountIdByClientConfigCache = new(LRUCapacity); + + // cache instances of AmazonServiceClient + private static readonly LRUHashSet> AmazonServiceClientInstanceCache = new(LRUCapacity); + + public bool IsTransactionRequired => false; + + public CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo) + { + return new CanWrapResponse(instrumentedMethodInfo.RequestedWrapperName == nameof(AmazonServiceClientWrapper)); + } + + public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) + { + object client = instrumentedMethodCall.MethodCall.InvocationTarget; + + var weakReferenceKey = new WeakReferenceKey(client); + if (AmazonServiceClientInstanceCache.Contains(weakReferenceKey)) // don't do anything if we've already seen this client instance + return Delegates.NoOp; + + AmazonServiceClientInstanceCache.Add(weakReferenceKey); + + string awsAccountId; + try + { + // get the AWSCredentials parameter + dynamic awsCredentials = instrumentedMethodCall.MethodCall.MethodArguments[0]; + + dynamic immutableCredentials = awsCredentials.GetCredentials(); + string accessKey = immutableCredentials.AccessKey; + + // convert the access key to an account id + awsAccountId = AwsAccountIdDecoder.GetAccountId(accessKey); + } + catch (Exception e) + { + agent.Logger.Info($"Unable to parse AWS Account ID from AccessKey. Using AccountId from configuration instead. Exception: {e.Message}"); + awsAccountId = agent.Configuration.AwsAccountId; + } + + return Delegates.GetDelegateFor(onComplete: () => + { + // get the _config field from the client + object clientConfig = ((dynamic)client).Config; + + // cache the account id using clientConfig as the key + AwsAccountIdByClientConfigCache.Put(new WeakReferenceKey(clientConfig), awsAccountId); + }); + } +} 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 33c19c9514..03e49fa48f 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/AwsSdkPipelineWrapper.cs @@ -1,10 +1,14 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -using System.Collections.Generic; +using System; +using System.Linq; using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.AwsSdk; +using NewRelic.Agent.Extensions.Caching; using NewRelic.Agent.Extensions.Collections; using NewRelic.Agent.Extensions.Providers.Wrapper; +using NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers; namespace NewRelic.Providers.Wrapper.AwsSdk { @@ -14,12 +18,62 @@ public class AwsSdkPipelineWrapper : IWrapper private const string WrapperName = "AwsSdkPipelineWrapper"; private static ConcurrentHashSet _unsupportedRequestTypes = new(); + private static bool _reportBadAccountId = true; + private static bool _reportBadArnBuilder = false; public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) { return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); } + private ArnBuilder CreateArnBuilder(IAgent agent, dynamic requestContext) + { + string partition = null; + string systemName = null; + string accountId = null; + try + { + var clientConfig = requestContext.ClientConfig; + accountId = GetAccountId(agent, clientConfig); + if (clientConfig.RegionEndpoint != null) + { + var regionEndpoint = clientConfig.RegionEndpoint; + systemName = regionEndpoint.SystemName; + partition = regionEndpoint.PartitionName; + } + } + catch (Exception e) + { + if (_reportBadArnBuilder) + { + agent.Logger.Debug(e, $"AwsSdkPipelineWrapper: Unable to get required ARN components from requestContext."); + _reportBadArnBuilder = false; + } + } + + return new ArnBuilder(partition, systemName, accountId); + } + + private string GetAccountId(IAgent agent, object clientConfig) + { + var cacheKey = new WeakReferenceKey(clientConfig); + string accountId = AmazonServiceClientWrapper.AwsAccountIdByClientConfigCache.ContainsKey(cacheKey) ? AmazonServiceClientWrapper.AwsAccountIdByClientConfigCache.Get(cacheKey) : agent.Configuration.AwsAccountId; + + if (accountId != null) + { + if ((accountId.Length != 12) || accountId.Any(c => (c < '0') || (c > '9'))) + { + if (_reportBadAccountId) + { + agent.Logger.Warn("Supplied AWS Account ID appears to be invalid"); + _reportBadAccountId = false; + } + } + } + + return accountId; + } + public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) { // Get the IExecutionContext (the only parameter) @@ -49,18 +103,25 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins } dynamic request = requestContext.OriginalRequest; string requestType = request.GetType().FullName; + ArnBuilder builder = CreateArnBuilder(agent, requestContext); if (requestType.StartsWith("Amazon.SQS")) { return SQSRequestHandler.HandleSQSRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext); } - else if (requestType.StartsWith("Amazon.DynamoDBv2")) + + if (requestType == "Amazon.Lambda.Model.InvokeRequest") { - return DynamoDbRequestHandler.HandleDynamoDbRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext); + return LambdaInvokeRequestHandler.HandleInvokeRequest(instrumentedMethodCall, agent, transaction, request, isAsync, builder); + } + + if (requestType.StartsWith("Amazon.DynamoDBv2")) + { + 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/DynamoDbRequestHandler.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/DynamoDbRequestHandler.cs deleted file mode 100644 index 023ee2281e..0000000000 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/DynamoDbRequestHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020 New Relic, Inc. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; -using System.Threading.Tasks; -using NewRelic.Agent.Api; -using NewRelic.Agent.Extensions.Parsing; -using NewRelic.Agent.Extensions.Providers.Wrapper; - -namespace NewRelic.Providers.Wrapper.AwsSdk -{ - internal static class DynamoDbRequestHandler - { - - private static ConcurrentDictionary _operationNameCache = new ConcurrentDictionary(); - - public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, dynamic executionContext) - { - var requestType = request.GetType().Name as string; - - string model; - string operation; - - // PutItemRequest => put_item, - // CreateTableRequest => create_table, etc. - operation = _operationNameCache.GetOrAdd(requestType, GetOperationNameFromRequestType); - - // Even though there is no common interface they all implement, every Request type I checked - // has a TableName property - model = request.TableName; - - var segment = transaction.StartDatastoreSegment(instrumentedMethodCall.MethodCall, new ParsedSqlStatement(DatastoreVendor.DynamoDB, model, operation), isLeaf: true); - return isAsync ? - Delegates.GetAsyncDelegateFor(agent, segment) - : - Delegates.GetDelegateFor(segment); - } - - private static string GetOperationNameFromRequestType(string requestType) - { - return requestType.Replace("Request", string.Empty).ToSnakeCase(); - } - } -} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/Instrumentation.xml b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/Instrumentation.xml index c909ea4cd5..556e0d0d82 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/Instrumentation.xml @@ -13,5 +13,14 @@ SPDX-License-Identifier: Apache-2.0 + + + + + + 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 new file mode 100644 index 0000000000..bdf0371a96 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/DynamoDbRequestHandler.cs @@ -0,0 +1,71 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Threading.Tasks; +using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.AwsSdk; +using NewRelic.Agent.Extensions.Parsing; +using NewRelic.Agent.Extensions.Providers.Wrapper; + +namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers +{ + internal static class DynamoDbRequestHandler + { + private static readonly ConcurrentDictionary _operationNameCache = new(); + + public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, ArnBuilder builder) + { + var requestType = ((object)request).GetType().Name; + + var operation = _operationNameCache.GetOrAdd(requestType, requestType.Replace("Request", string.Empty).ToSnakeCase()); + + // all request objects implement a TableName property + string model = request.TableName; + + var segment = transaction.StartDatastoreSegment(instrumentedMethodCall.MethodCall, new ParsedSqlStatement(DatastoreVendor.DynamoDB, model, operation), isLeaf: true); + + var arn = builder.Build("dynamodb", $"table/{model}"); + if (!string.IsNullOrEmpty(arn)) + segment.AddCloudSdkAttribute("cloud.resource_id", arn); + segment.AddCloudSdkAttribute("aws.operation", operation); + segment.AddCloudSdkAttribute("aws.region", builder.Region); + + return isAsync ? + Delegates.GetAsyncDelegateFor(agent, segment, true, responseTask => + { + try + { + if (responseTask.IsFaulted) + transaction.NoticeError(responseTask.Exception); + else + SetRequestIdIfAvailable(agent, segment, ((dynamic)responseTask).Result); + } + finally + { + segment.End(); + } + + }, TaskContinuationOptions.ExecuteSynchronously) + : + Delegates.GetDelegateFor( + onFailure: segment.End, + onSuccess: response => + { + SetRequestIdIfAvailable(agent, segment, response); + segment.End(); + } + ); + + } + + private static void SetRequestIdIfAvailable(IAgent agent, ISegment segment, dynamic response) + { + if (response != null && response.ResponseMetadata != null && response.ResponseMetadata.RequestId != null) + { + string requestId = response.ResponseMetadata.RequestId; + segment.AddCloudSdkAttribute("aws.requestId", requestId); + } + } + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/LambdaInvokeRequestHandler.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/LambdaInvokeRequestHandler.cs new file mode 100644 index 0000000000..b0bf16a8a7 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/LambdaInvokeRequestHandler.cs @@ -0,0 +1,125 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System; +using System.Threading.Tasks; +using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.Providers.Wrapper; +using NewRelic.Reflection; +using NewRelic.Agent.Extensions.AwsSdk; + +namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers +{ + internal static class LambdaInvokeRequestHandler + { + private static Func _getResultFromGenericTask; + private static readonly ConcurrentDictionary _arnCache = new(); + private static bool _reportMissingRequestId = true; + private static bool _reportBadInvocationName = true; + private const int MAX_CACHE_SIZE = 25; // Shouldn't ever get this big, but just in case + + private static object GetTaskResult(object task) + { + if (((Task)task).IsFaulted) + { + return null; + } + + var getResponse = _getResultFromGenericTask ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(task.GetType(), "Result"); + return getResponse(task); + } + + private static void SetRequestIdIfAvailable(IAgent agent, ISegment segment, dynamic response) + { + try + { + dynamic metadata = response.ResponseMetadata; + string requestId = metadata.RequestId; + segment.AddCloudSdkAttribute("aws.requestId", requestId); + } + catch (Exception e) + { + if (_reportMissingRequestId) + { + agent.Logger.Debug(e, "Unable to get RequestId from response metadata."); + _reportMissingRequestId = false; + } + } + } + + public static AfterWrappedMethodDelegate HandleInvokeRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, ArnBuilder builder) + { + string functionName = request.FunctionName; + string qualifier = request.Qualifier; + if (!string.IsNullOrEmpty(qualifier) && !functionName.EndsWith(qualifier)) + { + functionName = $"{functionName}:{qualifier}"; + } + string arn; + if (functionName.StartsWith("arn:")) + { + arn = functionName; + } + else + { + if (!_arnCache.TryGetValue(functionName, out arn)) + { + arn = builder.BuildFromPartialLambdaArn(functionName); + if (_arnCache.Count < MAX_CACHE_SIZE) + { + _arnCache.TryAdd(functionName, arn); + } + } + } + var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, "InvokeRequest"); + + segment.AddCloudSdkAttribute("cloud.platform", "aws_lambda"); + segment.AddCloudSdkAttribute("aws.operation", "InvokeRequest"); + segment.AddCloudSdkAttribute("aws.region", builder.Region); + + + if (!string.IsNullOrEmpty(arn)) + { + segment.AddCloudSdkAttribute("cloud.resource_id", arn); + } + else if (_reportBadInvocationName) + { + agent.Logger.Debug($"Unable to resolve Lambda invocation named '{functionName}' [{builder.ToString()}]"); + _reportBadInvocationName = false; + } + + if (isAsync) + { + return Delegates.GetAsyncDelegateFor(agent, segment, true, InvokeTryProcessResponse, TaskContinuationOptions.ExecuteSynchronously); + + void InvokeTryProcessResponse(Task responseTask) + { + try + { + if (responseTask.Status == TaskStatus.Faulted) + { + transaction.NoticeError(responseTask.Exception); + } + SetRequestIdIfAvailable(agent, segment, GetTaskResult(responseTask)); + } + finally + { + segment?.End(); + } + } + } + else + { + return Delegates.GetDelegateFor( + onFailure: ex => segment.End(ex), + onSuccess: response => + { + SetRequestIdIfAvailable(agent, segment, response); + segment.End(); + } + ); + } + } + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/SQSRequestHandler.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/SQSRequestHandler.cs similarity index 95% rename from src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/SQSRequestHandler.cs rename to src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/SQSRequestHandler.cs index 45f4fecffb..587f4ba69f 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/SQSRequestHandler.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/RequestHandlers/SQSRequestHandler.cs @@ -11,7 +11,7 @@ using NewRelic.Agent.Extensions.Providers.Wrapper; using NewRelic.Reflection; -namespace NewRelic.Providers.Wrapper.AwsSdk +namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers { internal static class SQSRequestHandler { @@ -48,7 +48,7 @@ public static AfterWrappedMethodDelegate HandleSQSRequest(InstrumentedMethodCall var dtHeaders = agent.GetConfiguredDTHeaders(); string requestQueueUrl = request.QueueUrl; - ISegment segment = SqsHelper.GenerateSegment(transaction, instrumentedMethodCall.MethodCall, requestQueueUrl, action); + var segment = SqsHelper.GenerateSegment(transaction, instrumentedMethodCall.MethodCall, requestQueueUrl, action); switch (action) { case MessageBrokerAction.Produce when requestType == "SendMessageRequest": @@ -117,7 +117,7 @@ public static AfterWrappedMethodDelegate HandleSQSRequest(InstrumentedMethodCall void ProcessResponse(Task responseTask) { - if (!ValidTaskResponse(responseTask) || (segment == null) || action != MessageBrokerAction.Consume) + if (!ValidTaskResponse(responseTask) || segment == null || action != MessageBrokerAction.Consume) return; // taskResult is a ReceiveMessageResponse diff --git a/src/Agent/NewRelic/Home/Home.csproj b/src/Agent/NewRelic/Home/Home.csproj index 58165ba6a3..29cf52ca73 100644 --- a/src/Agent/NewRelic/Home/Home.csproj +++ b/src/Agent/NewRelic/Home/Home.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs index e07fe0aa84..a0ddde2000 100644 --- a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs +++ b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkDynamoDBExerciser.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using Amazon.Runtime; -using System.Threading; namespace AwsSdkTestApp.AwsSdkExercisers { @@ -24,12 +23,18 @@ public AwsSdkDynamoDBExerciser() private AmazonDynamoDBClient GetDynamoDBClient() { + AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig + { + // Set the endpoint URL + ServiceURL = "http://dynamodb:8000", // port must match what is set in docker compose + AuthenticationRegion = "us-east-2" + //RegionEndpoint = RegionEndpoint.USEast2 **DO NOT* specify RegionEndpoint for local tests + }; + + // use plausible (but fake) access key and fake secret key so account id parsing can be tested + var creds = new BasicAWSCredentials("FOOIHSFODNNAEXAMPLE", + "MOREGIBBERISH"); // account id will be "520056171328" - 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); return client; diff --git a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkSQSExerciser.cs b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkSQSExerciser.cs index ffef934382..922a1ab8b1 100644 --- a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkSQSExerciser.cs +++ b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExercisers/AwsSdkSQSExerciser.cs @@ -8,6 +8,7 @@ using Amazon.SQS.Model; using System.Linq; using System.Collections.Generic; +using Amazon.Runtime; namespace AwsSdkTestApp.AwsSdkExercisers { @@ -25,14 +26,15 @@ public AwsSdkSQSExerciser() private AmazonSQSClient GetSqsClient() { // configure the client to use LocalStack - var awsCredentials = new Amazon.Runtime.BasicAWSCredentials("dummy", "dummy"); + // use plausible (but fake) access key and fake secret key so account id parsing can be tested + var creds = new BasicAWSCredentials("FOOIHSHSDNNAEXAMPLE", "MOREGIBBERISH"); var config = new AmazonSQSConfig { ServiceURL = "http://localstack:4566", AuthenticationRegion = "us-west-2" }; - var sqsClient = new AmazonSQSClient(awsCredentials, config); + var sqsClient = new AmazonSQSClient(creds, config); return sqsClient; } diff --git a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkTestApp.csproj b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkTestApp.csproj index ab3a4e5a0a..f5c8d68196 100644 --- a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkTestApp.csproj +++ b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkTestApp.csproj @@ -14,8 +14,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/Controllers/AwsSdkMultiServiceController.cs b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/Controllers/AwsSdkMultiServiceController.cs new file mode 100644 index 0000000000..c1f63f858e --- /dev/null +++ b/tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/Controllers/AwsSdkMultiServiceController.cs @@ -0,0 +1,59 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using AwsSdkTestApp.AwsSdkExercisers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace AwsSdkTestApp.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AwsSdkMultiServiceController : ControllerBase + { + private readonly ILogger _logger; + + public AwsSdkMultiServiceController(ILogger logger) + { + _logger = logger; + _logger.LogInformation("Created AwsSdkMultiServiceController"); + } + + [HttpGet("CallMultipleServicesAsync")] + public async Task CallMultipleServicesAsync([FromQuery, Required]string queueName, [FromQuery, Required]string tableName, [FromQuery, Required]string bookName) + { + _logger.LogInformation("Starting CallMultipleServicesAsync"); + + using var sqsExerciser = new AwsSdkSQSExerciser(); + using var dynamoDbExerciser = new AwsSdkDynamoDBExerciser(); + + await sqsExerciser.SQS_InitializeAsync(queueName); + + // send an SQS message + await sqsExerciser.SQS_SendMessageAsync(bookName); + + await Task.Delay(TimeSpan.FromSeconds(2)); // may not really be necessary + + // receive an SQS message + var messages = await sqsExerciser.SQS_ReceiveMessageAsync(); + + var movieName = messages.First().Body; + + // create a DynamoDB table + await dynamoDbExerciser.CreateTableAsync(tableName); + // put an item in a DynamoDB table + await dynamoDbExerciser.PutItemAsync(tableName, movieName, "2021"); + + // delete the table + await dynamoDbExerciser.DeleteTableAsync(tableName); + + await sqsExerciser.SQS_TeardownAsync(); + + _logger.LogInformation("Finished CallMultipleServicesAsync"); + } + } +} diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerDynamoDBTestFixture.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerDynamoDBTestFixture.cs new file mode 100644 index 0000000000..313858f5d6 --- /dev/null +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerDynamoDBTestFixture.cs @@ -0,0 +1,56 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NewRelic.Agent.ContainerIntegrationTests.Applications; + +namespace NewRelic.Agent.ContainerIntegrationTests.Fixtures; + +public class AwsSdkContainerDynamoDBTestFixture : AwsSdkContainerTestFixtureBase +{ + private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; + private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; + private const string DistroTag = "jammy"; + + private readonly string BaseUrl; + + public AwsSdkContainerDynamoDBTestFixture() : base(DistroTag, Architecture, Dockerfile) + { + BaseUrl = $"http://localhost:{Port}/awssdkdynamodb"; + } + + public void CreateTableAsync(string tableName) + { + GetAndAssertStatusCode($"{BaseUrl}/CreateTableAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); + } + public void DeleteTableAsync(string tableName) + { + GetAndAssertStatusCode($"{BaseUrl}/DeleteTableAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); + } + + public void PutItemAsync(string tableName, string title, string year) + { + GetAndAssertStatusCode($"{BaseUrl}/PutItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + } + public void GetItemAsync(string tableName, string title, string year) + { + GetAndAssertStatusCode($"{BaseUrl}/GetItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + } + public void UpdateItemAsync(string tableName, string title, string year) + { + GetAndAssertStatusCode($"{BaseUrl}/UpdateItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + } + + public void DeleteItemAsync(string tableName, string title, string year) + { + GetAndAssertStatusCode($"{BaseUrl}/DeleteItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + } + public void QueryAsync(string tableName, string title, string year) + { + GetAndAssertStatusCode($"{BaseUrl}/QueryAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + } + public void ScanAsync(string tableName) + { + GetAndAssertStatusCode($"{BaseUrl}/ScanAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); + } + +} diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerMultiServiceTestFixture.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerMultiServiceTestFixture.cs new file mode 100644 index 0000000000..d55ce7d03e --- /dev/null +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerMultiServiceTestFixture.cs @@ -0,0 +1,25 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NewRelic.Agent.ContainerIntegrationTests.Applications; + +namespace NewRelic.Agent.ContainerIntegrationTests.Fixtures; + +public class AwsSdkContainerMultiServiceTestFixture : AwsSdkContainerTestFixtureBase +{ + private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; + private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; + private const string DistroTag = "jammy"; + + private readonly string BaseUrl; + + public AwsSdkContainerMultiServiceTestFixture() : base(DistroTag, Architecture, Dockerfile) + { + BaseUrl = $"http://localhost:{Port}/awssdkmultiservice"; + } + + public void ExerciseMultiService(string tableName, string queueName, string bookName) + { + GetAndAssertStatusCode($"{BaseUrl}/CallMultipleServicesAsync?tableName={tableName}&queueName={queueName}&bookName={bookName}", System.Net.HttpStatusCode.OK); + } +} diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerSQSTestFixture.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerSQSTestFixture.cs new file mode 100644 index 0000000000..75738863be --- /dev/null +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerSQSTestFixture.cs @@ -0,0 +1,53 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NewRelic.Agent.ContainerIntegrationTests.Applications; + +namespace NewRelic.Agent.ContainerIntegrationTests.Fixtures +{ + public class AwsSdkContainerSQSTestFixture : AwsSdkContainerTestFixtureBase + { + private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; + private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; + private const string DistroTag = "jammy"; + + private readonly string BaseUrl; + + public AwsSdkContainerSQSTestFixture() : base(DistroTag, Architecture, Dockerfile) + { + BaseUrl = $"http://localhost:{Port}/awssdksqs"; + } + + public void ExerciseSQS_SendReceivePurge(string queueName) + { + // The exerciser will return a 500 error if the `RequestMessage.MessageAttributeNames` collection is modified by our instrumentation. + // See https://github.com/newrelic/newrelic-dotnet-agent/pull/2646 + GetAndAssertStatusCode($"{BaseUrl}/SQS_SendReceivePurge?queueName={queueName}", System.Net.HttpStatusCode.OK); + } + + public string ExerciseSQS_SendAndReceiveInSeparateTransactions(string queueName) + { + var queueUrl = GetString($"{BaseUrl}/SQS_InitializeQueue?queueName={queueName}"); + + GetAndAssertStatusCode($"{BaseUrl}/SQS_SendMessageToQueue?message=Hello&messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); + + var messagesJson = GetString($"{BaseUrl}/SQS_ReceiveMessageFromQueue?messageQueueUrl={queueUrl}"); + + GetAndAssertStatusCode($"{BaseUrl}/SQS_DeleteQueue?messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); + + return messagesJson; + } + + public string ExerciseSQS_ReceiveEmptyMessage(string queueName) + { + var queueUrl = GetString($"{BaseUrl}/SQS_InitializeQueue?queueName={queueName}"); + + var messagesJson = GetString($"{BaseUrl}/SQS_ReceiveMessageFromQueue?messageQueueUrl={queueUrl}"); + + GetAndAssertStatusCode($"{BaseUrl}/SQS_DeleteQueue?messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); + + return messagesJson; + } + + } +} diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs index b70159f279..a40c277e3e 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs @@ -4,122 +4,24 @@ using System; using System.Threading.Tasks; using NewRelic.Agent.ContainerIntegrationTests.Applications; -using NewRelic.Agent.ContainerIntegrationTests.Fixtures; using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures; -namespace NewRelic.Agent.ContainerIntegrationTests.Fixtures -{ - public abstract class AwsSdkContainerTestFixtureBase( - string distroTag, - ContainerApplication.Architecture containerArchitecture, - string dockerfile, - string dockerComposeFile = "docker-compose-awssdk.yml") - : RemoteApplicationFixture(new ContainerApplication(distroTag, containerArchitecture, DotnetVersion, dockerfile, - dockerComposeFile, "awssdktestapp")) - { - private const string DotnetVersion = "8.0"; - - protected override int MaxTries => 1; - - public void Delay(int seconds) - { - Task.Delay(TimeSpan.FromSeconds(seconds)).GetAwaiter().GetResult(); - } - } -} - -public class AwsSdkContainerSQSTestFixture : AwsSdkContainerTestFixtureBase -{ - private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; - private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; - private const string DistroTag = "jammy"; - - private readonly string BaseUrl; - - public AwsSdkContainerSQSTestFixture() : base(DistroTag, Architecture, Dockerfile) - { - BaseUrl = $"http://localhost:{Port}/awssdksqs"; - } - - public void ExerciseSQS_SendReceivePurge(string queueName) - { - // The exerciser will return a 500 error if the `RequestMessage.MessageAttributeNames` collection is modified by our instrumentation. - // See https://github.com/newrelic/newrelic-dotnet-agent/pull/2646 - GetAndAssertStatusCode($"{BaseUrl}/SQS_SendReceivePurge?queueName={queueName}", System.Net.HttpStatusCode.OK); - } - - public string ExerciseSQS_SendAndReceiveInSeparateTransactions(string queueName) - { - var queueUrl = GetString($"{BaseUrl}/SQS_InitializeQueue?queueName={queueName}"); - - GetAndAssertStatusCode($"{BaseUrl}/SQS_SendMessageToQueue?message=Hello&messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); - - var messagesJson = GetString($"{BaseUrl}/SQS_ReceiveMessageFromQueue?messageQueueUrl={queueUrl}"); - - GetAndAssertStatusCode($"{BaseUrl}/SQS_DeleteQueue?messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); - - return messagesJson; - } - - public string ExerciseSQS_ReceiveEmptyMessage(string queueName) - { - var queueUrl = GetString($"{BaseUrl}/SQS_InitializeQueue?queueName={queueName}"); - - var messagesJson = GetString($"{BaseUrl}/SQS_ReceiveMessageFromQueue?messageQueueUrl={queueUrl}"); +namespace NewRelic.Agent.ContainerIntegrationTests.Fixtures; - GetAndAssertStatusCode($"{BaseUrl}/SQS_DeleteQueue?messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK); - - return messagesJson; - } - -} - -public class AwsSdkContainerDynamoDBTestFixture : AwsSdkContainerTestFixtureBase +public abstract class AwsSdkContainerTestFixtureBase( + string distroTag, + ContainerApplication.Architecture containerArchitecture, + string dockerfile, + string dockerComposeFile = "docker-compose-awssdk.yml") + : RemoteApplicationFixture(new ContainerApplication(distroTag, containerArchitecture, DotnetVersion, dockerfile, + dockerComposeFile, "awssdktestapp")) { - private const string Dockerfile = "AwsSdkTestApp/Dockerfile"; - private const ContainerApplication.Architecture Architecture = ContainerApplication.Architecture.X64; - private const string DistroTag = "jammy"; - - private readonly string BaseUrl; + private const string DotnetVersion = "8.0"; - public AwsSdkContainerDynamoDBTestFixture() : base(DistroTag, Architecture, Dockerfile) - { - BaseUrl = $"http://localhost:{Port}/awssdkdynamodb"; - } - - public void CreateTableAsync(string tableName) - { - GetAndAssertStatusCode($"{BaseUrl}/CreateTableAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); - } - public void DeleteTableAsync(string tableName) - { - GetAndAssertStatusCode($"{BaseUrl}/DeleteTableAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); - } + protected override int MaxTries => 1; - public void PutItemAsync(string tableName, string title, string year) + public void Delay(int seconds) { - GetAndAssertStatusCode($"{BaseUrl}/PutItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); + Task.Delay(TimeSpan.FromSeconds(seconds)).GetAwaiter().GetResult(); } - public void GetItemAsync(string tableName, string title, string year) - { - GetAndAssertStatusCode($"{BaseUrl}/GetItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); - } - public void UpdateItemAsync(string tableName, string title, string year) - { - GetAndAssertStatusCode($"{BaseUrl}/UpdateItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); - } - - public void DeleteItemAsync(string tableName, string title, string year) - { - GetAndAssertStatusCode($"{BaseUrl}/DeleteItemAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); - } - public void QueryAsync(string tableName, string title, string year) - { - GetAndAssertStatusCode($"{BaseUrl}/QueryAsync?tableName={tableName}&title={title}&year={year}", System.Net.HttpStatusCode.OK); - } - public void ScanAsync(string tableName) - { - GetAndAssertStatusCode($"{BaseUrl}/ScanAsync?tableName={tableName}", System.Net.HttpStatusCode.OK); - } - } diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs index c136e83bf7..4b40aec78f 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkDynamoDBTest.cs @@ -4,13 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using NewRelic.Agent.ContainerIntegrationTests.Fixtures; using NewRelic.Agent.IntegrationTestHelpers; using Xunit; using Xunit.Abstractions; namespace NewRelic.Agent.ContainerIntegrationTests.Tests.AwsSdk; -public abstract class AwsSdkDynamoDBTestBase : NewRelicIntegrationTest +public class AwsSdkDynamoDBTest : NewRelicIntegrationTest { private readonly AwsSdkContainerDynamoDBTestFixture _fixture; @@ -18,7 +19,9 @@ public abstract class AwsSdkDynamoDBTestBase : NewRelicIntegrationTest { @@ -66,9 +67,6 @@ protected AwsSdkDynamoDBTestBase(AwsSdkContainerDynamoDBTestFixture fixture, ITe [Fact] public void Test() { - Assert.Equal(0, _fixture.AgentLog.GetWrapperExceptionLineCount()); - Assert.Equal(0, _fixture.AgentLog.GetApplicationErrorLineCount()); - var metrics = _fixture.AgentLog.GetMetrics().ToList(); var metricScopeBase = "WebTransaction/MVC/AwsSdkDynamoDB/"; @@ -104,17 +102,36 @@ public void Test() }; - Assertions.MetricsExist(expectedMetrics, metrics); - } -} + var expectedOperations = new[] { "create_table", "describe_table", "put_item", "get_item", "update_item", "delete_item", "query", "scan", "delete_table" }; + var expectedOperationsCount = expectedOperations.Length; -// Base class with derived classes pattern copied from another tests file -// but we currently don't need to use it for anything + string expectedArn = $"arn:aws:dynamodb:(unknown):{_accountId}:table/{_tableName}"; + var expectedAwsAgentAttributes = new string[] + { + "aws.operation", "aws.requestId", "aws.region", "cloud.resource_id", + }; -public class AwsSdkDynamoDBTest : AwsSdkDynamoDBTestBase -{ - public AwsSdkDynamoDBTest(AwsSdkContainerDynamoDBTestFixture fixture, ITestOutputHelper output) : base(fixture, output) - { + + // get all datastore span events so we can verify counts and operations + var datastoreSpanEvents = _fixture.AgentLog.GetSpanEvents() + .Where(se => (string)se.IntrinsicAttributes["category"] == "datastore") + .ToList(); + + // select the set of AgentAttributes values with a key of "aws.operation" + var awsOperations = datastoreSpanEvents.Select(se => (string)se.AgentAttributes["aws.operation"]).ToList(); + + + Assert.Multiple( + () => Assert.Equal(0, _fixture.AgentLog.GetWrapperExceptionLineCount()), + () => Assert.Equal(0, _fixture.AgentLog.GetApplicationErrorLineCount()), + + () => Assert.Equal(expectedOperationsCount, datastoreSpanEvents.Count), + () => Assert.Equal(expectedOperationsCount, awsOperations.Intersect(expectedOperations).Count()), + + () => Assert.All(datastoreSpanEvents, se => Assert.Contains(expectedAwsAgentAttributes, key => se.AgentAttributes.ContainsKey(key))), + () => Assert.All(datastoreSpanEvents, se => Assert.Equal(expectedArn, se.AgentAttributes["cloud.resource_id"])), + + () => Assertions.MetricsExist(expectedMetrics, metrics) + ); } } - diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkMultiServiceTest.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkMultiServiceTest.cs new file mode 100644 index 0000000000..c97a4dfe3e --- /dev/null +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkMultiServiceTest.cs @@ -0,0 +1,72 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Linq; +using NewRelic.Agent.ContainerIntegrationTests.Fixtures; +using NewRelic.Agent.IntegrationTestHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.ContainerIntegrationTests.Tests.AwsSdk; + +public class AwsSdkMultiServiceTest : NewRelicIntegrationTest +{ + private readonly AwsSdkContainerMultiServiceTestFixture _fixture; + + private readonly string _tableName = $"TableName_{Guid.NewGuid()}"; + private readonly string _queueName = $"QueueName_{Guid.NewGuid()}"; + private readonly string _bookName = $"BookName_{Guid.NewGuid()}"; + + private const string _expectedAccountId = "520056171328"; // matches the account ID parsed from the fake access key used in AwsSdkDynamoDBExerciser + private const string _unxpectedAccountId = "520198777664"; // matches the account ID parsed from the fake access key used in AwsSdkSQSExerciser + + + public AwsSdkMultiServiceTest(AwsSdkContainerMultiServiceTestFixture fixture, ITestOutputHelper output) : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + + _fixture.Actions(setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetLogLevel("finest"); + configModifier.ForceTransactionTraces(); + configModifier.EnableDistributedTrace(); + configModifier.ConfigureFasterMetricsHarvestCycle(15); + configModifier.ConfigureFasterSpanEventsHarvestCycle(15); + configModifier.ConfigureFasterTransactionTracesHarvestCycle(15); + }, + exerciseApplication: () => + { + _fixture.Delay(5); + + _fixture.ExerciseMultiService(_tableName, _queueName, _bookName); + + _fixture.AgentLog.WaitForLogLine(AgentLogBase.MetricDataLogLineRegex, TimeSpan.FromMinutes(2)); + _fixture.AgentLog.WaitForLogLine(AgentLogBase.TransactionTransformCompletedLogLineRegex, + TimeSpan.FromMinutes(2)); + }); + + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + // get all span events + var spanEvents = _fixture.AgentLog.GetSpanEvents(); + // select all span events having an Agent attribute with a key of "cloud.resource_id" + var cloudResourceIdSpanEvents = spanEvents.Where(spanEvent => spanEvent.AgentAttributes.ContainsKey("cloud.resource_id")).ToList(); + + string expectedArn = $"arn:aws:dynamodb:(unknown):{_expectedAccountId}:table/{_tableName}"; + string unExpectedArn = $"arn:aws:dynamodb:(unknown):{_unxpectedAccountId}:table/{_tableName}"; + + // verify all span events contain the expected arn, and do not contain the unexpected arn and all are of category datastore + Assert.Multiple( + () => Assert.All(cloudResourceIdSpanEvents, se => Assert.Equal(expectedArn, se.AgentAttributes["cloud.resource_id"])), + () => Assert.All(cloudResourceIdSpanEvents, se => Assert.NotEqual(_unxpectedAccountId, se.AgentAttributes["cloud.resource_id"])), + () => Assert.All(cloudResourceIdSpanEvents, se => Assert.Equal("datastore", se.IntrinsicAttributes["category"])) + ); + } +} diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs index edd06af3fb..8c73ba3bfe 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NewRelic.Agent.ContainerIntegrationTests.Fixtures; using NewRelic.Agent.IntegrationTestHelpers; using NewRelic.Testing.Assertions; using Xunit; @@ -40,8 +41,6 @@ protected AwsSdkSQSTestBase(AwsSdkContainerSQSTestFixture fixture, ITestOutputHe configModifier.ConfigureFasterMetricsHarvestCycle(15); configModifier.ConfigureFasterSpanEventsHarvestCycle(15); configModifier.ConfigureFasterTransactionTracesHarvestCycle(15); - configModifier.LogToConsole(); - }, exerciseApplication: () => { @@ -176,4 +175,3 @@ public AwsSdkSQSTestNullCollections(AwsSdkContainerSQSTestFixture fixture, ITest { } } - diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/InfiniteTracingContainerTests.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/InfiniteTracingContainerTests.cs index 007ee9573d..309e2f1600 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/InfiniteTracingContainerTests.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/InfiniteTracingContainerTests.cs @@ -33,7 +33,6 @@ protected InfiniteTracingContainerTest(T fixture, ITestOutputHelper output) : ba configModifier.ConfigureFasterMetricsHarvestCycle(10); configModifier.ConfigureFasterTransactionTracesHarvestCycle(10); configModifier.SetLogLevel("Finest"); - configModifier.LogToConsole(); }, exerciseApplication: () => { diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/KafkaTests.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/KafkaTests.cs index c0065cc893..f7d6ba9713 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/KafkaTests.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/KafkaTests.cs @@ -34,7 +34,6 @@ protected LinuxKafkaTest(T fixture, ITestOutputHelper output) : base(fixture) var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); configModifier.SetLogLevel("debug"); configModifier.ConfigureFasterMetricsHarvestCycle(10); - configModifier.LogToConsole(); _fixture.RemoteApplication.SetAdditionalEnvironmentVariable("NEW_RELIC_KAFKA_TOPIC", _topicName); }, diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/LinuxContainerTests.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/LinuxContainerTests.cs index 0ea0a0bfdb..04a6952b71 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/LinuxContainerTests.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/LinuxContainerTests.cs @@ -22,7 +22,6 @@ protected LinuxContainerTest(T fixture, ITestOutputHelper output) : base(fixture { var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); configModifier.ConfigureFasterMetricsHarvestCycle(10); - configModifier.LogToConsole(); }, exerciseApplication: () => { diff --git a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/MemcachedTests.cs b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/MemcachedTests.cs index 679febe860..d36d99bfc8 100644 --- a/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/MemcachedTests.cs +++ b/tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/MemcachedTests.cs @@ -26,7 +26,6 @@ protected LinuxMemcachedTest(T fixture, ITestOutputHelper output) : base(fixture var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); configModifier.SetLogLevel("debug"); configModifier.ConfigureFasterMetricsHarvestCycle(10); - configModifier.LogToConsole(); }, exerciseApplication: () => { diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs index 538a620818..ce19d291ee 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs @@ -44,7 +44,7 @@ public abstract class AgentLogBase public const string CustomEventDataLogLineRegex = DebugLogLinePrefixRegex + @"Request\(.{36}\): Invoked ""custom_event_data"" with : (.*)"; // Collector responses - public const string ConnectResponseLogLineRegex = DebugLogLinePrefixRegex + @"Request\(.{36}\): Invocation of ""connect"" yielded response : {""return_value"":{""agent_run_id""(.*)"; + public const string ConnectResponseLogLineRegex = DebugLogLinePrefixRegex + @"Request\(.{36}\): Invocation of ""connect"" yielded response : {""return_value"":(.*)"; public const string ErrorResponseLogLinePrefixRegex = ErrorLogLinePrefixRegex + @"Request\(.{36}\): "; public const string ThreadProfileStartingLogLineRegex = InfoLogLinePrefixRegex + @"Starting a thread profiling session"; @@ -496,7 +496,7 @@ public IEnumerable GetConnectResponseDatas() foreach (var match in matches) { - var json = "{ \"agent_run_id\"" + match; + var json = match; json = json?.Trim('[', ']'); json = json.Remove(json.Length - 1); // remove the extra } 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/IntegrationTests/AwsSdk/InvokeLambdaTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AwsSdk/InvokeLambdaTests.cs new file mode 100644 index 0000000000..4d42f15097 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/AwsSdk/InvokeLambdaTests.cs @@ -0,0 +1,138 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.AwsSdk +{ + public abstract class InvokeLambdaTestBase : NewRelicIntegrationTest + where TFixture : ConsoleDynamicMethodFixture + { + private readonly TFixture _fixture; + private readonly string _function; + private readonly string _qualifier; + private readonly string _arn; + private readonly bool _isAsync; + + public InvokeLambdaTestBase(TFixture fixture, ITestOutputHelper output, bool useAsync, string function, string qualifier, string arn) : base(fixture) + { + _function = function; + _qualifier = qualifier; + _arn = arn; + _isAsync = useAsync; + + _fixture = fixture; + _fixture.SetTimeout(TimeSpan.FromMinutes(2)); + + _fixture.TestLogger = output; + _fixture.AddActions( + setupConfiguration: () => + { + new NewRelicConfigModifier(fixture.DestinationNewRelicConfigFilePath) + .ForceTransactionTraces() + .SetLogLevel("finest"); + }, + exerciseApplication: () => + { + _fixture.AgentLog.WaitForLogLines(AgentLogBase.TransactionTransformCompletedLogLineRegex, TimeSpan.FromMinutes(2),2); + } + ); + + if (useAsync) + { + _fixture.AddCommand($"InvokeLambdaExerciser InvokeLambdaAsync {_function}:{_qualifier} \"fakepayload\""); + _fixture.AddCommand($"InvokeLambdaExerciser InvokeLambdaAsyncWithQualifier {_function} {_qualifier} \"fakepayload\""); + } + else + { + _fixture.AddCommand($"InvokeLambdaExerciser InvokeLambdaSync {_function} \"fakepayload\""); + } + _fixture.Initialize(); + } + + [Fact] + public void InvokeLambda() + { + var metrics = _fixture.AgentLog.GetMetrics().ToList(); + + var expectedCount = _isAsync ? 2 : 1; + var expectedMetrics = new List + { + new Assertions.ExpectedMetric {metricName = @"DotNet/InvokeRequest", CallCountAllHarvests = expectedCount}, + }; + Assertions.MetricsExist(expectedMetrics, metrics); + + var transactions = _fixture.AgentLog.GetTransactionEvents().ToList(); + Assert.Equal(expectedCount, transactions.Count()); + + foreach (var transaction in transactions) + { + Assert.StartsWith("OtherTransaction/Custom/MultiFunctionApplicationHelpers.NetStandardLibraries.AwsSdk.InvokeLambdaExerciser/InvokeLambda", transaction.IntrinsicAttributes["name"].ToString()); + } + + var allSpans = _fixture.AgentLog.GetSpanEvents() + .Where(e => e.AgentAttributes.ContainsKey("cloud.platform")) + .ToList(); + Assert.Equal(expectedCount, allSpans.Count); + + foreach (var span in allSpans) + { + Assert.Equal("aws_lambda", span.AgentAttributes["cloud.platform"]); + Assert.Equal("InvokeRequest", span.AgentAttributes["aws.operation"]); + Assert.Equal("us-west-2", span.AgentAttributes["aws.region"]); + } + + // There should be one fewer span in this list, because there's one where there wasn't + // enough info to create an ARN + var spansWithArn = _fixture.AgentLog.GetSpanEvents() + .Where(e => e.AgentAttributes.ContainsKey("cloud.resource_id")) + .ToList(); + Assert.Equal(expectedCount, spansWithArn.Count); + foreach (var span in spansWithArn) + { + Assert.Equal(_arn, span.AgentAttributes["cloud.resource_id"]); + } + } + } + [NetFrameworkTest] + public class InvokeLambdaTest_Sync_FW462 : InvokeLambdaTestBase + { + public InvokeLambdaTest_Sync_FW462(ConsoleDynamicMethodFixtureFW462 fixture, ITestOutputHelper output) + : base(fixture, output, false, "342444490463:NotARealFunction", null, "arn:aws:lambda:us-west-2:342444490463:function:NotARealFunction") + { + } + } + [NetFrameworkTest] + public class InvokeLambdaTest_Sync_FWLatest : InvokeLambdaTestBase + { + public InvokeLambdaTest_Sync_FWLatest(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output) + : base(fixture, output, false, "342444490463:NotARealFunction", null, "arn:aws:lambda:us-west-2:342444490463:function:NotARealFunction") + { + } + } + [NetCoreTest] + public class InvokeLambdaTest_Async_CoreOldest : InvokeLambdaTestBase + { + public InvokeLambdaTest_Async_CoreOldest(ConsoleDynamicMethodFixtureCoreOldest fixture, ITestOutputHelper output) + : base(fixture, output, true, "342444490463:NotARealFunction", "NotARealAlias", "arn:aws:lambda:us-west-2:342444490463:function:NotARealFunction:NotARealAlias") + { + } + } + + [NetCoreTest] + public class InvokeLambdaTest_Async_CoreLatest : InvokeLambdaTestBase + { + public InvokeLambdaTest_Async_CoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output) + : base(fixture, output, true, "342444490463:NotARealFunction", "NotARealAlias", "arn:aws:lambda:us-west-2:342444490463:function:NotARealFunction:NotARealAlias") + { + } + } + +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMDisabledAndServerSideHSMEnabledTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMDisabledAndServerSideHSMEnabledTests.cs index 5dc3430a9a..07eb226038 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMDisabledAndServerSideHSMEnabledTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMDisabledAndServerSideHSMEnabledTests.cs @@ -41,7 +41,7 @@ public void Test() { // This test looks for the connect response body that was intended to be removed in P17, but was not. If it does get removed this will fail. // 12/14/23 - the response status changed from "Gone" to "Conflict". If this test fails in the future, be alert for it possibly changing back. - var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict with message {\"exception\":{\"message\":\"Account Security Violation: *?"); + var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict *?"); Assert.NotNull(notConnectedLogLine); } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMEnabledAndServerSideHSMDisabledTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMEnabledAndServerSideHSMDisabledTests.cs index e9f516736e..834bddab1b 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMEnabledAndServerSideHSMDisabledTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/CSP/AspNetCoreLocalHSMEnabledAndServerSideHSMDisabledTests.cs @@ -49,7 +49,7 @@ public void Test() { // This test looks for the connect response body that was intended to be removed in P17, but was not. If it does get removed this will fail. // 12/14/23 - the response status changed from "Gone" to "Conflict". If this test fails in the future, be alert for it possibly changing back. - var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict with message {\"exception\":{\"message\":\"Account Security Violation: *?"); + var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict *?"); Assert.NotNull(notConnectedLogLine); } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerDisabled.cs b/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerDisabled.cs index b920e60491..525f8a3703 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerDisabled.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerDisabled.cs @@ -46,7 +46,7 @@ public void Test() { // This test looks for the connect response body that was intended to be removed in P17, but was not. If it does get removed this will fail. // 12/14/23 - the response status changed from "Gone" to "Conflict". If this test fails in the future, be alert for it possibly changing back. - var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict with message {\"exception\":{\"message\":\"Account Security Violation: *?"); + var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict *?"); Assert.NotNull(notConnectedLogLine); } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerEnabled.cs b/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerEnabled.cs index d4860a62c7..fc847b65c0 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerEnabled.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/CSP/HighSecurityModeServerEnabled.cs @@ -44,7 +44,7 @@ public void Test() { // This test looks for the connect response body that was intended to be removed in P17, but was not. If it does get removed this will fail. // 12/14/23 - the response status changed from "Gone" to "Conflict". If this test fails in the future, be alert for it possibly changing back. - var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict with message {\"exception\":{\"message\":\"Account Security Violation: *?"); + var notConnectedLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.ErrorResponseLogLinePrefixRegex + "Received HTTP status code Conflict *?"); Assert.NotNull(notConnectedLogLine); } } 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/MFALatestPackages/MFALatestPackages.csproj b/tests/Agent/IntegrationTests/SharedApplications/Common/MFALatestPackages/MFALatestPackages.csproj index 5bc977554a..71174b6180 100644 --- a/tests/Agent/IntegrationTests/SharedApplications/Common/MFALatestPackages/MFALatestPackages.csproj +++ b/tests/Agent/IntegrationTests/SharedApplications/Common/MFALatestPackages/MFALatestPackages.csproj @@ -5,9 +5,12 @@ - - + + + + + @@ -35,14 +38,14 @@ - - + + - - + + - - + + @@ -72,7 +75,7 @@ - + diff --git a/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/MultiFunctionApplicationHelpers.csproj b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/MultiFunctionApplicationHelpers.csproj index afc9b9b64f..728ebd5aa7 100644 --- a/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/MultiFunctionApplicationHelpers.csproj +++ b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/MultiFunctionApplicationHelpers.csproj @@ -275,6 +275,14 @@ + + + + + + + + diff --git a/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/AwsSdk/InvokeLambdaExerciser.cs b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/AwsSdk/InvokeLambdaExerciser.cs new file mode 100644 index 0000000000..269e812fcb --- /dev/null +++ b/tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/AwsSdk/InvokeLambdaExerciser.cs @@ -0,0 +1,97 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Security.AccessControl; +using System.Threading.Tasks; +using Amazon; +using Amazon.Lambda.Model; +using NewRelic.Agent.IntegrationTests.Shared.ReflectionHelpers; +using NewRelic.Api.Agent; + +namespace MultiFunctionApplicationHelpers.NetStandardLibraries.AwsSdk +{ + [Library] + public class InvokeLambdaExerciser + { + [LibraryMethod] + [Transaction] + public void InvokeLambdaSync(string function, string payload) + { +#if NETFRAMEWORK + var client = new Amazon.Lambda.AmazonLambdaClient(RegionEndpoint.USWest2); + var request = new Amazon.Lambda.Model.InvokeRequest + { + FunctionName = function, + Payload = payload + }; + + // Note that we aren't invoking a lambda that exists! This is guaranteed to fail, but all we care + // about is that the agent is able to instrument the call. + try + { + var response = client.Invoke(request); + } + catch + { + } +#else + throw new Exception($"Synchronous calls are only supported on .NET Framework!"); +#endif + } + + [LibraryMethod] + [Transaction] + public async Task InvokeLambdaAsync(string function, string payload) + { + var client = new Amazon.Lambda.AmazonLambdaClient(RegionEndpoint.USWest2); + var request = new Amazon.Lambda.Model.InvokeRequest + { + FunctionName = function, + Payload = payload + }; + + // Note that we aren't invoking a lambda that exists! This is guaranteed to fail, but all we care + // about is that the agent is able to instrument the call. + try + { + var response = await client.InvokeAsync(request); + MemoryStream stream = response.Payload; + string returnValue = System.Text.Encoding.UTF8.GetString(stream.ToArray()); + return returnValue; + } + catch + { + } + return null; + } + + [LibraryMethod] + [Transaction] + public async Task InvokeLambdaAsyncWithQualifier(string function, string qualifier, string payload) + { + var client = new Amazon.Lambda.AmazonLambdaClient(RegionEndpoint.USWest2); + var request = new Amazon.Lambda.Model.InvokeRequest + { + FunctionName = function, + Qualifier = qualifier, + Payload = payload + }; + + // Note that we aren't invoking a lambda that exists! This is guaranteed to fail, but all we care + // about is that the agent is able to instrument the call. + try + { + var response = await client.InvokeAsync(request); + MemoryStream stream = response.Payload; + string returnValue = System.Text.Encoding.UTF8.GetString(stream.ToArray()); + return returnValue; + } + catch + { + } + return null; + } + } +} 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/CompositeTests/AgentApiTests.cs b/tests/Agent/UnitTests/CompositeTests/AgentApiTests.cs index c0da43590b..5da764b13e 100644 --- a/tests/Agent/UnitTests/CompositeTests/AgentApiTests.cs +++ b/tests/Agent/UnitTests/CompositeTests/AgentApiTests.cs @@ -2280,5 +2280,50 @@ public void SpanCustomAttributes() } #endregion + + #region Span Cloud SDK Attributes + [Test] + public void SpanCloudSdkAttributes() + { + var agentWrapperApi = _compositeTestAgent.GetAgent(); + var dtm1 = DateTime.Now; + var dtm2 = DateTimeOffset.Now; + + // ACT + var transaction = agentWrapperApi.CreateTransaction( + isWeb: true, + category: EnumNameCache.GetName(WebTransactionType.Action), + transactionDisplayName: "name", + doNotTrackAsUnitOfWork: true); + + var segment = agentWrapperApi.StartTransactionSegmentOrThrow("segment"); + + segment.AddCloudSdkAttribute("cloud.platform", "aws_lambda"); + segment.AddCloudSdkAttribute("aws.region", "us-west-2"); + segment.AddCloudSdkAttribute("cloud.resource_id", "arn:aws:lambda:us-west-2:123456789012:function:myfunction"); + + var expectedAttributes = new[] + { + new ExpectedAttribute(){ Key = "cloud.platform", Value = "aws_lambda"}, + new ExpectedAttribute(){ Key = "aws.region", Value = "us-west-2"}, + new ExpectedAttribute(){ Key = "cloud.resource_id", Value = "arn:aws:lambda:us-west-2:123456789012:function:myfunction"}, + }; + + segment.End(); + transaction.End(); + + _compositeTestAgent.Harvest(); + + var allSpans = _compositeTestAgent.SpanEvents; + var testSpan = allSpans.LastOrDefault(); + + NrAssert.Multiple + ( + () => Assert.That(allSpans, Has.Count.EqualTo(2)), + () => SpanAssertions.HasAttributes(expectedAttributes, AttributeClassification.AgentAttributes, testSpan) + ); + } + + #endregion } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs b/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs index 528470beaf..1da15a9534 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs @@ -61,6 +61,7 @@ private IConfiguration GetDefaultConfiguration() Mock.Arrange(() => configuration.LoggingEnabled).Returns(() => _enableLogging); Mock.Arrange(() => configuration.IgnoredInstrumentation).Returns(() => _ignoredInstrumentation); Mock.Arrange(() => configuration.GCSamplerV2Enabled).Returns(true); + Mock.Arrange(() => configuration.AwsAccountId).Returns("123456789012"); Mock.Arrange(() => configuration.LabelsEnabled).Returns(true); return configuration; @@ -534,5 +535,12 @@ public void GCSamplerV2EnabledSupportabiliityMetricPresent() _agentHealthReporter.CollectMetrics(); Assert.That(_publishedMetrics.Any(x => x.MetricNameModel.Name == "Supportability/Dotnet/GCSamplerV2/Enabled"), Is.True); } + + [Test] + public void AwsAccountIdSupportabiliityMetricPresent() + { + _agentHealthReporter.CollectMetrics(); + Assert.That(_publishedMetrics.Any(x => x.MetricNameModel.Name == "Supportability/Dotnet/AwsAccountId/Config"), Is.True); + } } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/NewRelic.Agent.Core.FromLegacy/LoggerBootstrapperTest.cs b/tests/Agent/UnitTests/Core.UnitTest/NewRelic.Agent.Core.FromLegacy/LoggerBootstrapperTest.cs index 26adcd4408..4134cd6b7b 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/NewRelic.Agent.Core.FromLegacy/LoggerBootstrapperTest.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/NewRelic.Agent.Core.FromLegacy/LoggerBootstrapperTest.cs @@ -29,6 +29,21 @@ public static void No_log_levels_are_enabled_when_config_log_is_off() ); } + [Test] + public void NoLogLevelsAreEnabled_WhenLogIsDisabled() + { + ILogConfig config = LogConfigFixtureWithLogEnabled(false); + LoggerBootstrapper.Initialize(); + LoggerBootstrapper.ConfigureLogger(config); + NrAssert.Multiple( + () => Assert.That(Log.IsFinestEnabled, Is.False), + () => Assert.That(Log.IsDebugEnabled, Is.False), + () => Assert.That(Log.IsInfoEnabled, Is.False), + () => Assert.That(Log.IsWarnEnabled, Is.False), + () => Assert.That(Log.IsErrorEnabled, Is.False) + ); + } + [Test] public static void All_log_levels_are_enabled_when_config_log_is_all() { @@ -210,7 +225,7 @@ private static ILogConfig LogConfigFixtureWithLogEnabled(bool enabled) " " + " Test" + " " + - " " + + " " + "", enabled.ToString().ToLower()); diff --git a/tests/Agent/UnitTests/Core.UnitTest/Segments/SegmentTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Segments/SegmentTests.cs index 7a9e87fbe0..6c7c59aab8 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Segments/SegmentTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Segments/SegmentTests.cs @@ -58,5 +58,57 @@ public void DurationOrZero_ReturnsDuration_IfDurationIsSet() Assert.That(duration, Is.EqualTo(TimeSpan.FromSeconds(1))); } + [Test] + public void Misc_Segment_Setters() + { + var segment = new Segment(TransactionSegmentStateHelpers.GetItransactionSegmentState(), new MethodCallData("Type", "Method", 1)); + Assert.That(segment.IsLeaf, Is.False); + segment.MakeLeaf(); + Assert.That(segment.IsLeaf, Is.True); + + segment.SetName("foo"); + Assert.That(segment.GetTransactionTraceName(), Is.EqualTo("foo")); + + Assert.That(segment.Combinable, Is.False); + segment.MakeCombinable(); + Assert.That(segment.Combinable, Is.True); + + Assert.That(segment.IsExternal, Is.False); + } + + [Test] + public void NoOpSegment() + { + var segment = new NoOpSegment(); + Assert.That(segment.IsDone, Is.True); + Assert.That(segment.IsValid, Is.False); + Assert.That(segment.IsDone, Is.True); + Assert.That(segment.DurationShouldBeDeductedFromParent, Is.False); + Assert.That(segment.IsLeaf, Is.False); + Assert.That(segment.IsExternal, Is.False); + Assert.That(segment.SpanId, Is.Null); + Assert.That(segment.SegmentData, Is.Not.Null); + Assert.That(segment.AttribDefs, Is.Not.Null); + Assert.That(segment.AttribValues, Is.Not.Null); + Assert.That(segment.TypeName, Is.EqualTo(string.Empty)); + Assert.That(segment.UserCodeFunction, Is.EqualTo(string.Empty)); + Assert.That(segment.UserCodeNamespace, Is.EqualTo(string.Empty)); + Assert.That(segment.SegmentNameOverride, Is.Null); + + Assert.DoesNotThrow(() => segment.End()); + Assert.DoesNotThrow(() => segment.End(new Exception())); + Assert.DoesNotThrow(() => segment.EndStackExchangeRedis()); + Assert.DoesNotThrow(() => segment.MakeCombinable()); + Assert.DoesNotThrow(() => segment.MakeLeaf()); + Assert.DoesNotThrow(() => segment.RemoveSegmentFromCallStack()); + Assert.DoesNotThrow(() => segment.SetMessageBrokerDestination("destination")); + Assert.DoesNotThrow(() => segment.SetSegmentData(null)); + Assert.DoesNotThrow(() => segment.AddCustomAttribute("key", "value")); + Assert.DoesNotThrow(() => segment.AddCloudSdkAttribute("key", "value")); + Assert.DoesNotThrow(() => segment.SetName("name")); + Assert.That(segment.GetCategory(), Is.EqualTo(string.Empty)); + Assert.That(segment.DurationOrZero, Is.EqualTo(TimeSpan.Zero)); + + } } } diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUCacheTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUCacheTests.cs new file mode 100644 index 0000000000..fd9a5bfee8 --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUCacheTests.cs @@ -0,0 +1,170 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; +using NewRelic.Agent.Extensions.Caching; + +namespace Agent.Extensions.Tests.Cache +{ + [TestFixture] + public class LRUCacheTests + { + [Test] + public void Constructor_ShouldThrowException_WhenCapacityIsZeroOrNegative() + { + Assert.Throws(() => new LRUCache(0)); + Assert.Throws(() => new LRUCache(-1)); + } + + [Test] + public void Put_ShouldAddItemToCache() + { + // Arrange + var cache = new LRUCache(2); + + // Act + cache.Put(1, "one"); + + // Assert + Assert.That(cache.ContainsKey(1), Is.True); + Assert.That(cache.Get(1), Is.EqualTo("one")); + } + + [Test] + public void Get_ShouldThrowException_WhenKeyNotFound() + { + // Arrange + var cache = new LRUCache(2); + + // Act & Assert + Assert.Throws(() => cache.Get(1)); + } + + [Test] + public void Put_ShouldEvictLeastRecentlyUsedItem_WhenCapacityIsExceeded() + { + // Arrange + var cache = new LRUCache(2); + cache.Put(1, "one"); + cache.Put(2, "two"); + + // Act + cache.Put(3, "three"); + + // Assert + Assert.That(cache.ContainsKey(1), Is.False); + Assert.That(cache.ContainsKey(2), Is.True); + Assert.That(cache.ContainsKey(3), Is.True); + } + + [Test] + public void Get_ShouldMoveAccessedItemToFront() + { + // Arrange + var cache = new LRUCache(2); + cache.Put(1, "one"); + cache.Put(2, "two"); + + // Act + var value = cache.Get(1); + cache.Put(3, "three"); + + // Assert + Assert.That(cache.ContainsKey(1), Is.True); + Assert.That(cache.ContainsKey(2), Is.False); + Assert.That(cache.ContainsKey(3), Is.True); + } + + [Test] + public void Put_ShouldUpdateValue_WhenKeyAlreadyExists() + { + // Arrange + var cache = new LRUCache(2); + cache.Put(1, "one"); + + // Act + cache.Put(1, "uno"); + + // Assert + Assert.That(cache.Get(1), Is.EqualTo("uno")); + } + + [Test] + public void ContainsKey_ShouldReturnTrueForExistingKey() + { + // Arrange + var cache = new LRUCache(2); + cache.Put(1, "one"); + + // Act + var containsKey = cache.ContainsKey(1); + + // Assert + Assert.That(containsKey, Is.True); + } + + [Test] + public void ContainsKey_ShouldReturnFalseForNonExistingKey() + { + // Arrange + var cache = new LRUCache(2); + + // Act + var containsKey = cache.ContainsKey(1); + + // Assert + Assert.That(containsKey, Is.False); + } + + [Test] + public void Put_ShouldHandleEdgeCaseForCapacity() + { + // Arrange + var cache = new LRUCache(1); + cache.Put(1, "one"); + + // Act + cache.Put(2, "two"); + + // Assert + Assert.That(cache.ContainsKey(1), Is.False); + Assert.That(cache.ContainsKey(2), Is.True); + } + + [Test] + public void Cache_ShouldBeThreadSafe() + { + // Arrange + var cache = new LRUCache(100); + var putTasks = new List(); + var getTasks = new List(); + + // Act + for (int i = 0; i < 100; i++) + { + int index = i; + putTasks.Add(Task.Run(() => cache.Put(index, $"value{index}"))); + } + + Task.WaitAll(putTasks.ToArray()); + + for (int i = 0; i < 100; i++) + { + int index = i; + getTasks.Add(Task.Run(() => cache.Get(index))); + } + + Task.WaitAll(getTasks.ToArray()); + + // Assert + for (int i = 0; i < 100; i++) + { + Assert.That(cache.ContainsKey(i), Is.True); + Assert.That(cache.Get(i), Is.EqualTo($"value{i}")); + } + } + } +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUHashSetTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUHashSetTests.cs new file mode 100644 index 0000000000..dbd100cea0 --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/LRUHashSetTests.cs @@ -0,0 +1,230 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; +using NewRelic.Agent.Extensions.Caching; + +namespace Agent.Extensions.Tests.Cache +{ + [TestFixture] + public class LRUHashSetTests + { + [Test] + public void Constructor_ShouldThrowException_WhenCapacityIsZeroOrNegative() + { + Assert.Throws(() => new LRUHashSet(0)); + Assert.Throws(() => new LRUHashSet(-1)); + } + + [Test] + public void Add_ShouldAddItemToSet() + { + var set = new LRUHashSet(2); + var added = set.Add(1); + Assert.That(added, Is.True); + Assert.That(set.Contains(1), Is.True); + } + + [Test] + public void Add_ShouldNotAddDuplicateItem() + { + var set = new LRUHashSet(2); + set.Add(1); + var added = set.Add(1); + Assert.That(added, Is.False); + Assert.That(set.Contains(1), Is.True); + } + + [Test] + public void Add_ShouldEvictLeastRecentlyUsedItem_WhenCapacityIsExceeded() + { + var set = new LRUHashSet(2); + set.Add(1); + set.Add(2); + set.Add(3); + Assert.That(set.Contains(1), Is.False); + Assert.That(set.Contains(2), Is.True); + Assert.That(set.Contains(3), Is.True); + } + + [Test] + public void Contains_ShouldReturnTrueForExistingItem() + { + var set = new LRUHashSet(2); + set.Add(1); + var contains = set.Contains(1); + Assert.That(contains, Is.True); + } + + [Test] + public void Contains_ShouldReturnFalseForNonExistingItem() + { + var set = new LRUHashSet(2); + var contains = set.Contains(1); + Assert.That(contains, Is.False); + } + + [Test] + public void Remove_ShouldRemoveItemFromSet() + { + var set = new LRUHashSet(2); + set.Add(1); + var removed = set.Remove(1); + Assert.That(removed, Is.True); + Assert.That(set.Contains(1), Is.False); + } + + [Test] + public void Remove_ShouldReturnFalseForNonExistingItem() + { + var set = new LRUHashSet(2); + var removed = set.Remove(1); + Assert.That(removed, Is.False); + } + + [Test] + public void Count_ShouldReturnNumberOfItemsInSet() + { + var set = new LRUHashSet(2); + set.Add(1); + set.Add(2); + var count = set.Count; + Assert.That(count, Is.EqualTo(2)); + } + + [Test] + public void Set_ShouldBeThreadSafe() + { + var set = new LRUHashSet(100); + var addTasks = new List(); + var containsTasks = new List(); + + for (int i = 0; i < 100; i++) + { + int index = i; + addTasks.Add(Task.Run(() => set.Add(index))); + } + + Task.WaitAll(addTasks.ToArray()); + + for (int i = 0; i < 100; i++) + { + int index = i; + containsTasks.Add(Task.Run(() => set.Contains(index))); + } + + Task.WaitAll(containsTasks.ToArray()); + + for (int i = 0; i < 100; i++) + { + Assert.That(set.Contains(i), Is.True); + } + } + + [Test] + public void Add_ShouldHandleNullValues() + { + var set = new LRUHashSet(2); + var added = set.Add(null); + Assert.That(added, Is.True); + Assert.That(set.Contains(null), Is.True); + } + + [Test] + public void Add_ShouldHandleCapacityOfOne() + { + var set = new LRUHashSet(1); + set.Add(1); + set.Add(2); + Assert.That(set.Contains(1), Is.False); + Assert.That(set.Contains(2), Is.True); + } + + [Test] + public void Add_ShouldMoveAccessedItemToFront() + { + var set = new LRUHashSet(2); + set.Add(1); + set.Add(2); + set.Add(1); + set.Add(3); + Assert.That(set.Contains(1), Is.True); + Assert.That(set.Contains(2), Is.False); + Assert.That(set.Contains(3), Is.True); + } + + [Test] + public void ConcurrentAddAndRemove_ShouldBeThreadSafe() + { + var set = new LRUHashSet(100); + var addTasks = new List(); + var removeTasks = new List(); + + // Add items concurrently + for (int i = 0; i < 100; i++) + { + int index = i; + addTasks.Add(Task.Run(() => set.Add(index))); + } + + Task.WaitAll(addTasks.ToArray()); + + // Remove items concurrently + for (int i = 0; i < 100; i++) + { + int index = i; + removeTasks.Add(Task.Run(() => set.Remove(index))); + } + + Task.WaitAll(removeTasks.ToArray()); + + // Verify that all items have been removed + for (int i = 0; i < 100; i++) + { + Assert.That(set.Contains(i), Is.False); + } + } + + [Test] + public void Add_ShouldHandleLargeNumberOfItems() + { + var set = new LRUHashSet(1000); + for (int i = 0; i < 1000; i++) + { + set.Add(i); + } + for (int i = 0; i < 1000; i++) + { + Assert.That(set.Contains(i), Is.True); + } + } + + [Test] + public void Add_ShouldHandleDuplicateAdditions() + { + var set = new LRUHashSet(2); + set.Add(1); + set.Add(1); + Assert.That(set.Count, Is.EqualTo(1)); + } + + [Test] + public void Remove_ShouldHandleNonExistentItem() + { + var set = new LRUHashSet(2); + var removed = set.Remove(1); + Assert.That(removed, Is.False); + } + + [Test] + public void Add_ShouldHandleMaxCapacity() + { + var set = new LRUHashSet(int.MaxValue); + set.Add(1); + Assert.That(set.Contains(1), Is.True); + } + } +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/WeakReferenceKeyTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/WeakReferenceKeyTests.cs new file mode 100644 index 0000000000..375780f36b --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Cache/WeakReferenceKeyTests.cs @@ -0,0 +1,177 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Threading.Tasks; +using NewRelic.Agent.Extensions.Caching; +using NUnit.Framework; + +namespace Agent.Extensions.Tests.Cache +{ + [TestFixture] + public class WeakReferenceKeyTests + { + [Test] + public void Constructor_ShouldInitializeWeakReference() + { + // Arrange + var foo = new Foo(); + + // Act + var weakReferenceKey = new WeakReferenceKey(foo); + + // Assert + Assert.Multiple(() => + { + Assert.That(weakReferenceKey, Is.Not.Null); + Assert.That(weakReferenceKey.Value, Is.Not.Null); + Assert.That(weakReferenceKey.Value, Is.SameAs(foo)); + }); + } + + [Test] + public void Equals_ShouldReturnTrueForSameObject() + { + // Arrange + var foo = new Foo(); + var weakReferenceKey1 = new WeakReferenceKey(foo); + var weakReferenceKey2 = new WeakReferenceKey(foo); + + // Act + var result = weakReferenceKey1.Equals(weakReferenceKey2); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void Equals_ShouldReturnFalseForDifferentObjects() + { + // Arrange + var foo1 = new Foo(); + var foo2 = new Foo(); + var weakReferenceKey1 = new WeakReferenceKey(foo1); + var weakReferenceKey2 = new WeakReferenceKey(foo2); + + // Act + var result = weakReferenceKey1.Equals(weakReferenceKey2); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void GetHashCode_ShouldReturnSameHashCodeForSameObject() + { + // Arrange + var foo = new Foo(); + var weakReferenceKey1 = new WeakReferenceKey(foo); + var weakReferenceKey2 = new WeakReferenceKey(foo); + + // Act + var hashCode1 = weakReferenceKey1.GetHashCode(); + var hashCode2 = weakReferenceKey2.GetHashCode(); + + // Assert + Assert.That(hashCode1, Is.EqualTo(hashCode2)); + } + + [Test] + public void GetHashCode_ShouldReturnDifferentHashCodeForDifferentObjects() + { + // Arrange + var foo1 = new Foo(); + var foo2 = new Foo(); + var weakReferenceKey1 = new WeakReferenceKey(foo1); + var weakReferenceKey2 = new WeakReferenceKey(foo2); + + // Act + var hashCode1 = weakReferenceKey1.GetHashCode(); + var hashCode2 = weakReferenceKey2.GetHashCode(); + + // Assert + Assert.That(hashCode1, Is.Not.EqualTo(hashCode2)); + } + + [Test] + public async Task GetHashCode_ShouldReturnZeroIfTargetIsGarbageCollected() + { + // Arrange + var weakRefKey = GetWeakReferenceKey(); + + // Act + Assert.That(weakRefKey.Value, Is.Not.Null); + // force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + await Task.Delay(500); + GC.Collect(); // Force another collection + + // Assert + Assert.That(weakRefKey.GetHashCode(), Is.EqualTo(0)); + } + + [Test] + public async Task Value_ShouldReturnNullIfTargetIsGarbageCollected() + { + // Arrange + var weakRefKey = GetWeakReferenceKey(); + + // Act + Assert.That(weakRefKey.Value, Is.Not.Null); + // force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + await Task.Delay(500); + GC.Collect(); // Force another collection + + // Assert + Assert.That(weakRefKey.Value, Is.Null); + } + + private WeakReferenceKey GetWeakReferenceKey() + { + var foo = new Foo(); + return new WeakReferenceKey(foo); + } + [Test] + public void Equals_ShouldReturnFalseForNonWeakReferenceKeyObject() + { + // Arrange + var foo = new Foo(); + var weakReferenceKey = new WeakReferenceKey(foo); + + // Act + var result = weakReferenceKey.Equals(new object()); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task Equals_ShouldReturnFalseIfTargetIsGarbageCollected() + { + // Arrange + var weakRefKey1 = GetWeakReferenceKey(); + var weakRefKey2 = GetWeakReferenceKey(); + + // Act + Assert.That(weakRefKey1.Value, Is.Not.Null); + Assert.That(weakRefKey2.Value, Is.Not.Null); + // force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + await Task.Delay(500); + GC.Collect(); // Force another collection + + // Assert + Assert.That(weakRefKey1.Equals(weakRefKey2), Is.False); + } + + private class Foo + { + public string Bar { get; set; } + } + } + +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Collections/ConcurrentHashSet.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Collections/ConcurrentHashSet.cs index 85a69a634d..2f4e589362 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Collections/ConcurrentHashSet.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Collections/ConcurrentHashSet.cs @@ -96,6 +96,24 @@ public void ConcurrentHashSet_IsThreadSafe() tasks.ForEach(task => task.Wait()); } + [Test] + [TestCase(new[] { 1, 2, 3 }, new[] { true, true, true })] + [TestCase(new[] { 1, 2, 2, 3 }, new[] { true, true, false, true })] + [TestCase(new[] { 4, 4, 4, 4 }, new[] { true, false, false, false })] + [TestCase(new[] { 5, 6, 7, 8, 9 }, new[] { true, true, true, true, true })] + [TestCase(new[] { 10, 10, 11, 11, 12 }, new[] { true, false, true, false, true })] + public void ConcurrentHashSet_TryAdd(int[] values, bool[] results) + { + ConcurrentHashSet set = new(); + for (var i = 0; i < values.Length; i++) + { + var value = values[i]; + var result = results[i]; + + Assert.That(set.TryAdd(value), Is.EqualTo(result)); + } + } + private static void ExerciseFullApi(ConcurrentHashSet hashSet, int[] numbersToAdd) { dynamic _; 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 + } + + } +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsSdkHelperTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsSdkHelperTests.cs new file mode 100644 index 0000000000..e44e2d5722 --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/AwsSdkHelperTests.cs @@ -0,0 +1,84 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NUnit.Framework; +using NewRelic.Agent.Extensions.AwsSdk; + +namespace Agent.Extensions.Tests.Helpers +{ + public class AwsSdkArnBuilderTests + { + [Test] + [TestCase("myfunction", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("myfunction", "us-west-2", "", null)] + [TestCase("myfunction", "", "123456789012", "arn:aws:lambda:(unknown):123456789012:function:myfunction")] + [TestCase("myfunction:alias", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction:alias")] + [TestCase("myfunction:alias", "us-west-2", "", null)] + [TestCase("123456789012:function:my-function", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:my-function")] + [TestCase("123456789012:function:my-function:myalias", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:my-function:myalias")] + [TestCase("123456789012:function:my-function:myalias:extra", "us-west-2", "123456789012", null)] + [TestCase("123456789012:function:my-function:myalias:extra:lots:of:extra:way:too:many", "us-west-2", "123456789012", null)] + [TestCase("arn:aws:", "us-west-2", "123456789012", "arn:aws:")] + [TestCase("arn:aws:lambda:us-west-2:123456789012:function:myfunction", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("arn:aws:lambda:us-west-2:123456789012:function:myfunction", "us-west-2", "", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("arn:aws:lambda:us-west-2:123456789012:function:myfunction", "", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("myfunction", "us-east-1", "987654321098", "arn:aws:lambda:us-east-1:987654321098:function:myfunction")] + [TestCase("myfunction:prod", "eu-west-1", "111122223333", "arn:aws:lambda:eu-west-1:111122223333:function:myfunction:prod")] + [TestCase("my-function", "ap-southeast-1", "444455556666", "arn:aws:lambda:ap-southeast-1:444455556666:function:my-function")] + [TestCase("my-function:beta", "ca-central-1", "777788889999", "arn:aws:lambda:ca-central-1:777788889999:function:my-function:beta")] + [TestCase("arn:aws:lambda:eu-central-1:222233334444:function:myfunction", "eu-central-1", "222233334444", "arn:aws:lambda:eu-central-1:222233334444:function:myfunction")] + [TestCase("us-west-2:myfunction", null, "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("us-west-2:myfunction", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction")] + [TestCase("us-west-2:myfunction", "us-west-2", "", null)] + [TestCase("us-west-2:myfunction:alias", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:myfunction:alias")] + [TestCase("us-west-2:myfunction:alias", "us-west-2", "", null)] + [TestCase("123456789012:my-function", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:my-function")] + [TestCase("123456789012:my-function:myalias", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:my-function:myalias")] + [TestCase("123456789012:my-function:myalias:extra", "us-west-2", "123456789012", null)] + [TestCase("123456789012:my-function:myalias:extra:lots:of:extra:way:too:many", "us-west-2", "123456789012", null)] + [TestCase("eu-west-1:us-west-2", "eu-west-1", "123456789012", "arn:aws:lambda:eu-west-1:123456789012:function:us-west-2")] + [TestCase("", "us-west-2", "123456789012", null)] + // Edge cases: functions that look like account IDs or region names + [TestCase("123456789012:444455556666", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:444455556666")] + [TestCase("444455556666", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:444455556666")] + [TestCase("us-west-2", "us-west-2", "123456789012", "arn:aws:lambda:us-west-2:123456789012:function:us-west-2")] + public void ConstructLambdaArn(string name, string region, string accountId, string arn) + { + var arnBuilder = new ArnBuilder("aws", region, accountId); + var constructedArn = arnBuilder.BuildFromPartialLambdaArn(name); + Assert.That(constructedArn, Is.EqualTo(arn), "Did not get expected ARN"); + } + + [Test] + [TestCase("aws", "s3", "us-west-2", "123456789012", "bucket_name", "arn:aws:s3:us-west-2:123456789012:bucket_name")] + [TestCase("aws", "dynamodb", "us-east-1", "987654321098", "table_name", "arn:aws:dynamodb:us-east-1:987654321098:table_name")] + [TestCase("aws", "dynamodb", "us-east-1", "987654321098", "table/tabletName", "arn:aws:dynamodb:us-east-1:987654321098:table/tabletName")] + [TestCase("aws", "ec2", "eu-west-1", "111122223333", "instance_id", "arn:aws:ec2:eu-west-1:111122223333:instance_id")] + [TestCase("aws", "sqs", "ap-southeast-1", "444455556666", "queue_name", "arn:aws:sqs:ap-southeast-1:444455556666:queue_name")] + [TestCase("aws", "sns", "ca-central-1", "777788889999", "topic_name", "arn:aws:sns:ca-central-1:777788889999:topic_name")] + [TestCase("aws-cn", "lambda", "cn-north-1", "222233334444", "function_name", "arn:aws-cn:lambda:cn-north-1:222233334444:function_name")] + [TestCase("aws-us-gov", "iam", "us-gov-west-1", "555566667777", "role_name", "arn:aws-us-gov:iam:us-gov-west-1:555566667777:role_name")] + [TestCase("aws", "rds", "sa-east-1", "888899990000", "db_instance", "arn:aws:rds:sa-east-1:888899990000:db_instance")] + [TestCase("aws", "s3", "", "123456789012", "bucket_name", "arn:aws:s3:(unknown):123456789012:bucket_name")] + [TestCase("aws", "s3", "us-west-2", "", "bucket_name", null)] + public void ConstructGenericArn(string partition, string service, string region, string accountId, string resource, string expectedArn) + { + var arnBuilder = new ArnBuilder(partition, region, accountId); + var constructedArn = arnBuilder.Build(service, resource); + Assert.That(constructedArn, Is.EqualTo(expectedArn), "Did not get expected ARN"); + } + + [Test] + [TestCase("aws", "us-west-2", "123456789012", "Partition: aws, Region: us-west-2, AccountId: [Present]")] + [TestCase("aws", "", "123456789012", "Partition: aws, Region: (unknown), AccountId: [Present]")] + [TestCase("aws", "us-west-2", "", "Partition: aws, Region: us-west-2, AccountId: [Missing]")] + [TestCase("aws", "us-west-2", null, "Partition: aws, Region: us-west-2, AccountId: [Missing]")] + [TestCase("", "", "", "Partition: aws, Region: (unknown), AccountId: [Missing]")] + [TestCase(null, null, null, "Partition: aws, Region: (unknown), AccountId: [Missing]")] + public void ArnBuilderToString(string partition, string region, string accountId, string expected) + { + var arnBuilder = new ArnBuilder(partition, region, accountId); + Assert.That(arnBuilder.ToString(), Is.EqualTo(expected), "Did not get expected string"); + } + } +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/SqsHelperTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/SqsHelperTests.cs index e064d5c351..1e16c5e0f3 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/SqsHelperTests.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/SqsHelperTests.cs @@ -217,6 +217,10 @@ public ISpan AddCustomAttribute(string key, object value) { throw new NotImplementedException(); } + public ISpan AddCloudSdkAttribute(string key, object value) + { + throw new NotImplementedException(); + } public ISpan SetName(string name) {