diff --git a/datadog_lambda/constants.py b/datadog_lambda/constants.py index 9bd49ce3..5fb6683d 100644 --- a/datadog_lambda/constants.py +++ b/datadog_lambda/constants.py @@ -42,3 +42,15 @@ class XrayDaemon(object): XRAY_TRACE_ID_HEADER_NAME = "_X_AMZN_TRACE_ID" XRAY_DAEMON_ADDRESS = "AWS_XRAY_DAEMON_ADDRESS" FUNCTION_NAME_HEADER_NAME = "AWS_LAMBDA_FUNCTION_NAME" + + +class Headers(object): + Parent_Span_Finish_Time = "x-datadog-parent-span-finish-time" + + # For one request from the client, the event.requestContext.requestIds in the authorizer lambda + # invocation and the main function invocation are IDENTICAL. Therefore we can use it to tell + # whether current invocation is the actual original authorizing request or a cached request. + Authorizing_Request_Id = "x-datadog-authorizing-requestid" + + # injected by the HTTPPropagator.inject but no use + TAGS_HEADER_TO_DELETE = "x-datadog-tags" diff --git a/datadog_lambda/tracing.py b/datadog_lambda/tracing.py index 1e63a78b..62c64c61 100644 --- a/datadog_lambda/tracing.py +++ b/datadog_lambda/tracing.py @@ -23,16 +23,18 @@ TraceHeader, TraceContextSource, XrayDaemon, + Headers, ) from datadog_lambda.xray import ( send_segment, parse_xray_header, ) -from ddtrace import tracer, patch +from ddtrace import tracer, patch, Span from ddtrace import __version__ as ddtrace_version from ddtrace.propagation.http import HTTPPropagator from datadog_lambda import __version__ as datadog_lambda_version from datadog_lambda.trigger import ( + _EventSource, parse_event_source, get_first_record, EventTypes, @@ -167,13 +169,39 @@ def extract_context_from_lambda_context(lambda_context): return trace_id, parent_id, sampling_priority -def extract_context_from_http_event_or_context(event, lambda_context): +def extract_context_from_http_event_or_context( + event, + lambda_context, + event_source: _EventSource, + decode_authorizer_context: bool = True, +): """ Extract Datadog trace context from the `headers` key in from the Lambda `event` object. Falls back to lambda context if no trace data is found in the `headers` """ + if decode_authorizer_context: + is_http_api = event_source.equals( + EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API + ) + injected_authorizer_data = get_injected_authorizer_data(event, is_http_api) + if injected_authorizer_data: + try: + # fail fast on any KeyError here + trace_id = injected_authorizer_data[TraceHeader.TRACE_ID] + parent_id = injected_authorizer_data[TraceHeader.PARENT_ID] + sampling_priority = injected_authorizer_data[ + TraceHeader.SAMPLING_PRIORITY + ] + return trace_id, parent_id, sampling_priority + except Exception as e: + logger.debug( + "extract_context_from_authorizer_event returned with error. \ + Continue without injecting the authorizer span %s", + e, + ) + headers = event.get("headers", {}) or {} lowercase_headers = {k.lower(): v for k, v in headers.items()} @@ -317,7 +345,45 @@ def extract_context_custom_extractor(extractor, event, lambda_context): return None, None, None -def extract_dd_trace_context(event, lambda_context, extractor=None): +def get_injected_authorizer_data(event, is_http_api) -> dict: + try: + authorizer_headers = event.get("requestContext", {}).get("authorizer") + if not authorizer_headers: + return None + + dd_data_raw = ( + authorizer_headers.get("lambda", {}).get("_datadog") + if is_http_api + else authorizer_headers.get("_datadog") + ) + + if not dd_data_raw: + return None + + injected_data = json.loads(base64.b64decode(dd_data_raw)) + + # Lambda authorizer's results can be cached. But the payload will still have the injected + # data in cached requests. How to distinguish cached case and ignore the injected data ? + # APIGateway automatically injects a integrationLatency data in some cases. If it's >0 we + # know that it's not cached. But integrationLatency is not available for Http API case. In + # that case, we use the injected Authorizing_Request_Id to tell if it's cached. But token + # authorizers don't pass on the requestId. The Authorizing_Request_Id can't work for all + # cases neither. As a result, we combine both methods as shown below. + if authorizer_headers.get("integrationLatency", 0) > 0 or event.get( + "requestContext", {} + ).get("requestId") == injected_data.get(Headers.Authorizing_Request_Id): + return injected_data + else: + return None + + except Exception as e: + logger.debug("Failed to check if invocated by an authorizer. error %s", e) + return None + + +def extract_dd_trace_context( + event, lambda_context, extractor=None, decode_authorizer_context: bool = True +): """ Extract Datadog trace context from the Lambda `event` object. @@ -339,7 +405,9 @@ def extract_dd_trace_context(event, lambda_context, extractor=None): trace_id, parent_id, sampling_priority, - ) = extract_context_from_http_event_or_context(event, lambda_context) + ) = extract_context_from_http_event_or_context( + event, lambda_context, event_source, decode_authorizer_context + ) elif event_source.equals(EventTypes.SNS) or event_source.equals(EventTypes.SQS): ( trace_id, @@ -379,7 +447,7 @@ def extract_dd_trace_context(event, lambda_context, extractor=None): if dd_trace_context: trace_context_source = TraceContextSource.XRAY logger.debug("extracted dd trace context %s", dd_trace_context) - return dd_trace_context, trace_context_source + return dd_trace_context, trace_context_source, event_source def get_dd_trace_context(): @@ -502,14 +570,22 @@ def set_dd_trace_py_root(trace_context_source, merge_xray_traces): ) -def create_inferred_span(event, context): - event_source = parse_event_source(event) +def create_inferred_span( + event, + context, + event_source: _EventSource = None, + decode_authorizer_context: bool = True, +): + if event_source is None: + event_source = parse_event_source(event) try: if event_source.equals( EventTypes.API_GATEWAY, subtype=EventSubtypes.API_GATEWAY ): logger.debug("API Gateway event detected. Inferring a span") - return create_inferred_span_from_api_gateway_event(event, context) + return create_inferred_span_from_api_gateway_event( + event, decode_authorizer_context + ) elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL): logger.debug("Function URL event detected. Inferring a span") return create_inferred_span_from_lambda_function_url_event(event, context) @@ -517,12 +593,16 @@ def create_inferred_span(event, context): EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API ): logger.debug("HTTP API event detected. Inferring a span") - return create_inferred_span_from_http_api_event(event, context) + return create_inferred_span_from_http_api_event( + event, context, decode_authorizer_context + ) elif event_source.equals( EventTypes.API_GATEWAY, subtype=EventSubtypes.WEBSOCKET ): logger.debug("API Gateway Websocket event detected. Inferring a span") - return create_inferred_span_from_api_gateway_websocket_event(event, context) + return create_inferred_span_from_api_gateway_websocket_event( + event, decode_authorizer_context + ) elif event_source.equals(EventTypes.SQS): logger.debug("SQS event detected. Inferring a span") return create_inferred_span_from_sqs_event(event, context) @@ -543,7 +623,7 @@ def create_inferred_span(event, context): return create_inferred_span_from_eventbridge_event(event, context) except Exception as e: logger.debug( - "Unable to infer span. Detected type: {}. Reason: {}", + "Unable to infer span. Detected type: %s. Reason: %s", event_source.to_string(), e, ) @@ -588,7 +668,72 @@ def is_api_gateway_invocation_async(event): return event.get("headers", {}).get("X-Amz-Invocation-Type") == "Event" -def create_inferred_span_from_api_gateway_websocket_event(event, context): +def insert_upstream_authorizer_span( + kwargs_to_start_span, other_tags_for_span, start_time_ns, finish_time_ns +): + """Insert the authorizer span. + Without this: parent span --child-> inferred span + With this insertion: parent span --child-> upstreamAuthorizerSpan --child-> inferred span + + Args: + kwargs_to_start_span (Dict): the same keyword arguments used for the inferred span + other_tags_for_span (Dict): the same tag keyword arguments used for the inferred span + start_time_ns (int): the start time of the span in nanoseconds + finish_time_ns (int): the finish time of the sapn in nanoseconds + """ + trace_ctx = tracer.current_trace_context() + upstream_authorizer_span = tracer.trace( + "aws.apigateway.authorizer", **kwargs_to_start_span + ) + upstream_authorizer_span.set_tags(other_tags_for_span) + upstream_authorizer_span.set_tag("operation_name", "aws.apigateway.authorizer") + # always sync for the authorizer invocation + InferredSpanInfo.set_tags_to_span(upstream_authorizer_span, synchronicity="sync") + upstream_authorizer_span.start_ns = int(start_time_ns) + upstream_authorizer_span.finish(finish_time_ns / 1e9) + # trace context needs to be set again as it is reset by finish() + tracer.context_provider.activate(trace_ctx) + return upstream_authorizer_span + + +def process_injected_data(event, request_time_epoch_ms, args, tags): + """ + This covers the ApiGateway RestAPI and Websocket cases. It doesn't cover Http API cases. + """ + injected_authorizer_data = get_injected_authorizer_data(event, False) + if injected_authorizer_data: + try: + start_time_ns = int( + injected_authorizer_data.get(Headers.Parent_Span_Finish_Time) + ) + finish_time_ns = ( + request_time_epoch_ms + + ( + int( + event["requestContext"]["authorizer"].get( + "integrationLatency", 0 + ) + ) + ) + ) * 1e6 + upstream_authorizer_span = insert_upstream_authorizer_span( + args, tags, start_time_ns, finish_time_ns + ) + return upstream_authorizer_span, finish_time_ns + except Exception as e: + logger.debug( + "Unable to insert authorizer span. Continue to generate the main span.\ + Reason: %s", + e, + ) + return None, None + else: + return None, None + + +def create_inferred_span_from_api_gateway_websocket_event( + event, decode_authorizer_context: bool = True +): request_context = event.get("requestContext") domain = request_context.get("domainName") endpoint = request_context.get("routeKey") @@ -605,7 +750,7 @@ def create_inferred_span_from_api_gateway_websocket_event(event, context): "event_type": request_context.get("eventType"), "message_direction": request_context.get("messageDirection"), } - request_time_epoch = request_context.get("requestTimeEpoch") + request_time_epoch_ms = int(request_context.get("requestTimeEpoch")) if is_api_gateway_invocation_async(event): InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="async") else: @@ -616,14 +761,28 @@ def create_inferred_span_from_api_gateway_websocket_event(event, context): "span_type": "web", } tracer.set_tags({"_dd.origin": "lambda"}) + upstream_authorizer_span = None + finish_time_ns = None + if decode_authorizer_context: + upstream_authorizer_span, finish_time_ns = process_injected_data( + event, request_time_epoch_ms, args, tags + ) span = tracer.trace("aws.apigateway.websocket", **args) if span: span.set_tags(tags) - span.start = request_time_epoch / 1000 + span.start_ns = int( + finish_time_ns + if finish_time_ns is not None + else request_time_epoch_ms * 1e6 + ) + if upstream_authorizer_span: + span.parent_id = upstream_authorizer_span.span_id return span -def create_inferred_span_from_api_gateway_event(event, context): +def create_inferred_span_from_api_gateway_event( + event, decode_authorizer_context: bool = True +): request_context = event.get("requestContext") domain = request_context.get("domainName", "") method = event.get("httpMethod") @@ -640,7 +799,7 @@ def create_inferred_span_from_api_gateway_event(event, context): "stage": request_context.get("stage"), "request_id": request_context.get("requestId"), } - request_time_epoch = request_context.get("requestTimeEpoch") + request_time_epoch_ms = int(request_context.get("requestTimeEpoch")) if is_api_gateway_invocation_async(event): InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="async") else: @@ -651,14 +810,29 @@ def create_inferred_span_from_api_gateway_event(event, context): "span_type": "http", } tracer.set_tags({"_dd.origin": "lambda"}) + upstream_authorizer_span = None + finish_time_ns = None + if decode_authorizer_context: + upstream_authorizer_span, finish_time_ns = process_injected_data( + event, request_time_epoch_ms, args, tags + ) span = tracer.trace("aws.apigateway", **args) if span: span.set_tags(tags) - span.start = request_time_epoch / 1000 + # start time pushed by the inserted authorizer span + span.start_ns = int( + finish_time_ns + if finish_time_ns is not None + else request_time_epoch_ms * 1e6 + ) + if upstream_authorizer_span: + span.parent_id = upstream_authorizer_span.span_id return span -def create_inferred_span_from_http_api_event(event, context): +def create_inferred_span_from_http_api_event( + event, context, decode_authorizer_context: bool = True +): request_context = event.get("requestContext") domain = request_context.get("domainName") method = request_context.get("http", {}).get("method") @@ -678,7 +852,7 @@ def create_inferred_span_from_http_api_event(event, context): "apiname": request_context.get("apiId"), "stage": request_context.get("stage"), } - request_time_epoch = request_context.get("timeEpoch") + request_time_epoch_ms = int(request_context.get("timeEpoch")) if is_api_gateway_invocation_async(event): InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="async") else: @@ -689,10 +863,17 @@ def create_inferred_span_from_http_api_event(event, context): "span_type": "http", } tracer.set_tags({"_dd.origin": "lambda"}) + inferred_span_start_ns = request_time_epoch_ms * 1e6 + if decode_authorizer_context: + injected_authorizer_data = get_injected_authorizer_data(event, True) + if injected_authorizer_data: + inferred_span_start_ns = injected_authorizer_data.get( + Headers.Parent_Span_Finish_Time + ) span = tracer.trace("aws.httpapi", **args) if span: span.set_tags(tags) - span.start = request_time_epoch / 1000 + span.start_ns = int(inferred_span_start_ns) return span @@ -973,14 +1154,39 @@ class InferredSpanInfo(object): SYNCHRONICITY = f"{BASE_NAME}.synchronicity" TAG_SOURCE = f"{BASE_NAME}.tag_source" - @classmethod + @staticmethod def set_tags( - cls, tags: Dict[str, str], synchronicity: Optional[Literal["sync", "async"]] = None, tag_source: Optional[Literal["labmda", "self"]] = None, ): if synchronicity is not None: - tags[cls.SYNCHRONICITY] = str(synchronicity) + tags[InferredSpanInfo.SYNCHRONICITY] = str(synchronicity) + if tag_source is not None: + tags[InferredSpanInfo.TAG_SOURCE] = str(tag_source) + + @staticmethod + def set_tags_to_span( + span: Span, + synchronicity: Optional[Literal["sync", "async"]] = None, + tag_source: Optional[Literal["labmda", "self"]] = None, + ): + if synchronicity is not None: + span.set_tags({InferredSpanInfo.SYNCHRONICITY: synchronicity}) if tag_source is not None: - tags[cls.TAG_SOURCE] = str(tag_source) + span.set_tags({InferredSpanInfo.TAG_SOURCE: str(tag_source)}) + + @staticmethod + def is_async(span: Span) -> bool: + if not span: + return False + try: + return span.get_tag(InferredSpanInfo.SYNCHRONICITY) == "async" + except Exception as e: + logger.debug( + "Unabled to read the %s tag, returning False. \ + Reason: %s.", + InferredSpanInfo.SYNCHRONICITY, + e, + ) + return False diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py index 88508ab3..f0d56a3d 100644 --- a/datadog_lambda/wrapper.py +++ b/datadog_lambda/wrapper.py @@ -3,16 +3,21 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2019 Datadog, Inc. +import base64 import os import logging import traceback from importlib import import_module +import json +from time import time_ns +from ddtrace.propagation.http import HTTPPropagator from datadog_lambda.extension import should_use_extension, flush_extension from datadog_lambda.cold_start import set_cold_start, is_cold_start from datadog_lambda.constants import ( TraceContextSource, XraySubsegment, + Headers, ) from datadog_lambda.metric import ( flush_stats, @@ -33,7 +38,10 @@ create_inferred_span, InferredSpanInfo, ) -from datadog_lambda.trigger import extract_trigger_tags, extract_http_status_code_tag +from datadog_lambda.trigger import ( + extract_trigger_tags, + extract_http_status_code_tag, +) from datadog_lambda.tag_object import tag_object profiling_env_var = os.environ.get("DD_PROFILING_ENABLED", "false").lower() == "true" @@ -116,6 +124,12 @@ def __init__(self, func): self.make_inferred_span = ( os.environ.get("DD_TRACE_MANAGED_SERVICES", "true").lower() == "true" ) + self.encode_authorizer_context = ( + os.environ.get("DD_ENCODE_AUTHORIZER_CONTEXT", "true").lower() == "true" + ) + self.decode_authorizer_context = ( + os.environ.get("DD_DECODE_AUTHORIZER_CONTEXT", "true").lower() == "true" + ) self.response = None if profiling_env_var: self.prof = profiler.Profiler(env=env_env_var, service=service_env_var) @@ -157,6 +171,30 @@ def __call__(self, event, context, **kwargs): finally: self._after(event, context) + def _inject_authorizer_span_headers(self, request_id): + reference_span = self.inferred_span if self.inferred_span else self.span + assert reference_span.finished + # the finish_time_ns should be set as the end of the inferred span if it exist + # or the end of the current span + finish_time_ns = ( + reference_span.start_ns + reference_span.duration_ns + if reference_span is not None + and hasattr(reference_span, "start_ns") + and hasattr(reference_span, "duration_ns") + else time_ns() + ) + injected_headers = {} + source_span = self.inferred_span if self.inferred_span else self.span + HTTPPropagator.inject(source_span.context, injected_headers) + # remove unused header + injected_headers.pop(Headers.TAGS_HEADER_TO_DELETE, None) + injected_headers[Headers.Parent_Span_Finish_Time] = finish_time_ns + if request_id is not None: + injected_headers[Headers.Authorizing_Request_Id] = request_id + datadog_data = base64.b64encode(json.dumps(injected_headers).encode()) + self.response.setdefault("context", {}) + self.response["context"]["_datadog"] = datadog_data + def _before(self, event, context): try: self.response = None @@ -164,9 +202,13 @@ def _before(self, event, context): submit_invocations_metric(context) self.trigger_tags = extract_trigger_tags(event, context) # Extract Datadog trace context and source from incoming requests - dd_context, trace_context_source = extract_dd_trace_context( - event, context, extractor=self.trace_extractor + dd_context, trace_context_source, event_source = extract_dd_trace_context( + event, + context, + extractor=self.trace_extractor, + decode_authorizer_context=self.decode_authorizer_context, ) + self.event_source = event_source # Create a Datadog X-Ray subsegment with the trace context if dd_context and trace_context_source == TraceContextSource.EVENT: create_dd_dummy_metadata_subsegment( @@ -176,7 +218,9 @@ def _before(self, event, context): if dd_tracing_enabled: set_dd_trace_py_root(trace_context_source, self.merge_xray_traces) if self.make_inferred_span: - self.inferred_span = create_inferred_span(event, context) + self.inferred_span = create_inferred_span( + event, context, event_source, self.decode_authorizer_context + ) self.span = create_function_execution_span( context, self.function_name, @@ -221,11 +265,7 @@ def _after(self, event, context): if status_code: self.inferred_span.set_tag("http.status_code", status_code) - if ( - self.inferred_span.get_tag(InferredSpanInfo.SYNCHRONICITY) - == "async" - and self.span - ): + if InferredSpanInfo.is_async(self.inferred_span) and self.span: self.inferred_span.finish(finish_time=self.span.start) else: self.inferred_span.finish() @@ -234,6 +274,16 @@ def _after(self, event, context): flush_stats() if should_use_extension: flush_extension() + + if ( + self.encode_authorizer_context + and self.response + and self.response.get("principalId") + and self.response.get("policyDocument") + ): + self._inject_authorizer_span_headers( + event.get("requestContext", {}).get("requestId") + ) logger.debug("datadog_lambda_wrapper _after() done") except Exception: traceback.print_exc() diff --git a/tests/event_samples/authorizer-request-api-gateway-v1-cached.json b/tests/event_samples/authorizer-request-api-gateway-v1-cached.json new file mode 100644 index 00000000..937e94b6 --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-v1-cached.json @@ -0,0 +1,93 @@ +{ + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "secretT0k3n", + "authorizationToken": "secretT0k3n", + "Cache-Control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-ASN": "12271", + "CloudFront-Viewer-Country": "US", + "Host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "Postman-Token": "5ddd364a-4191-4dbc-9831-09f1bf46bca8", + "User-Agent": "PostmanRuntime/7.29.2", + "Via": "1.1 x.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "ToqncSIka6Ae1pZlyxEOtyqqcns8cwL3hVxwo0ZZqMC60zTSSXW7yA==", + "X-Amzn-Trace-Id": "Root=1-63580c1d-12aa2dcf30cd2d3476a6f18f", + "X-Forwarded-For": "24.193.182.233, 15.158.35.232", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Accept-Encoding": ["gzip, deflate, br"], + "Authorization": ["secretT0k3n"], + "authorizationToken": ["secretT0k3n"], + "Cache-Control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-ASN": ["12271"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["amddr1rix9.execute-api.sa-east-1.amazonaws.com"], + "Postman-Token": ["5ddd364a-4191-4dbc-9831-09f1bf46bca8"], + "User-Agent": ["PostmanRuntime/7.29.2"], + "Via": ["1.1 x.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["ToqncSIka6Ae1pZlyxEOtyqqcns8cwL3hVxwo0ZZqMC60zTSSXW7yA=="], + "X-Amzn-Trace-Id": ["Root=1-63580c1d-12aa2dcf30cd2d3476a6f18f"], + "X-Forwarded-For": ["24.193.182.233, 15.158.35.232"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "var16m", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMzQyMDA1NDg3OTQxNzk5OTg3NyIsICJ4LWRhdGFkb2ctcGFyZW50LWlkIjogIjE1NDY2ODY0ODU4OTY3MTY5NjM2IiwgIngtZGF0YWRvZy1zYW1wbGluZy1wcmlvcml0eSI6ICIxIiwgIngtZGF0YWRvZy1wYXJlbnQtc3Bhbi1maW5pc2gtdGltZSI6IDE2NjY3MTQ2NDg1OTAuMTA5NCwgIngtZGF0YWRvZy1hdXRob3JpemluZy1yZXF1ZXN0aWQiOiAiMjRkNjk4OTItNjcwMi00OTYxLThlNDgtMDg1NWNjNTU0MjQwIn0=", + "scope": "this is just a string", + "principalId": "foo", + "integrationLatency": 0 + }, + "resourcePath": "/hello", + "httpMethod": "GET", + "extendedRequestId": "akbUqFBZmjQFbJg=", + "requestTime": "25/Oct/2022:16:17:33 +0000", + "path": "/dev/hello", + "accountId": "425362996713", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "amddr1rix9", + "requestTimeEpoch": 1666714653636, + "requestId": "abc123", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "24.193.182.233", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "PostmanRuntime/7.29.2", + "user": null + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "apiId": "amddr1rix9" + }, + "body": null, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-request-api-gateway-v1.json b/tests/event_samples/authorizer-request-api-gateway-v1.json new file mode 100644 index 00000000..00818eac --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-v1.json @@ -0,0 +1,93 @@ +{ + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "secretT0k3n", + "authorizationToken": "secretT0k3n", + "Cache-Control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-ASN": "12271", + "CloudFront-Viewer-Country": "US", + "Host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "Postman-Token": "75a5e2b0-d8e8-4370-ad2c-7209761c3f43", + "User-Agent": "PostmanRuntime/7.29.2", + "Via": "1.1 ed4584f7c263c11cf4adf75ba3a25764.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "wbruuv68N6jFldH7oUaE9kWABzHsvPi3zzByABVGXCVsnGYOhcRjow==", + "X-Amzn-Trace-Id": "Root=1-6323de2b-0155211e49422a532809a10e", + "X-Forwarded-For": "24.193.182.233, 15.158.35.8", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Accept-Encoding": ["gzip, deflate, br"], + "Authorization": ["secretT0k3n"], + "authorizationToken": ["secretT0k3n"], + "Cache-Control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-ASN": ["12271"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["amddr1rix9.execute-api.sa-east-1.amazonaws.com"], + "Postman-Token": ["75a5e2b0-d8e8-4370-ad2c-7209761c3f43"], + "User-Agent": ["PostmanRuntime/7.29.2"], + "Via": ["1.1 ed4584f7c263c11cf4adf75ba3a25764.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["wbruuv68N6jFldH7oUaE9kWABzHsvPi3zzByABVGXCVsnGYOhcRjow=="], + "X-Amzn-Trace-Id": ["Root=1-6323de2b-0155211e49422a532809a10e"], + "X-Forwarded-For": ["24.193.182.233, 15.158.35.8"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "0et54l", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTM0Nzg3MDU5OTU3OTcyMjEyMDkiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICI4NDcxMjg4MjYzMzg0MjE2ODk2IiwgIngtZGF0YWRvZy1zYW1wbGluZy1wcmlvcml0eSI6ICIxIiwgIngtZGF0YWRvZy1wYXJlbnQtc3Bhbi1maW5pc2gtdGltZSI6IDE2NjMyOTUwMjE4MjcuNTIxLCAieC1kYXRhZG9nLWF1dGhvcml6aW5nLXJlcXVlc3RpZCI6ICJhYmMxMjMifQ==", + "scope": "this is just a string", + "principalId": "foo", + "integrationLatency": 1897 + }, + "resourcePath": "/hello", + "httpMethod": "GET", + "extendedRequestId": "Yh-m5G_wGjQFftQ=", + "requestTime": "16/Sep/2022:02:23:39 +0000", + "path": "/dev/hello", + "accountId": "601427279990", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "amddr1rix9", + "requestTimeEpoch": 1663295019935, + "requestId": "abc123", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "24.193.182.233", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "PostmanRuntime/7.29.2", + "user": null + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "apiId": "amddr1rix9" + }, + "body": null, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-request-api-gateway-v2-cached.json b/tests/event_samples/authorizer-request-api-gateway-v2-cached.json new file mode 100644 index 00000000..53ee1624 --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-v2-cached.json @@ -0,0 +1,47 @@ +{ + "version": "2.0", + "routeKey": "GET /hello", + "rawPath": "/hello", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "authorization": "secretT0k3n", + "authorizationtoken": "secretT0k3n", + "cache-control": "no-cache", + "content-length": "0", + "host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "postman-token": "f901530c-cc2f-475f-8634-d9156e9cd9d7", + "user-agent": "PostmanRuntime/7.29.2", + "userid": "1236", + "x-amzn-trace-id": "Root=1-63580f25-5f01511071135d5966c8c63d", + "x-forwarded-for": "24.193.182.233", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "425362996713", + "apiId": "amddr1rix9", + "authorizer": { + "lambda": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTc4MDIxMTQ1NTc4ODI1OTY0MTYiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICIxMjg3ODYzNTg0NTE5MTY5NDQ5NCIsICJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiAiMSIsICJ4LWRhdGFkb2ctcGFyZW50LXNwYW4tZmluaXNoLXRpbWUiOiAxNjY2NzE1NDI5Mzk1Ljc3ODgsICJ4LWRhdGFkb2ctYXV0aG9yaXppbmctcmVxdWVzdGlkIjogImFrZE4zaEdER2pRRU1Ddz0ifQ==", + "scope": "this is just a string" + } + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "domainPrefix": "amddr1rix9", + "http": { + "method": "GET", + "path": "/hello", + "protocol": "HTTP/1.1", + "sourceIp": "24.193.182.233", + "userAgent": "PostmanRuntime/7.29.2" + }, + "requestId": "abc123", + "routeKey": "GET /hello", + "stage": "dev", + "time": "25/Oct/2022:16:30:29 +0000", + "timeEpoch": 1666715429349 + }, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-request-api-gateway-v2.json b/tests/event_samples/authorizer-request-api-gateway-v2.json new file mode 100644 index 00000000..92ce09ba --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-v2.json @@ -0,0 +1,47 @@ +{ + "version": "2.0", + "routeKey": "GET /hello", + "rawPath": "/hello", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "authorization": "secretT0k3n", + "authorizationtoken": "secretT0k3n", + "cache-control": "no-cache", + "content-length": "0", + "host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "postman-token": "5e0efdb2-d93f-4d44-8792-45404d40dae2", + "user": "12345", + "user-agent": "curl/7.64.1", + "x-amzn-trace-id": "Root=1-63321d1e-6b80ef0d2c2cf49600b9c28e", + "x-forwarded-for": "38.122.226.210", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "601427279990", + "apiId": "amddr1rix9", + "authorizer": { + "lambda": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTQzNTY5ODM2MTk4NTI5MzMzNTQiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICIxMjY1ODYyMTA4MzUwNTQxMzgwOSIsICJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiAiMSIsICJ4LWRhdGFkb2ctcGFyZW50LXNwYW4tZmluaXNoLXRpbWUiOiAxNjY0MjI4NjM5NTMzNzc1NDAwLCAieC1kYXRhZG9nLWF1dGhvcml6aW5nLXJlcXVlc3RpZCI6ICJhYmMxMjMifQ==", + "scope": "this is just a string" + } + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "domainPrefix": "amddr1rix9", + "http": { + "method": "GET", + "path": "/hello", + "protocol": "HTTP/1.1", + "sourceIp": "38.122.226.210", + "userAgent": "curl/7.64.1" + }, + "requestId": "abc123", + "routeKey": "GET /hello", + "stage": "dev", + "time": "26/Sep/2022:21:43:58 +0000", + "timeEpoch": 1664228638084 + }, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-request-api-gateway-websocket-connect.json b/tests/event_samples/authorizer-request-api-gateway-websocket-connect.json new file mode 100644 index 00000000..1875097d --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-websocket-connect.json @@ -0,0 +1,48 @@ +{ + "headers": { + "Auth": "secretT0k3n", + "Host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", + "Sec-WebSocket-Key": "j9g8xXvniFqyyA+34vQ/Lg==", + "Sec-WebSocket-Version": "13", + "X-Amzn-Trace-Id": "Root=1-63348d21-727f66307696916d50f93f1b", + "X-Forwarded-For": "38.122.226.210", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Auth": ["secretT0k3n"], + "Host": ["amddr1rix9.execute-api.sa-east-1.amazonaws.com"], + "Sec-WebSocket-Extensions": ["permessage-deflate; client_max_window_bits"], + "Sec-WebSocket-Key": ["j9g8xXvniFqyyA+34vQ/Lg=="], + "Sec-WebSocket-Version": ["13"], + "X-Amzn-Trace-Id": ["Root=1-63348d21-727f66307696916d50f93f1b"], + "X-Forwarded-For": ["38.122.226.210"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "requestContext": { + "routeKey": "$connect", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiNTM1MTA0NzQwNDgzNDcyMzE4OSIsICJ4LWRhdGFkb2ctcGFyZW50LWlkIjogIjE4MjMwNDYwNjMxMTU2MTYxODM3IiwgIngtZGF0YWRvZy1zYW1wbGluZy1wcmlvcml0eSI6ICIxIiwgIngtZGF0YWRvZy1wYXJlbnQtc3Bhbi1maW5pc2gtdGltZSI6IDE2NjQzODgzODY4OTAsICJ4LWRhdGFkb2ctYXV0aG9yaXppbmctcmVxdWVzdGlkIjogImFiYzEyMyJ9", + "scope": "this is just a string", + "principalId": "foo", + "integrationLatency": 1440 + }, + "eventType": "CONNECT", + "extendedRequestId": "abc123", + "requestTime": "28/Sep/2022:18:06:25 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1664388385450, + "requestTimeEpoch": 1664388385452, + "identity": { + "sourceIp": "38.122.226.210" + }, + "requestId": "abc123", + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "connectionId": "ZLr9QeNLmjQCIZA=", + "apiId": "amddr1rix9" + }, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-request-api-gateway-websocket-message.json b/tests/event_samples/authorizer-request-api-gateway-websocket-message.json new file mode 100644 index 00000000..8ac5d2e9 --- /dev/null +++ b/tests/event_samples/authorizer-request-api-gateway-websocket-message.json @@ -0,0 +1,27 @@ +{ + "requestContext": { + "routeKey": "main", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTczNzQwNjkwNjUwMjU0MzQ5MjAiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICI3OTE1MTg2NzUxOTE0MTc2MTA2IiwgIngtZGF0YWRvZy1zYW1wbGluZy1wcmlvcml0eSI6ICIxIiwgIngtZGF0YWRvZy1wYXJlbnQtc3Bhbi1maW5pc2gtdGltZSI6IDE2NjQzOTAzMzM1MjYuNTA2OCwgIngtZGF0YWRvZy1hdXRob3JpemluZy1yZXF1ZXN0aWQiOiAiYWJjMzIxIn0=", + "scope": "this is just a string", + "principalId": "foo" + }, + "messageId": "ZLw3leP1GjQCI8Q=", + "eventType": "MESSAGE", + "extendedRequestId": "ZLw3lFowmjQFvSA=", + "requestTime": "28/Sep/2022:18:39:57 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1664390332261, + "requestTimeEpoch": 1664390397117, + "identity": { + "sourceIp": "38.122.226.210" + }, + "requestId": "abc123", + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "connectionId": "ZLwtceO1mjQCI8Q=", + "apiId": "amddr1rix9" + }, + "body": "{\n \"action\":\"main\",\n \"message\":\"hi\"\n}", + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-token-api-gateway-v1-cached.json b/tests/event_samples/authorizer-token-api-gateway-v1-cached.json new file mode 100644 index 00000000..18ade248 --- /dev/null +++ b/tests/event_samples/authorizer-token-api-gateway-v1-cached.json @@ -0,0 +1,93 @@ +{ + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "secretT0k3n", + "authorizationToken": "secretT0k3n", + "Cache-Control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-ASN": "174", + "CloudFront-Viewer-Country": "US", + "Host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "Postman-Token": "bb30e418-d4ee-45b6-a659-89f4dbd41c84", + "User-Agent": "PostmanRuntime/7.29.2", + "Via": "1.1 x.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "iFM3jhMRNr0jJn8b4A7lgYVLUhUb6pOxFQm1YA4JDTDylUHJOOl-Jw==", + "X-Amzn-Trace-Id": "Root=1-635967a6-2b0999ba248345b32a8ff5ce", + "X-Forwarded-For": "38.122.226.210, 130.176.179.111", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Accept-Encoding": ["gzip, deflate, br"], + "Authorization": ["secretT0k3n"], + "authorizationToken": ["secretT0k3n"], + "Cache-Control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-ASN": ["174"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["amddr1rix9.execute-api.sa-east-1.amazonaws.com"], + "Postman-Token": ["bb30e418-d4ee-45b6-a659-89f4dbd41c84"], + "User-Agent": ["PostmanRuntime/7.29.2"], + "Via": ["1.1 x.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["iFM3jhMRNr0jJn8b4A7lgYVLUhUb6pOxFQm1YA4JDTDylUHJOOl-Jw=="], + "X-Amzn-Trace-Id": ["Root=1-635967a6-2b0999ba248345b32a8ff5ce"], + "X-Forwarded-For": ["38.122.226.210, 130.176.179.111"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "mi7nzd", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTM3NTkyNDk3NDA5MDA1OTMyNjEiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICIxNTkzNTAyMzY1NTYxMjM0MTgxOSIsICJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiAiMSIsICJ4LWRhdGFkb2ctcGFyZW50LXNwYW4tZmluaXNoLXRpbWUiOiAxNjY2ODAzNjE2OTMzLjIwNzh9", + "scope": "this is just a string", + "principalId": "foo", + "integrationLatency": 0 + }, + "resourcePath": "/hello", + "httpMethod": "GET", + "extendedRequestId": "an0iHGmHmjQFjLw=", + "requestTime": "26/Oct/2022:17:00:22 +0000", + "path": "/dev/hello", + "accountId": "425362996713", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "amddr1rix9", + "requestTimeEpoch": 1666803622990, + "requestId": "abc123", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "38.122.226.210", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "PostmanRuntime/7.29.2", + "user": null + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "apiId": "amddr1rix9" + }, + "body": null, + "isBase64Encoded": false +} diff --git a/tests/event_samples/authorizer-token-api-gateway-v1.json b/tests/event_samples/authorizer-token-api-gateway-v1.json new file mode 100644 index 00000000..1edba05e --- /dev/null +++ b/tests/event_samples/authorizer-token-api-gateway-v1.json @@ -0,0 +1,93 @@ +{ + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "secretT0k3n", + "authorizationToken": "secretT0k3n", + "Cache-Control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-ASN": "174", + "CloudFront-Viewer-Country": "US", + "Host": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "Postman-Token": "791b2f79-1732-4877-abc1-5c538daf4642", + "User-Agent": "PostmanRuntime/7.29.2", + "Via": "1.1 d58463d219ef6ca0331e7200a6667c18.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "tqU0SVyX_dKKI0XIN4-kwzCHrROBFECLXtAKCtjKr1uRQ-uijj99sQ==", + "X-Amzn-Trace-Id": "Root=1-632b4732-3e69c6a34f21f00d7f9b5599", + "X-Forwarded-For": "38.122.226.210, 64.252.135.71", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Accept-Encoding": ["gzip, deflate, br"], + "Authorization": ["secretT0k3n"], + "authorizationToken": ["secretT0k3n"], + "Cache-Control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-ASN": ["174"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["amddr1rix9.execute-api.sa-east-1.amazonaws.com"], + "Postman-Token": ["791b2f79-1732-4877-abc1-5c538daf4642"], + "User-Agent": ["PostmanRuntime/7.29.2"], + "Via": ["1.1 d58463d219ef6ca0331e7200a6667c18.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["tqU0SVyX_dKKI0XIN4-kwzCHrROBFECLXtAKCtjKr1uRQ-uijj99sQ=="], + "X-Amzn-Trace-Id": ["Root=1-632b4732-3e69c6a34f21f00d7f9b5599"], + "X-Forwarded-For": ["38.122.226.210, 64.252.135.71"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "elem3u", + "authorizer": { + "_datadog": "eyJ4LWRhdGFkb2ctdHJhY2UtaWQiOiAiMTc4NzQ3OTgyNjgxNDQ5MDI3MTIiLCAieC1kYXRhZG9nLXBhcmVudC1pZCI6ICIxNjE4NDY2NzM5OTMxNTM3MjEwMSIsICJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiAiMSIsICJ4LWRhdGFkb2ctcGFyZW50LXNwYW4tZmluaXNoLXRpbWUiOiAxNjYzNzIxNjAyNDQwLjgyODZ9", + "scope": "this is just a string", + "principalId": "foo", + "integrationLatency": 1897 + }, + "resourcePath": "/hello", + "httpMethod": "GET", + "extendedRequestId": "Y0gP8FG9mjQFZTQ=", + "requestTime": "21/Sep/2022:17:17:38 +0000", + "path": "/dev/hello", + "accountId": "601427279990", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "amddr1rix9", + "requestTimeEpoch": 1663295019935, + "requestId": "abc123", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "38.122.226.210", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "PostmanRuntime/7.29.2", + "user": null + }, + "domainName": "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + "apiId": "amddr1rix9" + }, + "body": null, + "isBase64Encoded": false +} diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 7f5ba4d3..47a028da 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -29,6 +29,7 @@ InferredSpanInfo, extract_context_from_eventbridge_event, ) +from datadog_lambda.trigger import EventTypes function_arn = "arn:aws:lambda:us-west-1:123457598159:function:python-layer-test" @@ -84,7 +85,7 @@ def tearDown(self): def test_without_datadog_trace_headers(self): lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context({}, lambda_ctx) + ctx, source, event_source = extract_dd_trace_context({}, lambda_ctx) self.assertEqual(source, "xray") self.assertDictEqual( ctx, @@ -106,7 +107,7 @@ def test_without_datadog_trace_headers(self): def test_with_non_object_event(self): lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context(b"", lambda_ctx) + ctx, source, event_source = extract_dd_trace_context(b"", lambda_ctx) self.assertEqual(source, "xray") self.assertDictEqual( ctx, @@ -128,7 +129,7 @@ def test_with_non_object_event(self): def test_with_incomplete_datadog_trace_headers(self): lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context( + ctx, source, event_source = extract_dd_trace_context( {"headers": {TraceHeader.TRACE_ID: "123", TraceHeader.PARENT_ID: "321"}}, lambda_ctx, ) @@ -152,7 +153,7 @@ def test_with_incomplete_datadog_trace_headers(self): def test_with_complete_datadog_trace_headers(self): lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context( + ctx, source, event_source = extract_dd_trace_context( { "headers": { TraceHeader.TRACE_ID: "123", @@ -193,7 +194,7 @@ def extractor_foo(event, context): return trace_id, parent_id, sampling_priority lambda_ctx = get_mock_context() - ctx, ctx_source = extract_dd_trace_context( + ctx, ctx_source, event_source = extract_dd_trace_context( { "foo": { TraceHeader.TRACE_ID: "123", @@ -227,7 +228,7 @@ def extractor_raiser(event, context): raise Exception("kreator") lambda_ctx = get_mock_context() - ctx, ctx_source = extract_dd_trace_context( + ctx, ctx_source, event_source = extract_dd_trace_context( { "foo": { TraceHeader.TRACE_ID: "123", @@ -289,7 +290,7 @@ def test_with_sqs_distributed_datadog_trace_data(self): } ] } - ctx, source = extract_dd_trace_context(sqs_event, lambda_ctx) + ctx, source, event_source = extract_dd_trace_context(sqs_event, lambda_ctx) self.assertEqual(source, "event") self.assertDictEqual( ctx, @@ -323,7 +324,7 @@ def test_with_legacy_client_context_datadog_trace_data(self): } } ) - ctx, source = extract_dd_trace_context({}, lambda_ctx) + ctx, source, event_source = extract_dd_trace_context({}, lambda_ctx) self.assertEqual(source, "event") self.assertDictEqual( ctx, @@ -356,7 +357,7 @@ def test_with_new_client_context_datadog_trace_data(self): TraceHeader.SAMPLING_PRIORITY: "1", } ) - ctx, source = extract_dd_trace_context({}, lambda_ctx) + ctx, source, event_source = extract_dd_trace_context({}, lambda_ctx) self.assertEqual(source, "event") self.assertDictEqual( ctx, @@ -556,7 +557,7 @@ def test_mixed_parent_context_when_merging(self): # use the dd-trace trace-id and the x-ray parent-id # This allows parenting relationships like dd-trace -> x-ray -> dd-trace lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context( + ctx, source, event_type = extract_dd_trace_context( { "headers": { TraceHeader.TRACE_ID: "123", @@ -579,6 +580,157 @@ def test_mixed_parent_context_when_merging(self): self.mock_activate.assert_has_calls([call(expected_context)]) +class TestAuthorizerInferredSpans(unittest.TestCase): + def setUp(self): + patcher = patch("ddtrace.Span.finish", autospec=True) + self.mock_span_stop = patcher.start() + self.addCleanup(patcher.stop) + + def test_create_inferred_span_from_authorizer_request_api_gateway_v1_event(self): + event_sample_source = "authorizer-request-api-gateway-v1" + finish_time = ( + 1663295021.832 # request_time_epoch + integrationLatency for api-gateway-v1 + ) + span = self._authorizer_span_testing_items(event_sample_source, finish_time) + self._basic_common_checks(span, "aws.apigateway.rest") + + def test_create_inferred_span_from_authorizer_request_api_gateway_v1_cached_event( + self, + ): + event_sample_source = "authorizer-request-api-gateway-v1-cached" + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" # injected data's requestId is abc321 + span = create_inferred_span(event, ctx) + self.mock_span_stop.assert_not_called() # NO authorizer span is injected + self._basic_common_checks(span, "aws.apigateway.rest") + + def test_create_inferred_span_from_authorizer_token_api_gateway_v1_event(self): + event_sample_source = "authorizer-token-api-gateway-v1" + finish_time = ( + 1663295021.832 # request_time_epoch + integrationLatency for api-gateway-v1 + ) + span = self._authorizer_span_testing_items(event_sample_source, finish_time) + self._basic_common_checks(span, "aws.apigateway.rest") + + def test_create_inferred_span_from_authorizer_token_api_gateway_v2_cached_event( + self, + ): + event_sample_source = "authorizer-token-api-gateway-v1-cached" + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" # injected data's requestId is abc321 + span = create_inferred_span(event, ctx) + self.mock_span_stop.assert_not_called() # NO authorizer span is injected + self._basic_common_checks(span, "aws.apigateway.rest") + + def test_create_inferred_span_from_authorizer_request_api_gateway_v2_event(self): + event_sample_source = "authorizer-request-api-gateway-v2" + finish_time = 1664228639533775400 # use the injected parent span finish time as an approximation + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" + span = create_inferred_span(event, ctx) + self.assertEqual(span.get_tag(InferredSpanInfo.TAG_SOURCE), "self") + self.assertEqual(span.get_tag(InferredSpanInfo.SYNCHRONICITY), "sync") + self.mock_span_stop.assert_not_called() + self.assertEqual(span.start_ns, finish_time) + self._basic_common_checks(span, "aws.httpapi") + + def test_create_inferred_span_from_authorizer_request_api_gateway_v2_cached_event( + self, + ): + event_sample_source = "authorizer-request-api-gateway-v2-cached" + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" # injected data's requestId is abc321 + span = create_inferred_span(event, ctx) + self.mock_span_stop.assert_not_called() # NO authorizer span is injected + self._basic_common_checks(span, "aws.httpapi") + + def test_create_inferred_span_from_authorizer_request_api_gateway_websocket_connect_event( + self, + ): + event_sample_source = "authorizer-request-api-gateway-websocket-connect" + finish_time = ( + 1664388386.892 # request_time_epoch + integrationLatency in websocket case + ) + span = self._authorizer_span_testing_items(event_sample_source, finish_time) + self._basic_common_checks( + span, "aws.apigateway.websocket", "web", "$connect", None + ) + + def test_create_inferred_span_from_authorizer_request_api_gateway_websocket_message_event( + self, + ): + event_sample_source = "authorizer-request-api-gateway-websocket-message" + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" # injected data's requestId is abc321 + span = create_inferred_span(event, ctx) + self.mock_span_stop.assert_not_called() # NO authorizer span is injected + self._basic_common_checks(span, "aws.apigateway.websocket", "web", "main", None) + + def _authorizer_span_testing_items(self, event_sample_source, finish_time): + test_file = event_samples + event_sample_source + ".json" + with open(test_file, "r") as event: + event = json.load(event) + ctx = get_mock_context() + ctx.aws_request_id = "abc123" + span = create_inferred_span(event, ctx) + self.assertEqual(span.get_tag(InferredSpanInfo.TAG_SOURCE), "self") + self.assertEqual(span.get_tag(InferredSpanInfo.SYNCHRONICITY), "sync") + + # checking the upstream_authorizer_span + self.mock_span_stop.assert_called_once() + args, kwargs = self.mock_span_stop.call_args_list[0] + self.assertEqual(kwargs.get("finish_time", args[1]), finish_time) + self.assertEqual(span.start, finish_time) + authorizer_span = args[0] + self.assertEqual(authorizer_span.name, "aws.apigateway.authorizer") + self.assertEqual(span.parent_id, authorizer_span.span_id) + return span + + def _basic_common_checks( + self, + span, + operation_name, + span_type="http", + route_key="/hello", + http_method="GET", + ): + self.assertEqual(span.get_tag("apiid"), "amddr1rix9") + self.assertEqual(span.get_tag("apiname"), "amddr1rix9") + self.assertEqual(span.get_tag("stage"), "dev") + self.assertEqual(span.get_tag("operation_name"), operation_name) + self.assertEqual(span.span_type, span_type) + self.assertEqual( + span.service, + "amddr1rix9.execute-api.sa-east-1.amazonaws.com", + ) + self.assertEqual( + span.get_tag("http.url"), + "amddr1rix9.execute-api.sa-east-1.amazonaws.com" + route_key, + ) + self.assertEqual(span.get_tag("endpoint"), route_key) + self.assertEqual(span.get_tag("http.method"), http_method) + self.assertEqual( + span.get_tag("resource_names"), + f"{http_method} {route_key}" if http_method else route_key, + ) + self.assertEqual(span.get_tag("request_id"), "abc123") + + class TestInferredSpans(unittest.TestCase): def test_create_inferred_span_from_api_gateway_event(self): event_sample_source = "api-gateway" @@ -643,7 +795,7 @@ def test_create_inferred_span_from_api_gateway_non_proxy_event_async(self): self.assertEqual(span.get_tag("apiid"), "lgxbo6a518") self.assertEqual(span.get_tag("apiname"), "lgxbo6a518") self.assertEqual(span.get_tag("stage"), "dev") - self.assertEqual(span.start, 1631210915.2509997) + self.assertEqual(span.start, 1631210915.2510002) self.assertEqual(span.span_type, "http") self.assertEqual(span.get_tag(InferredSpanInfo.TAG_SOURCE), "self") self.assertEqual(span.get_tag(InferredSpanInfo.SYNCHRONICITY), "async") @@ -677,7 +829,7 @@ def test_create_inferred_span_from_api_gateway_non_proxy_event_sync(self): self.assertEqual(span.get_tag("apiid"), "lgxbo6a518") self.assertEqual(span.get_tag("apiname"), "lgxbo6a518") self.assertEqual(span.get_tag("stage"), "dev") - self.assertEqual(span.start, 1631210915.2509997) + self.assertEqual(span.start, 1631210915.2510002) self.assertEqual(span.span_type, "http") self.assertEqual(span.get_tag(InferredSpanInfo.TAG_SOURCE), "self") self.assertEqual(span.get_tag(InferredSpanInfo.SYNCHRONICITY), "sync") @@ -1100,7 +1252,7 @@ def test_extract_dd_trace_context_for_eventbridge(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_type = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "12345") self.assertEqual(context["parent-id"], "67890") @@ -1110,7 +1262,7 @@ def test_extract_context_from_sqs_event_with_string_msg_attr(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_type = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "2684756524522091840") self.assertEqual(context["parent-id"], "7431398482019833808") self.assertEqual(context["sampling-priority"], "1") @@ -1121,7 +1273,7 @@ def test_extract_context_from_sqs_batch_event(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "2684756524522091840") self.assertEqual(context["parent-id"], "7431398482019833808") self.assertEqual(context["sampling-priority"], "1") @@ -1132,7 +1284,7 @@ def test_extract_context_from_sns_event_with_string_msg_attr(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "4948377316357291421") self.assertEqual(context["parent-id"], "6746998015037429512") self.assertEqual(context["sampling-priority"], "1") @@ -1143,7 +1295,7 @@ def test_extract_context_from_sns_event_with_b64_msg_attr(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "4948377316357291421") self.assertEqual(context["parent-id"], "6746998015037429512") self.assertEqual(context["sampling-priority"], "1") @@ -1154,7 +1306,7 @@ def test_extract_context_from_sns_batch_event(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "4948377316357291421") self.assertEqual(context["parent-id"], "6746998015037429512") self.assertEqual(context["sampling-priority"], "1") @@ -1165,7 +1317,7 @@ def test_extract_context_from_kinesis_event(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "4948377316357291421") self.assertEqual(context["parent-id"], "2876253380018681026") self.assertEqual(context["sampling-priority"], "1") @@ -1176,7 +1328,7 @@ def test_extract_context_from_kinesis_batch_event(self): with open(test_file, "r") as event: event = json.load(event) ctx = get_mock_context() - context, source = extract_dd_trace_context(event, ctx) + context, source, event_source = extract_dd_trace_context(event, ctx) self.assertEqual(context["trace-id"], "4948377316357291421") self.assertEqual(context["parent-id"], "2876253380018681026") self.assertEqual(context["sampling-priority"], "1") @@ -1240,7 +1392,7 @@ def test_mark_trace_as_error_for_5xx_responses_sends_error_metric_and_set_error_ def test_no_error_with_nonetype_headers(self): lambda_ctx = get_mock_context() - ctx, source = extract_dd_trace_context( + ctx, source, event_type = extract_dd_trace_context( {"headers": None}, lambda_ctx, ) diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index a78cddfc..4efadd96 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -1,7 +1,10 @@ +import base64 +import json import os import unittest from unittest.mock import patch, call, ANY, MagicMock +from datadog_lambda.constants import TraceHeader from datadog_lambda.wrapper import datadog_lambda_wrapper from datadog_lambda.metric import lambda_metric @@ -38,7 +41,7 @@ def setUp(self): patcher = patch("datadog_lambda.wrapper.extract_dd_trace_context") self.mock_extract_dd_trace_context = patcher.start() - self.mock_extract_dd_trace_context.return_value = ({}, None) + self.mock_extract_dd_trace_context.return_value = ({}, None, None) self.addCleanup(patcher.stop) patcher = patch("datadog_lambda.wrapper.set_correlation_ids") @@ -109,7 +112,7 @@ def lambda_handler(event, context): ] ) self.mock_extract_dd_trace_context.assert_called_with( - lambda_event, lambda_context, extractor=None + lambda_event, lambda_context, extractor=None, decode_authorizer_context=True ) self.mock_set_correlation_ids.assert_called() self.mock_inject_correlation_ids.assert_called() @@ -504,3 +507,40 @@ def lambda_handler(event, context): self.assertEqual(os.environ.get("DD_REQUESTS_SERVICE_NAME"), "myAwesomeService") del os.environ["DD_SERVICE"] + + def test_encode_authorizer_span(self): + @datadog_lambda_wrapper + def lambda_handler(event, context): + return { + "principalId": "foo", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": "dummy", + }, + ], + }, + "context": {"scope": "still here"}, + } + + lambda_event = {} + + lambda_context = get_mock_context() + mock_span = MagicMock() + mock_span.context.span_id = "123" + mock_span.context.trace_id = "456" + mock_span.context.sampling_priority = "1" + mock_span.context.dd_origin = None + mock_span.start_ns = 1668127541671386817 + mock_span.duration_ns = 1e8 + lambda_handler.inferred_span = mock_span + + result = lambda_handler(lambda_event, lambda_context) + inject_data = json.loads(base64.b64decode(result["context"]["_datadog"])) + self.assertEquals(inject_data[TraceHeader.PARENT_ID], "123") + self.assertEquals(inject_data[TraceHeader.TRACE_ID], "456") + self.assertEquals(inject_data[TraceHeader.SAMPLING_PRIORITY], "1") + self.assertEquals(result["context"]["scope"], "still here")