Skip to content

Commit 850c9cb

Browse files
authored
Fix merging dicts for duplicate dotted keys (#46)
* Fix merging dicts for duplicate dotted keys Fixes #45 * CHANGELOG * Force `message` to a string * Avoid conflicts between `event` and `event.dataset` * Better handling for elasticapm client cleanup * Better errors for conflicting dotted keys * Peg APM tests to py 3.6+ only * Reviewer suggestions
1 parent 8249208 commit 850c9cb

File tree

6 files changed

+66
-15
lines changed

6 files changed

+66
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* Fixed an issue in `StructlogFormatter` caused by a conflict with `event`
6+
(used for the log `message`) and `event.dataset` (a field provided by the
7+
`elasticapm` integration) ([#46](https://github.com/elastic/ecs-logging-python/pull/46))
8+
39
## 1.0.0 (2021-02-08)
410

511
* Remove "beta" designation

ecs_logging/_structlog.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ class StructlogFormatter:
2929

3030
def __call__(self, _, name, event_dict):
3131
# type: (Any, str, Dict[str, Any]) -> str
32+
33+
# Handle event -> message now so that stuff like `event.dataset` doesn't
34+
# cause problems down the line
35+
event_dict["message"] = str(event_dict.pop("event"))
3236
event_dict = normalize_dict(event_dict)
3337
event_dict.setdefault("log", {}).setdefault("level", name.lower())
3438
event_dict = self.format_to_ecs(event_dict)
3539
return json_dumps(event_dict)
3640

3741
def format_to_ecs(self, event_dict):
3842
# type: (Dict[str, Any]) -> Dict[str, Any]
39-
event_dict["message"] = event_dict.pop("event")
4043
if "@timestamp" not in event_dict:
4144
event_dict["@timestamp"] = (
4245
datetime.datetime.utcfromtimestamp(time.time()).strftime(

ecs_logging/_utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,16 @@ def merge_dicts(from_, into):
111111
When called has side-effects within 'destination'.
112112
"""
113113
for key, value in from_.items():
114-
if isinstance(value, dict):
115-
merge_dicts(value, into.setdefault(key, {}))
114+
into.setdefault(key, {})
115+
if isinstance(value, dict) and isinstance(into[key], dict):
116+
merge_dicts(value, into[key])
117+
elif into[key] != {}:
118+
raise TypeError(
119+
"Type mismatch at key `{}`: merging dicts would replace value `{}` with `{}`. This is likely due to "
120+
"dotted keys in the event dict being turned into nested dictionaries, causing a conflict.".format(
121+
key, into[key], value
122+
)
123+
)
116124
else:
117125
into[key] = value
118126
return into

tests/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import json
2020
import os
2121
import collections
22+
import sys
23+
import logging
24+
import elasticapm
2225

2326
import pytest
2427

@@ -27,6 +30,10 @@ class ValidationError(Exception):
2730
pass
2831

2932

33+
if sys.version_info[0] >= 3:
34+
basestring = str
35+
36+
3037
@pytest.fixture
3138
def spec_validator():
3239
with open(
@@ -81,3 +88,16 @@ def validator(data_json):
8188
return data_json
8289

8390
return validator
91+
92+
93+
@pytest.fixture
94+
def apm():
95+
if sys.version_info < (3, 6):
96+
pytest.skip("elasticapm only supports python 3.6+")
97+
if sys.version_info[0] >= 3:
98+
record_factory = logging.getLogRecordFactory()
99+
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
100+
yield apm
101+
apm.close()
102+
if sys.version_info[0] >= 3:
103+
logging.setLogRecordFactory(record_factory)

tests/test_apm.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
from .compat import StringIO
2828

2929

30-
def test_elasticapm_structlog_log_correlation_ecs_fields(spec_validator):
31-
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
30+
def test_elasticapm_structlog_log_correlation_ecs_fields(spec_validator, apm):
3231
stream = StringIO()
3332
logger = structlog.PrintLogger(stream)
3433
logger = structlog.wrap_logger(
@@ -57,14 +56,11 @@ def test_elasticapm_structlog_log_correlation_ecs_fields(spec_validator):
5756
"trace": {"id": trace_id},
5857
"transaction": {"id": transaction_id},
5958
"service": {"name": "apm-service"},
59+
"event": {"dataset": "apm-service.log"},
6060
}
6161

6262

63-
@pytest.mark.skipif(
64-
sys.version_info < (3, 2), reason="elastic-apm uses logger factory in Python 3.2+"
65-
)
66-
def test_elastic_apm_stdlib_no_filter_log_correlation_ecs_fields():
67-
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
63+
def test_elastic_apm_stdlib_no_filter_log_correlation_ecs_fields(apm):
6864
stream = StringIO()
6965
logger = logging.getLogger("apm-logger")
7066
handler = logging.StreamHandler(stream)
@@ -104,11 +100,11 @@ def test_elastic_apm_stdlib_no_filter_log_correlation_ecs_fields():
104100
"trace": {"id": trace_id},
105101
"transaction": {"id": transaction_id},
106102
"service": {"name": "apm-service"},
103+
"event": {"dataset": "apm-service.log"},
107104
}
108105

109106

110-
def test_elastic_apm_stdlib_with_filter_log_correlation_ecs_fields():
111-
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
107+
def test_elastic_apm_stdlib_with_filter_log_correlation_ecs_fields(apm):
112108
stream = StringIO()
113109
logger = logging.getLogger("apm-logger")
114110
handler = logging.StreamHandler(stream)
@@ -149,11 +145,11 @@ def test_elastic_apm_stdlib_with_filter_log_correlation_ecs_fields():
149145
"trace": {"id": trace_id},
150146
"transaction": {"id": transaction_id},
151147
"service": {"name": "apm-service"},
148+
"event": {"dataset": "apm-service.log"},
152149
}
153150

154151

155-
def test_elastic_apm_stdlib_exclude_fields():
156-
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
152+
def test_elastic_apm_stdlib_exclude_fields(apm):
157153
stream = StringIO()
158154
logger = logging.getLogger("apm-logger")
159155
handler = logging.StreamHandler(stream)
@@ -195,4 +191,5 @@ def test_elastic_apm_stdlib_exclude_fields():
195191
"message": "test message",
196192
"trace": {"id": trace_id},
197193
"service": {"name": "apm-service"},
194+
"event": {"dataset": "apm-service.log"},
198195
}

tests/test_structlog_formatter.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,24 @@
2020
import mock
2121
from .compat import StringIO
2222

23+
import pytest
24+
2325

2426
def make_event_dict():
25-
return {"event": "test message", "log.logger": "logger-name"}
27+
return {
28+
"event": "test message",
29+
"event.dataset": "agent.log",
30+
"log.logger": "logger-name",
31+
"foo": "bar",
32+
}
33+
34+
35+
def test_conflicting_event_dict():
36+
formatter = ecs_logging.StructlogFormatter()
37+
event_dict = make_event_dict()
38+
event_dict["foo.bar"] = "baz"
39+
with pytest.raises(TypeError):
40+
formatter(None, "debug", event_dict)
2641

2742

2843
@mock.patch("time.time")
@@ -33,6 +48,8 @@ def test_event_dict_formatted(time, spec_validator):
3348
assert spec_validator(formatter(None, "debug", make_event_dict())) == (
3449
'{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",'
3550
'"message":"test message","ecs":{"version":"1.6.0"},'
51+
'"event":{"dataset":"agent.log"},'
52+
'"foo":"bar",'
3653
'"log":{"logger":"logger-name"}}'
3754
)
3855

0 commit comments

Comments
 (0)