|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# Copyright The OpenTelemetry Authors |
| 4 | +# |
| 5 | +# SPDX-License-Identifier: Apache-2.0 |
| 6 | + |
| 7 | +module OpenTelemetry |
| 8 | + module Instrumentation |
| 9 | + module AwsLambda |
| 10 | + # Helper module that can be used to wrap a lambda handler method |
| 11 | + module Wrap # rubocop:disable Metrics/ModuleLength |
| 12 | + AWS_TRIGGERS = ['aws:sqs', 'aws:s3', 'aws:sns', 'aws:dynamodb'].freeze |
| 13 | + DEFAULT_FLUSH_TIMEOUT = ENV.fetch('OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT', '30000').to_i |
| 14 | + |
| 15 | + def instrument_handler(method, flush_timeout: DEFAULT_FLUSH_TIMEOUT) |
| 16 | + raise ArgumentError, "#{method} is not a method of #{name}" unless respond_to?(method) |
| 17 | + |
| 18 | + uninstrumented_method = "#{method}_without_instrumentation" |
| 19 | + singleton_class.alias_method uninstrumented_method, method |
| 20 | + |
| 21 | + handler = "#{name}.#{method}" |
| 22 | + |
| 23 | + define_singleton_method(method) do |event:, context:| |
| 24 | + wrap_lambda(event: event, context: context, handler: handler, flush_timeout: flush_timeout) { public_send(uninstrumented_method, event: event, context: context) } |
| 25 | + end |
| 26 | + end |
| 27 | + |
| 28 | + # Try to record and re-raise any exception from the wrapped function handler |
| 29 | + # Instrumentation should never raise its own exception |
| 30 | + def wrap_lambda(event:, context:, handler:, flush_timeout: DEFAULT_FLUSH_TIMEOUT) |
| 31 | + parent_context = extract_parent_context(event) |
| 32 | + |
| 33 | + span_kind = if event['Records'] && AWS_TRIGGERS.include?(event['Records'].dig(0, 'eventSource')) |
| 34 | + :consumer |
| 35 | + else |
| 36 | + :server |
| 37 | + end |
| 38 | + |
| 39 | + original_handler_error = nil |
| 40 | + original_response = nil |
| 41 | + OpenTelemetry::Context.with_current(parent_context) do |
| 42 | + tracer.in_span(handler, attributes: otel_attributes(event, context), kind: span_kind) do |span| |
| 43 | + begin |
| 44 | + response = yield |
| 45 | + |
| 46 | + unless span.attributes.key?(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE) |
| 47 | + status_code = response['statusCode'] || response[:statusCode] if response.is_a?(Hash) |
| 48 | + span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, status_code) if status_code |
| 49 | + end |
| 50 | + rescue StandardError => e |
| 51 | + original_handler_error = e |
| 52 | + ensure |
| 53 | + original_response = response |
| 54 | + end |
| 55 | + if original_handler_error |
| 56 | + span.record_exception(original_handler_error) |
| 57 | + span.status = OpenTelemetry::Trace::Status.error(original_handler_error.message) |
| 58 | + end |
| 59 | + end |
| 60 | + end |
| 61 | + |
| 62 | + OpenTelemetry.tracer_provider.force_flush(timeout: flush_timeout) |
| 63 | + OpenTelemetry.meter_provider.force_flush(timeout: flush_timeout) if OpenTelemetry.respond_to?(:meter_provider) |
| 64 | + |
| 65 | + raise original_handler_error if original_handler_error |
| 66 | + |
| 67 | + original_response |
| 68 | + end |
| 69 | + |
| 70 | + def instrumentation_config |
| 71 | + AwsLambda::Instrumentation.instance.config |
| 72 | + end |
| 73 | + |
| 74 | + def tracer |
| 75 | + AwsLambda::Instrumentation.instance.tracer |
| 76 | + end |
| 77 | + |
| 78 | + private |
| 79 | + |
| 80 | + # Extract parent context from request headers |
| 81 | + # Downcase Traceparent and Tracestate because TraceContext::TextMapPropagator's TRACEPARENT_KEY and TRACESTATE_KEY are all lowercase |
| 82 | + # If any error occur, rescue and give empty context |
| 83 | + def extract_parent_context(event) |
| 84 | + headers = event['headers'] || {} |
| 85 | + headers.transform_keys! do |key| |
| 86 | + %w[Traceparent Tracestate].include?(key) ? key.downcase : key |
| 87 | + end |
| 88 | + |
| 89 | + OpenTelemetry.propagation.extract( |
| 90 | + headers, |
| 91 | + getter: OpenTelemetry::Context::Propagation.text_map_getter |
| 92 | + ) |
| 93 | + rescue StandardError => e |
| 94 | + OpenTelemetry.logger.error("aws-lambda instrumentation exception occurred while extracting the parent context: #{e.message}") |
| 95 | + OpenTelemetry::Context.empty |
| 96 | + end |
| 97 | + |
| 98 | + # lambda event version 1.0 and version 2.0 |
| 99 | + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html |
| 100 | + def v1_proxy_attributes(event) |
| 101 | + attributes = { |
| 102 | + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => event['httpMethod'], |
| 103 | + OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => event['resource'], |
| 104 | + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => event['resource'] |
| 105 | + } |
| 106 | + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] += "?#{event['queryStringParameters']}" if event['queryStringParameters'] |
| 107 | + |
| 108 | + headers = event['headers'] |
| 109 | + if headers |
| 110 | + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_USER_AGENT] = headers['User-Agent'] |
| 111 | + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME] = headers['X-Forwarded-Proto'] |
| 112 | + attributes[OpenTelemetry::SemanticConventions::Trace::NET_HOST_NAME] = headers['Host'] |
| 113 | + end |
| 114 | + attributes |
| 115 | + end |
| 116 | + |
| 117 | + def v2_proxy_attributes(event) |
| 118 | + request_context = event['requestContext'] |
| 119 | + attributes = { |
| 120 | + OpenTelemetry::SemanticConventions::Trace::NET_HOST_NAME => request_context['domainName'], |
| 121 | + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request_context['http']['method'], |
| 122 | + OpenTelemetry::SemanticConventions::Trace::HTTP_USER_AGENT => request_context['http']['userAgent'], |
| 123 | + OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => request_context['http']['path'], |
| 124 | + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => request_context['http']['path'] |
| 125 | + } |
| 126 | + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] += "?#{event['rawQueryString']}" if event['rawQueryString'] |
| 127 | + attributes |
| 128 | + end |
| 129 | + |
| 130 | + # fass.trigger set to http: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#api-gateway |
| 131 | + # TODO: need to update Semantic Conventions for invocation_id, trigger and resource_id |
| 132 | + def otel_attributes(event, context) |
| 133 | + span_attributes = {} |
| 134 | + span_attributes['faas.invocation_id'] = context.aws_request_id |
| 135 | + span_attributes['cloud.resource_id'] = context.invoked_function_arn |
| 136 | + span_attributes[OpenTelemetry::SemanticConventions::Trace::AWS_LAMBDA_INVOKED_ARN] = context.invoked_function_arn |
| 137 | + span_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID] = context.invoked_function_arn.split(':')[4] |
| 138 | + |
| 139 | + if event['requestContext'] |
| 140 | + request_attributes = event['version'] == '2.0' ? v2_proxy_attributes(event) : v1_proxy_attributes(event) |
| 141 | + request_attributes[OpenTelemetry::SemanticConventions::Trace::FAAS_TRIGGER] = 'http' |
| 142 | + span_attributes.merge!(request_attributes) |
| 143 | + end |
| 144 | + |
| 145 | + if event['Records'] |
| 146 | + trigger_attributes = trigger_type_attributes(event) |
| 147 | + span_attributes.merge!(trigger_attributes) |
| 148 | + end |
| 149 | + |
| 150 | + span_attributes |
| 151 | + rescue StandardError => e |
| 152 | + OpenTelemetry.logger.error("aws-lambda instrumentation exception occurred while preparing span attributes: #{e.message}") |
| 153 | + {} |
| 154 | + end |
| 155 | + |
| 156 | + # sqs spec for lambda: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#sqs |
| 157 | + # current there is no spec for 'aws:sns', 'aws:s3' and 'aws:dynamodb' |
| 158 | + def trigger_type_attributes(event) |
| 159 | + attributes = {} |
| 160 | + case event['Records'].dig(0, 'eventSource') |
| 161 | + when 'aws:sqs' |
| 162 | + attributes[OpenTelemetry::SemanticConventions::Trace::FAAS_TRIGGER] = 'pubsub' |
| 163 | + attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_OPERATION] = 'process' |
| 164 | + attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_SYSTEM] = 'AmazonSQS' |
| 165 | + end |
| 166 | + attributes |
| 167 | + end |
| 168 | + end |
| 169 | + end |
| 170 | + end |
| 171 | +end |
0 commit comments