From 5e688badd5a56c8ea688dc73c062a0a7e7c76382 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Mon, 18 Oct 2021 11:53:58 -0600 Subject: [PATCH 1/9] Set transaction result for error cases in Lambda --- elasticapm/contrib/serverless/aws.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 2c6a822b8..711cdcfd4 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -160,9 +160,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): """ Transaction teardown """ - if exc_val: - self.client.capture_exception(exc_info=(exc_type, exc_val, exc_tb), handled=False) - if self.response and isinstance(self.response, dict): elasticapm.set_context( lambda: get_data_from_response(self.response, capture_headers=self.client.config.capture_headers), @@ -180,6 +177,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): result = "HTTP {}xx".format(int(status_code) // 100) elasticapm.set_transaction_result(result, override=False) + if exc_val: + self.client.capture_exception(exc_info=(exc_type, exc_val, exc_tb), handled=False) + 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") + self.client.end_transaction() try: From 1f424a9d59772ad37514d25dc10d356d9482ff15 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Tue, 19 Oct 2021 10:45:58 -0600 Subject: [PATCH 2/9] Only use HTTP result/outcome for API Gateway --- elasticapm/contrib/serverless/aws.py | 10 +++++++--- elasticapm/traces.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 711cdcfd4..3d7bed394 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -179,9 +179,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): if exc_val: self.client.capture_exception(exc_info=(exc_type, exc_val, exc_tb), handled=False) - 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") + if self.source == "api": + 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") + else: + elasticapm.set_transaction_result("failure", override=False) + elasticapm.set_transaction_outcome(outcome="failure", override=False) self.client.end_transaction() diff --git a/elasticapm/traces.py b/elasticapm/traces.py index 3656717ce..03673368d 100644 --- a/elasticapm/traces.py +++ b/elasticapm/traces.py @@ -935,7 +935,7 @@ def set_transaction_name(name, override=True): def set_transaction_result(result, override=True): """ Sets the result of the transaction. The result could be e.g. the HTTP status class (e.g "HTTP 5xx") for - HTTP requests, or "success"/"fail" for background tasks. + HTTP requests, or "success"/"failure" for background tasks. :param name: the name of the transaction :param override: if set to False, the name is only set if no name has been set before From db5dd711d3eea486c986b5960c848f5a96419604 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Tue, 19 Oct 2021 10:57:03 -0600 Subject: [PATCH 3/9] Enable (and fix) server version fetching --- elasticapm/conf/constants.py | 2 +- elasticapm/contrib/serverless/aws.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/elasticapm/conf/constants.py b/elasticapm/conf/constants.py index c2a8bb641..4f7a24927 100644 --- a/elasticapm/conf/constants.py +++ b/elasticapm/conf/constants.py @@ -59,7 +59,7 @@ def _starmatch_to_regex(pattern): EVENTS_API_PATH = "intake/v2/events" AGENT_CONFIG_PATH = "config/v1/agents" -SERVER_INFO_PATH = "/" +SERVER_INFO_PATH = "" TRACE_CONTEXT_VERSION = 0 TRACEPARENT_HEADER_NAME = "traceparent" diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 3d7bed394..a028a6338 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -79,8 +79,6 @@ def __init__(self, name=None, **kwargs): kwargs["central_config"] = False kwargs["cloud_provider"] = "none" kwargs["framework_name"] = "AWS Lambda" - # TODO this can probably be removed once the extension proxies the serverinfo endpoint - kwargs["server_version"] = (8, 0, 0) if "service_name" not in kwargs: kwargs["service_name"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"] From 4f4085971f18871506e00bb37b52f12a490e4d41 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 20 Oct 2021 11:06:23 -0600 Subject: [PATCH 4/9] Add lambda docs (+elasticapm.get_client docs) --- docs/api.asciidoc | 15 ++++++++++ docs/serverless.asciidoc | 63 ++++++++++++++++++++++++++++++++++++++++ docs/set-up.asciidoc | 5 +++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 docs/serverless.asciidoc diff --git a/docs/api.asciidoc b/docs/api.asciidoc index a8ec88dda..f4229ef3f 100644 --- a/docs/api.asciidoc +++ b/docs/api.asciidoc @@ -34,6 +34,21 @@ client = Client({'SERVICE_NAME': 'example'}, **defaults) NOTE: framework integrations like <> and <> instantiate the client automatically. +[float] +[[api-get-client]] +===== `elasticapm.get_client()` + +[small]#Added in v6.1.0. + +Retrieves the `Client` singleton. This is useful for many framework integrations, +where the client is instantiated automatically. + +[source,python] +---- +client = elasticapm.get_client() +client.capture_message('foo') +---- + [float] [[error-api]] ==== Errors diff --git a/docs/serverless.asciidoc b/docs/serverless.asciidoc new file mode 100644 index 000000000..f09f7f87b --- /dev/null +++ b/docs/serverless.asciidoc @@ -0,0 +1,63 @@ +[[lambda-support]] +=== AWS Lambda Support + +Incorporating Elastic APM into your AWS Lambda functions is easy! + +[float] +[[lambda-installation]] +==== Installation + +First, you need to add `elastic-apm` as a dependency for your python function. +Depending on your deployment strategy, this could be as easy as adding +`elastic-apm` to your `requirements.txt` file, or installing it in the directory +you plan to deploy using pip: + +[source,bash] +---- +$ pip install -t elastic-apm +---- + +You should also add the +https://github.com/elastic/apm-aws-lambda[Elastic AWS Lambda Extension layer] +to your function. + +[float] +[[lambda-setup]] +==== Setup + +Once the library is included as a dependency in your function, you must +import the `capture_serverless` decorator and apply it to your handler: + +[source,python] +---- +from elasticapm import capture_serverless + +@capture_serverless() +def handler(event, context): + return {"statusCode": r.status_code, "body": "Success!"} +---- + +The agent uses environment variables for <> + +[source] +---- +ELASTIC_APM_LAMBDA_APM_SERVER= +ELASTIC_APM_SECRET_TOKEN= +ELASTIC_APM_SERVICE_NAME=my-awesome-service +---- + +Note that the above configuration assumes you're using the Elastic Lambda +Extension. The agent will automatically send data to the extension at `localhost`, +and the extension will then send to the APM Server as specified with +`ELASTIC_APM_LAMBDA_APM_SERVER`. + +[float] +[[lambda-usage]] +==== Usage + +Once the agent is installed and working, spans will be captured for +<>. You can also use +<> to capture custom spans, and +you can retrieve the `Client` object for capturing exceptions/messages +using <>. + diff --git a/docs/set-up.asciidoc b/docs/set-up.asciidoc index ee7fe788c..2dd223971 100644 --- a/docs/set-up.asciidoc +++ b/docs/set-up.asciidoc @@ -8,6 +8,7 @@ To get you off the ground, we’ve prepared guides for setting up the Agent with * <> * <> * <> + * <> For custom instrumentation, see <>. @@ -19,4 +20,6 @@ include::./aiohttp-server.asciidoc[] include::./tornado.asciidoc[] -include::./starlette.asciidoc[] \ No newline at end of file +include::./starlette.asciidoc[] + +include::./serverless.asciidoc[] \ No newline at end of file From bcf2fc1e4b5f6609405a5969f651e98edb7e3121 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 27 Oct 2021 10:54:02 -0600 Subject: [PATCH 5/9] Move faas from metadata to top-level on the transaction --- elasticapm/contrib/serverless/aws.py | 4 ++-- elasticapm/traces.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index a028a6338..39d900f62 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -282,8 +282,6 @@ def set_metadata_and_context(self, coldstart): cloud_context["origin"]["region"] = record["awsRegion"] cloud_context["origin"]["provider"] = "aws" - metadata["faas"] = faas - metadata["service"] = {} metadata["service"]["name"] = os.environ.get("AWS_LAMBDA_FUNCTION_NAME") metadata["service"]["framework"] = {"name": "AWS Lambda"} @@ -312,6 +310,8 @@ def set_metadata_and_context(self, coldstart): elasticapm.set_context(cloud_context, "cloud") elasticapm.set_context(service_context, "service") + # faas doesn't actually belong in context, but we handle this in to_dict + elasticapm.set_context(faas, "faas") if message_context: elasticapm.set_context(service_context, "message") self.client._transport.add_metadata(metadata) diff --git a/elasticapm/traces.py b/elasticapm/traces.py index 03673368d..411cf92e6 100644 --- a/elasticapm/traces.py +++ b/elasticapm/traces.py @@ -374,6 +374,9 @@ def to_dict(self) -> dict: # only set parent_id if this transaction isn't the root if self.trace_parent.span_id and self.trace_parent.span_id != self.id: result["parent_id"] = self.trace_parent.span_id + # faas context belongs top-level on the transaction + if "faas" in self.context: + result["faas"] = self.context.pop("faas") if self.is_sampled: result["context"] = self.context return result From 118f9e98e068f601a6cc328a147c47461c08235f Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Mon, 1 Nov 2021 10:09:02 -0600 Subject: [PATCH 6/9] Fix for possibly-missing message attributes (used as headers) --- elasticapm/contrib/serverless/aws.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 39d900f62..4e94da615 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -241,7 +241,7 @@ def set_metadata_and_context(self, coldstart): message_context["age"] = int((time.time() * 1000) - int(record["attributes"]["SentTimestamp"])) if self.client.config.capture_body in ("transactions", "all") and "body" in record: message_context["body"] = record["body"] - if self.client.config.capture_headers and record["messageAttributes"]: + if self.client.config.capture_headers and record.get("messageAttributes"): message_context["headers"] = record["messageAttributes"] elif self.source == "sns": record = self.event["Records"][0] @@ -267,7 +267,7 @@ def set_metadata_and_context(self, coldstart): ) if self.client.config.capture_body in ("transactions", "all") and "Message" in record["Sns"]: message_context["body"] = record["Sns"]["Message"] - if self.client.config.capture_headers and record["Sns"]["MessageAttributes"]: + if self.client.config.capture_headers and record["Sns"].get("MessageAttributes"): message_context["headers"] = record["Sns"]["MessageAttributes"] elif self.source == "s3": record = self.event["Records"][0] From 593276f427ef419c50283ae45d237553801c4dc8 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Mon, 1 Nov 2021 11:21:44 -0600 Subject: [PATCH 7/9] Add API v2 sample payload and update data collection to match --- elasticapm/contrib/serverless/aws.py | 42 ++++++++++++------- .../serverless/aws_api2_test_data.json | 35 ++++++++++++++++ tests/contrib/serverless/aws_tests.py | 38 ++++++++++++++++- 3 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 tests/contrib/serverless/aws_api2_test_data.json diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 4e94da615..5e81b685d 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -121,10 +121,13 @@ def __enter__(self): transaction_type = "request" transaction_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", self.name) - if "httpMethod" in self.event: # API Gateway + self.httpmethod = self.event.get("requestContext", {}).get( + "httpMethod", self.event.get("requestContext", {}).get("http", {}).get("method") + ) + if self.httpmethod: # API Gateway self.source = "api" if os.environ.get("AWS_LAMBDA_FUNCTION_NAME"): - transaction_name = "{} {}".format(self.event["httpMethod"], os.environ["AWS_LAMBDA_FUNCTION_NAME"]) + transaction_name = "{} {}".format(self.httpmethod, os.environ["AWS_LAMBDA_FUNCTION_NAME"]) else: transaction_name = self.name elif "Records" in self.event and len(self.event["Records"]) == 1: @@ -211,15 +214,19 @@ def set_metadata_and_context(self, coldstart): if self.source == "api": faas["trigger"]["type"] = "http" faas["trigger"]["request_id"] = self.event["requestContext"]["requestId"] + path = ( + self.event["requestContext"].get("resourcePath") + or self.event["requestContext"]["http"]["path"].split(self.event["requestContext"]["stage"])[-1] + ) service_context["origin"] = { "name": "{} {}/{}".format( - self.event["requestContext"]["httpMethod"], - self.event["requestContext"]["resourcePath"], + self.httpmethod, + path, self.event["requestContext"]["stage"], ) } service_context["origin"]["id"] = self.event["requestContext"]["apiId"] - service_context["origin"]["version"] = "2.0" if self.event["headers"]["Via"].startswith("2.0") else "1.0" + service_context["origin"]["version"] = self.event.get("version", "1.0") cloud_context["origin"] = {} cloud_context["origin"]["service"] = {"name": "api gateway"} cloud_context["origin"]["account"] = {"id": self.event["requestContext"]["accountId"]} @@ -324,12 +331,15 @@ def get_data_from_request(event, capture_body=False, capture_headers=True): result = {} if capture_headers and "headers" in event: result["headers"] = event["headers"] - if "httpMethod" not in event: + method = event.get("requestContext", {}).get( + "httpMethod", event.get("requestContext", {}).get("http", {}).get("method") + ) + if not method: # Not API Gateway return result - result["method"] = event["httpMethod"] - if event["httpMethod"] in constants.HTTP_WITH_BODY and "body" in event: + result["method"] = method + if method in constants.HTTP_WITH_BODY and "body" in event: body = event["body"] if capture_body: if event.get("isBase64Encoded"): @@ -367,21 +377,23 @@ def get_url_dict(event): Reconstruct URL from API Gateway """ headers = event.get("headers", {}) - proto = headers.get("X-Forwarded-Proto", "https") - host = headers.get("Host", "") - path = event.get("path", "") - port = headers.get("X-Forwarded-Port") + protocol = headers.get("X-Forwarded-Proto", headers.get("x-forwarded-proto", "https")) + host = headers.get("Host", headers.get("host", "")) stage = "/" + event.get("requestContext", {}).get("stage", "") + path = event.get("path", event.get("rawPath", "").split(stage)[-1]) + port = headers.get("X-Forwarded-Port", headers.get("x-forwarded-port")) query = "" - if event.get("queryStringParameters"): + if "rawQueryString" in event: + query = event["rawQueryString"] + elif event.get("queryStringParameters"): query = "?" for k, v in compat.iteritems(event["queryStringParameters"]): query += "{}={}".format(k, v) - url = proto + "://" + host + stage + path + query + url = protocol + "://" + host + stage + path + query url_dict = { "full": encoding.keyword_field(url), - "protocol": proto, + "protocol": protocol, "hostname": encoding.keyword_field(host), "pathname": encoding.keyword_field(stage + path), } diff --git a/tests/contrib/serverless/aws_api2_test_data.json b/tests/contrib/serverless/aws_api2_test_data.json new file mode 100644 index 000000000..aa470107a --- /dev/null +++ b/tests/contrib/serverless/aws_api2_test_data.json @@ -0,0 +1,35 @@ +{ + "version": "2.0", + "routeKey": "ANY /fetch_all", + "rawPath": "/dev/fetch_all", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "content-length": "0", + "host": "02plqthge2.execute-api.us-east-1.amazonaws.com", + "user-agent": "curl/7.64.1", + "x-amzn-trace-id": "Root=1-618018c5-763ade2b18f5734547c93e98", + "x-forwarded-for": "67.171.184.49", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "627286350134", + "apiId": "02plqthge2", + "domainName": "02plqthge2.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "02plqthge2", + "http": { + "method": "GET", + "path": "/dev/fetch_all", + "protocol": "HTTP/1.1", + "sourceIp": "67.171.184.49", + "userAgent": "curl/7.64.1" + }, + "requestId": "IIjO5hs7PHcEPIA=", + "routeKey": "ANY /fetch_all", + "stage": "dev", + "time": "01/Nov/2021:16:41:41 +0000", + "timeEpoch": 1635784901594 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/tests/contrib/serverless/aws_tests.py b/tests/contrib/serverless/aws_tests.py index c5a48d966..b65927e0a 100644 --- a/tests/contrib/serverless/aws_tests.py +++ b/tests/contrib/serverless/aws_tests.py @@ -46,6 +46,13 @@ def event_api(): return json.load(f) +@pytest.fixture +def event_api2(): + aws_data_file = os.path.join(os.path.dirname(__file__), "aws_api2_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") @@ -89,13 +96,19 @@ def __init__(self): self.aws_request_id = "12345" -def test_request_data(event_api): +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["headers"]["Host"] == "02plqthge2.execute-api.us-east-1.amazonaws.com" + data = get_data_from_request(event_api2, 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["headers"]["host"] == "02plqthge2.execute-api.us-east-1.amazonaws.com" + data = get_data_from_request(event_api, capture_body=False, capture_headers=False) assert data["method"] == "GET" @@ -144,6 +157,29 @@ def test_func(event, context): assert transaction["context"]["response"]["status_code"] == 200 +def test_capture_serverless_api_gateway_v2(event_api2, context, elasticapm_client): + + os.environ["AWS_LAMBDA_FUNCTION_NAME"] = "test_func" + + @capture_serverless() + def test_func(event, context): + with capture_span("test_span"): + time.sleep(0.01) + return {"statusCode": 200, "headers": {"foo": "bar"}} + + test_func(event_api2, context) + + assert len(elasticapm_client.events[constants.TRANSACTION]) == 1 + transaction = elasticapm_client.events[constants.TRANSACTION][0] + + assert transaction["name"] == "GET test_func" + assert transaction["result"] == "HTTP 2xx" + assert transaction["span_count"]["started"] == 1 + assert transaction["context"]["request"]["method"] == "GET" + 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" From c2bc9116ea663ebfe113a14389310a2d3683c977 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Tue, 2 Nov 2021 11:08:24 -0600 Subject: [PATCH 8/9] Use nested_key and add docstring --- elasticapm/contrib/serverless/aws.py | 14 ++++++-------- elasticapm/utils/__init__.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 5e81b685d..27cc5f372 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -39,7 +39,7 @@ import elasticapm from elasticapm.base import Client, get_client from elasticapm.conf import constants -from elasticapm.utils import compat, encoding, get_name_from_func +from elasticapm.utils import compat, encoding, get_name_from_func, nested_key from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger @@ -121,8 +121,8 @@ def __enter__(self): transaction_type = "request" transaction_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", self.name) - self.httpmethod = self.event.get("requestContext", {}).get( - "httpMethod", self.event.get("requestContext", {}).get("http", {}).get("method") + self.httpmethod = nested_key(self.event, "requestContext", "httpMethod") or nested_key( + self.event, "requestContext", "http", "method" ) if self.httpmethod: # API Gateway self.source = "api" @@ -305,7 +305,7 @@ def set_metadata_and_context(self, coldstart): # This is the one piece of metadata that requires deep merging. We add it manually # here to avoid having to deep merge in _transport.add_metadata() if self.client._transport._metadata: - node_name = self.client._transport._metadata.get("service", {}).get("node", {}).get("name") + node_name = nested_key(self.client._transport._metadata, "service", "node", "name") if node_name: metadata["service"]["node"]["name"] = node_name @@ -331,9 +331,7 @@ def get_data_from_request(event, capture_body=False, capture_headers=True): result = {} if capture_headers and "headers" in event: result["headers"] = event["headers"] - method = event.get("requestContext", {}).get( - "httpMethod", event.get("requestContext", {}).get("http", {}).get("method") - ) + method = nested_key(event, "requestContext", "httpMethod") or nested_key(event, "requestContext", "http", "method") if not method: # Not API Gateway return result @@ -379,7 +377,7 @@ def get_url_dict(event): headers = event.get("headers", {}) protocol = headers.get("X-Forwarded-Proto", headers.get("x-forwarded-proto", "https")) host = headers.get("Host", headers.get("host", "")) - stage = "/" + event.get("requestContext", {}).get("stage", "") + stage = "/" + (nested_key(event, "requestContext", "stage") or "") path = event.get("path", event.get("rawPath", "").split(stage)[-1]) port = headers.get("X-Forwarded-Port", headers.get("x-forwarded-port")) query = "" diff --git a/elasticapm/utils/__init__.py b/elasticapm/utils/__init__.py index 1fda0125d..fd217c438 100644 --- a/elasticapm/utils/__init__.py +++ b/elasticapm/utils/__init__.py @@ -198,6 +198,19 @@ def starmatch_to_regex(pattern: str) -> Pattern: def nested_key(d: dict, *args): + """ + Traverses a dictionary for nested keys. Returns `None` if the at any point + in the traversal a key cannot be found. + + Example: + + >>> from elasticapm.utils import nested_key + >>> d = {"a": {"b": {"c": 0}}} + >>> nested_key(d, "a", "b", "c") + 0 + >>> nested_key(d, "a", "b", "d") + None + """ for arg in args: try: d = d[arg] From 65b93e76dc2b4233ce5838b5dbdc93f27846fbcd Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Tue, 2 Nov 2021 11:13:56 -0600 Subject: [PATCH 9/9] CHANGELOG + experimental tag in docs --- CHANGELOG.asciidoc | 12 ++++++++++++ docs/serverless.asciidoc | 2 ++ elasticapm/contrib/serverless/aws.py | 3 --- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 55ce8a846..85a6e4399 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -28,6 +28,18 @@ endif::[] // //[float] //===== 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 + + +[float] +===== Bug fixes + +* Fix some context fields and metadata handling in AWS Lambda support {pull}1368[#1368] [[release-notes-6.x]] diff --git a/docs/serverless.asciidoc b/docs/serverless.asciidoc index f09f7f87b..a477fd786 100644 --- a/docs/serverless.asciidoc +++ b/docs/serverless.asciidoc @@ -1,6 +1,8 @@ [[lambda-support]] === AWS Lambda Support +experimental::[] + Incorporating Elastic APM into your AWS Lambda functions is easy! [float] diff --git a/elasticapm/contrib/serverless/aws.py b/elasticapm/contrib/serverless/aws.py index 27cc5f372..6aff53eea 100644 --- a/elasticapm/contrib/serverless/aws.py +++ b/elasticapm/contrib/serverless/aws.py @@ -63,9 +63,6 @@ class capture_serverless(object): @capture_serverless() def handler(event, context): return {"statusCode": r.status_code, "body": "Success!"} - - Note: This is an experimental feature, and we may introduce breaking - changes in the future. """ def __init__(self, name=None, **kwargs):