Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -463,6 +463,22 @@ Parameters description:

These settings are subsequently passed to [opentelemetry](https://opentelemetry.io/), finalizing your Opentelemetry integration.

If an authoritative baggage value becomes available only after the current span has started, pass the same mapping to
`opentelemetry_baggage_scope` to also materialize that value on the already-started span:

```python
from microbootstrap import opentelemetry_baggage_scope


with opentelemetry_baggage_scope(
{"conversation_id": conversation_id},
current_span_attributes=settings.opentelemetry_baggage_span_attributes,
):
...
```

Only non-`None` baggage values supplied to the scope and present in the mapping are added to a recording current span.

#### FastStream

For FastStream you also should pass `opentelemetry_middleware_cls` - OpenTelemetry middleware for your broker
Expand Down
8 changes: 7 additions & 1 deletion microbootstrap/instruments/opentelemetry_instrument.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import SpanKind, format_span_id, set_tracer_provider
from opentelemetry.trace import SpanKind, format_span_id, get_current_span, set_tracer_provider
from opentelemetry.util._importlib_metadata import entry_points

from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
Expand All@@ -44,6 +44,8 @@
@contextlib.contextmanager
def opentelemetry_baggage_scope(
baggage_values: typing.Mapping[str, object | None],
*,
current_span_attributes: typing.Mapping[str, str] | None = None,
) -> typing.Iterator[None]:
baggage_context = context.get_current()
for key, value in baggage_values.items():
Expand All@@ -55,6 +57,10 @@ def opentelemetry_baggage_scope(

token: typing.Final = context.attach(baggage_context)
try:
if current_span_attributes and (current_span := get_current_span()).is_recording():
for baggage_key, span_attribute in current_span_attributes.items():
if baggage_key in baggage_values and (value := baggage_values[baggage_key]) is not None:
current_span.set_attribute(span_attribute, str(value))
yield
except Exception as exc:
with contextlib.suppress(Exception):
Expand Down
45 changes: 45 additions & 0 deletions tests/instruments/test_opentelemetry.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,51 @@ def test_opentelemetry_baggage_scope_does_not_replace_exception_when_snapshot_fa
raise ValueError("application error")


def test_opentelemetry_baggage_scope_materializes_supplied_values_on_current_span(
monkeypatch: pytest.MonkeyPatch,
) -> None:
conversation_id = 42
current_span = MagicMock(spec=Span)
current_span.is_recording.return_value = True
monkeypatch.setattr(opentelemetry_instrument, "get_current_span", Mock(return_value=current_span))

with opentelemetry_baggage_scope(
{"conversation_id": conversation_id, "not_configured": "ignored"},
current_span_attributes={
"conversation_id": "conversation.id",
"not_supplied": "not.supplied",
},
):
assert baggage.get_baggage("conversation_id") == conversation_id

current_span.set_attribute.assert_called_once_with("conversation.id", str(conversation_id))


@pytest.mark.parametrize(
("baggage_value", "is_recording"),
[
(None, True),
("conversation-1", False),
],
)
def test_opentelemetry_baggage_scope_skips_current_span_attribute(
baggage_value: str | None,
is_recording: bool,
monkeypatch: pytest.MonkeyPatch,
) -> None:
current_span = MagicMock(spec=Span)
current_span.is_recording.return_value = is_recording
monkeypatch.setattr(opentelemetry_instrument, "get_current_span", Mock(return_value=current_span))

with opentelemetry_baggage_scope(
{"conversation_id": baggage_value},
current_span_attributes={"conversation_id": "conversation.id"},
):
pass

current_span.set_attribute.assert_not_called()


@pytest.mark.parametrize(
("span_kind", "expected_attribute"),
[
Expand Down
Loading