3535import os
3636import platform
3737import time
38+ import warnings
3839from typing import Optional , TypeVar
3940from urllib .parse import urlencode
4041
4142import elasticapm
42- from elasticapm .base import Client
43+ from elasticapm .base import Client , get_client
4344from elasticapm .conf import constants
4445from elasticapm .utils import encoding , get_name_from_func , nested_key
4546from elasticapm .utils .disttracing import TraceParent
5051logger = get_logger ("elasticapm.serverless" )
5152
5253COLD_START = True
54+ INSTRUMENTED = False
5355
54- _AnnotatedFunctionT = TypeVar ("_AnnotatedFunctionT " )
56+ _AWSLambdaContextT = TypeVar ("_AWSLambdaContextT " )
5557
5658
57- class capture_serverless (object ) :
59+ def capture_serverless (func : Optional [ callable ] = None , ** kwargs ) -> callable :
5860 """
59- Context manager and decorator designed for instrumenting serverless
60- functions.
61-
62- Begins and ends a single transaction, waiting for the transport to flush
63- before returning from the wrapped function.
61+ Decorator for instrumenting AWS Lambda functions.
6462
6563 Example usage:
6664
6765 from elasticapm import capture_serverless
6866
69- @capture_serverless()
67+ @capture_serverless
7068 def handler(event, context):
7169 return {"statusCode": r.status_code, "body": "Success!"}
7270 """
71+ if not func :
72+ # This allows for `@capture_serverless()` in addition to
73+ # `@capture_serverless` decorator usage
74+ return functools .partial (capture_serverless , ** kwargs )
75+
76+ if kwargs :
77+ warnings .warn (
78+ PendingDeprecationWarning (
79+ "Passing keyword arguments to capture_serverless will be deprecated in the next major release."
80+ )
81+ )
7382
74- def __init__ (self , name : Optional [str ] = None , elasticapm_client : Optional [Client ] = None , ** kwargs ) -> None :
75- self .name = name
76- self .event = {}
77- self .context = {}
78- self .response = None
79- self .instrumented = False
80- self .client = elasticapm_client # elasticapm_client is intended for testing only
81-
82- # Disable all background threads except for transport
83- kwargs ["metrics_interval" ] = "0ms"
84- kwargs ["breakdown_metrics" ] = False
85- if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os .environ :
86- # Allow users to override metrics sets
87- kwargs ["metrics_sets" ] = []
88- kwargs ["central_config" ] = False
89- kwargs ["cloud_provider" ] = "none"
90- kwargs ["framework_name" ] = "AWS Lambda"
91- if "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os .environ :
92- kwargs ["service_name" ] = os .environ ["AWS_LAMBDA_FUNCTION_NAME" ]
93- if "service_version" not in kwargs and "ELASTIC_APM_SERVICE_VERSION" not in os .environ :
94- kwargs ["service_version" ] = os .environ .get ("AWS_LAMBDA_FUNCTION_VERSION" )
95-
96- self .client_kwargs = kwargs
97-
98- def __call__ (self , func : _AnnotatedFunctionT ) -> _AnnotatedFunctionT :
99- self .name = self .name or get_name_from_func (func )
100-
101- @functools .wraps (func )
102- def decorated (* args , ** kwds ):
103- if len (args ) == 2 :
104- # Saving these for request context later
105- self .event , self .context = args
106- else :
107- self .event , self .context = {}, {}
108- # We delay client creation until the function is called, so that
109- # multiple @capture_serverless instances in the same file don't create
110- # multiple clients
111- if not self .client :
112- # Don't use get_client() as we may have a config mismatch due to **kwargs
113- self .client = Client (** self .client_kwargs )
114- if (
115- not self .instrumented
116- and not self .client .config .debug
117- and self .client .config .instrument
118- and self .client .config .enabled
119- ):
120- elasticapm .instrument ()
121- self .instrumented = True
122-
123- if not self .client .config .debug and self .client .config .instrument and self .client .config .enabled :
124- with self :
125- self .response = func (* args , ** kwds )
126- return self .response
127- else :
128- return func (* args , ** kwds )
83+ name = kwargs .pop ("name" , None )
84+
85+ # Disable all background threads except for transport
86+ kwargs ["metrics_interval" ] = "0ms"
87+ kwargs ["breakdown_metrics" ] = False
88+ if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os .environ :
89+ # Allow users to override metrics sets
90+ kwargs ["metrics_sets" ] = []
91+ kwargs ["central_config" ] = False
92+ kwargs ["cloud_provider" ] = "none"
93+ kwargs ["framework_name" ] = "AWS Lambda"
94+ if "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os .environ :
95+ kwargs ["service_name" ] = os .environ ["AWS_LAMBDA_FUNCTION_NAME" ]
96+ if "service_version" not in kwargs and "ELASTIC_APM_SERVICE_VERSION" not in os .environ :
97+ kwargs ["service_version" ] = os .environ .get ("AWS_LAMBDA_FUNCTION_VERSION" )
98+
99+ global INSTRUMENTED
100+ client = get_client ()
101+ if not client :
102+ client = Client (** kwargs )
103+ if not client .config .debug and client .config .instrument and client .config .enabled and not INSTRUMENTED :
104+ elasticapm .instrument ()
105+ INSTRUMENTED = True
106+
107+ @functools .wraps (func )
108+ def decorated (* args , ** kwds ):
109+ if len (args ) == 2 :
110+ # Saving these for request context later
111+ event , context = args
112+ else :
113+ event , context = {}, {}
114+
115+ if not client .config .debug and client .config .instrument and client .config .enabled :
116+ with _lambda_transaction (func , name , client , event , context ) as sls :
117+ sls .response = func (* args , ** kwds )
118+ return sls .response
119+ else :
120+ return func (* args , ** kwds )
121+
122+ return decorated
123+
124+
125+ class _lambda_transaction (object ):
126+ """
127+ Context manager for creating transactions around AWS Lambda functions.
129128
130- return decorated
129+ Begins and ends a single transaction, waiting for the transport to flush
130+ before releasing the context.
131+ """
132+
133+ def __init__ (
134+ self , func : callable , name : Optional [str ], client : Client , event : dict , context : _AWSLambdaContextT
135+ ) -> None :
136+ self .func = func
137+ self .name = name or get_name_from_func (func )
138+ self .event = event
139+ self .context = context
140+ self .response = None
141+ self .client = client
131142
132143 def __enter__ (self ):
133144 """
@@ -152,7 +163,7 @@ def __enter__(self):
152163 if self .httpmethod : # http request
153164 if nested_key (self .event , "requestContext" , "elb" ):
154165 self .source = "elb"
155- resource = nested_key ( self . event , "path" )
166+ resource = "unknown route"
156167 elif nested_key (self .event , "requestContext" , "httpMethod" ):
157168 self .source = "api"
158169 # API v1
@@ -193,7 +204,7 @@ def __enter__(self):
193204 else :
194205 links = []
195206
196- self .transaction = self . client .begin_transaction (transaction_type , trace_parent = trace_parent , links = links )
207+ self .client .begin_transaction (transaction_type , trace_parent = trace_parent , links = links )
197208 elasticapm .set_transaction_name (transaction_name , override = False )
198209 if self .source in SERVERLESS_HTTP_REQUEST :
199210 elasticapm .set_context (
@@ -205,6 +216,7 @@ def __enter__(self):
205216 "request" ,
206217 )
207218 self .set_metadata_and_context (cold_start )
219+ return self
208220
209221 def __exit__ (self , exc_type , exc_val , exc_tb ):
210222 """
@@ -219,9 +231,12 @@ def __exit__(self, exc_type, exc_val, exc_tb):
219231 try :
220232 result = "HTTP {}xx" .format (int (self .response ["statusCode" ]) // 100 )
221233 elasticapm .set_transaction_result (result , override = False )
234+ if result == "HTTP 5xx" :
235+ elasticapm .set_transaction_outcome (outcome = "failure" , override = False )
222236 except ValueError :
223237 logger .warning ("Lambda function's statusCode was not formed as an int. Assuming 5xx result." )
224238 elasticapm .set_transaction_result ("HTTP 5xx" , override = False )
239+ elasticapm .set_transaction_outcome (outcome = "failure" , override = False )
225240 if exc_val :
226241 self .client .capture_exception (exc_info = (exc_type , exc_val , exc_tb ), handled = False )
227242 if self .source in SERVERLESS_HTTP_REQUEST :
0 commit comments