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

Lambda #1998

Merged
merged 3 commits into from
Aug 22, 2024
Merged

Lambda #1998

Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,16 @@ public interface CollectionFactory {
* @param <V> the type of value stored/returned
*/
<K, V> Function<K, V> memorize(Function<K, V> loader, int maxSize);

/**
* Create a time based eviction cache in which an entry's age is determined on a last-access basis.
*
* @param <K> key type
* @param <V> cached type
* @param ageInSeconds how old, in seconds, a cache entry must be to be evicted after last access
* @param initialCapacity the initial capacity of the cache
* @param loader the function to calculate the value for a key, used if the key is not cached
* @return a time based concurrent cache
*/
<K, V> Function<K, V> createAccessTimeBasedCache(long ageInSeconds, int initialCapacity, Function<K, V> loader);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ public <K, V> Function<K, V> memorize(Function<K, V> loader, int maxSize) {
return loader.apply(k1);
});
}

/**
* Note: In this implementation, this method will return the loader function as is.
*/
@Override
public <K, V> Function<K, V> createAccessTimeBasedCache(long ageInSeconds, int initialCapacity, Function<K, V> loader) {
return loader;
}
}
20 changes: 20 additions & 0 deletions instrumentation/aws-java-sdk-lambda-1.11.280/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.aws-java-sdk-lambda-1.11.280' }
}

dependencies {
implementation(project(":agent-bridge"))
implementation(project(":agent-bridge-datastore"))

implementation("com.amazonaws:aws-java-sdk-lambda:1.12.763")
}

verifyInstrumentation {
// not using passesOnly to decrease the number of artifacts this is tested against
passes 'com.amazonaws:aws-java-sdk-lambda:[1.11.280,)'
}

