Skip to content

Commit 3b655e4

Browse files
authored
feat: Instrument Lambda invocations in AWS SDK (#2901)
1 parent bd249b9 commit 3b655e4

File tree

34 files changed

+1109
-26
lines changed

34 files changed

+1109
-26
lines changed

.github/workflows/all_solutions.yml

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ jobs:
254254
AwsLambda.Sns,
255255
AwsLambda.Sqs,
256256
AwsLambda.WebRequest,
257+
AwsSdk,
257258
AzureFunction,
258259
BasicInstrumentation,
259260
CatInbound,

src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ private void CollectOneTimeMetrics()
685685
ReportIfLoggingDisabled();
686686
ReportIfInstrumentationIsDisabled();
687687
ReportIfGCSamplerV2IsEnabled();
688+
ReportIfAwsAccountIdProvided();
688689
}
689690

690691
public void CollectMetrics()
@@ -847,8 +848,14 @@ private void ReportIfGCSamplerV2IsEnabled()
847848
{
848849
ReportSupportabilityCountMetric(MetricNames.SupportabilityGCSamplerV2Enabled);
849850
}
850-
851851
}
852852

853+
private void ReportIfAwsAccountIdProvided()
854+
{
855+
if (!string.IsNullOrEmpty(_configuration.AwsAccountId))
856+
{
857+
ReportSupportabilityCountMetric(MetricNames.SupportabilityAwsAccountIdProvided);
858+
}
859+
}
853860
}
854861
}

src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs

+16
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public interface IAttributeDefinitions
126126

127127
AttributeDefinition<object, object> GetLambdaAttribute(string name);
128128
AttributeDefinition<object, object> GetFaasAttribute(string name);
129+
AttributeDefinition<object, object> GetCloudSdkAttribute(string name);
129130

130131
AttributeDefinition<string, string> GetRequestParameterAttribute(string paramName);
131132

@@ -190,6 +191,7 @@ public AttributeDefinitions(IAttributeFilter attribFilter)
190191
private readonly ConcurrentDictionary<string, AttributeDefinition<string, string>> _requestHeadersAttributes = new ConcurrentDictionary<string, AttributeDefinition<string, string>>();
191192
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _lambdaAttributes = new ConcurrentDictionary<string, AttributeDefinition<object, object>>();
192193
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _faasAttributes = new();
194+
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _cloudSdkAttributes = new();
193195

194196
private readonly ConcurrentDictionary<TypeAttributeValue, AttributeDefinition<TypeAttributeValue, string>> _typeAttributes = new ConcurrentDictionary<TypeAttributeValue, AttributeDefinition<TypeAttributeValue, string>>();
195197

@@ -281,6 +283,20 @@ public AttributeDefinition<object, object> GetFaasAttribute(string name)
281283
}
282284

283285

