microbootstrap 1.5.2__tar.gz → 1.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/PKG-INFO +17 -1
  2. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/README.md +16 -0
  3. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/opentelemetry_instrument.py +12 -1
  4. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/sentry_instrument.py +102 -19
  5. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/pyproject.toml +1 -1
  6. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/pyproject.toml.orig +1 -1
  7. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/__init__.py +0 -0
  8. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/__init__.py +0 -0
  9. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/base.py +0 -0
  10. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/fastapi.py +0 -0
  11. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/fastmcp.py +0 -0
  12. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/faststream.py +0 -0
  13. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/bootstrappers/litestar.py +0 -0
  14. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/config/__init__.py +0 -0
  15. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/config/fastapi.py +0 -0
  16. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/config/fastmcp.py +0 -0
  17. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/config/faststream.py +0 -0
  18. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/config/litestar.py +0 -0
  19. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/console_writer.py +0 -0
  20. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/exceptions.py +0 -0
  21. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/granian_server.py +0 -0
  22. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/helpers.py +0 -0
  23. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/__init__.py +0 -0
  24. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/base.py +0 -0
  25. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/cors_instrument.py +0 -0
  26. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/health_checks_instrument.py +0 -0
  27. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/instrument_box.py +0 -0
  28. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/logging_instrument.py +0 -0
  29. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/prometheus_instrument.py +0 -0
  30. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/pyroscope_instrument.py +0 -0
  31. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments/swagger_instrument.py +0 -0
  32. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/instruments_setupper.py +0 -0
  33. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/middlewares/__init__.py +0 -0
  34. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/middlewares/fastapi.py +0 -0
  35. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/middlewares/fastmcp.py +0 -0
  36. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/middlewares/litestar.py +0 -0
  37. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/py.typed +0 -0
  38. {microbootstrap-1.5.2 → microbootstrap-1.5.4}/microbootstrap/settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: microbootstrap
3
- Version: 1.5.2
3
+ Version: 1.5.4
4
4
  Summary: Package for bootstrapping new micro-services
5
5
  Keywords: python,microservice,bootstrap,opentelemetry,logging,error-tracing,litestar,fastapi,fastmcp,mcp
6
6
  Author: community-of-python
@@ -528,6 +528,22 @@ Parameters description:
528
528
 
