Skip to content

Commit 16f1036

Browse files
committed
refactor: AWS Lambda instrumentation.
Signed-off-by: Paulo Vital <[email protected]>
1 parent b6133e5 commit 16f1036

File tree

5 files changed

+273
-196
lines changed

5 files changed

+273
-196
lines changed

src/instana/__init__.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import importlib
1414
import os
1515
import sys
16+
from typing import Tuple
1617

1718
from instana.collector.helpers.runtime import (
1819
is_autowrapt_instrumented,
@@ -55,7 +56,7 @@
5556
]
5657

5758

58-
def load(_):
59+
def load(_: object) -> None:
5960
"""
6061
Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP
6162
environment variable.
@@ -66,15 +67,15 @@ def load(_):
6667
return None
6768

6869

69-
def apply_gevent_monkey_patch():
70+
def apply_gevent_monkey_patch() -> None:
7071
from gevent import monkey
7172

7273
if os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS"):
7374

74-
def short_key(k):
75+
def short_key(k: str) -> str:
7576
return k[3:] if k.startswith("no-") else k
7677

77-
def key_to_bool(k):
78+
def key_to_bool(k: str) -> bool:
7879
return not k.startswith("no-")
7980

8081
import inspect
@@ -99,14 +100,16 @@ def key_to_bool(k):
99100
monkey.patch_all()
100101

101102

102-
def get_lambda_handler_or_default():
103+
def get_aws_lambda_handler() -> Tuple[str, str]:
103104
"""
104-
For instrumenting AWS Lambda, users specify their original lambda handler in the LAMBDA_HANDLER environment
105-
variable. This function searches for and parses that environment variable or returns the defaults.
106-
107-
The default handler value for AWS Lambda is 'lambda_function.lambda_handler' which
108-
equates to the function "lambda_handler in a file named "lambda_function.py" or in Python
109-
terms "from lambda_function import lambda_handler"
105+
For instrumenting AWS Lambda, users specify their original lambda handler
106+
in the LAMBDA_HANDLER environment variable. This function searches for and
107+
parses that environment variable or returns the defaults.
108+
109+
The default handler value for AWS Lambda is 'lambda_function.lambda_handler'
110+
which equates to the function "lambda_handler in a file named
111+
lambda_function.py" or in Python terms
112+
"from lambda_function import lambda_handler"
110113
"""
111114
handler_module = "lambda_function"
112115
handler_function = "lambda_handler"
@@ -118,20 +121,20 @@ def get_lambda_handler_or_default():
118121
parts = handler.split(".")
119122
handler_function = parts.pop().strip()
120123
handler_module = ".".join(parts).strip()
121-
except Exception:
122-
pass
124+
except Exception as exc:
125+
print(f"get_aws_lambda_handler error: {exc}")
123126

124127
return handler_module, handler_function
125128

126129

127-
def lambda_handler(event, context):
130+
def lambda_handler(event: str, context: str) -> None:
128131
"""
129132
Entry point for AWS Lambda monitoring.
130133
131134
This function will trigger the initialization of Instana monitoring and then call
132135
the original user specified lambda handler function.
133136
"""
134-
module_name, function_name = get_lambda_handler_or_default()
137+
module_name, function_name = get_aws_lambda_handler()
135138

136139
try:
137140
# Import the module specified in module_name
@@ -151,7 +154,7 @@ def lambda_handler(event, context):
151154
)
152155

153156

154-
def boot_agent():
157+
def boot_agent() -> None:
155158
"""Initialize the Instana agent and conditionally load auto-instrumentation."""
156159

157160
import instana.singletons # noqa: F401
@@ -189,8 +192,7 @@ def boot_agent():
189192
client, # noqa: F401
190193
server, # noqa: F401
191194
)
192-
193-
# from instana.instrumentation.aws import lambda_inst # noqa: F401
195+
from instana.instrumentation.aws import lambda_inst # noqa: F401
194196
from instana.instrumentation import celery # noqa: F401
195197
from instana.instrumentation.django import middleware # noqa: F401
196198
from instana.instrumentation.google.cloud import (
@@ -229,7 +231,7 @@ def boot_agent():
229231
apply_gevent_monkey_patch()
230232
# AutoProfile
231233
if "INSTANA_AUTOPROFILE" in os.environ:
232-
from .singletons import get_profiler
234+
from instana.singletons import get_profiler
233235

234236
profiler = get_profiler()
235237
if profiler:

src/instana/agent/aws_lambda.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
# (c) Copyright Instana Inc. 2020
33

44
"""
5-
The Instana agent (for AWS Lambda functions) that manages
5+
The Instana Agent for AWS Lambda functions that manages
66
monitoring state and reporting that data.
77
"""
8-
import time
9-
from ..log import logger
10-
from ..util import to_json
11-
from .base import BaseAgent
12-
from ..version import VERSION
13-
from ..collector.aws_lambda import AWSLambdaCollector
14-
from ..options import AWSLambdaOptions
8+
9+
from typing import Any, Dict
10+
from instana.agent.base import BaseAgent
11+
from instana.collector.aws_lambda import AWSLambdaCollector
12+
from instana.log import logger
13+
from instana.options import AWSLambdaOptions
14+
from instana.util import to_json
15+
from instana.version import VERSION
1516

1617

1718
class AWSLambdaAgent(BaseAgent):
18-
""" In-process agent for AWS Lambda """
19-
def __init__(self):
19+
"""In-process Agent for AWS Lambda"""
20+
21+
def __init__(self) -> None:
2022
super(AWSLambdaAgent, self).__init__()
2123

2224
self.collector = None
@@ -27,29 +29,33 @@ def __init__(self):
2729
# Update log level from what Options detected
2830
self.update_log_level()
2931

30-
logger.info("Stan is on the AWS Lambda scene. Starting Instana instrumentation version: %s", VERSION)
32+
logger.info(
33+
f"Stan is on the AWS Lambda scene. Starting Instana instrumentation version: {VERSION}",
34+
)
3135

3236
if self._validate_options():
3337
self._can_send = True
3438
self.collector = AWSLambdaCollector(self)
3539
self.collector.start()
3640
else:
37-
logger.warning("Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. "
38-
"We will not be able monitor this function.")
41+
logger.warning(
42+
"Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. "
43+
"We will not be able monitor this function."
44+
)
3945

40-
def can_send(self):
46+
def can_send(self) -> bool:
4147
"""
4248
Are we in a state where we can send data?
4349
@return: Boolean
4450
"""
4551
return self._can_send
4652

47-
def get_from_structure(self):
53+
def get_from_structure(self) -> Dict[str, Any]:
4854
"""
4955
Retrieves the From data that is reported alongside monitoring data.
5056
@return: dict()
5157
"""
52-
return {'hl': True, 'cp': 'aws', 'e': self.collector.get_fq_arn()}
58+
return {"hl": True, "cp": "aws", "e": self.collector.get_fq_arn()}
5359

5460
def report_data_payload(self, payload):
5561
"""
@@ -64,30 +70,38 @@ def report_data_payload(self, payload):
6470
self.report_headers["X-Instana-Host"] = self.collector.get_fq_arn()
6571
self.report_headers["X-Instana-Key"] = self.options.agent_key
6672