286+
private AttributeDefinition<object, object> CreateCloudSdkAttribute(string attribName)
287+
{
288+
return AttributeDefinitionBuilder
289+
.Create<object, object>(attribName, AttributeClassification.AgentAttributes)
290+
.AppliesTo(AttributeDestinations.TransactionTrace)
291+
.AppliesTo(AttributeDestinations.SpanEvent)
292+
.WithConvert(x => x)
293+
.Build(_attribFilter);
294+
}
295+
296+
public AttributeDefinition<object, object> GetCloudSdkAttribute(string name)
297+
{
298+
return _cloudSdkAttributes.GetOrAdd(name, CreateCloudSdkAttribute);
299+
}
284300
public AttributeDefinition<object, object> GetCustomAttributeForTransaction(string name)
285301
{
286302
return _trxCustomAttributes.GetOrAdd(name, CreateCustomAttributeForTransaction);

src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs

+1
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,7 @@ public static string GetSupportabilityInstallType(string installType)
838838

839839
public const string SupportabilityIgnoredInstrumentation = SupportabilityDotnetPs + "IgnoredInstrumentation";
840840
public const string SupportabilityGCSamplerV2Enabled = SupportabilityDotnetPs + "GCSamplerV2/Enabled";
841+
public const string SupportabilityAwsAccountIdProvided = SupportabilityDotnetPs + "AwsAccountId/Config";
841842

842843
#endregion Supportability
843844

src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public ISpan AddCustomAttribute(string key, object value)
6161
{
6262
return this;
6363
}
64+
public ISpan AddCloudSdkAttribute(string key, object value)
65+
{
66+
return this;
67+
}
6468

6569
public ISpan SetName(string name)
6670
{

src/Agent/NewRelic/Agent/Core/Segments/Segment.cs

+18-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class Segment : IInternalSpan, ISegmentDataState
3838
public IAttributeDefinitions AttribDefs => _transactionSegmentState.AttribDefs;
3939
public string TypeName => MethodCallData.TypeName;
4040

41-
private SpanAttributeValueCollection _customAttribValues;
41+
private SpanAttributeValueCollection _attribValues;
4242

4343
public Segment(ITransactionSegmentState transactionSegmentState, MethodCallData methodCallData)
4444
{
@@ -318,7 +318,7 @@ public TimeSpan ExclusiveDurationOrZero
318318

319319
public SpanAttributeValueCollection GetAttributeValues()
320320
{
321-
var attribValues = _customAttribValues ?? new SpanAttributeValueCollection();
321+
var attribValues = _attribValues ?? new SpanAttributeValueCollection();
322322

323323
AttribDefs.Duration.TrySetValue(attribValues, DurationOrZero);
324324
AttribDefs.NameForSpan.TrySetValue(attribValues, GetTransactionTraceName());
@@ -434,21 +434,34 @@ public ISegmentExperimental MakeLeaf()
434434
return this;
435435
}
436436

437-
private readonly object _customAttribValuesSyncRoot = new object();
437+
private readonly object _attribValuesSyncRoot = new object();
438438

439439
public ISpan AddCustomAttribute(string key, object value)
440440
{
441441
SpanAttributeValueCollection customAttribValues;
442-
lock (_customAttribValuesSyncRoot)
442+
lock (_attribValuesSyncRoot)
443443
{
444-
customAttribValues = _customAttribValues ?? (_customAttribValues = new SpanAttributeValueCollection());
444+
customAttribValues = _attribValues ?? (_attribValues = new SpanAttributeValueCollection());
445445
}
446446

447447
AttribDefs.GetCustomAttributeForSpan(key).TrySetValue(customAttribValues, value);
448448

449449
return this;
450450
}
451451

452+
public ISpan AddCloudSdkAttribute(string key, object value)
453+
{
454+
SpanAttributeValueCollection attribValues;
455+
lock (_attribValuesSyncRoot)
456+
{
457+
attribValues = _attribValues ?? (_attribValues = new SpanAttributeValueCollection());
458+
}
459+
460+
AttribDefs.GetCloudSdkAttribute(key).TrySetValue(attribValues, value);
461+
462+
return this;
463+
}
464+
452465
public ISpan SetName(string name)
453466
{
454467
SegmentNameOverride = name;

src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs

+5
Original file line numberDiff line numberDiff line change
@@ -331,5 +331,10 @@ public void AddFaasAttribute(string name, object value)
331331
{
332332
return;
333333
}
334+
335+
public void AddCloudSdkAttribute(string name, object value)
336+
{
337+
return;
338+
}
334339
}
335340
}

src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1374,7 +1374,7 @@ public void AddLambdaAttribute(string name, object value)
13741374
{
13751375
if (string.IsNullOrWhiteSpace(name))
13761376
{
1377-
Log.Debug($"AddLambdaAttribute - Unable to set Lambda value on transaction because the key is null/empty");
1377+
Log.Debug($"AddLambdaAttribute - Name cannot be null/empty");
13781378
return;
13791379
}
13801380

@@ -1386,7 +1386,7 @@ public void AddFaasAttribute(string name, object value)
13861386
{
13871387
if (string.IsNullOrWhiteSpace(name))
13881388
{
1389-
Log.Debug($"AddFaasAttribute - Unable to set FaaS value on transaction because the key is null/empty");
1389+
Log.Debug($"AddFaasAttribute - Name cannot be null/empty");
13901390
return;
13911391
}
13921392

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ISpan.cs

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public interface ISpan
1111
{
1212
ISpan AddCustomAttribute(string key, object value);
1313

14+
ISpan AddCloudSdkAttribute(string key, object value);
15+
1416
ISpan SetName(string name);
1517
}
1618
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Linq;
5+
using System.Text.RegularExpressions;
6+
7+
namespace NewRelic.Agent.Extensions.AwsSdk
8+
{
9+
public class ArnBuilder
10+
{
11+
public readonly string Partition;
12+
public readonly string Region;
13+
public readonly string AccountId;
14+
15+
public ArnBuilder(string partition, string region, string accountId)
16+
{
17+
Partition = string.IsNullOrEmpty(partition) ? "aws" : partition;
18+
Region = string.IsNullOrEmpty(region) ? "(unknown)" : region;
19+
AccountId = accountId ?? "";
20+
}
21+
22+
public string Build(string service, string resource) => ConstructArn(Partition, service, Region, AccountId, resource);
23+
24+
// This is the full regex pattern for a Lambda ARN:
25+
// (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-_]+))?
26+
27+
// If it's a full ARN, it has to start with 'arn:'
28+
// A partial ARN can contain up to 5 segments separated by ':'
29+
// 1. Region
30+
// 2. Account ID
31+
// 3. 'function' (fixed string)
32+
// 4. Function name
33+
// 5. Alias or version
34+
// Only the function name is required, the rest are all optional. e.g. you could have region and function name and nothing else
35+
public string BuildFromPartialLambdaArn(string invocationName)
36+
{
37+
if (invocationName.StartsWith("arn:"))
38+
{
39+
return invocationName;
40+
}
41+
var segments = invocationName.Split(':');
42+
string functionName = null;
43+
string alias = null;
44+
string fallback = null;
45+
string region = null;
46+
string accountId = null;
47+
48+
// If there's only one segment, assume it's the function name
49+
if (segments.Length == 1)
50+
{
51+
functionName = segments[0];
52+
}
53+
else
54+
{
55+
// All we should need is the function name, but if we find a region or account ID, we'll use it
56+
// since it should be more accurate
57+
foreach (var segment in segments)
58+
{
59+
// A string that looks like a region or account ID could also be the function name
60+
// Assume it's the former, unless we never find a function name
61+
if (LooksLikeARegion(segment))
62+
{
63+
if (string.IsNullOrEmpty(region))
64+
{
65+
region = segment;
66+
}
67+
else
68+
{
69+
fallback = segment;
70+
}
71+
continue;
72+
}
73+
else if (LooksLikeAnAccountId(segment))
74+
{
75+
if (string.IsNullOrEmpty(accountId))
76+
{
77+
accountId = segment;
78+
}
79+
else
80+
{
81+
fallback = segment;
82+
}
83+
continue;
84+
}
85+
else if (segment == "function")
86+
{
87+
continue;
88+
}
89+
else if (functionName == null)
90+
{
91+
functionName = segment;
92+
}
93+
else if (alias == null)
94+
{
95+
alias = segment;
96+
}
97+
else
98+
{
99+
return null;
100+
}
101+
}
102+
}
103+
104+
if (string.IsNullOrEmpty(functionName))
105+
{
106+
if (!string.IsNullOrEmpty(fallback))
107+
{
108+
functionName = fallback;
109+
}
110+
else
111+
{
112+
return null;
113+
}
114+
}
115+
116+
accountId = !string.IsNullOrEmpty(accountId) ? accountId : AccountId;
117+
if (string.IsNullOrEmpty(accountId))
118+
{
119+
return null;
120+
}
121+
122+
// The member Region cannot be blank (it has a default) so we don't need to check it here
123+
region = !string.IsNullOrEmpty(region) ? region : Region;
124+
125+
if (!string.IsNullOrEmpty(alias))
126+
{
127+
functionName += $":{alias}";
128+
}
129+
return ConstructArn(Partition, "lambda", region, accountId, $"function:{functionName}");
130+
}
131+
132+
public override string ToString()
133+
{
134+
string idPresent = string.IsNullOrEmpty(AccountId) ? "[Missing]" : "[Present]";
135+
136+
return $"Partition: {Partition}, Region: {Region}, AccountId: {idPresent}";
137+
}
138+
139+
private static Regex RegionRegex = new Regex(@"^[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}$", RegexOptions.Compiled);
140+
private static bool LooksLikeARegion(string text) => RegionRegex.IsMatch(text);
141+
private static bool LooksLikeAnAccountId(string text) => (text.Length == 12) && text.All(c => c >= '0' && c <= '9');
142+
143+
private string ConstructArn(string partition, string service, string region, string accountId, string resource)
144+
{
145+
if (string.IsNullOrEmpty(partition) || string.IsNullOrEmpty(region) || string.IsNullOrEmpty(accountId)
146+
|| string.IsNullOrEmpty(service) || string.IsNullOrEmpty(resource))
147+
{
148+
return null;
149+
}
150+
return "arn:" + partition + ":" + service + ":" + region + ":" + accountId + ":" + resource;
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)