Skip to content

Commit 12a50f0

Browse files
committed
Add support for Elastic APM log correlation
1 parent b6555c7 commit 12a50f0

File tree

4 files changed

+145
-3
lines changed

4 files changed

+145
-3
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ python -m pip install ecs-logging
2626
[`logging`](https://docs.python.org/3/library/logging.html) module
2727
and the [`structlog`](https://www.structlog.org/en/stable/) package.
2828

29-
### Logging Example
29+
## Elastic APM Log Correlation
30+
31+
`ecs-logging-python` supports automatically collecting [ECS tracing fields](https://www.elastic.co/guide/en/ecs/master/ecs-tracing.html)
32+
from the [Elastic APM Python agent](https://github.com/elastic/apm-agent-python) in order to
33+
[correlate logs to spans, transactions and traces](https://www.elastic.co/guide/en/apm/agent/python/current/log-correlation.html) in Elastic APM.
34+
35+
## Standard Library `logging` Module
3036

3137
```python
3238
import logging
@@ -71,7 +77,7 @@ logger.debug("Example message!", extra={"http.request.method": "get"})
7177
}
7278
```
7379

74-
### Structlog Example
80+
## Structlog Example
7581

7682
```python
7783
import structlog

ecs_logging/_stdlib.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,17 @@ def format_to_ecs(self, record):
158158
extra_keys = set(available).difference(self._LOGRECORD_DICT)
159159
extras = flatten_dict({key: available[key] for key in extra_keys})
160160

161+
# Pop all Elastic APM extras and add them
162+
# to standard tracing ECS fields.
163+
extras["span.id"] = extras.pop("elasticapm_span_id", None)
164+
extras["transaction.id"] = extras.pop("elasticapm_transaction_id", None)
165+
extras["trace.id"] = extras.pop("elasticapm_trace_id", None)
166+
161167
# Merge in any keys that were set within 'extra={...}'
162168
for field, value in extras.items():
163-
if self._is_field_excluded(field):
169+
if field.startswith("elasticapm_labels."):
170+
continue # Unconditionally remove, we don't need this info.
171+
if value is None or self._is_field_excluded(field):
164172
continue
165173
merge_dicts(de_dot(field, value), result)
166174

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ develop = [
3333
"pytest-cov",
3434
"mock",
3535
"structlog",
36+
"elastic-apm",
3637
]
3738

3839
[tool.flit.metadata.urls]

tests/test_apm.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import json
2+
import elasticapm
3+
from elasticapm.handlers.structlog import structlog_processor
4+
import ecs_logging
5+
import logging
6+
import structlog
7+
from .compat import StringIO
8+
9+
10+
def test_elasticapm_structlog_log_correlation_ecs_fields():
11+
apm = elasticapm.Client({"SERVICE_NAME": "apm-service"})
12+
stream = StringIO()
13+
logger = structlog.PrintLogger(stream)
14+
logger = structlog.wrap_logger(
15+
logger, processors=[structlog_processor, ecs_logging.StructlogFormatter()]
16+
)
17+
log = logger.new()
18+
19+
apm.begin_transaction("test-transaction")
20+
try:
21+
with elasticapm.capture_span("test-span"):
22+
span_id = elasticapm.get_span_id()
23+
trace_id = elasticapm.get_trace_id()
24+
transaction_id = elasticapm.get_transaction_id()
25+
26+
log.info("test message")
27+
finally:
28+
apm.end_transaction("test-transaction")
29+
30+
ecs = json.loads(stream.getvalue().rstrip())
31+
ecs.pop("@timestamp")
32+
assert ecs == {
33+
"ecs": {"version": "1.5.0"},
34+
"log": {"level": "info"},
35+
"message": "test message",
36+
"span": {"id": span_id},
37+
"trace": {"id": trace_id},
38+
"transaction": {"id": transaction_id},
39+
}
40+
41+
42+
def test_elastic_apm_stdlib_log_correlation_ecs_fields():
43+
apm = elasticapm.Client({"SERVICE_NAME": "apm-service"})
44+
stream = StringIO()
45+
logger = logging.getLogger("apm-logger")
46+
handler = logging.StreamHandler(stream)
47+
handler.setFormatter(
48+
ecs_logging.StdlibFormatter(
49+
exclude_fields=["@timestamp", "process", "log.origin.file.line"]
50+
)
51+
)
52+
logger.addHandler(handler)
53+
logger.setLevel(logging.DEBUG)
54+
55+
apm.begin_transaction("test-transaction")
56+
try:
57+
with elasticapm.capture_span("test-span"):
58+
span_id = elasticapm.get_span_id()
59+
trace_id = elasticapm.get_trace_id()
60+
transaction_id = elasticapm.get_transaction_id()
61+
62+
logger.info("test message")
63+
finally:
64+
apm.end_transaction("test-transaction")
65+
66+
ecs = json.loads(stream.getvalue().rstrip())
67+
assert ecs == {
68+
"ecs": {"version": "1.5.0"},
69+
"log": {
70+
"level": "info",
71+
"logger": "apm-logger",
72+
"origin": {
73+
"file": {"name": "test_apm.py"},
74+
"function": "test_elastic_apm_stdlib_log_correlation_ecs_fields",
75+
},
76+
"original": "test message",
77+
},
78+
"message": "test message",
79+
"span": {"id": span_id},
80+
"trace": {"id": trace_id},
81+
"transaction": {"id": transaction_id},
82+
}
83+
84+
85+
def test_elastic_apm_stdlib_exclude_fields():
86+
apm = elasticapm.Client({"SERVICE_NAME": "apm-service"})
87+
stream = StringIO()
88+
logger = logging.getLogger("apm-logger")
89+
handler = logging.StreamHandler(stream)
90+
handler.setFormatter(
91+
ecs_logging.StdlibFormatter(
92+
exclude_fields=[
93+
"@timestamp",
94+
"process",
95+
"log.origin.file.line",
96+
"span",
97+
"transaction.id",
98+
]
99+
)
100+
)
101+
logger.addHandler(handler)
102+
logger.setLevel(logging.DEBUG)
103+
104+
apm.begin_transaction("test-transaction")
105+
try:
106+
with elasticapm.capture_span("test-span"):
107+
trace_id = elasticapm.get_trace_id()
108+
109+
logger.info("test message")
110+
finally:
111+
apm.end_transaction("test-transaction")
112+
113+
ecs = json.loads(stream.getvalue().rstrip())
114+
assert ecs == {
115+
"ecs": {"version": "1.5.0"},
116+
"log": {
117+
"level": "info",
118+
"logger": "apm-logger",
119+
"origin": {
120+
"file": {"name": "test_apm.py"},
121+
"function": "test_elastic_apm_stdlib_exclude_fields",
122+
},
123+
"original": "test message",
124+
},
125+
"message": "test message",
126+
"trace": {"id": trace_id},
127+
}

0 commit comments

Comments
 (0)