67-
response = self.client.post(self.__data_bundle_url(),
68-
data=to_json(payload),
69-
headers=self.report_headers,
70-
timeout=self.options.timeout,
71-
verify=self.options.ssl_verify,
72-
proxies=self.options.endpoint_proxy)
73+
response = self.client.post(
74+
self.__data_bundle_url(),
75+
data=to_json(payload),
76+
headers=self.report_headers,
77+
timeout=self.options.timeout,
78+
verify=self.options.ssl_verify,
79+
proxies=self.options.endpoint_proxy,
80+
)
7381

7482
if 200 <= response.status_code < 300:
75-
logger.debug("report_data_payload: Instana responded with status code %s", response.status_code)
83+
logger.debug(
84+
"report_data_payload: Instana responded with status code %s",
85+
response.status_code,
86+
)
7687
else:
77-
logger.info("report_data_payload: Instana responded with status code %s", response.status_code)
88+
logger.info(
89+
"report_data_payload: Instana responded with status code %s",
90+
response.status_code,
91+
)
7892
except Exception as exc:
7993
logger.debug("report_data_payload: connection error (%s)", type(exc))
8094

8195
return response
8296

83-
def _validate_options(self):
97+
def _validate_options(self) -> bool:
8498
"""
8599
Validate that the options used by this Agent are valid. e.g. can we report data?
86100
"""
87-
return self.options.endpoint_url is not None and self.options.agent_key is not None
101+
return self.options.endpoint_url and self.options.agent_key
88102

89-
def __data_bundle_url(self):
103+
def __data_bundle_url(self) -> str:
90104
"""
91105
URL for posting metrics to the host agent. Only valid when announced.
92106
"""
93-
return "%s/bundle" % self.options.endpoint_url
107+
return f"{self.options.endpoint_url}/bundle"

src/instana/instrumentation/aws/lambda_inst.py

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,66 +4,89 @@
44
"""
55
Instrumentation for AWS Lambda functions
66
"""
7+
78
import sys
9+
import traceback
10+
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
11+
812
import wrapt
9-
import opentracing.ext.tags as ext
13+
from opentelemetry.semconv.trace import SpanAttributes
1014