529
529
  These settings are subsequently passed to [opentelemetry](https://opentelemetry.io/), finalizing your Opentelemetry integration.
530
530
 
531
+ If an authoritative baggage value becomes available only after the current span has started, pass the same mapping to
532
+ `opentelemetry_baggage_scope` to also materialize that value on the already-started span:
533
+
534
+ ```python
535
+ from microbootstrap import opentelemetry_baggage_scope
536
+
537
+
538
+ with opentelemetry_baggage_scope(
539
+ {"conversation_id": conversation_id},
540
+ current_span_attributes=settings.opentelemetry_baggage_span_attributes,
541
+ ):
542
+ ...
543
+ ```
544
+
545
+ Only non-`None` baggage values supplied to the scope and present in the mapping are added to a recording current span.
546
+
531
547
  #### FastStream
532
548
 
533
549
  For FastStream you also should pass `opentelemetry_middleware_cls` - OpenTelemetry middleware for your broker
@@ -463,6 +463,22 @@ Parameters description:
463
463
 
464
464
  These settings are subsequently passed to [opentelemetry](https://opentelemetry.io/), finalizing your Opentelemetry integration.
465
465
 
466
+ If an authoritative baggage value becomes available only after the current span has started, pass the same mapping to
467
+ `opentelemetry_baggage_scope` to also materialize that value on the already-started span:
468
+
469
+ ```python
470
+ from microbootstrap import opentelemetry_baggage_scope
471
+
472
+
473
+ with opentelemetry_baggage_scope(
474
+ {"conversation_id": conversation_id},
475
+ current_span_attributes=settings.opentelemetry_baggage_span_attributes,
476
+ ):
477
+ ...
478
+ ```
479
+
480
+ Only non-`None` baggage values supplied to the scope and present in the mapping are added to a recording current span.
481
+
466
482
  #### FastStream
467
483
 
468
484
  For FastStream you also should pass `opentelemetry_middleware_cls` - OpenTelemetry middleware for your broker
@@ -17,10 +17,11 @@ from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
17
17
  from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
18
18
  from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor
19
19
  from opentelemetry.semconv.resource import ResourceAttributes
20
- from opentelemetry.trace import SpanKind, format_span_id, set_tracer_provider
20
+ from opentelemetry.trace import SpanKind, format_span_id, get_current_span, set_tracer_provider
21
21
  from opentelemetry.util._importlib_metadata import entry_points
22
22
 
23
23
  from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
24
+ from microbootstrap.instruments.sentry_instrument import snapshot_sentry_opentelemetry_baggage
24
25
 
25
26
 
26
27
  LOGGER_OBJ: typing.Final = structlog.get_logger(__name__)
@@ -43,6 +44,8 @@ OpentelemetryConfigT = typing.TypeVar("OpentelemetryConfigT", bound="Opentelemet
43
44
  @contextlib.contextmanager
44
45
  def opentelemetry_baggage_scope(
45
46
  baggage_values: typing.Mapping[str, object | None],
47
+ *,
48
+ current_span_attributes: typing.Mapping[str, str] | None = None,
46
49
  ) -> typing.Iterator[None]:
47
50
  baggage_context = context.get_current()
48
51
  for key, value in baggage_values.items():
@@ -54,7 +57,15 @@ def opentelemetry_baggage_scope(
54
57
 
55
58
  token: typing.Final = context.attach(baggage_context)
56
59
  try:
60
+ if current_span_attributes and (current_span := get_current_span()).is_recording():
61
+ for baggage_key, span_attribute in current_span_attributes.items():
62
+ if baggage_key in baggage_values and (value := baggage_values[baggage_key]) is not None:
63
+ current_span.set_attribute(span_attribute, str(value))
57
64
  yield
65
+ except Exception as exc:
66
+ with contextlib.suppress(Exception):
67
+ snapshot_sentry_opentelemetry_baggage(exc)
68
+ raise
58
69
  finally:
59
70
  context.detach(token)
60
71
 
@@ -3,13 +3,14 @@ import contextlib
3
3
  import functools
4
4
  import typing
5
5
  import urllib.parse
6
+ from collections.abc import Mapping
6
7
 
7
8
  import orjson
8
9
  import pydantic
9
10
  import sentry_sdk
10
11
  from opentelemetry import baggage
11
12
  from sentry_sdk import _types as sentry_types
12
- from sentry_sdk.integrations import Integration # noqa: TC002
13
+ from sentry_sdk.integrations import Integration
13
14
 
14
15
  from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
15
16
 
@@ -66,6 +67,71 @@ def enrich_sentry_event_from_structlog_log(event: sentry_types.Event, _hint: sen
66
67
 
67
68
  SENTRY_EXTRA_OTEL_TRACE_ID_KEY: typing.Final = "otelTraceID"
68
69
  SENTRY_EXTRA_OTEL_TRACE_URL_KEY: typing.Final = "otelTraceURL"
70
+ SENTRY_OTEL_BAGGAGE_SNAPSHOT_ATTRIBUTE: typing.Final = "__microbootstrap_sentry_opentelemetry_baggage__"
71
+
72
+
73
+ @typing.final
74
+ class SentryOpentelemetryBaggageIntegration(Integration):
75
+ identifier = "microbootstrap_opentelemetry_baggage"
76
+
77
+ def __init__(self, baggage_keys: set[str], baggage_url_templates: dict[str, str]) -> None:
78
+ self.baggage_keys: typing.Final = frozenset(baggage_keys)
79
+ self.baggage_url_templates: typing.Final = dict(baggage_url_templates)
80
+
81
+ @staticmethod
82
+ def setup_once() -> None:
83
+ pass
84
+
85
+
86
+ def snapshot_sentry_opentelemetry_baggage(exception: BaseException) -> None:
87
+ integration = sentry_sdk.get_client().get_integration(SentryOpentelemetryBaggageIntegration)
88
+ if not isinstance(integration, SentryOpentelemetryBaggageIntegration) or hasattr(
89
+ exception,
90
+ SENTRY_OTEL_BAGGAGE_SNAPSHOT_ATTRIBUTE,
91
+ ):
92
+ return
93
+
94
+ configured_keys: typing.Final = integration.baggage_keys.union(integration.baggage_url_templates)
95
+ snapshot: typing.Final = {key: baggage.get_baggage(key) for key in configured_keys}
96
+ with contextlib.suppress(AttributeError, TypeError):
97
+ setattr(exception, SENTRY_OTEL_BAGGAGE_SNAPSHOT_ATTRIBUTE, snapshot)
98
+
99
+
100
+ def _find_sentry_opentelemetry_baggage_snapshot(hint: sentry_types.Hint) -> Mapping[str, object | None] | None:
101
+ if not isinstance(hint, Mapping):
102
+ return None
103
+
104
+ exc_info: typing.Final = hint.get("exc_info")
105
+ if not isinstance(exc_info, tuple):
106
+ return None
107
+ try:
108
+ _, exception_value, _ = exc_info
109
+ except ValueError:
110
+ return None
111
+ if not isinstance(exception_value, BaseException):
112
+ return None
113
+
114
+ exceptions_to_visit: list[BaseException] = [exception_value]
115
+ visited_exceptions: set[int] = set()
116
+ while exceptions_to_visit:
117
+ exception = exceptions_to_visit.pop()
118
+ if id(exception) in visited_exceptions:
119
+ continue
120
+ visited_exceptions.add(id(exception))
121
+ exception_snapshot = getattr(exception, SENTRY_OTEL_BAGGAGE_SNAPSHOT_ATTRIBUTE, None)
122
+ if isinstance(exception_snapshot, Mapping):
123
+ return exception_snapshot
124
+
125
+ nested_exceptions = getattr(exception, "exceptions", ())
126
+ if isinstance(nested_exceptions, tuple):
127
+ exceptions_to_visit.extend(
128
+ reversed([nested for nested in nested_exceptions if isinstance(nested, BaseException)])
129
+ )
130
+ if chained_exception := exception.__cause__ or (
131
+ None if exception.__suppress_context__ else exception.__context__
132
+ ):
133
+ exceptions_to_visit.append(chained_exception)
134
+ return None
69
135
 
70
136
 
71
137
  def add_trace_url_to_event(
@@ -80,27 +146,30 @@ def enrich_sentry_event_from_opentelemetry_baggage(
80
146
  baggage_keys: set[str],
81
147
  baggage_url_templates: dict[str, str],
82
148
  event: sentry_types.Event,
83
- _hint: sentry_types.Hint,
149
+ hint: sentry_types.Hint,
84
150
  ) -> sentry_types.Event:
85
- baggage_values = {
86
- key: value
87
- for key in baggage_keys.union(baggage_url_templates)
88
- if (value := baggage.get_baggage(key)) is not None
89
- }
90
- if not baggage_values:
91
- return event
92
-
93
- if tag_values := {key: str(baggage_values[key]) for key in baggage_keys if key in baggage_values}:
94
- event.setdefault("tags", {}).update(tag_values)
151
+ configured_keys: typing.Final = baggage_keys.union(baggage_url_templates)
152
+ snapshot: typing.Final = _find_sentry_opentelemetry_baggage_snapshot(hint)
153
+ baggage_values: typing.Final = (
154
+ snapshot if snapshot is not None else {key: baggage.get_baggage(key) for key in configured_keys}
155
+ )
156
+
157
+ for key in baggage_keys:
158
+ if (value := baggage_values.get(key)) is not None:
159
+ event.setdefault("tags", {})[key] = str(value)
160
+ elif tags := event.get("tags"):
161
+ tags.pop(key, None)
95
162
 
96
163
  for key, url_template in baggage_url_templates.items():
97
164
  placeholder = f"{{{key}}}"
98
- if key not in baggage_values or placeholder not in url_template:
99
- continue
100
- event.setdefault("extra", {})[f"{key}_url"] = url_template.replace(
101
- placeholder,
102
- urllib.parse.quote(str(baggage_values[key]), safe=""),
103
- )
165
+ extra_key = f"{key}_url"
166
+ if (value := baggage_values.get(key)) is not None and placeholder in url_template:
167
+ event.setdefault("extra", {})[extra_key] = url_template.replace(
168
+ placeholder,
169
+ urllib.parse.quote(str(value), safe=""),
170
+ )
171
+ elif extra := event.get("extra"):
172
+ extra.pop(extra_key, None)
104
173
  return event
105
174
 
106
175
 
@@ -126,6 +195,17 @@ class SentryInstrument(Instrument[SentryConfig]):
126
195
  return bool(self.instrument_config.sentry_dsn)
127
196
 
128
197
  def bootstrap(self) -> None:
198
+ baggage_integration: typing.Final = (
199
+ SentryOpentelemetryBaggageIntegration(
200
+ self.instrument_config.sentry_opentelemetry_baggage_keys,
201
+ self.instrument_config.sentry_opentelemetry_baggage_url_templates,
202
+ )
203
+ if (
204
+ self.instrument_config.sentry_opentelemetry_baggage_keys
205
+ or self.instrument_config.sentry_opentelemetry_baggage_url_templates
206
+ )
207
+ else None
208
+ )
129
209
  sentry_sdk.init(
130
210
  dsn=self.instrument_config.sentry_dsn,
131
211
  sample_rate=self.instrument_config.sentry_sample_rate,
@@ -153,7 +233,10 @@ class SentryInstrument(Instrument[SentryConfig]):
153
233
  else None,
154
234
  self.instrument_config.sentry_before_send,
155
235
  ),
156
- integrations=self.instrument_config.sentry_integrations,
236
+ integrations=[
237
+ *self.instrument_config.sentry_integrations,
238
+ *([baggage_integration] if baggage_integration else []),
239
+ ],
157
240
  **self.instrument_config.sentry_additional_params,
158
241
  )
159
242
  if self.instrument_config.sentry_tags:
@@ -58,7 +58,7 @@ dependencies = [
58
58
  "opentelemetry-instrumentation-asgi>=0.46b0",
59
59
  "orjson>=3.10.18",
60
60
  ]
61
- version = "1.5.2"
61
+ version = "1.5.4"
62
62
 
63
63
  [[project.authors]]
64
64
  name = "community-of-python"
@@ -58,7 +58,7 @@ dependencies = [
58
58
  "opentelemetry-instrumentation-asgi>=0.46b0",
59
59
  "orjson>=3.10.18",
60
60
  ]
61
- version = "1.5.2"
61
+ version = "1.5.4"
62
62
  authors = [{ name = "community-of-python" }]
63
63
 
64
64
  [project.optional-dependencies]