From 444a29bab906cdd1bc0ebafdd41fe5dd9fafeef8 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Mon, 10 Oct 2022 15:52:36 +0530 Subject: [PATCH 1/9] add boilerplate for adding changes --- .../README.rst | 0 .../pyproject.toml | 0 .../instrumentation/cherrypy/__init__.py | 13 +++++++++++++ .../instrumentation/cherrypy/package.py | 0 .../instrumentation/cherrypy/version.py | 0 5 files changed, 13 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst b/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml b/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py new file mode 100644 index 0000000000..2e14b6c9ee --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -0,0 +1,13 @@ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +class CherryPyInstrumentor(BaseInstrumentor): + """An instrumentor for FastAPI + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + pass + + def _uninstrument(self, **kwargs): + pass diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py new file mode 100644 index 0000000000..e69de29bb2 From bf6f8161e3632ce1a7a53434ed7b676684d54e9e Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Mon, 17 Oct 2022 17:15:22 +0530 Subject: [PATCH 2/9] add instrumentor and patch __call__ method --- .../instrumentation/cherrypy/__init__.py | 113 +++++++++++++++++- .../instrumentation/cherrypy/version.py | 1 + 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index 2e14b6c9ee..8d90e5de06 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -1,4 +1,18 @@ +from email.policy import default +from logging import getLogger +from time import time_ns +from timeit import default_timer +from urllib import response +import cherrypy from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import _start_internal_or_server_span +import opentelemetry.instrumentation.wsgi as otel_wsgi +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.instrumentation.falcon.version import __version__ + + +_logger = getLogger(__name__) class CherryPyInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI @@ -7,7 +21,102 @@ class CherryPyInstrumentor(BaseInstrumentor): """ def _instrument(self, **kwargs): - pass + self._original_cherrypy_application = cherrypy._cptree.Application + cherrypy._cptree.Application = _InstrumentedCherryPyApplication + cherrypy.Application = _InstrumentedCherryPyApplication def _uninstrument(self, **kwargs): - pass + cherrypy.Application = self._original_cherrypy_application + +class _InstrumentedCherryPyApplication(cherrypy._cptree.Application): + def __init__(self, *args, **kwargs): + tracer_provider = kwargs.pop('tracer_provider', None) + meter_provider = kwargs.pop('metr_provider', None) + self._otel_tracer = trace.get_tracer(__name__, __version__, tracer_provider) + otel_meter = get_meter(__name__, __version__, meter_provider) + self.duration_hostogram = otel_meter.create_histogram( + name="http.server.duration", + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + self.active_requests_counter = otel_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", + ) + self.request_hook = kwargs.pop('request_hook', None) + self.response_hook = kwargs.pop('response_hook', None) + # self._otel_excluded_urls = get_excluded_urls("") + self._is_instrumented_by_opentelemetry = True + super().__init__(*args, **kwargs) + + def __call__(self, environ, start_response): + # if self._otel_ecluded_urls.url_disabled(environ.get('PATH_INFO', '/')): + # return super().__call__(environ, start_response) + + if not self._is_instrumented_by_opentelemetry: + return super().__call__(environ, start_response) + + start_time = time_ns() + span, token = _start_internal_or_server_span( + tracer=self._otel_tracer, + span_name=otel_wsgi.get_default_span_name(environ), + start_time=start_time, + context_carrier=environ, + context_getter=otel_wsgi.wsgi_getter, + ) + if self.request_hook: + self.request_hook(span, environ) + attributes = otel_wsgi.collect_request_attributes(environ) + active_requests_count_attrs = ( + otel_wsgi._parse_active_request_count_attrs(attributes) + ) + duration_attrs = otel_wsgi._parse_duration_attrs(attributes) + self.active_requests_counter.add(1, active_requests_count_attrs) + + if span.is_recording(): + for key, value in attributes.items(): + span.set_attribute(key, value) + if span.is_recording() and span.kind == trace.SpanKind.SERVER: + custom_attributes = ( + otel_wsgi.collect_custom_request_headers_attributes(environ) + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + activation = trace.use_span(span, end_on_exit=True) + activation.__enter__() + + def _start_response(status, response_headers, *args, **kwargs): + if span: + otel_wsgi.add_response_attributes(span, status, response_headers) + status_code = otel_wsgi._parse_status_code(status) + if status_code is not None: + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code + if span.is_recording() and span.kind == trace.SpanKind.SERVER: + custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(response_headers) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + if self.response_hook: + self.response_hook(span, status, response_headers) + return start_response(status, response_headers, *args, **kwargs) + exception = None + start = default_timer() + try: + return super().__call__(environ, _start_response) + except Exception as exc: + exception = exc + raise + finally: + if exception is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), + exc, + getattr(exc, "__traceback__", None), + ) + duration = max(round((default_timer() - start) * 1000), 0) + self.duration_histogram.record(duration, duration_attrs) + self.active_requests_counter.add(-1, active_requests_count_attrs) \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py index e69de29bb2..a590883f7e 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py @@ -0,0 +1 @@ +__version__ = "0.34b0" From 215cd024be3f5074b02e854930fe1812fa27e6f1 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Thu, 27 Oct 2022 15:56:12 +0530 Subject: [PATCH 3/9] working instrumentation in cherrypy --- .../pyproject.toml | 60 +++++++++++++++++++ .../instrumentation/cherrypy/__init__.py | 30 ++++++++-- .../instrumentation/cherrypy/package.py | 3 + 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml b/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml index e69de29bb2..28c01be103 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-cherrypy" +dynamic = ["version"] +description = "CherryPy instrumentation for OpenTelemetry" +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.12", + "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation-wsgi == 0.34b0", + "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-util-http == 0.34b0", +] + +[project.optional-dependencies] +instruments = [ + "cherrypy >= 1.0", +] +test = [ + "opentelemetry-instrumentation-cherrypy[instruments]", + "markupsafe==2.0.1", + "opentelemetry-test-utils == 0.34b0", +] + +[project.entry-points.opentelemetry_instrumentor] +cherrypy = "opentelemetry.instrumentation.cherrypy:CherryPyInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-cherrypy" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/cherrypy/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index 8d90e5de06..2c5c7da520 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -2,17 +2,25 @@ from logging import getLogger from time import time_ns from timeit import default_timer -from urllib import response +from typing import Collection + +from opentelemetry.util.http import parse_excluded_urls, get_excluded_urls import cherrypy +from opentelemetry.instrumentation.cherrypy.package import _instruments from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import _start_internal_or_server_span import opentelemetry.instrumentation.wsgi as otel_wsgi -from opentelemetry import trace +from opentelemetry.instrumentation.propagators import ( + get_global_response_propagator, +) +from opentelemetry import trace, context from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.instrumentation.falcon.version import __version__ +from opentelemetry.metrics import get_meter _logger = getLogger(__name__) +_excluded_urls_from_env = get_excluded_urls("CHERRYPY") class CherryPyInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI @@ -20,6 +28,9 @@ class CherryPyInstrumentor(BaseInstrumentor): See `BaseInstrumentor` """ + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + def _instrument(self, **kwargs): self._original_cherrypy_application = cherrypy._cptree.Application cherrypy._cptree.Application = _InstrumentedCherryPyApplication @@ -34,7 +45,7 @@ def __init__(self, *args, **kwargs): meter_provider = kwargs.pop('metr_provider', None) self._otel_tracer = trace.get_tracer(__name__, __version__, tracer_provider) otel_meter = get_meter(__name__, __version__, meter_provider) - self.duration_hostogram = otel_meter.create_histogram( + self.duration_histogram = otel_meter.create_histogram( name="http.server.duration", unit="ms", description="measures the duration of the inbound HTTP request", @@ -46,13 +57,14 @@ def __init__(self, *args, **kwargs): ) self.request_hook = kwargs.pop('request_hook', None) self.response_hook = kwargs.pop('response_hook', None) - # self._otel_excluded_urls = get_excluded_urls("") + excluded_urls = kwargs.pop('excluded_urls', None) + self._otel_excluded_urls = (_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)) self._is_instrumented_by_opentelemetry = True super().__init__(*args, **kwargs) def __call__(self, environ, start_response): - # if self._otel_ecluded_urls.url_disabled(environ.get('PATH_INFO', '/')): - # return super().__call__(environ, start_response) + if self._otel_excluded_urls.url_disabled(environ.get('PATH_INFO', '/')): + return super().__call__(environ, start_response) if not self._is_instrumented_by_opentelemetry: return super().__call__(environ, start_response) @@ -88,6 +100,10 @@ def __call__(self, environ, start_response): activation.__enter__() def _start_response(status, response_headers, *args, **kwargs): + propagator = get_global_response_propagator() + if propagator: + propagator.inject(response_headers, setter=otel_wsgi.default_response_propagation_setter) + if span: otel_wsgi.add_response_attributes(span, status, response_headers) status_code = otel_wsgi._parse_status_code(status) @@ -117,6 +133,8 @@ def _start_response(status, response_headers, *args, **kwargs): exc, getattr(exc, "__traceback__", None), ) + if token is not None: + context.detach(token) duration = max(round((default_timer() - start) * 1000), 0) self.duration_histogram.record(duration, duration_attrs) self.active_requests_counter.add(-1, active_requests_count_attrs) \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py index e69de29bb2..12ecad28e2 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py @@ -0,0 +1,3 @@ +_instruments = ("cherrypy >= 1.0",) + +_supports_metrics = True \ No newline at end of file From 4933b32e93d6fa5f8258e9c289c0363d30059257 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Mon, 28 Nov 2022 14:33:30 +0530 Subject: [PATCH 4/9] revert back to previous commit --- .../src/opentelemetry/instrumentation/cherrypy/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index 2c5c7da520..0702d4fbc8 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -1,4 +1,3 @@ -from email.policy import default from logging import getLogger from time import time_ns from timeit import default_timer @@ -15,7 +14,7 @@ ) from opentelemetry import trace, context from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.instrumentation.falcon.version import __version__ +from opentelemetry.instrumentation.cherrypy.version import __version__ from opentelemetry.metrics import get_meter From fa30e558fb56e8d8e6b8b0a81bc6009bda920a26 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Tue, 29 Nov 2022 16:10:52 +0530 Subject: [PATCH 5/9] fix lint --- instrumentation/README.md | 1 + .../instrumentation/cherrypy/__init__.py | 48 ++++++++++++++----- .../instrumentation/cherrypy/package.py | 2 +- .../pyproject.toml | 1 + .../instrumentation/bootstrap_gen.py | 4 ++ 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/instrumentation/README.md b/instrumentation/README.md index 33e178af86..99d107083d 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -11,6 +11,7 @@ | [opentelemetry-instrumentation-boto3sqs](./opentelemetry-instrumentation-boto3sqs) | boto3 ~= 1.0 | No | [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No +| [opentelemetry-instrumentation-cherrypy](./opentelemetry-instrumentation-cherrypy) | cherrypy >= 1.0 | Yes | [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka ~= 1.8.2 | No | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index 0702d4fbc8..c82f2fc1fa 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -21,6 +21,7 @@ _logger = getLogger(__name__) _excluded_urls_from_env = get_excluded_urls("CHERRYPY") + class CherryPyInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI @@ -38,11 +39,14 @@ def _instrument(self, **kwargs): def _uninstrument(self, **kwargs): cherrypy.Application = self._original_cherrypy_application + class _InstrumentedCherryPyApplication(cherrypy._cptree.Application): def __init__(self, *args, **kwargs): tracer_provider = kwargs.pop('tracer_provider', None) meter_provider = kwargs.pop('metr_provider', None) - self._otel_tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self._otel_tracer = trace.get_tracer( + __name__, __version__, tracer_provider + ) otel_meter = get_meter(__name__, __version__, meter_provider) self.duration_histogram = otel_meter.create_histogram( name="http.server.duration", @@ -57,17 +61,23 @@ def __init__(self, *args, **kwargs): self.request_hook = kwargs.pop('request_hook', None) self.response_hook = kwargs.pop('response_hook', None) excluded_urls = kwargs.pop('excluded_urls', None) - self._otel_excluded_urls = (_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)) + self._otel_excluded_urls = ( + _excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls) + ) self._is_instrumented_by_opentelemetry = True super().__init__(*args, **kwargs) - + def __call__(self, environ, start_response): - if self._otel_excluded_urls.url_disabled(environ.get('PATH_INFO', '/')): + if self._otel_excluded_urls.url_disabled( + environ.get('PATH_INFO', '/') + ): return super().__call__(environ, start_response) if not self._is_instrumented_by_opentelemetry: return super().__call__(environ, start_response) - + start_time = time_ns() span, token = _start_internal_or_server_span( tracer=self._otel_tracer, @@ -90,7 +100,9 @@ def __call__(self, environ, start_response): span.set_attribute(key, value) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = ( - otel_wsgi.collect_custom_request_headers_attributes(environ) + otel_wsgi.collect_custom_request_headers_attributes( + environ + ) ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) @@ -101,21 +113,33 @@ def __call__(self, environ, start_response): def _start_response(status, response_headers, *args, **kwargs): propagator = get_global_response_propagator() if propagator: - propagator.inject(response_headers, setter=otel_wsgi.default_response_propagation_setter) + propagator.inject( + response_headers, + setter=otel_wsgi.default_response_propagation_setter, + ) if span: - otel_wsgi.add_response_attributes(span, status, response_headers) + otel_wsgi.add_response_attributes( + span, status, response_headers + ) status_code = otel_wsgi._parse_status_code(status) if status_code is not None: - duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = status_code if span.is_recording() and span.kind == trace.SpanKind.SERVER: - custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(response_headers) + custom_attributes = ( + otel_wsgi.collect_custom_response_headers_attributes( + response_headers + ) + ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) - + if self.response_hook: self.response_hook(span, status, response_headers) return start_response(status, response_headers, *args, **kwargs) + exception = None start = default_timer() try: @@ -136,4 +160,4 @@ def _start_response(status, response_headers, *args, **kwargs): context.detach(token) duration = max(round((default_timer() - start) * 1000), 0) self.duration_histogram.record(duration, duration_attrs) - self.active_requests_counter.add(-1, active_requests_count_attrs) \ No newline at end of file + self.active_requests_counter.add(-1, active_requests_count_attrs) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py index 12ecad28e2..46c55f2807 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/package.py @@ -1,3 +1,3 @@ _instruments = ("cherrypy >= 1.0",) -_supports_metrics = True \ No newline at end of file +_supports_metrics = True diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index 7c4fa194fe..314ce2b7e8 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "opentelemetry-instrumentation-boto3sqs==0.34b0", "opentelemetry-instrumentation-botocore==0.34b0", "opentelemetry-instrumentation-celery==0.34b0", + "opentelemetry-instrumentation-cherrypy==0.34b0", "opentelemetry-instrumentation-confluent-kafka==0.34b0", "opentelemetry-instrumentation-dbapi==0.34b0", "opentelemetry-instrumentation-django==0.34b0", diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 7b943ff30c..500db4e8ef 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -52,6 +52,10 @@ "library": "celery >= 4.0, < 6.0", "instrumentation": "opentelemetry-instrumentation-celery==0.34b0", }, + "cherrypy": { + "library": "cherrypy >= 1.0", + "instrumentation": "opentelemetry-instrumentation-cherrypy==0.34b0", + }, "confluent-kafka": { "library": "confluent-kafka ~= 1.8.2", "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.34b0", From aedbcd1fa866ef6a719e4e417ba6d1dfb9f93a41 Mon Sep 17 00:00:00 2001 From: Rahul Mukherjee <40910308+rahulmukherjee68@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:38:52 +0530 Subject: [PATCH 6/9] Cherrypy Test and docs (#3) * added new testcases related to basic rest api calls and spans check for instrumentation * added tox changes and metrices testcases and fixed review comments * added metrices testcases and review comments fixed * added fixes for review comments Co-authored-by: rahmukhe --- .../README.rst | 40 +++ .../tests/__init__.py | 0 .../tests/test_cherrypy.py | 314 ++++++++++++++++++ tox.ini | 9 + 4 files changed, 363 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst b/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst index e69de29bb2..5cd5e43035 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst @@ -0,0 +1,40 @@ +OpenTelemetry CherryPy Tracing +============================ + +|pypi| + +.. |pypi| image:: TODO + :target: TODO + +This library builds on the OpenTelemetry WSGI middleware to track web requests +in CherryPy applications. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-cherrypy + +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + +References +---------- + +* `OpenTelemetry CherryPy Instrumentation `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py new file mode 100644 index 0000000000..d1f32ad543 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py @@ -0,0 +1,314 @@ +# 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. +# +from timeit import default_timer +from unittest.mock import Mock, patch + +import pytest +from cherrypy import __version__ as _cherrypy_verison +import cherrypy +from cherrypy.test import helper +from packaging import version as package_version + +from opentelemetry import trace +from opentelemetry.instrumentation.cherrypy import CherryPyInstrumentor +from opentelemetry.instrumentation.propagators import ( + TraceResponsePropagator, + get_global_response_propagator, + set_global_response_propagator, +) +from opentelemetry.instrumentation.wsgi import ( + _active_requests_count_attrs, + _duration_attrs, +) +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + NumberDataPoint, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.trace import StatusCode +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, +) + + +_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 TestCherryPyBase(TestBase, helper.CPWebCase): + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + "OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS": "ping", + "OTEL_PYTHON_CHERRYPY_TRACED_REQUEST_ATTRS": "query_string", + }, + ) + self.env_patch.start() + + CherryPyInstrumentor().instrument( + request_hook=getattr(self, "request_hook", None), + response_hook=getattr(self, "response_hook", None), + ) + + + def call(self, *args, **kwargs): + self.setup_server() + return self.getPage(*args, **kwargs) + + @staticmethod + def setup_server(): + class CherryPyApp(object): + @cherrypy.expose + def hello(self): + return {"message": "hello world"} + + @cherrypy.expose + def user(self, username): + return {"user": username} + + @cherrypy.expose + def exclude(self, param): + return {"message": param} + + @cherrypy.expose + def healthzz(self): + return {"message": "ok"} + + @cherrypy.expose + def error(self): + raise cherrypy.HTTPError(500, 'error') + + return cherrypy.tree.mount(CherryPyApp()) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + CherryPyInstrumentor().uninstrument() + self.env_patch.stop() + + +class TestCherryPyInstrumentation(TestCherryPyBase, WsgiTestBase): + def test_get(self): + self._test_method("GET") + + def test_post(self): + self._test_method("POST") + + def test_patch(self): + self._test_method("PATCH") + + def test_put(self): + self._test_method("PUT") + + def test_delete(self): + self._test_method("DELETE") + + def test_head(self): + self._test_method("HEAD") + + def _test_method(self, method): + res = self.call(method=method, url="/hello") + self.assertEqual(res[0],'200 OK') + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, f"HTTP {method.upper()}") + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 54583, + SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + SpanAttributes.HTTP_TARGET: "/hello", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + ) + self.memory_exporter.clear() + + def test_404(self): + res = self.call(method="GET", url="/does-not-exit") + self.assertEqual(res[0],'404 Not Found') + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, f"HTTP GET") + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 54583, + SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + SpanAttributes.HTTP_TARGET: "/does-not-exit", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_STATUS_CODE: 404, + }, + ) + self.memory_exporter.clear() + + def test_500(self): + res = self.call(method="GET", url="/error") + self.assertEqual(res[0],'500 Internal Server Error') + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, f"HTTP GET") + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 54583, + SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + SpanAttributes.HTTP_TARGET: "/error", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_STATUS_CODE: 500, + }, + ) + self.memory_exporter.clear() + + def test_uninstrument(self): + self.call(method="GET", url="/healthzz") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + self.memory_exporter.clear() + + CherryPyInstrumentor().uninstrument() + self.setup_server() + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + def test_cherrypy_metrics(self): + self.setup_server() + self.call(url="/hello") + self.call(url="/hello") + self.call(url="/hello") + 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.setup_server() + self.call(url="/hello") + duration = max(round((default_timer() - start) * 1000), 0) + expected_duration_attributes = { + "http.method": "GET", + "http.host": "127.0.0.1:54583", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "127.0.0.1", + "net.host.port": 54583, + "http.status_code": 200, + } + expected_requests_count_attributes = { + "http.method": "GET", + "http.host": "127.0.0.1:54583", + "http.scheme": "http", + "http.flavor": "1.1", + "http.server_name": "127.0.0.1", + } + 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=30) + if isinstance(point, NumberDataPoint): + print(expected_requests_count_attributes) + print(dict(point.attributes)) + 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.setup_server() + self.call(url="/hello") + 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) + + + \ No newline at end of file diff --git a/tox.ini b/tox.ini index f61540b7c4..631814dc40 100644 --- a/tox.ini +++ b/tox.ini @@ -55,6 +55,10 @@ envlist = py3{7,8,9,10,11}-test-instrumentation-boto pypy3-test-instrumentation-boto + ; opentelemetry-instrumentation-cherrypy + py3{7,8,9,10}-test-instrumentation-cherrypy + pypy3-test-instrumentation-cherrypy + ; opentelemetry-instrumentation-elasticsearch py3{7,8,9,10,11}-test-instrumentation-elasticsearch{2,6} pypy3-test-instrumentation-elasticsearch{2,6} @@ -221,6 +225,7 @@ deps = test: pytest-benchmark coverage: pytest coverage: pytest-cov + cherrypy: CherryPy~=18.8.0 django1: django~=1.0 django2: django~=2.0 django3: django~=3.0 @@ -273,6 +278,7 @@ changedir = test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests test-instrumentation-boto3sqs: instrumentation/opentelemetry-instrumentation-boto3sqs/tests test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests + test-instrumentation-cherrypy: instrumentation/opentelemetry-instrumentation-cherrypy/tests test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests test-instrumentation-django{1,2,3,4}: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests @@ -326,6 +332,8 @@ commands_pre = celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] + cherrypy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-cherrypy[test] + pika{0,1}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] kafka-python: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python[test] @@ -484,6 +492,7 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] + python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-cherrypy[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] From 2ed744ebf71ef64374d2842e9232caea903acb25 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Tue, 29 Nov 2022 16:44:00 +0530 Subject: [PATCH 7/9] fix generate --- .../instrumentation/cherrypy/version.py | 2 +- .../pyproject.toml | 85 ++++++++++--------- .../instrumentation/bootstrap_gen.py | 2 +- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py index a590883f7e..5ea2af4ddd 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/version.py @@ -1 +1 @@ -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index d424a69d21..6ac7b91567 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -29,48 +29,49 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-instrumentation-aio-pika==0.34b0", - "opentelemetry-instrumentation-aiohttp-client==0.34b0", - "opentelemetry-instrumentation-aiopg==0.34b0", - "opentelemetry-instrumentation-asgi==0.34b0", - "opentelemetry-instrumentation-asyncpg==0.34b0", - "opentelemetry-instrumentation-aws-lambda==0.34b0", - "opentelemetry-instrumentation-boto==0.34b0", - "opentelemetry-instrumentation-boto3sqs==0.34b0", - "opentelemetry-instrumentation-botocore==0.34b0", - "opentelemetry-instrumentation-celery==0.34b0", - "opentelemetry-instrumentation-cherrypy==0.34b0", - "opentelemetry-instrumentation-confluent-kafka==0.34b0", - "opentelemetry-instrumentation-dbapi==0.34b0", - "opentelemetry-instrumentation-django==0.34b0", - "opentelemetry-instrumentation-elasticsearch==0.34b0", - "opentelemetry-instrumentation-falcon==0.34b0", - "opentelemetry-instrumentation-fastapi==0.34b0", - "opentelemetry-instrumentation-flask==0.34b0", - "opentelemetry-instrumentation-grpc==0.34b0", - "opentelemetry-instrumentation-httpx==0.34b0", - "opentelemetry-instrumentation-jinja2==0.34b0", - "opentelemetry-instrumentation-kafka-python==0.34b0", - "opentelemetry-instrumentation-logging==0.34b0", - "opentelemetry-instrumentation-mysql==0.34b0", - "opentelemetry-instrumentation-pika==0.34b0", - "opentelemetry-instrumentation-psycopg2==0.34b0", - "opentelemetry-instrumentation-pymemcache==0.34b0", - "opentelemetry-instrumentation-pymongo==0.34b0", - "opentelemetry-instrumentation-pymysql==0.34b0", - "opentelemetry-instrumentation-pyramid==0.34b0", - "opentelemetry-instrumentation-redis==0.34b0", - "opentelemetry-instrumentation-remoulade==0.34b0", - "opentelemetry-instrumentation-requests==0.34b0", - "opentelemetry-instrumentation-sklearn==0.34b0", - "opentelemetry-instrumentation-sqlalchemy==0.34b0", - "opentelemetry-instrumentation-sqlite3==0.34b0", - "opentelemetry-instrumentation-starlette==0.34b0", - "opentelemetry-instrumentation-system-metrics==0.34b0", - "opentelemetry-instrumentation-tornado==0.34b0", - "opentelemetry-instrumentation-urllib==0.34b0", - "opentelemetry-instrumentation-urllib3==0.34b0", - "opentelemetry-instrumentation-wsgi==0.34b0", + "opentelemetry-instrumentation-aio-pika==0.36b0.dev", + "opentelemetry-instrumentation-aiohttp-client==0.36b0.dev", + "opentelemetry-instrumentation-aiopg==0.36b0.dev", + "opentelemetry-instrumentation-asgi==0.36b0.dev", + "opentelemetry-instrumentation-asyncpg==0.36b0.dev", + "opentelemetry-instrumentation-aws-lambda==0.36b0.dev", + "opentelemetry-instrumentation-boto==0.36b0.dev", + "opentelemetry-instrumentation-boto3sqs==0.36b0.dev", + "opentelemetry-instrumentation-botocore==0.36b0.dev", + "opentelemetry-instrumentation-celery==0.36b0.dev", + "opentelemetry-instrumentation-cherrypy==0.36b0.dev", + "opentelemetry-instrumentation-confluent-kafka==0.36b0.dev", + "opentelemetry-instrumentation-dbapi==0.36b0.dev", + "opentelemetry-instrumentation-django==0.36b0.dev", + "opentelemetry-instrumentation-elasticsearch==0.36b0.dev", + "opentelemetry-instrumentation-falcon==0.36b0.dev", + "opentelemetry-instrumentation-fastapi==0.36b0.dev", + "opentelemetry-instrumentation-flask==0.36b0.dev", + "opentelemetry-instrumentation-grpc==0.36b0.dev", + "opentelemetry-instrumentation-httpx==0.36b0.dev", + "opentelemetry-instrumentation-jinja2==0.36b0.dev", + "opentelemetry-instrumentation-kafka-python==0.36b0.dev", + "opentelemetry-instrumentation-logging==0.36b0.dev", + "opentelemetry-instrumentation-mysql==0.36b0.dev", + "opentelemetry-instrumentation-pika==0.36b0.dev", + "opentelemetry-instrumentation-psycopg2==0.36b0.dev", + "opentelemetry-instrumentation-pymemcache==0.36b0.dev", + "opentelemetry-instrumentation-pymongo==0.36b0.dev", + "opentelemetry-instrumentation-pymysql==0.36b0.dev", + "opentelemetry-instrumentation-pyramid==0.36b0.dev", + "opentelemetry-instrumentation-redis==0.36b0.dev", + "opentelemetry-instrumentation-remoulade==0.36b0.dev", + "opentelemetry-instrumentation-requests==0.36b0.dev", + "opentelemetry-instrumentation-sklearn==0.36b0.dev", + "opentelemetry-instrumentation-sqlalchemy==0.36b0.dev", + "opentelemetry-instrumentation-sqlite3==0.36b0.dev", + "opentelemetry-instrumentation-starlette==0.36b0.dev", + "opentelemetry-instrumentation-system-metrics==0.36b0.dev", + "opentelemetry-instrumentation-tornado==0.36b0.dev", + "opentelemetry-instrumentation-tortoiseorm==0.36b0.dev", + "opentelemetry-instrumentation-urllib==0.36b0.dev", + "opentelemetry-instrumentation-urllib3==0.36b0.dev", + "opentelemetry-instrumentation-wsgi==0.36b0.dev", ] [project.optional-dependencies] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 61fb860c4e..a48451cfe6 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -54,7 +54,7 @@ }, "cherrypy": { "library": "cherrypy >= 1.0", - "instrumentation": "opentelemetry-instrumentation-cherrypy==0.34b0", + "instrumentation": "opentelemetry-instrumentation-cherrypy==0.36b0.dev", }, "confluent-kafka": { "library": "confluent-kafka ~= 1.8.2", From 66077e253c3bef96cc861fa1c9e308ecf6993392 Mon Sep 17 00:00:00 2001 From: Rahul Mukherjee <40910308+rahulmukherjee68@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:20:32 +0530 Subject: [PATCH 8/9] Cherrypy instrumentation docs tests (#4) * added new testcases related to basic rest api calls and spans check for instrumentation * added tox changes and metrices testcases and fixed review comments * added metrices testcases and review comments fixed * added fixes for review comments * added custom header testcases for instrumentation * removed print statements * added return statement to cherrypy app Co-authored-by: rahmukhe --- .../tests/test_cherrypy.py | 215 +++++++++++++++++- 1 file changed, 207 insertions(+), 8 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py index d1f32ad543..6bd34d0c05 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py @@ -14,14 +14,17 @@ # from timeit import default_timer from unittest.mock import Mock, patch +import unittest import pytest +import os from cherrypy import __version__ as _cherrypy_verison import cherrypy from cherrypy.test import helper from packaging import version as package_version from opentelemetry import trace +from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.instrumentation.cherrypy import CherryPyInstrumentor from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, @@ -44,6 +47,9 @@ 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, ) @@ -62,11 +68,16 @@ def setUp(self): self.env_patch = patch.dict( "os.environ", { - "OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS": "ping", + "OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS": "exclude,healthzz", "OTEL_PYTHON_CHERRYPY_TRACED_REQUEST_ATTRS": "query_string", }, ) self.env_patch.start() + self.exclude_patch = patch( + "opentelemetry.instrumentation.cherrypy._excluded_urls_from_env", + get_excluded_urls("CHERRYPY"), + ) + self.exclude_patch.start() CherryPyInstrumentor().instrument( request_hook=getattr(self, "request_hook", None), @@ -90,8 +101,8 @@ def user(self, username): return {"user": username} @cherrypy.expose - def exclude(self, param): - return {"message": param} + def exclude(self): + return "excluded route" @cherrypy.expose def healthzz(self): @@ -100,17 +111,25 @@ def healthzz(self): @cherrypy.expose def error(self): raise cherrypy.HTTPError(500, 'error') + + @cherrypy.expose + def check_header(self): + cherrypy.response.headers["custom-test-header-1"]="test-header-value-1" + cherrypy.response.headers["custom-test-header-2"]="test-header-value-2" + content = {"message": "hello world"} + return content return cherrypy.tree.mount(CherryPyApp()) def tearDown(self): super().tearDown() + self.exclude_patch.stop() with self.disable_logging(): CherryPyInstrumentor().uninstrument() self.env_patch.stop() -class TestCherryPyInstrumentation(TestCherryPyBase, WsgiTestBase): +class TestCherryPyAutoInstrumentation(TestCherryPyBase, WsgiTestBase): def test_get(self): self._test_method("GET") @@ -212,7 +231,7 @@ def test_500(self): self.memory_exporter.clear() def test_uninstrument(self): - self.call(method="GET", url="/healthzz") + self.call(method="GET", url="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -222,6 +241,14 @@ def test_uninstrument(self): self.setup_server() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) + + def test_exclude_lists(self): + self.call(method="GET", url="/exclude") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + self.call(method="GET", url="/healthzz") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) def test_cherrypy_metrics(self): self.setup_server() @@ -286,8 +313,6 @@ def test_basic_metric_success(self): self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=30) if isinstance(point, NumberDataPoint): - print(expected_requests_count_attributes) - print(dict(point.attributes)) self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), @@ -309,6 +334,180 @@ def test_basic_post_request_metric_success(self): self.assertAlmostEqual(duration, point.sum, delta=30) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) + + +class TestCherryPyCustomHeaders(TestBase, helper.CPWebCase): + + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + CherryPyInstrumentor().instrument() + + def call(self, *args, **kwargs): + self.setup_server() + return self.getPage(*args, **kwargs) - \ No newline at end of file + @staticmethod + def setup_server(): + class CherryPyApp(object): + + @cherrypy.expose + def check_header(self): + cherrypy.response.headers["custom-test-header-1"]="test-header-value-1" + cherrypy.response.headers["custom-test-header-2"]="test-header-value-2" + content = {"message": "hello world"} + return content + + return cherrypy.tree.mount(CherryPyApp()) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + CherryPyInstrumentor().uninstrument() + self.env_patch.stop() + + def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + resp = self.call( + url="/check_header", + headers=[ + ("custom-test-header-1","test-header-value-1"), + ("custom-test-header-2","test-header-value-2"), + ], + ) + self.assertEqual('200 OK', resp[0]) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) + + def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self.call( + url="/check_header", + headers=[ + ("custom-test-header-1","test-header-value-1"), + ("custom-test-header-2","test-header-value-2"), + ], + ) + self.assertEqual('200 OK', resp[0]) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + resp = self.call(url="/check_header") + self.assertEqual('200 OK', resp[0]) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + self.assertSpanHasAttributes(server_span, expected) + + def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self.call(url="/check_header") + self.assertEqual('200 OK', resp[0]) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + +class TestNonRecordingSpanWithCustomHeaders(TestBase, helper.CPWebCase): + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + + reset_trace_globals() + tracer_provider = trace.NoOpTracerProvider() + trace.set_tracer_provider(tracer_provider=tracer_provider) + + self._instrumentor = CherryPyInstrumentor() + self._instrumentor.instrument() + + def call(self, *args, **kwargs): + self.setup_server() + return self.getPage(*args, **kwargs) + + @staticmethod + def setup_server(): + class CherryPyApp(object): + + @cherrypy.expose + def check_header(self): + content = {"message": "hello world"} + return content + + return cherrypy.tree.mount(CherryPyApp()) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + CherryPyInstrumentor().uninstrument() + self.env_patch.stop() + + def test_custom_header_not_present_in_non_recording_span(self): + resp = self.call( + url="/check_header", + headers=[ + ("custom-test-header-1","test-header-value-1"), + ], + ) + self.assertEqual('200 OK', resp[0]) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + From 41d94e92a0afffb3b34240209ff023427933a827 Mon Sep 17 00:00:00 2001 From: "anshul.asawa" Date: Tue, 6 Dec 2022 14:37:20 +0530 Subject: [PATCH 9/9] fix get traced request attributes --- .../opentelemetry/instrumentation/cherrypy/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index c82f2fc1fa..193c46c303 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -3,11 +3,11 @@ from timeit import default_timer from typing import Collection -from opentelemetry.util.http import parse_excluded_urls, get_excluded_urls +from opentelemetry.util.http import parse_excluded_urls, get_excluded_urls, get_traced_request_attrs import cherrypy from opentelemetry.instrumentation.cherrypy.package import _instruments from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import _start_internal_or_server_span +from opentelemetry.instrumentation.utils import _start_internal_or_server_span, extract_attributes_from_object import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, @@ -66,6 +66,7 @@ def __init__(self, *args, **kwargs): if excluded_urls is None else parse_excluded_urls(excluded_urls) ) + self._traced_request_attrs = get_traced_request_attrs("CHERRYPY") self._is_instrumented_by_opentelemetry = True super().__init__(*args, **kwargs) @@ -96,6 +97,9 @@ def __call__(self, environ, start_response): self.active_requests_counter.add(1, active_requests_count_attrs) if span.is_recording(): + attributes = extract_attributes_from_object( + environ, self._traced_request_attrs, attributes + ) for key, value in attributes.items(): span.set_attribute(key, value) if span.is_recording() and span.kind == trace.SpanKind.SERVER: