From 420c3744a18a820a93d2bcd24fe02a53caae8fe8 Mon Sep 17 00:00:00 2001 From: fallwith Date: Tue, 13 Aug 2024 15:31:08 -0700 Subject: [PATCH 01/13] Enhanced AWS Lambda serverless functionality - Introduce support for parsing distributed tracing information for HTTP based trigger invocations of the instrumented Lambda function - Both AWS API Gateway versions 1.0 and 2.0 are supported - For web driven invocations of an instrumented function, categorize the invocation as being "web" based, and record relevant HTTP information including the method, URI, and status code post invocation. - Recognize and report on fully 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. - If an AWS resource based trigger is identified, record at least the type of the resource and the relevant arn. For many resources, also record additional context specific information. For example, for an S3 based invocation, record the S3 bucket name. --- CHANGELOG.md | 6 +- lib/new_relic/agent/serverless_handler.rb | 248 +++++++++++++++++- .../serverless_handler_event_sources.json | 155 +++++++++++ .../agent/serverless_handler_event_sources.rb | 51 ++++ .../agent/transaction/trace_context.rb | 2 +- .../serverless_handler_event_sources_test.rb | 40 +++ .../agent/serverless_handler_test.rb | 157 +++++++++++ 7 files changed, 645 insertions(+), 14 deletions(-) create mode 100644 lib/new_relic/agent/serverless_handler_event_sources.json create mode 100644 lib/new_relic/agent/serverless_handler_event_sources.rb create mode 100644 test/new_relic/agent/serverless_handler_event_sources_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 7268e81719..d25f9e63ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## dev -Version adds experimental OpenSearch instrumentation, updates framework detection, fixes Falcon dispatcher detection, and addresses a JRuby specific concurrency issue. +Version enhances support for AWS Lambda functions, adds experimental OpenSearch instrumentation, updates framework detection, fixes Falcon dispatcher detection, and addresses a JRuby specific concurrency issue. + +- **Feature: Enhanced AWS Lambda function instrumentation** + +When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. The agent's instrumentation for AWS Lambda functions now supports distributed tracing. Web triggered invocations are now identified as being "web" based when an API Gateway call is involved and both API Gateway versions 1.0 and 2.0 are supported. Web based calls have the HTTP method, URI, and status code recorded. The agent will now recognize and report on fully 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. The type of the triggering resource and its arn will be recorded for each resource and for many of them extra resource specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) - **Feature: Add experimental OpenSearch instrumentation** diff --git a/lib/new_relic/agent/serverless_handler.rb b/lib/new_relic/agent/serverless_handler.rb index c47031a0a5..1bd7d4cb35 100644 --- a/lib/new_relic/agent/serverless_handler.rb +++ b/lib/new_relic/agent/serverless_handler.rb @@ -4,13 +4,13 @@ require 'json' require 'new_relic/base64' +require 'uri' + +require_relative 'serverless_handler_event_sources' module NewRelic module Agent class ServerlessHandler - ATTRIBUTE_ARN = 'aws.lambda.arn' - ATTRIBUTE_COLD_START = 'aws.lambda.coldStart' - ATTRIBUTE_REQUEST_ID = 'aws.requestId' AGENT_ATTRIBUTE_DESTINATIONS = NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER | NewRelic::Agent::AttributeFilter::DST_TRANSACTION_EVENTS EXECUTION_ENVIRONMENT = "AWS_Lambda_ruby#{RUBY_VERSION.rpartition('.').first}".freeze @@ -22,12 +22,15 @@ class ServerlessHandler SUPPORTABILITY_METRIC = 'Supportability/AWSLambda/HandlerInvocation' FUNCTION_NAME = 'lambda_function' PAYLOAD_VERSION = ENV.fetch('NEW_RELIC_SERVERLESS_PAYLOAD_VERSION', 2) + DIGIT = /\d/.freeze + EVENT_SOURCES = NewRelic::Agent::ServerlessHandlerEventSources.to_hash def self.env_var_set? ENV.key?(LAMBDA_ENVIRONMENT_VARIABLE) end def initialize + @event = nil @context = nil @payloads = {} end @@ -35,12 +38,13 @@ def initialize def invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil) NewRelic::Agent.increment_metric(SUPPORTABILITY_METRIC) - @context = context + @event, @context = event, context - NewRelic::Agent::Tracer.in_transaction(category: :other, name: function_name) do - add_agent_attributes + NewRelic::Agent::Tracer.in_transaction(category: category, name: function_name) do + prep_transaction - NewRelic::LanguageSupport.constantize(namespace).send(method_name, event: event, context: context) + process_response(NewRelic::LanguageSupport.constantize(namespace) + .send(method_name, event: event, context: context)) end ensure harvest! @@ -86,6 +90,12 @@ def error_data(errors) private + def prep_transaction + process_api_gateway_info + process_headers + add_agent_attributes + end + def harvest! NewRelic::Agent.instance.harvest_and_send_analytic_event_data NewRelic::Agent.instance.harvest_and_send_custom_event_data @@ -109,6 +119,11 @@ def function_name ENV.fetch(LAMBDA_ENVIRONMENT_VARIABLE, FUNCTION_NAME) end + def category + @category ||= + @event&.dig('requestContext', 'http', 'method') || @event&.fetch('httpMethod', nil) ? :web : :other + end + def write_output string = PAYLOAD_VERSION == 1 ? payload_v1 : payload_v2 @@ -120,7 +135,7 @@ def write_output "BEGIN PAYLOAD>>>\n#{string}\n<< metadata, 'data' => @payloads} json = NewRelic::Agent.agent.service.marshaller.dump(payload_hash) gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json) @@ -129,7 +144,7 @@ def payload_v1 ::JSON.dump(array) end - def payload_v2 + def payload_v2 # New Relic serverless payload v2 json = NewRelic::Agent.agent.service.marshaller.dump(@payloads) gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json) base64_encoded = NewRelic::Base64.strict_encode64(gzipped) @@ -137,6 +152,84 @@ def payload_v2 ::JSON.dump(array) end + def determine_api_gateway_version + return unless @event + + version = @event.fetch('version', '') + if version.start_with?('2.') + return 2 + elsif version.start_with?('1.') + return 1 + end + + headers = headers_from_event + return unless headers + + if @event.dig('requestContext', 'http', 'path') && @event.dig('requestContext', 'http', 'method') + 2 + elsif @event.fetch('path', nil) && @event.fetch('httpMethod', nil) + 1 + end + end + + def process_api_gateway_info + api_v = determine_api_gateway_version + return unless api_v + + info = api_v == 2 ? info_for_api_gateway_v2 : info_for_api_gateway_v1 + info[:query_parameters] = @event.fetch('queryStringParameters', nil) + + @http_method = info[:method] + @http_uri = http_uri(info) + end + + def http_uri(info) + return unless info[:host] && info[:path] + + url_str = "https://#{info[:host]}:#{info[:port]}#{info[:path]}" + if info[:query_parameters] + qp = info[:query_parameters].map { |k, v| "#{k}=#{v}" }.join('&') + url_str += "?#{qp}" + end + + URI.parse(url_str) + rescue StandardError + end + + def info_for_api_gateway_v2 + ctx = @event.fetch('requestContext', nil) + return {} unless ctx + + {method: ctx.dig('http', 'method'), + path: ctx.dig('http', 'path'), + host: ctx.fetch('domainName', @event.dig('headers', 'Host')), + port: @event.dig('headers', 'X-Forwarded-Port') || 443} + end + + def info_for_api_gateway_v1 + headers = headers_from_event + {method: @event.fetch('httpMethod', nil), + path: @event.fetch('path', nil), + host: headers.fetch('Host', nil), + port: headers.fetch('X-Forwarded-Port', 443)} + end + + def process_headers + return unless ::NewRelic::Agent.config[:'distributed_tracing.enabled'] + + headers = headers_from_event + return unless headers && !headers.empty? + + dt_headers = headers.fetch(NewRelic::NEWRELIC_KEY, nil) + return unless dt_headers + + ::NewRelic::Agent::DistributedTracing::accept_distributed_trace_headers(dt_headers, 'Other') + end + + def headers_from_event + @headers ||= @event&.dig('requestContext', 'http') || @event&.dig('headers') + end + def use_named_pipe? return @use_named_pipe if defined?(@use_named_pipe) @@ -146,15 +239,141 @@ def use_named_pipe? def add_agent_attributes return unless NewRelic::Agent::Tracer.current_transaction - add_agent_attribute(ATTRIBUTE_COLD_START, true) if cold? - add_agent_attribute(ATTRIBUTE_ARN, @context.invoked_function_arn) - add_agent_attribute(ATTRIBUTE_REQUEST_ID, @context.aws_request_id) + add_agent_attribute('aws.lambda.coldStart', true) if cold? + add_agent_attribute('aws.lambda.arn', @context.invoked_function_arn) + add_agent_attribute('aws.requestId', @context.aws_request_id) + + add_event_source_attributes + add_http_attributes if api_gateway_event? + end + + def add_http_attributes + return unless category == :web + + if @http_uri + add_agent_attribute('uri.host', @http_uri.host) + add_agent_attribute('uri.port', @http_uri.port) + if NewRelic::Agent.instance.attribute_filter.allows_key?('http.url', AttributeFilter::DST_SPAN_EVENTS) + add_agent_attribute('http.url', @http_uri.to_s) + end + end + + if @http_method + add_agent_attribute('http.method', @http_method) + add_agent_attribute('http.request.method', @http_method) + end + end + + def api_gateway_event? + return false unless @event + + # '1.0' for API Gateway V1, '2.0' for API Gateway V2 + return true if @event.fetch('version', '').start_with?(DIGIT) + + return false unless headers_from_event + + # API Gateway V1 - look for toplevel 'path' and 'httpMethod' keys if a version is unset + return true if @event.fetch('path', nil) && @event.fetch('httpMethod', nil) + + # API Gateway V2 - look for 'requestContext/http' inner nested 'path' and 'method' keys if a version is unset + return true if @event.dig('requestContext', 'http', 'path') && @event.dig('requestContext', 'http', 'method') + + false + end + + def add_event_source_attributes + arn = event_source_arn + add_agent_attribute('aws.lambda.eventSource.arn', arn) if arn + + info = event_source_event_info + return unless info + + add_agent_attribute('aws.lambda.eventSource.eventType', info['name']) + + info['attributes'].each do |name, elements| + next if elements.empty? + + size = false + if elements.last.eql?('#size') + elements.pop + size = true + end + value = @event.dig(*elements) + value = value.size if size + next unless value + + add_agent_attribute(name, value) + end + end + + def event_source_arn + return unless @event + + # SQS/Kinesis Stream/DynamoDB/CodeCommit/S3/SNS + return event_source_arn_for_records if @event.fetch('Records', nil) + + # Kinesis Firehose + ds_arn = @event.fetch('deliveryStreamArn', nil) if @event.fetch('records', nil) + return ds_arn if ds_arn + + # ELB + elb_arn = @event.dig('requestContext', 'elb', 'targetGroupArn') + return elb_arn if elb_arn + + # (other) + es_arn = @event.dig('resources', 0) + return es_arn if es_arn + + NewRelic::Agent.logger.debug 'Unable to determine an event source arn' + + nil + end + + def event_source_event_info + return unless @event + + # if every required key for a source is found, consider that source + # to be a match + EVENT_SOURCES.each do |_type, info| + return info unless info['required_keys'].detect { |r| @event.dig(*r).nil? } + end + + nil + end + + def event_source_arn_for_records + record = @event['Records'].first + unless record + NewRelic::Agent.logger.debug "Unable to find any records in the event's 'Records' array" + return + end + + arn = record.fetch('eventSourceARN', nil) || # SQS/Kinesis Stream/DynamoDB/CodeCommit + record.dig('s3', 'bucket', 'arn') || # S3 + record.fetch('EventSubscriptionArn', nil) # SNS + + unless arn + NewRelic::Agent.logger.debug "Unable to determine an event source arn from the event's 'Records' array" + end + + arn end def add_agent_attribute(attribute, value) NewRelic::Agent::Tracer.current_transaction.add_agent_attribute(attribute, value, AGENT_ATTRIBUTE_DESTINATIONS) end + def process_response(response) + return response unless category == :web && response.respond_to?(:fetch) + + http_status = response.fetch(:statusCode, response.fetch('statusCode', nil)) + return unless http_status + + add_agent_attribute('http.statusCode', http_status) + + response + end + def cold? return @cold if defined?(@cold) @@ -163,7 +382,12 @@ def cold? end def reset! + @event = nil + @category = nil @context = nil + @headers = nil + @http_method = nil + @http_uri = nil @payloads.replace({}) end end diff --git a/lib/new_relic/agent/serverless_handler_event_sources.json b/lib/new_relic/agent/serverless_handler_event_sources.json new file mode 100644 index 0000000000..f0c9a4d054 --- /dev/null +++ b/lib/new_relic/agent/serverless_handler_event_sources.json @@ -0,0 +1,155 @@ +{ + "alb": { + "attributes": {}, + "name": "alb", + "required_keys": [ + "httpMethod", + "requestContext.elb" + ] + }, + + "apiGateway": { + "attributes": { + "aws.lambda.eventSource.accountId": "requestContext.accountId", + "aws.lambda.eventSource.apiId": "requestContext.apiId", + "aws.lambda.eventSource.resourceId": "requestContext.resourceId", + "aws.lambda.eventSource.resourcePath": "requestContext.resourcePath", + "aws.lambda.eventSource.stage": "requestContext.stage" + }, + "name": "apiGateway", + "required_keys": [ + "headers", + "httpMethod", + "path", + "requestContext", + "requestContext.stage" + ] + }, + + "apiGatewayV2": { + "attributes": { + "aws.lambda.eventSource.accountId": "requestContext.accountId", + "aws.lambda.eventSource.apiId": "requestContext.apiId", + "aws.lambda.eventSource.stage": "requestContext.stage" + }, + "name": "apiGatewayV2", + "required_keys": [ + "version", + "headers", + "requestContext.http", + "requestContext.http.path", + "requestContext.http.method", + "requestContext.stage" + ] + }, + + "cloudFront": { + "attributes": {}, + "name": "cloudFront", + "required_keys": [ + "Records[0].cf" + ] + }, + + "cloudWatchScheduled": { + "attributes": { + "aws.lambda.eventSource.account": "account", + "aws.lambda.eventSource.id": "id", + "aws.lambda.eventSource.region": "region", + "aws.lambda.eventSource.resource": "resources[0]", + "aws.lambda.eventSource.time": "time" + }, + "name": "cloudWatch_scheduled", + "required_keys": [ + "detail-type", + "source" + ] + }, + + "dynamoStreams": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length" + }, + "name": "dynamo_streams", + "required_keys": [ + "Records[0].dynamodb" + ] + }, + + "firehose": { + "attributes": { + "aws.lambda.eventSource.length": "records.length", + "aws.lambda.eventSource.region": "region" + }, + "name": "firehose", + "required_keys": [ + "deliveryStreamArn", + "records[0].kinesisRecordMetadata" + ] + }, + + "kinesis": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.region": "Records[0].awsRegion" + }, + "name": "kinesis", + "required_keys": [ + "Records[0].kinesis" + ] + }, + + "s3": { + "attributes": { + "aws.lambda.eventSource.bucketName": "Records[0].s3.bucket.name", + "aws.lambda.eventSource.eventName": "Records[0].eventName", + "aws.lambda.eventSource.eventTime": "Records[0].eventTime", + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.objectKey": "Records[0].s3.object.key", + "aws.lambda.eventSource.objectSequencer": "Records[0].s3.object.sequencer", + "aws.lambda.eventSource.objectSize": "Records[0].s3.object.size", + "aws.lambda.eventSource.region": "Records[0].awsRegion" + }, + "name": "s3", + "required_keys": [ + "Records[0].s3" + ] + }, + + "ses": { + "attributes": { + "aws.lambda.eventSource.date": "Records[0].ses.mail.commonHeaders.date", + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.messageId": "Records[0].ses.mail.commonHeaders.messageId", + "aws.lambda.eventSource.returnPath": "Records[0].ses.mail.commonHeaders.returnPath" + }, + "name": "ses", + "required_keys": [ + "Records[0].ses" + ] + }, + + "sns": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.messageId": "Records[0].Sns.MessageId", + "aws.lambda.eventSource.timestamp": "Records[0].Sns.Timestamp", + "aws.lambda.eventSource.topicArn": "Records[0].Sns.TopicArn", + "aws.lambda.eventSource.type": "Records[0].Sns.Type" + }, + "name": "sns", + "required_keys": [ + "Records[0].Sns" + ] + }, + + "sqs": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length" + }, + "name": "sqs", + "required_keys": [ + "Records[0].receiptHandle" + ] + } +} diff --git a/lib/new_relic/agent/serverless_handler_event_sources.rb b/lib/new_relic/agent/serverless_handler_event_sources.rb new file mode 100644 index 0000000000..5584c27d21 --- /dev/null +++ b/lib/new_relic/agent/serverless_handler_event_sources.rb @@ -0,0 +1,51 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'json' + +module NewRelic + module Agent + # ServerlessHandlerEventSources - New Relic's language agent devs maintain + # a cross-agent JSON map of all AWS resources with the potential to invoke + # an AWS Lambda function by issuing it an event. This map is used to glean + # source specific attributes while instrumenting the function's invocation. + # + # Given that the event arrives as a Ruby hash argument to the AWS Lambda + # function, the JSON map's values need to be converted into arrays that can + # be passed to `Hash#dig`. So a value such as `'records[0].name'` needs to + # be converted to `['records', 0, 'name']`. This class's `.to_hash` method + # yields the converted data. + # + # Furthermore, `.length` calls are converted to Ruby `#size` notation to + # denote that a method call must be performed on the dug value. + class ServerlessHandlerEventSources + JSON_SOURCE = File.join(File.dirname(__FILE__), 'serverless_handler_event_sources.json') + + def self.to_hash + hash = {} + raw = JSON.parse(File.read('lib/new_relic/agent/serverless_handler_event_sources.json')) + raw.each do |type, info| + hash[type] = {'attributes' => {}, + 'name' => info['name'], + 'required_keys' => []} + info['attributes'].each { |attr, value| hash[type]['attributes'][attr] = transform(value) } + info['required_keys'].each { |key| hash[type]['required_keys'].push(transform(key)) } + end + hash.freeze + end + + def self.transform(value) + value.gsub(/\[(\d+)\]/, '.\1').split('.').map do |e| + if e.match?(/^\d+$/) + e.to_i + elsif e == 'length' + '#size' + else + e + end + end + end + end + end +end diff --git a/lib/new_relic/agent/transaction/trace_context.rb b/lib/new_relic/agent/transaction/trace_context.rb index 26b267161f..dfca576427 100644 --- a/lib/new_relic/agent/transaction/trace_context.rb +++ b/lib/new_relic/agent/transaction/trace_context.rb @@ -95,7 +95,7 @@ def create_trace_state def create_trace_state_payload unless Agent.config[:'distributed_tracing.enabled'] - NewRelic::Agent.logger.warn('Not configured to create WC3 trace context payload') + NewRelic::Agent.logger.warn('Not configured to create W3C trace context payload') return end diff --git a/test/new_relic/agent/serverless_handler_event_sources_test.rb b/test/new_relic/agent/serverless_handler_event_sources_test.rb new file mode 100644 index 0000000000..b09587df68 --- /dev/null +++ b/test/new_relic/agent/serverless_handler_event_sources_test.rb @@ -0,0 +1,40 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'json' + +require_relative '../../test_helper' + +module NewRelic::Agent + class ServerlessHandlerEventSourcesTest < Minitest::Test + def test_hash_sanity + hash = ServerlessHandlerEventSources.to_hash + resources = %w[alb apiGateway apiGatewayV2 cloudFront cloudWatchScheduled dynamoStreams firehose kinesis s3 ses + sns sqs] + + assert_equal(resources.sort, hash.keys.sort) + assert_equal(%w[records #size], hash['firehose']['attributes']['aws.lambda.eventSource.length']) + assert_equal(['Records', 0, 's3', 'object', 'size'], + hash['s3']['attributes']['aws.lambda.eventSource.objectSize']) + end + + def test_transform_dig_functionality + input = 'action.collection[1138].key' + + assert_equal(['action', 'collection', 1138, 'key'], ServerlessHandlerEventSources.transform(input)) + end + + def test_transform_length_functionality + input = 'a.collection.length' + + assert_equal(['a', 'collection', '#size'], ServerlessHandlerEventSources.transform(input)) + end + + def test_transform_pass_through_functionality + input = 'simple' + + assert_equal([input], ServerlessHandlerEventSources.transform(input)) + end + end +end diff --git a/test/new_relic/agent/serverless_handler_test.rb b/test/new_relic/agent/serverless_handler_test.rb index 41dc447d25..7a7ebe3d91 100644 --- a/test/new_relic/agent/serverless_handler_test.rb +++ b/test/new_relic/agent/serverless_handler_test.rb @@ -2,6 +2,7 @@ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. # frozen_string_literal: true +require 'json' require 'logger' require 'stringio' require 'tempfile' @@ -32,6 +33,46 @@ def self.customer_lambda_function(event:, context:) module NewRelic::Agent class ServerlessHandler class ServerlessHandlerTest < Minitest::Test + EVENT_SOURCES = JSON.parse(File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cross_agent_tests', 'lambda', 'event_source_info.json'))) + + AWS_TYPE_SPECIFIC_ATTRIBUTES = { + 's3' => {'aws.lambda.eventSource.bucketName' => 'example-bucket', + 'aws.lambda.eventSource.eventName' => 'ObjectCreated:Put', + 'aws.lambda.eventSource.eventTime' => '1970-01-01T00:00:00.000Z', + 'aws.lambda.eventSource.length' => 1, + 'aws.lambda.eventSource.objectKey' => 'test/key', + 'aws.lambda.eventSource.objectSequencer' => '0A1B2C3D4E5F678901', + 'aws.lambda.eventSource.objectSize' => 1024, + 'aws.lambda.eventSource.region' => 'us-west-2'}, + 'dynamo_streams' => {'aws.lambda.eventSource.length' => 3}, + 'firehose' => {'aws.lambda.eventSource.length' => 1, + 'aws.lambda.eventSource.region' => 'us-west-2'}, + 'cloudFront' => {}, + 'sqs' => {'aws.lambda.eventSource.length' => 1}, + 'apiGateway' => {'aws.lambda.eventSource.accountId' => '123456789012', + 'aws.lambda.eventSource.apiId' => '1234567890', + 'aws.lambda.eventSource.resourceId' => '123456', + 'aws.lambda.eventSource.resourcePath' => '/{proxy+}', + 'aws.lambda.eventSource.stage' => 'prod'}, + 'cloudWatch_scheduled' => {'aws.lambda.eventSource.account' => '{{{account-id}}}', + 'aws.lambda.eventSource.id' => 'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', + 'aws.lambda.eventSource.region' => 'us-west-2', + 'aws.lambda.eventSource.resource' => 'arn:aws:events:us-west-2:123456789012:rule/ExampleRule', + 'aws.lambda.eventSource.time' => '1970-01-01T00:00:00Z'}, + 'ses' => {'aws.lambda.eventSource.date' => 'Wed, 7 Oct 2015 12:34:56 -0700', + 'aws.lambda.eventSource.length' => 1, + 'aws.lambda.eventSource.messageId' => '<0123456789example.com>', + 'aws.lambda.eventSource.returnPath' => 'janedoe@example.com'}, + 'sns' => {'aws.lambda.eventSource.length' => 1, + 'aws.lambda.eventSource.messageId' => '95df01b4-ee98-5cb9-9903-4c221d41eb5e', + 'aws.lambda.eventSource.timestamp' => '1970-01-01T00:00:00.000Z', + 'aws.lambda.eventSource.topicArn' => 'arn:aws:sns:us-west-2:123456789012:ExampleTopic', + 'aws.lambda.eventSource.type' => 'Notification'}, + 'alb' => {}, + 'kinesis' => {'aws.lambda.eventSource.length' => 1, + 'aws.lambda.eventSource.region' => 'us-west-2'} + } + def setup config_hash = {:'serverless_mode.enabled' => true} @test_config = NewRelic::Agent::Configuration::DottedHash.new(config_hash, true) @@ -162,6 +203,77 @@ def test_support_for_payload_format_v1 end end + def test_distributed_tracing_for_api_gateway_v1 + event = {'version' => '1.0', + 'httpMethod' => 'POST', + 'headers' => {NewRelic::NEWRELIC_KEY => { + NewRelic::TRACEPARENT_KEY => '00-a8e67265afe2773a3c611b94306ee5c2-fb1010463ea28a38-01', + NewRelic::TRACESTATE_KEY => '190@nr=0-0-190-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035' + }}} + perform_distributed_tracing_based_invocation(event) + end + + def test_distributed_tracing_for_api_gateway_v2 + event = {'version' => '2.0', + 'httpMethod' => 'POST', + 'requestContext' => {'http' => {NewRelic::NEWRELIC_KEY => { + NewRelic::TRACEPARENT_KEY => '00-a8e67265afe2773a3c611b94306ee5c2-fb1010463ea28a38-01', + NewRelic::TRACESTATE_KEY => '190@nr=0-0-190-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035' + }}}} + perform_distributed_tracing_based_invocation(event) + end + + def test_reports_web_attributes_for_api_gateway_v1 + event = {'version' => '1.0', + 'resource' => '/RG35XXSP', + 'path' => '/default/RG35XXSP', + 'httpMethod' => 'POST', + 'headers' => {'Content-Length' => '1138', + 'Content-Type' => 'application/json', + 'Host' => 'garbanz0.execute-api.us-west-1.amazonaws.com', + 'User-Agent' => 'curl/8.4.0', + 'X-Amzn-Trace-Id' => 'Root=1-08675309-3e0mfbschanamasala8302xv1', + 'X-Forwarded-For' => '123.456.769.101', + 'X-Forwarded-Port' => '443', + 'X-Forwarded-Proto' => 'https', + 'accept' => '*/*'}, + 'queryStringParameters' => {'param1': 'value1', 'param2': 'value2'}, + 'pathParameters' => nil, + 'stageVariables' => nil, + 'body' => '{"thekey1":"thevalue1"}', + 'isBase64Encoded' => false} + perform_http_attribute_based_invocation(event) + end + + def test_reports_web_attributes_for_api_gateway_v2 + event = {'version' => '2.0', + 'headers' => {'X-Forwarded-Port' => 443}, + 'queryStringParameters' => {'param1': 'value1', 'param2': 'value2'}, + 'requestContext' => {'http' => {'method' => 'POST', + 'path' => '/default/RG35XXSP'}, + 'domainName' => 'garbanz0.execute-api.us-west-1.amazonaws.com'}} + perform_http_attribute_based_invocation(event) + end + + EVENT_SOURCES.each do |type, info| + define_method(:"test_event_type_#{type}") do + output = with_output do + handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, + event: info['event'], + context: testing_context) + end + attributes = output.last['analytic_event_data'].last.last.last + + assert_equal info['expected_arn'], attributes['aws.lambda.eventSource.arn'] + assert_equal info['expected_type'], attributes['aws.lambda.eventSource.eventType'] + + AWS_TYPE_SPECIFIC_ATTRIBUTES[type].each do |key, value| + assert_equal value, attributes[key], + "Expected agent attribute of '#{key}' with a value of '#{value}'. Got '#{attributes['key']}'" + end + end + end + # unit style def test_named_pipe_check_true @@ -345,6 +457,51 @@ def with_output(&block) temp.close temp.unlink end + + def distributed_tracing_config + { + :account_id => 190, + :primary_application_id => '2827902', + :trusted_account_key => 190, + :'span_events.enabled' => true, + :'distributed_tracing.enabled' => true + } + end + + def perform_distributed_tracing_based_invocation(event) + output = nil + with_config(distributed_tracing_config) do + NewRelic::Agent.config.notify_server_source_added + output = with_output do + handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, + event: event, + context: testing_context) + end + end + success = output.last['metric_data'].last.detect do |metrics| + metrics.first['name'] == 'Supportability/TraceContext/Accept/Success' + end + + assert success, 'Failed to detect the supportability metric representing DT success' + end + + def perform_http_attribute_based_invocation(event) + output = with_output do + NewRelic::Agent.instance.attribute_filter.stub(:allows_key?, true, ['http.url', AttributeFilter::DST_SPAN_EVENTS]) do + handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, + event: event, + context: testing_context) + end + end + attrs = output.last['analytic_event_data'].last.last.last + + assert attrs, 'Unable to glean event attributes from the response output' + assert_equal 'POST', attrs.fetch('http.method', nil) + assert_equal 'POST', attrs.fetch('http.request.method', nil) + assert_equal 200, attrs.fetch('http.statusCode', nil) + assert_equal 'https://garbanz0.execute-api.us-west-1.amazonaws.com/default/RG35XXSP?param1=value1¶m2=value2', + attrs.fetch('http.url', nil) + end end end end From 4677f2eb708fce3f75ce810fc07c1bb8d221dae9 Mon Sep 17 00:00:00 2001 From: fallwith Date: Wed, 14 Aug 2024 13:49:35 -0700 Subject: [PATCH 02/13] ServerlessHandlerEventSources: json loading fixes - utilize the constant pointing to the JSON source file - only read from the JSON source file once --- .../agent/serverless_handler_event_sources.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/new_relic/agent/serverless_handler_event_sources.rb b/lib/new_relic/agent/serverless_handler_event_sources.rb index 5584c27d21..59a527edbd 100644 --- a/lib/new_relic/agent/serverless_handler_event_sources.rb +++ b/lib/new_relic/agent/serverless_handler_event_sources.rb @@ -20,19 +20,17 @@ module Agent # Furthermore, `.length` calls are converted to Ruby `#size` notation to # denote that a method call must be performed on the dug value. class ServerlessHandlerEventSources - JSON_SOURCE = File.join(File.dirname(__FILE__), 'serverless_handler_event_sources.json') + JSON_SOURCE = File.join(File.dirname(__FILE__), 'serverless_handler_event_sources.json').freeze + JSON_RAW = JSON.parse(File.read(JSON_SOURCE)).freeze def self.to_hash - hash = {} - raw = JSON.parse(File.read('lib/new_relic/agent/serverless_handler_event_sources.json')) - raw.each do |type, info| + JSON_RAW.each_with_object({}) do |(type, info), hash| hash[type] = {'attributes' => {}, 'name' => info['name'], 'required_keys' => []} info['attributes'].each { |attr, value| hash[type]['attributes'][attr] = transform(value) } info['required_keys'].each { |key| hash[type]['required_keys'].push(transform(key)) } - end - hash.freeze + end.freeze end def self.transform(value) From af5a204d80295a271ccc3fe8b2c7545eccebf8cd Mon Sep 17 00:00:00 2001 From: fallwith Date: Wed, 14 Aug 2024 14:02:40 -0700 Subject: [PATCH 03/13] serverless tests: require Ruby 3.2+ for 12 types for the dozen AWS resources under test, require Ruby 3.2+ --- test/new_relic/agent/serverless_handler_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/new_relic/agent/serverless_handler_test.rb b/test/new_relic/agent/serverless_handler_test.rb index 7a7ebe3d91..9beae6dcd9 100644 --- a/test/new_relic/agent/serverless_handler_test.rb +++ b/test/new_relic/agent/serverless_handler_test.rb @@ -257,6 +257,8 @@ def test_reports_web_attributes_for_api_gateway_v2 EVENT_SOURCES.each do |type, info| define_method(:"test_event_type_#{type}") do + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: info['event'], From c115011a965226036a201ea824113698ff82db9a Mon Sep 17 00:00:00 2001 From: fallwith Date: Wed, 14 Aug 2024 14:26:37 -0700 Subject: [PATCH 04/13] serverless: additional Ruby v3.2+ constraints skip unless Ruby v3.2+ (minimum available AWS Lambda runtime) --- test/new_relic/agent/serverless_handler_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/new_relic/agent/serverless_handler_test.rb b/test/new_relic/agent/serverless_handler_test.rb index 9beae6dcd9..610a8d6ad4 100644 --- a/test/new_relic/agent/serverless_handler_test.rb +++ b/test/new_relic/agent/serverless_handler_test.rb @@ -204,6 +204,8 @@ def test_support_for_payload_format_v1 end def test_distributed_tracing_for_api_gateway_v1 + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + event = {'version' => '1.0', 'httpMethod' => 'POST', 'headers' => {NewRelic::NEWRELIC_KEY => { @@ -214,6 +216,8 @@ def test_distributed_tracing_for_api_gateway_v1 end def test_distributed_tracing_for_api_gateway_v2 + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + event = {'version' => '2.0', 'httpMethod' => 'POST', 'requestContext' => {'http' => {NewRelic::NEWRELIC_KEY => { @@ -224,6 +228,8 @@ def test_distributed_tracing_for_api_gateway_v2 end def test_reports_web_attributes_for_api_gateway_v1 + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + event = {'version' => '1.0', 'resource' => '/RG35XXSP', 'path' => '/default/RG35XXSP', @@ -246,6 +252,8 @@ def test_reports_web_attributes_for_api_gateway_v1 end def test_reports_web_attributes_for_api_gateway_v2 + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + event = {'version' => '2.0', 'headers' => {'X-Forwarded-Port' => 443}, 'queryStringParameters' => {'param1': 'value1', 'param2': 'value2'}, From 53ef8930faea3723d6ef719a5e3cac540bf4cce4 Mon Sep 17 00:00:00 2001 From: fallwith Date: Wed, 14 Aug 2024 14:43:56 -0700 Subject: [PATCH 05/13] serverless test: additional skips constrain all relevant tests to Ruby 3.2+ --- test/new_relic/agent/serverless_handler_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/new_relic/agent/serverless_handler_test.rb b/test/new_relic/agent/serverless_handler_test.rb index 610a8d6ad4..2fec0cfe18 100644 --- a/test/new_relic/agent/serverless_handler_test.rb +++ b/test/new_relic/agent/serverless_handler_test.rb @@ -87,6 +87,8 @@ def teardown # integration style def test_the_complete_handoff_from_the_nr_lambda_layer + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -102,6 +104,8 @@ def test_the_complete_handoff_from_the_nr_lambda_layer end def test_rescued_errors_are_noticed + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + output = with_output do assert_raises RuntimeError do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -119,6 +123,8 @@ def test_rescued_errors_are_noticed end def test_log_events_are_reported + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: {simulate_logging: true}, @@ -129,6 +135,8 @@ def test_log_events_are_reported end def test_customer_function_lives_within_a_namespace + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -145,6 +153,8 @@ def test_customer_function_lives_within_a_namespace end def test_agent_attributes_are_present + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -161,6 +171,8 @@ def test_agent_attributes_are_present end def test_metric_data_adheres_to_the_agent_specs + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: {}, @@ -187,6 +199,8 @@ def test_metric_data_adheres_to_the_agent_specs end def test_support_for_payload_format_v1 + skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + NewRelic::Agent::ServerlessHandler.stub_const(:PAYLOAD_VERSION, 1) do output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, From 60c285008cd0370ebee46c889c5cd35ea7d85a95 Mon Sep 17 00:00:00 2001 From: fallwith Date: Wed, 14 Aug 2024 14:54:25 -0700 Subject: [PATCH 06/13] serverless handler: use #dup don't destructively alter the hash - use #dup instead --- lib/new_relic/agent/serverless_handler.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/new_relic/agent/serverless_handler.rb b/lib/new_relic/agent/serverless_handler.rb index 1bd7d4cb35..12eeafcfa8 100644 --- a/lib/new_relic/agent/serverless_handler.rb +++ b/lib/new_relic/agent/serverless_handler.rb @@ -295,6 +295,7 @@ def add_event_source_attributes size = false if elements.last.eql?('#size') + elements = elements.dup elements.pop size = true end From 0d693d3f5622238ded1066fb5ef8909c45bf1ead Mon Sep 17 00:00:00 2001 From: James Bunch Date: Wed, 14 Aug 2024 17:33:10 -0700 Subject: [PATCH 07/13] Update CHANGELOG.md Co-authored-by: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25f9e63ab..f3e924feac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Version enhances support for AWS Lambda functions, adds experimental OpenS - **Feature: Enhanced AWS Lambda function instrumentation** -When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. The agent's instrumentation for AWS Lambda functions now supports distributed tracing. Web triggered invocations are now identified as being "web" based when an API Gateway call is involved and both API Gateway versions 1.0 and 2.0 are supported. Web based calls have the HTTP method, URI, and status code recorded. The agent will now recognize and report on fully 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. The type of the triggering resource and its arn will be recorded for each resource and for many of them extra resource specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) +When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. The agent's instrumentation for AWS Lambda functions now supports distributed tracing. Web-triggered invocations are now identified as being "web" based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. Web-based calls have the HTTP method, URI, and status code recorded. The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. The type of the triggering resource and its ARN will be recorded for each resource, and for many of them, extra resource specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) - **Feature: Add experimental OpenSearch instrumentation** From 3b4cb7b3ba02deab113f7e05ee59da57a1281ff7 Mon Sep 17 00:00:00 2001 From: James Bunch Date: Fri, 16 Aug 2024 15:32:24 -0700 Subject: [PATCH 08/13] Update CHANGELOG.md tense fix for lambda updates Co-authored-by: Kayla Reopelle <87386821+kaylareopelle@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ac36c6e3..c11ba58185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Version enhances support for AWS Lambda functions, adds experimental OpenSearch instrumentation, updates framework detection, fixes Falcon dispatcher detection, fixes a bug with Redis instrumentation installation, and addresses a JRuby specific concurrency issue. -- **Feature: Enhanced AWS Lambda function instrumentation** +- **Feature: Enhance AWS Lambda function instrumentation** When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. The agent's instrumentation for AWS Lambda functions now supports distributed tracing. Web-triggered invocations are now identified as being "web" based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. Web-based calls have the HTTP method, URI, and status code recorded. The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. The type of the triggering resource and its ARN will be recorded for each resource, and for many of them, extra resource specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) From 9271b6b4f65a26bcdf39c78eb58e2aac612e39cf Mon Sep 17 00:00:00 2001 From: James Bunch Date: Fri, 16 Aug 2024 15:34:27 -0700 Subject: [PATCH 09/13] Update CHANGELOG.md use bullets for the lambda enhancements Co-authored-by: Kayla Reopelle <87386821+kaylareopelle@users.noreply.github.com> --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11ba58185..946d229641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,13 @@ Version enhances support for AWS Lambda functions, adds experimental OpenS - **Feature: Enhance AWS Lambda function instrumentation** -When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. The agent's instrumentation for AWS Lambda functions now supports distributed tracing. Web-triggered invocations are now identified as being "web" based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. Web-based calls have the HTTP method, URI, and status code recorded. The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. The type of the triggering resource and its ARN will be recorded for each resource, and for many of them, extra resource specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) +When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. +* The agent's instrumentation for AWS Lambda functions now supports distributed tracing. +* Web-triggered invocations are now identified as being "web"-based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. +* Web-based calls have the HTTP method, URI, and status code recorded. +* The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. +* The type of the triggering resource and its ARN will be recorded for each resource, and for many of them, extra resource-specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. +[PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) - **Feature: Add experimental OpenSearch instrumentation** From c27f24c3d82aa2ddd10376fa224765a68234da98 Mon Sep 17 00:00:00 2001 From: fallwith Date: Fri, 16 Aug 2024 15:42:39 -0700 Subject: [PATCH 10/13] serverless: remove rescue left over from testing remove `rescue` that was intended to be temporary --- lib/new_relic/agent/serverless_handler.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/new_relic/agent/serverless_handler.rb b/lib/new_relic/agent/serverless_handler.rb index 12eeafcfa8..ed685150f2 100644 --- a/lib/new_relic/agent/serverless_handler.rb +++ b/lib/new_relic/agent/serverless_handler.rb @@ -193,7 +193,6 @@ def http_uri(info) end URI.parse(url_str) - rescue StandardError end def info_for_api_gateway_v2 From af52a76ae743ea8629c6c9138d6077b753e5ab50 Mon Sep 17 00:00:00 2001 From: fallwith Date: Fri, 16 Aug 2024 15:48:48 -0700 Subject: [PATCH 11/13] serverless: leverage #each_value use `each_value` instead of `each` with `_key` --- lib/new_relic/agent/serverless_handler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/new_relic/agent/serverless_handler.rb b/lib/new_relic/agent/serverless_handler.rb index ed685150f2..e7cfe64974 100644 --- a/lib/new_relic/agent/serverless_handler.rb +++ b/lib/new_relic/agent/serverless_handler.rb @@ -334,7 +334,7 @@ def event_source_event_info # if every required key for a source is found, consider that source # to be a match - EVENT_SOURCES.each do |_type, info| + EVENT_SOURCES.each_value do |info| return info unless info['required_keys'].detect { |r| @event.dig(*r).nil? } end From 839bb1b0e777c4dbe5d710b99efce296de5cc4d3 Mon Sep 17 00:00:00 2001 From: fallwith Date: Fri, 16 Aug 2024 15:56:16 -0700 Subject: [PATCH 12/13] serverless tests: skip unless Ruby 3.2+ the serverless handler is only used with Ruby v3.2+, so only test with Ruby v3.2+ --- .../agent/serverless_handler_test.rb | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/test/new_relic/agent/serverless_handler_test.rb b/test/new_relic/agent/serverless_handler_test.rb index 2fec0cfe18..0c051b24ca 100644 --- a/test/new_relic/agent/serverless_handler_test.rb +++ b/test/new_relic/agent/serverless_handler_test.rb @@ -74,6 +74,8 @@ class ServerlessHandlerTest < Minitest::Test } def setup + skip 'Serverless usage is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 + config_hash = {:'serverless_mode.enabled' => true} @test_config = NewRelic::Agent::Configuration::DottedHash.new(config_hash, true) NewRelic::Agent.config.add_config_for_testing(@test_config, true) @@ -81,14 +83,14 @@ def setup end def teardown + skip unless defined?(@test_config) + NewRelic::Agent.config.remove_config(@test_config) end # integration style def test_the_complete_handoff_from_the_nr_lambda_layer - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -104,8 +106,6 @@ def test_the_complete_handoff_from_the_nr_lambda_layer end def test_rescued_errors_are_noticed - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - output = with_output do assert_raises RuntimeError do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -123,8 +123,6 @@ def test_rescued_errors_are_noticed end def test_log_events_are_reported - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: {simulate_logging: true}, @@ -135,8 +133,6 @@ def test_log_events_are_reported end def test_customer_function_lives_within_a_namespace - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -153,8 +149,6 @@ def test_customer_function_lives_within_a_namespace end def test_agent_attributes_are_present - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - context = testing_context output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -171,8 +165,6 @@ def test_agent_attributes_are_present end def test_metric_data_adheres_to_the_agent_specs - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: {}, @@ -199,8 +191,6 @@ def test_metric_data_adheres_to_the_agent_specs end def test_support_for_payload_format_v1 - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - NewRelic::Agent::ServerlessHandler.stub_const(:PAYLOAD_VERSION, 1) do output = with_output do result = handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, @@ -218,8 +208,6 @@ def test_support_for_payload_format_v1 end def test_distributed_tracing_for_api_gateway_v1 - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - event = {'version' => '1.0', 'httpMethod' => 'POST', 'headers' => {NewRelic::NEWRELIC_KEY => { @@ -230,8 +218,6 @@ def test_distributed_tracing_for_api_gateway_v1 end def test_distributed_tracing_for_api_gateway_v2 - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - event = {'version' => '2.0', 'httpMethod' => 'POST', 'requestContext' => {'http' => {NewRelic::NEWRELIC_KEY => { @@ -242,8 +228,6 @@ def test_distributed_tracing_for_api_gateway_v2 end def test_reports_web_attributes_for_api_gateway_v1 - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - event = {'version' => '1.0', 'resource' => '/RG35XXSP', 'path' => '/default/RG35XXSP', @@ -266,8 +250,6 @@ def test_reports_web_attributes_for_api_gateway_v1 end def test_reports_web_attributes_for_api_gateway_v2 - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - event = {'version' => '2.0', 'headers' => {'X-Forwarded-Port' => 443}, 'queryStringParameters' => {'param1': 'value1', 'param2': 'value2'}, @@ -279,8 +261,6 @@ def test_reports_web_attributes_for_api_gateway_v2 EVENT_SOURCES.each do |type, info| define_method(:"test_event_type_#{type}") do - skip 'This serverless test is limited to Ruby v3.2+' unless ruby_version_float >= 3.2 - output = with_output do handler.invoke_lambda_function_with_new_relic(method_name: :customer_lambda_function, event: info['event'], From f3768db4d5dbcc8d522a36c96928cfc8fec602b6 Mon Sep 17 00:00:00 2001 From: fallwith Date: Fri, 16 Aug 2024 15:58:05 -0700 Subject: [PATCH 13/13] serverless don't freeze a regex constant removed redundant freezing --- lib/new_relic/agent/serverless_handler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/new_relic/agent/serverless_handler.rb b/lib/new_relic/agent/serverless_handler.rb index e7cfe64974..b31f9eb696 100644 --- a/lib/new_relic/agent/serverless_handler.rb +++ b/lib/new_relic/agent/serverless_handler.rb @@ -22,7 +22,7 @@ class ServerlessHandler SUPPORTABILITY_METRIC = 'Supportability/AWSLambda/HandlerInvocation' FUNCTION_NAME = 'lambda_function' PAYLOAD_VERSION = ENV.fetch('NEW_RELIC_SERVERLESS_PAYLOAD_VERSION', 2) - DIGIT = /\d/.freeze + DIGIT = /\d/ EVENT_SOURCES = NewRelic::Agent::ServerlessHandlerEventSources.to_hash def self.env_var_set?