Skip to content

Commit

Permalink
added support to Elastic Load Balancer triggers (#3411)
Browse files Browse the repository at this point in the history

---------

Co-authored-by: Sylvain Juge <[email protected]>
  • Loading branch information
videnkz and SylvainJuge authored Mar 13, 2024
1 parent 3c4a753 commit 3b22aad
Show file tree
Hide file tree
Showing 13 changed files with 617 additions and 132 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
[float]
===== Features
* Differentiate Lambda URLs from API Gateway in AWS Lambda integration - {pull}3417[#3417]
* Added lambda support for ELB triggers {pull}#3411[#3411]
[[release-notes-1.x]]
=== Java Agent version 1.x
Expand Down Expand Up @@ -127,7 +128,7 @@ affect you, if you are using the OpenTelemetry API only and not the SDK. - {pull
===== Features
* Added protection against invalid timestamps provided by manual instrumentation - {pull}3363[#3363]
* Added support for AWS SDK 2.21 - {pull}3373[#3373]
* Capture bucket and object key to Lambda transaction as OTel attributes - `aws.s3.bueckt`, `aws.s3.key` - {pull}3364[#3364]
* Capture bucket and object key to Lambda transaction as OTel attributes - `aws.s3.bucket`, `aws.s3.key` - {pull}3364[#3364]
* Added `context_propagation_only` configuration option - {pull}3358[#3358]
* Added attribute[*] for JMX pattern metrics (all metrics can now be generated with `object_name[*:type=*,name=*] attribute[*]`) - {pull}3376[#3376]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@
package co.elastic.apm.agent.awslambda.helper;

import co.elastic.apm.agent.awslambda.MapTextHeaderGetter;
import co.elastic.apm.agent.tracer.GlobalTracer;
import co.elastic.apm.agent.sdk.internal.util.PrivilegedActionUtils;
import co.elastic.apm.agent.tracer.GlobalTracer;
import co.elastic.apm.agent.tracer.Tracer;
import co.elastic.apm.agent.tracer.Transaction;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

import javax.annotation.Nullable;
import java.util.Map;

public class APIGatewayProxyV1TransactionHelper extends AbstractAPIGatewayTransactionHelper<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

Expand All @@ -49,48 +48,17 @@ public static APIGatewayProxyV1TransactionHelper getInstance() {
@Override
protected Transaction<?> doStartTransaction(APIGatewayProxyRequestEvent apiGatewayEvent, Context lambdaContext) {
Transaction<?> transaction = tracer.startChildTransaction(apiGatewayEvent.getHeaders(), MapTextHeaderGetter.INSTANCE, PrivilegedActionUtils.getClassLoader(apiGatewayEvent.getClass()));
String host = getHost(apiGatewayEvent);

if (null != transaction) {
String host = getHost(apiGatewayEvent.getHeaders());

fillHttpRequestData(transaction, getHttpMethod(apiGatewayEvent), apiGatewayEvent.getHeaders(), host,
apiGatewayEvent.getRequestContext().getPath(), getQueryString(apiGatewayEvent), apiGatewayEvent.getBody());
apiGatewayEvent.getRequestContext().getPath(), getQueryString(apiGatewayEvent.getQueryStringParameters()), apiGatewayEvent.getBody());
}

return transaction;
}

@Nullable
private String getHost(APIGatewayProxyRequestEvent apiGatewayEvent) {
String host = null;
if (null != apiGatewayEvent.getHeaders()) {
host = apiGatewayEvent.getHeaders().get("host");
if (null == host) {
host = apiGatewayEvent.getHeaders().get("Host");
}
}
return host;
}

@Nullable
private String getQueryString(APIGatewayProxyRequestEvent apiGatewayEvent) {
Map<String, String> queryParameters = apiGatewayEvent.getQueryStringParameters();
if (null != queryParameters && !queryParameters.isEmpty()) {
StringBuilder queryString = new StringBuilder();
int i = 0;
for (Map.Entry<String, String> entry : apiGatewayEvent.getQueryStringParameters().entrySet()) {
if (i > 0) {
queryString.append('&');
}
queryString.append(entry.getKey());
queryString.append('=');
queryString.append(entry.getValue());
i++;
}
return queryString.toString();
}
return null;
}

@Override
public void captureOutputForTransaction(Transaction<?> transaction, APIGatewayProxyResponseEvent responseEvent) {
Integer statusCode = responseEvent.getStatusCode();
Expand All @@ -107,7 +75,7 @@ protected void setTransactionTriggerData(Transaction<?> transaction, APIGatewayP

if (null != rContext) {
setApiGatewayContextData(transaction, rContext.getRequestId(), rContext.getApiId(),
getHost(apiGatewayRequest), rContext.getAccountId());
getHost(apiGatewayRequest.getHeaders()), rContext.getAccountId());
}
}

Expand Down Expand Up @@ -149,4 +117,5 @@ protected String getStage(APIGatewayProxyRequestEvent event) {
protected String getResourcePath(APIGatewayProxyRequestEvent event) {
return event.getRequestContext().getResourcePath();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public static APIGatewayProxyV2TransactionHelper getInstance() {
}

@Override
protected Transaction doStartTransaction(APIGatewayV2HTTPEvent apiGatewayEvent, Context lambdaContext) {
Transaction transaction = tracer.startChildTransaction(apiGatewayEvent.getHeaders(), MapTextHeaderGetter.INSTANCE, PrivilegedActionUtils.getClassLoader(apiGatewayEvent.getClass()));
protected Transaction<?> doStartTransaction(APIGatewayV2HTTPEvent apiGatewayEvent, Context lambdaContext) {
Transaction<?> transaction = tracer.startChildTransaction(apiGatewayEvent.getHeaders(), MapTextHeaderGetter.INSTANCE, PrivilegedActionUtils.getClassLoader(apiGatewayEvent.getClass()));

APIGatewayV2HTTPEvent.RequestContext requestContext = apiGatewayEvent.getRequestContext();
if (transaction != null) {
Expand All @@ -60,12 +60,12 @@ protected Transaction doStartTransaction(APIGatewayV2HTTPEvent apiGatewayEvent,
}

@Override
public void captureOutputForTransaction(Transaction transaction, APIGatewayV2HTTPResponse responseEvent) {
public void captureOutputForTransaction(Transaction<?> transaction, APIGatewayV2HTTPResponse responseEvent) {
fillHttpResponseData(transaction, responseEvent.getHeaders(), responseEvent.getStatusCode());
}

@Override
protected void setTransactionTriggerData(Transaction transaction, APIGatewayV2HTTPEvent apiGatewayRequest) {
protected void setTransactionTriggerData(Transaction<?> transaction, APIGatewayV2HTTPEvent apiGatewayRequest) {
super.setTransactionTriggerData(transaction, apiGatewayRequest);
APIGatewayV2HTTPEvent.RequestContext rContext = apiGatewayRequest.getRequestContext();
setApiGatewayContextData(transaction, rContext.getRequestId(), rContext.getApiId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent;
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
Expand All @@ -50,6 +52,9 @@ public static Transaction<?> startTransaction(Object input, Context lambdaContex
} else if (input instanceof S3Event) {
// S3 event trigger
return S3TransactionHelper.getInstance().startTransaction((S3Event) input, lambdaContext);
} else if (input instanceof ApplicationLoadBalancerRequestEvent) {
// Load Balancer Request event trigger
return ApplicationLoadBalancerRequestTransactionHelper.getInstance().startTransaction((ApplicationLoadBalancerRequestEvent) input, lambdaContext);
}
return PlainTransactionHelper.getInstance().startTransaction(input, lambdaContext);
}
Expand All @@ -59,6 +64,8 @@ public static void finalizeTransaction(Transaction<?> transaction, Object output
APIGatewayProxyV2TransactionHelper.getInstance().finalizeTransaction(transaction, (APIGatewayV2HTTPResponse) output, thrown);
} else if (output instanceof APIGatewayProxyResponseEvent) {
APIGatewayProxyV1TransactionHelper.getInstance().finalizeTransaction(transaction, (APIGatewayProxyResponseEvent) output, thrown);
} else if (output instanceof ApplicationLoadBalancerResponseEvent) {
ApplicationLoadBalancerRequestTransactionHelper.getInstance().finalizeTransaction(transaction, (ApplicationLoadBalancerResponseEvent) output, thrown);
} else {
// use PlainTransactionHelper for all triggers that do not expect an output
PlainTransactionHelper.getInstance().finalizeTransaction(transaction, output, thrown);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@
*/
package co.elastic.apm.agent.awslambda.helper;

import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import co.elastic.apm.agent.tracer.AbstractSpan;
import co.elastic.apm.agent.tracer.ServiceOrigin;
import co.elastic.apm.agent.tracer.Tracer;
import co.elastic.apm.agent.tracer.Transaction;
import co.elastic.apm.agent.tracer.metadata.CloudOrigin;
import co.elastic.apm.agent.tracer.metadata.Request;
import co.elastic.apm.agent.tracer.metadata.Response;
import co.elastic.apm.agent.tracer.util.ResultUtil;
import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import co.elastic.apm.agent.tracer.AbstractSpan;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;

import javax.annotation.Nullable;
import java.nio.CharBuffer;
Expand Down Expand Up @@ -68,6 +67,38 @@ protected void fillHttpRequestData(Transaction<?> transaction, @Nullable String
}
}

@Nullable
protected String getHost(@Nullable Map<String, String> headers) {
if (null == headers) {
return null;
}
String host = headers.get("host");
if (null == host) {
host = headers.get("Host");
}
return host;
}

@Nullable
protected String getQueryString(@Nullable Map<String, String> queryParameters) {
if (null == queryParameters || queryParameters.isEmpty()) {
return null;
}
StringBuilder queryString = new StringBuilder();
int i = 0;
for (Map.Entry<String, String> entry : queryParameters.entrySet()) {
if (i > 0) {
queryString.append('&');
}
queryString.append(entry.getKey());
queryString.append('=');
queryString.append(entry.getValue());
i++;
}
return queryString.toString();
}


protected void fillHttpResponseData(Transaction<?> transaction, @Nullable Map<String, String> headers, int statusCode) {
Response response = transaction.getContext().getResponse();
response.withFinished(true);
Expand All @@ -82,7 +113,7 @@ protected void fillHttpResponseData(Transaction<?> transaction, @Nullable Map<St
}

private void fillUrlRelatedFields(Request request, @Nullable String serverName, @Nullable String path, @Nullable String queryString) {
String qString = queryString == null || queryString.trim().isEmpty() ? null: queryString;
String qString = queryString == null || queryString.trim().isEmpty() ? null : queryString;
request.getUrl().fillFrom("https", serverName, 443, path, qString);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent.awslambda.helper;

import co.elastic.apm.agent.awslambda.MapTextHeaderGetter;
import co.elastic.apm.agent.sdk.internal.util.PrivilegedActionUtils;
import co.elastic.apm.agent.tracer.*;
import co.elastic.apm.agent.tracer.metadata.CloudOrigin;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent;
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;

public class ApplicationLoadBalancerRequestTransactionHelper extends AbstractAPIGatewayTransactionHelper<ApplicationLoadBalancerRequestEvent, ApplicationLoadBalancerResponseEvent> {
@Nullable
private static ApplicationLoadBalancerRequestTransactionHelper INSTANCE;

private ApplicationLoadBalancerRequestTransactionHelper(Tracer tracer) {
super(tracer);
}

public static ApplicationLoadBalancerRequestTransactionHelper getInstance() {
if (INSTANCE == null) {
INSTANCE = new ApplicationLoadBalancerRequestTransactionHelper(GlobalTracer.get());
}
return INSTANCE;
}

@Override
protected Transaction doStartTransaction(ApplicationLoadBalancerRequestEvent loadBalancerRequestEvent, Context lambdaContext) {
Transaction transaction = tracer.startChildTransaction(loadBalancerRequestEvent.getHeaders(), MapTextHeaderGetter.INSTANCE, PrivilegedActionUtils.getClassLoader(loadBalancerRequestEvent.getClass()));

if (transaction != null) {
String host = getHost(loadBalancerRequestEvent.getHeaders());
super.fillHttpRequestData(transaction, loadBalancerRequestEvent.getHttpMethod(), loadBalancerRequestEvent.getHeaders(), host,
loadBalancerRequestEvent.getPath(), getQueryString(loadBalancerRequestEvent.getQueryStringParameters()), loadBalancerRequestEvent.getBody());
}

return transaction;
}

@Override
public void captureOutputForTransaction(Transaction transaction, ApplicationLoadBalancerResponseEvent responseEvent) {
fillHttpResponseData(transaction, responseEvent.getHeaders(), responseEvent.getStatusCode());
}

@Override
protected void setTransactionTriggerData(Transaction transaction, ApplicationLoadBalancerRequestEvent loadBalancerRequestEvent) {
transaction.withType(TRANSACTION_TYPE);
CloudOrigin cloudOrigin = transaction.getContext().getCloudOrigin();
cloudOrigin.withServiceName("elb");
cloudOrigin.withProvider("aws");
FaasTrigger faasTrigger = transaction.getFaas().getTrigger();
faasTrigger.withType("http");
faasTrigger.withRequestId(getHeader(loadBalancerRequestEvent, "x-amzn-trace-id"));
LoadBalancerElbTargetGroupArnMetadata metadata = parseMetadata(loadBalancerRequestEvent);
if (null != metadata) {
ServiceOrigin serviceOrigin = transaction.getContext().getServiceOrigin();
serviceOrigin.withName(metadata.getTargetGroupName());
serviceOrigin.withId(metadata.getTargetGroupArn());
cloudOrigin.withAccountId(metadata.getAccountId());
cloudOrigin.withRegion(metadata.getCloudRegion());
}
}

@Nullable
private String getHeader(@Nonnull ApplicationLoadBalancerRequestEvent loadBalancerRequestEvent,
@Nonnull String headerName) {
Map<String, String> headers = loadBalancerRequestEvent.getHeaders();
if (null == headers) {
return null;
}
return headers.get(headerName);
}

@Nullable
private LoadBalancerElbTargetGroupArnMetadata parseMetadata(ApplicationLoadBalancerRequestEvent event) {
if (null == event.getRequestContext()) {
return null;
}
ApplicationLoadBalancerRequestEvent.Elb elb = event.getRequestContext().getElb();
if (null == elb) {
return null;
}
String targetGroupArn = elb.getTargetGroupArn();
if (null == targetGroupArn) {
return null;
}
LoadBalancerElbTargetGroupArnMetadata metadata = new LoadBalancerElbTargetGroupArnMetadata(targetGroupArn);
String[] arnParts = targetGroupArn.split(":");
int arnPartsLength = arnParts.length;
if (arnPartsLength < 4) {
return metadata;
}
metadata.withCloudRegion(arnParts[3]);
if (arnPartsLength < 5) {
return metadata;
}
metadata.withAccountId(arnParts[4]);
if (arnPartsLength < 6) {
return metadata;
}
String targetGroup = arnParts[5];
String[] targetGroupParts = targetGroup.split("/");
if (targetGroupParts.length < 2) {
return metadata;
}
return metadata.withTargetGroupName(targetGroupParts[2]);
}

@Override
protected String getApiGatewayVersion() {
throw new UnsupportedOperationException("Not supported by ELB");
}

@Nullable
@Override
protected String getHttpMethod(ApplicationLoadBalancerRequestEvent event) {
return event.getHttpMethod();
}

@Nullable
@Override
protected String getRequestContextPath(ApplicationLoadBalancerRequestEvent event) {
return event.getPath();
}

@Nullable
@Override
protected String getStage(ApplicationLoadBalancerRequestEvent event) {
throw new UnsupportedOperationException("Not supported by ELB");
}

@Nullable
@Override
protected String getResourcePath(ApplicationLoadBalancerRequestEvent event) {
return null;
}

@Nullable
@Override
String getDomainName(ApplicationLoadBalancerRequestEvent apiGatewayRequest) {
return null;
}
}
Loading

0 comments on commit 3b22aad

Please sign in to comment.