Skip to content

Commit

Permalink
feat: Instrument Lambda invocations in AWS SDK (#2901)
Browse files Browse the repository at this point in the history
  • Loading branch information
chynesNR authored Jan 15, 2025
1 parent bd249b9 commit 3b655e4
Show file tree
Hide file tree
Showing 34 changed files with 1,109 additions and 26 deletions.
1 change: 1 addition & 0 deletions .github/workflows/all_solutions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ jobs:
AwsLambda.Sns,
AwsLambda.Sqs,
AwsLambda.WebRequest,
AwsSdk,
AzureFunction,
BasicInstrumentation,
CatInbound,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ private void CollectOneTimeMetrics()
ReportIfLoggingDisabled();
ReportIfInstrumentationIsDisabled();
ReportIfGCSamplerV2IsEnabled();
ReportIfAwsAccountIdProvided();
}

public void CollectMetrics()
Expand Down Expand Up @@ -847,8 +848,14 @@ private void ReportIfGCSamplerV2IsEnabled()
{
ReportSupportabilityCountMetric(MetricNames.SupportabilityGCSamplerV2Enabled);
}

}

private void ReportIfAwsAccountIdProvided()
{
if (!string.IsNullOrEmpty(_configuration.AwsAccountId))
{
ReportSupportabilityCountMetric(MetricNames.SupportabilityAwsAccountIdProvided);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public interface IAttributeDefinitions

AttributeDefinition<object, object> GetLambdaAttribute(string name);
AttributeDefinition<object, object> GetFaasAttribute(string name);
AttributeDefinition<object, object> GetCloudSdkAttribute(string name);

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

Expand Down Expand Up @@ -190,6 +191,7 @@ public AttributeDefinitions(IAttributeFilter attribFilter)
private readonly ConcurrentDictionary<string, AttributeDefinition<string, string>> _requestHeadersAttributes = new ConcurrentDictionary<string, AttributeDefinition<string, string>>();
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _lambdaAttributes = new ConcurrentDictionary<string, AttributeDefinition<object, object>>();
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _faasAttributes = new();
private readonly ConcurrentDictionary<string, AttributeDefinition<object, object>> _cloudSdkAttributes = new();

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

Expand Down Expand Up @@ -281,6 +283,20 @@ public AttributeDefinition<object, object> GetFaasAttribute(string name)
}


private AttributeDefinition<object, object> CreateCloudSdkAttribute(string attribName)
{
return AttributeDefinitionBuilder
.Create<object, object>(attribName, AttributeClassification.AgentAttributes)
.AppliesTo(AttributeDestinations.TransactionTrace)
.AppliesTo(AttributeDestinations.SpanEvent)
.WithConvert(x => x)
.Build(_attribFilter);
}

public AttributeDefinition<object, object> GetCloudSdkAttribute(string name)
{
return _cloudSdkAttributes.GetOrAdd(name, CreateCloudSdkAttribute);
}
public AttributeDefinition<object, object> GetCustomAttributeForTransaction(string name)
{
return _trxCustomAttributes.GetOrAdd(name, CreateCustomAttributeForTransaction);
Expand Down
1 change: 1 addition & 0 deletions src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
23 changes: 18 additions & 5 deletions src/Agent/NewRelic/Agent/Core/Segments/Segment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -434,21 +434,34 @@ 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);

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;
Expand Down
5 changes: 5 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,5 +331,10 @@ public void AddFaasAttribute(string name, object value)
{
return;
}

public void AddCloudSdkAttribute(string name, object value)
{
return;
}
}
}
4 changes: 2 additions & 2 deletions src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface ISpan
{
ISpan AddCustomAttribute(string key, object value);

ISpan AddCloudSdkAttribute(string key, object value);

ISpan SetName(string name);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading

0 comments on commit 3b655e4

Please sign in to comment.