Skip to content

Commit 280e6ef

Browse files
authored
feat(logger): add DatadogLogFormatter and observability provider (#2183)
1 parent af4679e commit 280e6ef

File tree

6 files changed

+129
-2
lines changed

6 files changed

+129
-2
lines changed

Diff for: aws_lambda_powertools/logging/formatter.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ def __init__(
139139
if self.utc:
140140
self.converter = time.gmtime
141141

142-
super(LambdaPowertoolsFormatter, self).__init__(datefmt=self.datefmt)
143-
144142
self.keys_combined = {**self._build_default_keys(), **kwargs}
145143
self.log_format.update(**self.keys_combined)
146144

145+
super().__init__(datefmt=self.datefmt)
146+
147147
def serialize(self, log: Dict) -> str:
148148
"""Serialize structured log dict to JSON str"""
149149
return self.json_serializer(log)

Diff for: aws_lambda_powertools/logging/formatters/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Built-in Logger formatters for Observability Providers that require custom config."""
2+
3+
# NOTE: we don't expose formatters directly (barrel import)
4+
# as we cannot know if they'll need additional dependencies in the future
5+
# so we isolate to avoid a performance hit and workarounds like lazy imports

Diff for: aws_lambda_powertools/logging/formatters/datadog.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Callable
4+
5+
from aws_lambda_powertools.logging.formatter import LambdaPowertoolsFormatter
6+
7+
8+
class DatadogLogFormatter(LambdaPowertoolsFormatter):
9+
def __init__(
10+
self,
11+
json_serializer: Callable[[dict], str] | None = None,
12+
json_deserializer: Callable[[dict | str | bool | int | float], str] | None = None,
13+
json_default: Callable[[Any], Any] | None = None,
14+
datefmt: str | None = None,
15+
use_datetime_directive: bool = False,
16+
log_record_order: list[str] | None = None,
17+
utc: bool = False,
18+
use_rfc3339: bool = True, # NOTE: The only change from our base formatter
19+
**kwargs,
20+
):
21+
"""Datadog formatter to comply with Datadog log parsing
22+
23+
Changes compared to the default Logger Formatter:
24+
25+
- timestamp format to use RFC3339 e.g., "2023-05-01T15:34:26.841+0200"
26+
27+
28+
Parameters
29+
----------
30+
log_record_order : list[str] | None, optional
31+
_description_, by default None
32+
33+
Parameters
34+
----------
35+
json_serializer : Callable, optional
36+
function to serialize `obj` to a JSON formatted `str`, by default json.dumps
37+
json_deserializer : Callable, optional
38+
function to deserialize `str`, `bytes`, bytearray` containing a JSON document to a Python `obj`,
39+
by default json.loads
40+
json_default : Callable, optional
41+
function to coerce unserializable values, by default str
42+
43+
Only used when no custom JSON encoder is set
44+
45+
datefmt : str, optional
46+
String directives (strftime) to format log timestamp.
47+
48+
See https://docs.python.org/3/library/time.html#time.strftime or
49+
use_datetime_directive: str, optional
50+
Interpret `datefmt` as a format string for `datetime.datetime.strftime`, rather than
51+
`time.strftime` - Only useful when used alongside `datefmt`.
52+
53+
See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior . This
54+
also supports a custom %F directive for milliseconds.
55+
56+
log_record_order : list, optional
57+
set order of log keys when logging, by default ["level", "location", "message", "timestamp"]
58+
59+
utc : bool, optional
60+
set logging timestamp to UTC, by default False to continue to use local time as per stdlib
61+
use_rfc3339: bool, optional
62+
Whether to use a popular dateformat that complies with both RFC3339 and ISO8601.
63+
e.g., 2022-10-27T16:27:43.738+02:00.
64+
kwargs
65+
Key-value to persist in all log messages
66+
"""
67+
super().__init__(
68+
json_serializer=json_serializer,
69+
json_deserializer=json_deserializer,
70+
json_default=json_default,
71+
datefmt=datefmt,
72+
use_datetime_directive=use_datetime_directive,
73+
log_record_order=log_record_order,
74+
utc=utc,
75+
use_rfc3339=use_rfc3339,
76+
**kwargs,
77+
)

Diff for: docs/core/logger.md

+20
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,26 @@ If you prefer configuring it separately, or you'd want to bring this JSON Format
445445
--8<-- "examples/logger/src/powertools_formatter_setup.py"
446446
```
447447

448+
### Observability providers
449+
450+
!!! note "In this context, an observability provider is an [AWS Lambda Partner](https://go.aws/3HtU6CZ){target="_blank"} offering a platform for logging, metrics, traces, etc."
451+
452+
You can send logs to the observability provider of your choice via [Lambda Extensions](https://aws.amazon.com/blogs/compute/using-aws-lambda-extensions-to-send-logs-to-custom-destinations/){target="_blank"}. In most cases, you shouldn't need any custom Logger configuration, and logs will be shipped async without any performance impact.
453+
454+
#### Built-in formatters
455+
456+
In rare circumstances where JSON logs are not parsed correctly by your provider, we offer built-in formatters to make this transition easier.
457+
458+
| Provider | Formatter | Notes |
459+
| -------- | --------------------- | ---------------------------------------------------- |
460+
| Datadog | `DatadogLogFormatter` | Modifies default timestamp to use RFC3339 by default |
461+
462+
You can use import and use them as any other Logger formatter via `logger_formatter` parameter:
463+
464+
```python hl_lines="2 4" title="Using built-in Logger Formatters"
465+
--8<-- "examples/logger/src/observability_provider_builtin_formatters.py"
466+
```
467+
448468
### Migrating from other Loggers
449469

450470
If you're migrating from other Loggers, there are few key points to be aware of: [Service parameter](#the-service-parameter), [Inheriting Loggers](#inheriting-loggers), [Overriding Log records](#overriding-log-records), and [Logging exceptions](#logging-exceptions).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.logging.formatters.datadog import DatadogLogFormatter
3+
4+
logger = Logger(service="payment", logger_formatter=DatadogLogFormatter())
5+
logger.info("hello")

Diff for: tests/functional/test_logger_powertools_formatter.py

+20
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import json
44
import os
55
import random
6+
import re
67
import string
78
import time
89

910
import pytest
1011

1112
from aws_lambda_powertools import Logger
13+
from aws_lambda_powertools.logging.formatters.datadog import DatadogLogFormatter
1214

1315

1416
@pytest.fixture
@@ -22,6 +24,10 @@ def service_name():
2224
return "".join(random.SystemRandom().choice(chars) for _ in range(15))
2325

2426

27+
def capture_logging_output(stdout):
28+
return json.loads(stdout.getvalue().strip())
29+
30+
2531
@pytest.mark.parametrize("level", ["DEBUG", "WARNING", "ERROR", "INFO", "CRITICAL"])
2632
def test_setup_with_valid_log_levels(stdout, level, service_name):
2733
logger = Logger(service=service_name, level=level, stream=stdout, request_id="request id!", another="value")
@@ -309,3 +315,17 @@ def test_log_json_pretty_indent(stdout, service_name, monkeypatch):
309315
# THEN the json should contain more than line
310316
new_lines = stdout.getvalue().count(os.linesep)
311317
assert new_lines > 1
318+
319+
320+
def test_datadog_formatter_use_rfc3339_date(stdout, service_name):
321+
# GIVEN Datadog Log Formatter is used
322+
logger = Logger(service=service_name, stream=stdout, logger_formatter=DatadogLogFormatter())
323+
RFC3339_REGEX = r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$"
324+
325+
# WHEN a log statement happens
326+
logger.info({})
327+
328+
# THEN the timestamp uses RFC3339 by default
329+
log = capture_logging_output(stdout)
330+
331+
assert re.fullmatch(RFC3339_REGEX, log["timestamp"]) # "2022-10-27T17:42:26.841+0200"

0 commit comments

Comments
 (0)