Metric instrumentation pyramid (#1242)

This commit is contained in:
Anshul Asawa 2022-09-12 18:03:45 +05:30 committed by GitHub
parent 318a3a3afc
commit fee9926f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 2 deletions

View File

@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Flask sqlalchemy psycopg2 integration - Flask sqlalchemy psycopg2 integration
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224)) ([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
- Add metric instrumentation in Pyramid
([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242))
### Fixed ### Fixed

View File

@ -14,6 +14,7 @@
from logging import getLogger from logging import getLogger
from time import time_ns from time import time_ns
from timeit import default_timer
from pyramid.events import BeforeTraversal from pyramid.events import BeforeTraversal
from pyramid.httpexceptions import HTTPException, HTTPServerError from pyramid.httpexceptions import HTTPException, HTTPServerError
@ -27,6 +28,7 @@ from opentelemetry.instrumentation.propagators import (
) )
from opentelemetry.instrumentation.pyramid.version import __version__ from opentelemetry.instrumentation.pyramid.version import __version__
from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.instrumentation.utils import _start_internal_or_server_span
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.util.http import get_excluded_urls from opentelemetry.util.http import get_excluded_urls
@ -122,8 +124,20 @@ def _before_traversal(event):
def trace_tween_factory(handler, registry): def trace_tween_factory(handler, registry):
# pylint: disable=too-many-statements
settings = registry.settings settings = registry.settings
enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) 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 not enabled:
# If disabled, make a tween that signals to the # If disabled, make a tween that signals to the
@ -137,14 +151,23 @@ def trace_tween_factory(handler, registry):
# make a request tracing function # make a request tracing function
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def trace_tween(request): def trace_tween(request):
# pylint: disable=E1101 # pylint: disable=E1101, too-many-locals
if _excluded_urls.url_disabled(request.url): if _excluded_urls.url_disabled(request.url):
request.environ[_ENVIRON_ENABLED_KEY] = False request.environ[_ENVIRON_ENABLED_KEY] = False
# short-circuit when we don't want to trace anything # short-circuit when we don't want to trace anything
return handler(request) return handler(request)
attributes = otel_wsgi.collect_request_attributes(request.environ)
request.environ[_ENVIRON_ENABLED_KEY] = True request.environ[_ENVIRON_ENABLED_KEY] = True
request.environ[_ENVIRON_STARTTIME_KEY] = time_ns() 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 response = None
status = None status = None
@ -165,6 +188,15 @@ def trace_tween_factory(handler, registry):
status = "500 InternalServerError" status = "500 InternalServerError"
raise raise
finally: 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) span = request.environ.get(_ENVIRON_SPAN_KEY)
enabled = request.environ.get(_ENVIRON_ENABLED_KEY) enabled = request.environ.get(_ENVIRON_ENABLED_KEY)
if not span and enabled: if not span and enabled:
@ -174,7 +206,6 @@ def trace_tween_factory(handler, registry):
"PyramidInstrumentor().instrument_config(config) is called" "PyramidInstrumentor().instrument_config(config) is called"
) )
elif enabled: elif enabled:
status = getattr(response, "status", status)
if status is not None: if status is not None:
otel_wsgi.add_response_attributes( otel_wsgi.add_response_attributes(

View File

@ -12,12 +12,17 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from timeit import default_timer
from unittest.mock import patch from unittest.mock import patch
from pyramid.config import Configurator from pyramid.config import Configurator
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.instrumentation.pyramid import PyramidInstrumentor 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.globals_test import reset_trace_globals
from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.test.wsgitestutil import WsgiTestBase
from opentelemetry.trace import SpanKind from opentelemetry.trace import SpanKind
@ -25,11 +30,22 @@ from opentelemetry.trace.status import StatusCode
from opentelemetry.util.http import ( from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
_active_requests_count_attrs,
_duration_attrs,
) )
# pylint: disable=import-error # pylint: disable=import-error
from .pyramid_base_test import InstrumentationTest 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): class TestAutomatic(InstrumentationTest, WsgiTestBase):
def setUp(self): def setUp(self):
@ -156,6 +172,89 @@ class TestAutomatic(InstrumentationTest, WsgiTestBase):
span_list = self.memory_exporter.get_finished_spans() span_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(span_list), 1) 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): class TestWrappedWithOtherFramework(InstrumentationTest, WsgiTestBase):
def setUp(self): def setUp(self):