Skip to content

Commit 6a7f6c3

Browse files
committed
feat: enhanced OTEL instrumentation
Properly wrap guards, CLI, and events with OTEL spans
1 parent f37202b commit 6a7f6c3

File tree

8 files changed

+1127
-42
lines changed

8 files changed

+1127
-42
lines changed

docs/usage/metrics/open-telemetry.rst

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
OpenTelemetry
22
=============
33

4-
Litestar includes optional OpenTelemetry instrumentation that is exported from ``litestar.contrib.opentelemetry``. To use
4+
Litestar includes optional OpenTelemetry instrumentation that is exported from ``litestar.plugins.opentelemetry``. To use
55
this package, you should first install the required dependencies:
66

77
.. code-block:: bash
@@ -16,13 +16,12 @@ this package, you should first install the required dependencies:
1616
pip install 'litestar[opentelemetry]'
1717
1818
Once these requirements are satisfied, you can instrument your Litestar application by creating an instance
19-
of :class:`OpenTelemetryConfig <litestar.contrib.opentelemetry.OpenTelemetryConfig>` and passing the middleware it creates to
20-
the Litestar constructor:
19+
of :class:`OpenTelemetryConfig <litestar.plugins.opentelemetry.OpenTelemetryConfig>` and passing it to the plugin:
2120

2221
.. code-block:: python
2322
2423
from litestar import Litestar
25-
from litestar.contrib.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
24+
from litestar.plugins.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
2625
2726
open_telemetry_config = OpenTelemetryConfig()
2827
@@ -32,5 +31,110 @@ The above example will work out of the box if you configure a global ``tracer_pr
3231
exporter to use these (see the
3332
`OpenTelemetry Exporter docs <https://opentelemetry.io/docs/instrumentation/python/exporters/>`_ for further details).
3433

35-
You can also pass con figuration to the ``OpenTelemetryConfig`` telling it which providers to use. Consult
36-
:class:`reference docs <litestar.contrib.opentelemetry.OpenTelemetryConfig>` regarding the configuration options you can use.
34+
You can also pass configuration to the ``OpenTelemetryConfig`` telling it which providers to use. Consult the
35+
:class:`OpenTelemetryConfig <litestar.plugins.opentelemetry.OpenTelemetryConfig>` reference docs for all available configuration options.
36+
37+
Configuration options
38+
---------------------
39+
40+
Provider configuration
41+
~~~~~~~~~~~~~~~~~~~~~~
42+
43+
The following options allow you to configure custom OpenTelemetry providers:
44+
45+
- ``tracer_provider``: Custom ``TracerProvider`` instance. If omitted, the globally configured provider is used.
46+
- ``meter_provider``: Custom ``MeterProvider`` instance. If omitted, the globally configured provider is used.
47+
- ``meter``: Custom ``Meter`` instance. If omitted, the meter from the provider is used.
48+
49+
Example with custom tracer provider:
50+
51+
.. code-block:: python
52+
53+
from opentelemetry.sdk.trace import TracerProvider
54+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
55+
from litestar import Litestar
56+
from litestar.plugins.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
57+
58+
# Configure a custom tracer provider
59+
tracer_provider = TracerProvider()
60+
tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
61+
62+
config = OpenTelemetryConfig(tracer_provider=tracer_provider)
63+
app = Litestar(plugins=[OpenTelemetryPlugin(config)])
64+
65+
Hook handlers
66+
~~~~~~~~~~~~~
67+
68+
Hook handlers allow you to customize span behavior at various points in the request lifecycle:
69+
70+
- ``server_request_hook_handler``: Called with the server span and ASGI scope for every incoming request.
71+
- ``client_request_hook_handler``: Called with the internal span when the ``receive`` method is invoked.
72+
- ``client_response_hook_handler``: Called with the internal span when the ``send`` method is invoked.
73+
74+
Example adding custom attributes:
75+
76+
.. code-block:: python
77+
78+
from opentelemetry.trace import Span
79+
from litestar.plugins.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
80+
81+
def request_hook(span: Span, scope: dict) -> None:
82+
span.set_attribute("custom.user_agent", scope.get("headers", {}).get("user-agent", ""))
83+
84+
config = OpenTelemetryConfig(server_request_hook_handler=request_hook)
85+
app = Litestar(plugins=[OpenTelemetryPlugin(config)])
86+
87+
URL filtering
88+
~~~~~~~~~~~~~
89+
90+
You can exclude specific URLs from instrumentation:
91+
92+
- ``exclude``: Pattern or list of patterns to exclude from instrumentation.
93+
- ``exclude_opt_key``: Route option key to disable instrumentation on a per-route basis.
94+
- ``exclude_urls_env_key`` (default ``"LITESTAR"``): Environment variable prefix for excluded URLs. With the default, the environment variable ``LITESTAR_EXCLUDED_URLS`` will be checked.
95+
96+
Example excluding health check endpoints:
97+
98+
.. code-block:: python
99+
100+
from litestar.plugins.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
101+
102+
config = OpenTelemetryConfig(
103+
exclude=["/health", "/readiness", "/metrics"]
104+
)
105+
app = Litestar(plugins=[OpenTelemetryPlugin(config)])
106+
107+
Advanced options
108+
~~~~~~~~~~~~~~~~
109+
110+
- ``scope_span_details_extractor``: Callback that returns a tuple of ``(span_name, attributes)`` for customizing span details from the ASGI scope.
111+
- ``scopes``: ASGI scope types to process (e.g., ``{"http", "websocket"}``). If ``None``, both HTTP and WebSocket are processed.
112+
- ``middleware_class``: Custom middleware class. Must be a subclass of ``OpenTelemetryInstrumentationMiddleware``.
113+
114+
Litestar-specific spans
115+
------------------------
116+
117+
Litestar can automatically create spans for framework events. With :class:`OpenTelemetryConfig <litestar.plugins.opentelemetry.OpenTelemetryConfig>`,
118+
all instrumentation options are enabled by default:
119+
120+
- ``instrument_guards`` (default ``True``): Create ``guard.*`` spans for each guard executed within a request.
121+
- ``instrument_events`` (default ``True``): Create ``event.emit.*`` and ``event.listener.*`` spans for the event emitter.
122+
- ``instrument_lifecycle`` (default ``True``): Wrap application startup/shutdown hooks with ``lifecycle.*`` spans.
123+
- ``instrument_cli`` (default ``True``): Emit ``cli.*`` spans for Litestar CLI commands.
124+
125+
Example with selective instrumentation:
126+
127+
.. code-block:: python
128+
129+
from litestar import Litestar
130+
from litestar.plugins.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin
131+
132+
# Enable only guard and event instrumentation
133+
config = OpenTelemetryConfig(
134+
instrument_guards=True,
135+
instrument_events=True,
136+
instrument_lifecycle=False,
137+
instrument_cli=False
138+
)
139+
140+
app = Litestar(plugins=[OpenTelemetryPlugin(config)])
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
from .config import OpenTelemetryConfig
2+
from .instrumentation import (
3+
create_span,
4+
instrument_channel_operation,
5+
instrument_dependency,
6+
instrument_guard,
7+
instrument_lifecycle_event,
8+
)
29
from .middleware import OpenTelemetryInstrumentationMiddleware
310
from .plugin import OpenTelemetryPlugin
411

512
__all__ = (
613
"OpenTelemetryConfig",
714
"OpenTelemetryInstrumentationMiddleware",
815
"OpenTelemetryPlugin",
16+
"create_span",
17+
"instrument_channel_operation",
18+
"instrument_dependency",
19+
"instrument_guard",
20+
"instrument_lifecycle_event",
921
)

litestar/plugins/opentelemetry/config.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
__all__ = ("OpenTelemetryConfig",)
1414

1515

16-
try:
16+
try: # pragma: no cover - dependency is optional at runtime
1717
import opentelemetry # noqa: F401
18-
except ImportError as e:
18+
from opentelemetry.trace import Span, TracerProvider # pyright: ignore
19+
except ImportError as e: # pragma: no cover
1920
raise MissingDependencyException("opentelemetry") from e
2021

21-
22-
from opentelemetry.trace import Span, TracerProvider # pyright: ignore
23-
2422
if TYPE_CHECKING:
2523
from opentelemetry.metrics import Meter, MeterProvider
2624

25+
from litestar.plugins.opentelemetry.plugin import OpenTelemetryPlugin
2726
from litestar.types import Scope, Scopes
2827

2928
OpenTelemetryHookHandler = Callable[[Span, dict], None]
@@ -87,6 +86,18 @@ class OpenTelemetryConfig:
8786
Should be a subclass of OpenTelemetry
8887
InstrumentationMiddleware][litestar.contrib.opentelemetry.OpenTelemetryInstrumentationMiddleware].
8988
"""
89+
instrument_guards: bool = field(default=True)
90+
"""Whether to automatically instrument Litestar route guards."""
91+
instrument_events: bool = field(default=True)
92+
"""Whether to instrument event listeners and emitters."""
93+
instrument_lifecycle: bool = field(default=True)
94+
"""Whether to instrument application lifecycle hooks (startup/shutdown)."""
95+
instrument_middleware: bool = field(default=False)
96+
"""Reserved for future middleware-level instrumentation. Currently unused and left for forward compatibility."""
97+
instrument_cli: bool = field(default=True)
98+
"""Whether to instrument Litestar CLI commands (click entrypoints)."""
99+
100+
_plugin: OpenTelemetryPlugin | None = field(default=None, init=False, repr=False)
90101

91102
@property
92103
def middleware(self) -> DefineMiddleware:
@@ -100,3 +111,17 @@ def middleware(self) -> DefineMiddleware:
100111
An instance of ``DefineMiddleware``.
101112
"""
102113
return DefineMiddleware(self.middleware_class, config=self)
114+
115+
@property
116+
def plugin(self) -> OpenTelemetryPlugin:
117+
"""Convenience accessor to build an :class:`OpenTelemetryPlugin` from this config.
118+
119+
Returns:
120+
A plugin instance wired with this configuration.
121+
"""
122+
123+
if self._plugin is None:
124+
from litestar.plugins.opentelemetry.plugin import OpenTelemetryPlugin
125+
126+
self._plugin = OpenTelemetryPlugin(self)
127+
return self._plugin

0 commit comments

Comments
 (0)