apm-agent-python
apm-agent-python copied to clipboard
ElasticAPM fails to process the context from authorizer
Describe the bug:
[ERROR] AttributeError: 'dict' object has no attribute 'aws_request_id'
Traceback (most recent call last):
File "/usr/local/lib/python3.12/importlib/__init__.py", line 90, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 999, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/opt/threatintel/service.py", line 45, in <module>
@handler
File "/usr/local/lib/python3.12/site-packages/elasticapm/contrib/serverless/aws.py", line 111, in decorated
with _lambda_transaction(func, name, client, event, context) as sls:
File "/usr/local/lib/python3.12/site-packages/elasticapm/contrib/serverless/aws.py", line 257, in __enter__
self.set_metadata_and_context(cold_start)
File "/usr/local/lib/python3.12/site-packages/elasticapm/contrib/serverless/aws.py", line 313, in set_metadata_and_context
faas["execution"] = self.context.aws_request_id
Interesting enough, when disabling Elastic APM (commenting out), the function runs successfully and the context does have a valid aws_request_id.
Context: LambdaContext([aws_request_id=056c5ea1-0f46-44e3-bd86-09743a79bafc,log_group_name=/aws/lambda/<REDACTED>,log_stream_name=2025/06/06/[$LATEST]<REDACTED>function_name=<REDACTED>memory_limit_in_mb=256,function_version=$LATEST,invoked_function_arn=arn:aws:lambda:us-east-1:<REDACTED>:function:<REDACTED>,client_context=None,identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None]),tenant_id=None])
To Reproduce
- Create a AWS Lambda function to be used as an AWS API Gateway Authorizer
- Instrument with Elastic APM using
capture_serverless()at entrypoint (handler) - Submit a request to your API Gateway that would invoke the authorizer
- Observe error due to context missing value
Environment (please complete the following information)
- OS: [e.g. Linux] Debian (Docker image: python:3.12-slim)
- Python version: 3.12
- Framework and version [e.g. Django 2.1]: N/A
- APM Server version: 8.18.2
- Agent version: 6.23.0, Lambda extension: 1.5.8
Additional context
Add any other context about the problem here.
-
Agent config options
Click to expand
replace this line with your agent config options remember to mask any sensitive fields like tokens -
requirements.txt:Click to expand
replace this line with your `requirements.txt`
Thanks for reporting, [ERROR] AttributeError: 'dict' object has no attribute 'aws_request_id' kinda suggest that self.context["aws_request_id"] would have worked? Or maybe that's just the internals of the LambdaContext class
@xrmx Yea I think we could just add an type (isinstance) check and then access accordingly?
Happy to submit a code update for this too but wanted to validate the bug first.
@xrmx Yea I think we could just add an type (
isinstance) check and then access accordingly?
Don't know yet, need to reproduce and sort out why LambdaContext is not what we expect
Hey @xrmx any update here? Happy to help in any way I can!
@brett-fitz what kind of authorizer do you have? Can't reproduce with a lambda request authorizer, context is the following:
<class 'awslambdaric.lambda_context.LambdaContext'> ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_epoch_deadline_time_in_ms', 'aws_request_id', 'client_context', 'function_name', 'function_version', 'get_remaining_time_in_millis', 'identity', 'invoked_function_arn', 'log', 'log_group_name', 'log_stream_name', 'memory_limit_in_mb', 'tenant_id']
Hey @xrmx I can share our authorizer code here since its pretty simple. Thank you again for taking a look at this issue and let me know if you need anything else!
api-authorizer/service.py
from __future__ import annotations
from functools import lru_cache
import logging
from os import environ
import ipaddress
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
APIGatewayAuthorizerRequestEvent,
APIGatewayAuthorizerResponse,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from threatintel.utils.environment import ENVIRONMENT
if ENVIRONMENT.context.runtime != "aws":
environ["ELASTIC_APM_ENABLED"] = "false"
environ["AWS_LAMBDA_FUNCTION_NAME"] = "ti-api-authorizer"
logger = Logger(level=logging.INFO)
@lru_cache(maxsize=1)
def get_allowed_ips() -> list[str]:
"""Get list of allowed IPs and subnets from environment variable."""
allowed_ips = environ.get("ALLOWED_IPS", "")
return [ip.strip() for ip in allowed_ips.split(",") if ip.strip()]
def is_ip_allowed(ip: str, allowed_subnets: list[str]) -> bool:
"""Check if an IP address is within any of the allowed subnets."""
try:
ip_obj = ipaddress.ip_address(ip)
for subnet in allowed_subnets:
if ip_obj in ipaddress.ip_network(subnet):
return True
return False
except ValueError as e:
logger.error(f"Invalid IP address or subnet: {e}")
return False
def generate_policy(principal_id: str, effect: str, resource: str) -> APIGatewayAuthorizerResponse:
"""Generate an IAM policy document."""
return {
"principalId": principal_id,
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource}],
},
}
@logger.inject_lambda_context()
@event_source(data_class=APIGatewayAuthorizerRequestEvent)
def authorizer(
event: APIGatewayAuthorizerRequestEvent, _context: LambdaContext
) -> APIGatewayAuthorizerResponse:
"""Lambda synchronous handler for API Gateway authorizer.
Checks if the request IP is in the allowed IPs list.
"""
logger.info(f"Event: {event} Context: {_context}")
# Get source IP from the event
source_ip = event.request_context.get("identity", {}).get("sourceIp")
if not source_ip:
logger.error("No source IP found in the request")
return generate_policy("unauthorized", "Deny", event.method_arn)
allowed_ips = get_allowed_ips()
if not allowed_ips:
logger.warning("No allowed IPs found in the environment")
return generate_policy("unauthorized", "Deny", event.method_arn)
# Check if IP is allowed
if is_ip_allowed(source_ip, allowed_ips):
logger.info(f"IP {source_ip} is authorized")
return generate_policy(source_ip, "Allow", event.method_arn)
logger.warning(f"IP {source_ip} is not authorized, allowed subnets: {allowed_ips}")
return generate_policy("unauthorized", "Deny", event.method_arn)
@brett-fitz In the original description you mentioned using capture_serverless decorator but I don't see it here, where do you have it? Have you tried removing one powertools decorator at time to see if that makes a difference?
@xrmx sorry I removed that out because of this issue 😅. Here is the missing code:
I have another file that contains a handler / decorator that all my lambda functions leverage:
import asyncio
from collections.abc import Coroutine
import functools
import logging
from typing import Callable, TypeVar
from aws_lambda_powertools.utilities.typing import LambdaContext
from elasticapm import capture_serverless
__all__ = [
"handler",
]
EventT = TypeVar("EventT")
ReturnT = TypeVar("ReturnT")
@capture_serverless()
def handler(
fn: Callable[[EventT, LambdaContext], Coroutine[None, None, ReturnT]]
) -> Callable[[EventT, LambdaContext], ReturnT]:
"""Decorator to allow async Lambda handlers.
Wraps an async function so it can be used as a Lambda entrypoint.
Calls `asyncio.run` on the coroutine.
Note: `asyncio.run` cannot be called from a running event loop.
This is safe in AWS Lambda, but may raise an error in other environments.
"""
@functools.wraps(fn)
def wrapper(event: EventT, context: LambdaContext) -> ReturnT:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s : %(levelname)s : %(name)s : %(funcName)s : %(message)s',
datefmt='%Y-%m-%d %I:%M:%S %p',
force=True
)
if asyncio.iscoroutinefunction(fn):
return asyncio.run(fn(event, context))
return fn(event, context)
return wrapper
In the previous code, when I want to instrument with elasticapm, I would add the decorator to the authorizer function:
@handler
@logger.inject_lambda_context()
@event_source(data_class=APIGatewayAuthorizerRequestEvent)
def authorizer(
event: APIGatewayAuthorizerRequestEvent, _context: LambdaContext
) -> APIGatewayAuthorizerResponse:
"""Lambda synchronous handler for API Gateway authorizer.
Checks if the request IP is in the allowed IPs list.
"""
...
Note: ElasticAPM works on all my other lambdas through this implementation, only issue is the authorizer lambda.
@brett-fitz From my testing there is nothing special about the lambda being used as an authorizer, so my guess is there should be something else poking with the context parameter. What's the difference in these decorators between the other lambdas?
Hey @xrmx I'm going to re-enable this today and test to see if I can catch the exception again and the context around it. There's no difference in the exception procedure between the lambdas except for the input to the entrypoint.
@xrmx After testing the capture_serverless decorator at the entrypoint of the authorizer instead of my general decorator/entrypoint, handler, it is now working. There's gotta be something wrong with my decorator. I am going to close this issue out, my apologies.
@brett-fitz np, thanks for checking!