Skip to content

Commit 76e5cee

Browse files
committed
emit enhanced error metric and create span when an exception is raised outside of the handler function
1 parent f9aca11 commit 76e5cee

File tree

5 files changed

+198
-5
lines changed

5 files changed

+198
-5
lines changed

datadog_lambda/handler.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from importlib import import_module
88

99
import os
10+
import time
11+
12+
from datadog_lambda.tracing import emit_telemetry_on_exception_outside_of_handler
1013
from datadog_lambda.wrapper import datadog_lambda_wrapper
1114
from datadog_lambda.module_name import modify_module_name
1215

@@ -15,6 +18,27 @@ class HandlerError(Exception):
1518
pass
1619

1720

21+
class _ErrorOutsideHandlerDecorator(object):
22+
"""
23+
Decorator for when an exception occurs outside of the handler function.
24+
Emits telemetry and re-raises the exception.
25+
"""
26+
27+
def __init__(self, exception, modified_mod_name, start_time_ns):
28+
self.exception = exception
29+
self.modified_mod_name = modified_mod_name
30+
self.start_time_ns = start_time_ns
31+
32+
def __call__(self, event, context, **kwargs):
33+
emit_telemetry_on_exception_outside_of_handler(
34+
context,
35+
self.exception,
36+
self.modified_mod_name,
37+
self.start_time_ns,
38+
)
39+
raise self.exception
40+
41+
1842
path = os.environ.get("DD_LAMBDA_HANDLER", None)
1943
if path is None:
2044
raise HandlerError(
@@ -27,5 +51,11 @@ class HandlerError(Exception):
2751

2852
(mod_name, handler_name) = parts
2953
modified_mod_name = modify_module_name(mod_name)
30-
handler_module = import_module(modified_mod_name)
31-
handler = datadog_lambda_wrapper(getattr(handler_module, handler_name))
54+
55+
try:
56+
start_time_ns = time.time_ns()
57+
handler_module = import_module(modified_mod_name)
58+
handler_func = getattr(handler_module, handler_name)
59+
handler = datadog_lambda_wrapper(handler_func)
60+
except Exception as e:
61+
handler = _ErrorOutsideHandlerDecorator(e, modified_mod_name, start_time_ns)

datadog_lambda/metric.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def submit_enhanced_metric(metric_name, lambda_context):
100100
101101
Args:
102102
metric_name (str): metric name w/o enhanced prefix i.e. "invocations" or "errors"
103-
lambda_context (dict): Lambda context dict passed to the function by AWS
103+
lambda_context (object): Lambda context dict passed to the function by AWS
104104
"""
105105
if not enhanced_metrics_enabled:
106106
logger.debug(
@@ -118,7 +118,7 @@ def submit_invocations_metric(lambda_context):
118118
"""Increment aws.lambda.enhanced.invocations by 1, applying runtime, layer, and cold_start tags
119119
120120
Args:
121-
lambda_context (dict): Lambda context dict passed to the function by AWS
121+
lambda_context (object): Lambda context dict passed to the function by AWS
122122
"""
123123
submit_enhanced_metric("invocations", lambda_context)
124124

@@ -127,6 +127,6 @@ def submit_errors_metric(lambda_context):
127127
"""Increment aws.lambda.enhanced.errors by 1, applying runtime, layer, and cold_start tags
128128
129129
Args:
130-
lambda_context (dict): Lambda context dict passed to the function by AWS
130+
lambda_context (object): Lambda context dict passed to the function by AWS
131131
"""
132132
submit_enhanced_metric("errors", lambda_context)

datadog_lambda/tracing.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import os
88
import base64
9+
import traceback
910
import ujson as json
1011
from datetime import datetime, timezone
1112
from typing import Optional, Dict
@@ -1320,3 +1321,38 @@ def is_async(span: Span) -> bool:
13201321
e,
13211322
)
13221323
return False
1324+
1325+
1326+
def emit_telemetry_on_exception_outside_of_handler(
1327+
context, exception, resource_name, start_time_ns
1328+
):
1329+
"""
1330+
Emit an enhanced error metric and create a span for exceptions occuring outside of the handler
1331+
"""
1332+
submit_errors_metric(context)
1333+
1334+
span = tracer.trace(
1335+
"aws.lambda",
1336+
service="aws.lambda",
1337+
resource=resource_name,
1338+
span_type="serverless",
1339+
)
1340+
span.start_ns = start_time_ns
1341+
tags = {
1342+
"error.status": 500,
1343+
"error.type": type(exception).__name__,
1344+
"error.message": exception,
1345+
"error.stack": "".join(
1346+
traceback.format_exception(
1347+
type(exception), exception, exception.__traceback__
1348+
)
1349+
),
1350+
"resource_names": resource_name,
1351+
"resource.name": resource_name,
1352+
"operation_name": "aws.lambda",
1353+
"status": "error",
1354+
"request_id": context.aws_request_id,
1355+
}
1356+
span.set_tags(tags)
1357+
span.error = 1
1358+
span.finish()

tests/test_handler.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
import sys
3+
import unittest
4+
from unittest.mock import patch
5+
6+
from tests.utils import get_mock_context
7+
8+
9+
class TestHandler(unittest.TestCase):
10+
def tearDown(self):
11+
for mod in sys.modules.copy():
12+
if mod.startswith("datadog_lambda.handler"):
13+
del sys.modules[mod]
14+
15+
def test_dd_lambda_handler_env_var_none(self):
16+
with self.assertRaises(Exception) as context:
17+
import datadog_lambda.handler as handler
18+
19+
assert context.exception == handler.HandlerError(
20+
"DD_LAMBDA_HANDLER is not defined. Can't use prebuilt datadog handler"
21+
)
22+
23+
@patch.dict(os.environ, {"DD_LAMBDA_HANDLER": "malformed"}, clear=True)
24+
def test_dd_lambda_handler_env_var_malformed(self):
25+
with self.assertRaises(Exception) as context:
26+
import datadog_lambda.handler as handler
27+
28+
assert context.exception == handler.HandlerError(
29+
"Value malformed for DD_LAMBDA_HANDLER has invalid format."
30+
)
31+
32+
@patch.dict(os.environ, {"DD_LAMBDA_HANDLER": "nonsense.nonsense"}, clear=True)
33+
@patch("datadog_lambda.tracing.emit_telemetry_on_exception_outside_of_handler")
34+
@patch("time.time_ns", return_value=42)
35+
def test_exception_importing_module(self, mock_time, mock_emit_telemetry):
36+
with self.assertRaises(ModuleNotFoundError) as test_context:
37+
import datadog_lambda.handler
38+
39+
lambda_context = get_mock_context()
40+
datadog_lambda.handler.handler.__call__(None, lambda_context)
41+
mock_emit_telemetry.assert_called_once_with(
42+
lambda_context, test_context.exception, "nonsense", 42
43+
)
44+
45+
@patch.dict(os.environ, {"DD_LAMBDA_HANDLER": "nonsense.nonsense"}, clear=True)
46+
@patch("importlib.import_module", return_value=None)
47+
@patch("datadog_lambda.tracing.emit_telemetry_on_exception_outside_of_handler")
48+
@patch("time.time_ns", return_value=42)
49+
def test_exception_getting_handler_func(
50+
self, mock_time, mock_emit_telemetry, mock_import
51+
):
52+
with self.assertRaises(AttributeError) as test_context:
53+
import datadog_lambda.handler
54+
55+
lambda_context = get_mock_context()
56+
datadog_lambda.handler.handler.__call__(None, lambda_context)
57+
mock_emit_telemetry.assert_called_once_with(
58+
lambda_context, test_context.exception, "nonsense", 42
59+
)
60+
61+
@patch.dict(os.environ, {"DD_LAMBDA_HANDLER": "nonsense.nonsense"}, clear=True)
62+
@patch("importlib.import_module")
63+
@patch("datadog_lambda.tracing.emit_telemetry_on_exception_outside_of_handler")
64+
@patch("time.time_ns", return_value=42)
65+
@patch("datadog_lambda.wrapper.datadog_lambda_wrapper")
66+
def test_handler_success(
67+
self, mock_lambda_wrapper, mock_time, mock_emit_telemetry, mock_import
68+
):
69+
def nonsense():
70+
pass
71+
72+
mock_import.nonsense.return_value = nonsense
73+
74+
import datadog_lambda.handler
75+
76+
lambda_context = get_mock_context()
77+
datadog_lambda.handler.handler.__call__(None, lambda_context)
78+
79+
mock_emit_telemetry.assert_not_called()
80+
mock_lambda_wrapper.assert_called_once_with(mock_import().nonsense)

tests/test_tracing.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import functools
33
import json
4+
import traceback
45
import pytest
56
import os
67
import unittest
@@ -36,6 +37,7 @@
3637
determine_service_name,
3738
service_mapping as global_service_mapping,
3839
propagator,
40+
emit_telemetry_on_exception_outside_of_handler,
3941
)
4042
from datadog_lambda.trigger import EventTypes
4143

@@ -1999,3 +2001,48 @@ def test_deterministic_m5_hash__always_leading_with_zero(self):
19992001
# Leading zeros will be omitted, so only test for full 64 bits present
20002002
if len(result_in_binary) == 66: # "0b" + 64 bits.
20012003
self.assertTrue(result_in_binary.startswith("0b0"))
2004+
2005+
2006+
class TestExceptionOutsideHandler(unittest.TestCase):
2007+
@patch("datadog_lambda.tracing.submit_errors_metric")
2008+
def test_exception_outside_handler(self, mock_submit_errors_metric):
2009+
fake_error = ValueError("Some error message")
2010+
resource_name = "my_handler"
2011+
span_type = "aws.lambda"
2012+
mock_span = Mock()
2013+
context = get_mock_context()
2014+
with patch(
2015+
"datadog_lambda.tracing.tracer.trace", return_value=mock_span
2016+
) as mock_trace:
2017+
emit_telemetry_on_exception_outside_of_handler(
2018+
context, fake_error, resource_name, 42
2019+
)
2020+
2021+
mock_submit_errors_metric.assert_called_once_with(context)
2022+
2023+
mock_trace.assert_called_once_with(
2024+
span_type,
2025+
service="aws.lambda",
2026+
resource=resource_name,
2027+
span_type="serverless",
2028+
)
2029+
mock_span.set_tags.assert_called_once_with(
2030+
{
2031+
"error.status": 500,
2032+
"error.type": "ValueError",
2033+
"error.message": fake_error,
2034+
"error.stack": "".join(
2035+
traceback.format_exception(
2036+
type(fake_error), fake_error, fake_error.__traceback__
2037+
)
2038+
),
2039+
"resource_names": resource_name,
2040+
"resource.name": resource_name,
2041+
"operation_name": span_type,
2042+
"status": "error",
2043+
"request_id": context.aws_request_id,
2044+
}
2045+
)
2046+
mock_span.finish.assert_called_once()
2047+
assert mock_span.error == 1
2048+
assert mock_span.start_ns == 42

0 commit comments

Comments
 (0)