Repository: elastic/apm-agent-python

Issue:
AWS lambda: capture_serverless does not provide information when lambda is connected to ELB (not to API GW)
Event that lambda handler receiving in case of attachment through the ELB you can find in attachment: [elb-lambda-event.txt](https://github.com/elastic/apm-agent-python/files/9103558/elb-lambda-event.txt)

Please note that queryStringParameters are quoted (in the event which is coming from API gateway they are not).
Note that in this case requestContext contains only elb element and nothing else.


**Is your feature request related to a problem? Please describe.**
Recently AWS has introduced that lambda could be joined directly to ELB (without API gateway) and when transaction is getting recorded by capture_serverless then it has much less information comparing to the API gateway connected one.

This function should be rewritten to react accordingly. For the lambdas which are ELB api invocations - the " elasticapm.set_context(lambda: get_data_from_request( is not getting invoked.

    def __enter__(self):

..... 

 self.httpmethod = nested_key(self.event, "requestContext", "httpMethod") or nested_key(
            self.event, "requestContext", "http", "method"
        )
....

if self.httpmethod:  # API Gateway
    self.source = "api"

.....

.....
       if self.source == "api":
            elasticapm.set_context(
                lambda: get_data_from_request(

Discussion:
cla-checker-service[bot]: <!-- CLA-CHECK:1605 -->
&#10060; Author of the following commits did not sign a [Contributor Agreement](https://www.elastic.co/contributor-agreement):
   fa9da73b2bab7b29bb6659b2740a4986b59ff86f, 72d178e44ba5a23a46d47caad853832492ede680, 5f8e54eff6541e0e954839eb12cdfdaa99079166

   Please, read and sign the above mentioned agreement if you want to contribute to this project
apmmachine: 
## :grey_exclamation: Build Aborted
  
  > The PR is not allowed to run in the CI yet
  


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1605/detail/PR-1605/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1605/detail/PR-1605/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1605/detail/PR-1605/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1605/detail/PR-1605/1//artifacts)   [![preview](https://img.shields.io/badge/elastic-observability-blue)](https://ci-stats.elastic.co/app/apm/services/apm-ci/transactions/view?rangeFrom=2022-08-05T11:17:22.745Z&rangeTo=2022-08-05T11:37:22.745Z&transactionName=BUILD+apm-agent-python%2Fapm-agent-python-mbp%2FPR-%7Bnumber%7D&transactionType=job&latencyAggregationType=avg&traceId=2e2b40134a00923f4b2817a6cb56c37d&transactionId=0ae767ead3d8d84f)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats




* Start Time: 2022-08-05T11:27:22.745+0000


* Duration: 3 min 53 sec





</p>
</details>

<!-- TEST RESULTS IF ANY-->


<!-- STEPS ERRORS IF ANY -->














### Steps errors [![2](https://img.shields.io/badge/2%20-red)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1605/detail/PR-1605/1//pipeline)
  <details><summary>Expand to view the steps failures</summary>
  <p>


  
  
  
  
##### `Load a resource file from a shared library`
<ul>
<li>Took 0 min 0 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-python/pipelines/apm-agent-python-mbp/pipelines/PR-1605/runs/1/steps/87/log/?start=0">here</a></li>
<li>Description: <code>approval-list/elastic/apm-agent-python.yml</code></l1>
</ul>
  
  
  
  
##### `Error signal`
<ul>
<li>Took 0 min 0 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-python/pipelines/apm-agent-python-mbp/pipelines/PR-1605/runs/1/steps/95/log/?start=0">here</a></li>
<li>Description: <code>githubApiCall: The REST API call https://api.github.com/orgs/elastic/members/tonyman19 return the message : java.lang.Exception: httpRequest: Failure connecting to the service https://api.github.com/orgs/elastic/members/tonyman19 : httpRequest: Failure connecting to the service https://api.github.com/orgs/elastic/members/tonyman19 : Code: 404Error: {"message":"User does not exist or is not a member of the organization","documentation_url":"https://docs.github.com/rest/reference/orgs#check-organization-membership-for-a-user"}</code></l1>
</ul>
  
  </p>
  </details>



## :robot: GitHub comments

To re-run your PR in the CI, just comment with:



- `/test` : Re-trigger the build.


- `/test linters` : Run the Python linters only.


- `/test full` : Run the full matrix of tests.


- `/test benchmark` : Run the APM Agent Python benchmarks tests.


- `run` `elasticsearch-ci/docs` : Re-trigger the docs validation. (use unformatted text in the comment!)




<!--COMMENT_GENERATED_WITH_ID_comment.id-->
tonyman19: /test
apmmachine: ### :globe_with_meridians: Coverage report

Name | Metrics % (`covered/total`) | Diff
 --- | --- | ---
Packages | 100.0% (`68/68`) | :green_heart: 
Files | 100.0% (`227/227`) | :green_heart: 
Classes | 100.0% (`227/227`) | :green_heart: 
Lines | 89.744% (`17326/19306`) | :+1: 0.101
Conditionals | 77.133% (`3164/4102`) | :+1: 0.265

<!--COMMENT_GENERATED_WITH_ID_coverage-->
basepi: @tonyman19 Did you sign the CLA for the email `anton@mycarly.com`? That's the email attached to your commits.
tonyman19: Hi Colton,

Thanks for your PR review and getting back to me.
I've signed it with the ***@***.*** (same as my github
account).
***@***.*** - this is my work email which I'm using for our elastic
subscription - but the contribution will be from ***@***.***

In the attachment you can find the signed document (tried 3 times before
and everytime missed the button to finish the signature - but was able to
download PDFs :))

Best regards
Anton Matviyenko

On Mon, 8 Aug 2022 at 20:51, Colton Myers ***@***.***> wrote:

> @tonyman19 <https://github.com/tonyman19> Did you sign the CLA for the
> email ***@***.***? That's the email attached to your commits.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/elastic/apm-agent-python/pull/1605#issuecomment-1208486393>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABAIKX6L2XNZSR3AVABYZATVYFJMBANCNFSM55VYITJQ>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

tonyman19: Hi Colton,

Thank you for review and quick reply. Finalized signing for [anton.matviyenko@gmail.com](mailto:anton.matviyenko@gmail.com)
What about recorded commits - is it possible to change from [anton@mycarly.com](mailto:anton@mycarly.com) to the abovementioned email?
Or is it ok the way they are ATM?
Will sign the CLA for anton@mycarly.com as well.
tonyman19: Short question about following inconsistency that I have found. Is it by purpose or just a mistake?

```
if self.source == "api":
 ....
   cloud_context["origin"]["account"] = {"id": self.event["requestContext"]["accountId"]}
elif self.source == "sns":
 .....
   cloud_context["origin"]["account_id"] = record["Sns"]["TopicArn"].split(":")[4]
```

So first time (and everywhere else) we are creating subobject with id field ["origin"]["account"]["id"] - next time we create subelement account_id which is located under ["origin"]["account_id"].

Should we also fix that here?
tonyman19: Regarding your removal of:

```
service_context["origin"]["version"] = self.event.get("version", "1.0")
```

version field is also absent for rest (or http integrations) v1 (check aws_api_test_data.json)

I assumed that there might be a second version of elb integration where it will be present so I did it the same way. I did it conforming the spec for api gw: https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#api-gateway-v1--v2

context.service.origin.version | e.g. 1.0 | 1.0 for API Gateway V1, 2.0 for API Gateway V2. | event.version (or 1.0 if that field is not present)
-- | -- | -- | --


basepi: I suppose there could be a v2 for ELB some day. We did it that way for API Gateway because there were two prominent (and quite different) versions in use when we launched the integration. I think since there's only one version of ELB right now, we'll leave off the version (like we do for the other non-http triggers), and we can add it in the future if it becomes relevant.

Thanks for signing the CLA with the correct email. I can't change the email on the commits, you would have to re-configure your git client and then rebase the commits. But everything is good now that you signed the CLA for the correct email.

Good catch on the SNS `account_id` -- that was definitely a bug! I have fixed it.

Patch:
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index e03b47b3..55b7a387 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -29,6 +29,18 @@ endif::[]
 //===== Bug fixes
 //
 
+=== Unreleased
+
+// Unreleased changes go here
+// When the next release happens, nest these changes under the "Python Agent version 6.x" heading
+[float]
+===== Features
+
+* Added lambda support for ELB triggers {pull}#1605[#1605]
+
+//[float]
+//===== Bug fixes
+
 
 
 [[release-notes-6.x]]
diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py
index e0e20709..59b246ab 100644
--- a/elasticapm/contrib/serverless/aws.py
+++ b/elasticapm/contrib/serverless/aws.py
@@ -36,6 +36,7 @@ import os
 import platform
 import time
 from typing import Optional
+from urllib.parse import urlencode
 
 import elasticapm
 from elasticapm.base import Client
@@ -44,6 +45,8 @@ from elasticapm.utils import encoding, get_name_from_func, nested_key
 from elasticapm.utils.disttracing import TraceParent
 from elasticapm.utils.logging import get_logger
 
+SERVERLESS_HTTP_REQUEST = ("api", "elb")
+
 logger = get_logger("elasticapm.serverless")
 
 COLD_START = True
@@ -134,18 +137,25 @@ class capture_serverless(object):
         transaction_type = "request"
         transaction_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", self.name)
 
-        self.httpmethod = nested_key(self.event, "requestContext", "httpMethod") or nested_key(
-            self.event, "requestContext", "http", "method"
+        self.httpmethod = (
+            nested_key(self.event, "requestContext", "httpMethod")
+            or nested_key(self.event, "requestContext", "http", "method")
+            or nested_key(self.event, "httpMethod")
         )
-        if self.httpmethod:  # API Gateway
-            self.source = "api"
-            if nested_key(self.event, "requestContext", "httpMethod"):
+
+        if self.httpmethod:  # http request
+            if nested_key(self.event, "requestContext", "elb"):
+                self.source = "elb"
+                resource = nested_key(self.event, "path")
+            elif nested_key(self.event, "requestContext", "httpMethod"):
+                self.source = "api"
                 # API v1
                 resource = "/{}{}".format(
                     nested_key(self.event, "requestContext", "stage"),
                     nested_key(self.event, "requestContext", "resourcePath"),
                 )
             else:
+                self.source = "api"
                 # API v2
                 route_key = nested_key(self.event, "requestContext", "routeKey")
                 route_key = f"/{route_key}" if route_key.startswith("$") else route_key.split(" ", 1)[-1]
@@ -170,7 +180,7 @@ class capture_serverless(object):
 
         self.transaction = self.client.begin_transaction(transaction_type, trace_parent=trace_parent)
         elasticapm.set_transaction_name(transaction_name, override=False)
-        if self.source == "api":
+        if self.source in SERVERLESS_HTTP_REQUEST:
             elasticapm.set_context(
                 lambda: get_data_from_request(
                     self.event,
@@ -199,7 +209,7 @@ class capture_serverless(object):
                     elasticapm.set_transaction_result("HTTP 5xx", override=False)
         if exc_val:
             self.client.capture_exception(exc_info=(exc_type, exc_val, exc_tb), handled=False)
-            if self.source == "api":
+            if self.source in SERVERLESS_HTTP_REQUEST:
                 elasticapm.set_transaction_result("HTTP 5xx", override=False)
                 elasticapm.set_transaction_outcome(http_status_code=500, override=False)
                 elasticapm.set_context({"status_code": 500}, "response")
@@ -246,6 +256,17 @@ class capture_serverless(object):
             cloud_context["origin"]["service"] = {"name": "api gateway"}
             cloud_context["origin"]["account"] = {"id": self.event["requestContext"]["accountId"]}
             cloud_context["origin"]["provider"] = "aws"
+        elif self.source == "elb":
+            elb_target_group_arn = self.event["requestContext"]["elb"]["targetGroupArn"]
+            faas["trigger"]["type"] = "http"
+            faas["trigger"]["request_id"] = self.event["headers"]["x-amzn-trace-id"]
+            service_context["origin"] = {"name": elb_target_group_arn.split(":")[5]}
+            service_context["origin"]["id"] = elb_target_group_arn
+            cloud_context["origin"] = {}
+            cloud_context["origin"]["service"] = {"name": "elb"}
+            cloud_context["origin"]["account"] = {"id": elb_target_group_arn.split(":")[4]}
+            cloud_context["origin"]["region"] = elb_target_group_arn.split(":")[3]
+            cloud_context["origin"]["provider"] = "aws"
         elif self.source == "sqs":
             record = self.event["Records"][0]
             faas["trigger"]["type"] = "pubsub"
@@ -281,7 +302,7 @@ class capture_serverless(object):
             service_context["origin"]["service"] = {"name": "sns"}
             cloud_context["origin"] = {}
             cloud_context["origin"]["region"] = record["Sns"]["TopicArn"].split(":")[3]
-            cloud_context["origin"]["account_id"] = record["Sns"]["TopicArn"].split(":")[4]
+            cloud_context["origin"]["account"] = {"id": record["Sns"]["TopicArn"].split(":")[4]}
             cloud_context["origin"]["provider"] = "aws"
             message_context["queue"] = {"name": service_context["origin"]["name"]}
             if "Timestamp" in record["Sns"]:
@@ -354,7 +375,13 @@ def get_data_from_request(event: dict, capture_body: bool = False, capture_heade
     result = {}
     if capture_headers and "headers" in event:
         result["headers"] = event["headers"]
-    method = nested_key(event, "requestContext", "httpMethod") or nested_key(event, "requestContext", "http", "method")
+
+    method = (
+        nested_key(event, "requestContext", "httpMethod")
+        or nested_key(event, "requestContext", "http", "method")
+        or nested_key(event, "httpMethod")
+    )
+
     if not method:
         # Not API Gateway
         return result
@@ -405,16 +432,23 @@ def get_url_dict(event: dict) -> dict:
     headers = event.get("headers", {})
     protocol = headers.get("X-Forwarded-Proto", headers.get("x-forwarded-proto", "https"))
     host = headers.get("Host", headers.get("host", ""))
-    stage = "/" + (nested_key(event, "requestContext", "stage") or "")
-    path = event.get("path", event.get("rawPath", "").split(stage)[-1])
+    stage = nested_key(event, "requestContext", "stage") or ""
+    raw_path = event.get("rawPath", "")
+    if stage:
+        stage = "/" + stage
+        raw_path = raw_path.split(stage)[-1]
+
+    path = event.get("path", raw_path)
     port = headers.get("X-Forwarded-Port", headers.get("x-forwarded-port"))
     query = ""
     if "rawQueryString" in event:
         query = event["rawQueryString"]
     elif event.get("queryStringParameters"):
-        query = "?"
-        for k, v in event["queryStringParameters"].items():
-            query += "{}={}".format(k, v)
+        if stage:  # api requires parameters encoding to build correct url
+            query = "?" + urlencode(event["queryStringParameters"])
+        else:  # for elb we do not have the stage
+            query = "?" + "&".join(["{}={}".format(k, v) for k, v in event["queryStringParameters"].items()])
+
     url = protocol + "://" + host + stage + path + query
 
     url_dict = {


Test patch:
diff --git a/tests/contrib/serverless/aws_api_test_data.json b/tests/contrib/serverless/aws_api_test_data.json
index f66f8249..58b5e383 100644
--- a/tests/contrib/serverless/aws_api_test_data.json
+++ b/tests/contrib/serverless/aws_api_test_data.json
@@ -78,7 +78,9 @@
             "https"
         ]
     },
-    "queryStringParameters": null,
+    "queryStringParameters": {
+        "test@key": "test@value"
+    },
     "multiValueQueryStringParameters": null,
     "pathParameters": null,
     "stageVariables": null,
@@ -114,4 +116,4 @@
     },
     "body": null,
     "isBase64Encoded": false
-}
\ No newline at end of file
+}
diff --git a/tests/contrib/serverless/aws_elb_test_data.json b/tests/contrib/serverless/aws_elb_test_data.json
new file mode 100644
index 00000000..87e05ac8
--- /dev/null
+++ b/tests/contrib/serverless/aws_elb_test_data.json
@@ -0,0 +1,25 @@
+{
+    "requestContext": {
+        "elb": {
+            "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
+        }
+    },
+    "httpMethod": "POST",
+    "path": "/toolz/api/v2.0/downloadPDF/PDF_2020-09-11_11-06-01.pdf",
+    "queryStringParameters": {
+        "test%40key": "test%40value",
+        "language": "en-DE"
+    },
+    "headers": {
+        "accept-encoding": "gzip,deflate",
+        "connection": "Keep-Alive",
+        "host": "blabla.com",
+        "user-agent": "Apache-HttpClient/4.5.13 (Java/11.0.15)",
+        "x-amzn-trace-id": "Root=1-xxxxxxxxxxxxxx",
+        "x-forwarded-for": "199.99.99.999",
+        "x-forwarded-port": "443",
+        "x-forwarded-proto": "https"
+    },
+    "body": "blablablabody",
+    "isBase64Encoded": false
+}
diff --git a/tests/contrib/serverless/aws_tests.py b/tests/contrib/serverless/aws_tests.py
index 67ad75c2..97803fa7 100644
--- a/tests/contrib/serverless/aws_tests.py
+++ b/tests/contrib/serverless/aws_tests.py
@@ -53,6 +53,13 @@ def event_api2():
         return json.load(f)
 
 
+@pytest.fixture
+def event_elb():
+    aws_data_file = os.path.join(os.path.dirname(__file__), "aws_elb_test_data.json")
+    with open(aws_data_file) as f:
+        return json.load(f)
+
+
 @pytest.fixture
 def event_s3():
     aws_data_file = os.path.join(os.path.dirname(__file__), "aws_s3_test_data.json")
@@ -100,7 +107,7 @@ def test_request_data(event_api, event_api2):
     data = get_data_from_request(event_api, capture_body=True, capture_headers=True)
 
     assert data["method"] == "GET"
-    assert data["url"]["full"] == "https://02plqthge2.execute-api.us-east-1.amazonaws.com/dev/fetch_all"
+    assert data["url"]["full"] == "https://02plqthge2.execute-api.us-east-1.amazonaws.com/dev/fetch_all?test%40key=test%40value"
     assert data["headers"]["Host"] == "02plqthge2.execute-api.us-east-1.amazonaws.com"
 
     data = get_data_from_request(event_api2, capture_body=True, capture_headers=True)
@@ -112,10 +119,28 @@ def test_request_data(event_api, event_api2):
     data = get_data_from_request(event_api, capture_body=False, capture_headers=False)
 
     assert data["method"] == "GET"
-    assert data["url"]["full"] == "https://02plqthge2.execute-api.us-east-1.amazonaws.com/dev/fetch_all"
+    assert data["url"]["full"] == "https://02plqthge2.execute-api.us-east-1.amazonaws.com/dev/fetch_all?test%40key=test%40value"
     assert "headers" not in data
 
 
+def test_elb_request_data(event_elb):
+    data = get_data_from_request(event_elb, capture_body=True, capture_headers=True)
+
+    assert data["method"] == "POST"
+    assert data["url"][
+               "full"] == "https://blabla.com/toolz/api/v2.0/downloadPDF/PDF_2020-09-11_11-06-01.pdf?test%40key=test%40value&language=en-DE"
+    assert data["headers"]["host"] == "blabla.com"
+    assert data["body"] == "blablablabody"
+
+    data = get_data_from_request(event_elb, capture_body=False, capture_headers=False)
+
+    assert data["method"] == "POST"
+    assert data["url"][
+               "full"] == "https://blabla.com/toolz/api/v2.0/downloadPDF/PDF_2020-09-11_11-06-01.pdf?test%40key=test%40value&language=en-DE"
+    assert "headers" not in data
+    assert data["body"] == "[REDACTED]"
+
+
 def test_response_data():
     response = {"statusCode": "200", "headers": {"foo": "bar"}}
     data = get_data_from_response(response, capture_headers=True)
@@ -136,7 +161,6 @@ def test_response_data():
 
 
 def test_capture_serverless_api_gateway(event_api, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -159,7 +183,6 @@ def test_capture_serverless_api_gateway(event_api, context, elasticapm_client):
 
 
 def test_capture_serverless_api_gateway_v2(event_api2, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -181,8 +204,29 @@ def test_capture_serverless_api_gateway_v2(event_api2, context, elasticapm_clien
     assert transaction["context"]["response"]["status_code"] == 200
 
 
-def test_capture_serverless_s3(event_s3, context, elasticapm_client):
+def test_capture_serverless_elb(event_elb, context, elasticapm_client):
+    os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
+
+    @capture_serverless(elasticapm_client=elasticapm_client)
+    def test_func(event, context):
+        with capture_span("test_span"):
+            time.sleep(0.01)
+        return {"statusCode": 200, "headers": {"foo": "bar"}}
+
+    test_func(event_elb, context)
 
+    assert len(elasticapm_client.events[constants.TRANSACTION]) == 1
+    transaction = elasticapm_client.events[constants.TRANSACTION][0]
+
+    assert transaction["name"] == "POST /toolz/api/v2.0/downloadPDF/PDF_2020-09-11_11-06-01.pdf"
+    assert transaction["result"] == "HTTP 2xx"
+    assert transaction["span_count"]["started"] == 1
+    assert transaction["context"]["request"]["method"] == "POST"
+    assert transaction["context"]["request"]["headers"]
+    assert transaction["context"]["response"]["status_code"] == 200
+
+
+def test_capture_serverless_s3(event_s3, context, elasticapm_client):
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -201,7 +245,6 @@ def test_capture_serverless_s3(event_s3, context, elasticapm_client):
 
 
 def test_capture_serverless_sns(event_sns, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -222,7 +265,6 @@ def test_capture_serverless_sns(event_sns, context, elasticapm_client):
 
 
 def test_capture_serverless_sqs(event_sqs, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -243,7 +285,6 @@ def test_capture_serverless_sqs(event_sqs, context, elasticapm_client):
 
 
 def test_capture_serverless_s3_batch(event_s3_batch, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)
@@ -263,7 +304,6 @@ def test_capture_serverless_s3_batch(event_s3_batch, context, elasticapm_client)
 
 @pytest.mark.parametrize("elasticapm_client", [{"service_name": "override"}], indirect=True)
 def test_service_name_override(event_api, context, elasticapm_client):
-
     os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func"
 
     @capture_serverless(elasticapm_client=elasticapm_client)