Skip to content

Commit ee288d8

Browse files
authored
Ensure @timestamp, log.level, and message fields are ordered per spec
1 parent 659820d commit ee288d8

File tree

5 files changed

+111
-23
lines changed

5 files changed

+111
-23
lines changed

ecs_logging/_utils.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,48 @@ def merge_dicts(from_, into):
103103

104104
def json_dumps(value):
105105
# type: (Dict[str, Any]) -> str
106-
return json.dumps(value, sort_keys=True, separators=(",", ":"))
106+
107+
# Ensure that the first three fields are '@timestamp',
108+
# 'log.level', and 'message' per ECS spec
109+
ordered_fields = []
110+
try:
111+
ordered_fields.append(("@timestamp", value.pop("@timestamp")))
112+
except KeyError:
113+
pass
114+
115+
# log.level can either be nested or not nested so we have to try both
116+
try:
117+
ordered_fields.append(("log.level", value["log"].pop("level")))
118+
if not value["log"]: # Remove the 'log' dictionary if it's now empty
119+
value.pop("log", None)
120+
except KeyError:
121+
try:
122+
ordered_fields.append(("log.level", value.pop("log.level")))
123+
except KeyError:
124+
pass
125+
try:
126+
ordered_fields.append(("message", value.pop("message")))
127+
except KeyError:
128+
pass
129+
130+
# Because we want to use 'sorted_keys=True' we manually build
131+
# the first three keys and then build the rest with json.dumps()
132+
if ordered_fields:
133+
# Need to call json.dumps() on values just in
134+
# case the given values aren't strings (even though
135+
# they should be according to the spec)
136+
ordered_json = ",".join(
137+
'"%s":%s' % (k, json.dumps(v, sort_keys=True, separators=(",", ":")))
138+
for k, v in ordered_fields
139+
)
140+
if value:
141+
return "{%s,%s" % (
142+
ordered_json,
143+
json.dumps(value, sort_keys=True, separators=(",", ":"))[1:],
144+
)
145+
else:
146+
return "{%s}" % ordered_json
147+
# If there are no fields with ordering requirements we
148+
# pass everything into json.dumps()
149+
else:
150+
return json.dumps(value, sort_keys=True, separators=(",", ":"))

