Skip to content

Commit f22002e

Browse files
committed
Flush metrics to log
1 parent c95df67 commit f22002e

File tree

11 files changed

+137
-28
lines changed

11 files changed

+137
-28
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 100

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ COPY requirements.txt requirements.txt
1212
RUN pip install -r requirements.txt -t ./python/lib/$runtime/site-packages
1313

1414
# Install datadog_lambda
15-
COPY datadog_lambda ./python/lib/$runtime/site-packages
15+
COPY datadog_lambda ./python/lib/$runtime/site-packages/datadog_lambda
1616

1717
# Remove *.pyc files
1818
RUN find ./python/lib/$runtime/site-packages -name \*.pyc -delete

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ arn:aws:lambda:<AWS_REGION>:464622532012:layer:Datadog-Python37:<VERSION>
1212

1313
Replace `<AWS_REGION>` with the region where your Lambda function lives, and `<VERSION>` with the desired (or the latest) version that can be found from [CHANGELOG](CHANGELOG.md).
1414

15-
The following Datadog environment variables must be defined via [AWS CLI](https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html) or [Serverless Framework](https://serverless-stack.com/chapters/serverless-environment-variables.html):
15+
The Datadog API must be defined as an environment variable via [AWS CLI](https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html) or [Serverless Framework](https://serverless-stack.com/chapters/serverless-environment-variables.html):
1616

17-
* DATADOG_API_KEY
18-
* DATADOG_APP_KEY
17+
* DD_API_KEY or DD_KMS_API_KEY (if encrypted by KMS)
18+
19+
Set the following Datadog environment variable to `datadoghq.eu` to send your data to the Datadog EU site.
20+
21+
* DD_SITE
22+
23+
If your Lambda function powers a performance-critical task (e.g., a consumer-facing API). You can avoid the added latencies of metric-submitting API calls, by setting the following Datadog environment variable to `True`. Custom metrics will be submitted asynchronously through CloudWatch Logs and [the Datadog log forwarder](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring).
24+
25+
* DATADOG_FLUSH_TO_LOG
1926

2027
### The Serverless Framework
2128

@@ -40,7 +47,6 @@ functions:
4047
- arn:aws:lambda:us-east-1:464622532012:layer:Datadog-Python37:1
4148
environment:
4249
DATADOG_API_KEY: xxx
43-
DATADOG_APP_KEY: yyy
4450
```
4551
4652

datadog_lambda/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# This product includes software developed at Datadog (https://www.datadoghq.com/).
44
# Copyright 2019 Datadog, Inc.
55

6+
67
# Datadog trace sampling priority
78
class SamplingPriority(object):
89
USER_REJECT = -1

datadog_lambda/metric.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
import os
77
import sys
8+
import json
9+
import time
10+
import base64
811

12+
import boto3
913
from datadog import api
1014
from datadog.threadstats import ThreadStats
1115
from datadog_lambda import __version__
@@ -22,26 +26,42 @@ def _format_dd_lambda_layer_tag():
2226
return "dd_lambda_layer:datadog-{}_{}".format(runtime, __version__)
2327

2428

25-
def _tag_dd_lambda_layer(args, kwargs):
29+
def _tag_dd_lambda_layer(tags):
2630
"""
2731
Used by lambda_metric to insert the dd_lambda_layer tag
2832
"""
2933
dd_lambda_layer_tag = _format_dd_lambda_layer_tag()
30-
if 'tags' in kwargs:
31-
kwargs['tags'].append(dd_lambda_layer_tag)
32-
elif len(args) >= 4:
33-
args[3].append(dd_lambda_layer_tag)
34+
if tags:
35+
return tags + [dd_lambda_layer_tag]
3436
else:
35-
kwargs['tags'] = [dd_lambda_layer_tag]
37+
return [dd_lambda_layer_tag]
3638

3739

38-
def lambda_metric(*args, **kwargs):
40+
def lambda_metric(metric_name, value, timestamp=None, tags=None):
3941
"""
4042
Submit a data point to Datadog distribution metrics.
4143
https://docs.datadoghq.com/graphing/metrics/distributions/
44+
45+
When DATADOG_LOG_FORWARDER is True, write metric to log, and
46+
wait for the Datadog Log Forwarder Lambda function to submit
47+
the metrics asynchronously.
48+
49+
Otherwise, the metrics will be submitted to the Datadog API
50+
periodically and at the end of the function execution in a
51+
background thread.
4252
"""
43-
_tag_dd_lambda_layer(args, kwargs)
44-
lambda_stats.distribution(*args, **kwargs)
53+
tags = _tag_dd_lambda_layer(tags)
54+
if os.environ.get('DATADOG_FLUSH_TO_LOG') == 'True':
55+
print(json.dumps({
56+
'metric_name': metric_name,
57+
'value': value,
58+
'timestamp': timestamp or int(time.time()),
59+
'tags': tags
60+
}))
61+
else:
62+
lambda_stats.distribution(
63+
metric_name, value, timestamp=timestamp, tags=tags
64+
)
4565

4666

4767
def init_api_client():
@@ -57,8 +77,20 @@ def init_api_client():
5777
By making the initial request async, we spare a lot of execution
5878
time in the lambdas.
5979
"""
60-
api._api_key = os.environ.get('DATADOG_API_KEY')
61-
api._api_host = os.environ.get('DATADOG_HOST', 'https://api.datadoghq.com')
80+
if "DD_KMS_API_KEY" in os.environ:
81+
DD_KMS_API_KEY = os.environ["DD_KMS_API_KEY"]
82+
api._api_key = boto3.client("kms").decrypt(
83+
CiphertextBlob=base64.b64decode(DD_KMS_API_KEY)
84+
)["Plaintext"]
85+
else:
86+
api._api_key = os.environ.get(
87+
'DATADOG_API_KEY',
88+
os.environ.get('DD_API_KEY'),
89+
)
90+
api._api_host = os.environ.get(
91+
'DATADOG_HOST',
92+
'https://api.' + os.environ.get('DD_SITE', 'datadoghq.com')
93+
)
6294
try:
6395
api.api_client.APIClient.submit('GET', 'validate')
6496
except Exception:

datadog_lambda/tracing.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ def get_dd_trace_context():
5555
Return the Datadog trace context to be propogated on the outgoing requests.
5656
5757
If the Lambda function is invoked by a Datadog-traced service, a Datadog
58-
trace
59-
context may already exist, and it should be used. Otherwise, use the
58+
trace context may already exist, and it should be used. Otherwise, use the
6059
current X-Ray trace entity.
6160
6261
Most of widely-used HTTP clients are patched to inject the context

datadog_lambda/wrapper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# This product includes software developed at Datadog (https://www.datadoghq.com/).
44
# Copyright 2019 Datadog, Inc.
55

6+
import os
67
import traceback
78
from threading import Thread
89

@@ -33,12 +34,14 @@ class _LambdaDecorator(object):
3334

3435
def __init__(self, func):
3536
self.func = func
37+
self.flush_to_log = os.environ.get('DATADOG_FLUSH_TO_LOG') == 'True'
3638

3739
def _before(self, event, context):
3840
try:
3941
# Async initialization of the TLS connection with Datadog API,
4042
# and reduces the overhead to the final metric flush at the end.
41-
Thread(target=init_api_client).start()
43+
if not self.flush_to_log:
44+
Thread(target=init_api_client).start()
4245

4346
# Extract Datadog trace context from incoming requests
4447
extract_dd_trace_context(event)
@@ -50,7 +53,8 @@ def _before(self, event, context):
5053

5154
def _after(self, event, context):
5255
try:
53-
lambda_stats.flush(float("inf"))
56+
if not self.flush_to_log:
57+
lambda_stats.flush(float("inf"))
5458
except Exception:
5559
traceback.print_exc()
5660

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
aws-xray-sdk==2.4.2
22
datadog==0.28.0
3-
wrapt==1.11.1
3+
wrapt==1.11.1
4+
setuptools==40.8.0
5+
boto3==1.9.160

tests/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ FROM python:$python_version
33

44
ENV PYTHONDONTWRITEBYTECODE True
55

6+
COPY .flake8 .flake8
67
COPY requirements.txt requirements.txt
78
COPY requirements-dev.txt requirements-dev.txt
89
RUN pip install -r requirements.txt

tests/test_metric.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import os
12
import unittest
23
try:
34
from unittest.mock import patch, call
45
except ImportError:
56
from mock import patch, call
67

78
from datadog_lambda.metric import (
9+
init_api_client,
810
lambda_metric,
911
_format_dd_lambda_layer_tag,
1012
)
@@ -18,12 +20,52 @@ def setUp(self):
1820
self.addCleanup(patcher.stop)
1921

2022
def test_lambda_metric_tagged_with_dd_lambda_layer(self):
21-
lambda_metric('test.metric', 1)
22-
lambda_metric('test.metric', 1, 123, ['tag1:test'])
23-
lambda_metric('test.metric', 1, tags=['tag1:test'])
23+
lambda_metric('test', 1)
24+
lambda_metric('test', 1, 123, [])
25+
lambda_metric('test', 1, tags=['tag1:test'])
2426
expected_tag = _format_dd_lambda_layer_tag()
2527
self.mock_metric_lambda_stats.distribution.assert_has_calls([
26-
call('test.metric', 1, tags=[expected_tag]),
27-
call('test.metric', 1, 123, ['tag1:test', expected_tag]),
28-
call('test.metric', 1, tags=['tag1:test', expected_tag]),
28+
call('test', 1, timestamp=None, tags=[expected_tag]),
29+
call('test', 1, timestamp=123, tags=[expected_tag]),
30+
call('test', 1, timestamp=None, tags=['tag1:test', expected_tag]),
2931
])
32+
33+
def test_lambda_metric_flush_to_log(self):
34+
os.environ["DATADOG_FLUSH_TO_LOG"] = 'True'
35+
36+
lambda_metric('test', 1)
37+
self.mock_metric_lambda_stats.distribution.assert_not_called()
38+
39+
del os.environ["DATADOG_FLUSH_TO_LOG"]
40+
41+
42+
class TestAPIClient(unittest.TestCase):
43+
44+
def setUp(self):
45+
patcher = patch('datadog_lambda.metric.api')
46+
self.mock_api = patcher.start()
47+
self.addCleanup(patcher.stop)
48+
49+
def test_init_api_client(self):
50+
os.environ["DD_API_KEY"] = '123456'
51+
os.environ["DD_SITE"] = 'dummy-site'
52+
53+
init_api_client()
54+
55+
self.assertEqual(self.mock_api._api_key, '123456')
56+
self.assertEqual(self.mock_api._api_host, 'https://api.dummy-site')
57+
self.mock_api.api_client.APIClient.submit.assert_called()
58+
59+
os.environ["DATADOG_API_KEY"] = '654321'
60+
os.environ["DATADOG_HOST"] = 'dummy-host'
61+
62+
init_api_client()
63+
64+
self.assertEqual(self.mock_api._api_key, '654321')
65+
self.assertEqual(self.mock_api._api_host, 'dummy-host')
66+
self.mock_api.api_client.APIClient.submit.assert_called()
67+
68+
del os.environ["DATADOG_API_KEY"]
69+
del os.environ["DATADOG_HOST"]
70+
del os.environ["DD_API_KEY"]
71+
del os.environ["DD_SITE"]

tests/test_wrapper.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import unittest
23
try:
34
from unittest.mock import patch, call, ANY
@@ -35,9 +36,28 @@ def lambda_handler(event, context):
3536
lambda_event = {}
3637
lambda_context = {}
3738
lambda_handler(lambda_event, lambda_context)
39+
3840
self.mock_metric_lambda_stats.distribution.assert_has_calls([
39-
call('test.metric', 100, tags=ANY)
41+
call('test.metric', 100, timestamp=None, tags=ANY)
4042
])
4143
self.mock_wrapper_lambda_stats.flush.assert_called()
4244
self.mock_extract_dd_trace_context.assert_called_with(lambda_event)
4345
self.mock_patch_all.assert_called()
46+
47+
def test_datadog_lambda_wrapper_flush_to_log(self):
48+
os.environ["DATADOG_FLUSH_TO_LOG"] = 'True'
49+
50+
@datadog_lambda_wrapper
51+
def lambda_handler(event, context):
52+
lambda_metric("test.metric", 100)
53+
54+
lambda_event = {}
55+
lambda_context = {}
56+
lambda_handler(lambda_event, lambda_context)
57+
58+
self.mock_metric_lambda_stats.distribution.assert_not_called()
59+
self.mock_wrapper_lambda_stats.flush.assert_not_called()
60+
self.mock_extract_dd_trace_context.assert_called_with(lambda_event)
61+
self.mock_patch_all.assert_called()
62+
63+
del os.environ["DATADOG_FLUSH_TO_LOG"]

0 commit comments

Comments
 (0)