Skip to content

Commit 758b770

Browse files
fix: Handle null or empty SQS messages and/or message attributes (#2833)
* Null check and collection count check to prevent exceptions * Some more null checks needed in both agent and test code * Run SQS tests with AWSConfigs.InitializeCollections set both true and false * Test receiving empty messages * Attempt to solve test container conflict * PR feedback * Allow tests to run in parallel without container name conflicts * disabled CentOS / arm64 container tests --------- Co-authored-by: Marty Tippin <[email protected]>
1 parent 0a98da4 commit 758b770

File tree

10 files changed

+172
-41
lines changed

10 files changed

+172
-41
lines changed

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/AwsSdk/SqsHelper.cs

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections;
66
using System.Collections.Concurrent;
77
using System.Collections.Generic;
8-
using System.Linq;
98
using NewRelic.Agent.Api;
109
using NewRelic.Agent.Api.Experimental;
1110
using NewRelic.Agent.Extensions.Providers.Wrapper;

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsSdk/SQSRequestHandler.cs

+13-5
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ public static AfterWrappedMethodDelegate HandleSQSRequest(InstrumentedMethodCall
107107
var ec = executionContext;
108108
var response = ec.ResponseContext.Response; // response is a ReceiveMessageResponse
109109

110-
// accept distributed trace headers from the first message in the response
111-
SqsHelper.AcceptDistributedTraceHeaders(transaction, response.Messages[0].MessageAttributes);
110+
AcceptTracingHeadersIfSafe(transaction, response);
112111
}
113112
);
114113

@@ -119,10 +118,10 @@ void ProcessResponse(Task responseTask)
119118

120119
// taskResult is a ReceiveMessageResponse
121120
var taskResultGetter = _getRequestResponseFromGeneric.GetOrAdd(responseTask.GetType(), t => VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(t, "Result"));
122-
dynamic receiveMessageResponse = taskResultGetter(responseTask);
121+
dynamic response = taskResultGetter(responseTask);
122+
123+
AcceptTracingHeadersIfSafe(transaction, response);
123124

124-
// accept distributed trace headers from the first message in the response
125-
SqsHelper.AcceptDistributedTraceHeaders(transaction, receiveMessageResponse.Messages[0].MessageAttributes);
126125
}
127126
}
128127

@@ -131,5 +130,14 @@ private static bool ValidTaskResponse(Task response)
131130
return response?.Status == TaskStatus.RanToCompletion;
132131
}
133132

133+
private static void AcceptTracingHeadersIfSafe(ITransaction transaction, dynamic response)
134+
{
135+
if (response.Messages != null && response.Messages.Count > 0 && response.Messages[0].MessageAttributes != null)
136+
{
137+
// accept distributed trace headers from the first message in the response
138+
SqsHelper.AcceptDistributedTraceHeaders(transaction, response.Messages[0].MessageAttributes);
139+
}
140+
}
141+
134142
}
135143
}

tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/AwsSdkExerciser/AwsSdkExerciser.cs

+24-13
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private AmazonSQSClient GetSqsClient()
3535
var awsCredentials = new Amazon.Runtime.BasicAWSCredentials("dummy", "dummy");
3636
var config = new AmazonSQSConfig
3737
{
38-
ServiceURL = "http://localstack-containertest:4566",
38+
ServiceURL = "http://localstack:4566",
3939
AuthenticationRegion = "us-west-2"
4040
};
4141

@@ -117,23 +117,34 @@ public async Task<IEnumerable<Message>> SQS_ReceiveMessageAsync(int maxMessagesT
117117
if (messageAttributeNames.Count != 1)
118118
throw new Exception("Expected messageAttributeNames to have a single element");
119119

120-
foreach (var message in response.Messages)
120+
if (response.Messages != null)
121121
{
122-
Console.WriteLine($"Message: {message.Body}");
123-
foreach (var attr in message.MessageAttributes)
122+
foreach (var message in response.Messages)
124123
{
125-
Console.WriteLine($"MessageAttributes: {attr.Key} = {{ DataType = {attr.Value.DataType}, StringValue = {attr.Value.StringValue}}}");
124+
Console.WriteLine($"Message: {message.Body}");
125+
if (message.MessageAttributes != null)
126+
{
127+
foreach (var attr in message.MessageAttributes)
128+
{
129+
Console.WriteLine($"MessageAttributes: {attr.Key} = {{ DataType = {attr.Value.DataType}, StringValue = {attr.Value.StringValue}}}");
130+
}
131+
}
132+
133+
// delete message
134+
await _amazonSqsClient.DeleteMessageAsync(new DeleteMessageRequest
135+
{
136+
QueueUrl = _sqsQueueUrl,
137+
ReceiptHandle = message.ReceiptHandle
138+
});
126139
}
127140

128-
// delete message
129-
await _amazonSqsClient.DeleteMessageAsync(new DeleteMessageRequest
130-
{
131-
QueueUrl = _sqsQueueUrl,
132-
ReceiptHandle = message.ReceiptHandle
133-
});
141+
return response.Messages;
142+
}
143+
else
144+
{
145+
// received an empty response, so return an empty list of messages
146+
return new List<Message>();
134147
}
135-
136-
return response.Messages;
137148
}
138149

