Skip to content

Commit 3f4eddc

Browse files
authored
AWS specific instrumentations (#1054)
* add localstack setup for enhanced botocore tests * add specific instrumentation data for S3 note: this will break current botocore tests. These will get fixed at a later point. * use correct localstack URL * added support for SNS and DynamoDB * added some more testing for SNS and fixed an issue with default handlers * updated docs * add DB context fields to dynamodb * add WAIT_FOR config for localstack * don't use bridged network for localstack
1 parent eb067c9 commit 3f4eddc

File tree

6 files changed

+283
-70
lines changed

6 files changed

+283
-70
lines changed

.editorconfig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ trim_trailing_whitespace = false
1414

1515
[Makefile]
1616
indent_style = tab
17-
indent_size = 4
1817

1918
[Jenkinsfile]
2019
indent_size = 2
@@ -24,3 +23,6 @@ indent_size = 2
2423

2524
[*.feature]
2625
indent_size = 2
26+
27+
[*.yml]
28+
indent_size = 2

docs/supported-technologies.asciidoc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,13 +516,32 @@ Instrumented methods:
516516

517517
* `botocore.client.BaseClient._make_api_call`
518518

519-
Collected trace data:
519+
Collected trace data for all services:
520520

521521
* AWS region (e.g. `eu-central-1`)
522522
* AWS service name (e.g. `s3`)
523523
* operation name (e.g. `ListBuckets`)
524524

525+
Additionally, some services collect more specific data
526+
527+
[float]
528+
[[automatic-instrumentation-s3]]
529+
===== S3
530+
531+
* Bucket name
532+
533+
[float]
534+
[[automatic-instrumentation-dynamodb]]
535+
===== DynamoDB
536+
537+
* Table name
538+
539+
540+
[float]
541+
[[automatic-instrumentation-sns]]
542+
===== SNS
525543

544+
* Topic name
526545

527546
[float]
528547
[[automatic-instrumentation-template-engines]]

elasticapm/instrumentation/packages/botocore.py

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030

31+
from collections import namedtuple
32+
3133
from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
3234
from elasticapm.traces import capture_span
3335
from elasticapm.utils.compat import urlparse
3436

37+
HandlerInfo = namedtuple("HandlerInfo", ("signature", "span_type", "span_subtype", "span_action", "context"))
38+
3539

3640
class BotocoreInstrumentation(AbstractInstrumentedModule):
3741
name = "botocore"
@@ -44,14 +48,109 @@ def call(self, module, method, wrapped, instance, args, kwargs):
4448
else:
4549
operation_name = args[0]
4650

47-
target_endpoint = instance._endpoint.host
48-
parsed_url = urlparse.urlparse(target_endpoint)
49-
if "." in parsed_url.hostname:
50-
service = parsed_url.hostname.split(".", 2)[0]
51-
else:
52-
service = parsed_url.hostname
51+
service = instance._service_model.service_id
5352

54-
signature = "{}:{}".format(service, operation_name)
53+
parsed_url = urlparse.urlparse(instance._endpoint.host)
54+
context = {
55+
"destination": {
56+
"address": parsed_url.hostname,
57+
"port": parsed_url.port,
58+
"cloud": {"region": instance.meta.region_name},
59+
}
60+
}
5561

56-
with capture_span(signature, "aws", leaf=True, span_subtype=service, span_action=operation_name):
62+
handler_info = None
63+
handler = handlers.get(service, False)
64+
if handler:
65+
handler_info = handler(operation_name, service, instance, args, kwargs, context)
66+
if not handler_info:
67+
handler_info = handle_default(operation_name, service, instance, args, kwargs, context)
68+
69+
with capture_span(
70+
handler_info.signature,
71+
span_type=handler_info.span_type,
72+
leaf=True,
73+
span_subtype=handler_info.span_subtype,
74+
span_action=handler_info.span_action,
75+
extra=handler_info.context,
76+
):
5777
return wrapped(*args, **kwargs)
78+
79+
80+
def handle_s3(operation_name, service, instance, args, kwargs, context):
81+
span_type = "storage"
82+
span_subtype = "s3"
83+
span_action = operation_name
84+
if len(args) > 1 and "Bucket" in args[1]:
85+
bucket = args[1]["Bucket"]
86+
else:
87+
# TODO handle Access Points
88+
bucket = ""
89+
signature = f"S3 {operation_name} {bucket}"
90+
91+
context["destination"]["name"] = span_subtype
92+
context["destination"]["resource"] = bucket
93+
context["destination"]["service"] = {"type": span_type}
94+
95+
return HandlerInfo(signature, span_type, span_subtype, span_action, context)
96+
97+
98+
def handle_dynamodb(operation_name, service, instance, args, kwargs, context):
99+
span_type = "db"
100+
span_subtype = "dynamodb"
101+
span_action = "query"
102+
if len(args) > 1 and "TableName" in args[1]:
103+
table = args[1]["TableName"]
104+
else:
105+
table = ""
106+
signature = f"DynamoDB {operation_name} {table}".rstrip()
107+
108+
context["db"] = {"type": "dynamodb", "instance": instance.meta.region_name}
109+
if operation_name == "Query" and len(args) > 1 and "KeyConditionExpression" in args[1]:
110+
context["db"]["statement"] = args[1]["KeyConditionExpression"]
111+
112+
context["destination"]["name"] = span_subtype
113+
context["destination"]["resource"] = table
114+
context["destination"]["service"] = {"type": span_type}
115+
return HandlerInfo(signature, span_type, span_subtype, span_action, context)
116+
117+
118+
def handle_sns(operation_name, service, instance, args, kwargs, context):
119+
if operation_name != "Publish":
120+
# only "publish" is handled specifically, other endpoints get the default treatment
121+
return False
122+
span_type = "messaging"
123+
span_subtype = "sns"
124+
span_action = "send"
125+
topic_name = ""
126+
if len(args) > 1:
127+
if "Name" in args[1]:
128+
topic_name = args[1]["Name"]
129+
if "TopicArn" in args[1]:
130+
topic_name = args[1]["TopicArn"].rsplit(":", maxsplit=1)[-1]
131+
signature = f"SNS {operation_name} {topic_name}".rstrip()
132+
context["destination"]["name"] = span_subtype
133+
context["destination"]["resource"] = f"{span_subtype}/{topic_name}" if topic_name else span_subtype
134+
context["destination"]["type"] = span_type
135+
return HandlerInfo(signature, span_type, span_subtype, span_action, context)
136+
137+
138+
def handle_sqs(operation_name, service, instance, args, kwargs, destination):
139+
pass
140+
141+
142+
def handle_default(operation_name, service, instance, args, kwargs, destination):
143+
span_type = "aws"
144+
span_subtype = service.lower()
145+
span_action = operation_name
146+
147+
signature = f"{service}:{operation_name}"
148+
return HandlerInfo(signature, span_type, span_subtype, span_action, destination)
149+
150+
151+
handlers = {
152+
"S3": handle_s3,
153+
"DynamoDB": handle_dynamodb,
154+
"SNS": handle_sns,
155+
"default": handle_default,
156+
}

tests/docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,23 @@ services:
122122
volumes:
123123
- mysqldata:/var/lib/mysql
124124

125+
localstack:
126+
image: localstack/localstack
127+
ports:
128+
- "4566:4566"
129+
- "4571:4571"
130+
environment:
131+
- HOSTNAME=localstack
132+
- HOSTNAME_EXTERNAL=localstack
133+
- SERVICES=sns,sqs,s3,dynamodb,ec2
134+
- DEBUG=${DEBUG- }
135+
- DOCKER_HOST=unix:///var/run/docker.sock
136+
- HOST_TMP_FOLDER=${TMPDIR}
137+
- START_WEB=0
138+
volumes:
139+
- "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
140+
- "/var/run/docker.sock:/var/run/docker.sock"
141+
125142
run_tests:
126143
image: apm-agent-python:${PYTHON_VERSION}
127144
environment:

0 commit comments

Comments
 (0)