Skip to content

Commit

Permalink
Add FastAPI extension (#1124)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikait authored Mar 9, 2023
1 parent fd064f4 commit c38c71b
Show file tree
Hide file tree
Showing 14 changed files with 514 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ Trace Exporter
.. _Datadog: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-datadog
.. _Django: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-django
.. _Flask: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-flask
.. _FastAPI: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-fastapi
.. _gevent: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-gevent
.. _Google Cloud Client Libraries: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-google-cloud-clientlibs
.. _gRPC: https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-grpc
Expand Down
7 changes: 7 additions & 0 deletions contrib/opencensus-ext-fastapi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## Unreleased

## 0.1.0

- Initial version
50 changes: 50 additions & 0 deletions contrib/opencensus-ext-fastapi/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
OpenCensus FastAPI Integration
============================================================================

|pypi|

.. |pypi| image:: https://badge.fury.io/py/opencensus-ext-fastapi.svg
:target: https://pypi.org/project/opencensus-ext-fastapi/

Installation
------------

::

pip install opencensus-ext-fastapi

Usage
-----

.. code:: python
from fastapi import FastAPI
from opencensus.ext.fastapi.fastapi_middleware import FastAPIMiddleware
app = FastAPI(__name__)
app.add_middleware(FastAPIMiddleware)
@app.get('/')
def hello():
return 'Hello World!'
Additional configuration can be provided, please read
`Customization <https://github.com/census-instrumentation/opencensus-python#customization>`_
for a complete reference.

.. code:: python
app.add_middleware(
FastAPIMiddleware,
excludelist_paths=["paths"],
excludelist_hostnames=["hostnames"],
sampler=sampler,
exporter=exporter,
propagator=propagator,
)
References
----------

* `OpenCensus Project <https://opencensus.io/>`_
1 change: 1 addition & 0 deletions contrib/opencensus-ext-fastapi/opencensus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
1 change: 1 addition & 0 deletions contrib/opencensus-ext-fastapi/opencensus/ext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Copyright 2022, OpenCensus Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import traceback
from typing import Union

from starlette.middleware.base import (
BaseHTTPMiddleware,
RequestResponseEndpoint,
)
from starlette.requests import Request
from starlette.responses import Response
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
from starlette.types import ASGIApp

from opencensus.trace import (
attributes_helper,
execution_context,
integrations,
print_exporter,
samplers,
)
from opencensus.trace import span as span_module
from opencensus.trace import tracer as tracer_module
from opencensus.trace import utils
from opencensus.trace.blank_span import BlankSpan
from opencensus.trace.propagation import trace_context_http_header_format
from opencensus.trace.span import Span

HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES["HTTP_HOST"]
HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES["HTTP_METHOD"]
HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES["HTTP_PATH"]
HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES["HTTP_ROUTE"]
HTTP_URL = attributes_helper.COMMON_ATTRIBUTES["HTTP_URL"]
HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]
ERROR_MESSAGE = attributes_helper.COMMON_ATTRIBUTES['ERROR_MESSAGE']
ERROR_NAME = attributes_helper.COMMON_ATTRIBUTES['ERROR_NAME']
STACKTRACE = attributes_helper.COMMON_ATTRIBUTES["STACKTRACE"]

module_logger = logging.getLogger(__name__)


class FastAPIMiddleware(BaseHTTPMiddleware):
"""FastAPI middleware to automatically trace requests.
:type app: :class: `~fastapi.FastAPI`
:param app: A fastapi application.
:type excludelist_paths: list
:param excludelist_paths: Paths that do not trace.
:type excludelist_hostnames: list
:param excludelist_hostnames: Hostnames that do not trace.
:type sampler: :class:`~opencensus.trace.samplers.base.Sampler`
:param sampler: A sampler. It should extend from the base
:class:`.Sampler` type and implement
:meth:`.Sampler.should_sample`. Defaults to
:class:`.ProbabilitySampler`. Other options include
:class:`.AlwaysOnSampler` and :class:`.AlwaysOffSampler`.
:type exporter: :class:`~opencensus.trace.base_exporter.exporter`
:param exporter: An exporter. Default to
:class:`.PrintExporter`. The rest options are
:class:`.FileExporter`, :class:`.LoggingExporter` and
trace exporter extensions.
:type propagator: :class: 'object'
:param propagator: A propagator. Default to
:class:`.TraceContextPropagator`. The rest options
are :class:`.BinaryFormatPropagator`,
:class:`.GoogleCloudFormatPropagator` and
:class:`.TextFormatPropagator`.
"""

