Skip to content

Commit 187f138

Browse files
committed
Add spec validation
1 parent c63bd87 commit 187f138

File tree

5 files changed

+85
-10
lines changed

5 files changed

+85
-10
lines changed

.flake8

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[flake8]
2+
exclude=
3+
tests/**,
4+
conftest.py,
5+
setup.py
6+
max-line-length=120
7+
ignore=E731,W503,E203,BLK100,B301

tests/conftest.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import datetime
2+
import json
3+
import os
4+
5+
import pytest
6+
7+
8+
class ValidationError(Exception):
9+
pass
10+
11+
12+
@pytest.fixture
13+
def spec_validator():
14+
with open(
15+
os.path.join(os.path.dirname(__file__), "resources", "spec.json"), "r"
16+
) as fh:
17+
spec = json.load(fh)
18+
19+
def validator(data_json):
20+
"""
21+
Throws a ValidationError if anything doesn't match the spec.
22+
23+
Returns the original json (pass-through)
24+
"""
25+
fields = spec["fields"]
26+
data = json.loads(data_json)
27+
for k, v in fields.items():
28+
if v.get("required"):
29+
found = False
30+
if k in data:
31+
found = True
32+
elif "." in k:
33+
# Dotted keys could be nested, like ecs.version
34+
subkeys = k.split(".")
35+
subval = data
36+
for subkey in subkeys:
37+
subval = subval.get(subkey, {})
38+
if subval:
39+
found = True
40+
if not found:
41+
raise ValidationError("Missing required key {}".format(k))
42+
if k in data:
43+
if v["type"] == "string" and not isinstance(data[k], str):
44+
raise ValidationError(
45+
"Value {0} for key {1} should be string".format(data[k], k)
46+
)
47+
if v["type"] == "datetime":
48+
try:
49+
datetime.datetime.fromisoformat(data[k].rstrip("Z"))
50+
except ValueError:
51+
raise ValidationError(
52+
"Value {0} for key {1} doesn't parse as an ISO datetime".format(
53+
data[k], k
54+
)
55+
)
56+
if v.get("index"):
57+
index = v.get("index")
58+
key = json.loads(
59+
data_json.split(",")[index].split(":")[0].strip().lstrip("{")
60+
)
61+
if key != k:
62+
raise ValidationError(
63+
"Key {0} is not at index {1}".format(k, index)
64+
)
65+
66+
return data_json
67+
68+
return validator

tests/test_apm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .compat import StringIO
1111

1212

13-
def test_elasticapm_structlog_log_correlation_ecs_fields():
13+
def test_elasticapm_structlog_log_correlation_ecs_fields(spec_validator):
1414
apm = elasticapm.Client({"SERVICE_NAME": "apm-service", "DISABLE_SEND": True})
1515
stream = StringIO()
1616
logger = structlog.PrintLogger(stream)
@@ -30,7 +30,7 @@ def test_elasticapm_structlog_log_correlation_ecs_fields():
3030
finally:
3131
apm.end_transaction("test-transaction")
3232

33-
ecs = json.loads(stream.getvalue().rstrip())
33+
ecs = json.loads(spec_validator(stream.getvalue().rstrip()))
3434
ecs.pop("@timestamp")
3535
assert ecs == {
3636
"ecs": {"version": "1.6.0"},

tests/test_stdlib_formatter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,25 @@ def make_record():
3535
return record
3636

3737

38-
def test_record_formatted():
38+
def test_record_formatted(spec_validator):
3939
formatter = ecs_logging.StdlibFormatter(exclude_fields=["process"])
4040

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

4747

48-
def test_can_be_overridden():
48+
def test_can_be_overridden(spec_validator):
4949
class CustomFormatter(ecs_logging.StdlibFormatter):
5050
def format_to_ecs(self, record):
5151
ecs_dict = super(CustomFormatter, self).format_to_ecs(record)
5252
ecs_dict["custom"] = "field"
5353
return ecs_dict
5454

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

tests/test_structlog_formatter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ def make_event_dict():
99

1010

1111
@mock.patch("time.time")
12-
def test_event_dict_formatted(time):
12+
def test_event_dict_formatted(time, spec_validator):
1313
time.return_value = 1584720997.187709
1414

1515
formatter = ecs_logging.StructlogFormatter()
16-
assert formatter(None, "debug", make_event_dict()) == (
16+
assert spec_validator(formatter(None, "debug", make_event_dict())) == (
1717
'{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",'
1818
'"message":"test message","ecs":{"version":"1.6.0"},'
1919
'"log":{"logger":"logger-name"}}'
2020
)
2121

2222

2323
@mock.patch("time.time")
24-
def test_can_be_set_as_processor(time):
24+
def test_can_be_set_as_processor(time, spec_validator):
2525
time.return_value = 1584720997.187709
2626

2727
stream = StringIO()
@@ -35,7 +35,7 @@ def test_can_be_set_as_processor(time):
3535
logger = structlog.get_logger("logger-name")
3636
logger.debug("test message", custom="key", **{"dot.ted": 1})
3737

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

0 commit comments

Comments
 (0)