diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ebb67d73..1c27d4e846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Flask sqlalchemy psycopg2 integration ([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224)) +- Add metric instrumentation in fastapi + ([#1199](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1199)) +- Add metric instrumentation in Pyramid + ([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242)) ### Fixed - `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies. - ([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234)) + ([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234)) +- `opentelemetry-instrumentation-pymongo` Change span names to not contain queries but only database name and command name + ([#1247](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1247)) - restoring metrics in django framework ([#1208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1208)) - `opentelemetry-instrumentation-aiohttp-client` Fix producing additional spans with each newly created ClientSession - ([#1246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1246)) +- Add _is_openetlemetry_instrumented check in _InstrumentedFastAPI class + ([#1313](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1313)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 diff --git a/instrumentation/README.md b/instrumentation/README.md index deeb3693f0..d7dd93c3a3 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -16,7 +16,7 @@ | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No | [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | No -| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | No +| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0, < 3.0 | Yes | [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 21e8a189b5..ce42c99536 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -396,10 +396,15 @@ def __init__( client_response_hook: _ClientResponseHookT = None, tracer_provider=None, meter_provider=None, + meter=None, ): self.app = guarantee_single_callable(app) self.tracer = trace.get_tracer(__name__, __version__, tracer_provider) - self.meter = get_meter(__name__, __version__, meter_provider) + self.meter = ( + get_meter(__name__, __version__, meter_provider) + if meter is None + else meter + ) self.duration_histogram = self.meter.create_histogram( name="http.server.duration", unit="ms", diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 1c76c8bb50..f1fd27533b 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -137,7 +137,9 @@ def client_response_hook(span: Span, message: dict): from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi.package import _instruments +from opentelemetry.instrumentation.fastapi.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.metrics import get_meter from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls @@ -165,6 +167,7 @@ def instrument_app( client_request_hook: _ClientRequestHookT = None, client_response_hook: _ClientResponseHookT = None, tracer_provider=None, + meter_provider=None, excluded_urls=None, ): """Instrument an uninstrumented FastAPI application.""" @@ -176,6 +179,7 @@ def instrument_app( excluded_urls = _excluded_urls_from_env else: excluded_urls = parse_excluded_urls(excluded_urls) + meter = get_meter(__name__, __version__, meter_provider) app.add_middleware( OpenTelemetryMiddleware, @@ -185,6 +189,7 @@ def instrument_app( client_request_hook=client_request_hook, client_response_hook=client_response_hook, tracer_provider=tracer_provider, + meter=meter, ) app._is_instrumented_by_opentelemetry = True else: @@ -223,6 +228,7 @@ def _instrument(self, **kwargs): if _excluded_urls is None else parse_excluded_urls(_excluded_urls) ) + _InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider") fastapi.FastAPI = _InstrumentedFastAPI def _uninstrument(self, **kwargs): @@ -231,6 +237,7 @@ def _uninstrument(self, **kwargs): class _InstrumentedFastAPI(fastapi.FastAPI): _tracer_provider = None + _meter_provider = None _excluded_urls = None _server_request_hook: _ServerRequestHookT = None _client_request_hook: _ClientRequestHookT = None @@ -238,6 +245,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + meter = get_meter( + __name__, __version__, _InstrumentedFastAPI._meter_provider + ) self.add_middleware( OpenTelemetryMiddleware, excluded_urls=_InstrumentedFastAPI._excluded_urls, @@ -246,7 +256,9 @@ def __init__(self, *args, **kwargs): client_request_hook=_InstrumentedFastAPI._client_request_hook, client_response_hook=_InstrumentedFastAPI._client_response_hook, tracer_provider=_InstrumentedFastAPI._tracer_provider, + meter=meter, ) + self._is_instrumented_by_opentelemetry = True def _get_route_details(scope): diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py index cccaa51a8c..8df84fc931 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py @@ -14,3 +14,5 @@ _instruments = ("fastapi ~= 0.58",) + +_supports_metrics = True diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 0d42e7533c..aaf728f5fa 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from timeit import default_timer from unittest.mock import patch import fastapi @@ -22,6 +23,10 @@ import opentelemetry.instrumentation.fastapi as otel_fastapi from opentelemetry import trace from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + NumberDataPoint, +) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.globals_test import reset_trace_globals @@ -29,9 +34,20 @@ from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + _active_requests_count_attrs, + _duration_attrs, get_excluded_urls, ) +_expected_metric_names = [ + "http.server.active_requests", + "http.server.duration", +] +_recommended_attrs = { + "http.server.active_requests": _active_requests_count_attrs, + "http.server.duration": _duration_attrs, +} + class TestFastAPIManualInstrumentation(TestBase): def _create_app(self): @@ -161,6 +177,124 @@ def test_fastapi_excluded_urls_not_env(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) + def test_fastapi_metrics(self): + self._client.get("/foobar") + self._client.get("/foobar") + self._client.get("/foobar") + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) == 1) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) == 1) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) == 2) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_basic_metric_success(self): + start = default_timer() + self._client.get("/foobar") + duration = max(round((default_timer() - start) * 1000), 0) + expected_duration_attributes = { + "http.method": "GET", + "http.host": "testserver", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "testserver", + "net.host.port": 80, + "http.status_code": 200, + } + expected_requests_count_attributes = { + "http.method": "GET", + "http.host": "testserver", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "testserver", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=20) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_post_request_metric_success(self): + start = default_timer() + self._client.post("/foobar") + duration = max(round((default_timer() - start) * 1000), 0) + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=30) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + + def test_metric_uninstruemnt_app(self): + self._client.get("/foobar") + self._instrumentor.uninstrument_app(self._app) + self._client.get("/foobar") + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + + def test_metric_uninstrument(self): + # instrumenting class and creating app to send request + self._instrumentor.instrument() + app = self._create_fastapi_app() + client = TestClient(app) + client.get("/foobar") + # uninstrumenting class and creating the app again + self._instrumentor.uninstrument() + app = self._create_fastapi_app() + client = TestClient(app) + client.get("/foobar") + + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + @staticmethod def _create_fastapi_app(): app = fastapi.FastAPI() @@ -274,6 +408,14 @@ def test_request(self): self.assertEqual(span.resource.attributes["key1"], "value1") self.assertEqual(span.resource.attributes["key2"], "value2") + def test_mulitple_way_instrumentation(self): + self._instrumentor.instrument_app(self._app) + count = 0 + for middleware in self._app.user_middleware: + if middleware.cls is OpenTelemetryMiddleware: + count += 1 + self.assertEqual(count, 1) + def tearDown(self): self._instrumentor.uninstrument() super().tearDown() diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index ba3afae11f..91cd81aab3 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -121,10 +121,10 @@ def started(self, event: monitoring.CommandStartedEvent): ): return command = event.command.get(event.command_name, "") - name = event.command_name + name = event.database_name + name += "." + event.command_name statement = event.command_name if command: - name += "." + str(command) statement += " " + str(command) try: diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index ef3734c596..15a5a0be0b 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -40,7 +40,6 @@ def test_pymongo_instrumentor(self): ) with patch: PymongoInstrumentor().instrument() - self.assertTrue(mock_register.called) def test_started(self): @@ -59,7 +58,7 @@ def test_started(self): # pylint: disable=protected-access span = command_tracer._pop_span(mock_event) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(span.name, "command_name.find") + self.assertEqual(span.name, "database_name.command_name") self.assertEqual(span.attributes[SpanAttributes.DB_SYSTEM], "mongodb") self.assertEqual( span.attributes[SpanAttributes.DB_NAME], "database_name" @@ -189,8 +188,7 @@ def test_int_command(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] - - self.assertEqual(span.name, "command_name.123") + self.assertEqual(span.name, "database_name.command_name") class MockCommand: diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index c9ebf7081e..cb650715a9 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -14,6 +14,7 @@ from logging import getLogger from time import time_ns +from timeit import default_timer from pyramid.events import BeforeTraversal from pyramid.httpexceptions import HTTPException, HTTPServerError @@ -27,6 +28,7 @@ ) from opentelemetry.instrumentation.pyramid.version import __version__ from opentelemetry.instrumentation.utils import _start_internal_or_server_span +from opentelemetry.metrics import get_meter from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.util.http import get_excluded_urls @@ -122,8 +124,20 @@ def _before_traversal(event): def trace_tween_factory(handler, registry): + # pylint: disable=too-many-statements settings = registry.settings enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) + meter = get_meter(__name__, __version__) + duration_histogram = meter.create_histogram( + name="http.server.duration", + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + active_requests_counter = meter.create_up_down_counter( + name="http.server.active_requests", + unit="requests", + description="measures the number of concurrent HTTP requests that are currently in-flight", + ) if not enabled: # If disabled, make a tween that signals to the @@ -137,14 +151,23 @@ def disabled_tween(request): # make a request tracing function # pylint: disable=too-many-branches def trace_tween(request): - # pylint: disable=E1101 + # pylint: disable=E1101, too-many-locals if _excluded_urls.url_disabled(request.url): request.environ[_ENVIRON_ENABLED_KEY] = False # short-circuit when we don't want to trace anything return handler(request) + attributes = otel_wsgi.collect_request_attributes(request.environ) + request.environ[_ENVIRON_ENABLED_KEY] = True request.environ[_ENVIRON_STARTTIME_KEY] = time_ns() + active_requests_count_attrs = ( + otel_wsgi._parse_active_request_count_attrs(attributes) + ) + duration_attrs = otel_wsgi._parse_duration_attrs(attributes) + + start = default_timer() + active_requests_counter.add(1, active_requests_count_attrs) response = None status = None @@ -165,6 +188,15 @@ def trace_tween(request): status = "500 InternalServerError" raise finally: + duration = max(round((default_timer() - start) * 1000), 0) + status = getattr(response, "status", status) + status_code = otel_wsgi._parse_status_code(status) + if status_code is not None: + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = otel_wsgi._parse_status_code(status) + duration_histogram.record(duration, duration_attrs) + active_requests_counter.add(-1, active_requests_count_attrs) span = request.environ.get(_ENVIRON_SPAN_KEY) enabled = request.environ.get(_ENVIRON_ENABLED_KEY) if not span and enabled: @@ -174,7 +206,6 @@ def trace_tween(request): "PyramidInstrumentor().instrument_config(config) is called" ) elif enabled: - status = getattr(response, "status", status) if status is not None: otel_wsgi.add_response_attributes( diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index 89df49e49e..93ec314f97 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -12,12 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from timeit import default_timer from unittest.mock import patch from pyramid.config import Configurator from opentelemetry import trace from opentelemetry.instrumentation.pyramid import PyramidInstrumentor +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + NumberDataPoint, +) from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind @@ -25,11 +30,22 @@ from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + _active_requests_count_attrs, + _duration_attrs, ) # pylint: disable=import-error from .pyramid_base_test import InstrumentationTest +_expected_metric_names = [ + "http.server.active_requests", + "http.server.duration", +] +_recommended_attrs = { + "http.server.active_requests": _active_requests_count_attrs, + "http.server.duration": _duration_attrs, +} + class TestAutomatic(InstrumentationTest, WsgiTestBase): def setUp(self): @@ -156,6 +172,89 @@ def test_400s_response_is_not_an_error(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) + def test_pyramid_metric(self): + self.client.get("/hello/756") + self.client.get("/hello/756") + self.client.get("/hello/756") + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) == 1) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) == 1) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) == 2) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_basic_metric_success(self): + start = default_timer() + self.client.get("/hello/756") + duration = max(round((default_timer() - start) * 1000), 0) + expected_duration_attributes = { + "http.method": "GET", + "http.host": "localhost", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "localhost", + "net.host.port": 80, + "http.status_code": 200, + } + expected_requests_count_attributes = { + "http.method": "GET", + "http.host": "localhost", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "localhost", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=20) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_metric_uninstruemnt(self): + self.client.get("/hello/756") + PyramidInstrumentor().uninstrument() + self.config = Configurator() + self._common_initialization(self.config) + self.client.get("/hello/756") + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + class TestWrappedWithOtherFramework(InstrumentationTest, WsgiTestBase): def setUp(self): diff --git a/opentelemetry-distro/MANIFEST.in b/opentelemetry-distro/MANIFEST.in deleted file mode 100644 index 191b7d1959..0000000000 --- a/opentelemetry-distro/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -prune tests -graft src -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include MANIFEST.in -include README.rst diff --git a/opentelemetry-distro/pyproject.toml b/opentelemetry-distro/pyproject.toml new file mode 100644 index 0000000000..6707fc5c29 --- /dev/null +++ b/opentelemetry-distro/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-distro" +dynamic = ["version"] +description = "OpenTelemetry Python Distro" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-instrumentation == 0.33b0", + "opentelemetry-sdk == 1.12.0", +] + +[project.optional-dependencies] +otlp = [ + "opentelemetry-exporter-otlp == 1.12.0", +] +test = [] + +[project.entry-points.opentelemetry_configurator] +configurator = "opentelemetry.distro:OpenTelemetryConfigurator" + +[project.entry-points.opentelemetry_distro] +distro = "opentelemetry.distro:OpenTelemetryDistro" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-distro" + +[tool.hatch.version] +path = "src/opentelemetry/distro/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg deleted file mode 100644 index 59f199a07e..0000000000 --- a/opentelemetry-distro/setup.cfg +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright The OpenTelemetry 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. -# -[metadata] -name = opentelemetry-distro -description = OpenTelemetry Python Distro -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-distro -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api ~= 1.12 - opentelemetry-instrumentation == 0.33b0 - opentelemetry-sdk == 1.12.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_distro = - distro = opentelemetry.distro:OpenTelemetryDistro -opentelemetry_configurator = - configurator = opentelemetry.distro:OpenTelemetryConfigurator - -[options.extras_require] -test = -otlp = - opentelemetry-exporter-otlp == 1.12.0 diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py deleted file mode 100644 index ddf6de91aa..0000000000 --- a/opentelemetry-distro/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The OpenTelemetry 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 os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "distro", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in deleted file mode 100644 index faee277146..0000000000 --- a/opentelemetry-instrumentation/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include MANIFEST.in -include README.rst -include LICENSE diff --git a/opentelemetry-instrumentation/pyproject.toml b/opentelemetry-instrumentation/pyproject.toml new file mode 100644 index 0000000000..6d219f6428 --- /dev/null +++ b/opentelemetry-instrumentation/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation" +dynamic = ["version"] +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "opentelemetry-api ~= 1.4", + "setuptools >= 16.0", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +test = [] + +[project.scripts] +opentelemetry-bootstrap = "opentelemetry.instrumentation.bootstrap:run" +opentelemetry-instrument = "opentelemetry.instrumentation.auto_instrumentation:run" + +[project.entry-points.opentelemetry_environment_variables] +instrumentation = "opentelemetry.instrumentation.environment_variables" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg deleted file mode 100644 index fcbde9b507..0000000000 --- a/opentelemetry-instrumentation/setup.cfg +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright The OpenTelemetry 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. -# -[metadata] -name = opentelemetry-instrumentation -description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api ~= 1.4 - setuptools >= 16.0 - wrapt >= 1.0.0, < 2.0.0 - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run - opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run -opentelemetry_environment_variables = - instrumentation = opentelemetry.instrumentation.environment_variables - -[options.extras_require] -test = diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py deleted file mode 100644 index 9d1d5b7b06..0000000000 --- a/opentelemetry-instrumentation/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry 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 os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 103a63de88..7b1359226a 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -518,18 +518,19 @@ def lint_args(args): runsubprocess( args.dry_run, - ("black", ".") + (("--diff", "--check") if args.check_only else ()), + ("black", "--config", f"{rootdir}/pyproject.toml", ".") + + (("--diff", "--check") if args.check_only else ()), cwd=rootdir, check=True, ) runsubprocess( args.dry_run, - ("isort", ".") + ("isort", "--settings-path", f"{rootdir}/.isort.cfg", ".") + (("--diff", "--check-only") if args.check_only else ()), cwd=rootdir, check=True, ) - runsubprocess(args.dry_run, ("flake8", rootdir), check=True) + runsubprocess(args.dry_run, ("flake8", "--config", f"{rootdir}/.flake8", rootdir), check=True) execute_args( parse_subargs( args, ("exec", "pylint {}", "--all", "--mode", "lintroots") @@ -644,7 +645,7 @@ def update_dependencies(targets, version, packages): update_files( targets, - "setup.cfg", + "pyproject.toml", fr"({package_name}.*)==(.*)", r"\1== " + version, ) @@ -717,13 +718,16 @@ def format_args(args): format_dir = str(find_projectroot()) if args.path: format_dir = os.path.join(format_dir, args.path) - + root_dir = str(find_projectroot()) runsubprocess( - args.dry_run, ("black", "."), cwd=format_dir, check=True, + args.dry_run, + ("black", "--config", f"{root_dir}/pyproject.toml", "."), + cwd=format_dir, + check=True, ) runsubprocess( args.dry_run, - ("isort", "--profile", "black", "."), + ("isort", "--settings-path", f"{root_dir}/.isort.cfg", "--profile", "black", "."), cwd=format_dir, check=True, ) diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 9088da3a37..9222b9c727 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -28,7 +28,7 @@ ) # List of recommended metrics attributes -_duration_attrs = [ +_duration_attrs = { SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_HOST, SpanAttributes.HTTP_SCHEME, @@ -37,15 +37,15 @@ SpanAttributes.HTTP_SERVER_NAME, SpanAttributes.NET_HOST_NAME, SpanAttributes.NET_HOST_PORT, -] +} -_active_requests_count_attrs = [ +_active_requests_count_attrs = { SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_HOST, SpanAttributes.HTTP_SCHEME, SpanAttributes.HTTP_FLAVOR, SpanAttributes.HTTP_SERVER_NAME, -] +} class ExcludeList: @@ -150,16 +150,16 @@ def get_custom_headers(env_var: str) -> List[str]: def _parse_active_request_count_attrs(req_attrs): - active_requests_count_attrs = {} - for attr_key in _active_requests_count_attrs: - if req_attrs.get(attr_key) is not None: - active_requests_count_attrs[attr_key] = req_attrs[attr_key] + active_requests_count_attrs = { + key: req_attrs[key] + for key in _active_requests_count_attrs.intersection(req_attrs.keys()) + } return active_requests_count_attrs def _parse_duration_attrs(req_attrs): - duration_attrs = {} - for attr_key in _duration_attrs: - if req_attrs.get(attr_key) is not None: - duration_attrs[attr_key] = req_attrs[attr_key] + duration_attrs = { + key: req_attrs[key] + for key in _duration_attrs.intersection(req_attrs.keys()) + } return duration_attrs