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: 15 additions & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,10 +495,24 @@ lite-bootstrap logging on. The message triggers no work, so read the µs column:
| Prometheus | +11.6 | |
| OpenTelemetry, 100% | +73.0 | producer span plus context injection |
| OpenTelemetry, 10% | +30.6 | |
| Sentry | +28.8 | RedisIntegration (auto-enabling) on the redis client |
| Sentry, 1.x defaults | +28.8 | RedisIntegration (auto-enabling) on the redis client |
| Sentry, `auto_enabling_integrations=False` | +0.0 | |
<!-- --8<-- [end:faststreampublish] -->

Since 2.0, lite-bootstrap leaves sentry-sdk's auto-enabling integrations off while Sentry tracing is
off. Rerun per publish on the M2 host (macOS, CPython 3.14.7, faststream 0.7.8, redis 8.1.0, Redis 8 in a local
container, `bench.py faststream log@pub,sentry@pub,sentry_auto@pub,sentry_noauto@pub --repeat 3`),
over a baseline of about 136 µs:

| scenario | sentry-sdk 2.73.0 | 3.0.0b2 |
|---|---:|---:|
| `sentry`: the 2.0 default | -3.0 | -1.7 |
| `sentry_auto`: `sentry_auto_enabling_integrations=True` | +27.1 | +9.5 |
| `sentry_noauto`: `auto_enabling_integrations=False` in `sentry_additional_params` | -1.6 | -1.4 |

The first round of the baseline ran 11% slower than the other two, so the negative rows are noise.
`sentry_noauto` now has the same config as `sentry`.

### 8e. FastMCP

<!-- --8<-- [start:fastmcp] -->
Expand Down
1 change: 1 addition & 0 deletions benchmarks/frameworks/faststream_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"sentry_log_info": Scenario(config=base_config(**LOG, **sentry(), **INFO)),
"sentry_log_handler3": Scenario(config=base_config(**LOG, **sentry()), kind="structlog"),
"sentry_noauto": Scenario(config=base_config(**LOG, **sentry(auto_enabling_integrations=False))),
"sentry_auto": Scenario(config=base_config(**LOG, **sentry(), sentry_auto_enabling_integrations=True)),
"full": Scenario(config=base_config(**OTEL_MW, **PROM_MW, **LOG, **sentry())),
}
SCENARIOS: typing.Final = {**_BASE, **{f"{name}@pub": scenario for name, scenario in _BASE.items()}}
Expand Down
6 changes: 4 additions & 2 deletions docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ Measured in-process on a do-nothing handler, over the same app without Sentry:
| the same, request carries a sampled `sentry-trace` | +59.2 µs | +113.2 µs |

sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
cannot remove one that an earlier `sentry_sdk.init` installed. If that happened, lite-bootstrap
warns that errors may be reported twice.
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
[`sentry_auto_enabling_integrations`](../introduction/configuration.md#sentry-auto-enabling-integrations)
on, since lean capture otherwise leaves auto-enabling off and sentry-sdk never enables the framework
integration. With it on, lite-bootstrap warns that errors may be reported twice.

## Logging

Expand Down
7 changes: 5 additions & 2 deletions docs/integrations/fastmcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ While Sentry tracing is off, lite-bootstrap reports errors in every application
`application.http_app()` builds itself, instead of through sentry-sdk's `StarletteIntegration`.
FastMCP's HTTP application is a Starlette application, so it works as it does
[on FastAPI](fastapi.md#sentry): the same `sentry_lean_error_capture` rule, the same middleware and
the same request data. `MCPIntegration` is left alone.
the same request data. Lean capture also leaves
[auto-enabling integrations](../introduction/configuration.md#sentry-auto-enabling-integrations) off,
which drops `MCPIntegration` too. With tracing off it measured no cost per tool call, and the only
difference it made to a tool error's event was a `parent_span_id` from its unsampled span.

A tool that raises does not fail the request: FastMCP answers with an error result and logs the
exception, and the logging integration turns that record into one event with the `/mcp` request
attached, with lean capture on or off.
attached, with lean capture on or off and with `MCPIntegration` on or off.

Measured in-process per `tools/call` over stateless JSON HTTP, over the same app without Sentry:

Expand Down
6 changes: 4 additions & 2 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,10 @@ Measured in-process on a do-nothing handler, over the same app without Sentry:
| the same, request carries a sampled `sentry-trace` | +74.4 µs | +147.4 µs |

sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
cannot remove one that an earlier `sentry_sdk.init` installed. If that happened, lite-bootstrap
warns that errors may be reported twice.
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
[`sentry_auto_enabling_integrations`](../introduction/configuration.md#sentry-auto-enabling-integrations)
on, since lean capture otherwise leaves auto-enabling off and sentry-sdk never enables the framework
integration. With it on, lite-bootstrap warns that errors may be reported twice.

## Prometheus

Expand Down
26 changes: 26 additions & 0 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Additional parameters can also be supplied through the settings object:
- `sentry_tags` - key/value string pairs that are both indexed and searchable
- `sentry_additional_params` - additional params, which will be passed to `sentry_sdk.init`, overriding any of the above that map to the same keyword
- `sentry_default_integrations` - whether to use sentry's default integrations (default: `True`)
- `sentry_auto_enabling_integrations` - whether sentry-sdk enables an integration for every installed package it knows, such as SQLAlchemy, asyncpg, redis and httpx (default: `None`, meaning off while Sentry tracing is off). See [Sentry auto-enabling integrations](#sentry-auto-enabling-integrations).
- `sentry_before_send` - optional callback chained after the built-in structlog enricher, passed to `sentry_sdk.init(before_send=...)`
- `sentry_lean_error_capture` - Litestar, FastAPI and FastMCP: report request errors without sentry-sdk's framework integration (default: `None`, meaning on while Sentry tracing is off). See [Litestar](../integrations/litestar.md#sentry), [FastAPI](../integrations/fastapi.md#sentry) and [FastMCP](../integrations/fastmcp.md#sentry).

Expand Down Expand Up @@ -58,6 +59,31 @@ Two ways to opt out of the appended integration: supply your own `LoggingIntegra
`sentry_default_integrations=False`, which suppresses it along with every other default integration.
Under either, `sentry_logging_breadcrumb_level` is ignored and lite-bootstrap warns.

### Sentry auto-enabling integrations

sentry-sdk enables an integration for every installed package it has one for: the web frameworks,
SQLAlchemy, asyncpg, redis, httpx, aiohttp, MCP and others. lite-bootstrap passes
`auto_enabling_integrations` to `sentry_sdk.init`, and `sentry_auto_enabling_integrations` picks it:

- `None`, the default: off while Sentry tracing is off, on while it is on. On Litestar, FastAPI and
FastMCP it follows [`sentry_lean_error_capture`](#sentry) instead: off with lean capture, on when
sentry-sdk's framework integration reports request errors, because that integration is
auto-enabling too.
- `True` or `False`: that value always. An `auto_enabling_integrations` in `sentry_additional_params`
still wins over both.

With tracing off, those integrations still open a span and record a breadcrumb for every SQL
statement, Redis command or outgoing HTTP request. `SqlalchemyIntegration` measured about 64 µs per
statement on sentry-sdk 2.70, and `RedisIntegration` +27.1 µs per FastStream publish on 2.73.0 and
+9.5 µs on 3.0.0b2 ([FastStream](https://github.com/modern-python/lite-bootstrap/blob/main/benchmarks/README.md#8d-faststream)). The default integrations stay:
dedupe, excepthook, threading, stdlib, atexit, modules, argv and lite-bootstrap's own
`LoggingIntegration`.

Off, error events lose what those integrations add: SQL, Redis and outgoing-HTTP breadcrumbs, and
anything else an auto-enabled integration attaches. To keep them, set
`sentry_auto_enabling_integrations=True`, or list just the integrations you want in
`sentry_integrations`, for example `[SqlalchemyIntegration()]`.

### Sentry data collection

lite-bootstrap passes an explicit `data_collection` to `sentry_sdk.init`, close to what
Expand Down
9 changes: 6 additions & 3 deletions docs/introduction/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,18 @@ request, every request then pays the per-record cost measured here:

--8<-- "benchmarks/README.md:logging"

lite-bootstrap already applies four savings for you. It turns Sentry Logs off in the logging
lite-bootstrap already applies five savings for you. It turns Sentry Logs off in the logging
integration, because it never enables them and on 2.x the handler formats every record before
checking whether they are enabled; that one costs nothing. Since 2.0 it also leaves release-health
sessions off and starts breadcrumbs at `WARNING`, so an `INFO` record skips the breadcrumb handler.
Those two cost you release health and `INFO` breadcrumbs on error events; set
`sentry_auto_session_tracking=True` or `sentry_logging_breadcrumb_level=logging.INFO` to get them
back. And while Sentry tracing is off it reports request errors without sentry-sdk's framework
integration, which costs you request transactions and bodies the handler never read; set `sentry_lean_error_capture=False` to keep the integration. The numbers on this page were
measured before 2.0, against the old defaults.
integration, which costs you request transactions and bodies the handler never read; set `sentry_lean_error_capture=False` to keep the integration. With tracing off it also turns
sentry-sdk's [auto-enabling integrations](configuration.md#sentry-auto-enabling-integrations) off,
which costs you their SQL, Redis and outgoing-HTTP breadcrumbs; set
`sentry_auto_enabling_integrations=True` to keep them. The numbers on this page were measured
before 2.0, against the old defaults.

## What each saving costs you

Expand Down
12 changes: 12 additions & 0 deletions lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ class SentryConfig(BaseConfig):
sentry_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
sentry_tags: dict[str, str] | None = None
sentry_default_integrations: bool = True
sentry_auto_enabling_integrations: bool | None = None
sentry_before_send: "sentry_types.EventProcessor | None" = None

def is_sentry_auto_enabling_integrations_on(self) -> bool:
if self.sentry_auto_enabling_integrations is not None:
return self.sentry_auto_enabling_integrations
return self.is_sentry_tracing_requested()

def is_sentry_tracing_requested(self) -> bool:
return any(
option is not None
Expand Down Expand Up @@ -101,6 +107,11 @@ def is_sentry_lean_error_capture_on(self) -> bool:
return self.sentry_lean_error_capture
return not self.is_sentry_tracing_requested() and not self._supplied_sentry_lean_replaced_integrations()

def is_sentry_auto_enabling_integrations_on(self) -> bool:
if self.sentry_auto_enabling_integrations is not None:
return self.sentry_auto_enabling_integrations
return not self.is_sentry_lean_error_capture_on()


SENTRY_FAILED_REQUEST_STATUS_CODES: typing.Final = range(500, 600)
_SENTRY_TRACE_HEADERS: typing.Final = frozenset({b"sentry-trace", b"baggage"})
Expand Down Expand Up @@ -233,6 +244,7 @@ def _build_init_params(self) -> dict[str, typing.Any]:
"auto_session_tracking": config.sentry_auto_session_tracking,
"integrations": self._build_integrations(),
"default_integrations": config.sentry_default_integrations,
"auto_enabling_integrations": config.is_sentry_auto_enabling_integrations_on(),
"data_collection": DEFAULT_DATA_COLLECTION,
"before_send": wrap_before_send_callbacks(
enrich_sentry_event_from_structlog_log, config.sentry_before_send
Expand Down
82 changes: 82 additions & 0 deletions tests/instruments/test_sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,85 @@ def test_sentry_does_not_warn_when_the_breadcrumb_level_is_left_at_its_default(
assert [one for one in recwarn if "sentry_logging_breadcrumb_level" in str(one.message)] == []
finally:
instrument.teardown()


_AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT: typing.Final = frozenset({"aiohttp", "httpx", "redis"})


def _active_integrations(bootstrap_config: SentryConfig) -> set[str]:
instrument = SentryInstrument(bootstrap_config=bootstrap_config)
instrument.bootstrap()
try:
return set(sentry_sdk.get_client().integrations)
finally:
instrument.teardown()


def test_sentry_leaves_auto_enabling_integrations_off_while_tracing_is_off(
minimal_sentry_config: SentryConfig,
) -> None:
active = _active_integrations(minimal_sentry_config)

assert not _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT & active
assert "logging" in active


@pytest.mark.parametrize(
"tracing_overrides",
[
pytest.param({"sentry_traces_sample_rate": 0.0}, id="sentry_traces_sample_rate"),
pytest.param({"sentry_additional_params": {"traces_sample_rate": 0.5}}, id="traces_sample_rate"),
pytest.param({"sentry_additional_params": {"traces_sampler": lambda _: 0.5}}, id="traces_sampler"),
],
)
def test_sentry_keeps_auto_enabling_integrations_while_tracing_is_on(
minimal_sentry_config: SentryConfig, tracing_overrides: dict[str, typing.Any]
) -> None:
additional_params = {
**minimal_sentry_config.sentry_additional_params,
**tracing_overrides.pop("sentry_additional_params", {}),
}
bootstrap_config = dataclasses.replace(
minimal_sentry_config, sentry_additional_params=additional_params, **tracing_overrides
)

assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT


@pytest.mark.parametrize(
("auto_enabling_integrations", "traces_sample_rate", "expected"),
[
pytest.param(True, None, True, id="on-while-tracing-is-off"),
pytest.param(False, 1.0, False, id="off-while-tracing-is-on"),
],
)
def test_sentry_auto_enabling_integrations_set_explicitly_wins(
minimal_sentry_config: SentryConfig,
auto_enabling_integrations: bool,
traces_sample_rate: float | None,
expected: bool,
) -> None:
bootstrap_config = dataclasses.replace(
minimal_sentry_config,
sentry_auto_enabling_integrations=auto_enabling_integrations,
sentry_traces_sample_rate=traces_sample_rate,
)
instrument = SentryInstrument(bootstrap_config=bootstrap_config)
instrument.bootstrap()

try:
assert sentry_sdk.get_client().options["auto_enabling_integrations"] is expected
finally:
instrument.teardown()


def test_sentry_additional_params_auto_enabling_integrations_wins_over_the_field(
minimal_sentry_config: SentryConfig,
) -> None:
bootstrap_config = dataclasses.replace(
minimal_sentry_config,
sentry_auto_enabling_integrations=False,
sentry_additional_params={**minimal_sentry_config.sentry_additional_params, "auto_enabling_integrations": True},
)

assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT
36 changes: 35 additions & 1 deletion tests/test_litestar_sentry_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,26 @@ def test_lean_capture_reports_once_when_litestar_logs_the_exception(
assert _exception_types(sentry_mock) == ["RuntimeError"]


_AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT: typing.Final = frozenset({"aiohttp", "httpx", "redis"})


def test_lean_capture_leaves_auto_enabling_integrations_off_and_still_reports(sentry_mock: SentryTestTransport) -> None:
application = LitestarBootstrapper(bootstrap_config=_build_config(sentry_mock)).bootstrap()
with TestClient(app=application, raise_server_exceptions=False) as test_client:
active = test_client.get("/integrations").json()
test_client.get("/boom")

assert not _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT & set(active)
[event] = _error_events(sentry_mock)
assert event["request"]["url"] == "http://testserver.local/boom"


def test_integration_capture_keeps_auto_enabling_integrations(sentry_mock: SentryTestTransport) -> None:
active = _active_integrations(sentry_mock, sentry_lean_error_capture=False)

assert set(active) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT


def test_lean_capture_replaces_the_litestar_integration(sentry_mock: SentryTestTransport) -> None:
active = _active_integrations(sentry_mock, sentry_lean_error_capture=True)

Expand All @@ -441,13 +461,27 @@ def test_lean_capture_warns_when_an_earlier_init_installed_the_litestar_integrat
sentry_mock: SentryTestTransport,
) -> None:
sentry_integrations._installed_integrations.add(LitestarIntegration.identifier) # noqa: SLF001
bootstrapper = LitestarBootstrapper(bootstrap_config=_build_config(sentry_mock))
bootstrapper = LitestarBootstrapper(
bootstrap_config=_build_config(sentry_mock, sentry_auto_enabling_integrations=True)
)

with pytest.warns(UserWarning, match="installed by an earlier sentry_sdk.init"):
bootstrapper.bootstrap()
bootstrapper.teardown()


def test_lean_capture_ignores_an_earlier_installed_litestar_integration_while_auto_enabling_is_off(
sentry_mock: SentryTestTransport,
) -> None:
sentry_integrations._installed_integrations.add(LitestarIntegration.identifier) # noqa: SLF001

with warnings.catch_warnings():
warnings.simplefilter("error")
active = _active_integrations(sentry_mock)

assert LitestarIntegration.identifier not in active


def test_forced_lean_capture_warns_that_traces_sample_rate_gets_no_request_transactions(
sentry_mock: SentryTestTransport,
) -> None:
Expand Down
Loading
Loading