11-
from ...log import logger
12-
from ...singletons import env_is_aws_lambda
13-
from ... import get_lambda_handler_or_default
14-
from ...singletons import get_agent, get_tracer
15-
from .triggers import enrich_lambda_span, get_context
16-
import traceback
15+
from instana import get_aws_lambda_handler
16+
from instana.instrumentation.aws.triggers import enrich_lambda_span, get_context
17+
from instana.log import logger
18+
from instana.singletons import env_is_aws_lambda, get_agent, get_tracer
19+
20+
if TYPE_CHECKING:
21+
from instana.agent.aws_lambda import AWSLambdaAgent
1722

1823

19-
def lambda_handler_with_instana(wrapped, instance, args, kwargs):
24+
def lambda_handler_with_instana(
25+
wrapped: Callable[..., object],
26+
instance: object,
27+
args: Tuple[object, ...],
28+
kwargs: Dict[str, Any],
29+
) -> object:
2030
event = args[0]
21-
agent = get_agent()
31+
agent: "AWSLambdaAgent" = get_agent()
2232
tracer = get_tracer()
2333

2434
agent.collector.collect_snapshot(*args)
2535
incoming_ctx = get_context(tracer, event)
2636

2737
result = None
28-
with tracer.start_active_span("aws.lambda.entry", child_of=incoming_ctx) as scope:
29-
enrich_lambda_span(agent, scope.span, *args)
38+
with tracer.start_as_current_span(
39+
"aws.lambda.entry", span_context=incoming_ctx
40+
) as span:
41+
enrich_lambda_span(agent, span, *args)
3042
try:
3143
result = wrapped(*args, **kwargs)
3244

3345
if isinstance(result, dict):
34-
server_timing_value = "intid;desc=%s" % scope.span.context.trace_id
35-
if 'headers' in result:
36-
result['headers']['Server-Timing'] = server_timing_value
37-
elif 'multiValueHeaders' in result:
38-
result['multiValueHeaders']['Server-Timing'] = [server_timing_value]
39-
if 'statusCode' in result and result.get('statusCode'):
40-
status_code = int(result['statusCode'])
41-
scope.span.set_tag(ext.HTTP_STATUS_CODE, status_code)
46+
server_timing_value = f"intid;desc={span.context.trace_id}"
47+
if "headers" in result:
48+
result["headers"]["Server-Timing"] = server_timing_value
49+
elif "multiValueHeaders" in result:
50+
result["multiValueHeaders"]["Server-Timing"] = [server_timing_value]
51+
if "statusCode" in result and result.get("statusCode"):
52+
status_code = int(result["statusCode"])
53+
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
4254
if 500 <= status_code:
43-
scope.span.log_exception(f'HTTP status {status_code}')
55+
span.record_exception(f"HTTP status {status_code}")
4456
except Exception as exc:
45-
if scope.span:
57+
logger.debug(f"AWS Lambda lambda_handler_with_instana error: {exc}")
58+
if span:
4659
exc = traceback.format_exc()
47-
scope.span.log_exception(exc)
60+
span.record_exception(exc)
4861
raise
4962
finally:
50-
scope.close()
5163
agent.collector.shutdown()
5264

53-
agent.collector.shutdown()
65+
if agent.collector.started:
66+
agent.collector.shutdown()
67+
5468
return result
5569

5670

57-
if env_is_aws_lambda is True:
58-
handler_module, handler_function = get_lambda_handler_or_default()
71+
if env_is_aws_lambda:
72+
handler_module, handler_function = get_aws_lambda_handler()
5973

60-
if handler_module is not None and handler_function is not None:
74+
if handler_module and handler_function:
6175
try:
62-
logger.debug("Instrumenting AWS Lambda handler (%s.%s)" % (handler_module, handler_function))
63-
sys.path.insert(0, '/var/runtime')
64-
sys.path.insert(0, '/var/task')
65-
wrapt.wrap_function_wrapper(handler_module, handler_function, lambda_handler_with_instana)
76+
logger.debug(
77+
f"Instrumenting AWS Lambda handler ({handler_module}.{handler_function})"
78+
)
79+
sys.path.insert(0, "/var/runtime")
80+
sys.path.insert(0, "/var/task")
81+
wrapt.wrap_function_wrapper(
82+
handler_module, handler_function, lambda_handler_with_instana
83+
)
6684
except (ModuleNotFoundError, ImportError) as exc:
67-
logger.warning("Instana: Couldn't instrument AWS Lambda handler. Not monitoring.")
85+
logger.debug(f"AWS Lambda error: {exc}")
86+
logger.warning(
87+
"Instana: Couldn't instrument AWS Lambda handler. Not monitoring."
88+
)
6889
else:
69-
logger.warning("Instana: Couldn't determine AWS Lambda Handler. Not monitoring.")
90+
logger.warning(
91+
"Instana: Couldn't determine AWS Lambda Handler. Not monitoring."
92+
)

0 commit comments

Comments
 (0)