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
19 changes: 19 additions & 0 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ jobs:
- run: uv python pin ${{ matrix.python-version }}
- run: just install
- run: just test-ci
sentry-majors:
# The pytest jobs resolve whichever sentry-sdk major is newest; this runs the suite on each one.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sentry-sdk: ["sentry-sdk<3", "sentry-sdk==3.0.0b2"]
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v4
- uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.14
- run: uv python pin 3.14
- run: just install
- run: uv pip install "${{ matrix.sentry-sdk }}"
- run: just test
free-threaded:
runs-on: ubuntu-latest
strategy:
Expand Down
6 changes: 6 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,12 @@ scenario there, `full_all_tuned` included, is reachable through `FastAPIConfig`.
[sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400) and imports nothing
from this directory.

Every number here was measured on sentry-sdk 2.67.1. The scripts also run on 3.x, which builds no
transaction while tracing is off, so the patched scenarios, `errors_only_txn_endpoint_style` and
`repro_sentry_txn.py --patched` exist only on 2.x. On 3.x, `micro.py` times `continue_trace` and
the root `start_span` in place of the `Transaction` calls. 3.x also leaves `LoggingIntegration` out
of its default integrations, so its `errors_only` records no log breadcrumbs.

Numbers are machine-specific and move a few percent run to run; the ratios and the ordering are
the durable part. Everything here was measured in one session on one idle machine, which is the
only way the columns are comparable to each other.
Expand Down
51 changes: 37 additions & 14 deletions benchmarks/micro.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Per-operation costs of the pieces sentry-sdk runs on every request.

Attributes the per-request total from `run.py` to individual calls, which is what makes the
upstream issues actionable: `Transaction(...)` and the Mersenne Twister seed inside it are the
two largest items, and neither is reachable from a configuration knob.
upstream issues actionable: on sentry-sdk 2.x, `Transaction(...)` and the Mersenne Twister seed
inside it are the two largest items, and neither is reachable from a configuration knob. 3.x builds
no transaction while tracing is off, so there it times `continue_trace` and the root `start_span`
that the ASGI integration calls on every request instead.
"""

import importlib
import statistics
import timeit
import typing
Expand All @@ -14,9 +17,10 @@
import sentry_scenarios
import sentry_sdk
from sentry_sdk.scope import Scope
from sentry_sdk.tracing import Transaction
from sentry_sdk.tracing_utils import Baggage, PropagationContext, _generate_sample_rand

from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2


HEADERS: typing.Final = {
"host": "testserver",
Expand All @@ -25,6 +29,7 @@
"connection": "keep-alive",
}
TRACE_ID: typing.Final = uuid.uuid4().hex
SDK2_TRACING: typing.Final[typing.Any] = importlib.import_module("sentry_sdk.tracing") if IS_SENTRY_SDK_2 else None


def timed(label: str, stmt: typing.Callable[[], object], number: int = 20000, repeat: int = 5) -> None:
Expand All @@ -34,15 +39,35 @@ def timed(label: str, stmt: typing.Callable[[], object], number: int = 20000, re
print(f"{label:<46} {best:>8.2f} us (best) {median:>8.2f} us (med)")


def main() -> None:
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
scope = Scope.get_isolation_scope()
def time_sdk2_transaction(isolation_scope: typing.Any, current_scope: typing.Any) -> None: # noqa: ANN401
def new_transaction() -> object:
return SDK2_TRACING.Transaction(op="http.server", name="GET /ping", source="route")

def start_and_finish() -> None:
transaction = Transaction(op="http.server", name="GET /ping", source="route")
with sentry_sdk.start_transaction(transaction, custom_sampling_context={"asgi_scope": {}}):
with current_scope.start_transaction(new_transaction(), custom_sampling_context={"asgi_scope": {}}):
pass

timed(
"scope.continue_trace(headers)",
lambda: isolation_scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
)
timed("Transaction(op, name, source)", new_transaction)
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)


def time_sdk3_span() -> None:
def start_and_end() -> None:
with sentry_sdk.start_span(name="GET /ping", parent_span=None):
pass

timed("continue_trace(headers)", lambda: sentry_sdk.continue_trace(HEADERS))
timed("start_span(root) + exit (tracing off)", start_and_end, number=5000)


def main() -> None:
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
scope = Scope.get_isolation_scope()

def isolation() -> None:
with sentry_sdk.isolation_scope():
pass
Expand All @@ -54,12 +79,10 @@ def isolation() -> None:
timed("PropagationContext.from_incoming_data(headers)", lambda: PropagationContext.from_incoming_data(HEADERS))
timed("Baggage.from_incoming_header(None)", lambda: Baggage.from_incoming_header(None))
timed("scope.generate_propagation_context(headers)", lambda: scope.generate_propagation_context(HEADERS))
timed(
"scope.continue_trace(headers)",
lambda: scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
)
timed("Transaction(op, name, source)", lambda: Transaction(op="http.server", name="GET /ping", source="route"))
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)
if IS_SENTRY_SDK_2:
time_sdk2_transaction(scope, sentry_sdk.get_current_scope())
else:
time_sdk3_span()
timed("isolation_scope() enter/exit", isolation)
timed("scope.fork()", scope.fork)
timed("get_client()", sentry_sdk.get_client)
Expand Down
6 changes: 5 additions & 1 deletion benchmarks/repro_sentry_txn.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
python repro_sentry_txn.py --off # SDK never initialised
python repro_sentry_txn.py # SDK as shipped
python repro_sentry_txn.py --patched # skip the Transaction, keep trace propagation

`--patched` runs on sentry-sdk 2.x only: 3.x builds no transaction while tracing is off.
"""

