microbootstrap 1.6.0__tar.gz → 1.7.0__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 (41) hide show
  1. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/PKG-INFO +47 -4
  2. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/README.md +46 -3
  3. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/fastmcp.py +72 -3
  4. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/litestar.py +14 -14
  5. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/opentelemetry_instrument.py +26 -10
  6. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/settings.py +1 -0
  7. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/pyproject.toml +2 -1
  8. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/pyproject.toml.orig +2 -1
  9. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/__init__.py +0 -0
  10. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/__init__.py +0 -0
  11. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/base.py +0 -0
  12. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/fastapi.py +0 -0
  13. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/faststream.py +0 -0
  14. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/config/__init__.py +0 -0
  15. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/config/fastapi.py +0 -0
  16. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/config/fastmcp.py +0 -0
  17. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/config/faststream.py +0 -0
  18. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/config/litestar.py +0 -0
  19. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/console_writer.py +0 -0
  20. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/exceptions.py +0 -0
  21. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/granian_server.py +0 -0
  22. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/helpers.py +0 -0
  23. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/__init__.py +0 -0
  24. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/base.py +0 -0
  25. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/cors_instrument.py +0 -0
  26. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/health_checks_instrument.py +0 -0
  27. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/instrument_box.py +0 -0
  28. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/logging_instrument.py +0 -0
  29. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/openapi_security_schemes.py +0 -0
  30. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/openapi_version_docs.py +0 -0
  31. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/prometheus_instrument.py +0 -0
  32. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/pyroscope_instrument.py +0 -0
  33. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/sentry_instrument.py +0 -0
  34. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments/swagger_instrument.py +0 -0
  35. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/instruments_setupper.py +0 -0
  36. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/middlewares/__init__.py +0 -0
  37. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/middlewares/fastapi.py +0 -0
  38. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/middlewares/fastmcp.py +0 -0
  39. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/middlewares/faststream.py +0 -0
  40. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/middlewares/litestar.py +0 -0
  41. {microbootstrap-1.6.0 → microbootstrap-1.7.0}/microbootstrap/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: microbootstrap
3
- Version: 1.6.0
3
+ Version: 1.7.0
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
@@ -505,7 +505,7 @@ class YourSettings(BaseServiceSettings):
505
505
  opentelemetry_namespace: str | None = None
506
506
  opentelemetry_insecure: bool = True
507
507
  opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = []
508
- opentelemetry_exclude_urls: list[str] = []
508
+ opentelemetry_exclude_urls: list[str] = ["/metrics"]
509
509
  opentelemetry_baggage_span_attributes: dict[str, str] = {}
510
510
 
511
511
  ... # Other settings here
@@ -521,9 +521,9 @@ Parameters description:
521
521
  - `opentelemetry_insecure` - is opentelemetry connection secure.
522
522
  - `opentelemetry_container_name` - will be passed to the `Resource`.
523
523
  - `opentelemetry_instrumentors` - a list of extra instrumentors.
524
- - `opentelemetry_exclude_urls` - list of ignored urls.
524
+ - `opentelemetry_exclude_urls` - list of url regexes that produce no server spans (`["/metrics"]` by default). For Litestar they are combined with `OTEL_PYTHON_LITESTAR_EXCLUDED_URLS` (or `OTEL_PYTHON_EXCLUDED_URLS`), for FastMCP with `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS` (or `OTEL_PYTHON_EXCLUDED_URLS`).
525
525
  - `opentelemetry_log_traces` - traces will be logged to stdout.
526
- - `opentelemetry_generate_health_check_spans` - generate spans for health check handlers if `True`
526
+ - `opentelemetry_generate_health_check_spans` - generate spans for health check handlers if `True`; if `False`, `health_checks_path` is added to the excluded urls.
527
527
  - `opentelemetry_baggage_span_attributes` - maps allowed baggage keys to attributes added to local server and consumer spans.
528
528
 
