Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

added support to Elastic Load Balancer triggers #3411

Merged
merged 10 commits into from
Mar 13, 2024
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
Loading