site {
title 'AWS Lambda'
type 'Framework'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.agent.instrumentation.awsjavasdk1.services.lambda;

/**
* Function data extracted from the request and config.
*/
class FunctionProcessedData {
private final String functionName;
private final String arn;

public FunctionProcessedData(String functionName, String arn) {
this.functionName = functionName;
this.arn = arn;
}

public String getFunctionName() {
return functionName;
}

public String getArn() {
return arn;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.agent.instrumentation.awsjavasdk1.services.lambda;

import java.util.Objects;

/**
* Data necessary to calculate the ARN. This class is used as the key for the ARN cache.
*/
public class FunctionRawData {
private final String functionRef;
private final String qualifier;
private final String region;

public FunctionRawData(String functionRef, String qualifier, String region) {
this.functionRef = functionRef;
this.qualifier = qualifier;
this.region = region;
}

public String getFunctionRef() {
return functionRef;
}

public String getQualifier() {
return qualifier;
}

public String getRegion() {
return region;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof FunctionRawData)) {
return false;
}
FunctionRawData that = (FunctionRawData) o;
return Objects.equals(functionRef, that.functionRef) && Objects.equals(qualifier, that.qualifier) &&
Objects.equals(region, that.region);
}

@Override
public int hashCode() {
return Objects.hash(functionRef, qualifier, region);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.agent.instrumentation.awsjavasdk1.services.lambda;

import com.newrelic.agent.bridge.AgentBridge;
import com.newrelic.api.agent.CloudParameters;

import java.util.function.Function;

public class LambdaUtil {

private static final String PLATFORM = "aws_lambda";
private static final String NULL_ARN = "";
private static final String PREFIX = "arn:aws:lambda:";
private static final Function<FunctionRawData, FunctionProcessedData> CACHE =
AgentBridge.collectionFactory.createAccessTimeBasedCache(3600, 8, LambdaUtil::processData);

public static CloudParameters getCloudParameters(FunctionRawData functionRawData) {
FunctionProcessedData data = CACHE.apply(functionRawData);
String arn = data.getArn();
CloudParameters.ResourceIdParameter cloudParameters = CloudParameters.provider(PLATFORM);
// the cache will always return the NULL_ARN when it is not possible to calculate the ARN
// so saving a few cycles by using != instead of equals.
if (arn != NULL_ARN) {
cloudParameters.resourceId(arn);
}

return cloudParameters.build();
}

/**
* <p>
* Calculates the simple function name and ARN given
* the function name, qualifier, and possibly region (provided by config).
* </p>
* <p>
* Aliases are returned as part of the ARN, but versions are removed
* because they would make it harder to link to Lambdas/Alias entities.
* </p>
* <p>
* If qualifiers are provided both in the function ref, and as a qualifier, the one in function ref "wins".
* If they differ, the LambdaClient will throw an exception.
* </p>
*
* @return a FunctionProcessedData object with the function name and ARN.
* If any of its values cannot be calculated, it will be the NULL_ARN.
*/
// Visible for testing
static FunctionProcessedData processData(FunctionRawData data) {
String functionRef = data.getFunctionRef();

String[] parts = functionRef.split(":");

String functionName = NULL_ARN;
String arn = NULL_ARN;

if (parts.length == 1) {
// function ref is only function name
// does not have the account id, so cannot assemble the ARN.
functionName = functionRef;
} else if (parts.length == 2) {
// function ref is only function name with alias/version
// does not have the account id, so cannot assemble the ARN.
functionName = parts[0];
} else if (parts.length == 3) {
// partial ARN: {account-id}:function:{function-name}
functionName = parts[2];
String qualifier = data.getQualifier();
if (qualifier == null) {
arn = PREFIX + data.getRegion() + ":" + functionRef;
} else {
arn = PREFIX + data.getRegion() + ":" + functionRef + ":" + qualifier;
}
} else if (parts.length == 4) {
// partial ARN with qualifier: {account-id}:function:{function-name}:{qualifier}
functionName = parts[2];
arn = PREFIX + data.getRegion() + ":" + functionRef;
} else if (parts.length == 7) {
// full ARN: arn:aws:lambda:{region}:{account-id}:function:{function-name}
functionName = parts[6];
String qualifier = data.getQualifier();
if (qualifier == null) {
arn = functionRef;
} else {
arn = functionRef + ":" + qualifier;
}
} else if (parts.length == 8) {
// full ARN with qualifier: arn:aws:lambda:{region}:{account-id}:function:{function-name}:{qualifier}
functionName = parts[6];
arn = functionRef;
}
// reference should be invalid if the number of parts do not match any of the expected cases

return new FunctionProcessedData(functionName, arn);
}


public static String getSimpleFunctionName(FunctionRawData functionRawData) {
return CACHE.apply(functionRawData).getFunctionName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.amazonaws.services.lambda;

import com.agent.instrumentation.awsjavasdk1.services.lambda.FunctionRawData;
import com.agent.instrumentation.awsjavasdk1.services.lambda.LambdaUtil;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.newrelic.api.agent.CloudParameters;
import com.newrelic.api.agent.NewRelic;
import com.newrelic.api.agent.Segment;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

import java.util.concurrent.Future;

@Weave(type = MatchType.ExactClass, originalName = "com.amazonaws.services.lambda.AWSLambdaAsyncClient")
public abstract class AWSLambdaAsyncClient_Instrumentation {

protected abstract String getSigningRegion();

public Future<InvokeResult> invokeAsync(final InvokeRequest request, AsyncHandler<InvokeRequest, InvokeResult> asyncHandler) {
FunctionRawData functionRawData = new FunctionRawData(request.getFunctionName(), request.getQualifier(), getSigningRegion());
CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData);
String functionName = LambdaUtil.getSimpleFunctionName(functionRawData);
Segment segment = NewRelic.getAgent().getTransaction().startSegment("Lambda", "invoke/" + functionName);

try {
segment.reportAsExternal(cloudParameters);
asyncHandler = new SegmentEndingAsyncHandler(asyncHandler, segment);
return Weaver.callOriginal();
} catch (Throwable t) {
segment.end();
throw t;
}
}

private static class SegmentEndingAsyncHandler implements AsyncHandler<InvokeRequest, InvokeResult> {
private final AsyncHandler<InvokeRequest, InvokeResult> originalHandler;
private final Segment segment;

public SegmentEndingAsyncHandler(
AsyncHandler<InvokeRequest, InvokeResult> asyncHandler, Segment segment) {
this.segment = segment;
this.originalHandler = asyncHandler;
}

@Override
public void onError(Exception exception) {
segment.end();
if (originalHandler != null) {
originalHandler.onError(exception);
}
}

@Override
public void onSuccess(InvokeRequest request, InvokeResult invokeResult) {
segment.end();
if (originalHandler != null) {
originalHandler.onSuccess(request, invokeResult);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.amazonaws.services.lambda;

import com.agent.instrumentation.awsjavasdk1.services.lambda.FunctionRawData;
import com.agent.instrumentation.awsjavasdk1.services.lambda.LambdaUtil;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.newrelic.api.agent.CloudParameters;
import com.newrelic.api.agent.NewRelic;
import com.newrelic.api.agent.Trace;
import com.newrelic.api.agent.TracedMethod;
import com.newrelic.api.agent.weaver.CatchAndLog;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

@Weave(type = MatchType.ExactClass, originalName = "com.amazonaws.services.lambda.AWSLambdaClient")
public abstract class AWSLambdaClient_Instrumentation {

abstract protected String getSigningRegion();

@Trace(leaf = true)
public InvokeResult invoke(InvokeRequest invokeRequest) {
FunctionRawData functionRawData = new FunctionRawData(invokeRequest.getFunctionName(), invokeRequest.getQualifier(), getSigningRegion());
CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData);
TracedMethod tracedMethod = NewRelic.getAgent().getTracedMethod();
tracedMethod.reportAsExternal(cloudParameters);
tracedMethod.setMetricName("Lambda", "invoke", LambdaUtil.getSimpleFunctionName(functionRawData));
return Weaver.callOriginal();
}

}

Loading
Loading