139150
// send message batch

tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/Dockerfile

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,19 @@ ARG NEW_RELIC_HOST
2727
ARG NEW_RELIC_LICENSE_KEY
2828
ARG NEW_RELIC_APP_NAME
2929

30+
# Control whether or not 'empty' things (e.g. message attributes) are initialized
31+
# to an empty collection or left null
32+
ARG AWSSDK_INITCOLLECTIONS
33+
3034
ENV CORECLR_ENABLE_PROFILING=1 \
3135
CORECLR_PROFILER={36032161-FFC0-4B61-B559-F6C5D41BAE5A} \
3236
CORECLR_NEW_RELIC_HOME=/usr/local/newrelic-dotnet-agent \
3337
CORECLR_PROFILER_PATH=/usr/local/newrelic-dotnet-agent/libNewRelicProfiler.so \
3438
NEW_RELIC_HOST=${NEW_RELIC_HOST} \
3539
NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} \
3640
NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} \
37-
NEW_RELIC_LOG_DIRECTORY=/app/logs
41+
NEW_RELIC_LOG_DIRECTORY=/app/logs \
42+
AWSSDK_INITCOLLECTIONS=${AWSSDK_INITCOLLECTIONS}
3843

3944
WORKDIR /app
4045
COPY --from=publish /app/publish .

tests/Agent/IntegrationTests/ContainerApplications/AwsSdkTestApp/Program.cs

+28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Net;
77
using System.Threading.Tasks;
8+
using Amazon;
89
using AwsSdkTestApp.SQSBackgroundService;
910
using Microsoft.AspNetCore.Builder;
1011
using Microsoft.AspNetCore.Hosting;
@@ -23,6 +24,10 @@ public static async Task Main(string[] args)
2324
builder.Logging.ClearProviders();
2425
builder.Logging.AddConsole();
2526

27+
var initCollections = GetBoolFromEnvVar("AWSSDK_INITCOLLECTIONS", true);
28+
29+
AWSConfigs.InitializeCollections = initCollections;
30+
2631
// Add services to the container.
2732
builder.Services.AddControllers();
2833

@@ -57,4 +62,27 @@ static void CreatePidFile()
5762
using var file = File.CreateText(pidFileNameAndPath);
5863
file.WriteLine(pid);
5964
}
65+
66+
static bool GetBoolFromEnvVar(string name, bool defaultValue)
67+
{
68+
bool returnVal = defaultValue;
69+
var envVarVal = Environment.GetEnvironmentVariable(name);
70+
if (envVarVal != null)
71+
{
72+
Console.WriteLine($"Value of env var {name}={envVarVal}");
73+
if (bool.TryParse(envVarVal, out returnVal))
74+
{
75+
Console.WriteLine($"Parsed bool from env var: {returnVal}");
76+
}
77+
else
78+
{
79+
Console.WriteLine("Could not parse bool from env var val: " + envVarVal);
80+
}
81+
}
82+
else
83+
{
84+
Console.WriteLine($"{name} is not set in the environment");
85+
}
86+
return returnVal;
87+
}
6088
}

tests/Agent/IntegrationTests/ContainerApplications/docker-compose-awssdk.yml

+8-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@
2222

2323
services:
2424
localstack:
25-
container_name: "localstack-containertest"
2625
image: localstack/localstack:stable
27-
expose: # ports are only available intneral to the service, not external so there's no chance for conflicts
26+
expose: # ports are only available internal to the service, not external so there's no chance for conflicts
2827
- "4566" # LocalStack Gateway
2928
- "4510-4559" # external services port range
3029
environment:
@@ -52,8 +51,15 @@ services:
5251
NEW_RELIC_HOST: ${NEW_RELIC_HOST}
5352
DOTNET_VERSION: ${DOTNET_VERSION}
5453
APP_DOTNET_VERSION: ${APP_DOTNET_VERSION}
54+
AWSSDK_INITCOLLECTIONS: ${AWSSDK_INITCOLLECTIONS}
5555
ports:
5656
- "${PORT}:80"
5757
volumes:
5858
- ${AGENT_PATH}:/usr/local/newrelic-dotnet-agent # AGENT_PATH from .env, points to newrelichome_linux_x64
5959
- ${LOG_PATH}:/app/logs # LOG_PATH from .env, should be a folder unique to this run of the smoketest app
60+
61+
networks:
62+
default:
63+
driver: bridge
64+
driver_opts:
65+
com.docker.network.bridge.enable_icc: "true"

tests/Agent/IntegrationTests/ContainerIntegrationTests/Fixtures/AwsSdkContainerTestFixtures.cs

