Skip to content

Commit 9846c96

Browse files
authored
Merge pull request #4 from DataDog/tian.chu/flush-metric-to-log
Submit custom metrics via CloudWatch Logs
2 parents 4e9fbfe + 123fc69 commit 9846c96

File tree

14 files changed

+139
-50
lines changed

14 files changed

+139
-50
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

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
CHANGELOG
22
=========
3+
# v2 / 2019-06-10
4+
5+
* Support submitting metrics through CloudWatch Logs
6+
* Support submitting metrics to `datadoghq.eu`
7+
* Support KMS-encrypted DD API Key
8+
* Fix a few bugs
39

410
# v1 / 2019-05-06
511

612
* First release
713
* Support submitting distribution metrics from Lambda functions
8-
* Supports distributed tracing between serverful and serverless services
14+
* Support distributed tracing between serverful and serverless services

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/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1"
1+
__version__ = "2"

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: 44 additions & 26 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,44 +26,58 @@ 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/
42-
"""
43-
_tag_dd_lambda_layer(args, kwargs)
44-
lambda_stats.distribution(*args, **kwargs)
4544
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.
4648
47-
def init_api_client():
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.
4852
"""
49-
No-op GET to initialize the requests connection with DD's endpoints,
50-
to make the final flush faster.
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+
)
5165

52-
We keep alive the Requests session, this means that we can re-use
53-
the connection. The consequence is that the HTTP Handshake, which
54-
can take hundreds of ms, is now made at the beginning of a lambda
55-
instead of at the end.
5666

57-
By making the initial request async, we spare a lot of execution
58-
time in the lambdas.
59-
"""
60-
api._api_key = os.environ.get('DATADOG_API_KEY')
61-
api._api_host = os.environ.get('DATADOG_HOST', 'https://api.datadoghq.com')
62-
try:
63-
api.api_client.APIClient.submit('GET', 'validate')
64-
except Exception:
65-
pass
67+
# Decrypt code should run once and variables stored outside of the function
68+
# handler so that these are decrypted once per container
69+
DD_KMS_API_KEY = os.environ.get("DD_KMS_API_KEY")
70+
if DD_KMS_API_KEY:
71+
DD_KMS_API_KEY = boto3.client("kms").decrypt(
72+
CiphertextBlob=base64.b64decode(DD_KMS_API_KEY)
73+
)["Plaintext"]
74+
75+
# Set API Key and Host in the module, so they only set once per container
76+
api._api_key = os.environ.get(
77+
'DATADOG_API_KEY',
78+
os.environ.get('DD_API_KEY', DD_KMS_API_KEY),
79+
)
80+
api._api_host = os.environ.get(
81+
'DATADOG_HOST',
82+
'https://api.' + os.environ.get('DD_SITE', 'datadoghq.com')
83+
)

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: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
# This product includes software developed at Datadog (https://www.datadoghq.com/).
44
# Copyright 2019 Datadog, Inc.
55

6+
import os
67
import traceback
7-
from threading import Thread
88

9-
from datadog_lambda.metric import init_api_client, lambda_stats
9+
from datadog_lambda.metric import lambda_stats
1010
from datadog_lambda.tracing import extract_dd_trace_context
1111
from datadog_lambda.patch import patch_all
1212

@@ -33,13 +33,10 @@ class _LambdaDecorator(object):
3333

3434
def __init__(self, func):
3535
self.func = func
36+
self.flush_to_log = os.environ.get('DATADOG_FLUSH_TO_LOG') == 'True'
3637

3738
def _before(self, event, context):
3839
try:
39-
# Async initialization of the TLS connection with Datadog API,
40-
# and reduces the overhead to the final metric flush at the end.
41-
Thread(target=init_api_client).start()
42-
4340
# Extract Datadog trace context from incoming requests
4441
extract_dd_trace_context(event)
4542

@@ -50,7 +47,8 @@ def _before(self, event, context):
5047

5148
def _after(self, event, context):
5249
try:
53-
lambda_stats.flush(float("inf"))
50+
if not self.flush_to_log:
51+
lambda_stats.flush(float("inf"))
5452
except Exception:
5553
traceback.print_exc()
5654

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

setup.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from setuptools import setup
2+
from os import path
3+
from io import open
4+
5+
here = path.abspath(path.dirname(__file__))
6+
7+
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
8+
long_description = f.read()
9+
10+
setup(
11+
name='ddlambda',
12+
version='0.2.0',
13+
description='The Datadog AWS Lambda Layer',
14+
long_description=long_description,
15+
long_description_content_type='text/markdown',
16+
url='https://github.com/DataDog/datadog-lambda-layer-python',
17+
author='Datadog, Inc.',
18+
author_email='dev@datadoghq.com',
19+
classifiers=[
20+
'Programming Language :: Python :: 2.7',
21+
'Programming Language :: Python :: 3.6',
22+
'Programming Language :: Python :: 3.7',
23+
],
24+
keywords='datadog aws lambda layer',
25+
packages=['datadog_lambda'],
26+
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4',
27+
)

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: 15 additions & 6 deletions
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
@@ -18,12 +19,20 @@ def setUp(self):
1819
self.addCleanup(patcher.stop)
1920

2021
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'])
22+
lambda_metric('test', 1)
23+
lambda_metric('test', 1, 123, [])
24+
lambda_metric('test', 1, tags=['tag1:test'])
2425
expected_tag = _format_dd_lambda_layer_tag()
2526
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]),
27+
call('test', 1, timestamp=None, tags=[expected_tag]),
28+
call('test', 1, timestamp=123, tags=[expected_tag]),
29+
call('test', 1, timestamp=None, tags=['tag1:test', expected_tag]),
2930
])
31+
32+
def test_lambda_metric_flush_to_log(self):
33+
os.environ["DATADOG_FLUSH_TO_LOG"] = 'True'
34+
35+
lambda_metric('test', 1)
36+
self.mock_metric_lambda_stats.distribution.assert_not_called()
37+
38+
del os.environ["DATADOG_FLUSH_TO_LOG"]

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)