microbootstrap 1.6.1__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.1 → microbootstrap-1.7.0}/PKG-INFO +45 -2
  2. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/README.md +44 -1
  3. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/fastmcp.py +72 -3
  4. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/settings.py +1 -0
  5. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/pyproject.toml +2 -1
  6. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/pyproject.toml.orig +2 -1
  7. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/__init__.py +0 -0
  8. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/__init__.py +0 -0
  9. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/base.py +0 -0
  10. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/fastapi.py +0 -0
  11. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/faststream.py +0 -0
  12. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/bootstrappers/litestar.py +0 -0
  13. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/config/__init__.py +0 -0
  14. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/config/fastapi.py +0 -0
  15. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/config/fastmcp.py +0 -0
  16. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/config/faststream.py +0 -0
  17. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/config/litestar.py +0 -0
  18. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/console_writer.py +0 -0
  19. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/exceptions.py +0 -0
  20. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/granian_server.py +0 -0
  21. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/helpers.py +0 -0
  22. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/__init__.py +0 -0
  23. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/base.py +0 -0
  24. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/cors_instrument.py +0 -0
  25. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/health_checks_instrument.py +0 -0
  26. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/instrument_box.py +0 -0
  27. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/logging_instrument.py +0 -0
  28. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/openapi_security_schemes.py +0 -0
  29. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/openapi_version_docs.py +0 -0
  30. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/opentelemetry_instrument.py +0 -0
  31. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/prometheus_instrument.py +0 -0
  32. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/pyroscope_instrument.py +0 -0
  33. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/sentry_instrument.py +0 -0
  34. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments/swagger_instrument.py +0 -0
  35. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/instruments_setupper.py +0 -0
  36. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/middlewares/__init__.py +0 -0
  37. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/middlewares/fastapi.py +0 -0
  38. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/middlewares/fastmcp.py +0 -0
  39. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/middlewares/faststream.py +0 -0
  40. {microbootstrap-1.6.1 → microbootstrap-1.7.0}/microbootstrap/middlewares/litestar.py +0 -0
  41. {microbootstrap-1.6.1 → 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.1
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
@@ -521,7 +521,7 @@ 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 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`).
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
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.
@@ -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`.
@@ -456,7 +456,7 @@ 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 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`).
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
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.
@@ -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]
@@ -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.1"
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.1"
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",