diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9351422c4..74dbffa31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,7 @@ on: jobs: build: - # 18.04 needed for python3.4 - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: # We use these variables to convert between tox and GHA version literals py27: 2.7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c222c430..b1585fe03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +# 0.11.2 +Released 2023-03-10 + +- Updated `azure`, `fastapi`,`flask` modules + # 0.11.1 Released 2023-01-18 diff --git a/README.rst b/README.rst index 5edf1ed77..8c8653a20 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/contrib/opencensus-ext-azure/CHANGELOG.md b/contrib/opencensus-ext-azure/CHANGELOG.md index d717d803f..3db97a53b 100644 --- a/contrib/opencensus-ext-azure/CHANGELOG.md +++ b/contrib/opencensus-ext-azure/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +## 1.1.9 + +Released 2023-03-10 + +- Fix export of exception information in traces +([#1187](https://github.com/census-instrumentation/opencensus-python/pull/1187)) +- Modify metrics exporter to include setting export interval to 60s +([#1193](https://github.com/census-instrumentation/opencensus-python/pull/1193)) + ## 1.1.8 Released 2023-01-18 diff --git a/contrib/opencensus-ext-azure/README.rst b/contrib/opencensus-ext-azure/README.rst index 1332c824b..1fbead631 100644 --- a/contrib/opencensus-ext-azure/README.rst +++ b/contrib/opencensus-ext-azure/README.rst @@ -166,8 +166,11 @@ The **Azure Monitor Metrics Exporter** allows you to export metrics to `Azure Mo def main(): # Enable metrics - # Set the interval in seconds in which you want to send metrics - exporter = metrics_exporter.new_metrics_exporter(connection_string='InstrumentationKey=') + # Set the interval in seconds to 60s, which is the time interval application insights + # aggregates your metrics + exporter = metrics_exporter.new_metrics_exporter( + connection_string='InstrumentationKey=' + ) view_manager.register_exporter(exporter) view_manager.register_view(CARROTS_VIEW) @@ -176,7 +179,6 @@ The **Azure Monitor Metrics Exporter** allows you to export metrics to `Azure Mo mmap.measure_int_put(CARROTS_MEASURE, 1000) mmap.record(tmap) - # Default export interval is every 15.0s print("Done recording metrics") @@ -196,10 +198,13 @@ The exporter also includes a set of performance counters that are exported to Az from opencensus.ext.azure import metrics_exporter def main(): - # All you need is the next line. You can disable performance counters by + # Performance counters are sent by default. You can disable performance counters by # passing in enable_standard_metrics=False into the constructor of # new_metrics_exporter() - _exporter = metrics_exporter.new_metrics_exporter(connection_string='InstrumentationKey=') + _exporter = metrics_exporter.new_metrics_exporter( + connection_string='InstrumentationKey=', + export_interval=60, + ) for i in range(100): print(psutil.virtual_memory()) @@ -256,8 +261,12 @@ Modifying Metrics def main(): # Enable metrics - # Set the interval in seconds in which you want to send metrics - exporter = metrics_exporter.new_metrics_exporter(connection_string='InstrumentationKey=') + # Set the interval in seconds to 60s, which is the time interval application insights + # aggregates your metrics + exporter = metrics_exporter.new_metrics_exporter( + connection_string='InstrumentationKey=', + export_interval=60, + ) exporter.add_telemetry_processor(callback_function) view_manager.register_exporter(exporter) @@ -267,7 +276,6 @@ Modifying Metrics mmap.measure_int_put(CARROTS_MEASURE, 1000) mmap.record(tmap) - # Default export interval is every 15.0s print("Done recording metrics") diff --git a/contrib/opencensus-ext-azure/examples/metrics/simple.py b/contrib/opencensus-ext-azure/examples/metrics/simple.py index 611d3b146..353843486 100644 --- a/contrib/opencensus-ext-azure/examples/metrics/simple.py +++ b/contrib/opencensus-ext-azure/examples/metrics/simple.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + from opencensus.ext.azure import metrics_exporter from opencensus.stats import aggregation as aggregation_module from opencensus.stats import measure as measure_module @@ -34,12 +36,12 @@ def main(): - # Enable metrics - # Set the interval in seconds in which you want to send metrics - # TODO: you need to specify the instrumentation key in a connection string - # and place it in the APPLICATIONINSIGHTS_CONNECTION_STRING - # environment variable. - exporter = metrics_exporter.new_metrics_exporter() + # Enable metrics. Set the interval in seconds to 60s, which is the time + # interval application insights aggregates your metrics + exporter = metrics_exporter.new_metrics_exporter( + connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"], + export_interval=60, + ) view_manager.register_exporter(exporter) view_manager.register_view(CARROTS_VIEW) diff --git a/contrib/opencensus-ext-azure/examples/metrics/standard.py b/contrib/opencensus-ext-azure/examples/metrics/standard.py deleted file mode 100644 index 3d726385c..000000000 --- a/contrib/opencensus-ext-azure/examples/metrics/standard.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2019, 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 time - -import psutil - -from opencensus.ext.azure import metrics_exporter - - -def main(): - # TODO: you need to specify the instrumentation key in a connection string - # and place it in the APPLICATIONINSIGHTS_CONNECTION_STRING - # environment variable. - # All you need is the next line. You can disable standard metrics by - # passing in enable_standard_metrics=False into the constructor of - # new_metrics_exporter() - _exporter = metrics_exporter.new_metrics_exporter() - - print(_exporter.max_batch_size) - for i in range(100): - print(psutil.virtual_memory()) - time.sleep(5) - - print("Done recording metrics") - - -if __name__ == "__main__": - main() diff --git a/contrib/opencensus-ext-azure/examples/metrics/sum.py b/contrib/opencensus-ext-azure/examples/metrics/sum.py index 355c72954..27db12949 100644 --- a/contrib/opencensus-ext-azure/examples/metrics/sum.py +++ b/contrib/opencensus-ext-azure/examples/metrics/sum.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import time from opencensus.ext.azure import metrics_exporter @@ -36,12 +37,12 @@ def main(): - # Enable metrics - # Set the interval in seconds in which you want to send metrics - # TODO: you need to specify the instrumentation key in a connection string - # and place it in the APPLICATIONINSIGHTS_CONNECTION_STRING - # environment variable. - exporter = metrics_exporter.new_metrics_exporter() + # Enable metrics. Set the interval in seconds to 60s, which is the time + # interval application insights aggregates your metrics + exporter = metrics_exporter.new_metrics_exporter( + connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"], + export_interval=60, + ) view_manager.register_exporter(exporter) view_manager.register_view(NUM_REQUESTS_VIEW) diff --git a/contrib/opencensus-ext-azure/opencensus/ext/azure/common/version.py b/contrib/opencensus-ext-azure/opencensus/ext/azure/common/version.py index 9d4c5b4c7..27bd84636 100644 --- a/contrib/opencensus-ext-azure/opencensus/ext/azure/common/version.py +++ b/contrib/opencensus-ext-azure/opencensus/ext/azure/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.1.8' +__version__ = '1.1.9' diff --git a/contrib/opencensus-ext-azure/opencensus/ext/azure/trace_exporter/__init__.py b/contrib/opencensus-ext-azure/opencensus/ext/azure/trace_exporter/__init__.py index bb29c1b30..8a54bc2cb 100644 --- a/contrib/opencensus-ext-azure/opencensus/ext/azure/trace_exporter/__init__.py +++ b/contrib/opencensus-ext-azure/opencensus/ext/azure/trace_exporter/__init__.py @@ -96,7 +96,8 @@ def span_data_to_envelope(self, sd): ) if sd.span_kind == SpanKind.SERVER: if ERROR_MESSAGE in sd.attributes: - envelope.name = 'Microsoft.ApplicationInsights.Exception' + exc_env = Envelope(**envelope) + exc_env.name = 'Microsoft.ApplicationInsights.Exception' data = ExceptionData( exceptions=[{ 'id': 1, @@ -107,8 +108,8 @@ def span_data_to_envelope(self, sd): 'parsedStack': sd.attributes.get(STACKTRACE, None) }], ) - envelope.data = Data(baseData=data, baseType='ExceptionData') - yield envelope + exc_env.data = Data(baseData=data, baseType='ExceptionData') + yield exc_env envelope.name = 'Microsoft.ApplicationInsights.Request' data = Request( diff --git a/contrib/opencensus-ext-azure/setup.py b/contrib/opencensus-ext-azure/setup.py index 75c415410..cfb54bd24 100644 --- a/contrib/opencensus-ext-azure/setup.py +++ b/contrib/opencensus-ext-azure/setup.py @@ -43,7 +43,7 @@ install_requires=[ 'azure-core >= 1.12.0, < 2.0.0', 'azure-identity >= 1.5.0, < 2.0.0', - 'opencensus >= 0.11.1, < 1.0.0', + 'opencensus >= 0.11.2, < 1.0.0', 'psutil >= 5.6.3', 'requests >= 2.19.0', ], diff --git a/contrib/opencensus-ext-azure/tests/test_azure_trace_exporter.py b/contrib/opencensus-ext-azure/tests/test_azure_trace_exporter.py index 1613b4dae..c8bbd9185 100644 --- a/contrib/opencensus-ext-azure/tests/test_azure_trace_exporter.py +++ b/contrib/opencensus-ext-azure/tests/test_azure_trace_exporter.py @@ -498,7 +498,7 @@ def test_span_data_to_envelope(self): 'RequestData') # SpanKind.SERVER HTTP - with exceptions - envelopes = exporter.span_data_to_envelope(SpanData( + envelopes = list(exporter.span_data_to_envelope(SpanData( name='test', context=SpanContext( trace_id='6e0c63257de34c90bf9efcd03927272e', @@ -529,9 +529,10 @@ def test_span_data_to_envelope(self): same_process_as_parent_span=None, child_span_count=None, span_kind=SpanKind.SERVER, - )) + ))) + self.assertEqual(len(envelopes), 2) - envelope = next(envelopes) + envelope = envelopes[0] self.assertEqual( envelope.iKey, '12345678-1234-5678-abcd-12345678abcd') @@ -551,7 +552,7 @@ def test_span_data_to_envelope(self): envelope.data.baseType, 'ExceptionData') - envelope = next(envelopes) + envelope = envelopes[1] self.assertEqual( envelope.iKey, '12345678-1234-5678-abcd-12345678abcd') diff --git a/contrib/opencensus-ext-fastapi/CHANGELOG.md b/contrib/opencensus-ext-fastapi/CHANGELOG.md new file mode 100644 index 000000000..f4c2570de --- /dev/null +++ b/contrib/opencensus-ext-fastapi/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## Unreleased + +## 0.1.0 + +Released 2023-03-10 + +- Initial version +([#1124](https://github.com/census-instrumentation/opencensus-python/pull/1124)) diff --git a/contrib/opencensus-ext-fastapi/README.rst b/contrib/opencensus-ext-fastapi/README.rst new file mode 100644 index 000000000..7946d56ed --- /dev/null +++ b/contrib/opencensus-ext-fastapi/README.rst @@ -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 `_ +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 `_ diff --git a/contrib/opencensus-ext-fastapi/opencensus/__init__.py b/contrib/opencensus-ext-fastapi/opencensus/__init__.py new file mode 100644 index 000000000..69e3be50d --- /dev/null +++ b/contrib/opencensus-ext-fastapi/opencensus/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/contrib/opencensus-ext-fastapi/opencensus/ext/__init__.py b/contrib/opencensus-ext-fastapi/opencensus/ext/__init__.py new file mode 100644 index 000000000..69e3be50d --- /dev/null +++ b/contrib/opencensus-ext-fastapi/opencensus/ext/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/__init__.py b/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/__init__.py new file mode 100644 index 000000000..69e3be50d --- /dev/null +++ b/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/fastapi_middleware.py b/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/fastapi_middleware.py new file mode 100644 index 000000000..6dfd1a812 --- /dev/null +++ b/contrib/opencensus-ext-fastapi/opencensus/ext/fastapi/fastapi_middleware.py @@ -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 diff --git a/contrib/opencensus-ext-fastapi/setup.cfg b/contrib/opencensus-ext-fastapi/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/contrib/opencensus-ext-fastapi/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/contrib/opencensus-ext-fastapi/setup.py b/contrib/opencensus-ext-fastapi/setup.py new file mode 100644 index 000000000..98a002643 --- /dev/null +++ b/contrib/opencensus-ext-fastapi/setup.py @@ -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='census-developers@googlegroups.com', + 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.11.2, < 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, +) diff --git a/contrib/opencensus-ext-fastapi/tests/test_fastapi_middleware.py b/contrib/opencensus-ext-fastapi/tests/test_fastapi_middleware.py new file mode 100644 index 000000000..e2d8f113f --- /dev/null +++ b/contrib/opencensus-ext-fastapi/tests/test_fastapi_middleware.py @@ -0,0 +1,197 @@ +# 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 sys +import traceback +import unittest +from unittest.mock import ANY + +import mock +from fastapi import FastAPI +from starlette.testclient import TestClient + +from opencensus.ext.fastapi.fastapi_middleware import FastAPIMiddleware +from opencensus.trace import print_exporter, samplers +from opencensus.trace import span as span_module +from opencensus.trace import tracer as tracer_module +from opencensus.trace.propagation import trace_context_http_header_format + + +class FastAPITestException(Exception): + pass + + +class TestFastAPIMiddleware(unittest.TestCase): + + def tearDown(self) -> None: + from opencensus.trace import execution_context + execution_context.clear() + + return super().tearDown() + + def create_app(self): + app = FastAPI() + + @app.get('/') + def index(): + return 'test fastapi trace' # pragma: NO COVER + + @app.get('/wiki/{entry}') + def wiki(entry): + return 'test fastapi trace' # pragma: NO COVER + + @app.get('/health') + def health_check(): + return 'test health check' # pragma: NO COVER + + @app.get('/error') + def error(): + raise FastAPITestException('test error') + + return app + + def test_constructor_default(self): + app = self.create_app() + middleware = FastAPIMiddleware(app) + + self.assertIs(middleware.app, app) + self.assertIsNone(middleware.excludelist_paths) + self.assertIsNone(middleware.excludelist_hostnames) + self.assertIsInstance(middleware.sampler, samplers.AlwaysOnSampler) + self.assertIsInstance(middleware.exporter, + print_exporter.PrintExporter) + self.assertIsInstance( + middleware.propagator, + trace_context_http_header_format.TraceContextPropagator) + + def test_constructor_explicit(self): + excludelist_paths = mock.Mock() + excludelist_hostnames = mock.Mock() + sampler = mock.Mock() + exporter = mock.Mock() + propagator = mock.Mock() + + app = self.create_app() + middleware = FastAPIMiddleware( + app=app, + excludelist_paths=excludelist_paths, + excludelist_hostnames=excludelist_hostnames, + sampler=sampler, + exporter=exporter, + propagator=propagator) + + self.assertEqual(middleware.app, app) + self.assertEqual(middleware.excludelist_paths, excludelist_paths) + self.assertEqual( + middleware.excludelist_hostnames, excludelist_hostnames) + self.assertEqual(middleware.sampler, sampler) + self.assertEqual(middleware.exporter, exporter) + self.assertEqual(middleware.propagator, propagator) + + @mock.patch.object(tracer_module.Tracer, "finish") + @mock.patch.object(tracer_module.Tracer, "end_span") + @mock.patch.object(tracer_module.Tracer, "start_span") + def test_request(self, mock_m1, mock_m2, mock_m3): + app = self.create_app() + app.add_middleware( + FastAPIMiddleware, sampler=samplers.AlwaysOnSampler()) + + test_client = TestClient(app) + test_client.get("/wiki/Rabbit") + + mock_span = mock_m1.return_value + self.assertEqual(mock_span.add_attribute.call_count, 6) + mock_span.add_attribute.assert_has_calls([ + mock.call("http.host", "testserver"), + mock.call("http.method", "GET"), + mock.call("http.path", "/wiki/Rabbit"), + mock.call("http.url", "http://testserver/wiki/Rabbit"), + mock.call("http.route", "/wiki/Rabbit"), + mock.call("http.status_code", 200) + ]) + mock_m2.assert_called_once() + mock_m3.assert_called_once() + + self.assertEqual( + mock_span.span_kind, + span_module.SpanKind.SERVER) + self.assertEqual( + mock_span.name, + "[{}]{}".format("GET", "http://testserver/wiki/Rabbit")) + + @mock.patch.object(FastAPIMiddleware, "_prepare_tracer") + def test_request_excludelist(self, mock_m): + app = self.create_app() + app.add_middleware( + FastAPIMiddleware, + excludelist_paths=["health"], + sampler=samplers.AlwaysOnSampler()) + + test_client = TestClient(app) + test_client.get("/health") + + mock_m.assert_not_called() + + @mock.patch.object(tracer_module.Tracer, "finish") + @mock.patch.object(tracer_module.Tracer, "end_span") + @mock.patch.object(tracer_module.Tracer, "start_span") + def test_request_exception(self, mock_m1, mock_m2, mock_m3): + app = self.create_app() + app.add_middleware(FastAPIMiddleware) + + test_client = TestClient(app) + + with self.assertRaises(FastAPITestException): + test_client.get("/error") + + mock_span = mock_m1.return_value + self.assertEqual(mock_span.add_attribute.call_count, 9) + mock_span.add_attribute.assert_has_calls([ + mock.call("http.host", "testserver"), + mock.call("http.method", "GET"), + mock.call("http.path", "/error"), + mock.call("http.url", "http://testserver/error"), + mock.call("http.route", "/error"), + mock.call("error.name", "FastAPITestException"), + mock.call("error.message", "test error"), + mock.call("stacktrace", ANY), + mock.call("http.status_code", 500) + ]) + mock_m2.assert_called_once() + mock_m3.assert_called_once() + + def test_request_exception_stacktrace(self): + tb = None + try: + raise RuntimeError("bork bork bork") + except Exception as exc: + test_exception = exc + if hasattr(exc, "__traceback__"): + tb = exc.__traceback__ + else: + _, _, tb = sys.exc_info() + + app = self.create_app() + middleware = FastAPIMiddleware(app) + + mock_span = mock.Mock() + mock_span.add_attribute = mock.Mock() + middleware._handle_exception(mock_span, test_exception) + + mock_span.add_attribute.assert_has_calls([ + mock.call("error.name", "RuntimeError"), + mock.call("error.message", "bork bork bork"), + mock.call("stacktrace", "\n".join(traceback.format_tb(tb))), + mock.call("http.status_code", 500) + ]) diff --git a/contrib/opencensus-ext-fastapi/version.py b/contrib/opencensus-ext-fastapi/version.py new file mode 100644 index 000000000..f210ff2ea --- /dev/null +++ b/contrib/opencensus-ext-fastapi/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = '0.1.0' diff --git a/contrib/opencensus-ext-flask/CHANGELOG.md b/contrib/opencensus-ext-flask/CHANGELOG.md index 742594345..1218b195d 100644 --- a/contrib/opencensus-ext-flask/CHANGELOG.md +++ b/contrib/opencensus-ext-flask/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 0.8.2 +Released 2023-03-10 + +- Add exception information to span attributes +([#1188](https://github.com/census-instrumentation/opencensus-python/pull/1188)) + ## 0.8.1 Released 2022-08-03 diff --git a/contrib/opencensus-ext-flask/opencensus/ext/flask/common/version.py b/contrib/opencensus-ext-flask/opencensus/ext/flask/common/version.py index 671fc3d04..84023e0f7 100644 --- a/contrib/opencensus-ext-flask/opencensus/ext/flask/common/version.py +++ b/contrib/opencensus-ext-flask/opencensus/ext/flask/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.9.dev0' +__version__ = '0.8.2' diff --git a/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py b/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py index 3900c4d61..08ccc10ab 100644 --- a/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py +++ b/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py @@ -16,6 +16,7 @@ import logging import sys +import traceback import flask from google.rpc import code_pb2 @@ -40,6 +41,9 @@ 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'] EXCLUDELIST_PATHS = 'EXCLUDELIST_PATHS' EXCLUDELIST_HOSTNAMES = 'EXCLUDELIST_HOSTNAMES' @@ -217,16 +221,28 @@ def _teardown_request(self, exception): code=code_pb2.UNKNOWN, message=str(exception) ) - # try attaching the stack trace to the span, only populated - # if the app has 'PROPAGATE_EXCEPTIONS', 'DEBUG', or - # 'TESTING' enabled - exc_type, _, exc_traceback = sys.exc_info() + span.add_attribute( + attribute_key=ERROR_NAME, + attribute_value=exception.__class__.__name__) + span.add_attribute( + attribute_key=ERROR_MESSAGE, + attribute_value=str(exception)) + + if hasattr(exception, '__traceback__'): + exc_traceback = exception.__traceback__ + else: + exc_type, _, exc_traceback = sys.exc_info() if exc_traceback is not None: span.stack_trace = ( stack_trace.StackTrace.from_traceback( exc_traceback ) ) + span.add_attribute( + attribute_key=STACKTRACE, + attribute_value='\n'.join( + traceback.format_tb(exc_traceback)) + ) tracer.end_span() tracer.finish() diff --git a/contrib/opencensus-ext-flask/setup.py b/contrib/opencensus-ext-flask/setup.py index 63aa51329..9c8d71cd9 100644 --- a/contrib/opencensus-ext-flask/setup.py +++ b/contrib/opencensus-ext-flask/setup.py @@ -50,7 +50,7 @@ long_description=open('README.rst').read(), install_requires=[ 'flask >= 0.12.3, < 3.0.0, != 1.1.3', - 'opencensus >= 0.12.dev0, < 1.0.0', + 'opencensus >= 0.11.2, < 1.0.0', ], extras_require={}, license='Apache-2.0', diff --git a/contrib/opencensus-ext-flask/tests/test_flask_middleware.py b/contrib/opencensus-ext-flask/tests/test_flask_middleware.py index 4ef5a9982..218acab34 100644 --- a/contrib/opencensus-ext-flask/tests/test_flask_middleware.py +++ b/contrib/opencensus-ext-flask/tests/test_flask_middleware.py @@ -383,6 +383,13 @@ def test_teardown_include_exception(self): exported_spandata.status.canonical_code, code_pb2.UNKNOWN ) self.assertEqual(exported_spandata.status.description, 'error') + self.assertEqual( + exported_spandata.attributes["error.name"], 'FlaskTestException' + ) + self.assertEqual( + exported_spandata.attributes["error.message"], 'error' + ) + self.assertIsNotNone(exported_spandata.attributes["error.message"]) def test_teardown_include_exception_and_traceback(self): mock_exporter = mock.MagicMock() diff --git a/contrib/opencensus-ext-httpx/CHANGELOG.md b/contrib/opencensus-ext-httpx/CHANGELOG.md index 2fe82b9e8..755e63048 100644 --- a/contrib/opencensus-ext-httpx/CHANGELOG.md +++ b/contrib/opencensus-ext-httpx/CHANGELOG.md @@ -7,3 +7,4 @@ Released 2023-01-18 - Initial release +([#1098](https://github.com/census-instrumentation/opencensus-python/pull/1098)) diff --git a/contrib/opencensus-ext-httpx/setup.py b/contrib/opencensus-ext-httpx/setup.py index db9f2a60a..9cdc40055 100644 --- a/contrib/opencensus-ext-httpx/setup.py +++ b/contrib/opencensus-ext-httpx/setup.py @@ -33,7 +33,7 @@ ], description="OpenCensus HTTPX Integration", include_package_data=True, - long_description="", + long_description=open('README.rst').read(), install_requires=["opencensus >= 0.11.1, < 1.0.0", "httpx >= 0.22.0"], extras_require={}, license="Apache-2.0", diff --git a/contrib/opencensus-ext-httpx/version.py b/contrib/opencensus-ext-httpx/version.py index b794fd409..506a49340 100644 --- a/contrib/opencensus-ext-httpx/version.py +++ b/contrib/opencensus-ext-httpx/version.py @@ -1 +1 @@ -__version__ = '0.1.0' +__version__ = '0.2.dev0' diff --git a/noxfile.py b/noxfile.py index 466079fc8..aa9d3dba6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,6 +28,7 @@ def _install_dev_packages(session): session.install('-e', 'contrib/opencensus-ext-datadog') session.install('-e', 'contrib/opencensus-ext-dbapi') session.install('-e', 'contrib/opencensus-ext-django') + session.install('-e', 'contrib/opencensus-ext-fastapi') session.install('-e', 'contrib/opencensus-ext-flask') session.install('-e', 'contrib/opencensus-ext-gevent') session.install('-e', 'contrib/opencensus-ext-grpc') diff --git a/opencensus/common/version/__init__.py b/opencensus/common/version/__init__.py index 4c11e3de6..232b8fb04 100644 --- a/opencensus/common/version/__init__.py +++ b/opencensus/common/version/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.11.1' +__version__ = '0.11.2' diff --git a/opencensus/trace/integrations.py b/opencensus/trace/integrations.py index c5b09a850..7a58f4911 100644 --- a/opencensus/trace/integrations.py +++ b/opencensus/trace/integrations.py @@ -33,6 +33,7 @@ class _Integrations: REQUESTS = 1024 SQLALCHEMY = 2056 HTTPX = 4096 + FASTAPI = 8192 def get_integrations(): diff --git a/tox.ini b/tox.ini index 3f7f28d1a..9802276e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = +envlist = py{27,35,36,37,38,39}-unit py39-bandit py39-lint @@ -12,7 +12,7 @@ unit-base-command = py.test --quiet --cov={envdir}/opencensus --cov=context --co [testenv] install_command = python -m pip install {opts} {packages} -deps = +deps = unit,lint: mock==3.0.5 unit,lint: pytest==4.6.4 unit,lint: pytest-cov @@ -30,6 +30,7 @@ deps = ; unit,lint: -e contrib/opencensus-ext-datadog unit,lint,bandit: -e contrib/opencensus-ext-dbapi unit,lint,bandit: -e contrib/opencensus-ext-django + py3{6,7,8,9}-unit,lint,bandit: -e contrib/opencensus-ext-fastapi unit,lint,bandit: -e contrib/opencensus-ext-flask unit,lint,bandit: -e contrib/opencensus-ext-gevent unit,lint,bandit: -e contrib/opencensus-ext-grpc @@ -45,7 +46,7 @@ deps = unit,lint,bandit: -e contrib/opencensus-ext-pymysql unit,lint,bandit: -e contrib/opencensus-ext-pyramid unit,lint,bandit: -e contrib/opencensus-ext-requests - py3{7,8,9}-unit,lint,bandit: -e contrib/opencensus-ext-httpx + py3{7,8,9}-unit,lint,bandit: -e contrib/opencensus-ext-httpx unit,lint,bandit: -e contrib/opencensus-ext-sqlalchemy py3{6,7,8,9}-unit,lint,bandit: -e contrib/opencensus-ext-stackdriver unit,lint,bandit: -e contrib/opencensus-ext-threading @@ -58,8 +59,8 @@ deps = docs: setuptools >= 36.4.0 docs: sphinx >= 1.6.3 -commands = - py{27,34,35}-unit: {[constants]unit-base-command} --ignore=contrib/opencensus-ext-stackdriver --ignore=contrib/opencensus-ext-flask --ignore=contrib/opencensus-ext-httpx +commands = + py{27,34,35}-unit: {[constants]unit-base-command} --ignore=contrib/opencensus-ext-stackdriver --ignore=contrib/opencensus-ext-flask --ignore=contrib/opencensus-ext-httpx --ignore=contrib/opencensus-ext-fastapi py36-unit: {[constants]unit-base-command} --ignore=contrib/opencensus-ext-httpx py3{7,8,9}-unit: {[constants]unit-base-command}