Skip to content

Commit fd8cdb0

Browse files
authored
fix: disable async background flush of traces (#29)
When running in complex test suite that mocks many things, trying to flush in the background traces does not work. For example network requests might be mocked, or time might be mocked, making SSL connection impossible. This stores the span in memory until a flush is requested at the end of pytest. This also means there is no way to capture the opentelemetry log in the background are we can now intercept any error raised by the code while flushing the traces.
1 parent 7140d19 commit fd8cdb0

File tree

3 files changed

+35
-34
lines changed

3 files changed

+35
-34
lines changed

pytest_mergify/__init__.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@ def pytest_terminal_summary(
3737
# sure that we capture the possible error logs, otherwise they are
3838
# emitted on exit (atexit()).
3939
if self.mergify_tracer.tracer_provider is not None:
40-
self.mergify_tracer.tracer_provider.shutdown() # type: ignore[no-untyped-call]
41-
42-
if self.mergify_tracer.log_handler.log_list:
43-
terminalreporter.write_line(
44-
"There are been some errors reported by the tracer:", red=True
45-
)
46-
for line in self.mergify_tracer.log_handler.log_list:
47-
terminalreporter.write_line(line)
40+
try:
41+
self.mergify_tracer.tracer_provider.force_flush()
42+
except Exception as e:
43+
terminalreporter.write_line(
44+
f"Error while exporting traces: {e}",
45+
red=True,
46+
)
47+
try:
48+
self.mergify_tracer.tracer_provider.shutdown() # type: ignore[no-untyped-call]
49+
except Exception as e:
50+
terminalreporter.write_line(
51+
f"Error while shutting down the tracer: {e}",
52+
red=True,
53+
)
4854

4955
if self.mergify_tracer.token is None:
5056
terminalreporter.write_line(

pytest_mergify/tracer.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import dataclasses
2-
import logging
32
import os
43

54
import opentelemetry.sdk.resources
65
from opentelemetry.sdk.trace import export
76
from opentelemetry import context
8-
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor, Span
7+
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor, Span, ReadableSpan
98
from opentelemetry.exporter.otlp.proto.http import Compression
109
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
1110
OTLPSpanExporter,
@@ -33,15 +32,21 @@ def on_start(
3332
self.trace_id = span.context.trace_id
3433

3534

36-
class ListLogHandler(logging.Handler):
37-
"""Custom logging handler to capture log messages into a list."""
35+
class SynchronousBatchSpanProcessor(export.SimpleSpanProcessor):
36+
def __init__(self, exporter: export.SpanExporter) -> None:
37+
super().__init__(exporter)
38+
self.queue: list[ReadableSpan] = []
3839

39-
def __init__(self) -> None:
40-
super().__init__()
41-
self.log_list: list[str] = []
40+
def force_flush(self, timeout_millis: int = 30_000) -> bool:
41+
self.span_exporter.export(self.queue)
42+
self.queue.clear()
43+
return True
44+
45+
def on_end(self, span: ReadableSpan) -> None:
46+
if not span.context.trace_flags.sampled:
47+
return
4248

43-
def emit(self, record: logging.LogRecord) -> None:
44-
self.log_list.append(self.format(record))
49+
self.queue.append(span)
4550

4651

4752
@dataclasses.dataclass
@@ -63,25 +68,10 @@ class MergifyTracer:
6368
tracer_provider: opentelemetry.sdk.trace.TracerProvider | None = dataclasses.field(
6469
init=False, default=None
6570
)
66-
log_handler: ListLogHandler = dataclasses.field(
67-
init=False, default_factory=ListLogHandler
68-
)
6971

7072
def __post_init__(self) -> None:
7173
span_processor: SpanProcessor
7274

73-
# Set up the logger
74-
self.log_handler.setLevel(logging.ERROR) # Capture ERROR logs by default
75-
self.log_handler.setFormatter(
76-
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
77-
)
78-
79-
logger = logging.getLogger("opentelemetry")
80-
# FIXME: we should remove the handler when this tracer is not used
81-
# (e.g., reconfigure() is called) Since reconfigure() is unlikely to be
82-
# called outside our testing things, it's not a big deal to leak it.
83-
logger.addHandler(self.log_handler)
84-
8575
if os.environ.get("PYTEST_MERGIFY_DEBUG"):
8676
self.exporter = export.ConsoleSpanExporter()
8777
span_processor = export.SimpleSpanProcessor(self.exporter)
@@ -101,7 +91,7 @@ def __post_init__(self) -> None:
10191
headers={"Authorization": f"Bearer {self.token}"},
10292
compression=Compression.Gzip,
10393
)
104-
span_processor = export.BatchSpanProcessor(self.exporter)
94+
span_processor = SynchronousBatchSpanProcessor(self.exporter)
10595
else:
10696
return
10797

tests/test_plugin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ def test_span(pytestconfig):
116116
)
117117
result = pytester.runpytest_subprocess()
118118
result.assert_outcomes(passed=1)
119-
assert "There are been some errors reported by the tracer:" in result.stdout.lines
119+
assert any(
120+
line.startswith(
121+
"Error while exporting traces: HTTPConnectionPool(host='localhost', port=9999): Max retries exceeded with url"
122+
)
123+
for line in result.stdout.lines
124+
)

0 commit comments

Comments
 (0)