+15-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Threading.Tasks;
76
using NewRelic.Agent.ContainerIntegrationTests.Applications;
87
using NewRelic.Agent.ContainerIntegrationTests.Fixtures;
@@ -14,7 +13,7 @@ public abstract class AwsSdkContainerTestFixtureBase(
1413
string distroTag,
1514
ContainerApplication.Architecture containerArchitecture,
1615
string dockerfile,
17-
string dockerComposeFile = "docker-compose-awssdk.yml")
16+
string dockerComposeFile = "docker-compose-awssdk.yml")
1817
: RemoteApplicationFixture(new ContainerApplication(distroTag, containerArchitecture, DotnetVersion, dockerfile,
1918
dockerComposeFile, "awssdktestapp"))
2019
{
@@ -50,7 +49,7 @@ public string ExerciseSQS_SendAndReceiveInSeparateTransactions(string queueName)
5049
{
5150
var address = $"http://localhost:{Port}/awssdk";
5251

53-
var queueUrl = GetString($"{address}/SQS_InitializeQueue?queueName={queueName}");
52+
var queueUrl = GetString($"{address}/SQS_InitializeQueue?queueName={queueName}");
5453

5554
GetAndAssertStatusCode($"{address}/SQS_SendMessageToQueue?message=Hello&messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK);
5655

@@ -61,4 +60,17 @@ public string ExerciseSQS_SendAndReceiveInSeparateTransactions(string queueName)
6160
return messagesJson;
6261
}
6362

63+
public string ExerciseSQS_ReceiveEmptyMessage(string queueName)
64+
{
65+
var address = $"http://localhost:{Port}/awssdk";
66+
67+
var queueUrl = GetString($"{address}/SQS_InitializeQueue?queueName={queueName}");
68+
69+
var messagesJson = GetString($"{address}/SQS_ReceiveMessageFromQueue?messageQueueUrl={queueUrl}");
70+
71+
GetAndAssertStatusCode($"{address}/SQS_DeleteQueue?messageQueueUrl={queueUrl}", System.Net.HttpStatusCode.OK);
72+
73+
return messagesJson;
74+
}
75+
6476
}

tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/AwsSdk/AwsSdkSQSTest.cs

+54-10
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,24 @@
1111

1212
namespace NewRelic.Agent.ContainerIntegrationTests.Tests.AwsSdk;
1313

14-
public class AwsSdkSQSTest : NewRelicIntegrationTest<AwsSdkContainerSQSTestFixture>
14+
public abstract class AwsSdkSQSTestBase : NewRelicIntegrationTest<AwsSdkContainerSQSTestFixture>
1515
{
1616
private readonly AwsSdkContainerSQSTestFixture _fixture;
1717

1818
private readonly string _testQueueName1 = $"TestQueue1-{Guid.NewGuid()}";
1919
private readonly string _testQueueName2 = $"TestQueue2-{Guid.NewGuid()}";
20+
private readonly string _testQueueName3 = $"TestQueue3-{Guid.NewGuid()}";
2021
private readonly string _metricScope1 = "WebTransaction/MVC/AwsSdk/SQS_SendReceivePurge/{queueName}";
2122
private readonly string _metricScope2 = "WebTransaction/MVC/AwsSdk/SQS_SendMessageToQueue/{message}/{messageQueueUrl}";
23+
private bool _initCollections;
2224

23-
public AwsSdkSQSTest(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper output) : base(fixture)
25+
protected AwsSdkSQSTestBase(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper output, bool initCollections) : base(fixture)
2426
{
2527
_fixture = fixture;
2628
_fixture.TestLogger = output;
29+
_initCollections = initCollections;
30+
31+
_fixture.SetAdditionalEnvironmentVariable("AWSSDK_INITCOLLECTIONS", initCollections.ToString());
2732

2833

2934
_fixture.Actions(setupConfiguration: () =>
@@ -44,6 +49,7 @@ public AwsSdkSQSTest(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper ou
4449

4550
_fixture.ExerciseSQS_SendReceivePurge(_testQueueName1);
4651
_fixture.ExerciseSQS_SendAndReceiveInSeparateTransactions(_testQueueName2);
52+
_fixture.ExerciseSQS_ReceiveEmptyMessage(_testQueueName3);
4753

4854
_fixture.AgentLog.WaitForLogLine(AgentLogBase.MetricDataLogLineRegex, TimeSpan.FromMinutes(2));
4955
_fixture.AgentLog.WaitForLogLine(AgentLogBase.TransactionTransformCompletedLogLineRegex, TimeSpan.FromMinutes(2));
@@ -60,6 +66,12 @@ public AwsSdkSQSTest(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper ou
6066
[Fact]
6167
public void Test()
6268
{
69+
// Making sure there are no application errors or wrapper exceptions
70+
// See https://github.com/newrelic/newrelic-dotnet-agent/issues/2811
71+
72+
Assert.Equal(0, _fixture.AgentLog.GetWrapperExceptionLineCount());
73+
Assert.Equal(0, _fixture.AgentLog.GetApplicationErrorLineCount());
74+
6375
var metrics = _fixture.AgentLog.GetMetrics().ToList();
6476

6577
var expectedMetrics = new List<Assertions.ExpectedMetric>
@@ -76,9 +88,18 @@ public void Test()
7688
new() { metricName = $"MessageBroker/SQS/Queue/Consume/Named/{_testQueueName2}", callCount = 1},
7789
new() { metricName = $"MessageBroker/SQS/Queue/Consume/Named/{_testQueueName2}", callCount = 1, metricScope = "OtherTransaction/Custom/AwsSdkTestApp.SQSBackgroundService.SQSReceiverService/ProcessRequestAsync"},
7890

79-
new () { metricName = "Supportability/TraceContext/Accept/Success", callCount = 1}, // only one accept should occur (from the SQSReceiverService/ProcessRequestAsync transaction)
91+
// Only consume metrics for queue 3
92+
new() { metricName = $"MessageBroker/SQS/Queue/Consume/Named/{_testQueueName3}", callCount = 1},
93+
new() { metricName = $"MessageBroker/SQS/Queue/Consume/Named/{_testQueueName3}", callCount = 1, metricScope = "OtherTransaction/Custom/AwsSdkTestApp.SQSBackgroundService.SQSReceiverService/ProcessRequestAsync"},
94+
8095
};
8196

97+
// If the AWS SDK is configured to NOT initialize empty collections, trace headers will not be accepted
98+
if (_initCollections)
99+
{
100+
expectedMetrics.Add(new() { metricName = "Supportability/TraceContext/Accept/Success", callCount = 1 });
101+
}
102+
82103
var sendReceivePurgeTransactionEvent = _fixture.AgentLog.TryGetTransactionEvent(_metricScope1);
83104
var sendReceivePurgeTransactionSample = _fixture.AgentLog.TryGetTransactionSample(_metricScope1);
84105
var sendReceivePurgeExpectedTransactionTraceSegments = new List<string>
@@ -102,25 +123,34 @@ public void Test()
102123
() => Assert.True(receiveMessageTransactionEvent != null, "receiveMessageTransactionEvent should not be null")
103124
);
104125

105-
// verify that distributed trace worked as expected -- the last produce span should have the same traceId and parentId as the last consume span
106126
var queueProduce = $"MessageBroker/SQS/Queue/Produce/Named/{_testQueueName2}";
107127
var queueConsume = $"MessageBroker/SQS/Queue/Consume/Named/{_testQueueName2}";
108128

109129
var spans = _fixture.AgentLog.GetSpanEvents().ToList();
110130
var produceSpan = spans.LastOrDefault(s => s.IntrinsicAttributes["name"].Equals(queueProduce));
111131
var consumeSpan = spans.LastOrDefault(s => s.IntrinsicAttributes["name"].Equals(queueConsume));
112-
var processRequestSpan = spans.LastOrDefault(s => s.IntrinsicAttributes["name"].Equals("OtherTransaction/Custom/AwsSdkTestApp.SQSBackgroundService.SQSReceiverService/ProcessRequestAsync"));
113132

114133
NrAssert.Multiple(
115134
() => Assert.True(produceSpan != null, "produceSpan should not be null"),
116135
() => Assert.True(consumeSpan != null, "consumeSpan should not be null"),
117-
() => Assert.True(processRequestSpan != null, "processRequestSpan should not be null"),
118136
() => Assert.True(produceSpan!.IntrinsicAttributes.ContainsKey("traceId")),
119137
() => Assert.True(produceSpan!.IntrinsicAttributes.ContainsKey("guid")),
120-
() => Assert.True(consumeSpan!.IntrinsicAttributes.ContainsKey("traceId")),
121-
() => Assert.True(processRequestSpan!.IntrinsicAttributes.ContainsKey("parentId")),
122-
() => Assert.Equal(produceSpan!.IntrinsicAttributes["traceId"], consumeSpan!.IntrinsicAttributes["traceId"]),
123-
() => Assert.Equal(produceSpan!.IntrinsicAttributes["guid"], processRequestSpan!.IntrinsicAttributes["parentId"]),
138+
() => Assert.True(consumeSpan!.IntrinsicAttributes.ContainsKey("traceId"))
139+
);
140+
141+
if (_initCollections)
142+
{
143+
// verify that distributed trace worked as expected -- the last produce span should have the same traceId and parentId as the last consume span
144+
var processRequestSpan = spans.LastOrDefault(s => s.IntrinsicAttributes["name"].Equals("OtherTransaction/Custom/AwsSdkTestApp.SQSBackgroundService.SQSReceiverService/ProcessRequestAsync") && s.IntrinsicAttributes.ContainsKey("parentId"));
145+
146+
NrAssert.Multiple(
147+
() => Assert.True(processRequestSpan != null, "processRequestSpan should not be null"),
148+
() => Assert.Equal(produceSpan!.IntrinsicAttributes["traceId"], consumeSpan!.IntrinsicAttributes["traceId"]),
149+
() => Assert.Equal(produceSpan!.IntrinsicAttributes["guid"], processRequestSpan!.IntrinsicAttributes["parentId"])
150+
);
151+
}
152+
153+
NrAssert.Multiple(
124154
// entity relationship attributes
125155
() => Assert.Equal(produceSpan!.AgentAttributes["messaging.system"], "aws_sqs"),
126156
() => Assert.Equal(produceSpan!.AgentAttributes["messaging.destination.name"], _testQueueName2),
@@ -133,3 +163,17 @@ public void Test()
133163
);
134164
}
135165
}
166+
167+
public class AwsSdkSQSTestInitializedCollections : AwsSdkSQSTestBase
168+
{
169+
public AwsSdkSQSTestInitializedCollections(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper output) : base(fixture, output, true)
170+
{
171+
}
172+
}
173+
public class AwsSdkSQSTestNullCollections : AwsSdkSQSTestBase
174+
{
175+
public AwsSdkSQSTestNullCollections(AwsSdkContainerSQSTestFixture fixture, ITestOutputHelper output) : base(fixture, output, false)
176+
{
177+
}
178+
}
179+

tests/Agent/IntegrationTests/ContainerIntegrationTests/Tests/LinuxContainerTests.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,13 @@ public CentosX64ContainerTest(CentosX64ContainerTestFixture fixture, ITestOutput
8989
}
9090
}
9191

92-
public class CentosArm64ContainerTest : LinuxContainerTest<CentosArm64ContainerTestFixture>
93-
{
94-
public CentosArm64ContainerTest(CentosArm64ContainerTestFixture fixture, ITestOutputHelper output) : base(fixture, output)
95-
{
96-
}
97-
}
92+
// temporarily disabled until the checksum issue is resolved
93+
// public class CentosArm64ContainerTest : LinuxContainerTest<CentosArm64ContainerTestFixture>
94+
// {
95+
// public CentosArm64ContainerTest(CentosArm64ContainerTestFixture fixture, ITestOutputHelper output) : base(fixture, output)
96+
// {
97+
// }
98+
// }
9899

99100
public class AmazonX64ContainerTest : LinuxContainerTest<AmazonX64ContainerTestFixture>
100101
{

0 commit comments

Comments
 (0)