Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherrypy instrumentation #1410

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions instrumentation/opentelemetry-instrumentation-cherrypy/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
OpenTelemetry CherryPy Tracing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an example how to use this instrumentation

============================

|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 <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/cherrypy/cherrypy.html>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from logging import getLogger
from time import time_ns
from timeit import default_timer
from typing import Collection

from opentelemetry.util.http import parse_excluded_urls, get_excluded_urls, get_traced_request_attrs
import cherrypy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

organize the import statement properly

from opentelemetry.instrumentation.cherrypy.package import _instruments
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
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,
)
from opentelemetry import trace, context
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.instrumentation.cherrypy.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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the docstring from FastAPI to CherryPy. Also please add an example how to use it


See `BaseInstrumentor`
"""

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
self._original_cherrypy_application = cherrypy._cptree.Application
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a better way to do this. We can't use the private symbols because they are not guaranteed to remain backward compatible.

cherrypy._cptree.Application = _InstrumentedCherryPyApplication
cherrypy.Application = _InstrumentedCherryPyApplication
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue with these hooks are that they needed to be called for every paths that we want to use them for. They do not apply directly to everywhere. source


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
)
otel_meter = get_meter(__name__, __version__, meter_provider)
self.duration_histogram = 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)
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._traced_request_attrs = get_traced_request_attrs("CHERRYPY")
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', '/')
):
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():
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:
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):
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)
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests. If the PR is not ready then please mark it a draft.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I'm really sorry. I forgot to mark this PR draft. There's still lot of work required in this PR.

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use exception instead of exc just to make it more readable

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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
_instruments = ("cherrypy >= 1.0",)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also pin the upper version, there can be breaking changes b/w major version changes.


_supports_metrics = True
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.36b0.dev"
Empty file.
Loading