diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/CollectionFactory.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/CollectionFactory.java index 378905c034..33d604e00b 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/CollectionFactory.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/CollectionFactory.java @@ -49,4 +49,16 @@ public interface CollectionFactory { * @param the type of value stored/returned */ Function memorize(Function loader, int maxSize); + + /** + * Create a time based eviction cache in which an entry's age is determined on a last-access basis. + * + * @param key type + * @param 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 + */ + Function createAccessTimeBasedCache(long ageInSeconds, int initialCapacity, Function loader); } diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/DefaultCollectionFactory.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/DefaultCollectionFactory.java index f42329d028..97e1521eda 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/DefaultCollectionFactory.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/DefaultCollectionFactory.java @@ -43,4 +43,12 @@ public Function memorize(Function loader, int maxSize) { return loader.apply(k1); }); } + + /** + * Note: In this implementation, this method will return the loader function as is. + */ + @Override + public Function createAccessTimeBasedCache(long ageInSeconds, int initialCapacity, Function loader) { + return loader; + } } diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/build.gradle b/instrumentation/aws-java-sdk-lambda-1.11.280/build.gradle new file mode 100644 index 0000000000..464c45fed6 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/build.gradle @@ -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' +} diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionProcessedData.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionProcessedData.java new file mode 100644 index 0000000000..a51bdf1f17 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionProcessedData.java @@ -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; + } +} diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionRawData.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionRawData.java new file mode 100644 index 0000000000..604a3a0673 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/FunctionRawData.java @@ -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); + } +} diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtil.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtil.java new file mode 100644 index 0000000000..44cd29c058 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtil.java @@ -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 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(); + } + + /** + *

+ * Calculates the simple function name and ARN given + * the function name, qualifier, and possibly region (provided by config). + *

+ *

+ * Aliases are returned as part of the ARN, but versions are removed + * because they would make it harder to link to Lambdas/Alias entities. + *

+ *

+ * 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. + *

