Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
AllCops:
TargetRubyVersion: 3.2
Exclude:
- 'test/**/*'

Metrics/MethodLength:
Max: 20
13 changes: 9 additions & 4 deletions lib/datadog/lambda.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require 'datadog/lambda/trace/listener'
require 'datadog/lambda/utils/logger'
require 'datadog/lambda/utils/extension'
require 'datadog/lambda/utils/version'
require 'datadog/lambda/trace/patch_http'
require 'json'
require 'datadog/lambda/version'
Expand All @@ -29,6 +30,8 @@ module Lambda
# Configures Datadog's APM tracer with lambda specific defaults.
# Same options can be given as Datadog.configure in tracer
# See https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#quickstart-for-ruby-applications
#
# rubocop:disable Metrics/AbcSize
def self.configure_apm
require 'datadog/tracing'
require 'datadog/tracing/transport/io'
Expand All @@ -48,8 +51,12 @@ def self.configure_apm
c.tracing.instrument :aws if trace_managed_services?

yield(c) if block_given?

# Activation is gated by AppSec.enabled? at runtime — this only registers the integration
c.appsec.instrument(:aws_lambda)
Comment thread
Strech marked this conversation as resolved.
end
end
# rubocop:enable Metrics/AbcSize

# Wrap the body of a lambda invocation
# @param event [Object] event sent to lambda
Expand Down Expand Up @@ -113,10 +120,8 @@ def self.gen_enhanced_tags(context)
resource: context.function_name,
datadog_lambda: Datadog::Lambda::VERSION::STRING.to_sym
}
begin
tags[:dd_trace] = Gem.loaded_specs['datadog'].version
rescue StandardError
Datadog::Utils.logger.debug 'datadog unavailable'
if (dd_trace = Datadog::Utils.dd_trace_version)
tags[:dd_trace] = dd_trace
end
# If we have an alias...
unless function_alias.nil?
Expand Down
86 changes: 86 additions & 0 deletions lib/datadog/lambda/appsec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

require_relative 'appsec/request'

module Datadog
module Lambda
# AppSec integration for AWS Lambda invocations.
module AppSec
class << self
def on_start(event, trace:, span:, inferred_span: nil)
@request = nil
@inferred_span = inferred_span
return unless enabled?

context = create_context(trace, span)
return unless Datadog::AppSec::Context.active

@request = Request.from_event(event)

payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
event, context: context
)
Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.request.start', payload)
rescue StandardError => e
Datadog::Utils.logger.debug "failed to start AppSec: #{e}"
end

def on_finish(response)
return unless enabled?

context = Datadog::AppSec::Context.active
return unless context

payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
response, context: context
)

Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.response.start', payload)
Datadog::AppSec::Event.record(context, request: @request)
copy_appsec_span_tags(context.span, @inferred_span)

context.export_metrics
context.export_request_telemetry
rescue StandardError => e
Datadog::Utils.logger.debug "failed to finish AppSec: #{e}"
ensure
Datadog::AppSec::Context.deactivate if context
end

private

def enabled?
defined?(Datadog::AppSec) &&
Datadog::AppSec.respond_to?(:enabled?) &&
Datadog::AppSec.enabled?
end

# NOTE: _dd.appsec.json is required on both the service-entry span
# and the inferred span.
def copy_appsec_span_tags(source_span, destination_span)
return unless source_span && destination_span

appsec_json = source_span.get_tag('_dd.appsec.json')
destination_span.set_tag('_dd.appsec.json', appsec_json) if appsec_json

appsec_enabled = source_span.get_metric('_dd.appsec.enabled')
destination_span.set_metric('_dd.appsec.enabled', appsec_enabled) if appsec_enabled
end

def create_context(trace, span)
return if trace.nil? || span.nil?

security_engine = Datadog::AppSec.security_engine
return unless security_engine

context = Datadog::AppSec::Context.new(trace, span, security_engine.new_runner)
Datadog::AppSec::Context.activate(context)

span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1)

