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
6 changes: 3 additions & 3 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ structlog 26.1.0. In-process, baseline 17.7 µs per request. The handler does no
| scenario | +µs/req | note |
|---|---:|---|
| logging configured, no records | +0.1 | |
| `fastapi_logging_middleware_enabled=True` | +18.3 | one access-log record per request, about 10 µs of it the record |
| `logging_access_log_enabled=True` | +18.3 | one access-log record per request, about 10 µs of it the record |
| Prometheus | +19.1 | |
| OpenTelemetry, 100% sampling | +147.9 | |
| OpenTelemetry, `opentelemetry_exclude_spans=["receive", "send"]` | +101.6 | -46.3 |
Expand Down Expand Up @@ -354,7 +354,7 @@ prometheus-client 0.26.0, structlog 26.1.0. In-process, baseline 12.1 µs per re
|---|---:|---|
| logging configured, no records | -0.1 | free until something logs |
| logging, 3 structlog records in the handler | +33.8 | 11.3 µs per record |
| `litestar_logging_middleware_enabled=True` | +70.7 | 2 records per request, each written twice; a second run gave +74.4 |
| `logging_access_log_enabled=True` | +70.7 | 2 records per request, each written twice; a second run gave +74.4 |
| Prometheus | +11.6 | |
| Prometheus, `prometheus_group_path=False` | +11.8 | |
| OpenTelemetry, 100% sampling | +145.4 | a second run gave +131.8 |
Expand Down Expand Up @@ -477,7 +477,7 @@ it FastMCP and the MCP SDK. Noise on this suite is about ±30 µs even with 3 re
| scenario | +µs/call | note |
|---|---:|---|
| logging configured | +15 | noise |
| `fastmcp_logging_middleware_enabled=True` | +51 | one record per MCP message |
| `logging_access_log_enabled=True` | +51 | one record per MCP message |
| Prometheus (HTTP instrumentator and tool-call middleware) | +35 | the split between the two is within noise |
| OpenTelemetry, 100% | +272 | 3 ASGI spans plus FastMCP's own server span |
| OpenTelemetry, 100%, FastMCP's own spans replaced by a no-op tracer | +226 | FastMCP's spans cost 46 |
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/frameworks/fastapi_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
SCENARIOS: typing.Final = {
"bare": Scenario(config=base_config()),
"log": Scenario(config=base_config(**LOG)),
"log_mw": Scenario(config=base_config(**LOG, fastapi_logging_middleware_enabled=True)),
"log_mw": Scenario(config=base_config(**LOG, logging_access_log_enabled=True)),
"prom": Scenario(config=base_config(**PROM)),
"otel": Scenario(config=base_config(**OTEL)),
"otel_excl": Scenario(config=base_config(**OTEL, **EXCL)),
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/frameworks/fastmcp_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def no_fastmcp_spans() -> None:
SCENARIOS: typing.Final = {
"bare": Scenario(config=base_config()),
"log": Scenario(config=base_config(**LOG)),
"log_mw": Scenario(config=base_config(**LOG, fastmcp_logging_middleware_enabled=True)),
"log_mw": Scenario(config=base_config(**LOG, logging_access_log_enabled=True)),
"prom": Scenario(config=base_config(**PROM)),
"otel": Scenario(config=base_config(**OTEL)),
"otel_nofastmcp_spans": Scenario(config=base_config(**OTEL), patch=no_fastmcp_spans),
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/frameworks/litestar_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"defaults_noobs": Scenario(config=base_config(**LITESTAR_DEFAULTS)),
"log": Scenario(config=base_config(**LOG)),
"log_log3": Scenario(config=base_config(**LOG), kind="structlog"),
"log_mw": Scenario(config=base_config(**LOG, litestar_logging_middleware_enabled=True)),
"log_mw": Scenario(config=base_config(**LOG, logging_access_log_enabled=True)),
"prom": Scenario(config=base_config(**PROM)),
"prom_nogroup": Scenario(config=base_config(**PROM, prometheus_group_path=False)),
"otel": Scenario(config=base_config(**OTEL)),
Expand Down
4 changes: 2 additions & 2 deletions docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ place to add a log record. Turn it on explicitly:
```python
FastAPIConfig(
service_name="microservice",
fastapi_logging_middleware_enabled=True,
logging_access_log_enabled=True,
)
```

Expand All @@ -81,7 +81,7 @@ structured one, clear uvicorn's handlers:
```python
FastAPIConfig(
service_name="microservice",
fastapi_logging_middleware_enabled=True,
logging_access_log_enabled=True,
logging_unset_handlers=["uvicorn.access"],
)
```
2 changes: 1 addition & 1 deletion docs/integrations/fastmcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ explicitly:
```python
FastMcpConfig(
service_name="microservice",
fastmcp_logging_middleware_enabled=True,
logging_access_log_enabled=True,
)
```

Expand Down
4 changes: 2 additions & 2 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Swagger bundle into your logs. Turn it on explicitly:
```python
LitestarConfig(
service_name="microservice",
litestar_logging_middleware_enabled=True,
logging_access_log_enabled=True,
)
```

Expand All @@ -92,7 +92,7 @@ from litestar.middleware.logging import LoggingMiddlewareConfig

LitestarConfig(
service_name="microservice",
litestar_logging_middleware_enabled=True,
logging_access_log_enabled=True,
litestar_logging_middleware_config=LoggingMiddlewareConfig(request_log_fields=("path", "method", "content_type")),
)
```
Expand Down
19 changes: 10 additions & 9 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ Prometheus's integration for FastAPI uses `prometheus_fastapi_instrumentator` pa

Additional parameters for FastAPI integration:

- `prometheus_instrumentator_params` - passed to `prometheus_fastapi_instrumentator.Instrumentator`
- `prometheus_instrument_params` - passed to `method Instrumentator(...).instrument`
- `prometheus_expose_params` - passed to `method Instrumentator(...).expose`.
- `prometheus_instrumentator_additional_params` - passed to `prometheus_fastapi_instrumentator.Instrumentator`
- `prometheus_instrument_additional_params` - passed to `method Instrumentator(...).instrument`
- `prometheus_expose_additional_params` - passed to `method Instrumentator(...).expose`.

### Prometheus FastMCP

Expand All @@ -110,9 +110,9 @@ route itself is not counted. Tool calls are counted in `fastmcp_tool_calls_total

Additional parameters for FastMCP integration:

- `prometheus_instrumentator_params` - passed to `prometheus_fastapi_instrumentator.Instrumentator`;
- `prometheus_instrumentator_additional_params` - passed to `prometheus_fastapi_instrumentator.Instrumentator`;
`excluded_handlers` defaults to the metrics route.
- `prometheus_instrument_params` - passed to `method Instrumentator(...).instrument`.
- `prometheus_instrument_additional_params` - passed to `method Instrumentator(...).instrument`.
- `fastmcp_prometheus_tool_metrics_enabled` - count and time tool calls (default: `True`).


Expand Down Expand Up @@ -321,16 +321,16 @@ async def handler(request: Request) -> dict[str, str]:

Additional parameters for Litestar's access-log middleware:

- `litestar_logging_middleware_enabled` - turn on request/response access logging (default: `False`).
- `litestar_logging_middleware_config` - a caller-supplied `LoggingMiddlewareConfig` that replaces the built-in defaults wholesale. It is ignored, with a warning, unless `litestar_logging_middleware_enabled` is `True`.
- `logging_access_log_enabled` - turn on request/response access logging (default: `False`).
- `litestar_logging_middleware_config` - a caller-supplied `LoggingMiddlewareConfig` that replaces the built-in defaults wholesale. It is ignored, with a warning, unless `logging_access_log_enabled` is `True`.

See [the Litestar integration guide](../integrations/litestar.md#logging) for what gets logged and why access logging defaults to off.

### Structlog FastAPI

FastAPI ships no access log of its own, so lite-bootstrap provides one. It is **off by default**:

- `fastapi_logging_middleware_enabled` - turn on the structured access log (default: `False`).
- `logging_access_log_enabled` - turn on the structured access log (default: `False`).

See [the FastAPI integration guide](../integrations/fastapi.md#logging) for what gets logged and why
it defaults to off.
Expand All @@ -339,7 +339,7 @@ it defaults to off.

The per-MCP-message access log is **off by default**:

- `fastmcp_logging_middleware_enabled` - turn on the access log (default: `False`).
- `logging_access_log_enabled` - turn on the access log (default: `False`).

See [the FastMCP integration guide](../integrations/fastmcp.md#logging) for what gets logged.

Expand Down Expand Up @@ -383,6 +383,7 @@ To bootstrap swagger, you have the following parameters:
- `swagger_static_path` - path for offline docs static
- `swagger_path`
- `swagger_offline_docs` - option to turn on offline docs.
- `swagger_additional_params` - Litestar only: extra keyword arguments for `litestar.openapi.OpenAPIConfig`. It cannot repeat `path`, `title`, `version`, `description` or `render_plugins`, which lite-bootstrap already passes.

Swagger is on by default at `swagger_path` (default: `/docs`). For Litestar, set `swagger_path` to an empty string to disable it.

Expand Down
4 changes: 2 additions & 2 deletions docs/introduction/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ so there is nothing else material hiding in it. That configuration is:
--8<-- "benchmarks/README.md:tuned"

One thing to leave off rather than turn on: the FastAPI access log
([`fastapi_logging_middleware_enabled`](../integrations/fastapi.md#logging)) is off by default, and
([`logging_access_log_enabled`](../integrations/fastapi.md#logging)) is off by default, and
turning it on makes every request emit a log record. On a service that otherwise logs nothing per
request, every request then pays the per-record cost measured here:

Expand Down Expand Up @@ -102,7 +102,7 @@ The same app on Granian, over real sockets:

**Sentry costs more on Litestar than on FastAPI**, and all of it is `LitestarIntegration`: turning
off auto-enabling integrations removes the cost and, with it, request error capture. The access log
([`litestar_logging_middleware_enabled`](../integrations/litestar.md#logging)) is the other large opt-in
([`logging_access_log_enabled`](../integrations/litestar.md#logging)) is the other large opt-in
item, at about 70 µs per request in-process.

## FastStream
Expand Down
35 changes: 26 additions & 9 deletions lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import dataclasses
import time
import typing
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, Mapping

from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
Expand Down Expand Up @@ -63,10 +63,27 @@ class FastAPIConfig(
application_kwargs: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
# Not on OpenTelemetryConfig: only the FastAPI and Litestar instrumentors take `exclude_spans`.
opentelemetry_exclude_spans: list[typing.Literal["receive", "send"]] = dataclasses.field(default_factory=list)
prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_expose_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
fastapi_logging_middleware_enabled: bool = False
prometheus_instrumentator_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrument_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_expose_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
logging_access_log_enabled: bool = False
# Deprecated names, removed in 2.0.
_renamed_fields: typing.ClassVar[Mapping[str, str]] = {
"prometheus_instrumentator_params": "prometheus_instrumentator_additional_params",
"prometheus_instrument_params": "prometheus_instrument_additional_params",
"prometheus_expose_params": "prometheus_expose_additional_params",
"fastapi_logging_middleware_enabled": "logging_access_log_enabled",
}
prometheus_instrumentator_params: dict[str, typing.Any] | UnsetType = dataclasses.field(
default=UNSET, repr=False, compare=False
)
prometheus_instrument_params: dict[str, typing.Any] | UnsetType = dataclasses.field(
default=UNSET, repr=False, compare=False
)
prometheus_expose_params: dict[str, typing.Any] | UnsetType = dataclasses.field(
default=UNSET, repr=False, compare=False
)
fastapi_logging_middleware_enabled: bool | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False)

def __post_init__(self) -> None:
# @dataclass(slots=True) replaces the class object, breaking bare super().
Expand Down Expand Up @@ -158,7 +175,7 @@ async def send_wrapper(message: "Message") -> None:
class FastAPILoggingInstrument(LoggingInstrument[FastAPIConfig]):
def bootstrap(self) -> None:
super().bootstrap()
if not self.bootstrap_config.fastapi_logging_middleware_enabled:
if not self.bootstrap_config.logging_access_log_enabled:
return
self.bootstrap_config.app.add_middleware(_AccessLogMiddleware, excluded_paths=self._build_excluded_paths())

Expand Down Expand Up @@ -218,14 +235,14 @@ def dependencies_installed() -> bool:
def bootstrap(self) -> None:
config = self.bootstrap_config
application = config.app
Instrumentator(**config.prometheus_instrumentator_params).instrument(
Instrumentator(**config.prometheus_instrumentator_additional_params).instrument(
application,
**config.prometheus_instrument_params,
**config.prometheus_instrument_additional_params,
).expose(
application,
endpoint=config.prometheus_metrics_path,
include_in_schema=config.prometheus_metrics_include_in_schema,
**config.prometheus_expose_params,
**config.prometheus_expose_additional_params,
)


Expand Down
30 changes: 22 additions & 8 deletions lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
import time
import typing
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, Mapping

from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
Expand All @@ -15,6 +15,7 @@
from lite_bootstrap.instruments.prometheus_instrument import PrometheusConfig, PrometheusInstrument
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
from lite_bootstrap.types import UNSET, UnsetType


if import_checker.is_fastmcp_installed:
Expand Down Expand Up @@ -193,10 +194,23 @@ class FastMcpConfig(
HealthChecksConfig, LoggingConfig, OpenTelemetryConfig, PrometheusConfig, PyroscopeConfig, SentryConfig
):
application: "FastMCP[typing.Any]" = dataclasses.field(default_factory=_make_fastmcp)
fastmcp_logging_middleware_enabled: bool = False
logging_access_log_enabled: bool = False
fastmcp_prometheus_tool_metrics_enabled: bool = True
prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrumentator_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrument_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
# Deprecated names, removed in 2.0.
_renamed_fields: typing.ClassVar[Mapping[str, str]] = {
"fastmcp_logging_middleware_enabled": "logging_access_log_enabled",
"prometheus_instrumentator_params": "prometheus_instrumentator_additional_params",
"prometheus_instrument_params": "prometheus_instrument_additional_params",
}
fastmcp_logging_middleware_enabled: bool | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False)
prometheus_instrumentator_params: dict[str, typing.Any] | UnsetType = dataclasses.field(
default=UNSET, repr=False, compare=False
)
prometheus_instrument_params: dict[str, typing.Any] | UnsetType = dataclasses.field(
default=UNSET, repr=False, compare=False
)


@dataclasses.dataclass(kw_only=True)
Expand Down Expand Up @@ -279,19 +293,19 @@ def _instrument_http_app(self, http_application: "StarletteWithLifespan") -> "St
config: typing.Final = self.bootstrap_config
instrumentator_params: typing.Final[dict[str, typing.Any]] = {
"excluded_handlers": [f"^{re.escape(config.prometheus_metrics_path)}$"],
**config.prometheus_instrumentator_params,
**config.prometheus_instrumentator_additional_params,
}
# A second http_app() would register the default metrics again, which the instrumentator
# treats as a duplicate and silently skips, so every application shares the first set.
if self._default_metrics is None:
default_metrics_params: typing.Final[dict[str, typing.Any]] = {
name: value
for name, value in {**instrumentator_params, **config.prometheus_instrument_params}.items()
for name, value in {**instrumentator_params, **config.prometheus_instrument_additional_params}.items()
if name in _DEFAULT_METRICS_PARAMETERS
}
self._default_metrics = instrumentator_metrics.default(**default_metrics_params)
Instrumentator(**instrumentator_params).add(self._default_metrics).instrument(
http_application, **config.prometheus_instrument_params
http_application, **config.prometheus_instrument_additional_params
)
return http_application

Expand Down Expand Up @@ -323,7 +337,7 @@ def teardown(self) -> None:
class FastMcpLoggingInstrument(LoggingInstrument[FastMcpConfig]):
def bootstrap(self) -> None:
super().bootstrap()
if not self.bootstrap_config.fastmcp_logging_middleware_enabled:
if not self.bootstrap_config.logging_access_log_enabled:
return
self.bootstrap_config.application.add_middleware(FastMcpLoggingMiddleware())

Expand Down
Loading
Loading