|
| 1 | +# Unless explicitly stated otherwise all files in this repository are licensed |
| 2 | +# under the Apache License Version 2.0. |
| 3 | +# This product includes software developed at Datadog (https://www.datadoghq.com/). |
| 4 | +# Copyright 2019 Datadog, Inc. |
| 5 | + |
| 6 | +import base64 |
| 7 | +import gzip |
| 8 | +import json |
| 9 | +from io import BytesIO, BufferedReader |
| 10 | + |
| 11 | + |
| 12 | +EVENT_SOURCES = [ |
| 13 | + "aws:dynamodb", |
| 14 | + "aws:kinesis", |
| 15 | + "aws:s3", |
| 16 | + "aws:sns", |
| 17 | + "aws:sqs", |
| 18 | +] |
| 19 | + |
| 20 | + |
| 21 | +def get_aws_partition_by_region(region): |
| 22 | + if region.startswith("us-gov-"): |
| 23 | + return "aws-us-gov" |
| 24 | + if region.startswith("cn-"): |
| 25 | + return "aws-cn" |
| 26 | + return "aws" |
| 27 | + |
| 28 | + |
| 29 | +def get_first_record(event): |
| 30 | + records = event.get("Records") |
| 31 | + if records and len(records) > 0: |
| 32 | + return records[0] |
| 33 | + |
| 34 | + |
| 35 | +def parse_event_source(event): |
| 36 | + """Determines the source of the trigger event |
| 37 | +
|
| 38 | + Possible Returns: |
| 39 | + api-gateway | application-load-balancer | cloudwatch-logs | |
| 40 | + cloudwatch-events | cloudfront | dynamodb | kinesis | s3 | sns | sqs |
| 41 | + """ |
| 42 | + event_source = event.get("eventSource") or event.get("EventSource") |
| 43 | + |
| 44 | + request_context = event.get("requestContext") |
| 45 | + if request_context and request_context.get("stage"): |
| 46 | + event_source = "api-gateway" |
| 47 | + |
| 48 | + if request_context and request_context.get("elb"): |
| 49 | + event_source = "application-load-balancer" |
| 50 | + |
| 51 | + if event.get("awslogs"): |
| 52 | + event_source = "cloudwatch-logs" |
| 53 | + |
| 54 | + event_detail = event.get("detail") |
| 55 | + cw_event_categories = event_detail and event_detail.get("EventCategories") |
| 56 | + if event.get("source") == "aws.events" or cw_event_categories: |
| 57 | + event_source = "cloudwatch-events" |
| 58 | + |
| 59 | + event_record = get_first_record(event) |
| 60 | + if event_record: |
| 61 | + event_source = event_record.get("eventSource") or event_record.get( |
| 62 | + "EventSource" |
| 63 | + ) |
| 64 | + if event_record.get("cf"): |
| 65 | + event_source = "cloudfront" |
| 66 | + |
| 67 | + if event_source in EVENT_SOURCES: |
| 68 | + event_source = event_source.replace("aws:", "") |
| 69 | + return event_source |
| 70 | + |
| 71 | + |
| 72 | +def parse_event_source_arn(source, event, context): |
| 73 | + """ |
| 74 | + Parses the trigger event for an available ARN. If an ARN field is not provided |
| 75 | + in the event we stitch it together. |
| 76 | + """ |
| 77 | + split_function_arn = context.invoked_function_arn.split(":") |
| 78 | + region = split_function_arn[3] |
| 79 | + account_id = split_function_arn[4] |
| 80 | + aws_arn = get_aws_partition_by_region(region) |
| 81 | + |
| 82 | + event_record = get_first_record(event) |
| 83 | + # e.g. arn:aws:s3:::lambda-xyz123-abc890 |
| 84 | + if source == "s3": |
| 85 | + return event_record.get("s3")["bucket"]["arn"] |
| 86 | + |
| 87 | + # e.g. arn:aws:sns:us-east-1:123456789012:sns-lambda |
| 88 | + if source == "sns": |
| 89 | + return event_record.get("Sns")["TopicArn"] |
| 90 | + |
| 91 | + # e.g. arn:aws:cloudfront::123456789012:distribution/ABC123XYZ |
| 92 | + if source == "cloudfront": |
| 93 | + distribution_id = event_record.get("cf")["config"]["distributionId"] |
| 94 | + return "arn:{}:cloudfront::{}:distribution/{}".format( |
| 95 | + aws_arn, account_id, distribution_id |
| 96 | + ) |
| 97 | + |
| 98 | + # e.g. arn:aws:apigateway:us-east-1::/restapis/xyz123/stages/default |
| 99 | + if source == "api-gateway": |
| 100 | + request_context = event.get("requestContext") |
| 101 | + return "arn:{}:apigateway:{}::/restapis/{}/stages/{}".format( |
| 102 | + aws_arn, region, request_context["apiId"], request_context["stage"] |
| 103 | + ) |
| 104 | + |
| 105 | + # e.g. arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-xyz/123 |
| 106 | + if source == "application-load-balancer": |
| 107 | + request_context = event.get("requestContext") |
| 108 | + return request_context.get("elb")["targetGroupArn"] |
| 109 | + |
| 110 | + # e.g. arn:aws:logs:us-west-1:123456789012:log-group:/my-log-group-xyz |
| 111 | + if source == "cloudwatch-logs": |
| 112 | + with gzip.GzipFile( |
| 113 | + fileobj=BytesIO(base64.b64decode(event["awslogs"]["data"])) |
| 114 | + ) as decompress_stream: |
| 115 | + data = b"".join(BufferedReader(decompress_stream)) |
| 116 | + logs = json.loads(data) |
| 117 | + log_group = logs.get("logGroup", "cloudwatch") |
| 118 | + return "arn:{}:logs:{}:{}:log-group:{}".format( |
| 119 | + aws_arn, region, account_id, log_group |
| 120 | + ) |
| 121 | + |
| 122 | + # e.g. arn:aws:events:us-east-1:123456789012:rule/my-schedule |
| 123 | + if source == "cloudwatch-events" and event.get("resources"): |
| 124 | + return event.get("resources")[0] |
| 125 | + |
| 126 | + |
| 127 | +def get_event_source_arn(source, event, context): |
| 128 | + event_source_arn = event.get("eventSourceARN") or event.get("eventSourceArn") |
| 129 | + |
| 130 | + event_record = get_first_record(event) |
| 131 | + if event_record: |
| 132 | + event_source_arn = event_record.get("eventSourceARN") or event_record.get( |
| 133 | + "eventSourceArn" |
| 134 | + ) |
| 135 | + |
| 136 | + if event_source_arn is None: |
| 137 | + event_source_arn = parse_event_source_arn(source, event, context) |
| 138 | + |
| 139 | + return event_source_arn |
| 140 | + |
| 141 | + |
| 142 | +def extract_http_tags(event): |
| 143 | + """ |
| 144 | + Extracts HTTP facet tags from the triggering event |
| 145 | + """ |
| 146 | + http_tags = {} |
| 147 | + request_context = event.get("requestContext") |
| 148 | + path = event.get("path") |
| 149 | + method = event.get("httpMethod") |
| 150 | + if request_context and request_context.get("stage"): |
| 151 | + if request_context.get("domainName"): |
| 152 | + http_tags["http.url"] = request_context["domainName"] |
| 153 | + |
| 154 | + path = request_context.get("path") |
| 155 | + method = request_context.get("httpMethod") |
156 | + # Version 2.0 HTTP API Gateway |
| 157 | + apigateway_v2_http = request_context.get("http") |
| 158 | + if event.get("version") == "2.0" and apigateway_v2_http: |
| 159 | + path = apigateway_v2_http.get("path") |
| 160 | + method = apigateway_v2_http.get("method") |
| 161 | + |
| 162 | + if path: |
| 163 | + http_tags["http.url_details.path"] = path |
| 164 | + if method: |
| 165 | + http_tags["http.method"] = method |
| 166 | + |
| 167 | + headers = event.get("headers") |
| 168 | + if headers and headers.get("Referer"): |
| 169 | + http_tags["http.referer"] = headers["Referer"] |
| 170 | + |
| 171 | + return http_tags |
| 172 | + |
| 173 | + |
| 174 | +def extract_trigger_tags(event, context): |
| 175 | + """ |
| 176 | + Parses the trigger event object to get tags to be added to the span metadata |
| 177 | + """ |
| 178 | + trigger_tags = {} |
| 179 | + event_source = parse_event_source(event) |
| 180 | + if event_source: |
| 181 | + trigger_tags["function_trigger.event_source"] = event_source |
| 182 | + |
| 183 | + event_source_arn = get_event_source_arn(event_source, event, context) |
| 184 | + if event_source_arn: |
| 185 | + trigger_tags["function_trigger.event_source_arn"] = event_source_arn |
| 186 | + |
| 187 | + if event_source in ["api-gateway", "application-load-balancer"]: |
| 188 | + trigger_tags.update(extract_http_tags(event)) |
| 189 | + |
| 190 | + return trigger_tags |
| 191 | + |
| 192 | + |
| 193 | +def extract_http_status_code_tag(trigger_tags, response): |
| 194 | + """ |
| 195 | + If the Lambda was triggered by API Gateway or ALB add the returned status code |
| 196 | + as a tag to the function execution span. |
| 197 | + """ |
| 198 | + is_http_trigger = trigger_tags and ( |
| 199 | + trigger_tags.get("function_trigger.event_source") == "api-gateway" |
| 200 | + or trigger_tags.get("function_trigger.event_source") |
| 201 | + == "application-load-balancer" |
| 202 | + ) |
| 203 | + if not is_http_trigger: |
| 204 | + return |
| 205 | + |
| 206 | + status_code = "200" |
| 207 | + if response is None: |
| 208 | + # Return a 502 status if no response is found |
| 209 | + status_code = "502" |
| 210 | + elif response.get("statusCode"): |
| 211 | + status_code = response.get("statusCode") |
| 212 | + |
| 213 | + return status_code |
0 commit comments