529
529
  These settings are subsequently passed to [opentelemetry](https://opentelemetry.io/), finalizing your Opentelemetry integration.
@@ -559,6 +559,49 @@ class YourSettings(FastStreamSettings):
559
559
  ...
560
560
  ```
561
561
 
562
+ #### FastMCP
563
+
564
+ `FastMcpSettings` include all OpenTelemetry settings, so tracing is enabled with `opentelemetry_endpoint` alone:
565
+
566
+ ```python
567
+ from fastmcp import FastMCP
568
+ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
569
+
570
+ from microbootstrap import FastMcpSettings
571
+ from microbootstrap.bootstrappers.fastmcp import FastMcpBootstrapper
572
+ from microbootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrumentor
573
+
574
+
575
+ class YourSettings(FastMcpSettings):
576
+ opentelemetry_endpoint: str | None = "otel-collector:4317"
577
+ opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = [OpenTelemetryInstrumentor(HTTPXClientInstrumentor())]
578
+
579
+
580
+ application: FastMCP = FastMcpBootstrapper(YourSettings()).bootstrap()
581
+ http_application = application.http_app(path="/mcp")
582
+ ```
583
+
584
+ FastMCP creates its ASGI application only when `http_app()` is called (directly or by `application.run(transport="http")`),
585
+ so `SERVER` spans are added to every application returned by `http_app()`. Each request to it is wrapped in
586
+ [`OpenTelemetryMiddleware`](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/asgi/asgi.html)
587
+ and produces a span named like `POST /mcp` or `GET /health/` with the `http.route` attribute and the response status code.
588
+ Requests to unknown paths produce spans named after the HTTP method only, without `http.route`.
589
+
590
+ - `opentelemetry_endpoint` - OTLP endpoint for exported traces.
591
+ - `opentelemetry_instrumentors` - extra instrumentors, e.g. for HTTP clients used by your tools.
592
+ - `opentelemetry_exclude_urls` - urls without spans, `["/metrics"]` by default. Combined with `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`.
593
+ - `opentelemetry_generate_health_check_spans` - set to `False` to skip spans for `health_checks_path`.
594
+ - The status code attribute name depends on `OTEL_SEMCONV_STABILITY_OPT_IN`: `http.status_code` when unset,
595
+ `http.response.status_code` for `http`, both for `http/dup`.
596
+
597
+ Applications already instrumented by `StarletteInstrumentor` are left as is, so the request is never traced twice.
598
+ To post-process every created ASGI application yourself, use `add_http_application_postprocessor` on the bootstrapped application:
599
+
600
+ ```python
601
+ application = FastMcpBootstrapper(settings).bootstrap()
602
+ application.add_http_application_postprocessor(lambda http_application: http_application)
603
+ ```
604
+
562
605
  ### [Pyroscope](https://pyroscope.io)
563
606
 
564
607
  To integrate Pyroscope, specify the `pyroscope_endpoint`.
@@ -440,7 +440,7 @@ class YourSettings(BaseServiceSettings):
440
440
  opentelemetry_namespace: str | None = None
441
441
  opentelemetry_insecure: bool = True
442
442
  opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = []
443
- opentelemetry_exclude_urls: list[str] = []
443
+ opentelemetry_exclude_urls: list[str] = ["/metrics"]
444
444
  opentelemetry_baggage_span_attributes: dict[str, str] = {}
445
445
 
446
446
  ... # Other settings here
@@ -456,9 +456,9 @@ Parameters description:
456
456
  - `opentelemetry_insecure` - is opentelemetry connection secure.
457
457
  - `opentelemetry_container_name` - will be passed to the `Resource`.
458
458
  - `opentelemetry_instrumentors` - a list of extra instrumentors.
459
- - `opentelemetry_exclude_urls` - list of ignored urls.
459
+ - `opentelemetry_exclude_urls` - list of url regexes that produce no server spans (`["/metrics"]` by default). For Litestar they are combined with `OTEL_PYTHON_LITESTAR_EXCLUDED_URLS` (or `OTEL_PYTHON_EXCLUDED_URLS`), for FastMCP with `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS` (or `OTEL_PYTHON_EXCLUDED_URLS`).
460
460
  - `opentelemetry_log_traces` - traces will be logged to stdout.
461
- - `opentelemetry_generate_health_check_spans` - generate spans for health check handlers if `True`
461
+ - `opentelemetry_generate_health_check_spans` - generate spans for health check handlers if `True`; if `False`, `health_checks_path` is added to the excluded urls.
462
462
  - `opentelemetry_baggage_span_attributes` - maps allowed baggage keys to attributes added to local server and consumer spans.
463
463
 
464
464
  These settings are subsequently passed to [opentelemetry](https://opentelemetry.io/), finalizing your Opentelemetry integration.
@@ -494,6 +494,49 @@ class YourSettings(FastStreamSettings):
494
494
  ...
495
495
  ```
496
496
 
497
+ #### FastMCP
498
+
499
+ `FastMcpSettings` include all OpenTelemetry settings, so tracing is enabled with `opentelemetry_endpoint` alone:
500
+
501
+ ```python
502
+ from fastmcp import FastMCP
503
+ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
504
+
505
+ from microbootstrap import FastMcpSettings
506
+ from microbootstrap.bootstrappers.fastmcp import FastMcpBootstrapper
507
+ from microbootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrumentor
508
+
509
+
510
+ class YourSettings(FastMcpSettings):
511
+ opentelemetry_endpoint: str | None = "otel-collector:4317"
512
+ opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = [OpenTelemetryInstrumentor(HTTPXClientInstrumentor())]
513
+
514
+
515
+ application: FastMCP = FastMcpBootstrapper(YourSettings()).bootstrap()
516
+ http_application = application.http_app(path="/mcp")
517
+ ```
518
+
519
+ FastMCP creates its ASGI application only when `http_app()` is called (directly or by `application.run(transport="http")`),
520
+ so `SERVER` spans are added to every application returned by `http_app()`. Each request to it is wrapped in
521
+ [`OpenTelemetryMiddleware`](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/asgi/asgi.html)
522
+ and produces a span named like `POST /mcp` or `GET /health/` with the `http.route` attribute and the response status code.
523
+ Requests to unknown paths produce spans named after the HTTP method only, without `http.route`.
524
+
525
+ - `opentelemetry_endpoint` - OTLP endpoint for exported traces.
526
+ - `opentelemetry_instrumentors` - extra instrumentors, e.g. for HTTP clients used by your tools.
527
+ - `opentelemetry_exclude_urls` - urls without spans, `["/metrics"]` by default. Combined with `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`.
528
+ - `opentelemetry_generate_health_check_spans` - set to `False` to skip spans for `health_checks_path`.
529
+ - The status code attribute name depends on `OTEL_SEMCONV_STABILITY_OPT_IN`: `http.status_code` when unset,
530
+ `http.response.status_code` for `http`, both for `http/dup`.
531
+
532
+ Applications already instrumented by `StarletteInstrumentor` are left as is, so the request is never traced twice.
533
+ To post-process every created ASGI application yourself, use `add_http_application_postprocessor` on the bootstrapped application:
534
+
535
+ ```python
536
+ application = FastMcpBootstrapper(settings).bootstrap()
537
+ application.add_http_application_postprocessor(lambda http_application: http_application)
538
+ ```
539
+
497
540
  ### [Pyroscope](https://pyroscope.io)
498
541
 
499
542
  To integrate Pyroscope, specify the `pyroscope_endpoint`.
@@ -4,10 +4,15 @@ import typing
4
4
  import prometheus_client
5
5
  import typing_extensions
6
6
  from fastmcp import FastMCP
7
+ from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
8
+ from opentelemetry.util.http import ExcludeList, get_excluded_urls
9
+ from starlette.applications import Starlette
7
10
  from starlette.responses import JSONResponse, Response
11
+ from starlette.routing import Match, Mount, Route
8
12
 
9
13
  from microbootstrap.bootstrappers.base import ApplicationBootstrapper
10
14
  from microbootstrap.config.fastmcp import FastMcpConfig
15
+ from microbootstrap.instruments import opentelemetry_instrument
11
16
  from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument, HealthCheckTypedDict
12
17
  from microbootstrap.instruments.logging_instrument import LoggingInstrument
13
18
  from microbootstrap.instruments.prometheus_instrument import FastMcpPrometheusConfig, PrometheusInstrument
@@ -18,16 +23,46 @@ from microbootstrap.settings import FastMcpSettings
18
23
 
19
24
 
20
25
  if typing.TYPE_CHECKING:
26
+ from fastmcp.server.http import StarletteWithLifespan
21
27
  from starlette.requests import Request
28
+ from starlette.types import Scope
29
+
30
+
31
+ StarletteT = typing.TypeVar("StarletteT", bound=Starlette)
22
32
 
23
33
 
24
34
  class KwargsFastMCP(FastMCP[typing.Any]):
25
35
  def __init__(self, **kwargs: typing.Any) -> None: # noqa: ANN401
26
36
  super().__init__(**kwargs)
37
+ self.http_application_postprocessors: list[typing.Callable[[StarletteWithLifespan], StarletteWithLifespan]] = []
38
+
39
+ def add_http_application_postprocessor(
40
+ self, postprocessor: typing.Callable[[StarletteWithLifespan], StarletteWithLifespan]
41
+ ) -> None:
42
+ self.http_application_postprocessors.append(postprocessor)
43
+
44
+ def http_app(self, *args: typing.Any, **kwargs: typing.Any) -> StarletteWithLifespan: # noqa: ANN401
45
+ # ASGI application is created by the user after bootstrap, so instruments subscribe to its creation
46
+ http_application = super().http_app(*args, **kwargs)
47
+ for postprocessor in self.http_application_postprocessors:
48
+ http_application = postprocessor(http_application)
49
+ return http_application
50
+
51
+
52
+ def build_fastmcp_route_details_from_scope(
53
+ scope: Scope,
54
+ routes: typing.Iterable[typing.Any],
55
+ ) -> tuple[str, dict[str, str]]:
56
+ method: typing.Final = str(scope.get("method", "HTTP")).strip()
57
+ for route in routes:
58
+ if isinstance(route, (Route, Mount)) and route.matches(scope)[0] == Match.FULL:
59
+ return opentelemetry_instrument.build_span_name(method, route.path), {"http.route": route.path}
60
+ # Unmatched paths get no `http.route` to keep its cardinality low
61
+ return method, {}
27
62
 
28
63
 
29
64
  class FastMcpBootstrapper(
30
- ApplicationBootstrapper[FastMcpSettings, FastMCP[typing.Any], FastMcpConfig],
65
+ ApplicationBootstrapper[FastMcpSettings, KwargsFastMCP, FastMcpConfig],
31
66
  ):
32
67
  application_config = FastMcpConfig()
33
68
  application_type = KwargsFastMCP
@@ -41,8 +76,8 @@ class FastMcpBootstrapper(
41
76
 
42
77
  def bootstrap_before_instruments_after_app_created(
43
78
  self,
44
- application: FastMCP[typing.Any],
45
- ) -> FastMCP[typing.Any]:
79
+ application: KwargsFastMCP,
80
+ ) -> KwargsFastMCP:
46
81
  self.console_writer.print_bootstrap_table()
47
82
  return application
48
83
 
@@ -51,6 +86,40 @@ FastMcpBootstrapper.use_instrument()(SentryInstrument)
51
86
  FastMcpBootstrapper.use_instrument()(PyroscopeInstrument)
52
87
 
53
88
 
89
+ @FastMcpBootstrapper.use_instrument()
90
+ class FastMcpOpentelemetryInstrument(
91
+ opentelemetry_instrument.BaseOpentelemetryInstrument[opentelemetry_instrument.OpentelemetryConfig]
92
+ ):
93
+ def bootstrap_after(self, application: FastMCP[typing.Any]) -> FastMCP[typing.Any]: # type: ignore[override]
94
+ if isinstance(application, KwargsFastMCP):
95
+ application.add_http_application_postprocessor(self.__instrument_http_app)
96
+ return application
97
+
98
+ def __instrument_http_app(self, http_application: StarletteT) -> StarletteT:
99
+ # `StarletteInstrumentor` marks applications the same way, so each application is instrumented once
100
+ if getattr(http_application, "_is_instrumented_by_opentelemetry", False):
101
+ return http_application
102
+
103
+ def build_route_details(scope: Scope) -> tuple[str, dict[str, str]]:
104
+ return build_fastmcp_route_details_from_scope(scope, http_application.routes)
105
+
106
+ http_application.add_middleware(
107
+ OpenTelemetryMiddleware,
108
+ tracer_provider=self.tracer_provider,
109
+ default_span_details=build_route_details,
110
+ excluded_urls=opentelemetry_instrument.CombinedExcludeList(
111
+ ExcludeList(self.define_exclude_urls()),
112
+ get_excluded_urls("STARLETTE"),
113
+ ),
114
+ )
115
+ http_application._is_instrumented_by_opentelemetry = True # type: ignore[attr-defined] # noqa: SLF001
116
+ return http_application
117
+
118
+ @classmethod
119
+ def get_config_type(cls) -> type[opentelemetry_instrument.OpentelemetryConfig]:
120
+ return opentelemetry_instrument.OpentelemetryConfig
121
+
122
+
54
123
  @FastMcpBootstrapper.use_instrument()
55
124
  class FastMcpLoggingInstrument(LoggingInstrument):
56
125
  def bootstrap_after(self, application: FastMCP[typing.Any]) -> FastMCP[typing.Any]: # type: ignore[override]
@@ -15,11 +15,12 @@ from litestar.openapi.plugins import SwaggerRenderPlugin
15
15
  from litestar.types.asgi_types import ASGIApp, Scope
16
16
  from litestar_offline_docs import generate_static_files_config
17
17
  from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
18
- from opentelemetry.util.http import get_excluded_urls
18
+ from opentelemetry.util.http import ExcludeList, get_excluded_urls
19
19
  from sentry_sdk.integrations.litestar import LitestarIntegration
20
20
 
21
21
  from microbootstrap.bootstrappers.base import ApplicationBootstrapper
22
22
  from microbootstrap.config.litestar import LitestarConfig
23
+ from microbootstrap.instruments import opentelemetry_instrument
23
24
  from microbootstrap.instruments.cors_instrument import CorsInstrument
24
25
  from microbootstrap.instruments.health_checks_instrument import (
25
26
  HealthChecksInstrument,
@@ -35,7 +36,6 @@ from microbootstrap.instruments.openapi_security_schemes import (
35
36
  serialize_security_schemes,
36
37
  )
37
38
  from microbootstrap.instruments.openapi_version_docs import SUPPORTED_HTTP_METHODS
38
- from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
39
39
  from microbootstrap.instruments.prometheus_instrument import (
40
40
  LitestarPrometheusConfig,
41
41
  PrometheusInstrument,
@@ -239,12 +239,6 @@ class LitestarCorsInstrument(CorsInstrument):
239
239
  LitestarBootstrapper.use_instrument()(PyroscopeInstrument)
240
240
 
241
241
 
242
- def build_span_name(method: str, route: str) -> str:
243
- if not route:
244
- return method
245
- return f"{method} {route}"
246
-
247
-
248
242
  def build_litestar_route_details_from_scope(
249
243
  scope: Scope,
250
244
  ) -> tuple[str, dict[str, str]]:
@@ -261,18 +255,23 @@ def build_litestar_route_details_from_scope(
261
255
  method: typing.Final = str(scope.get("method", "HTTP")).strip()
262
256
  if path_template is not None:
263
257
  path_template_stripped: typing.Final = path_template.strip()
264
- return build_span_name(method, path_template_stripped), {"http.route": path_template_stripped}
258
+ span_name: typing.Final = opentelemetry_instrument.build_span_name(method, path_template_stripped)
259
+ return span_name, {"http.route": path_template_stripped}
265
260
 
266
261
  path: typing.Final = scope.get("path")
267
262
  if path is not None:
268
263
  path_stripped: typing.Final = path.strip()
269
- return build_span_name(method, path_stripped), {"http.route": path_stripped}
264
+ return opentelemetry_instrument.build_span_name(method, path_stripped), {"http.route": path_stripped}
270
265
  return method, {}
271
266
 
272
267
 
273
268
  class LitestarOpenTelemetryInstrumentationMiddleware(ASGIMiddleware):
274
- def __init__(self, config: OpenTelemetryConfig) -> None:
269
+ def __init__(self, config: OpenTelemetryConfig, exclude_urls: typing.Sequence[str] = ()) -> None:
275
270
  self.config = config
271
+ self.excluded_urls = opentelemetry_instrument.CombinedExcludeList(
272
+ ExcludeList(exclude_urls),
273
+ get_excluded_urls(self.config.exclude_urls_env_key),
274
+ )
276
275
 
277
276
  def create_open_telemetry_middleware(self, app: ASGIApp) -> OpenTelemetryMiddleware:
278
277
  return OpenTelemetryMiddleware(
@@ -280,7 +279,7 @@ class LitestarOpenTelemetryInstrumentationMiddleware(ASGIMiddleware):
280
279
  client_request_hook=self.config.client_request_hook_handler,
281
280
  client_response_hook=self.config.client_response_hook_handler,
282
281
  default_span_details=build_litestar_route_details_from_scope,
283
- excluded_urls=get_excluded_urls(self.config.exclude_urls_env_key),
282
+ excluded_urls=self.excluded_urls,
284
283
  meter=self.config.meter,
285
284
  meter_provider=self.config.meter_provider,
286
285
  server_request_hook=self.config.server_request_hook_handler,
@@ -292,7 +291,7 @@ class LitestarOpenTelemetryInstrumentationMiddleware(ASGIMiddleware):
292
291
 
293
292
 
294
293
  @LitestarBootstrapper.use_instrument()
295
- class LitestarOpentelemetryInstrument(OpentelemetryInstrument):
294
+ class LitestarOpentelemetryInstrument(opentelemetry_instrument.OpentelemetryInstrument):
296
295
  def bootstrap_before(self) -> dict[str, typing.Any]:
297
296
  return {
298
297
  "middleware": [
@@ -300,7 +299,8 @@ class LitestarOpentelemetryInstrument(OpentelemetryInstrument):
300
299
  LitestarOpentelemetryConfig(
301
300
  tracer_provider=self.tracer_provider,
302
301
  middleware_class=LitestarOpenTelemetryInstrumentationMiddleware, # type: ignore[arg-type]
303
- )
302
+ ),
303
+ exclude_urls=self.define_exclude_urls(),
304
304
  )
305
305
  ]
306
306
  }
@@ -19,6 +19,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExport
19
19
  from opentelemetry.semconv.resource import ResourceAttributes
20
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
+ from opentelemetry.util.http import ExcludeList
22
23
 
23
24
  from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
24
25
  from microbootstrap.instruments.sentry_instrument import snapshot_sentry_opentelemetry_baggage
@@ -70,6 +71,21 @@ def opentelemetry_baggage_scope(
70
71
  context.detach(token)
71
72
 
72
73
 
74
+ def build_span_name(method: str, route: str) -> str:
75
+ if not route:
76
+ return method
77
+ return f"{method} {route}"
78
+
79
+
80
+ class CombinedExcludeList(ExcludeList):
81
+ def __init__(self, *exclude_lists: ExcludeList) -> None:
82
+ super().__init__([])
83
+ self.exclude_lists = exclude_lists
84
+
85
+ def url_disabled(self, url: str) -> bool:
86
+ return any(one_exclude_list.url_disabled(url) for one_exclude_list in self.exclude_lists)
87
+
88
+
73
89
  @dataclasses.dataclass()
74
90
  class OpenTelemetryInstrumentor:
75
91
  instrumentor: BaseInstrumentor
@@ -167,6 +183,16 @@ class BaseOpentelemetryInstrument(Instrument[OpentelemetryConfigT]):
167
183
  LOGGER_OBJ.debug("Instrumenting failed", entry_point_name=entry_point.name)
168
184
  raise
169
185
 
186
+ def define_exclude_urls(self) -> list[str]:
187
+ exclude_urls: typing.Final = [*self.instrument_config.opentelemetry_exclude_urls]
188
+ if (
189
+ not self.instrument_config.opentelemetry_generate_health_check_spans
190
+ and self.instrument_config.health_checks_path
191
+ and self.instrument_config.health_checks_path not in exclude_urls
192
+ ):
193
+ exclude_urls.append(self.instrument_config.health_checks_path)
194
+ return exclude_urls
195
+
170
196
  def is_ready(self) -> bool:
171
197
  return (
172
198
  bool(self.instrument_config.opentelemetry_endpoint)
@@ -222,16 +248,6 @@ class BaseOpentelemetryInstrument(Instrument[OpentelemetryConfigT]):
222
248
 
223
249
 
224
250
  class OpentelemetryInstrument(BaseOpentelemetryInstrument[OpentelemetryConfig]):
225
- def define_exclude_urls(self) -> list[str]:
226
- exclude_urls: typing.Final = [*self.instrument_config.opentelemetry_exclude_urls]
227
- if (
228
- not self.instrument_config.opentelemetry_generate_health_check_spans
229
- and self.instrument_config.health_checks_path
230
- and self.instrument_config.health_checks_path not in exclude_urls
231
- ):
232
- exclude_urls.append(self.instrument_config.health_checks_path)
233
- return exclude_urls
234
-
235
251
  @classmethod
236
252
  def get_config_type(cls) -> type[OpentelemetryConfig]:
237
253
  return OpentelemetryConfig
@@ -107,6 +107,7 @@ class FastMcpSettings( # type: ignore[misc]
107
107
  BaseServiceSettings,
108
108
  ServerConfig,
109
109
  LoggingConfig,
110
+ OpentelemetryConfig,
110
111
  SentryConfig,
111
112
  FastMcpPrometheusConfig,
112
113
  HealthChecksConfig,
@@ -58,7 +58,7 @@ dependencies = [
58
58
  "opentelemetry-instrumentation-asgi>=0.46b0",
59
59
  "orjson>=3.10.18",
60
60
  ]
61
- version = "1.6.0"
61
+ version = "1.7.0"
62
62
 
63
63
  [[project.authors]]
64
64
  name = "community-of-python"
@@ -90,6 +90,7 @@ dev = [
90
90
  "anyio>=4.8.0",
91
91
  "httpx>=0.28.1",
92
92
  "mypy>=1.14.1",
93
+ "opentelemetry-instrumentation-starlette>=0.54b1",
93
94
  "pre-commit>=4.0.1",
94
95
  "pytest>=8.3.4",
95
96
  "pytest-cov>=6.0.0",
@@ -58,7 +58,7 @@ dependencies = [
58
58
  "opentelemetry-instrumentation-asgi>=0.46b0",
59
59
  "orjson>=3.10.18",
60
60
  ]
61
- version = "1.6.0"
61
+ version = "1.7.0"
62
62
  authors = [{ name = "community-of-python" }]
63
63
 
64
64
  [project.optional-dependencies]
@@ -82,6 +82,7 @@ dev = [
82
82
  "anyio>=4.8.0",
83
83
  "httpx>=0.28.1",
84
84
  "mypy>=1.14.1",
85
+ "opentelemetry-instrumentation-starlette>=0.54b1",
85
86
  "pre-commit>=4.0.1",
86
87
  "pytest>=8.3.4",
87
88
  "pytest-cov>=6.0.0",