tests/test_apm.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_elasticapm_structlog_log_correlation_ecs_fields():
3434
ecs.pop("@timestamp")
3535
assert ecs == {
3636
"ecs": {"version": "1.6.0"},
37-
"log": {"level": "info"},
37+
"log.level": "info",
3838
"message": "test message",
3939
"span": {"id": span_id},
4040
"trace": {"id": trace_id},
@@ -72,8 +72,8 @@ def test_elastic_apm_stdlib_no_filter_log_correlation_ecs_fields():
7272
ecs = json.loads(stream.getvalue().rstrip())
7373
assert ecs == {
7474
"ecs": {"version": "1.6.0"},
75+
"log.level": "info",
7576
"log": {
76-
"level": "info",
7777
"logger": "apm-logger",
7878
"origin": {
7979
"file": {"name": "test_apm.py"},
@@ -116,8 +116,8 @@ def test_elastic_apm_stdlib_with_filter_log_correlation_ecs_fields():
116116
ecs = json.loads(stream.getvalue().rstrip())
117117
assert ecs == {
118118
"ecs": {"version": "1.6.0"},
119+
"log.level": "info",
119120
"log": {
120-
"level": "info",
121121
"logger": "apm-logger",
122122
"origin": {
123123
"file": {"name": "test_apm.py"},
@@ -163,8 +163,8 @@ def test_elastic_apm_stdlib_exclude_fields():
163163
ecs = json.loads(stream.getvalue().rstrip())
164164
assert ecs == {
165165
"ecs": {"version": "1.6.0"},
166+
"log.level": "info",
166167
"log": {
167-
"level": "info",
168168
"logger": "apm-logger",
169169
"origin": {
170170
"file": {"name": "test_apm.py"},

tests/test_stdlib_formatter.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ def test_record_formatted():
3939
formatter = ecs_logging.StdlibFormatter(exclude_fields=["process"])
4040

4141
assert formatter.format(make_record()) == (
42-
'{"@timestamp":"2020-03-20T14:12:46.123Z","ecs":{"version":"1.6.0"},'
43-
'"log":{"level":"debug","logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},'
44-
'"function":"test_function"},"original":"1: hello"},"message":"1: hello"}'
42+
'{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs":{"version":"1.6.0"},'
43+
'"log":{"logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},"function":"test_function"},'
44+
'"original":"1: hello"}}'
4545
)
4646

4747

@@ -54,9 +54,9 @@ def format_to_ecs(self, record):
5454

5555
formatter = CustomFormatter(exclude_fields=["process"])
5656
assert formatter.format(make_record()) == (
57-
'{"@timestamp":"2020-03-20T14:12:46.123Z","custom":"field","ecs":{"version":"1.6.0"},'
58-
'"log":{"level":"debug","logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},'
59-
'"function":"test_function"},"original":"1: hello"},"message":"1: hello"}'
57+
'{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello",'
58+
'"custom":"field","ecs":{"version":"1.6.0"},"log":{"logger":"logger-name","origin":'
59+
'{"file":{"line":10,"name":"file.py"},"function":"test_function"},"original":"1: hello"}}'
6060
)
6161

6262

@@ -68,9 +68,9 @@ def test_can_be_set_on_handler():
6868
handler.handle(make_record())
6969

7070
assert stream.getvalue() == (
71-
'{"@timestamp":"2020-03-20T14:12:46.123Z","ecs":{"version":"1.6.0"},'
72-
'"log":{"level":"debug","logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},'
73-
'"function":"test_function"},"original":"1: hello"},"message":"1: hello"}\n'
71+
'{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello",'
72+
'"ecs":{"version":"1.6.0"},"log":{"logger":"logger-name","origin":{"file":{"line":10,'
73+
'"name":"file.py"},"function":"test_function"},"original":"1: hello"}}\n'
7474
)
7575

7676

@@ -103,8 +103,8 @@ def test_extra_is_merged(time, logger):
103103
assert ecs == {
104104
"@timestamp": "2020-03-20T16:16:37.187Z",
105105
"ecs": {"version": "1.6.0"},
106+
"log.level": "info",
106107
"log": {
107-
"level": "info",
108108
"logger": logger.name,
109109
"origin": {
110110
"file": {"name": "test_stdlib_formatter.py"},
@@ -161,7 +161,7 @@ def test_stack_trace_limit_disabled(stack_trace_limit, logger):
161161

162162
ecs = json.loads(stream.getvalue().rstrip())
163163
assert ecs["error"] == {"message": "error!", "type": "ValueError"}
164-
assert ecs["log"]["level"] == "info"
164+
assert ecs["log.level"] == "info"
165165
assert ecs["message"] == "there was an error"
166166
assert ecs["log"]["original"] == "there was an error"
167167

@@ -195,7 +195,7 @@ def h():
195195
"message": "error!",
196196
"type": "ValueError",
197197
}
198-
assert ecs["log"]["level"] == "info"
198+
assert ecs["log.level"] == "info"
199199
assert ecs["message"] == "there was an error"
200200
assert ecs["log"]["original"] == "there was an error"
201201

tests/test_structlog_formatter.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ def test_event_dict_formatted(time):
1414

1515
formatter = ecs_logging.StructlogFormatter()
1616
assert formatter(None, "debug", make_event_dict()) == (
17-
'{"@timestamp":"2020-03-20T16:16:37.187Z","ecs":{"version":"1.6.0"},'
18-
'"log":{"level":"debug","logger":"logger-name"},"message":"test message"}'
17+
'{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",'
18+
'"message":"test message","ecs":{"version":"1.6.0"},'
19+
'"log":{"logger":"logger-name"}}'
1920
)
2021

2122

@@ -35,7 +36,7 @@ def test_can_be_set_as_processor(time):
3536
logger.debug("test message", custom="key", **{"dot.ted": 1})
3637

3738
assert stream.getvalue() == (
38-
'{"@timestamp":"2020-03-20T16:16:37.187Z","custom":"key",'
39-
'"dot":{"ted":1},"ecs":{"version":"1.6.0"},"log":{"level":"debug"},'
40-
'"message":"test message"}\n'
39+
'{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",'
40+
'"message":"test message","custom":"key","dot":{"ted":1},'
41+
'"ecs":{"version":"1.6.0"}}\n'
4142
)

tests/test_utils.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from ecs_logging._utils import flatten_dict, de_dot, normalize_dict
2+
from ecs_logging._utils import flatten_dict, de_dot, normalize_dict, json_dumps
33

44

55
def test_flatten_dict():
@@ -30,3 +30,46 @@ def test_normalize_dict():
3030

3131
def test_normalize_dict_with_array():
3232
assert normalize_dict({"a": ["1", "2"]}) == {"a": ["1", "2"]}
33+
34+
35+
@pytest.mark.parametrize(
36+
["value", "expected"],
37+
[
38+
({}, "{}"),
39+
({"log": {"level": "info"}}, '{"log.level":"info"}'),
40+
({"log.level": "info"}, '{"log.level":"info"}'),
41+
(
42+
{"log": {"level": "info", "message": "hello"}},
43+
'{"log.level":"info","log":{"message":"hello"}}',
44+
),
45+
({"@timestamp": "2021-01-01..."}, '{"@timestamp":"2021-01-01..."}'),
46+
({"message": "hello"}, '{"message":"hello"}'),
47+
({"message": 1}, '{"message":1}'),
48+
({"message": ["hello"]}, '{"message":["hello"]}'),
49+
({"message": {"key": "val"}}, '{"message":{"key":"val"}}'),
50+
({"custom": "value"}, '{"custom":"value"}'),
51+
({"log.level": "info"}, '{"log.level":"info"}'),
52+
(
53+
{"log": {"message": "hello"}, "message": "hello"},
54+
'{"message":"hello","log":{"message":"hello"}}',
55+
),
56+
(
57+
{
58+
"log": {"message": "hello", "level": "info"},
59+
"message": "hello",
60+
"@timestamp": "2021-01-01...",
61+
},
62+
'{"@timestamp":"2021-01-01...","log.level":"info","message":"hello","log":{"message":"hello"}}',
63+
),
64+
(
65+
{
66+
"log": {"level": "info"},
67+
"message": "hello",
68+
"@timestamp": "2021-01-01...",
69+
},
70+
'{"@timestamp":"2021-01-01...","log.level":"info","message":"hello"}',
71+
),
72+
],
73+
)
74+
def test_json_dumps(value, expected):
75+
assert json_dumps(value) == expected

0 commit comments

Comments
 (0)