diff --git a/.github/workflows/periodic.yml b/.github/workflows/periodic.yml index 89fee9f..adc42e9 100644 --- a/.github/workflows/periodic.yml +++ b/.github/workflows/periodic.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python: [ '3.7', '3.8', '3.9', '3.10' ] + python: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 74476b1..4f82483 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - python: [ '3.7', '3.8', '3.9', '3.10' ] + python: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] fail-fast: false steps: - run: 'echo "No build required"' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d923427..4715352 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python: [ '3.7', '3.8', '3.9', '3.10' ] + python: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/ecs_logging/_stdlib.py b/ecs_logging/_stdlib.py index c0fed3a..9eff74d 100644 --- a/ecs_logging/_stdlib.py +++ b/ecs_logging/_stdlib.py @@ -209,7 +209,12 @@ def format_to_ecs(self, record): continue value = extractors[field](record) if value is not None: - merge_dicts(de_dot(field, value), result) + # special case ecs.version that should not be de-dotted + if field == "ecs.version": + field_dict = {field: value} + else: + field_dict = de_dot(field, value) + merge_dicts(field_dict, result) available = record.__dict__ diff --git a/ecs_logging/_structlog.py b/ecs_logging/_structlog.py index a701c23..84877d7 100644 --- a/ecs_logging/_structlog.py +++ b/ecs_logging/_structlog.py @@ -55,7 +55,7 @@ def format_to_ecs(self, event_dict): else: event_dict["error"] = {"stack_trace": stack_trace} - event_dict.setdefault("ecs", {}).setdefault("version", ECS_VERSION) + event_dict.setdefault("ecs.version", ECS_VERSION) return event_dict def _json_dumps(self, value): diff --git a/noxfile.py b/noxfile.py index a0a7082..cafd240 100644 --- a/noxfile.py +++ b/noxfile.py @@ -36,7 +36,7 @@ def tests_impl(session): ) -@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10"]) +@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]) def test(session): tests_impl(session) diff --git a/pyproject.toml b/pyproject.toml index 393e128..b1f2e5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: System :: Logging", "License :: OSI Approved :: Apache Software License" ] diff --git a/tests/test_apm.py b/tests/test_apm.py index e9495a2..14622cc 100644 --- a/tests/test_apm.py +++ b/tests/test_apm.py @@ -49,7 +49,7 @@ def test_elasticapm_structlog_log_correlation_ecs_fields(spec_validator, apm): ecs = json.loads(spec_validator(stream.getvalue().rstrip())) ecs.pop("@timestamp") assert ecs == { - "ecs": {"version": "1.6.0"}, + "ecs.version": "1.6.0", "log.level": "info", "message": "test message", "span": {"id": span_id}, @@ -84,7 +84,7 @@ def test_elastic_apm_stdlib_no_filter_log_correlation_ecs_fields(apm): ecs = json.loads(stream.getvalue().rstrip()) assert ecs == { - "ecs": {"version": "1.6.0"}, + "ecs.version": "1.6.0", "log.level": "info", "log": { "logger": "apm-logger", @@ -128,7 +128,7 @@ def test_elastic_apm_stdlib_with_filter_log_correlation_ecs_fields(apm): ecs = json.loads(stream.getvalue().rstrip()) assert ecs == { - "ecs": {"version": "1.6.0"}, + "ecs.version": "1.6.0", "log.level": "info", "log": { "logger": "apm-logger", @@ -175,7 +175,7 @@ def test_elastic_apm_stdlib_exclude_fields(apm): ecs = json.loads(stream.getvalue().rstrip()) assert ecs == { - "ecs": {"version": "1.6.0"}, + "ecs.version": "1.6.0", "log.level": "info", "log": { "logger": "apm-logger", diff --git a/tests/test_stdlib_formatter.py b/tests/test_stdlib_formatter.py index 62bc41b..e7b2b57 100644 --- a/tests/test_stdlib_formatter.py +++ b/tests/test_stdlib_formatter.py @@ -51,7 +51,7 @@ def test_record_formatted(spec_validator): formatter = ecs_logging.StdlibFormatter(exclude_fields=["process"]) assert spec_validator(formatter.format(make_record())) == ( - '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs":{"version":"1.6.0"},' + '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs.version":"1.6.0",' '"log":{"logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},"function":"test_function"},' '"original":"1: hello"}}' ) @@ -63,7 +63,7 @@ def test_extra_global_is_merged(spec_validator): ) assert spec_validator(formatter.format(make_record())) == ( - '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs":{"version":"1.6.0"},' + '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs.version":"1.6.0",' '"environment":"dev",' '"log":{"logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},"function":"test_function"},' '"original":"1: hello"}}' @@ -80,7 +80,7 @@ def format_to_ecs(self, record): formatter = CustomFormatter(exclude_fields=["process"]) assert spec_validator(formatter.format(make_record())) == ( '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello",' - '"custom":"field","ecs":{"version":"1.6.0"},"log":{"logger":"logger-name","origin":' + '"custom":"field","ecs.version":"1.6.0","log":{"logger":"logger-name","origin":' '{"file":{"line":10,"name":"file.py"},"function":"test_function"},"original":"1: hello"}}' ) @@ -94,7 +94,7 @@ def test_can_be_set_on_handler(): assert stream.getvalue() == ( '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello",' - '"ecs":{"version":"1.6.0"},"log":{"logger":"logger-name","origin":{"file":{"line":10,' + '"ecs.version":"1.6.0","log":{"logger":"logger-name","origin":{"file":{"line":10,' '"name":"file.py"},"function":"test_function"},"original":"1: hello"}}\n' ) @@ -127,7 +127,7 @@ def test_extra_is_merged(time, logger): assert isinstance(ecs["log"]["origin"]["file"].pop("line"), int) assert ecs == { "@timestamp": "2020-03-20T16:16:37.187Z", - "ecs": {"version": "1.6.0"}, + "ecs.version": "1.6.0", "log.level": "info", "log": { "logger": logger.name, @@ -254,8 +254,6 @@ def test_stack_trace_limit_types_and_values(): "exclude_fields", [ "process", - "ecs", - "ecs.version", "log", "log.level", "message", @@ -279,6 +277,19 @@ def test_exclude_fields(exclude_fields): assert field_path[-1] not in obj +@pytest.mark.parametrize( + "exclude_fields", + [ + "ecs.version", + ], +) +def test_exclude_fields_not_dedotted(exclude_fields): + formatter = ecs_logging.StdlibFormatter(exclude_fields=[exclude_fields]) + ecs = formatter.format_to_ecs(make_record()) + for entry in exclude_fields: + assert entry not in ecs + + def test_exclude_fields_empty_json_object(): """Assert that if all JSON objects attributes are excluded then the object doesn't appear.""" formatter = ecs_logging.StdlibFormatter( @@ -350,7 +361,7 @@ def test_apm_data_conflicts(spec_validator): formatter = ecs_logging.StdlibFormatter(exclude_fields=["process"]) assert spec_validator(formatter.format(record)) == ( - '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs":{"version":"1.6.0"},' + '{"@timestamp":"2020-03-20T14:12:46.123Z","log.level":"debug","message":"1: hello","ecs.version":"1.6.0",' '"log":{"logger":"logger-name","origin":{"file":{"line":10,"name":"file.py"},"function":"test_function"},' '"original":"1: hello"},"service":{"environment":"dev","name":"myapp","version":"1.0.0"}}' ) diff --git a/tests/test_structlog_formatter.py b/tests/test_structlog_formatter.py index 3338a4c..e9a4296 100644 --- a/tests/test_structlog_formatter.py +++ b/tests/test_structlog_formatter.py @@ -66,7 +66,7 @@ def test_event_dict_formatted(time, spec_validator, event_dict): '{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",' '"message":"test message",' '"baz":"",' - '"ecs":{"version":"1.6.0"},' + '"ecs.version":"1.6.0",' '"foo":"bar",' '"log":{"logger":"logger-name"}}' ) @@ -90,7 +90,7 @@ def test_can_be_set_as_processor(time, spec_validator): assert spec_validator(stream.getvalue()) == ( '{"@timestamp":"2020-03-20T16:16:37.187Z","log.level":"debug",' '"message":"test message","custom":"key","dot":{"ted":1},' - '"ecs":{"version":"1.6.0"}}\n' + '"ecs.version":"1.6.0"}\n' )