-
Notifications
You must be signed in to change notification settings - Fork 629
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
base: main
Are you sure you want to change the base?
Cherrypy instrumentation #1410
Changes from all commits
444a29b
bf6f816
083b023
215cd02
4933b32
0ac9dc1
fa30e55
aedbcd1
232c6ca
2ed744e
c6db7ef
28f386c
66077e2
b37251f
41d94e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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",) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" |
There was a problem hiding this comment.
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