context
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/datadog/lambda/appsec/request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module Datadog
module Lambda
module AppSec
# Minimal request object for AppSec event recording.
#
# WARNING: It's a minimal data for interface compliance
#
# @see Datadog::AppSec::Event.record
# @see Datadog::AppSec::Contrib::Rack::Gateway::Request
class Request
attr_reader :host, :user_agent, :remote_addr, :headers

class << self
def from_event(event)
headers = normalize_headers(event)
remote_addres = event.dig('requestContext', 'identity', 'sourceIp') ||
event.dig('requestContext', 'http', 'sourceIp')

new(
host: headers['host'],
user_agent: headers['user-agent'],
remote_addr: remote_addres,
headers: headers
)
end

private

def normalize_headers(event)
event.fetch('headers', {}).each_with_object({}) do |(key, value), hash|
hash[key.downcase] = value
end
end
end

def initialize(host:, user_agent:, remote_addr:, headers:)
@host = host
@user_agent = user_agent
@remote_addr = remote_addr
@headers = headers
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/datadog/lambda/event_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

#
# Unless explicitly stated otherwise all files in this repository are licensed
# under the Apache License Version 2.0.
#
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2026 Datadog, Inc.
#

require_relative 'event_source/api_gateway_v1'
require_relative 'event_source/api_gateway_v2'

module Datadog
module Lambda
# Detects and parses Lambda event payloads into a uniform interface.
module EventSource
SOURCES = [ApiGatewayV1, ApiGatewayV2].freeze

def self.for(event)
klass = SOURCES.find { |source| source.match?(event) }
klass&.new(event)
end
end
end
end
54 changes: 54 additions & 0 deletions lib/datadog/lambda/event_source/api_gateway_v1.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

#
# Unless explicitly stated otherwise all files in this repository are licensed
# under the Apache License Version 2.0.
#
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2026 Datadog, Inc.
#

module Datadog
module Lambda
module EventSource
# Parses API Gateway REST API (v1) Lambda proxy integration events
# into a uniform interface.
#
# @see https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
class ApiGatewayV1
class << self
def match?(payload)
api_gateway?(payload) && payload.key?('httpMethod')
end

private

def api_gateway?(payload)
payload.is_a?(Hash) &&
payload.key?('requestContext') && payload['requestContext'].key?('stage')
end
end

def initialize(payload)
@payload = payload
@request_context = payload.fetch('requestContext', {})
end

def method = @payload['httpMethod']
def path = @payload.fetch('path', '/')
def resource_path = @request_context.fetch('resourcePath', path)
def domain = @request_context['domainName']
def api_id = @request_context['apiId']
def stage = @request_context['stage']
def request_time_ms = @request_context['requestTimeEpoch']
def user_agent = @request_context.dig('identity', 'userAgent')

def http_url
return path unless domain

"https://#{domain}#{path}"
end
end
end
end
end
55 changes: 55 additions & 0 deletions lib/datadog/lambda/event_source/api_gateway_v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

#
# Unless explicitly stated otherwise all files in this repository are licensed
# under the Apache License Version 2.0.
#
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2026 Datadog, Inc.
#

module Datadog
module Lambda
module EventSource
# Parses API Gateway HTTP API (v2) Lambda proxy integration events
# into a uniform interface.
#
# @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format
class ApiGatewayV2
class << self
def match?(payload)
api_gateway?(payload) && payload.key?('routeKey')
end

private

def api_gateway?(payload)
payload.is_a?(Hash) &&
payload.key?('requestContext') && payload['requestContext'].key?('stage')
end
end

def initialize(payload)
@payload = payload
@request_context = payload.fetch('requestContext', {})
@http = @request_context.fetch('http', {})
end

def method = @http['method']
def path = @payload.fetch('rawPath', '/')
def resource_path = @payload['routeKey']&.sub(/\A[A-Z]+ /, '') || path
def domain = @request_context['domainName']
def api_id = @request_context['apiId']
def stage = @request_context['stage']
def request_time_ms = @request_context['timeEpoch']
def user_agent = @http['userAgent']

def http_url
return path unless domain

"https://#{domain}#{path}"
end
end
end
end
end
Loading
Loading