Metric instrumentation pyramid (#1242)
This commit is contained in:
parent
318a3a3afc
commit
fee9926f50
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue