ray-llm
357 строк · 13.7 Кб
1# This is a lightweight fork of Opentelemetry's FastAPI instrmentor that fixes
2# an issue where they create a `meter` from a `meter_provider` inside the
3# instrumentor. Unfortunately the `meter` contains a threading.Lock, so it
4# isn't safe to serialize which explodes when Ray Serve tries to serialize it.
5# This is totally unncessary since if they didn't do that, it does it anyways on
6# the other side. Forking to quick fix instead of waiting for upstream to fix.
7
8# Copyright The OpenTelemetry Authors
9#
10# Licensed under the Apache License, Version 2.0 (the "License");
11# you may not use this file except in compliance with the License.
12# You may obtain a copy of the License at
13#
14# http://www.apache.org/licenses/LICENSE-2.0
15#
16# Unless required by applicable law or agreed to in writing, software
17# distributed under the License is distributed on an "AS IS" BASIS,
18# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19# See the License for the specific language governing permissions and
20# limitations under the License.
21
22"""
23Usage
24-----
25
26.. code-block:: python
27
28import fastapi
29from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
30
31app = fastapi.FastAPI()
32
33@app.get("/foobar")
34async def foobar():
35return {"message": "hello world"}
36
37FastAPIInstrumentor.instrument_app(app)
38
39Configuration
40-------------
41
42Exclude lists
43*************
44To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS``
45(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the
46URLs.
47
48For example,
49
50::
51
52export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="client/.*/info,healthcheck"
53
54will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
55
56You can also pass comma delimited regexes directly to the ``instrument_app`` method:
57
58.. code-block:: python
59
60FastAPIInstrumentor.instrument_app(app, excluded_urls="client/.*/info,healthcheck")
61
62Request/Response hooks
63**********************
64
65This instrumentation supports request and response hooks. These are functions that get called
66right after a span is created for a request and right before the span is finished for the response.
67
68- The server request hook is passed a server span and ASGI scope object for every incoming request.
69- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
70- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
71
72.. code-block:: python
73
74def server_request_hook(span: Span, scope: dict):
75if span and span.is_recording():
76span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
77
78def client_request_hook(span: Span, scope: dict):
79if span and span.is_recording():
80span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
81
82def client_response_hook(span: Span, message: dict):
83if span and span.is_recording():
84span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
85
86FastAPIInstrumentor().instrument(server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook)
87
88Capture HTTP request and response headers
89*****************************************
90You can configure the agent to capture specified HTTP headers as span attributes, according to the
91`semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
92
93Request headers
94***************
95To capture HTTP request headers as span attributes, set the environment variable
96``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
97
98For example,
99::
100
101export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
102
103will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
104
105Request header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
106variable will capture the header named ``custom-header``.
107
108Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
109::
110
111export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
112
113Would match all request headers that start with ``Accept`` and ``X-``.
114
115To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``.
116::
117
118export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
119
120The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
121is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
122single item list containing all the header values.
123
124For example:
125``http.request.header.custom_request_header = ["<value1>,<value2>"]``
126
127Response headers
128****************
129To capture HTTP response headers as span attributes, set the environment variable
130``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
131
132For example,
133::
134
135export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
136
137will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
138
139Response header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
140variable will capture the header named ``custom-header``.
141
142Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
143::
144
145export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
146
147Would match all response headers that start with ``Content`` and ``X-``.
148
149To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``.
150::
151
152export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
153
154The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
155is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
156single item list containing all the header values.
157
158For example:
159``http.response.header.custom_response_header = ["<value1>,<value2>"]``
160
161Sanitizing headers
162******************
163In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
164etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
165to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
166matched in a case-insensitive manner.
167
168For example,
169::
170
171export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
172
173will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
174
175Note:
176The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
177
178API
179---
180"""
181import logging182import typing183from typing import Collection184
185import fastapi186from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware187from opentelemetry.instrumentation.fastapi.package import _instruments188from opentelemetry.instrumentation.instrumentor import BaseInstrumentor189from opentelemetry.semconv.trace import SpanAttributes190from opentelemetry.trace import Span191from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls192from starlette.routing import Match193
194_excluded_urls_from_env = get_excluded_urls("FASTAPI")195_logger = logging.getLogger(__name__)196
197_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]198_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]199_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]200
201
202class FastAPIInstrumentor(BaseInstrumentor):203"""An instrumentor for FastAPI204
205See `BaseInstrumentor`
206"""
207
208_original_fastapi = None209
210@staticmethod211def instrument_app(212app: fastapi.FastAPI,213server_request_hook: _ServerRequestHookT = None,214client_request_hook: _ClientRequestHookT = None,215client_response_hook: _ClientResponseHookT = None,216tracer_provider=None,217meter_provider=None,218excluded_urls=None,219):220"""Instrument an uninstrumented FastAPI application."""221if not hasattr(app, "_is_instrumented_by_opentelemetry"):222app._is_instrumented_by_opentelemetry = False223
224if not getattr(app, "_is_instrumented_by_opentelemetry", False):225if excluded_urls is None:226excluded_urls = _excluded_urls_from_env227else:228excluded_urls = parse_excluded_urls(excluded_urls)229
230app.add_middleware(231OpenTelemetryMiddleware,232excluded_urls=excluded_urls,233default_span_details=_get_default_span_details,234server_request_hook=server_request_hook,235client_request_hook=client_request_hook,236client_response_hook=client_response_hook,237tracer_provider=tracer_provider,238meter_provider=meter_provider,239)240app._is_instrumented_by_opentelemetry = True241if app not in _InstrumentedFastAPI._instrumented_fastapi_apps:242_InstrumentedFastAPI._instrumented_fastapi_apps.add(app)243else:244_logger.warning(245"Attempting to instrument FastAPI app while already instrumented"246)247
248@staticmethod249def uninstrument_app(app: fastapi.FastAPI):250app.user_middleware = [251x for x in app.user_middleware if x.cls is not OpenTelemetryMiddleware252]253app.middleware_stack = app.build_middleware_stack()254app._is_instrumented_by_opentelemetry = False255
256def instrumentation_dependencies(self) -> Collection[str]:257return _instruments258
259def _instrument(self, **kwargs):260self._original_fastapi = fastapi.FastAPI261_InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider")262_InstrumentedFastAPI._server_request_hook = kwargs.get("server_request_hook")263_InstrumentedFastAPI._client_request_hook = kwargs.get("client_request_hook")264_InstrumentedFastAPI._client_response_hook = kwargs.get("client_response_hook")265_excluded_urls = kwargs.get("excluded_urls")266_InstrumentedFastAPI._excluded_urls = (267_excluded_urls_from_env
268if _excluded_urls is None269else parse_excluded_urls(_excluded_urls)270)271_InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")272fastapi.FastAPI = _InstrumentedFastAPI273
274def _uninstrument(self, **kwargs):275for instance in _InstrumentedFastAPI._instrumented_fastapi_apps:276self.uninstrument_app(instance)277_InstrumentedFastAPI._instrumented_fastapi_apps.clear()278fastapi.FastAPI = self._original_fastapi279
280
281class _InstrumentedFastAPI(fastapi.FastAPI):282_tracer_provider = None283_meter_provider = None284_excluded_urls = None285_server_request_hook: _ServerRequestHookT = None286_client_request_hook: _ClientRequestHookT = None287_client_response_hook: _ClientResponseHookT = None288_instrumented_fastapi_apps: typing.Set[fastapi.FastAPI] = set()289
290def __init__(self, *args, **kwargs):291super().__init__(*args, **kwargs)292self.add_middleware(293OpenTelemetryMiddleware,294excluded_urls=_InstrumentedFastAPI._excluded_urls,295default_span_details=_get_default_span_details,296server_request_hook=_InstrumentedFastAPI._server_request_hook,297client_request_hook=_InstrumentedFastAPI._client_request_hook,298client_response_hook=_InstrumentedFastAPI._client_response_hook,299tracer_provider=_InstrumentedFastAPI._tracer_provider,300meter_provider=_InstrumentedFastAPI._meter_provider,301)302
303self._is_instrumented_by_opentelemetry = True304_InstrumentedFastAPI._instrumented_fastapi_apps.add(self)305
306def __del__(self):307if self in _InstrumentedFastAPI._instrumented_fastapi_apps:308_InstrumentedFastAPI._instrumented_fastapi_apps.remove(self)309
310
311def _get_route_details(scope):312"""313Function to retrieve Starlette route from scope.
314
315TODO: there is currently no way to retrieve http.route from
316a starlette application from scope.
317See: https://github.com/encode/starlette/pull/804
318
319Args:
320scope: A Starlette scope
321Returns:
322A string containing the route or None
323"""
324app = scope["app"]325route = None326
327for starlette_route in app.routes:328match, _ = starlette_route.matches(scope)329if match == Match.FULL:330route = starlette_route.path331break332if match == Match.PARTIAL:333route = starlette_route.path334return route335
336
337def _get_default_span_details(scope):338"""339Callback to retrieve span name and attributes from scope.
340
341Args:
342scope: A Starlette scope
343Returns:
344A tuple of span name and attributes
345"""
346route = _get_route_details(scope)347method = scope.get("method", "")348attributes = {}349if route:350attributes[SpanAttributes.HTTP_ROUTE] = route351if method and route: # http352span_name = f"{method} {route}"353elif route: # websocket354span_name = route355else: # fallback356span_name = method357return span_name, attributes358