def __init__(
self,
app: ASGIApp,
excludelist_paths=None,
excludelist_hostnames=None,
sampler=None,
exporter=None,
propagator=None,
) -> None:
super().__init__(app)
self.excludelist_paths = excludelist_paths
self.excludelist_hostnames = excludelist_hostnames
self.sampler = sampler or samplers.AlwaysOnSampler()
self.exporter = exporter or print_exporter.PrintExporter()
self.propagator = (
propagator or
trace_context_http_header_format.TraceContextPropagator()
)

# pylint: disable=protected-access
integrations.add_integration(integrations._Integrations.FASTAPI)

def _prepare_tracer(self, request: Request) -> tracer_module.Tracer:
span_context = self.propagator.from_headers(request.headers)
tracer = tracer_module.Tracer(
span_context=span_context,
sampler=self.sampler,
exporter=self.exporter,
propagator=self.propagator,
)
return tracer

def _before_request(self, span: Union[Span, BlankSpan], request: Request):
span.span_kind = span_module.SpanKind.SERVER
span.name = "[{}]{}".format(request.method, request.url)
span.add_attribute(HTTP_HOST, request.url.hostname)
span.add_attribute(HTTP_METHOD, request.method)
span.add_attribute(HTTP_PATH, request.url.path)
span.add_attribute(HTTP_URL, str(request.url))
span.add_attribute(HTTP_ROUTE, request.url.path)
execution_context.set_opencensus_attr(
"excludelist_hostnames", self.excludelist_hostnames
)

def _after_request(self, span: Union[Span, BlankSpan], response: Response):
span.add_attribute(HTTP_STATUS_CODE, response.status_code)

def _handle_exception(self,
span: Union[Span, BlankSpan], exception: Exception):
span.add_attribute(ERROR_NAME, exception.__class__.__name__)
span.add_attribute(ERROR_MESSAGE, str(exception))
span.add_attribute(
STACKTRACE,
"\n".join(traceback.format_tb(exception.__traceback__)))
span.add_attribute(HTTP_STATUS_CODE, HTTP_500_INTERNAL_SERVER_ERROR)

async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:

# Do not trace if the url is in the exclude list
if utils.disable_tracing_url(str(request.url), self.excludelist_paths):
return await call_next(request)

try:
tracer = self._prepare_tracer(request)
span = tracer.start_span()
except Exception: # pragma: NO COVER
module_logger.error("Failed to trace request", exc_info=True)
return await call_next(request)

try:
self._before_request(span, request)
except Exception: # pragma: NO COVER
module_logger.error("Failed to trace request", exc_info=True)

try:
response = await call_next(request)
except Exception as err: # pragma: NO COVER
try:
self._handle_exception(span, err)
tracer.end_span()
tracer.finish()
except Exception: # pragma: NO COVER
module_logger.error("Failed to trace response", exc_info=True)
raise err

try:
self._after_request(span, response)
tracer.end_span()
tracer.finish()
except Exception: # pragma: NO COVER
module_logger.error("Failed to trace response", exc_info=True)

return response
2 changes: 2 additions & 0 deletions contrib/opencensus-ext-fastapi/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1
49 changes: 49 additions & 0 deletions contrib/opencensus-ext-fastapi/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2022, OpenCensus Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from setuptools import find_packages, setup

from version import __version__

setup(
name='opencensus-ext-fastapi',
version=__version__, # noqa
author='OpenCensus Authors',
author_email='[email protected]',
classifiers=[
'Intended Audience :: Developers',
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
description='OpenCensus FastAPI Integration',
include_package_data=True,
long_description=open('README.rst').read(),
install_requires=[
'fastapi >= 0.75.2',
'opencensus >= 0.9.dev0, < 1.0.0',
],
extras_require={},
license='Apache-2.0',
packages=find_packages(exclude=('tests',)),
namespace_packages=[],
url='https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-fastapi', # noqa: E501
zip_safe=False,
)
Loading

0 comments on commit c38c71b

Please sign in to comment.