Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit c38c71b

Browse files
authored
Add FastAPI extension (#1124)
1 parent fd064f4 commit c38c71b

File tree

14 files changed

+514
-5
lines changed

14 files changed

+514
-5
lines changed

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Trace Exporter
241241
.. _Datadog: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-datadog
242242
.. _Django: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-django
243243
.. _Flask: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-flask
244+
.. _FastAPI: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-fastapi
244245
.. _gevent: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-gevent
245246
.. _Google Cloud Client Libraries: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-google-cloud-clientlibs
246247
.. _gRPC: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-grpc
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
## 0.1.0
6+
7+
- Initial version
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
OpenCensus FastAPI Integration
2+
============================================================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opencensus-ext-fastapi.svg
7+
:target: https://pypi.org/project/opencensus-ext-fastapi/
8+
9+
Installation
10+
------------
11+
12+
::
13+
14+
pip install opencensus-ext-fastapi
15+
16+
Usage
17+
-----
18+
19+
.. code:: python
20+
21+
from fastapi import FastAPI
22+
from opencensus.ext.fastapi.fastapi_middleware import FastAPIMiddleware
23+
24+
app = FastAPI(__name__)
25+
app.add_middleware(FastAPIMiddleware)
26+
27+
@app.get('/')
28+
def hello():
29+
return 'Hello World!'
30+
31+
Additional configuration can be provided, please read
32+
`Customization <https://github.com/census-instrumentation/opencensus-python#customization>`_
33+
for a complete reference.
34+
35+
.. code:: python
36+
37+
app.add_middleware(
38+
FastAPIMiddleware,
39+
excludelist_paths=["paths"],
40+
excludelist_hostnames=["hostnames"],
41+
sampler=sampler,
42+
exporter=exporter,
43+
propagator=propagator,
44+
)
45+
46+
47+
References
48+
----------
49+
50+
* `OpenCensus Project <https://opencensus.io/>`_
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Copyright 2022, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import traceback
17+
from typing import Union
18+
19+
from starlette.middleware.base import (
20+
BaseHTTPMiddleware,
21+
RequestResponseEndpoint,
22+
)
23+
from starlette.requests import Request
24+
from starlette.responses import Response
25+
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
26+
from starlette.types import ASGIApp
27+
28+
from opencensus.trace import (
29+
attributes_helper,
30+
execution_context,
31+
integrations,
32+
print_exporter,
33+
samplers,
34+
)
35+
from opencensus.trace import span as span_module
36+
from opencensus.trace import tracer as tracer_module
37+
from opencensus.trace import utils
38+
from opencensus.trace.blank_span import BlankSpan
39+
from opencensus.trace.propagation import trace_context_http_header_format
40+
from opencensus.trace.span import Span
41+
42+
HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES["HTTP_HOST"]
43+
HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES["HTTP_METHOD"]
44+
HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES["HTTP_PATH"]
45+
HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES["HTTP_ROUTE"]
46+
HTTP_URL = attributes_helper.COMMON_ATTRIBUTES["HTTP_URL"]
47+
HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]
48+
ERROR_MESSAGE = attributes_helper.COMMON_ATTRIBUTES['ERROR_MESSAGE']
49+
ERROR_NAME = attributes_helper.COMMON_ATTRIBUTES['ERROR_NAME']
50+
STACKTRACE = attributes_helper.COMMON_ATTRIBUTES["STACKTRACE"]
51+
52+
module_logger = logging.getLogger(__name__)
53+
54+
55+
class FastAPIMiddleware(BaseHTTPMiddleware):
56+
"""FastAPI middleware to automatically trace requests.
57+
58+
:type app: :class: `~fastapi.FastAPI`
59+
:param app: A fastapi application.
60+
61+
:type excludelist_paths: list
62+
:param excludelist_paths: Paths that do not trace.
63+
64+
:type excludelist_hostnames: list
65+
:param excludelist_hostnames: Hostnames that do not trace.
66+
67+
:type sampler: :class:`~opencensus.trace.samplers.base.Sampler`
68+
:param sampler: A sampler. It should extend from the base
69+
:class:`.Sampler` type and implement
70+
:meth:`.Sampler.should_sample`. Defaults to
71+
:class:`.ProbabilitySampler`. Other options include
72+
:class:`.AlwaysOnSampler` and :class:`.AlwaysOffSampler`.
73+
74+
:type exporter: :class:`~opencensus.trace.base_exporter.exporter`
75+
:param exporter: An exporter. Default to
76+
:class:`.PrintExporter`. The rest options are
77+
:class:`.FileExporter`, :class:`.LoggingExporter` and
78+
trace exporter extensions.
79+
80+
:type propagator: :class: 'object'
81+
:param propagator: A propagator. Default to
82+
:class:`.TraceContextPropagator`. The rest options
83+
are :class:`.BinaryFormatPropagator`,
84+
:class:`.GoogleCloudFormatPropagator` and
85+
:class:`.TextFormatPropagator`.
86+
"""
87+
88+
def __init__(
89+
self,
90+
app: ASGIApp,
91+
excludelist_paths=None,
92+
excludelist_hostnames=None,
93+
sampler=None,
94+
exporter=None,
95+
propagator=None,
96+
) -> None:
97+
super().__init__(app)
98+
self.excludelist_paths = excludelist_paths
99+
self.excludelist_hostnames = excludelist_hostnames
100+
self.sampler = sampler or samplers.AlwaysOnSampler()
101+
self.exporter = exporter or print_exporter.PrintExporter()
102+
self.propagator = (
103+
propagator or
104+
trace_context_http_header_format.TraceContextPropagator()
105+
)
106+
107+
# pylint: disable=protected-access
108+
integrations.add_integration(integrations._Integrations.FASTAPI)
109+
110+
def _prepare_tracer(self, request: Request) -> tracer_module.Tracer:
111+
span_context = self.propagator.from_headers(request.headers)
112+
tracer = tracer_module.Tracer(
113+
span_context=span_context,
114+
sampler=self.sampler,
115+
exporter=self.exporter,
116+
propagator=self.propagator,
117+
)
118+
return tracer
119+
120+
def _before_request(self, span: Union[Span, BlankSpan], request: Request):
121+
span.span_kind = span_module.SpanKind.SERVER
122+
span.name = "[{}]{}".format(request.method, request.url)
123+
span.add_attribute(HTTP_HOST, request.url.hostname)
124+
span.add_attribute(HTTP_METHOD, request.method)
125+
span.add_attribute(HTTP_PATH, request.url.path)
126+
span.add_attribute(HTTP_URL, str(request.url))
127+
span.add_attribute(HTTP_ROUTE, request.url.path)
128+
execution_context.set_opencensus_attr(
129+
"excludelist_hostnames", self.excludelist_hostnames
130+
)
131+
132+
def _after_request(self, span: Union[Span, BlankSpan], response: Response):
133+
span.add_attribute(HTTP_STATUS_CODE, response.status_code)
134+
135+
def _handle_exception(self,
136+
span: Union[Span, BlankSpan], exception: Exception):
137+
span.add_attribute(ERROR_NAME, exception.__class__.__name__)
138+
span.add_attribute(ERROR_MESSAGE, str(exception))
139+
span.add_attribute(
140+
STACKTRACE,
141+
"\n".join(traceback.format_tb(exception.__traceback__)))
142+
span.add_attribute(HTTP_STATUS_CODE, HTTP_500_INTERNAL_SERVER_ERROR)
143+
144+
async def dispatch(
145+
self, request: Request, call_next: RequestResponseEndpoint
146+
) -> Response:
147+
148+
# Do not trace if the url is in the exclude list
149+
if utils.disable_tracing_url(str(request.url), self.excludelist_paths):
150+
return await call_next(request)
151+
152+
try:
153+
tracer = self._prepare_tracer(request)
154+
span = tracer.start_span()
155+
except Exception: # pragma: NO COVER
156+
module_logger.error("Failed to trace request", exc_info=True)
157+
return await call_next(request)
158+
159+
try:
160+
self._before_request(span, request)
161+
except Exception: # pragma: NO COVER
162+
module_logger.error("Failed to trace request", exc_info=True)
163+
164+
try:
165+
response = await call_next(request)
166+
except Exception as err: # pragma: NO COVER
167+
try:
168+
self._handle_exception(span, err)
169+
tracer.end_span()
170+
tracer.finish()
171+
except Exception: # pragma: NO COVER
172+
module_logger.error("Failed to trace response", exc_info=True)
173+
raise err
174+
175+
try:
176+
self._after_request(span, response)
177+
tracer.end_span()
178+
tracer.finish()
179+
except Exception: # pragma: NO COVER
180+
module_logger.error("Failed to trace response", exc_info=True)
181+
182+
return response
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bdist_wheel]
2+
universal = 1
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2022, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from setuptools import find_packages, setup
16+
17+
from version import __version__
18+
19+
setup(
20+
name='opencensus-ext-fastapi',
21+
version=__version__, # noqa
22+
author='OpenCensus Authors',
23+
author_email='[email protected]',
24+
classifiers=[
25+
'Intended Audience :: Developers',
26+
'Development Status :: 3 - Alpha',
27+
'Intended Audience :: Developers',
28+
'License :: OSI Approved :: Apache Software License',
29+
'Programming Language :: Python',
30+
'Programming Language :: Python :: 3',
31+
'Programming Language :: Python :: 3.6',
32+
'Programming Language :: Python :: 3.7',
33+
'Programming Language :: Python :: 3.8',
34+
'Programming Language :: Python :: 3.9',
35+
],
36+
description='OpenCensus FastAPI Integration',
37+
include_package_data=True,
38+
long_description=open('README.rst').read(),
39+
install_requires=[
40+
'fastapi >= 0.75.2',
41+
'opencensus >= 0.9.dev0, < 1.0.0',
42+
],
43+
extras_require={},
44+
license='Apache-2.0',
45+
packages=find_packages(exclude=('tests',)),
46+
namespace_packages=[],
47+
url='https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-fastapi', # noqa: E501
48+
zip_safe=False,
49+
)

0 commit comments

Comments
 (0)