Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Instrument Lambda invocations in AWS SDK #2901

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4434a8c
First pass at instrumentation
chynesNR Sep 26, 2024
5475b49
First pass at integration tests
chynesNR Sep 30, 2024
3d213e6
Update ARN parsing logic
chynesNR Oct 8, 2024
7c33292
Updating ARN parsing logic
chynesNR Oct 8, 2024
c15328c
Fixes to integration tests
chynesNR Oct 9, 2024
8728cff
Forgot to add tests to workflow
chynesNR Oct 9, 2024
cdb0bf3
Don't generate a segment for the HTTP request
chynesNR Oct 14, 2024
10a5e4b
Fix some typos
chynesNR Oct 14, 2024
71304dc
Merge branch 'main' into feature/invoke-lambda-instrumentation
chynesNR Oct 14, 2024
db2202c
Cache constructed ARNs
chynesNR Oct 15, 2024
a14adbb
PR feedback
chynesNR Oct 15, 2024
28a5ebe
Fix unit test
chynesNR Oct 16, 2024
c673718
Merge branch 'main' into feature/invoke-lambda-instrumentation
chynesNR Oct 25, 2024
4340181
First pass at incorporating account ID
chynesNR Oct 28, 2024
2d1e5c0
First pass at ArnBuilder
chynesNR Nov 11, 2024
eb4a73e
Another pass at generic ARN construction
chynesNR Nov 15, 2024
35dad1c
Increasing code coverage
chynesNR Nov 15, 2024
d811849
Add supportability metric
chynesNR Nov 16, 2024
d47b5a4
Some cleanup and better logging
chynesNR Nov 25, 2024
00d2251
Merge branch 'main' into feature/invoke-lambda-instrumentation
chynesNR Nov 25, 2024
f229fc1
More logging improvements
chynesNR Nov 25, 2024
8af3e95
Fix bad merge
chynesNR Nov 25, 2024
272ace4
Fixing more merge errors
chynesNR Nov 26, 2024
0b91f6a
Trying to address CodeQL and CodeCov issues
chynesNR Nov 26, 2024
17f810f
Only pass on expected error
chynesNR Nov 26, 2024
614270c
Merge branch 'main' into feature/invoke-lambda-instrumentation
tippmar-nr Dec 2, 2024
641eafc
Revert change (expected error is not consistent)
chynesNR Dec 3, 2024
d107619
Merge branch 'feature/invoke-lambda-instrumentation' of https://githu…
chynesNR Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/all_solutions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,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,19 @@ public AttributeDefinition<object, object> GetFaasAttribute(string name)
}


private AttributeDefinition<object, object> CreateCloudSdkAttribute(string attribName)
tippmar-nr marked this conversation as resolved.
Show resolved Hide resolved
{
return AttributeDefinitionBuilder
.Create<object, object>(attribName, AttributeClassification.AgentAttributes)
.AppliesTo(AttributeDestinations.TransactionTrace)
.AppliesTo(AttributeDestinations.SpanEvent)
.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
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;
}
}
}
16 changes: 14 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,12 +1386,24 @@ 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;
}

var faasAttrib = _attribDefs.GetFaasAttribute(name);
TransactionMetadata.UserAndRequestAttributes.TrySetValue(faasAttrib, value);
}

public void AddCloudSdkAttribute(string name, object value)
{
if (string.IsNullOrWhiteSpace(name))
{
Log.Debug($"AddCloudSdkAttribute - Name cannot be null/empty");
return;
}

var cloudAttrib = _attribDefs.GetCloudSdkAttribute(name);
TransactionMetadata.UserAndRequestAttributes.TrySetValue(cloudAttrib, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,5 +315,7 @@ ISegment StartMessageBrokerSegment(MethodCall methodCall, MessageBrokerDestinati
void AddLambdaAttribute(string name, object value);

void AddFaasAttribute(string name, object value);

void AddCloudSdkAttribute(string name, object value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Linq;
using System.Text.RegularExpressions;
using NewRelic.Agent.Extensions.Collections;

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 = partition ?? "";
Region = 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;
}

region = !string.IsNullOrEmpty(region) ? region : Region;
if (string.IsNullOrEmpty(region))
{
return null;
}

if (!string.IsNullOrEmpty(alias))
{
functionName += $":{alias}";
}
return ConstructArn(Partition, "lambda", region, accountId, $"function:{functionName}");
}

public override string ToString()
{
string partition = string.IsNullOrEmpty(Partition) ? "[Missing]" : Partition;
string region = string.IsNullOrEmpty(Region) ? "[Missing]" : Region;
string idPresent = string.IsNullOrEmpty(AccountId) ? "[Missing]" : "[Present]";

return $"Partition: {partition}, Region: {region}, AccountId: {idPresent}";
}

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;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ public void Add(T item)
_hashSet.Add(item);
}
}

public bool TryAdd(T item)
tippmar-nr marked this conversation as resolved.
Show resolved Hide resolved
{
using (_writeLock())
{
return _hashSet.Add(item);
}
}
public void Clear()
{
using (_writeLock())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading