class NewRelic::Agent::ServerlessHandler

Constants

AGENT_ATTRIBUTE_DESTINATIONS
DIGIT
EVENT_SOURCES
EXECUTION_ENVIRONMENT
FUNCTION_NAME
LAMBDA_ENVIRONMENT_VARIABLE
LAMBDA_MARKER
METHOD_BLOCKLIST
NAMED_PIPE
PAYLOAD_VERSION
SUPPORTABILITY_METRIC

Public Class Methods

env_var_set?() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 28
def self.env_var_set?
  ENV.key?(LAMBDA_ENVIRONMENT_VARIABLE)
end
new() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 32
def initialize
  @event = nil
  @context = nil
  @payloads = {}
end

Public Instance Methods

error_data(errors) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 87
def error_data(errors)
  store_payload(:error_data, [nil, errors.map(&:to_collector_array)])
end
invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 38
def invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil)
  NewRelic::Agent.increment_metric(SUPPORTABILITY_METRIC)

  @event, @context = event, context

  NewRelic::Agent::Tracer.in_transaction(category: category, name: function_name) do
    prep_transaction

    process_response(NewRelic::LanguageSupport.constantize(namespace)
      .send(method_name, event: event, context: context))
  end
ensure
  harvest!
  write_output
  reset!
end
metric_data(stats_hash) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 61
def metric_data(stats_hash)
  payload = [nil,
    stats_hash.started_at,
    (stats_hash.harvested_at || Process.clock_gettime(Process::CLOCK_REALTIME)),
    []]
  stats_hash.each do |metric_spec, stats|
    next if stats.is_reset?

    hash = {name: metric_spec.name}
    hash[:scope] = metric_spec.scope unless metric_spec.scope.empty?

    payload.last.push([hash, [
      stats.call_count,
      stats.total_call_time,
      stats.total_exclusive_time,
      stats.min_call_time,
      stats.max_call_time,
      stats.sum_of_squares
    ]])
  end

  return if payload.last.empty?

  store_payload(:metric_data, payload)
end
store_payload(method, payload) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 55
def store_payload(method, payload)
  return if METHOD_BLOCKLIST.include?(method)

  @payloads[method] = payload
end

Private Instance Methods

add_agent_attribute(attribute, value) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 367
def add_agent_attribute(attribute, value)
  NewRelic::Agent::Tracer.current_transaction.add_agent_attribute(attribute, value, AGENT_ATTRIBUTE_DESTINATIONS)
end
add_agent_attributes() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 243
def add_agent_attributes
  return unless NewRelic::Agent::Tracer.current_transaction

  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
add_event_source_attributes() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 288
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 = elements.dup
      elements.pop
      size = true
    end
    value = @event.dig(*elements)
    value = value.size if size
    next unless value

    add_agent_attribute(name, value)
  end
end
add_http_attributes() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 254
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
api_gateway_event?() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 271
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
category() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 122
def category
  @category ||=
    @event&.dig('requestContext', 'http', 'method') || @event&.fetch('httpMethod', nil) ? :web : :other
end
cold?() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 382
def cold?
  return @cold if defined?(@cold)

  @cold = false
  true
end
determine_api_gateway_version() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 155
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
event_source_arn() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 314
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
event_source_arn_for_records() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 349
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
event_source_event_info() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 337
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_value do |info|
    return info unless info['required_keys'].detect { |r| @event.dig(*r).nil? }
  end

  nil
end
function_name() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 118
def function_name
  ENV.fetch(LAMBDA_ENVIRONMENT_VARIABLE, FUNCTION_NAME)
end
harvest!() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 99
def harvest!
  NewRelic::Agent.instance.harvest_and_send_analytic_event_data
  NewRelic::Agent.instance.harvest_and_send_custom_event_data
  NewRelic::Agent.instance.harvest_and_send_data_types
end
headers_from_event() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 233
def headers_from_event
  @headers ||= @event&.dig('requestContext', 'http') || @event&.dig('headers')
end
http_uri(info) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 186
def http_uri(info)
  return unless info[:host] && info[:path]

  url_str = "https://#{info[:host]}"
  url_str += ":#{info[:port]}" unless info[:host].match?(':')
  url_str += "#{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 => e
  NewRelic::Agent.logger.error "ServerlessHandler failed to parse the source HTTP URI: #{e}"
end
info_for_api_gateway_v1() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 213
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
info_for_api_gateway_v2() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 203
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
metadata() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 105
def metadata
  m = {arn: @context.invoked_function_arn,
       protocol_version: NewRelic::Agent::NewRelicService::PROTOCOL_VERSION,
       function_version: @context.function_version,
       execution_environment: EXECUTION_ENVIRONMENT,
       agent_version: NewRelic::VERSION::STRING}
  if PAYLOAD_VERSION >= 2
    m[:metadata_version] = PAYLOAD_VERSION
    m[:agent_language] = NewRelic::LANGUAGE
  end
  m
end
payload_v1() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 138
def payload_v1 # New Relic serverless payload v1
  payload_hash = {'metadata' => metadata, 'data' => @payloads}
  json = NewRelic::Agent.agent.service.marshaller.dump(payload_hash)
  gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
  base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
  array = [PAYLOAD_VERSION, LAMBDA_MARKER, base64_encoded]
  ::JSON.dump(array)
end
payload_v2() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 147
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)
  array = [PAYLOAD_VERSION, LAMBDA_MARKER, metadata, base64_encoded]
  ::JSON.dump(array)
end
prep_transaction() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 93
def prep_transaction
  process_api_gateway_info
  process_headers
  add_agent_attributes
end
process_api_gateway_info() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 175
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
process_headers() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 221
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
process_response(response) click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 371
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
reset!() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 389
def reset!
  @event = nil
  @category = nil
  @context = nil
  @headers = nil
  @http_method = nil
  @http_uri = nil
  @payloads.replace({})
end
use_named_pipe?() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 237
def use_named_pipe?
  return @use_named_pipe if defined?(@use_named_pipe)

  @use_named_pipe = File.exist?(NAMED_PIPE) && File.writable?(NAMED_PIPE)
end
write_output() click to toggle source
# File lib/new_relic/agent/serverless_handler.rb, line 127
def write_output
  string = PAYLOAD_VERSION == 1 ? payload_v1 : payload_v2

  return puts string unless use_named_pipe?

  File.write(NAMED_PIPE, string)

  NewRelic::Agent.logger.debug "Wrote serverless payload to #{NAMED_PIPE}\n" \
    "BEGIN PAYLOAD>>>\n#{string}\n<<<END PAYLOAD"
end