Skip to content

Commit da5ec5f

Browse files
authored
New AWS Lambda Support (instana#219)
* Refactor, reorganize and cleanup for AWS prep * AWS Lambda Agent, Layer, Span & handler method * Functional Lambda Tracing and Infra linking * Bug fixes for test suite issues * Bug fixes for test suite issues: second batch * Restructured tag collection * Update suds and grpcio tests. Unify spans in span.py * Remove debug remnants * Update/fix cassandra tests * Check if agent supports extra_headers before proceeding * Centralize all lambda instrumentation * New Lambda test suite * New and improved lambda handler and handler parsing * Fix trailing slash bug reported by Justyn * Refinement and additions for the test suite * Linter love * Allow get/set of agent & tracer for tests * Updated build script * Restore agent and tracer after each test; lint fixes * Enable script for all regions * Better script name * Trigger support & tests * Move Lambda inst into it's own package * Code documentation * Update import path * Keep 2.7 compatible: no type hints * Py 2.7 compatible decompression * Fix Python 3.5 compatibility
1 parent be01dbe commit da5ec5f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3000
-1895
lines changed

bin/lambda_build_publish_layer.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
import json
5+
import shutil
6+
import time
7+
import distutils.spawn
8+
from subprocess import call, check_output
9+
10+
# Disable aws CLI pagination
11+
os.environ["AWS_PAGER"] = ""
12+
13+
# Check requirements first
14+
for cmd in ["pip", "zip"]:
15+
if distutils.spawn.find_executable(cmd) is None:
16+
print("Can't find required tool: %s" % cmd)
17+
exit(1)
18+
19+
# Determine where this script is running from
20+
this_file_path = os.path.dirname(os.path.realpath(__file__))
21+
22+
# Change directory to the base of the Python sensor repository
23+
os.chdir(this_file_path + "/../")
24+
25+
cwd = os.getcwd()
26+
print("===> Working directory is: %s" % cwd)
27+
28+
# For development, respect or set PYTHONPATH to this repository
29+
local_env = os.environ.copy()
30+
if "PYTHONPATH" not in os.environ:
31+
local_env["PYTHONPATH"] = os.getcwd()
32+
33+
build_directory = os.getcwd() + '/build/lambda/python'
34+
35+
if os.path.isdir(build_directory):
36+
print("===> Cleaning build pre-existing directory: %s" % build_directory)
37+
shutil.rmtree(build_directory)
38+
39+
print("===> Creating new build directory: %s" % build_directory)
40+
os.makedirs(build_directory, exist_ok=True)
41+
42+
print("===> Installing Instana and dependencies into build directory")
43+
call(["pip", "install", "-q", "-U", "-t", os.getcwd() + '/build/lambda/python', "instana"], env=local_env)
44+
45+
print("===> Manually copying in local dev code")
46+
shutil.rmtree(build_directory + "/instana")
47+
shutil.copytree(os.getcwd() + '/instana', build_directory + "/instana")
48+
49+
print("===> Creating Lambda ZIP file")
50+
timestamp = time.strftime("%Y-%m-%d_%H:%M:%S")
51+
zip_filename = "instana-py-layer-%s.zip" % timestamp
52+
53+
os.chdir(os.getcwd() + "/build/lambda/")
54+
call(["zip", "-q", "-r", zip_filename, "./python", "-x", "*.pyc", "./python/pip*", "./python/setuptools*", "./python/wheel*"])
55+
56+
fq_zip_filename = os.getcwd() + '/%s' % zip_filename
57+
aws_zip_filename = "fileb://%s" % fq_zip_filename
58+
print("Zipfile should be at: ", fq_zip_filename)
59+
60+
regions = ['ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', 'ca-central-1',
61+
'eu-central-1', 'eu-north-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1',
62+
'us-east-2', 'us-west-1', 'us-west-2']
63+
64+
# regions = ['us-west-1']
65+
66+
# LAYER_NAME = "instana-py-test"
67+
LAYER_NAME = "instana-python"
68+
69+
published = dict()
70+
71+
for region in regions:
72+
print("===> Uploading layer to AWS %s " % region)
73+
response = check_output(["aws", "--region", region, "lambda", "publish-layer-version",
74+
"--description",
75+
"Provides Instana tracing and monitoring of AWS Lambda functions built with Python",
76+
"--license-info", "MIT", "--output", "json",
77+
"--layer-name", LAYER_NAME, "--zip-file", aws_zip_filename,
78+
"--compatible-runtimes", "python2.7", "python3.6", "python3.7", "python3.8"])
79+
80+
json_data = json.loads(response)
81+
version = json_data['Version']
82+
print("===> Uploaded version is %s" % version)
83+
84+
print("===> Making layer public...")
85+
response = check_output(["aws", "--region", region, "lambda", "add-layer-version-permission",
86+
"--layer-name", LAYER_NAME, "--version-number", str(version),
87+
"--statement-id", "public-permission-all-accounts",
88+
"--principal", "*",
89+
"--action", "lambda:GetLayerVersion",
90+
"--output", "text"])
91+
92+
published[region] = json_data['LayerVersionArn']
93+
94+
95+
print("===> Published list:")
96+
for key in published.keys():
97+
print("%s\t%s" % (key, published[key]))

docker-compose.yml

+27-7
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,37 @@ services:
4040
- ./bin:/nodejs-collector-bin
4141
# command: ["/nodejs-collector-bin/wait-for-it.sh", "-s", "-t", "120", "zookeeper:2181", "--", "start-kafka.sh"]
4242

43-
mysql:
44-
image: mysql:8.0.1
43+
cassandra:
44+
image: cassandra:3.11.5
45+
ports:
46+
- 9042:9042
47+
48+
49+
couchbase:
50+
image: couchbase
51+
ports:
52+
- 8091-8094:8091-8094
53+
- 11210:11210
54+
55+
mariadb:
56+
image: mariadb
4557
ports:
4658
- 3306:3306
4759
environment:
48-
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
60+
MYSQL_DATABASE: 'circle_test'
61+
MYSQL_USER: 'root'
62+
MYSQL_PASSWORD: ''
63+
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
4964
MYSQL_ROOT_PASSWORD: ''
50-
MYSQL_DATABASE: circle_test
51-
MYSQL_USER: root
52-
MYSQL_PASSWORD:
53-
MYSQL_ROOT_HOST: '0.0.0.0'
65+
MYSQL_ROOT_HOST: '%'
5466
volumes:
5567
- ./tests/config/database/mysql/conf.d:/etc/mysql/conf.d
5668

69+
mongodb:
70+
image: 'mongo:3.4.1'
71+
ports:
72+
- '27017:27017'
73+
5774
postgres:
5875
image: postgres:10.5
5976
ports:
@@ -68,3 +85,6 @@ services:
6885
ports:
6986
- 5671:5671
7087
- 5672:5672
88+
89+
#volumes:
90+
# mysql-data:

instana/__init__.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121

2222
import os
2323
import sys
24-
from threading import Timer
24+
import importlib
2525
import pkg_resources
26+
from threading import Timer
2627

2728
__author__ = 'Instana Inc.'
2829
__copyright__ = 'Copyright 2020 Instana Inc.'
@@ -45,6 +46,57 @@ def load(_):
4546
if "INSTANA_DEBUG" in os.environ:
4647
print("Instana: activated via AUTOWRAPT_BOOTSTRAP")
4748

49+
if "INSTANA_ENDPOINT_URL" in os.environ:
50+
print("load: detected lambda environment")
51+
52+
53+
def get_lambda_handler_or_default():
54+
"""
55+
For instrumenting AWS Lambda, users specify their original lambda handler in the LAMBDA_HANDLER environment
56+
variable. This function searches for and parses that environment variable or returns the defaults.
57+
58+
The default handler value for AWS Lambda is 'lambda_function.lambda_handler' which
59+
equates to the function "lambda_handler in a file named "lambda_function.py" or in Python
60+
terms "from lambda_function import lambda_handler"
61+
"""
62+
handler_module = "lambda_function"
63+
handler_function = "lambda_handler"
64+
65+
try:
66+
handler = os.environ.get("LAMBDA_HANDLER", False)
67+
68+
if handler:
69+
parts = handler.split(".")
70+
handler_function = parts.pop()
71+
handler_module = ".".join(parts)
72+
except:
73+
pass
74+
75+
return handler_module, handler_function
76+
77+
78+
def lambda_handler(event, context):
79+
"""
80+
Entry point for AWS Lambda monitoring.
81+
82+
This function will trigger the initialization of Instana monitoring and then call
83+
the original user specified lambda handler function.
84+
"""
85+
module_name, function_name = get_lambda_handler_or_default()
86+
87+
try:
88+
# Import the module specified in module_name
89+
handler_module = importlib.import_module(module_name)
90+
except ImportError:
91+
print("Couldn't determine and locate default module handler: %s.%s", module_name, function_name)
92+
else:
93+
# Now get the function and execute it
94+
if hasattr(handler_module, function_name):
95+
handler_function = getattr(handler_module, function_name)
96+
return handler_function(event, context)
97+
else:
98+
print("Couldn't determine and locate default function handler: %s.%s", module_name, function_name)
99+
48100

49101
def boot_agent():
50102
"""Initialize the Instana agent and conditionally load auto-instrumentation."""
@@ -56,6 +108,8 @@ def boot_agent():
56108
# Instrumentation
57109
if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ:
58110
# Import & initialize instrumentation
111+
from .instrumentation.aws import lambda_inst
112+
59113
if sys.version_info >= (3, 5, 3):
60114
from .instrumentation import asyncio
61115
from .instrumentation.aiohttp import client

0 commit comments

Comments
 (0)