import asyncio
Expand Down Expand Up @@ -55,7 +57,7 @@ def setup(*, patched: bool) -> None:
# tracing is DISABLED: traces_sample_rate and traces_sampler are both unset
sentry_sdk.init(dsn="https://public@o0.ingest.sentry.io/0", transport=NullTransport)
if patched:
Scope.continue_trace = continue_trace_without_transaction # ty: ignore[invalid-assignment]
setattr(Scope, "continue_trace", continue_trace_without_transaction) # noqa: B010


async def drive(app: typing.Callable[..., typing.Awaitable[None]], requests: int) -> float:
Expand All @@ -76,6 +78,8 @@ async def send(message: dict[str, typing.Any]) -> None:

def main() -> None:
if "--patched" in sys.argv:
if not sentry_sdk.VERSION.startswith("2."):
sys.exit(f"--patched needs sentry-sdk 2.x; {sentry_sdk.VERSION} builds no transaction to skip")
mode = "patched"
elif "--off" in sys.argv:
mode = "off"
Expand Down
69 changes: 44 additions & 25 deletions benchmarks/sentry_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
`starlette.middleware.Middleware.__init__` and `logging.Logger.callHandlers`, and none of it
can be undone.

The `*_patch` scenarios are not configurations a user can reach - they simulate an upstream
fix so its value can be measured before proposing it.
The patched scenarios are not configurations a user can reach - they simulate an upstream
fix so its value can be measured before proposing it. They, and `errors_only_txn_endpoint_style`,
exist only on sentry-sdk 2.x: 3.x builds no transaction while tracing is off and has no
`transaction_style`.
"""

import importlib
import logging
import typing

import sentry_sdk
import sentry_sdk.tracing
from driver import DSN, TRANSPORT
from fastapi import FastAPI
from sentry_sdk.integrations.dedupe import DedupeIntegration
Expand All @@ -25,6 +27,10 @@
from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.scope import Scope

from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2


SDK2_TRACING: typing.Final = importlib.import_module("sentry_sdk.tracing") if IS_SENTRY_SDK_2 else None

logger = logging.getLogger("bench")

Expand Down Expand Up @@ -59,6 +65,17 @@ def _no_transaction_integrations() -> list[typing.Any]:
]


def _errors_and_breadcrumbs_only(*, breadcrumbs: bool) -> LoggingIntegration:
"""Errors become events and nothing goes to Sentry Logs; 3.0 renamed the arguments for that."""
breadcrumb_level = logging.INFO if breadcrumbs else None
kwargs: dict[str, typing.Any] = (
{"level": breadcrumb_level, "sentry_logs_level": None}
if IS_SENTRY_SDK_2
else {"breadcrumb_level": breadcrumb_level, "event_level": logging.ERROR, "level": None}
)
return LoggingIntegration(**kwargs)


SCENARIOS: dict[str, typing.Callable[[], dict[str, typing.Any]] | None] = {
"off": None,
# lite-bootstrap's own defaults: tracing off, all default and auto integrations on.
Expand Down Expand Up @@ -113,24 +130,16 @@ def _no_transaction_integrations() -> list[typing.Any]:
auto_enabling_integrations=False,
integrations=[StarletteIntegration()],
),
"errors_only_txn_endpoint_style": lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[
StarletteIntegration(transaction_style="endpoint"),
FastApiIntegration(transaction_style="endpoint"),
],
),
# --- logging integration knobs ---
"errors_only_no_sentry_logs": lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[LoggingIntegration(sentry_logs_level=None)],
integrations=[_errors_and_breadcrumbs_only(breadcrumbs=True)],
),
"errors_only_logging_lean": lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[LoggingIntegration(level=None, sentry_logs_level=None)],
integrations=[_errors_and_breadcrumbs_only(breadcrumbs=False)],
),
# --- tracing on ---
"traces_0": lambda: base_kwargs(traces_sample_rate=0.0, attach_stacktrace=True),
Expand All @@ -146,7 +155,7 @@ def _no_transaction_integrations() -> list[typing.Any]:

def _patch_lazy_sample_rand() -> None:
"""`Transaction.__init__` derives sample_rand from the trace id even when tracing is off."""
sentry_sdk.tracing._generate_sample_rand = lambda trace_id, **kwargs: 0.5 # ty: ignore[invalid-assignment] # noqa: ARG005
setattr(SDK2_TRACING, "_generate_sample_rand", lambda trace_id, **kwargs: 0.5) # noqa: ARG005, B010


def _patch_skip_transaction() -> None:
Expand All @@ -157,20 +166,30 @@ def continue_trace(
) -> None:
self.generate_propagation_context(environ_or_headers)

Scope.continue_trace = continue_trace # ty: ignore[invalid-assignment]
setattr(Scope, "continue_trace", continue_trace) # noqa: B010


PATCHES: dict[str, list[typing.Callable[[], None]]] = {
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
"errors_only_skip_txn": [_patch_skip_transaction],
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
}
PATCHES: dict[str, list[typing.Callable[[], None]]] = {}

SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]
if IS_SENTRY_SDK_2:
endpoint_style: dict[str, typing.Any] = {"transaction_style": "endpoint"}
SCENARIOS["errors_only_txn_endpoint_style"] = lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[StarletteIntegration(**endpoint_style), FastApiIntegration(**endpoint_style)],
)
PATCHES.update(
{
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
"errors_only_skip_txn": [_patch_skip_transaction],
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
}
)
SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]


def setup(scenario: str) -> None:
Expand Down
47 changes: 37 additions & 10 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Additional parameters can also be supplied through the settings object:
- `sentry_attach_stacktrace` - if True, stack traces are automatically attached to all messages logged
- `sentry_auto_session_tracking` - whether every request opens and closes a Sentry release-health `Session` (default: `True`), measured at ~7.7 µs per request. Set it to `False` if you do not use Sentry release health.
- `sentry_integrations` - list of integrations to enable
- `sentry_logging_breadcrumb_level` - the minimum standard-library log level recorded as a breadcrumb (default: `logging.INFO`). Passed as `LoggingIntegration(level=...)`; see below.
- `sentry_logging_breadcrumb_level` - the minimum standard-library log level recorded as a breadcrumb (default: `logging.INFO`). See below for how it reaches `LoggingIntegration`.
- `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`)
Expand All @@ -30,15 +30,21 @@ integration; see [Performance](performance.md).

### Sentry logging integration

Unless `sentry_integrations` already contains a `LoggingIntegration`, lite-bootstrap appends one built
as `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`. Its event
handler keeps the sentry-sdk default (`ERROR`), so the only departure from sentry-sdk's own default
integration is `sentry_logs_level`.

Turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry Logs is
off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are enabled
([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it formats
every `INFO`+ record and discards the result. Dropping breadcrumbs as well, with
lite-bootstrap supports sentry-sdk 2.70 and later, on both the 2.x and 3.x majors. Unless
`sentry_integrations` already contains a `LoggingIntegration`, it appends one that turns `ERROR`
records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends no
Sentry Logs. 3.0 renamed the integration's arguments, so the call differs per major:

- 2.x: `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`, with the
event level left at its `ERROR` default.
- 3.x: `LoggingIntegration(breadcrumb_level=sentry_logging_breadcrumb_level, event_level=logging.ERROR, level=None)`.
On 3.x `level` means Sentry Logs, and `event_level` defaults to `None`, so without it a logged
error creates no event.

On 2.x, turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
Logs is off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are
enabled ([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it
formats every `INFO`+ record and discards the result. Dropping breadcrumbs as well, with
`sentry_logging_breadcrumb_level=None`, saves more but costs you log breadcrumbs on error events, so
it stays on by default. Both are measured on the [performance page](performance.md#where-the-time-goes).

Expand All @@ -47,6 +53,27 @@ 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 data collection

lite-bootstrap passes an explicit `data_collection` to `sentry_sdk.init`, close to what
`send_default_pii=False` collected: no user identity, HTTP bodies, GraphQL documents, generative AI or
MCP inputs and outputs, database query data or queue message bodies. Request headers, cookies and URL
query parameters are sent with IP- and user-identifying keys filtered. Stack frame variables are sent
with secret-looking names (`password`, `token`, `key`, `session` and the rest of sentry-sdk's
denylist) replaced by `[Filtered]`. The dict is `sentry_instrument.DEFAULT_DATA_COLLECTION`.

To collect more, pass your own `data_collection` in `sentry_additional_params`. It replaces the
default as a whole, so keys you leave out take sentry-sdk's own defaults, which collect more. To
change one key, spread the default: `{**DEFAULT_DATA_COLLECTION, "user_info": True}`. Read more about
the option [here](https://docs.sentry.io/platforms/python/data-management/data-collected/).

`send_default_pii` has no effect on 2.x while `data_collection` is set, and on 3.x it is gone. 3.0
also raises `TypeError` at `sentry_sdk.init` for any option it removed, so passing one of these
through `sentry_additional_params` fails at bootstrap on 3.x, `enable_tracing`, `enable_logs` and
`include_local_variables` among them. sentry-sdk's
[migration guide](https://github.com/getsentry/sentry-python/blob/master/MIGRATION_GUIDE.md) lists
every removed option.


## Prometheus

Expand Down
Loading
Loading