+ * + * @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(); + } +} diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaAsyncClient_Instrumentation.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaAsyncClient_Instrumentation.java new file mode 100644 index 0000000000..047bec36f6 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaAsyncClient_Instrumentation.java @@ -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 invokeAsync(final InvokeRequest request, AsyncHandler 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 { + private final AsyncHandler originalHandler; + private final Segment segment; + + public SegmentEndingAsyncHandler( + AsyncHandler 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); + } + } + } +} diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaClient_Instrumentation.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaClient_Instrumentation.java new file mode 100644 index 0000000000..23245e49d4 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/main/java/com/amazonaws/services/lambda/AWSLambdaClient_Instrumentation.java @@ -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(); + } + +} + diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtilTest.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtilTest.java new file mode 100644 index 0000000000..d3a8302f95 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/agent/instrumentation/awsjavasdk1/services/lambda/LambdaUtilTest.java @@ -0,0 +1,164 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.awsjavasdk1.services.lambda; + +import com.amazonaws.regions.Regions; +import com.amazonaws.services.lambda.model.InvokeRequest; +import com.newrelic.api.agent.CloudParameters; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class LambdaUtilTest { + + @Test + public void testGetCloudParamFunctionName() { + FunctionRawData functionRawData = new FunctionRawData("my-function", null, getRegion()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertNull(cloudParameters.getResourceId()); + } + + @Test + public void testGetCloudParamPartialArn() { + FunctionRawData functionRawData = new FunctionRawData("123456789012:function:my-function", null, getRegion()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", cloudParameters.getResourceId()); + } + + @Test + public void testGetCloudParamArnQualifier() { + FunctionRawData functionRawData = new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "alias", getRegion()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", cloudParameters.getResourceId()); + } + + @Test + public void testGetArnFunctionName() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("my-function", null, getRegion())); + assertEquals("", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameWithAlias() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("my-function:alias", null, getRegion())); + assertEquals("", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameWithVersion() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("my-function:123", null, getRegion())); + assertEquals("", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameAndAliasQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("my-function", "alias", getRegion())); + assertEquals("", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameAndVersionQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("my-function", "123", getRegion())); + assertEquals("", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnPartialArn() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnPartialArnWithAlias() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function:alias", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnPartialArnWithVersion() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function:123", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnPartialArnAndAliasQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", "alias", getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnPartialArnAndVersionQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", "123", getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFullArn() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFullArnWithAlias() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFullArnWithVersion() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", null, getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFullArnAndAliasQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "alias", getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnFullArnAndVersionQualifier() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "123", getRegion())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + @Test + public void testGetArnDifferentRegion() { + FunctionProcessedData data = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-west-2:123456789012:function:my-function", null, getRegion())); + assertEquals("arn:aws:lambda:us-west-2:123456789012:function:my-function", data.getArn()); + assertEquals("my-function", data.getFunctionName()); + } + + public String getRegion() { + return Regions.US_EAST_1.getName(); + } + +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java new file mode 100644 index 0000000000..8c8ae40b8d --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java @@ -0,0 +1,102 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.amazonaws.services.lambda; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.lambda.model.InvokeRequest; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.agent.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"com.amazonaws"}, configName = "dt_enabled.yml") +public class DefaultLambdaAsyncClient_InstrumentationTest { + + public AWSLambdaAsync lambdaClient; + + @Rule + public HttpServerRule server = new HttpServerRule(); + + @Before + public void setup() throws URISyntaxException { + AwsClientBuilder.EndpointConfiguration endpoint = new AwsClientBuilder.EndpointConfiguration(server.getEndPoint().toString(), "us-east-1"); + lambdaClient = AWSLambdaAsyncClient.asyncBuilder() + .withEndpointConfiguration(endpoint) + .withCredentials(new CredProvider()) + .build(); + } + + @Test + public void testInvokeArn() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txn(); + SpanEvent lambdaSpan = introspector.getSpanEvents().stream() + .filter(span -> span.getName().equals("Lambda/invoke/my-function")) + .findFirst().orElse(null); + assertNotNull(lambdaSpan); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", lambdaSpan.getAgentAttributes().get("cloud.resource_id")); + + Collection transactionTraces = introspector.getTransactionTracesForTransaction( + "OtherTransaction/Custom/com.amazonaws.services.lambda.DefaultLambdaAsyncClient_InstrumentationTest/txn"); + assertEquals(1,transactionTraces.size()); + TransactionTrace transactionTrace = transactionTraces.iterator().next(); + List children = transactionTrace.getInitialTraceSegment().getChildren(); + assertEquals(1, children.size()); + TraceSegment trace = children.get(0); + assertEquals("Lambda/invoke/my-function", trace.getName()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", trace.getTracerAttributes().get("cloud.resource_id")); + } + + @Trace(dispatcher = true) + public void txn() { + InvokeRequest request = new InvokeRequest(); + request.setFunctionName("arn:aws:lambda:us-east-1:123456789012:function:my-function"); + lambdaClient.invoke(request); + } + + private static class CredProvider implements AWSCredentialsProvider { + @Override + public AWSCredentials getCredentials() { + return new AWSCredentials() { + + @Override + public String getAWSAccessKeyId() { + return "accessKeyId"; + } + + @Override + public String getAWSSecretKey() { + return "secretKey"; + } + }; + } + + @Override + public void refresh() { + + } + } +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaClient_InstrumentationTest.java b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaClient_InstrumentationTest.java new file mode 100644 index 0000000000..02584fb431 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/java/com/amazonaws/services/lambda/DefaultLambdaClient_InstrumentationTest.java @@ -0,0 +1,96 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.amazonaws.services.lambda; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.lambda.model.InvokeRequest; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.agent.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"com.amazonaws"}, configName = "dt_enabled.yml") +public class DefaultLambdaClient_InstrumentationTest { + + @Rule + public HttpServerRule server = new HttpServerRule(); + private AWSLambda lambdaClient; + + @Before + public void setup() throws URISyntaxException { + AwsClientBuilder.EndpointConfiguration endpoint = new AwsClientBuilder.EndpointConfiguration(server.getEndPoint().toString(), "us-east-1"); + lambdaClient = AWSLambdaClient.builder() + .withCredentials(new CredProvider()) + .withEndpointConfiguration(endpoint) + .build(); + } + + @Test + public void testInvokeArn() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txn(); + SpanEvent lambdaSpan = introspector.getSpanEvents().stream() + .filter(span -> span.getName().equals("Lambda/invoke/my-function")) + .findFirst().orElse(null); + assertNotNull(lambdaSpan); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", lambdaSpan.getAgentAttributes().get("cloud.resource_id")); + + Collection transactionTraces = introspector.getTransactionTracesForTransaction( + "OtherTransaction/Custom/com.amazonaws.services.lambda.DefaultLambdaClient_InstrumentationTest/txn"); + assertEquals(1,transactionTraces.size()); + TransactionTrace transactionTrace = transactionTraces.iterator().next(); + List children = transactionTrace.getInitialTraceSegment().getChildren(); + assertEquals(1, children.size()); + TraceSegment trace = children.get(0); + assertEquals("Lambda/invoke/my-function", trace.getName()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", trace.getTracerAttributes().get("cloud.resource_id")); + } + + @Trace(dispatcher = true) + public void txn() { + InvokeRequest request = new InvokeRequest(); + request.withFunctionName("arn:aws:lambda:us-east-1:123456789012:function:my-function"); + lambdaClient.invoke(request); + } + + + private static class CredProvider implements AWSCredentialsProvider { + @Override + public AWSCredentials getCredentials() { + AWSCredentials credentials = mock(AWSCredentials.class); + when(credentials.getAWSAccessKeyId()).thenReturn("accessKeyId"); + when(credentials.getAWSSecretKey()).thenReturn("secretAccessKey"); + return credentials; + } + + @Override + public void refresh() { + + } + } +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/resources/dt_enabled.yml b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/resources/dt_enabled.yml new file mode 100644 index 0000000000..53b0968002 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-1.11.280/src/test/resources/dt_enabled.yml @@ -0,0 +1,5 @@ +common: &default_settings + distributed_tracing: + enabled: true + span_events: + enabled: true \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-2.1/build.gradle b/instrumentation/aws-java-sdk-lambda-2.1/build.gradle new file mode 100644 index 0000000000..11dd84e6ed --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/build.gradle @@ -0,0 +1,23 @@ +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.aws-java-sdk-lambda-2.1' } +} + + + +dependencies { + implementation(project(":agent-bridge")) + implementation(project(":agent-bridge-datastore")) + + implementation("software.amazon.awssdk:lambda:2.10.14") +} + +verifyInstrumentation { + passes 'software.amazon.awssdk:lambda:[2.1.0,)' + excludeRegex ".*preview.*" + exclude "software.amazon.awssdk:lambda:2.17.200" +} + +site { + title 'AWS Lambda' + type 'Framework' +} diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionProcessedData.java b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionProcessedData.java new file mode 100644 index 0000000000..88413afe2d --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionProcessedData.java @@ -0,0 +1,29 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.awsjavasdk2.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; + } +} diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionRawData.java b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionRawData.java new file mode 100644 index 0000000000..dcc7be01c5 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/FunctionRawData.java @@ -0,0 +1,61 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.awsjavasdk2.services.lambda; + +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; + +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; + // the code only cares about the region, but the config is stored + // to prevent unnecessary calls to get the region + private final SdkClientConfiguration config; + + public FunctionRawData(String functionRef, String qualifier, SdkClientConfiguration config) { + this.functionRef = functionRef; + this.qualifier = qualifier; + this.config = config; + } + + public String getFunctionRef() { + return functionRef; + } + + public String getQualifier() { + return qualifier; + } + + public String getRegion() { + return config.option(AwsClientOption.AWS_REGION).toString(); + } + + @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) && + // config uses Object.equals, so should be fast + Objects.equals(config, that.config); + } + + @Override + public int hashCode() { + return Objects.hash(functionRef, qualifier, config); + } +} diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtil.java b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtil.java new file mode 100644 index 0000000000..2fdd592fc8 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtil.java @@ -0,0 +1,105 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.awsjavasdk2.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 CACHE = + AgentBridge.collectionFactory.createAccessTimeBasedCache(3600, 8, LambdaUtil::processData); + + public static CloudParameters getCloudParameters(FunctionRawData functionRawData) { + FunctionProcessedData functionData = CACHE.apply(functionRawData); + String arn = functionData.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(); + } + + /** + *

+ * Calculates the simple function name and ARN given + * the function name, qualifier, and possibly region (provided by config). + *

+ *

+ * Aliases are returned as part of the ARN, but versions are removed + * because they would make it harder to link to Lambdas/Alias entities. + *

+ *

+ * 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. + *

+ * + * @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(); + } +} diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_Instrumentation.java b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_Instrumentation.java new file mode 100644 index 0000000000..84378b169b --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_Instrumentation.java @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package software.amazon.awssdk.services.lambda; + +import com.agent.instrumentation.awsjavasdk2.services.lambda.FunctionRawData; +import com.agent.instrumentation.awsjavasdk2.services.lambda.LambdaUtil; +import com.newrelic.api.agent.CloudParameters; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.services.lambda.model.InvokeResponse; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.logging.Level; + +@Weave(type = MatchType.ExactClass, originalName = "software.amazon.awssdk.services.lambda.DefaultLambdaAsyncClient") +final class DefaultLambdaAsyncClient_Instrumentation { + + private final SdkClientConfiguration clientConfiguration = Weaver.callOriginal(); + + public CompletableFuture invoke(InvokeRequest invokeRequest) { + FunctionRawData functionRawData = new FunctionRawData(invokeRequest.functionName(), invokeRequest.qualifier(), clientConfiguration); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + String functionName = LambdaUtil.getSimpleFunctionName(functionRawData); + Segment segment = NewRelic.getAgent().getTransaction().startSegment("Lambda", "invoke/" + functionName); + + try { + segment.reportAsExternal(cloudParameters); + CompletableFuture invokeResponseCompletableFuture = Weaver.callOriginal(); + + return invokeResponseCompletableFuture.whenComplete(new SegmentFinisher(segment)); + } catch (Throwable t) { + + segment.end(); + throw t; + } + } + + private static class SegmentFinisher implements BiConsumer { + private final Segment segment; + + public SegmentFinisher(Segment segment) { + this.segment = segment; + } + + @Override + public void accept(InvokeResponse invokeResponse, Throwable throwable) { + segment.end(); + } + } +} diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_Instrumentation.java b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_Instrumentation.java new file mode 100644 index 0000000000..44b9c80c34 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/main/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_Instrumentation.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package software.amazon.awssdk.services.lambda; + +import com.agent.instrumentation.awsjavasdk2.services.lambda.FunctionRawData; +import com.agent.instrumentation.awsjavasdk2.services.lambda.LambdaUtil; +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.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.services.lambda.model.InvokeResponse; + +@Weave(type = MatchType.ExactClass, originalName = "software.amazon.awssdk.services.lambda.DefaultLambdaClient") +final class DefaultLambdaClient_Instrumentation { + + private final SdkClientConfiguration clientConfiguration = Weaver.callOriginal(); + + @Trace(leaf = true) + public InvokeResponse invoke(InvokeRequest invokeRequest) { + FunctionRawData functionRawData = new FunctionRawData(invokeRequest.functionName(), invokeRequest.qualifier(), clientConfiguration); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + TracedMethod tracedMethod = NewRelic.getAgent().getTracedMethod(); + tracedMethod.reportAsExternal(cloudParameters); + tracedMethod.setMetricName("Lambda", "invoke", LambdaUtil.getSimpleFunctionName(functionRawData)); + return Weaver.callOriginal(); + } +} + diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtilTest.java b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtilTest.java new file mode 100644 index 0000000000..82b767b55e --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/com/agent/instrumentation/awsjavasdk2/services/lambda/LambdaUtilTest.java @@ -0,0 +1,168 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.awsjavasdk2.services.lambda; + +import com.newrelic.api.agent.CloudParameters; +import org.junit.Test; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class LambdaUtilTest { + + @Test + public void testGetCloudParamFunctionName() { + FunctionRawData functionRawData = new FunctionRawData("my-function", null, getConfig()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertNull(cloudParameters.getResourceId()); + } + + @Test + public void testGetCloudParamPartialArn() { + FunctionRawData functionRawData = new FunctionRawData("123456789012:function:my-function", null, getConfig()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", cloudParameters.getResourceId()); + } + + @Test + public void testGetCloudParamArnQualifier() { + FunctionRawData functionRawData = new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "alias", getConfig()); + CloudParameters cloudParameters = LambdaUtil.getCloudParameters(functionRawData); + assertNotNull(cloudParameters); + assertEquals("aws_lambda", cloudParameters.getPlatform()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", cloudParameters.getResourceId()); + } + + @Test + public void testGetArnFunctionName() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("my-function", null, getConfig())); + assertEquals("", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameWithAlias() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("my-function:alias", null, getConfig())); + assertEquals("", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameWithVersion() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("my-function:123", null, getConfig())); + assertEquals("", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameAndAliasQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("my-function", "alias", getConfig())); + assertEquals("", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFunctionNameAndVersionQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("my-function", "123", getConfig())); + assertEquals("", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnPartialArn() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnPartialArnWithAlias() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function:alias", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnPartialArnWithVersion() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function:123", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnPartialArnAndAliasQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", "alias", getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnPartialArnAndVersionQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("123456789012:function:my-function", "123", getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFullArn() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFullArnWithAlias() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFullArnWithVersion() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", null, getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFullArnAndAliasQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "alias", getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:alias", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnFullArnAndVersionQualifier() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-east-1:123456789012:function:my-function", "123", getConfig())); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function:123", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + @Test + public void testGetArnDifferentRegion() { + FunctionProcessedData functionProcessedData = LambdaUtil.processData(new FunctionRawData("arn:aws:lambda:us-west-2:123456789012:function:my-function", null, getConfig())); + assertEquals("arn:aws:lambda:us-west-2:123456789012:function:my-function", functionProcessedData.getArn()); + assertEquals("my-function", functionProcessedData.getFunctionName()); + } + + private SdkClientConfiguration getConfig() { + SdkClientConfiguration config = SdkClientConfiguration.builder() + .option(AwsClientOption.AWS_REGION, Region.US_EAST_1) + .build(); + return config; + } +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java new file mode 100644 index 0000000000..e6674ef0d6 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaAsyncClient_InstrumentationTest.java @@ -0,0 +1,147 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package software.amazon.awssdk.services.lambda; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.api.agent.Trace; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.async.EmptyPublisher; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.utils.StringInputStream; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"software.amazon.awssdk.services.lambda"}, configName = "dt_enabled.yml") +public class DefaultLambdaAsyncClient_InstrumentationTest { + + public LambdaAsyncClient lambdaClient; + public HttpExecuteResponse response; + + @Before + public void setup() { + AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); + response = asyncHttpClient.getResponse(); + lambdaClient = LambdaAsyncClient.builder() + .httpClient(asyncHttpClient) + .credentialsProvider(new CredProvider()) + .region(Region.US_EAST_1) + .build(); + } + + @Test + public void testInvokeArn() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txn(); + SpanEvent lambdaSpan = introspector.getSpanEvents().stream() + .filter(span -> span.getName().equals("Lambda/invoke/my-function")) + .findFirst().orElse(null); + assertNotNull(lambdaSpan); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", lambdaSpan.getAgentAttributes().get("cloud.resource_id")); + + Collection transactionTraces = introspector.getTransactionTracesForTransaction( + "OtherTransaction/Custom/software.amazon.awssdk.services.lambda.DefaultLambdaAsyncClient_InstrumentationTest/txn"); + assertEquals(1,transactionTraces.size()); + TransactionTrace transactionTrace = transactionTraces.iterator().next(); + List children = transactionTrace.getInitialTraceSegment().getChildren(); + assertEquals(1, children.size()); + TraceSegment trace = children.get(0); + assertEquals("Lambda/invoke/my-function", trace.getName()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", trace.getTracerAttributes().get("cloud.resource_id")); + } + + @Trace(dispatcher = true) + public void txn() { + InvokeRequest request = InvokeRequest.builder() + .functionName("arn:aws:lambda:us-east-1:123456789012:function:my-function") + .build(); + lambdaClient.invoke(request); + } + + + // This mock SdkAsyncHttpClient allows testing the AWS SDK without making actual HTTP requests. + private static class AsyncHttpClient implements SdkAsyncHttpClient { + private ExecutableHttpRequest executableMock; + private HttpExecuteResponse response; + private SdkHttpFullResponse httpResponse; + + public AsyncHttpClient() { + executableMock = mock(ExecutableHttpRequest.class); + response = mock(HttpExecuteResponse.class, Mockito.RETURNS_DEEP_STUBS); + httpResponse = mock(SdkHttpFullResponse.class, Mockito.RETURNS_DEEP_STUBS); + when(response.httpResponse()).thenReturn(httpResponse); + when(httpResponse.toBuilder().content(any()).build()).thenReturn(httpResponse); + when(httpResponse.isSuccessful()).thenReturn(true); + AbortableInputStream inputStream = AbortableInputStream.create(new StringInputStream("Dont panic")); + when(httpResponse.content()).thenReturn(Optional.of(inputStream)); + try { + when(executableMock.call()).thenReturn(response); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + } + + @Override + public CompletableFuture execute(AsyncExecuteRequest asyncExecuteRequest) { + asyncExecuteRequest.responseHandler().onStream(new EmptyPublisher<>()); + return new CompletableFuture<>(); + } + + @Override + public String clientName() { + return "MockHttpClient"; + } + + public HttpExecuteResponse getResponse() { + return response; + } + } + + private static class CredProvider implements AwsCredentialsProvider { + @Override + public AwsCredentials resolveCredentials() { + AwsCredentials credentials = mock(AwsCredentials.class); + when(credentials.accessKeyId()).thenReturn("accessKeyId"); + when(credentials.secretAccessKey()).thenReturn("secretAccessKey"); + return credentials; + } + } +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_InstrumentationTest.java b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_InstrumentationTest.java new file mode 100644 index 0000000000..3db97f6345 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/test/java/software/amazon/awssdk/services/lambda/DefaultLambdaClient_InstrumentationTest.java @@ -0,0 +1,145 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package software.amazon.awssdk.services.lambda; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.api.agent.Trace; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.utils.StringInputStream; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"software.amazon.awssdk.services.lambda"}, configName = "dt_enabled.yml") +public class DefaultLambdaClient_InstrumentationTest { + + public LambdaClient lambdaClient; + public HttpExecuteResponse response; + + @Before + public void setup() { + MockHttpClient mockHttpClient = new MockHttpClient(); + response = mockHttpClient.getResponse(); + lambdaClient = LambdaClient.builder() + .httpClient(mockHttpClient) + .credentialsProvider(new CredProvider()) + .region(Region.US_EAST_1) + .build(); + } + + @Test + public void testInvokeArn() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txn(); + SpanEvent lambdaSpan = introspector.getSpanEvents().stream() + .filter(span -> span.getName().equals("Lambda/invoke/my-function")) + .findFirst().orElse(null); + assertNotNull(lambdaSpan); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", lambdaSpan.getAgentAttributes().get("cloud.resource_id")); + + Collection transactionTraces = introspector.getTransactionTracesForTransaction( + "OtherTransaction/Custom/software.amazon.awssdk.services.lambda.DefaultLambdaClient_InstrumentationTest/txn"); + assertEquals(1,transactionTraces.size()); + TransactionTrace transactionTrace = transactionTraces.iterator().next(); + List children = transactionTrace.getInitialTraceSegment().getChildren(); + assertEquals(1, children.size()); + TraceSegment trace = children.get(0); + assertEquals("Lambda/invoke/my-function", trace.getName()); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", trace.getTracerAttributes().get("cloud.resource_id")); + } + + @Trace(dispatcher = true) + public void txn() { + InvokeRequest request = InvokeRequest.builder() + .functionName("arn:aws:lambda:us-east-1:123456789012:function:my-function") + .build(); + lambdaClient.invoke(request); + } + + + // This mock SdkHttpClient allows testing the AWS SDK without making actual HTTP requests. + private static class MockHttpClient implements SdkHttpClient { + private ExecutableHttpRequest executableMock; + private HttpExecuteResponse response; + private SdkHttpFullResponse httpResponse; + + public MockHttpClient() { + executableMock = mock(ExecutableHttpRequest.class); + response = mock(HttpExecuteResponse.class, Mockito.RETURNS_DEEP_STUBS); + httpResponse = mock(SdkHttpFullResponse.class, Mockito.RETURNS_DEEP_STUBS); + when(response.httpResponse()).thenReturn(httpResponse); + when(httpResponse.toBuilder().content(any()).build()).thenReturn(httpResponse); + when(httpResponse.isSuccessful()).thenReturn(true); + AbortableInputStream inputStream = AbortableInputStream.create(new StringInputStream("42")); + when(httpResponse.content()).thenReturn(Optional.of(inputStream)); + try { + when(executableMock.call()).thenReturn(response); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + } + + @Override + public ExecutableHttpRequest prepareRequest(HttpExecuteRequest httpExecuteRequest) { + return executableMock; + } + + @Override + public String clientName() { + return "MockHttpClient"; + } + + public HttpExecuteResponse getResponse() { + return response; + } + } + + private static class CredProvider implements AwsCredentialsProvider { + @Override + public AwsCredentials resolveCredentials() { + AwsCredentials credentials = mock(AwsCredentials.class); + when(credentials.accessKeyId()).thenReturn("accessKeyId"); + when(credentials.secretAccessKey()).thenReturn("secretAccessKey"); + return credentials; + } + } +} \ No newline at end of file diff --git a/instrumentation/aws-java-sdk-lambda-2.1/src/test/resources/dt_enabled.yml b/instrumentation/aws-java-sdk-lambda-2.1/src/test/resources/dt_enabled.yml new file mode 100644 index 0000000000..53b0968002 --- /dev/null +++ b/instrumentation/aws-java-sdk-lambda-2.1/src/test/resources/dt_enabled.yml @@ -0,0 +1,5 @@ +common: &default_settings + distributed_tracing: + enabled: true + span_events: + enabled: true \ No newline at end of file diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java index 4734523a3c..5d86065fd4 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java @@ -53,17 +53,18 @@ public final class AttributeNames { public static final String REQUEST_USER_AGENT_PARAMETER_NAME = "request.headers.userAgent"; public static final String REQUEST_METHOD_PARAMETER_NAME = "request.method"; + public static final String RESPONSE_CONTENT_TYPE_PARAMETER_NAME = "response.headers.contentType"; + // Opem Telemetry compatible attributes for host and port public static final String SERVER_ADDRESS = "server.address"; public static final String SERVER_PORT = "server.port"; // cloud provider fields - public static final String CLOUD_RESOURCE_ID = "cloud.resource_id"; public static final String CLOUD_ACCOUNT_ID = "cloud.account.id"; + public static final String CLOUD_PLATFORM = "cloud.platform"; + public static final String CLOUD_RESOURCE_ID = "cloud.resource_id"; public static final String CLOUD_REGION = "cloud.region"; - public static final String RESPONSE_CONTENT_TYPE_PARAMETER_NAME = "response.headers.contentType"; - // high security matches public static final String HTTP_REQUEST_STAR = "request.parameters.*"; public static final String MESSAGE_REQUEST_STAR = "message.parameters.*"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java index 320c2e79f4..c178f61393 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java @@ -12,7 +12,6 @@ import com.newrelic.agent.attributes.AttributeValidator; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.AttributesConfig; -import com.newrelic.agent.config.TransactionEventsConfig; import com.newrelic.agent.database.SqlObfuscator; import com.newrelic.agent.model.AttributeFilter; import com.newrelic.agent.model.SpanCategory; @@ -24,6 +23,7 @@ import com.newrelic.agent.util.StackTraces; import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.ExternalParameters; +import com.newrelic.api.agent.CloudParameters; import com.newrelic.api.agent.HttpParameters; import com.newrelic.api.agent.MessageConsumeParameters; import com.newrelic.api.agent.MessageProduceParameters; @@ -301,6 +301,15 @@ public SpanEventFactory setCloudRegion(String region) { return this; } + private void setCloudPlatform(String platform){ + builder.putAgentAttribute(AttributeNames.CLOUD_PLATFORM, platform); + } + + private void setCloudResourceId(String name){ + builder.putAgentAttribute(AttributeNames.CLOUD_RESOURCE_ID, name); + } + + public SpanEventFactory setMessagingSystem(String messagingSystem) { builder.putAgentAttribute(AttributeNames.MESSAGING_SYSTEM, messagingSystem); return this; @@ -448,6 +457,11 @@ public SpanEventFactory setExternalParameterAttributes(ExternalParameters parame setServerAddress(messageConsumeParameters.getHost()); setServerPort(messageConsumeParameters.getPort()); setKind("consumer"); + } else if (parameters instanceof CloudParameters) { + CloudParameters cloudParameters = (CloudParameters) parameters; + setCategory(SpanCategory.generic); + setCloudPlatform(cloudParameters.getPlatform()); + setCloudResourceId(cloudParameters.getResourceId()); } else { setCategory(SpanCategory.generic); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java index 8f3c11f5b1..e19172a547 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java @@ -28,9 +28,11 @@ import com.newrelic.agent.tracers.metricname.SimpleMetricNameFormat; import com.newrelic.agent.util.ExternalsUtil; import com.newrelic.agent.util.Strings; +import com.newrelic.api.agent.CloudParameters; import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.DestinationType; import com.newrelic.api.agent.ExternalParameters; +import com.newrelic.api.agent.CloudParameters; import com.newrelic.api.agent.GenericParameters; import com.newrelic.api.agent.HttpParameters; import com.newrelic.api.agent.InboundHeaders; @@ -664,6 +666,8 @@ private void recordExternalMetrics() { recordMessageBrokerMetrics((MessageProduceParameters) this.externalParameters); } else if (externalParameters instanceof MessageConsumeParameters) { recordMessageBrokerMetrics((MessageConsumeParameters) this.externalParameters); + } else if (externalParameters instanceof CloudParameters) { + recordFaasAttributes((CloudParameters) externalParameters); } else { Agent.LOG.log(Level.SEVERE, "Unknown externalParameters type. This should not happen. {0} -- {1}", externalParameters, externalParameters.getClass()); @@ -848,6 +852,11 @@ private void recordMessageBrokerMetrics(MessageConsumeParameters messageConsumeP } } + private void recordFaasAttributes(CloudParameters cloudParameters) { + setAgentAttribute(AttributeNames.CLOUD_PLATFORM, cloudParameters.getPlatform()); + setAgentAttribute(AttributeNames.CLOUD_RESOURCE_ID, cloudParameters.getResourceId()); + } + private void recordSlowQueryData(SlowQueryDatastoreParameters slowQueryDatastoreParameters) { Transaction transaction = getTransactionActivity().getTransaction(); if (transaction != null && slowQueryDatastoreParameters.getRawQuery() != null diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/util/AgentCollectionFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/util/AgentCollectionFactory.java index 4f2bf184eb..6afb5ecaf3 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/util/AgentCollectionFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/util/AgentCollectionFactory.java @@ -47,4 +47,13 @@ public Function memorize(Function loader, int maxSize) { .build(loader::apply); return cache::get; } + + @Override + public Function createAccessTimeBasedCache(long ageInSeconds, int initialCapacity, Function loader) { + LoadingCache cache = Caffeine.newBuilder() + .initialCapacity(initialCapacity) + .expireAfterAccess(ageInSeconds, TimeUnit.SECONDS) + .build(loader::apply); + return cache::get; + } } diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/CloudParameters.java b/newrelic-api/src/main/java/com/newrelic/api/agent/CloudParameters.java new file mode 100644 index 0000000000..960816a122 --- /dev/null +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/CloudParameters.java @@ -0,0 +1,82 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.api.agent; + +/** + * Use to report a cloud service that do not match HTTP nor messaging. + * + * @since 8.14.0 + */ +public class CloudParameters implements ExternalParameters { + + private final String platform; + + private final String resourceId; + + private CloudParameters(Builder builder) { + this.platform = builder.platform; + this.resourceId = builder.resourceId; + } + + public String getPlatform() { + return platform; + } + + public String getResourceId() { + return resourceId; + } + + /** + * This method starts the process of creating a CloudParameters object. + * @param provider The cloud platform being invoked. E.g. aws_lambda, azure_function, gcp_cloud_run... + * @since 8.14.0 + */ + public static ResourceIdParameter provider(String provider) { + return new Builder(provider); + } + + private static class Builder implements ResourceIdParameter, Build { + private String platform; + private String resourceId; + + private Builder(String platform) { + this.platform = platform; + } + + public Build resourceId(String resourceId) { + this.resourceId = resourceId; + return this; + } + + public CloudParameters build() { + return new CloudParameters(this); + } + } + + public interface ResourceIdParameter extends Build { + /** + * @param resourceId the cloud provider unique identifier for the service instance. This should be an ARN for AWS, + * a fully qualified resource ID on Azure or a full resource name on GCP. + * @return the object that can be used to build the CloudParameters + * + * @since 8.14.0 + */ + Build resourceId(String resourceId); + } + + public interface Build { + /** + * Builds the CloudParameters object. + * @return the CloudParameters object with the specified parameters. + * + * @since 8.14.0 + */ + CloudParameters build(); + } + +} diff --git a/settings.gradle b/settings.gradle index 0e95fc03b0..1e2a07ecc6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -70,13 +70,15 @@ include 'instrumentation:activemq-client-5.8.0' include 'instrumentation:anorm-2.3' include 'instrumentation:anorm-2.4' include 'instrumentation:aws-bedrock-runtime-2.20' +include 'instrumentation:aws-java-sdk-dynamodb-1.11.106' +include 'instrumentation:aws-java-sdk-dynamodb-2.15.34' +include 'instrumentation:aws-java-sdk-lambda-1.11.280' +include 'instrumentation:aws-java-sdk-lambda-2.1' include 'instrumentation:aws-java-sdk-sqs-1.10.44' include 'instrumentation:aws-java-sdk-s3-1.2.13' include 'instrumentation:aws-java-sdk-s3-2.0' include 'instrumentation:aws-java-sdk-sns-1.11.12' include 'instrumentation:aws-java-sdk-sqs-2.1.0' -include 'instrumentation:aws-java-sdk-dynamodb-1.11.106' -include 'instrumentation:aws-java-sdk-dynamodb-2.15.34' include 'instrumentation:aws-java-sdk-sns-2.0' include 'instrumentation:aws-wrap-0.7.0' include 'instrumentation:akka-2.2'