Merge branch 'main' into dependabot/pip/requests-2.31.0
This commit is contained in:
commit
772834c973
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update falcon instrumentation to follow semantic conventions
|
||||
([#1824](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1824))
|
||||
|
||||
### Added
|
||||
|
||||
- Make Flask request span attributes available for `start_span`.
|
||||
([#1784](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1784))
|
||||
- Fix falcon instrumentation's usage of Span Status to only set the description if the status code is ERROR.
|
||||
([#1840](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1840))
|
||||
- Instrument all httpx versions >= 0.18. ([#1748](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1748))
|
||||
- Fix `Invalid type NoneType for attribute X (opentelemetry-instrumentation-aws-lambda)` error when some attributes do not exist
|
||||
([#1780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1780))
|
||||
- Add metric instrumentation for celery
|
||||
([#1679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1679))
|
||||
- `opentelemetry-instrumentation-asgi` Add `http.server.response.size` metric
|
||||
([#1789](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1789))
|
||||
|
||||
## Version 1.18.0/0.39b0 (2023-05-10)
|
||||
|
||||
- `opentelemetry-instrumentation-system-metrics` Add `process.` prefix to `runtime.memory`, `runtime.cpu.time`, and `runtime.gc_count`. Change `runtime.memory` from count to UpDownCounter. ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735))
|
||||
|
|
@ -14,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
([#1706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1706))
|
||||
- `opentelemetry-instrumentation-pymemcache` Update instrumentation to support pymemcache >4
|
||||
([#1764](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1764))
|
||||
- `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions of confluent_kafka
|
||||
([#1815](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1815))
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -21,9 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
([#1778](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1778))
|
||||
- Add `excluded_urls` functionality to `urllib` and `urllib3` instrumentations
|
||||
([#1733](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1733))
|
||||
- Make Django request span attributes available for `start_span`.
|
||||
- Make Django request span attributes available for `start_span`.
|
||||
([#1730](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1730))
|
||||
- Make ASGI request span attributes available for `start_span`.
|
||||
- Make ASGI request span attributes available for `start_span`.
|
||||
([#1762](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1762))
|
||||
- `opentelemetry-instrumentation-celery` Add support for anonymous tasks.
|
||||
([#1407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1407))
|
||||
|
|
@ -38,12 +59,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fix redis db.statements to be sanitized by default
|
||||
([#1778](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1778))
|
||||
- Fix elasticsearch db.statement attribute to be sanitized by default
|
||||
([#1758](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1758))
|
||||
- Fix `AttributeError` when AWS Lambda handler receives a list event
|
||||
([#1738](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1738))
|
||||
- Fix `None does not implement middleware` error when there are no middlewares registered
|
||||
([#1766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1766))
|
||||
- Fix Flask instrumentation to only close the span if it was created by the same request context.
|
||||
([#1692](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1692))
|
||||
|
||||
### Changed
|
||||
- Update HTTP server/client instrumentation span names to comply with spec
|
||||
([#1759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1759)
|
||||
|
||||
## Version 1.17.0/0.38b0 (2023-03-22)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ bleach==4.1.0 # transient dependency for readme-renderer
|
|||
grpcio-tools==1.29.0
|
||||
mypy-protobuf>=1.23
|
||||
protobuf~=3.13
|
||||
markupsafe==2.0.1
|
||||
markupsafe>=2.0.1
|
||||
codespell==2.1.0
|
||||
requests==2.31.0
|
||||
ruamel.yaml==0.17.21
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ boto~=2.0
|
|||
botocore~=1.0
|
||||
boto3~=1.0
|
||||
celery>=4.0
|
||||
confluent-kafka>= 1.8.2,< 2.0.0
|
||||
confluent-kafka>= 1.8.2,<= 2.2.0
|
||||
elasticsearch>=2.0,<9.0
|
||||
flask~=2.0
|
||||
falcon~=2.0
|
||||
|
|
|
|||
|
|
@ -11,7 +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-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, < 2.0.0 | No
|
||||
| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.2.0 | No
|
||||
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No
|
||||
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes
|
||||
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ instruments = [
|
|||
]
|
||||
test = [
|
||||
"opentelemetry-instrumentation-aiohttp-client[instruments]",
|
||||
"http-server-mock"
|
||||
]
|
||||
|
||||
[project.entry-points.opentelemetry_instrumentor]
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ def create_trace_config(
|
|||
return
|
||||
|
||||
http_method = params.method.upper()
|
||||
request_span_name = f"HTTP {http_method}"
|
||||
request_span_name = f"{http_method}"
|
||||
request_url = (
|
||||
remove_url_credentials(trace_config_ctx.url_filter(params.url))
|
||||
if callable(trace_config_ctx.url_filter)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from unittest import mock
|
|||
import aiohttp
|
||||
import aiohttp.test_utils
|
||||
import yarl
|
||||
from http_server_mock import HttpServerMock
|
||||
from pkg_resources import iter_entry_points
|
||||
|
||||
from opentelemetry import context
|
||||
|
|
@ -118,7 +119,7 @@ class TestAioHttpIntegration(TestBase):
|
|||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(span_status, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -212,7 +213,7 @@ class TestAioHttpIntegration(TestBase):
|
|||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(StatusCode.UNSET, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -246,7 +247,7 @@ class TestAioHttpIntegration(TestBase):
|
|||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(expected_status, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -273,7 +274,7 @@ class TestAioHttpIntegration(TestBase):
|
|||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(StatusCode.ERROR, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -300,7 +301,7 @@ class TestAioHttpIntegration(TestBase):
|
|||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(StatusCode.ERROR, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -313,27 +314,37 @@ class TestAioHttpIntegration(TestBase):
|
|||
def test_credential_removal(self):
|
||||
trace_configs = [aiohttp_client.create_trace_config()]
|
||||
|
||||
url = "http://username:password@httpbin.org/status/200"
|
||||
with self.subTest(url=url):
|
||||
app = HttpServerMock("test_credential_removal")
|
||||
|
||||
async def do_request(url):
|
||||
async with aiohttp.ClientSession(
|
||||
trace_configs=trace_configs,
|
||||
) as session:
|
||||
async with session.get(url):
|
||||
pass
|
||||
@app.route("/status/200")
|
||||
def index():
|
||||
return "hello"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(do_request(url))
|
||||
url = "http://username:password@localhost:5000/status/200"
|
||||
|
||||
with app.run("localhost", 5000):
|
||||
with self.subTest(url=url):
|
||||
|
||||
async def do_request(url):
|
||||
async with aiohttp.ClientSession(
|
||||
trace_configs=trace_configs,
|
||||
) as session:
|
||||
async with session.get(url):
|
||||
pass
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(do_request(url))
|
||||
|
||||
self.assert_spans(
|
||||
[
|
||||
(
|
||||
"HTTP GET",
|
||||
"GET",
|
||||
(StatusCode.UNSET, None),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_URL: "http://httpbin.org/status/200",
|
||||
SpanAttributes.HTTP_URL: (
|
||||
"http://localhost:5000/status/200"
|
||||
),
|
||||
SpanAttributes.HTTP_STATUS_CODE: int(HTTPStatus.OK),
|
||||
},
|
||||
)
|
||||
|
|
@ -380,6 +391,7 @@ class TestAioHttpClientInstrumentor(TestBase):
|
|||
self.get_default_request(), self.URL, self.default_handler
|
||||
)
|
||||
span = self.assert_spans(1)
|
||||
self.assertEqual("GET", span.name)
|
||||
self.assertEqual("GET", span.attributes[SpanAttributes.HTTP_METHOD])
|
||||
self.assertEqual(
|
||||
f"http://{host}:{port}/test-path",
|
||||
|
|
|
|||
|
|
@ -415,18 +415,23 @@ def set_status_code(span, status_code):
|
|||
|
||||
|
||||
def get_default_span_details(scope: dict) -> Tuple[str, dict]:
|
||||
"""Default implementation for get_default_span_details
|
||||
"""
|
||||
Default span name is the HTTP method and URL path, or just the method.
|
||||
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
|
||||
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
|
||||
|
||||
Args:
|
||||
scope: the ASGI scope dictionary
|
||||
Returns:
|
||||
a tuple of the span name, and any attributes to attach to the span.
|
||||
"""
|
||||
span_name = (
|
||||
scope.get("path", "").strip()
|
||||
or f"HTTP {scope.get('method', '').strip()}"
|
||||
)
|
||||
|
||||
return span_name, {}
|
||||
path = scope.get("path", "").strip()
|
||||
method = scope.get("method", "").strip()
|
||||
if method and path: # http
|
||||
return f"{method} {path}", {}
|
||||
if path: # websocket
|
||||
return path, {}
|
||||
return method, {} # http with no path
|
||||
|
||||
|
||||
def _collect_target_attribute(
|
||||
|
|
@ -501,6 +506,11 @@ class OpenTelemetryMiddleware:
|
|||
unit="ms",
|
||||
description="measures the duration of the inbound HTTP request",
|
||||
)
|
||||
self.server_response_size_histogram = self.meter.create_histogram(
|
||||
name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE,
|
||||
unit="By",
|
||||
description="measures the size of HTTP response messages (compressed).",
|
||||
)
|
||||
self.active_requests_counter = self.meter.create_up_down_counter(
|
||||
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
|
||||
unit="requests",
|
||||
|
|
@ -513,6 +523,7 @@ class OpenTelemetryMiddleware:
|
|||
self.server_request_hook = server_request_hook
|
||||
self.client_request_hook = client_request_hook
|
||||
self.client_response_hook = client_response_hook
|
||||
self.content_length_header = None
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
"""The ASGI application
|
||||
|
|
@ -588,6 +599,10 @@ class OpenTelemetryMiddleware:
|
|||
self.active_requests_counter.add(
|
||||
-1, active_requests_count_attrs
|
||||
)
|
||||
if self.content_length_header:
|
||||
self.server_response_size_histogram.record(
|
||||
self.content_length_header, duration_attrs
|
||||
)
|
||||
if token:
|
||||
context.detach(token)
|
||||
|
||||
|
|
@ -655,6 +670,13 @@ class OpenTelemetryMiddleware:
|
|||
setter=asgi_setter,
|
||||
)
|
||||
|
||||
content_length = asgi_getter.get(message, "content-length")
|
||||
if content_length:
|
||||
try:
|
||||
self.content_length_header = int(content_length[0])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
await send(message)
|
||||
|
||||
return otel_send
|
||||
|
|
|
|||
|
|
@ -46,10 +46,12 @@ from opentelemetry.util.http import (
|
|||
_expected_metric_names = [
|
||||
"http.server.active_requests",
|
||||
"http.server.duration",
|
||||
"http.server.response.size",
|
||||
]
|
||||
_recommended_attrs = {
|
||||
"http.server.active_requests": _active_requests_count_attrs,
|
||||
"http.server.duration": _duration_attrs,
|
||||
"http.server.response.size": _duration_attrs,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -61,7 +63,10 @@ async def http_app(scope, receive, send):
|
|||
{
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [[b"Content-Type", b"text/plain"]],
|
||||
"headers": [
|
||||
[b"Content-Type", b"text/plain"],
|
||||
[b"content-length", b"1024"],
|
||||
],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": b"*"})
|
||||
|
|
@ -103,7 +108,10 @@ async def error_asgi(scope, receive, send):
|
|||
{
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [[b"Content-Type", b"text/plain"]],
|
||||
"headers": [
|
||||
[b"Content-Type", b"text/plain"],
|
||||
[b"content-length", b"1024"],
|
||||
],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": b"*"})
|
||||
|
|
@ -126,7 +134,8 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
# Check http response start
|
||||
self.assertEqual(response_start["status"], 200)
|
||||
self.assertEqual(
|
||||
response_start["headers"], [[b"Content-Type", b"text/plain"]]
|
||||
response_start["headers"],
|
||||
[[b"Content-Type", b"text/plain"], [b"content-length", b"1024"]],
|
||||
)
|
||||
|
||||
exc_info = self.scope.get("hack_exc_info")
|
||||
|
|
@ -142,12 +151,12 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
self.assertEqual(len(span_list), 4)
|
||||
expected = [
|
||||
{
|
||||
"name": "/ http receive",
|
||||
"name": "GET / http receive",
|
||||
"kind": trace_api.SpanKind.INTERNAL,
|
||||
"attributes": {"type": "http.request"},
|
||||
},
|
||||
{
|
||||
"name": "/ http send",
|
||||
"name": "GET / http send",
|
||||
"kind": trace_api.SpanKind.INTERNAL,
|
||||
"attributes": {
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
|
|
@ -155,12 +164,12 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
},
|
||||
},
|
||||
{
|
||||
"name": "/ http send",
|
||||
"name": "GET / http send",
|
||||
"kind": trace_api.SpanKind.INTERNAL,
|
||||
"attributes": {"type": "http.response.body"},
|
||||
},
|
||||
{
|
||||
"name": "/",
|
||||
"name": "GET /",
|
||||
"kind": trace_api.SpanKind.SERVER,
|
||||
"attributes": {
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -231,7 +240,7 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
entry["name"] = span_name
|
||||
else:
|
||||
entry["name"] = " ".join(
|
||||
[span_name] + entry["name"].split(" ")[1:]
|
||||
[span_name] + entry["name"].split(" ")[2:]
|
||||
)
|
||||
return expected
|
||||
|
||||
|
|
@ -352,6 +361,7 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
response_start["headers"],
|
||||
[
|
||||
[b"Content-Type", b"text/plain"],
|
||||
[b"content-length", b"1024"],
|
||||
[b"traceresponse", f"{traceresponse}".encode()],
|
||||
[b"access-control-expose-headers", b"traceresponse"],
|
||||
],
|
||||
|
|
@ -493,9 +503,9 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
for entry in expected:
|
||||
if entry["kind"] == trace_api.SpanKind.SERVER:
|
||||
entry["name"] = "name from server hook"
|
||||
elif entry["name"] == "/ http receive":
|
||||
elif entry["name"] == "GET / http receive":
|
||||
entry["name"] = "name from client request hook"
|
||||
elif entry["name"] == "/ http send":
|
||||
elif entry["name"] == "GET / http send":
|
||||
entry["attributes"].update({"attr-from-hook": "value"})
|
||||
return expected
|
||||
|
||||
|
|
@ -565,6 +575,7 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
"http.flavor": "1.0",
|
||||
}
|
||||
metrics_list = self.memory_metrics_reader.get_metrics_data()
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for resource_metric in metrics_list.resource_metrics:
|
||||
for scope_metrics in resource_metric.scope_metrics:
|
||||
for metric in scope_metrics.metrics:
|
||||
|
|
@ -575,9 +586,12 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
dict(point.attributes),
|
||||
)
|
||||
self.assertEqual(point.count, 1)
|
||||
self.assertAlmostEqual(
|
||||
duration, point.sum, delta=5
|
||||
)
|
||||
if metric.name == "http.server.duration":
|
||||
self.assertAlmostEqual(
|
||||
duration, point.sum, delta=5
|
||||
)
|
||||
elif metric.name == "http.server.response.size":
|
||||
self.assertEqual(1024, point.sum)
|
||||
elif isinstance(point, NumberDataPoint):
|
||||
self.assertDictEqual(
|
||||
expected_requests_count_attributes,
|
||||
|
|
@ -602,13 +616,12 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
app = otel_asgi.OpenTelemetryMiddleware(target_asgi)
|
||||
self.seed_app(app)
|
||||
self.send_default_request()
|
||||
|
||||
metrics_list = self.memory_metrics_reader.get_metrics_data()
|
||||
assertions = 0
|
||||
for resource_metric in metrics_list.resource_metrics:
|
||||
for scope_metrics in resource_metric.scope_metrics:
|
||||
for metric in scope_metrics.metrics:
|
||||
if metric.name != "http.server.duration":
|
||||
if metric.name == "http.server.active_requests":
|
||||
continue
|
||||
for point in metric.data.data_points:
|
||||
if isinstance(point, HistogramDataPoint):
|
||||
|
|
@ -617,7 +630,7 @@ class TestAsgiApplication(AsgiTestBase):
|
|||
expected_target,
|
||||
)
|
||||
assertions += 1
|
||||
self.assertEqual(assertions, 1)
|
||||
self.assertEqual(assertions, 2)
|
||||
|
||||
def test_no_metric_for_websockets(self):
|
||||
self.scope = {
|
||||
|
|
@ -705,11 +718,11 @@ class TestAsgiAttributes(unittest.TestCase):
|
|||
self.assertEqual(self.span.set_status.call_count, 1)
|
||||
|
||||
def test_credential_removal(self):
|
||||
self.scope["server"] = ("username:password@httpbin.org", 80)
|
||||
self.scope["server"] = ("username:password@mock", 80)
|
||||
self.scope["path"] = "/status/200"
|
||||
attrs = otel_asgi.collect_request_attributes(self.scope)
|
||||
self.assertEqual(
|
||||
attrs[SpanAttributes.HTTP_URL], "http://httpbin.org/status/200"
|
||||
attrs[SpanAttributes.HTTP_URL], "http://mock/status/200"
|
||||
)
|
||||
|
||||
def test_collect_target_attribute_missing(self):
|
||||
|
|
|
|||
|
|
@ -201,30 +201,35 @@ def _set_api_gateway_v1_proxy_attributes(
|
|||
span.set_attribute(
|
||||
SpanAttributes.HTTP_METHOD, lambda_event.get("httpMethod")
|
||||
)
|
||||
span.set_attribute(SpanAttributes.HTTP_ROUTE, lambda_event.get("resource"))
|
||||
|
||||
if lambda_event.get("headers"):
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_USER_AGENT,
|
||||
lambda_event["headers"].get("User-Agent"),
|
||||
)
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_SCHEME,
|
||||
lambda_event["headers"].get("X-Forwarded-Proto"),
|
||||
)
|
||||
span.set_attribute(
|
||||
SpanAttributes.NET_HOST_NAME, lambda_event["headers"].get("Host")
|
||||
)
|
||||
if "User-Agent" in lambda_event["headers"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_USER_AGENT,
|
||||
lambda_event["headers"]["User-Agent"],
|
||||
)
|
||||
if "X-Forwarded-Proto" in lambda_event["headers"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_SCHEME,
|
||||
lambda_event["headers"]["X-Forwarded-Proto"],
|
||||
)
|
||||
if "Host" in lambda_event["headers"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.NET_HOST_NAME,
|
||||
lambda_event["headers"]["Host"],
|
||||
)
|
||||
if "resource" in lambda_event:
|
||||
span.set_attribute(SpanAttributes.HTTP_ROUTE, lambda_event["resource"])
|
||||
|
||||
if lambda_event.get("queryStringParameters"):
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
f"{lambda_event.get('resource')}?{urlencode(lambda_event.get('queryStringParameters'))}",
|
||||
)
|
||||
else:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET, lambda_event.get("resource")
|
||||
)
|
||||
if lambda_event.get("queryStringParameters"):
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
f"{lambda_event['resource']}?{urlencode(lambda_event['queryStringParameters'])}",
|
||||
)
|
||||
else:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET, lambda_event["resource"]
|
||||
)
|
||||
|
||||
return span
|
||||
|
||||
|
|
@ -237,35 +242,38 @@ def _set_api_gateway_v2_proxy_attributes(
|
|||
More info:
|
||||
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
|
||||
"""
|
||||
span.set_attribute(
|
||||
SpanAttributes.NET_HOST_NAME,
|
||||
lambda_event["requestContext"].get("domainName"),
|
||||
)
|
||||
if "domainName" in lambda_event["requestContext"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.NET_HOST_NAME,
|
||||
lambda_event["requestContext"]["domainName"],
|
||||
)
|
||||
|
||||
if lambda_event["requestContext"].get("http"):
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_METHOD,
|
||||
lambda_event["requestContext"]["http"].get("method"),
|
||||
)
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_USER_AGENT,
|
||||
lambda_event["requestContext"]["http"].get("userAgent"),
|
||||
)
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_ROUTE,
|
||||
lambda_event["requestContext"]["http"].get("path"),
|
||||
)
|
||||
|
||||
if lambda_event.get("rawQueryString"):
|
||||
if "method" in lambda_event["requestContext"]["http"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
f"{lambda_event['requestContext']['http'].get('path')}?{lambda_event.get('rawQueryString')}",
|
||||
SpanAttributes.HTTP_METHOD,
|
||||
lambda_event["requestContext"]["http"]["method"],
|
||||
)
|
||||
else:
|
||||
if "userAgent" in lambda_event["requestContext"]["http"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
lambda_event["requestContext"]["http"].get("path"),
|
||||
SpanAttributes.HTTP_USER_AGENT,
|
||||
lambda_event["requestContext"]["http"]["userAgent"],
|
||||
)
|
||||
if "path" in lambda_event["requestContext"]["http"]:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_ROUTE,
|
||||
lambda_event["requestContext"]["http"]["path"],
|
||||
)
|
||||
if lambda_event.get("rawQueryString"):
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
f"{lambda_event['requestContext']['http']['path']}?{lambda_event['rawQueryString']}",
|
||||
)
|
||||
else:
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
lambda_event["requestContext"]["http"]["path"],
|
||||
)
|
||||
|
||||
return span
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ API
|
|||
"""
|
||||
|
||||
import logging
|
||||
from timeit import default_timer
|
||||
from typing import Collection, Iterable
|
||||
|
||||
from celery import signals # pylint: disable=no-name-in-module
|
||||
|
|
@ -69,6 +70,7 @@ from opentelemetry.instrumentation.celery import utils
|
|||
from opentelemetry.instrumentation.celery.package import _instruments
|
||||
from opentelemetry.instrumentation.celery.version import __version__
|
||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||
from opentelemetry.metrics import get_meter
|
||||
from opentelemetry.propagate import extract, inject
|
||||
from opentelemetry.propagators.textmap import Getter
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
|
|
@ -104,6 +106,11 @@ celery_getter = CeleryGetter()
|
|||
|
||||
|
||||
class CeleryInstrumentor(BaseInstrumentor):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.metrics = None
|
||||
self.task_id_to_start_time = {}
|
||||
|
||||
def instrumentation_dependencies(self) -> Collection[str]:
|
||||
return _instruments
|
||||
|
||||
|
|
@ -113,6 +120,11 @@ class CeleryInstrumentor(BaseInstrumentor):
|
|||
# pylint: disable=attribute-defined-outside-init
|
||||
self._tracer = trace.get_tracer(__name__, __version__, tracer_provider)
|
||||
|
||||
meter_provider = kwargs.get("meter_provider")
|
||||
meter = get_meter(__name__, __version__, meter_provider)
|
||||
|
||||
self.create_celery_metrics(meter)
|
||||
|
||||
signals.task_prerun.connect(self._trace_prerun, weak=False)
|
||||
signals.task_postrun.connect(self._trace_postrun, weak=False)
|
||||
signals.before_task_publish.connect(
|
||||
|
|
@ -139,6 +151,7 @@ class CeleryInstrumentor(BaseInstrumentor):
|
|||
if task is None or task_id is None:
|
||||
return
|
||||
|
||||
self.update_task_duration_time(task_id)
|
||||
request = task.request
|
||||
tracectx = extract(request, getter=celery_getter) or None
|
||||
|
||||
|
|
@ -153,8 +166,7 @@ class CeleryInstrumentor(BaseInstrumentor):
|
|||
activation.__enter__() # pylint: disable=E1101
|
||||
utils.attach_span(task, task_id, (span, activation))
|
||||
|
||||
@staticmethod
|
||||
def _trace_postrun(*args, **kwargs):
|
||||
def _trace_postrun(self, *args, **kwargs):
|
||||
task = utils.retrieve_task(kwargs)
|
||||
task_id = utils.retrieve_task_id(kwargs)
|
||||
|
||||
|
|
@ -178,6 +190,9 @@ class CeleryInstrumentor(BaseInstrumentor):
|
|||
|
||||
activation.__exit__(None, None, None)
|
||||
utils.detach_span(task, task_id)
|
||||
self.update_task_duration_time(task_id)
|
||||
labels = {"task": task.name, "worker": task.request.hostname}
|
||||
self._record_histograms(task_id, labels)
|
||||
|
||||
def _trace_before_publish(self, *args, **kwargs):
|
||||
task = utils.retrieve_task_from_sender(kwargs)
|
||||
|
|
@ -277,3 +292,30 @@ class CeleryInstrumentor(BaseInstrumentor):
|
|||
# Use `str(reason)` instead of `reason.message` in case we get
|
||||
# something that isn't an `Exception`
|
||||
span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason))
|
||||
|
||||
def update_task_duration_time(self, task_id):
|
||||
cur_time = default_timer()
|
||||
task_duration_time_until_now = (
|
||||
cur_time - self.task_id_to_start_time[task_id]
|
||||
if task_id in self.task_id_to_start_time
|
||||
else cur_time
|
||||
)
|
||||
self.task_id_to_start_time[task_id] = task_duration_time_until_now
|
||||
|
||||
def _record_histograms(self, task_id, metric_attributes):
|
||||
if task_id is None:
|
||||
return
|
||||
|
||||
self.metrics["flower.task.runtime.seconds"].record(
|
||||
self.task_id_to_start_time.get(task_id),
|
||||
attributes=metric_attributes,
|
||||
)
|
||||
|
||||
def create_celery_metrics(self, meter) -> None:
|
||||
self.metrics = {
|
||||
"flower.task.runtime.seconds": meter.create_histogram(
|
||||
name="flower.task.runtime.seconds",
|
||||
unit="seconds",
|
||||
description="The time it took to run the task.",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import threading
|
||||
import time
|
||||
from timeit import default_timer
|
||||
|
||||
from opentelemetry.instrumentation.celery import CeleryInstrumentor
|
||||
from opentelemetry.test.test_base import TestBase
|
||||
|
||||
from .celery_test_tasks import app, task_add
|
||||
|
||||
|
||||
class TestMetrics(TestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._worker = app.Worker(
|
||||
app=app, pool="solo", concurrency=1, hostname="celery@akochavi"
|
||||
)
|
||||
self._thread = threading.Thread(target=self._worker.start)
|
||||
self._thread.daemon = True
|
||||
self._thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self._worker.stop()
|
||||
self._thread.join()
|
||||
|
||||
def get_metrics(self):
|
||||
result = task_add.delay(1, 2)
|
||||
|
||||
timeout = time.time() + 60 * 1 # 1 minutes from now
|
||||
while not result.ready():
|
||||
if time.time() > timeout:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
return self.get_sorted_metrics()
|
||||
|
||||
def test_basic_metric(self):
|
||||
CeleryInstrumentor().instrument()
|
||||
start_time = default_timer()
|
||||
task_runtime_estimated = (default_timer() - start_time) * 1000
|
||||
|
||||
metrics = self.get_metrics()
|
||||
CeleryInstrumentor().uninstrument()
|
||||
self.assertEqual(len(metrics), 1)
|
||||
|
||||
task_runtime = metrics[0]
|
||||
print(task_runtime)
|
||||
self.assertEqual(task_runtime.name, "flower.task.runtime.seconds")
|
||||
self.assert_metric_expected(
|
||||
task_runtime,
|
||||
[
|
||||
self.create_histogram_data_point(
|
||||
count=1,
|
||||
sum_data_point=task_runtime_estimated,
|
||||
max_data_point=task_runtime_estimated,
|
||||
min_data_point=task_runtime_estimated,
|
||||
attributes={
|
||||
"task": "tests.celery_test_tasks.task_add",
|
||||
"worker": "celery@akochavi",
|
||||
},
|
||||
)
|
||||
],
|
||||
est_value_delta=200,
|
||||
)
|
||||
|
||||
def test_metric_uninstrument(self):
|
||||
CeleryInstrumentor().instrument()
|
||||
metrics = self.get_metrics()
|
||||
self.assertEqual(len(metrics), 1)
|
||||
CeleryInstrumentor().uninstrument()
|
||||
|
||||
metrics = self.get_metrics()
|
||||
self.assertEqual(len(metrics), 1)
|
||||
|
||||
for metric in metrics:
|
||||
for point in list(metric.data.data_points):
|
||||
self.assertEqual(point.count, 1)
|
||||
|
|
@ -31,7 +31,7 @@ dependencies = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
instruments = [
|
||||
"confluent-kafka >= 1.8.2, < 2.0.0",
|
||||
"confluent-kafka >= 1.8.2, <= 2.2.0",
|
||||
]
|
||||
test = [
|
||||
"opentelemetry-instrumentation-confluent-kafka[instruments]",
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@
|
|||
# limitations under the License.
|
||||
|
||||
|
||||
_instruments = ("confluent-kafka >= 1.8.2, < 2.0.0",)
|
||||
_instruments = ("confluent-kafka >= 1.8.2, <= 2.2.0",)
|
||||
|
|
|
|||
|
|
@ -173,18 +173,12 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||
match = resolve(request.path)
|
||||
|
||||
if hasattr(match, "route"):
|
||||
return match.route
|
||||
return f"{request.method} {match.route}"
|
||||
|
||||
# Instead of using `view_name`, better to use `_func_name` as some applications can use similar
|
||||
# view names in different modules
|
||||
if hasattr(match, "_func_name"):
|
||||
return match._func_name # pylint: disable=protected-access
|
||||
|
||||
# Fallback for safety as `_func_name` private field
|
||||
return match.view_name
|
||||
return request.method
|
||||
|
||||
except Resolver404:
|
||||
return f"HTTP {request.method}"
|
||||
return request.method
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def process_request(self, request):
|
||||
|
|
|
|||
|
|
@ -150,9 +150,9 @@ class TestMiddleware(WsgiTestBase):
|
|||
|
||||
self.assertEqual(
|
||||
span.name,
|
||||
"^route/(?P<year>[0-9]{4})/template/$"
|
||||
"GET ^route/(?P<year>[0-9]{4})/template/$"
|
||||
if DJANGO_2_2
|
||||
else "tests.views.traced_template",
|
||||
else "GET",
|
||||
)
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
|
|
@ -177,9 +177,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(
|
||||
span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced"
|
||||
)
|
||||
self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
|
|
@ -215,9 +213,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(
|
||||
span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced"
|
||||
)
|
||||
self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "POST")
|
||||
|
|
@ -241,9 +237,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(
|
||||
span.name, "^error/" if DJANGO_2_2 else "tests.views.error"
|
||||
)
|
||||
self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
|
|
@ -307,9 +301,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
span = span_list[0]
|
||||
self.assertEqual(
|
||||
span.name,
|
||||
"^span_name/([0-9]{4})/$"
|
||||
if DJANGO_2_2
|
||||
else "tests.views.route_span_name",
|
||||
"GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET",
|
||||
)
|
||||
|
||||
def test_span_name_for_query_string(self):
|
||||
|
|
@ -323,9 +315,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
span = span_list[0]
|
||||
self.assertEqual(
|
||||
span.name,
|
||||
"^span_name/([0-9]{4})/$"
|
||||
if DJANGO_2_2
|
||||
else "tests.views.route_span_name",
|
||||
"GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET",
|
||||
)
|
||||
|
||||
def test_span_name_404(self):
|
||||
|
|
@ -334,7 +324,7 @@ class TestMiddleware(WsgiTestBase):
|
|||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
def test_traced_request_attrs(self):
|
||||
Client().get("/span_name/1234/", CONTENT_TYPE="test/ct")
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^route/(?P<year>[0-9]{4})/template/$")
|
||||
self.assertEqual(span.name, "GET ^route/(?P<year>[0-9]{4})/template/$")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
|
|
@ -160,7 +160,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^traced/")
|
||||
self.assertEqual(span.name, "GET ^traced/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
|
|
@ -195,7 +195,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^traced/")
|
||||
self.assertEqual(span.name, "POST ^traced/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "POST")
|
||||
|
|
@ -218,7 +218,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^error/")
|
||||
self.assertEqual(span.name, "GET ^error/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
|
|
@ -264,7 +264,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
|
||||
self.assertEqual(span.name, "GET ^span_name/([0-9]{4})/$")
|
||||
|
||||
async def test_span_name_for_query_string(self):
|
||||
"""
|
||||
|
|
@ -275,7 +275,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
|
||||
self.assertEqual(span.name, "GET ^span_name/([0-9]{4})/$")
|
||||
|
||||
async def test_span_name_404(self):
|
||||
await self.async_client.get("/span_name/1234567890/")
|
||||
|
|
@ -283,7 +283,7 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
async def test_traced_request_attrs(self):
|
||||
await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct")
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ from opentelemetry.instrumentation.utils import (
|
|||
from opentelemetry.metrics import get_meter
|
||||
from opentelemetry.semconv.metrics import MetricInstruments
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace.status import Status
|
||||
from opentelemetry.trace.status import Status, StatusCode
|
||||
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
|
@ -428,7 +428,6 @@ class _TraceMiddleware:
|
|||
|
||||
resource_name = resource.__class__.__name__
|
||||
span.set_attribute("falcon.resource", resource_name)
|
||||
span.update_name(f"{resource_name}.on_{req.method.lower()}")
|
||||
|
||||
def process_response(
|
||||
self, req, resp, resource, req_succeeded=None
|
||||
|
|
@ -461,11 +460,17 @@ class _TraceMiddleware:
|
|||
try:
|
||||
status_code = int(status)
|
||||
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
|
||||
otel_status_code = http_status_to_status_code(
|
||||
status_code, server_span=True
|
||||
)
|
||||
|
||||
# set the description only when the status code is ERROR
|
||||
if otel_status_code is not StatusCode.ERROR:
|
||||
reason = None
|
||||
|
||||
span.set_status(
|
||||
Status(
|
||||
status_code=http_status_to_status_code(
|
||||
status_code, server_span=True
|
||||
),
|
||||
status_code=otel_status_code,
|
||||
description=reason,
|
||||
)
|
||||
)
|
||||
|
|
@ -477,6 +482,12 @@ class _TraceMiddleware:
|
|||
response_headers = resp.headers
|
||||
|
||||
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
|
||||
# Check if low-cardinality route is available as per semantic-conventions
|
||||
if req.uri_template:
|
||||
span.update_name(f"{req.method} {req.uri_template}")
|
||||
else:
|
||||
span.update_name(f"{req.method}")
|
||||
|
||||
custom_attributes = (
|
||||
otel_wsgi.collect_custom_response_headers_attributes(
|
||||
response_headers.items()
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ class CustomResponseHeaderResource:
|
|||
resp.set_header("my-secret-header", "my-secret-value")
|
||||
|
||||
|
||||
class UserResource:
|
||||
def on_get(self, req, resp, user_id):
|
||||
# pylint: disable=no-member
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = f"Hello user {user_id}"
|
||||
|
||||
|
||||
def make_app():
|
||||
_parsed_falcon_version = package_version.parse(falcon.__version__)
|
||||
if _parsed_falcon_version < package_version.parse("3.0.0"):
|
||||
|
|
@ -76,4 +83,6 @@ def make_app():
|
|||
app.add_route(
|
||||
"/test_custom_response_headers", CustomResponseHeaderResource()
|
||||
)
|
||||
app.add_route("/user/{user_id}", UserResource())
|
||||
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class TestFalconInstrumentation(TestFalconBase, WsgiTestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
span = spans[0]
|
||||
self.assertEqual(span.name, f"HelloWorldResource.on_{method.lower()}")
|
||||
self.assertEqual(span.name, f"{method} /hello")
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(
|
||||
span.status.description,
|
||||
|
|
@ -145,7 +145,7 @@ class TestFalconInstrumentation(TestFalconBase, WsgiTestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
span = spans[0]
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertSpanHasAttributes(
|
||||
span,
|
||||
|
|
@ -177,7 +177,7 @@ class TestFalconInstrumentation(TestFalconBase, WsgiTestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
span = spans[0]
|
||||
self.assertEqual(span.name, "ErrorResource.on_get")
|
||||
self.assertEqual(span.name, "GET /error")
|
||||
self.assertFalse(span.status.is_ok)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
self.assertEqual(
|
||||
|
|
@ -206,6 +206,33 @@ class TestFalconInstrumentation(TestFalconBase, WsgiTestBase):
|
|||
span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1"
|
||||
)
|
||||
|
||||
def test_url_template(self):
|
||||
self.client().simulate_get("/user/123")
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
span = spans[0]
|
||||
self.assertEqual(span.name, "GET /user/{user_id}")
|
||||
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: "falconframework.org",
|
||||
SpanAttributes.HTTP_SCHEME: "http",
|
||||
SpanAttributes.NET_HOST_PORT: 80,
|
||||
SpanAttributes.HTTP_HOST: "falconframework.org",
|
||||
SpanAttributes.HTTP_TARGET: "/",
|
||||
SpanAttributes.NET_PEER_PORT: "65133",
|
||||
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||
"falcon.resource": "UserResource",
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
},
|
||||
)
|
||||
|
||||
def test_uninstrument(self):
|
||||
self.client().simulate_get(path="/hello")
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class FastAPIInstrumentor(BaseInstrumentor):
|
|||
app.add_middleware(
|
||||
OpenTelemetryMiddleware,
|
||||
excluded_urls=excluded_urls,
|
||||
default_span_details=_get_route_details,
|
||||
default_span_details=_get_default_span_details,
|
||||
server_request_hook=server_request_hook,
|
||||
client_request_hook=client_request_hook,
|
||||
client_response_hook=client_response_hook,
|
||||
|
|
@ -300,7 +300,7 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
|
|||
self.add_middleware(
|
||||
OpenTelemetryMiddleware,
|
||||
excluded_urls=_InstrumentedFastAPI._excluded_urls,
|
||||
default_span_details=_get_route_details,
|
||||
default_span_details=_get_default_span_details,
|
||||
server_request_hook=_InstrumentedFastAPI._server_request_hook,
|
||||
client_request_hook=_InstrumentedFastAPI._client_request_hook,
|
||||
client_response_hook=_InstrumentedFastAPI._client_response_hook,
|
||||
|
|
@ -316,15 +316,21 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
|
|||
|
||||
|
||||
def _get_route_details(scope):
|
||||
"""Callback to retrieve the fastapi route being served.
|
||||
"""
|
||||
Function to retrieve Starlette route from scope.
|
||||
|
||||
TODO: there is currently no way to retrieve http.route from
|
||||
a starlette application from scope.
|
||||
|
||||
See: https://github.com/encode/starlette/pull/804
|
||||
|
||||
Args:
|
||||
scope: A Starlette scope
|
||||
Returns:
|
||||
A string containing the route or None
|
||||
"""
|
||||
app = scope["app"]
|
||||
route = None
|
||||
|
||||
for starlette_route in app.routes:
|
||||
match, _ = starlette_route.matches(scope)
|
||||
if match == Match.FULL:
|
||||
|
|
@ -332,10 +338,27 @@ def _get_route_details(scope):
|
|||
break
|
||||
if match == Match.PARTIAL:
|
||||
route = starlette_route.path
|
||||
# method only exists for http, if websocket
|
||||
# leave it blank.
|
||||
span_name = route or scope.get("method", "")
|
||||
return route
|
||||
|
||||
|
||||
def _get_default_span_details(scope):
|
||||
"""
|
||||
Callback to retrieve span name and attributes from scope.
|
||||
|
||||
Args:
|
||||
scope: A Starlette scope
|
||||
Returns:
|
||||
A tuple of span name and attributes
|
||||
"""
|
||||
route = _get_route_details(scope)
|
||||
method = scope.get("method", "")
|
||||
attributes = {}
|
||||
if route:
|
||||
attributes[SpanAttributes.HTTP_ROUTE] = route
|
||||
if method and route: # http
|
||||
span_name = f"{method} {route}"
|
||||
elif route: # websocket
|
||||
span_name = route
|
||||
else: # fallback
|
||||
span_name = method
|
||||
return span_name, attributes
|
||||
|
|
|
|||
|
|
@ -44,10 +44,15 @@ from opentelemetry.util.http import (
|
|||
_expected_metric_names = [
|
||||
"http.server.active_requests",
|
||||
"http.server.duration",
|
||||
"http.server.response.size",
|
||||
]
|
||||
_recommended_attrs = {
|
||||
"http.server.active_requests": _active_requests_count_attrs,
|
||||
"http.server.duration": {*_duration_attrs, SpanAttributes.HTTP_TARGET},
|
||||
"http.server.response.size": {
|
||||
*_duration_attrs,
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -106,7 +111,7 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 3)
|
||||
for span in spans:
|
||||
self.assertIn("/foobar", span.name)
|
||||
self.assertIn("GET /foobar", span.name)
|
||||
|
||||
def test_uninstrument_app(self):
|
||||
self._client.get("/foobar")
|
||||
|
|
@ -138,7 +143,7 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 3)
|
||||
for span in spans:
|
||||
self.assertIn("/foobar", span.name)
|
||||
self.assertIn("GET /foobar", span.name)
|
||||
|
||||
def test_fastapi_route_attribute_added(self):
|
||||
"""Ensure that fastapi routes are used as the span name."""
|
||||
|
|
@ -146,7 +151,7 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 3)
|
||||
for span in spans:
|
||||
self.assertIn("/user/{username}", span.name)
|
||||
self.assertIn("GET /user/{username}", span.name)
|
||||
self.assertEqual(
|
||||
spans[-1].attributes[SpanAttributes.HTTP_ROUTE], "/user/{username}"
|
||||
)
|
||||
|
|
@ -187,7 +192,7 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||
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)
|
||||
self.assertTrue(len(scope_metric.metrics) == 3)
|
||||
for metric in scope_metric.metrics:
|
||||
self.assertIn(metric.name, _expected_metric_names)
|
||||
data_points = list(metric.data.data_points)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ dependencies = [
|
|||
"opentelemetry-instrumentation-wsgi == 0.40b0.dev",
|
||||
"opentelemetry-semantic-conventions == 0.40b0.dev",
|
||||
"opentelemetry-util-http == 0.40b0.dev",
|
||||
"packaging >= 21.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
@ -38,7 +39,7 @@ instruments = [
|
|||
]
|
||||
test = [
|
||||
"opentelemetry-instrumentation-flask[instruments]",
|
||||
"markupsafe==2.0.1",
|
||||
"markupsafe==2.1.2",
|
||||
"opentelemetry-test-utils == 0.40b0.dev",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -238,13 +238,14 @@ Note:
|
|||
API
|
||||
---
|
||||
"""
|
||||
import weakref
|
||||
from logging import getLogger
|
||||
from threading import get_ident
|
||||
from time import time_ns
|
||||
from timeit import default_timer
|
||||
from typing import Collection
|
||||
|
||||
import flask
|
||||
from packaging import version as package_version
|
||||
|
||||
import opentelemetry.instrumentation.wsgi as otel_wsgi
|
||||
from opentelemetry import context, trace
|
||||
|
|
@ -265,11 +266,21 @@ _logger = getLogger(__name__)
|
|||
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
|
||||
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
|
||||
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
|
||||
_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key"
|
||||
_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
|
||||
_ENVIRON_TOKEN = "opentelemetry-flask.token"
|
||||
|
||||
_excluded_urls_from_env = get_excluded_urls("FLASK")
|
||||
|
||||
if package_version.parse(flask.__version__) >= package_version.parse("2.2.0"):
|
||||
|
||||
def _request_ctx_ref() -> weakref.ReferenceType:
|
||||
return weakref.ref(flask.globals.request_ctx._get_current_object())
|
||||
|
||||
else:
|
||||
|
||||
def _request_ctx_ref() -> weakref.ReferenceType:
|
||||
return weakref.ref(flask._request_ctx_stack.top)
|
||||
|
||||
|
||||
def get_default_span_name():
|
||||
try:
|
||||
|
|
@ -364,27 +375,26 @@ def _wrapped_before_request(
|
|||
flask_request_environ = flask.request.environ
|
||||
span_name = get_default_span_name()
|
||||
|
||||
attributes = otel_wsgi.collect_request_attributes(
|
||||
flask_request_environ
|
||||
)
|
||||
if flask.request.url_rule:
|
||||
# For 404 that result from no route found, etc, we
|
||||
# don't have a url_rule.
|
||||
attributes[SpanAttributes.HTTP_ROUTE] = flask.request.url_rule.rule
|
||||
span, token = _start_internal_or_server_span(
|
||||
tracer=tracer,
|
||||
span_name=span_name,
|
||||
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
|
||||
context_carrier=flask_request_environ,
|
||||
context_getter=otel_wsgi.wsgi_getter,
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
if request_hook:
|
||||
request_hook(span, flask_request_environ)
|
||||
|
||||
if span.is_recording():
|
||||
attributes = otel_wsgi.collect_request_attributes(
|
||||
flask_request_environ
|
||||
)
|
||||
if flask.request.url_rule:
|
||||
# For 404 that result from no route found, etc, we
|
||||
# don't have a url_rule.
|
||||
attributes[
|
||||
SpanAttributes.HTTP_ROUTE
|
||||
] = flask.request.url_rule.rule
|
||||
for key, value in attributes.items():
|
||||
span.set_attribute(key, value)
|
||||
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
|
||||
|
|
@ -399,7 +409,7 @@ def _wrapped_before_request(
|
|||
activation = trace.use_span(span, end_on_exit=True)
|
||||
activation.__enter__() # pylint: disable=E1101
|
||||
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
|
||||
flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident()
|
||||
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
|
||||
flask_request_environ[_ENVIRON_SPAN_KEY] = span
|
||||
flask_request_environ[_ENVIRON_TOKEN] = token
|
||||
|
||||
|
|
@ -439,17 +449,22 @@ def _wrapped_teardown_request(
|
|||
return
|
||||
|
||||
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
|
||||
thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY)
|
||||
if not activation or thread_id != get_ident():
|
||||
|
||||
original_reqctx_ref = flask.request.environ.get(
|
||||
_ENVIRON_REQCTX_REF_KEY
|
||||
)
|
||||
current_reqctx_ref = _request_ctx_ref()
|
||||
if not activation or original_reqctx_ref != current_reqctx_ref:
|
||||
# This request didn't start a span, maybe because it was created in
|
||||
# a way that doesn't run `before_request`, like when it is created
|
||||
# with `app.test_request_context`.
|
||||
#
|
||||
# Similarly, check the thread_id against the current thread to ensure
|
||||
# tear down only happens on the original thread. This situation can
|
||||
# arise if the original thread handling the request spawn children
|
||||
# threads and then uses something like copy_current_request_context
|
||||
# to copy the request context.
|
||||
# Similarly, check that the request_ctx that created the span
|
||||
# matches the current request_ctx, and only tear down if they match.
|
||||
# This situation can arise if the original request_ctx handling
|
||||
# the request calls functions that push new request_ctx's,
|
||||
# like any decorated with `flask.copy_current_request_context`.
|
||||
|
||||
return
|
||||
if exc is None:
|
||||
activation.__exit__(None, None, None)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import flask
|
|||
from werkzeug.test import Client
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from opentelemetry import context
|
||||
from opentelemetry import context, trace
|
||||
|
||||
|
||||
class InstrumentationTest:
|
||||
|
|
@ -37,6 +37,21 @@ class InstrumentationTest:
|
|||
)
|
||||
return sqlcommenter_flask_values
|
||||
|
||||
@staticmethod
|
||||
def _copy_context_endpoint():
|
||||
@flask.copy_current_request_context
|
||||
def _extract_header():
|
||||
return flask.request.headers["x-req"]
|
||||
|
||||
# Despite `_extract_header` copying the request context,
|
||||
# calling it shouldn't detach the parent Flask span's contextvar
|
||||
request_header = _extract_header()
|
||||
|
||||
return {
|
||||
"span_name": trace.get_current_span().name,
|
||||
"request_header": request_header,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _multithreaded_endpoint(count):
|
||||
def do_random_stuff():
|
||||
|
|
@ -84,6 +99,7 @@ class InstrumentationTest:
|
|||
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
|
||||
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
|
||||
self.app.route("/multithreaded")(self._multithreaded_endpoint)
|
||||
self.app.route("/copy_context")(self._copy_context_endpoint)
|
||||
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
|
||||
self.app.route("/excluded")(excluded_endpoint)
|
||||
self.app.route("/excluded2")(excluded2_endpoint)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
# 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.
|
||||
import flask
|
||||
from werkzeug.test import Client
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
||||
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
||||
|
||||
from .base_test import InstrumentationTest
|
||||
|
||||
|
||||
class TestCopyContext(InstrumentationTest, WsgiTestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
FlaskInstrumentor().instrument()
|
||||
self.app = flask.Flask(__name__)
|
||||
self._common_initialization()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
with self.disable_logging():
|
||||
FlaskInstrumentor().uninstrument()
|
||||
|
||||
def test_copycontext(self):
|
||||
"""Test that instrumentation tear down does not blow up
|
||||
when the request calls functions where the context has been
|
||||
copied via `flask.copy_current_request_context`
|
||||
"""
|
||||
self.app = flask.Flask(__name__)
|
||||
self.app.route("/copy_context")(self._copy_context_endpoint)
|
||||
client = Client(self.app, Response)
|
||||
resp = client.get("/copy_context", headers={"x-req": "a-header"})
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual("/copy_context", resp.json["span_name"])
|
||||
self.assertEqual("a-header", resp.json["request_header"])
|
||||
|
|
@ -214,7 +214,7 @@ class TestProgrammatic(InstrumentationTest, WsgiTestBase):
|
|||
resp.close()
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
self.assertEqual(span_list[0].name, "HTTP POST")
|
||||
self.assertEqual(span_list[0].name, "POST /bye")
|
||||
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
|
||||
self.assertEqual(span_list[0].attributes, expected_attrs)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ When using the instrumentor, all clients will automatically trace requests.
|
|||
import httpx
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
HTTPXClientInstrumentor().instrument()
|
||||
|
||||
with httpx.Client() as client:
|
||||
|
|
@ -51,7 +51,7 @@ use the `instrument_client` method.
|
|||
import httpx
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
|
||||
with httpx.Client(transport=telemetry_transport) as client:
|
||||
HTTPXClientInstrumentor.instrument_client(client)
|
||||
|
|
@ -96,7 +96,7 @@ If you don't want to use the instrumentor class, you can use the transport class
|
|||
SyncOpenTelemetryTransport,
|
||||
)
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
transport = httpx.HTTPTransport()
|
||||
telemetry_transport = SyncOpenTelemetryTransport(transport)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ dependencies = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
instruments = [
|
||||
"httpx >= 0.18.0, <= 0.23.0",
|
||||
"httpx >= 0.18.0",
|
||||
]
|
||||
test = [
|
||||
"opentelemetry-instrumentation-httpx[instruments]",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ When using the instrumentor, all clients will automatically trace requests.
|
|||
import httpx
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
HTTPXClientInstrumentor().instrument()
|
||||
|
||||
with httpx.Client() as client:
|
||||
|
|
@ -46,7 +46,7 @@ use the `instrument_client` method.
|
|||
import httpx
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
|
||||
with httpx.Client(transport=telemetry_transport) as client:
|
||||
HTTPXClientInstrumentor.instrument_client(client)
|
||||
|
|
@ -91,7 +91,7 @@ If you don't want to use the instrumentor class, you can use the transport class
|
|||
SyncOpenTelemetryTransport,
|
||||
)
|
||||
|
||||
url = "https://httpbin.org/get"
|
||||
url = "https://some.url/get"
|
||||
transport = httpx.HTTPTransport()
|
||||
telemetry_transport = SyncOpenTelemetryTransport(transport)
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ class ResponseInfo(typing.NamedTuple):
|
|||
|
||||
|
||||
def _get_default_span_name(method: str) -> str:
|
||||
return f"HTTP {method.strip()}"
|
||||
return method.strip()
|
||||
|
||||
|
||||
def _apply_status_code(span: Span, status_code: int) -> None:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ def _async_call(coro: typing.Coroutine) -> asyncio.Task:
|
|||
def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"):
|
||||
span.set_attribute(
|
||||
HTTP_RESPONSE_BODY,
|
||||
response[2].read(),
|
||||
b"".join(response[2]),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ async def _async_response_hook(
|
|||
):
|
||||
span.set_attribute(
|
||||
HTTP_RESPONSE_BODY,
|
||||
await response[2].aread(),
|
||||
b"".join([part async for part in response[2]]),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ class BaseTestCases:
|
|||
class BaseTest(TestBase, metaclass=abc.ABCMeta):
|
||||
# pylint: disable=no-member
|
||||
|
||||
URL = "http://httpbin.org/status/200"
|
||||
URL = "http://mock/status/200"
|
||||
response_hook = staticmethod(_response_hook)
|
||||
request_hook = staticmethod(_request_hook)
|
||||
no_update_request_hook = staticmethod(_no_update_request_hook)
|
||||
|
|
@ -142,7 +142,7 @@ class BaseTestCases:
|
|||
span = self.assert_span()
|
||||
|
||||
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
|
|
@ -165,7 +165,7 @@ class BaseTestCases:
|
|||
self.assert_span(num_spans=2)
|
||||
|
||||
def test_not_foundbasic(self):
|
||||
url_404 = "http://httpbin.org/status/404"
|
||||
url_404 = "http://mock/status/404"
|
||||
|
||||
with respx.mock:
|
||||
respx.get(url_404).mock(httpx.Response(404))
|
||||
|
|
@ -258,7 +258,7 @@ class BaseTestCases:
|
|||
|
||||
span = self.assert_span()
|
||||
|
||||
self.assertEqual(span.name, "HTTP POST")
|
||||
self.assertEqual(span.name, "POST")
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_METHOD], "POST"
|
||||
)
|
||||
|
|
@ -350,7 +350,7 @@ class BaseTestCases:
|
|||
|
||||
self.assertEqual(result.text, "Hello!")
|
||||
span = self.assert_span()
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
def test_not_recording(self):
|
||||
with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span:
|
||||
|
|
@ -444,7 +444,7 @@ class BaseTestCases:
|
|||
|
||||
self.assertEqual(result.text, "Hello!")
|
||||
span = self.assert_span()
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
HTTPXClientInstrumentor().uninstrument()
|
||||
|
||||
def test_not_recording(self):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@
|
|||
import pyramid.httpexceptions as exc
|
||||
from pyramid.response import Response
|
||||
from werkzeug.test import Client
|
||||
from werkzeug.wrappers import BaseResponse
|
||||
|
||||
# opentelemetry-instrumentation-pyramid uses werkzeug==0.16.1 which has
|
||||
# werkzeug.wrappers.BaseResponse. This is not the case for newer versions of
|
||||
# werkzeug like the one lint uses.
|
||||
from werkzeug.wrappers import BaseResponse # pylint: disable=no-name-in-module
|
||||
|
||||
|
||||
class InstrumentationTest:
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class TestProgrammatic(InstrumentationTest, WsgiTestBase):
|
|||
resp.close()
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
self.assertEqual(span_list[0].name, "HTTP POST")
|
||||
self.assertEqual(span_list[0].name, "POST /bye")
|
||||
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
|
||||
self.assertEqual(span_list[0].attributes, expected_attrs)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
Instrument `redis`_ to report Redis queries.
|
||||
|
||||
There are two options for instrumenting code. The first option is to use the
|
||||
``opentelemetry-instrumentation`` executable which will automatically
|
||||
``opentelemetry-instrument`` executable which will automatically
|
||||
instrument your Redis client. The second is to programmatically enable
|
||||
instrumentation via the following code:
|
||||
|
||||
|
|
@ -64,8 +64,6 @@ this function signature is: def request_hook(span: Span, instance: redis.connec
|
|||
response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
|
||||
this function signature is: def response_hook(span: Span, instance: redis.connection.Connection, response) -> None
|
||||
|
||||
sanitize_query (Boolean) - default False, enable the Redis query sanitization
|
||||
|
||||
for example:
|
||||
|
||||
.. code: python
|
||||
|
|
@ -88,27 +86,11 @@ for example:
|
|||
client = redis.StrictRedis(host="localhost", port=6379)
|
||||
client.get("my-key")
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Query sanitization
|
||||
******************
|
||||
To enable query sanitization with an environment variable, set
|
||||
``OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS`` to "true".
|
||||
|
||||
For example,
|
||||
|
||||
::
|
||||
|
||||
export OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS="true"
|
||||
|
||||
will result in traced queries like "SET ? ?".
|
||||
|
||||
API
|
||||
---
|
||||
"""
|
||||
import typing
|
||||
from os import environ
|
||||
from typing import Any, Collection
|
||||
|
||||
import redis
|
||||
|
|
@ -116,9 +98,6 @@ from wrapt import wrap_function_wrapper
|
|||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||
from opentelemetry.instrumentation.redis.environment_variables import (
|
||||
OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS,
|
||||
)
|
||||
from opentelemetry.instrumentation.redis.package import _instruments
|
||||
from opentelemetry.instrumentation.redis.util import (
|
||||
_extract_conn_attributes,
|
||||
|
|
@ -161,10 +140,9 @@ def _instrument(
|
|||
tracer,
|
||||
request_hook: _RequestHookT = None,
|
||||
response_hook: _ResponseHookT = None,
|
||||
sanitize_query: bool = False,
|
||||
):
|
||||
def _traced_execute_command(func, instance, args, kwargs):
|
||||
query = _format_command_args(args, sanitize_query)
|
||||
query = _format_command_args(args)
|
||||
|
||||
if len(args) > 0 and args[0]:
|
||||
name = args[0]
|
||||
|
|
@ -194,7 +172,7 @@ def _instrument(
|
|||
|
||||
cmds = [
|
||||
_format_command_args(
|
||||
c.args if hasattr(c, "args") else c[0], sanitize_query
|
||||
c.args if hasattr(c, "args") else c[0],
|
||||
)
|
||||
for c in command_stack
|
||||
]
|
||||
|
|
@ -307,15 +285,6 @@ class RedisInstrumentor(BaseInstrumentor):
|
|||
tracer,
|
||||
request_hook=kwargs.get("request_hook"),
|
||||
response_hook=kwargs.get("response_hook"),
|
||||
sanitize_query=kwargs.get(
|
||||
"sanitize_query",
|
||||
environ.get(
|
||||
OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS, "false"
|
||||
)
|
||||
.lower()
|
||||
.strip()
|
||||
== "true",
|
||||
),
|
||||
)
|
||||
|
||||
def _uninstrument(self, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
# 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.
|
||||
|
||||
OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS = (
|
||||
"OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS"
|
||||
)
|
||||
|
|
@ -48,41 +48,23 @@ def _extract_conn_attributes(conn_kwargs):
|
|||
return attributes
|
||||
|
||||
|
||||
def _format_command_args(args, sanitize_query):
|
||||
def _format_command_args(args):
|
||||
"""Format and sanitize command arguments, and trim them as needed"""
|
||||
cmd_max_len = 1000
|
||||
value_too_long_mark = "..."
|
||||
if sanitize_query:
|
||||
# Sanitized query format: "COMMAND ? ?"
|
||||
args_length = len(args)
|
||||
if args_length > 0:
|
||||
out = [str(args[0])] + ["?"] * (args_length - 1)
|
||||
out_str = " ".join(out)
|
||||
|
||||
if len(out_str) > cmd_max_len:
|
||||
out_str = (
|
||||
out_str[: cmd_max_len - len(value_too_long_mark)]
|
||||
+ value_too_long_mark
|
||||
)
|
||||
else:
|
||||
out_str = ""
|
||||
return out_str
|
||||
# Sanitized query format: "COMMAND ? ?"
|
||||
args_length = len(args)
|
||||
if args_length > 0:
|
||||
out = [str(args[0])] + ["?"] * (args_length - 1)
|
||||
out_str = " ".join(out)
|
||||
|
||||
value_max_len = 100
|
||||
length = 0
|
||||
out = []
|
||||
for arg in args:
|
||||
cmd = str(arg)
|
||||
if len(out_str) > cmd_max_len:
|
||||
out_str = (
|
||||
out_str[: cmd_max_len - len(value_too_long_mark)]
|
||||
+ value_too_long_mark
|
||||
)
|
||||
else:
|
||||
out_str = ""
|
||||
|
||||
if len(cmd) > value_max_len:
|
||||
cmd = cmd[:value_max_len] + value_too_long_mark
|
||||
|
||||
if length + len(cmd) > cmd_max_len:
|
||||
prefix = cmd[: cmd_max_len - length]
|
||||
out.append(f"{prefix}{value_too_long_mark}")
|
||||
break
|
||||
|
||||
out.append(cmd)
|
||||
length += len(cmd)
|
||||
|
||||
return " ".join(out)
|
||||
return out_str
|
||||
|
|
|
|||
|
|
@ -168,22 +168,11 @@ class TestRedis(TestBase):
|
|||
span = spans[0]
|
||||
self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
|
||||
|
||||
def test_query_sanitizer_enabled_env(self):
|
||||
def test_query_sanitizer(self):
|
||||
redis_client = redis.Redis()
|
||||
connection = redis.connection.Connection()
|
||||
redis_client.connection = connection
|
||||
|
||||
RedisInstrumentor().uninstrument()
|
||||
|
||||
env_patch = mock.patch.dict(
|
||||
"os.environ",
|
||||
{"OTEL_PYTHON_INSTRUMENTATION_SANITIZE_REDIS": "true"},
|
||||
)
|
||||
env_patch.start()
|
||||
RedisInstrumentor().instrument(
|
||||
tracer_provider=self.tracer_provider,
|
||||
)
|
||||
|
||||
with mock.patch.object(redis_client, "connection"):
|
||||
redis_client.set("key", "value")
|
||||
|
||||
|
|
@ -192,21 +181,6 @@ class TestRedis(TestBase):
|
|||
|
||||
span = spans[0]
|
||||
self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
|
||||
env_patch.stop()
|
||||
|
||||
def test_query_sanitizer_disabled(self):
|
||||
redis_client = redis.Redis()
|
||||
connection = redis.connection.Connection()
|
||||
redis_client.connection = connection
|
||||
|
||||
with mock.patch.object(redis_client, "connection"):
|
||||
redis_client.set("key", "value")
|
||||
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
self.assertEqual(span.attributes.get("db.statement"), "SET key value")
|
||||
|
||||
def test_no_op_tracer_provider(self):
|
||||
RedisInstrumentor().uninstrument()
|
||||
|
|
|
|||
|
|
@ -245,8 +245,16 @@ def _uninstrument_from(instr_root, restore_as_bound_func=False):
|
|||
|
||||
|
||||
def get_default_span_name(method):
|
||||
"""Default implementation for name_callback, returns HTTP {method_name}."""
|
||||
return f"HTTP {method.strip()}"
|
||||
"""
|
||||
Default implementation for name_callback, returns HTTP {method_name}.
|
||||
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
|
||||
|
||||
Args:
|
||||
method: string representing HTTP method
|
||||
Returns:
|
||||
span name
|
||||
"""
|
||||
return method.strip()
|
||||
|
||||
|
||||
class RequestsInstrumentor(BaseInstrumentor):
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
# pylint: disable=no-member
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
URL = "http://httpbin.org/status/200"
|
||||
URL = "http://mock/status/200"
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
|
|
@ -116,7 +116,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
span = self.assert_span()
|
||||
|
||||
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
|
|
@ -152,7 +152,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
self.assertEqual(span.attributes["response_hook_attr"], "value")
|
||||
|
||||
def test_excluded_urls_explicit(self):
|
||||
url_404 = "http://httpbin.org/status/404"
|
||||
url_404 = "http://mock/status/404"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_404,
|
||||
|
|
@ -191,10 +191,10 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
self.assertEqual(result.text, "Hello!")
|
||||
span = self.assert_span()
|
||||
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
def test_not_foundbasic(self):
|
||||
url_404 = "http://httpbin.org/status/404"
|
||||
url_404 = "http://mock/status/404"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_404,
|
||||
|
|
@ -460,7 +460,7 @@ class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase):
|
|||
return session.get(url)
|
||||
|
||||
def test_credential_removal(self):
|
||||
new_url = "http://username:password@httpbin.org/status/200"
|
||||
new_url = "http://username:password@mock/status/200"
|
||||
self.perform_request(new_url)
|
||||
span = self.assert_span()
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase):
|
|||
|
||||
span = self.assert_span()
|
||||
self.assertIs(trace.SpanKind.CLIENT, span.kind)
|
||||
self.assertEqual("HTTP GET", span.name)
|
||||
self.assertEqual("GET", span.name)
|
||||
|
||||
attributes = {
|
||||
"http.status_code": 200,
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class StarletteInstrumentor(BaseInstrumentor):
|
|||
app.add_middleware(
|
||||
OpenTelemetryMiddleware,
|
||||
excluded_urls=_excluded_urls,
|
||||
default_span_details=_get_route_details,
|
||||
default_span_details=_get_default_span_details,
|
||||
server_request_hook=server_request_hook,
|
||||
client_request_hook=client_request_hook,
|
||||
client_response_hook=client_response_hook,
|
||||
|
|
@ -278,7 +278,7 @@ class _InstrumentedStarlette(applications.Starlette):
|
|||
self.add_middleware(
|
||||
OpenTelemetryMiddleware,
|
||||
excluded_urls=_excluded_urls,
|
||||
default_span_details=_get_route_details,
|
||||
default_span_details=_get_default_span_details,
|
||||
server_request_hook=_InstrumentedStarlette._server_request_hook,
|
||||
client_request_hook=_InstrumentedStarlette._client_request_hook,
|
||||
client_response_hook=_InstrumentedStarlette._client_response_hook,
|
||||
|
|
@ -294,15 +294,21 @@ class _InstrumentedStarlette(applications.Starlette):
|
|||
|
||||
|
||||
def _get_route_details(scope):
|
||||
"""Callback to retrieve the starlette route being served.
|
||||
"""
|
||||
Function to retrieve Starlette route from scope.
|
||||
|
||||
TODO: there is currently no way to retrieve http.route from
|
||||
a starlette application from scope.
|
||||
|
||||
See: https://github.com/encode/starlette/pull/804
|
||||
|
||||
Args:
|
||||
scope: A Starlette scope
|
||||
Returns:
|
||||
A string containing the route or None
|
||||
"""
|
||||
app = scope["app"]
|
||||
route = None
|
||||
|
||||
for starlette_route in app.routes:
|
||||
match, _ = starlette_route.matches(scope)
|
||||
if match == Match.FULL:
|
||||
|
|
@ -310,10 +316,27 @@ def _get_route_details(scope):
|
|||
break
|
||||
if match == Match.PARTIAL:
|
||||
route = starlette_route.path
|
||||
# method only exists for http, if websocket
|
||||
# leave it blank.
|
||||
span_name = route or scope.get("method", "")
|
||||
return route
|
||||
|
||||
|
||||
def _get_default_span_details(scope):
|
||||
"""
|
||||
Callback to retrieve span name and attributes from scope.
|
||||
|
||||
Args:
|
||||
scope: A Starlette scope
|
||||
Returns:
|
||||
A tuple of span name and attributes
|
||||
"""
|
||||
route = _get_route_details(scope)
|
||||
method = scope.get("method", "")
|
||||
attributes = {}
|
||||
if route:
|
||||
attributes[SpanAttributes.HTTP_ROUTE] = route
|
||||
if method and route: # http
|
||||
span_name = f"{method} {route}"
|
||||
elif route: # websocket
|
||||
span_name = route
|
||||
else: # fallback
|
||||
span_name = method
|
||||
return span_name, attributes
|
||||
|
|
|
|||
|
|
@ -49,10 +49,12 @@ from opentelemetry.util.http import (
|
|||
_expected_metric_names = [
|
||||
"http.server.active_requests",
|
||||
"http.server.duration",
|
||||
"http.server.response.size",
|
||||
]
|
||||
_recommended_attrs = {
|
||||
"http.server.active_requests": _active_requests_count_attrs,
|
||||
"http.server.duration": _duration_attrs,
|
||||
"http.server.response.size": _duration_attrs,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ class TestStarletteManualInstrumentation(TestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 3)
|
||||
for span in spans:
|
||||
self.assertIn("/foobar", span.name)
|
||||
self.assertIn("GET /foobar", span.name)
|
||||
|
||||
def test_starlette_route_attribute_added(self):
|
||||
"""Ensure that starlette routes are used as the span name."""
|
||||
|
|
@ -101,7 +103,7 @@ class TestStarletteManualInstrumentation(TestBase):
|
|||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 3)
|
||||
for span in spans:
|
||||
self.assertIn("/user/{username}", span.name)
|
||||
self.assertIn("GET /user/{username}", span.name)
|
||||
self.assertEqual(
|
||||
spans[-1].attributes[SpanAttributes.HTTP_ROUTE], "/user/{username}"
|
||||
)
|
||||
|
|
@ -128,7 +130,7 @@ class TestStarletteManualInstrumentation(TestBase):
|
|||
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)
|
||||
self.assertTrue(len(scope_metric.metrics) == 3)
|
||||
for metric in scope_metric.metrics:
|
||||
self.assertIn(metric.name, _expected_metric_names)
|
||||
data_points = list(metric.data.data_points)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ instruments = [
|
|||
test = [
|
||||
"opentelemetry-instrumentation-tornado[instruments]",
|
||||
"opentelemetry-test-utils == 0.40b0.dev",
|
||||
"http-server-mock"
|
||||
]
|
||||
|
||||
[project.entry-points.opentelemetry_instrumentor]
|
||||
|
|
|
|||
|
|
@ -454,10 +454,23 @@ def _get_attributes_from_request(request):
|
|||
)
|
||||
|
||||
|
||||
def _get_operation_name(handler, request):
|
||||
full_class_name = type(handler).__name__
|
||||
class_name = full_class_name.rsplit(".")[-1]
|
||||
return f"{class_name}.{request.method.lower()}"
|
||||
def _get_default_span_name(request):
|
||||
"""
|
||||
Default span name is the HTTP method and URL path, or just the method.
|
||||
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
|
||||
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
|
||||
|
||||
Args:
|
||||
request: Tornado request object.
|
||||
Returns:
|
||||
Default span name.
|
||||
"""
|
||||
|
||||
path = request.path
|
||||
method = request.method
|
||||
if method and path:
|
||||
return f"{method} {path}"
|
||||
return f"{method}"
|
||||
|
||||
|
||||
def _get_full_handler_name(handler):
|
||||
|
|
@ -468,7 +481,7 @@ def _get_full_handler_name(handler):
|
|||
def _start_span(tracer, handler) -> _TraceContext:
|
||||
span, token = _start_internal_or_server_span(
|
||||
tracer=tracer,
|
||||
span_name=_get_operation_name(handler, handler.request),
|
||||
span_name=_get_default_span_name(handler.request),
|
||||
start_time=time_ns(),
|
||||
context_carrier=handler.request.headers,
|
||||
context_getter=textmap.default_getter,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from http_server_mock import HttpServerMock
|
||||
from tornado.testing import AsyncHTTPTestCase
|
||||
|
||||
from opentelemetry import trace
|
||||
|
|
@ -135,7 +136,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(manual.parent, server.context)
|
||||
self.assertEqual(manual.context.trace_id, client.context.trace_id)
|
||||
|
||||
self.assertEqual(server.name, "MainHandler." + method.lower())
|
||||
self.assertEqual(server.name, f"{method} /")
|
||||
self.assertTrue(server.parent.is_remote)
|
||||
self.assertNotEqual(server.parent, client.context)
|
||||
self.assertEqual(server.parent.span_id, client.context.span_id)
|
||||
|
|
@ -196,7 +197,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 5)
|
||||
|
||||
client = spans.by_name("GET")
|
||||
server = spans.by_name(handler_name + ".get")
|
||||
server = spans.by_name(f"GET {url}")
|
||||
sub_wrapper = spans.by_name("sub-task-wrapper")
|
||||
|
||||
sub2 = spans.by_name("sub-task-2")
|
||||
|
|
@ -213,7 +214,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(sub_wrapper.parent, server.context)
|
||||
self.assertEqual(sub_wrapper.context.trace_id, client.context.trace_id)
|
||||
|
||||
self.assertEqual(server.name, handler_name + ".get")
|
||||
self.assertEqual(server.name, f"GET {url}")
|
||||
self.assertTrue(server.parent.is_remote)
|
||||
self.assertNotEqual(server.parent, client.context)
|
||||
self.assertEqual(server.parent.span_id, client.context.span_id)
|
||||
|
|
@ -229,6 +230,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
SpanAttributes.HTTP_TARGET: url,
|
||||
SpanAttributes.HTTP_CLIENT_IP: "127.0.0.1",
|
||||
SpanAttributes.HTTP_STATUS_CODE: 201,
|
||||
"tornado.handler": f"tests.tornado_test_app.{handler_name}",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -253,9 +255,9 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 2)
|
||||
|
||||
client = spans.by_name("GET")
|
||||
server = spans.by_name("BadHandler.get")
|
||||
server = spans.by_name("GET /error")
|
||||
|
||||
self.assertEqual(server.name, "BadHandler.get")
|
||||
self.assertEqual(server.name, "GET /error")
|
||||
self.assertEqual(server.kind, SpanKind.SERVER)
|
||||
self.assertSpanHasAttributes(
|
||||
server,
|
||||
|
|
@ -290,7 +292,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 2)
|
||||
server, client = spans
|
||||
|
||||
self.assertEqual(server.name, "ErrorHandler.get")
|
||||
self.assertEqual(server.name, "GET /missing-url")
|
||||
self.assertEqual(server.kind, SpanKind.SERVER)
|
||||
self.assertSpanHasAttributes(
|
||||
server,
|
||||
|
|
@ -325,7 +327,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 2)
|
||||
server, client = spans
|
||||
|
||||
self.assertEqual(server.name, "RaiseHTTPErrorHandler.get")
|
||||
self.assertEqual(server.name, "GET /raise_403")
|
||||
self.assertEqual(server.kind, SpanKind.SERVER)
|
||||
self.assertSpanHasAttributes(
|
||||
server,
|
||||
|
|
@ -366,7 +368,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 2)
|
||||
server, client = spans
|
||||
|
||||
self.assertEqual(server.name, "DynamicHandler.get")
|
||||
self.assertEqual(server.name, "GET /dyna")
|
||||
self.assertTrue(server.parent.is_remote)
|
||||
self.assertNotEqual(server.parent, client.context)
|
||||
self.assertEqual(server.parent.span_id, client.context.span_id)
|
||||
|
|
@ -407,7 +409,7 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.assertEqual(len(spans), 3)
|
||||
auditor, server, client = spans
|
||||
|
||||
self.assertEqual(server.name, "FinishedHandler.get")
|
||||
self.assertEqual(server.name, "GET /on_finish")
|
||||
self.assertTrue(server.parent.is_remote)
|
||||
self.assertNotEqual(server.parent, client.context)
|
||||
self.assertEqual(server.parent.span_id, client.context.span_id)
|
||||
|
|
@ -494,32 +496,35 @@ class TestTornadoInstrumentation(TornadoTest, WsgiTestBase):
|
|||
self.memory_exporter.clear()
|
||||
set_global_response_propagator(orig)
|
||||
|
||||
# todo(srikanthccv): fix this test
|
||||
# this test is making request to real httpbin.org/status/200 which
|
||||
# is not a good idea as it can fail due to availability of the
|
||||
# service.
|
||||
# def test_credential_removal(self):
|
||||
# response = self.fetch(
|
||||
# "http://username:password@httpbin.org/status/200"
|
||||
# )
|
||||
# self.assertEqual(response.code, 200)
|
||||
def test_credential_removal(self):
|
||||
app = HttpServerMock("test_credential_removal")
|
||||
|
||||
# spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
|
||||
# self.assertEqual(len(spans), 1)
|
||||
# client = spans[0]
|
||||
@app.route("/status/200")
|
||||
def index():
|
||||
return "hello"
|
||||
|
||||
# self.assertEqual(client.name, "GET")
|
||||
# self.assertEqual(client.kind, SpanKind.CLIENT)
|
||||
# self.assertSpanHasAttributes(
|
||||
# client,
|
||||
# {
|
||||
# SpanAttributes.HTTP_URL: "http://httpbin.org/status/200",
|
||||
# SpanAttributes.HTTP_METHOD: "GET",
|
||||
# SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
# },
|
||||
# )
|
||||
with app.run("localhost", 5000):
|
||||
response = self.fetch(
|
||||
"http://username:password@localhost:5000/status/200"
|
||||
)
|
||||
self.assertEqual(response.code, 200)
|
||||
|
||||
# self.memory_exporter.clear()
|
||||
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
|
||||
self.assertEqual(len(spans), 1)
|
||||
client = spans[0]
|
||||
|
||||
self.assertEqual(client.name, "GET")
|
||||
self.assertEqual(client.kind, SpanKind.CLIENT)
|
||||
self.assertSpanHasAttributes(
|
||||
client,
|
||||
{
|
||||
SpanAttributes.HTTP_URL: "http://localhost:5000/status/200",
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
},
|
||||
)
|
||||
|
||||
self.memory_exporter.clear()
|
||||
|
||||
|
||||
class TestTornadoInstrumentationWithXHeaders(TornadoTest):
|
||||
|
|
@ -531,7 +536,7 @@ class TestTornadoInstrumentationWithXHeaders(TornadoTest):
|
|||
self.assertEqual(response.code, 201)
|
||||
spans = self.get_finished_spans()
|
||||
self.assertSpanHasAttributes(
|
||||
spans.by_name("MainHandler.get"),
|
||||
spans.by_name("GET /"),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_SCHEME: "http",
|
||||
|
|
@ -605,7 +610,7 @@ class TestTornadoUninstrument(TornadoTest):
|
|||
self.assertEqual(len(spans), 3)
|
||||
manual, server, client = self.sorted_spans(spans)
|
||||
self.assertEqual(manual.name, "manual")
|
||||
self.assertEqual(server.name, "MainHandler.get")
|
||||
self.assertEqual(server.name, "GET /")
|
||||
self.assertEqual(client.name, "GET")
|
||||
self.memory_exporter.clear()
|
||||
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ def _instrument(
|
|||
|
||||
method = request.get_method().upper()
|
||||
|
||||
span_name = f"HTTP {method}".strip()
|
||||
span_name = method.strip()
|
||||
|
||||
url = remove_url_credentials(url)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ from opentelemetry.test.test_base import TestBase
|
|||
|
||||
|
||||
class TestUrllibMetricsInstrumentation(TestBase):
|
||||
URL = "http://httpbin.org/status/200"
|
||||
URL_POST = "http://httpbin.org/post"
|
||||
URL = "http://mock/status/200"
|
||||
URL_POST = "http://mock/post"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ from opentelemetry.util.http import get_excluded_urls
|
|||
class RequestsIntegrationTestBase(abc.ABC):
|
||||
# pylint: disable=no-member
|
||||
|
||||
URL = "http://httpbin.org/status/200"
|
||||
URL_TIMEOUT = "http://httpbin.org/timeout/0"
|
||||
URL_EXCEPTION = "http://httpbin.org/exception/0"
|
||||
URL = "http://mock/status/200"
|
||||
URL_TIMEOUT = "http://mock/timeout/0"
|
||||
URL_EXCEPTION = "http://mock/exception/0"
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
|
|
@ -83,7 +83,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
)
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
"http://httpbin.org/status/500",
|
||||
"http://mock/status/500",
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
span = self.assert_span()
|
||||
|
||||
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
|
|
@ -142,7 +142,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
)
|
||||
|
||||
def test_excluded_urls_explicit(self):
|
||||
url_201 = "http://httpbin.org/status/201"
|
||||
url_201 = "http://mock/status/201"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_201,
|
||||
|
|
@ -172,7 +172,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
self.assert_span(num_spans=1)
|
||||
|
||||
def test_not_foundbasic(self):
|
||||
url_404 = "http://httpbin.org/status/404/"
|
||||
url_404 = "http://mock/status/404/"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_404,
|
||||
|
|
@ -209,7 +209,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
span = self.assert_span()
|
||||
|
||||
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
|
|
@ -336,14 +336,14 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
|
||||
def test_requests_exception_with_response(self, *_, **__):
|
||||
with self.assertRaises(HTTPError):
|
||||
self.perform_request("http://httpbin.org/status/500")
|
||||
self.perform_request("http://mock/status/500")
|
||||
|
||||
span = self.assert_span()
|
||||
self.assertEqual(
|
||||
dict(span.attributes),
|
||||
{
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_URL: "http://httpbin.org/status/500",
|
||||
SpanAttributes.HTTP_URL: "http://mock/status/500",
|
||||
SpanAttributes.HTTP_STATUS_CODE: 500,
|
||||
},
|
||||
)
|
||||
|
|
@ -365,7 +365,7 @@ class RequestsIntegrationTestBase(abc.ABC):
|
|||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
|
||||
def test_credential_removal(self):
|
||||
url = "http://username:password@httpbin.org/status/200"
|
||||
url = "http://username:password@mock/status/200"
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
self.perform_request(url)
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ The hooks can be configured as follows:
|
|||
def response_hook(span, request, response):
|
||||
pass
|
||||
|
||||
URLLib3Instrumentor.instrument(
|
||||
request_hook=request_hook, response_hook=response_hook)
|
||||
URLLib3Instrumentor().instrument(
|
||||
request_hook=request_hook, response_hook=response_hook
|
||||
)
|
||||
|
||||
Exclude lists
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ The hooks can be configured as follows:
|
|||
def response_hook(span, request, response):
|
||||
pass
|
||||
|
||||
URLLib3Instrumentor.instrument(
|
||||
request_hook=request_hook, response_hook=response_hook)
|
||||
URLLib3Instrumentor().instrument(
|
||||
request_hook=request_hook, response_hook=response_hook
|
||||
)
|
||||
|
||||
Exclude lists
|
||||
|
|
@ -225,7 +225,7 @@ def _instrument(
|
|||
headers = _prepare_headers(kwargs)
|
||||
body = _get_url_open_arg("body", args, kwargs)
|
||||
|
||||
span_name = f"HTTP {method.strip()}"
|
||||
span_name = method.strip()
|
||||
span_attributes = {
|
||||
SpanAttributes.HTTP_METHOD: method,
|
||||
SpanAttributes.HTTP_URL: url,
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ from opentelemetry.util.http import get_excluded_urls
|
|||
|
||||
|
||||
class TestURLLib3Instrumentor(TestBase):
|
||||
HTTP_URL = "http://httpbin.org/status/200"
|
||||
HTTPS_URL = "https://httpbin.org/status/200"
|
||||
HTTP_URL = "http://mock/status/200"
|
||||
HTTPS_URL = "https://mock/status/200"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -87,7 +87,7 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
|
||||
span = self.assert_span()
|
||||
self.assertIs(trace.SpanKind.CLIENT, span.kind)
|
||||
self.assertEqual("HTTP GET", span.name)
|
||||
self.assertEqual("GET", span.name)
|
||||
|
||||
attributes = {
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
|
|
@ -123,7 +123,7 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
self.assert_success_span(response, self.HTTP_URL)
|
||||
|
||||
def test_basic_http_success_using_connection_pool(self):
|
||||
pool = urllib3.HTTPConnectionPool("httpbin.org")
|
||||
pool = urllib3.HTTPConnectionPool("mock")
|
||||
response = pool.request("GET", "/status/200")
|
||||
|
||||
self.assert_success_span(response, self.HTTP_URL)
|
||||
|
|
@ -133,13 +133,13 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
self.assert_success_span(response, self.HTTPS_URL)
|
||||
|
||||
def test_basic_https_success_using_connection_pool(self):
|
||||
pool = urllib3.HTTPSConnectionPool("httpbin.org")
|
||||
pool = urllib3.HTTPSConnectionPool("mock")
|
||||
response = pool.request("GET", "/status/200")
|
||||
|
||||
self.assert_success_span(response, self.HTTPS_URL)
|
||||
|
||||
def test_basic_not_found(self):
|
||||
url_404 = "http://httpbin.org/status/404"
|
||||
url_404 = "http://mock/status/404"
|
||||
httpretty.register_uri(httpretty.GET, url_404, status=404)
|
||||
|
||||
response = self.perform_request(url_404)
|
||||
|
|
@ -152,30 +152,30 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
self.assertIs(trace.status.StatusCode.ERROR, span.status.status_code)
|
||||
|
||||
def test_basic_http_non_default_port(self):
|
||||
url = "http://httpbin.org:666/status/200"
|
||||
url = "http://mock:666/status/200"
|
||||
httpretty.register_uri(httpretty.GET, url, body="Hello!")
|
||||
|
||||
response = self.perform_request(url)
|
||||
self.assert_success_span(response, url)
|
||||
|
||||
def test_basic_http_absolute_url(self):
|
||||
url = "http://httpbin.org:666/status/200"
|
||||
url = "http://mock:666/status/200"
|
||||
httpretty.register_uri(httpretty.GET, url, body="Hello!")
|
||||
pool = urllib3.HTTPConnectionPool("httpbin.org", port=666)
|
||||
pool = urllib3.HTTPConnectionPool("mock", port=666)
|
||||
response = pool.request("GET", url)
|
||||
|
||||
self.assert_success_span(response, url)
|
||||
|
||||
def test_url_open_explicit_arg_parameters(self):
|
||||
url = "http://httpbin.org:666/status/200"
|
||||
url = "http://mock:666/status/200"
|
||||
httpretty.register_uri(httpretty.GET, url, body="Hello!")
|
||||
pool = urllib3.HTTPConnectionPool("httpbin.org", port=666)
|
||||
pool = urllib3.HTTPConnectionPool("mock", port=666)
|
||||
response = pool.urlopen(method="GET", url="/status/200")
|
||||
|
||||
self.assert_success_span(response, url)
|
||||
|
||||
def test_excluded_urls_explicit(self):
|
||||
url_201 = "http://httpbin.org/status/201"
|
||||
url_201 = "http://mock/status/201"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_201,
|
||||
|
|
@ -301,7 +301,7 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
self.assert_success_span(response, self.HTTP_URL)
|
||||
|
||||
def test_credential_removal(self):
|
||||
url = "http://username:password@httpbin.org/status/200"
|
||||
url = "http://username:password@mock/status/200"
|
||||
|
||||
response = self.perform_request(url)
|
||||
self.assert_success_span(response, self.HTTP_URL)
|
||||
|
|
@ -339,7 +339,7 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
headers = {"header1": "value1", "header2": "value2"}
|
||||
body = "param1=1¶m2=2"
|
||||
|
||||
pool = urllib3.HTTPConnectionPool("httpbin.org")
|
||||
pool = urllib3.HTTPConnectionPool("mock")
|
||||
response = pool.request(
|
||||
"POST", "/status/200", body=body, headers=headers
|
||||
)
|
||||
|
|
@ -366,7 +366,7 @@ class TestURLLib3Instrumentor(TestBase):
|
|||
|
||||
body = "param1=1¶m2=2"
|
||||
|
||||
pool = urllib3.HTTPConnectionPool("httpbin.org")
|
||||
pool = urllib3.HTTPConnectionPool("mock")
|
||||
response = pool.urlopen("POST", "/status/200", body)
|
||||
|
||||
self.assertEqual(b"Hello!", response.data)
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase):
|
|||
|
||||
span = self.assert_span()
|
||||
self.assertIs(trace.SpanKind.CLIENT, span.kind)
|
||||
self.assertEqual("HTTP GET", span.name)
|
||||
self.assertEqual("GET", span.name)
|
||||
|
||||
attributes = {
|
||||
"http.status_code": 200,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from opentelemetry.test.test_base import TestBase
|
|||
|
||||
|
||||
class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
||||
HTTP_URL = "http://httpbin.org/status/200"
|
||||
HTTP_URL = "http://mock/status/200"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -68,11 +68,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=client_duration_estimated,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "GET",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -91,11 +91,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=0,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "GET",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -116,11 +116,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=expected_size,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "GET",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -144,11 +144,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=6,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "POST",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -172,11 +172,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=6,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "POST",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -201,11 +201,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=expected_value,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "POST",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
@ -229,11 +229,11 @@ class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase):
|
|||
min_data_point=6,
|
||||
attributes={
|
||||
"http.flavor": "1.1",
|
||||
"http.host": "httpbin.org",
|
||||
"http.host": "mock",
|
||||
"http.method": "POST",
|
||||
"http.scheme": "http",
|
||||
"http.status_code": 200,
|
||||
"net.peer.name": "httpbin.org",
|
||||
"net.peer.name": "mock",
|
||||
"net.peer.port": 80,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -440,8 +440,21 @@ def add_response_attributes(
|
|||
|
||||
|
||||
def get_default_span_name(environ):
|
||||
"""Default implementation for name_callback, returns HTTP {METHOD_NAME}."""
|
||||
return f"HTTP {environ.get('REQUEST_METHOD', '')}".strip()
|
||||
"""
|
||||
Default span name is the HTTP method and URL path, or just the method.
|
||||
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
|
||||
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
|
||||
|
||||
Args:
|
||||
environ: The WSGI environ object.
|
||||
Returns:
|
||||
The span name.
|
||||
"""
|
||||
method = environ.get("REQUEST_METHOD", "").strip()
|
||||
path = environ.get("PATH_INFO", "").strip()
|
||||
if method and path:
|
||||
return f"{method} {path}"
|
||||
return method
|
||||
|
||||
|
||||
class OpenTelemetryMiddleware:
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class TestWsgiApplication(WsgiTestBase):
|
|||
self,
|
||||
response,
|
||||
error=None,
|
||||
span_name="HTTP GET",
|
||||
span_name="GET /",
|
||||
http_method="GET",
|
||||
span_attributes=None,
|
||||
response_headers=None,
|
||||
|
|
@ -284,12 +284,13 @@ class TestWsgiApplication(WsgiTestBase):
|
|||
)
|
||||
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
|
||||
|
||||
def test_default_span_name_missing_request_method(self):
|
||||
"""Test that default span_names with missing request method."""
|
||||
self.environ.pop("REQUEST_METHOD")
|
||||
def test_default_span_name_missing_path_info(self):
|
||||
"""Test that default span_names with missing path info."""
|
||||
self.environ.pop("PATH_INFO")
|
||||
method = self.environ.get("REQUEST_METHOD", "").strip()
|
||||
app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
|
||||
response = app(self.environ, self.start_response)
|
||||
self.validate_response(response, span_name="HTTP", http_method=None)
|
||||
self.validate_response(response, span_name=method)
|
||||
|
||||
|
||||
class TestWsgiAttributes(unittest.TestCase):
|
||||
|
|
@ -437,10 +438,10 @@ class TestWsgiAttributes(unittest.TestCase):
|
|||
self.span.set_attribute.assert_has_calls(expected, any_order=True)
|
||||
|
||||
def test_credential_removal(self):
|
||||
self.environ["HTTP_HOST"] = "username:password@httpbin.com"
|
||||
self.environ["HTTP_HOST"] = "username:password@mock"
|
||||
self.environ["PATH_INFO"] = "/status/200"
|
||||
expected = {
|
||||
SpanAttributes.HTTP_URL: "http://httpbin.com/status/200",
|
||||
SpanAttributes.HTTP_URL: "http://mock/status/200",
|
||||
SpanAttributes.NET_HOST_PORT: 80,
|
||||
}
|
||||
self.assertGreaterEqual(
|
||||
|
|
@ -455,7 +456,7 @@ class TestWsgiMiddlewareWithTracerProvider(WsgiTestBase):
|
|||
response,
|
||||
exporter,
|
||||
error=None,
|
||||
span_name="HTTP GET",
|
||||
span_name="GET /",
|
||||
http_method="GET",
|
||||
):
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ libraries = {
|
|||
"instrumentation": "opentelemetry-instrumentation-celery==0.40b0.dev",
|
||||
},
|
||||
"confluent-kafka": {
|
||||
"library": "confluent-kafka >= 1.8.2, < 2.0.0",
|
||||
"library": "confluent-kafka >= 1.8.2, <= 2.2.0",
|
||||
"instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.40b0.dev",
|
||||
},
|
||||
"django": {
|
||||
|
|
@ -81,7 +81,7 @@ libraries = {
|
|||
"instrumentation": "opentelemetry-instrumentation-grpc==0.40b0.dev",
|
||||
},
|
||||
"httpx": {
|
||||
"library": "httpx >= 0.18.0, <= 0.23.0",
|
||||
"library": "httpx >= 0.18.0",
|
||||
"instrumentation": "opentelemetry-instrumentation-httpx==0.40b0.dev",
|
||||
},
|
||||
"jinja2": {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ def _start_internal_or_server_span(
|
|||
|
||||
Args:
|
||||
tracer : tracer in use by given instrumentation library
|
||||
name (string): name of the span
|
||||
span_name (string): name of the span
|
||||
start_time : start time of the span
|
||||
context_carrier : object which contains values that are
|
||||
used to construct a Context. This object
|
||||
|
|
|
|||
|
|
@ -303,8 +303,8 @@ def test_fn_exception_expected(celery_app, memory_exporter):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
assert span.status.is_ok is True
|
||||
assert span.status.status_code == StatusCode.UNSET
|
||||
assert span.status.is_ok is False
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
assert span.name == "run/test_celery_functional.fn_exception"
|
||||
assert span.attributes.get("celery.action") == "run"
|
||||
assert span.attributes.get("celery.state") == "FAILURE"
|
||||
|
|
@ -443,8 +443,8 @@ def test_class_task_exception_excepted(celery_app, memory_exporter):
|
|||
|
||||
span = spans[0]
|
||||
|
||||
assert span.status.is_ok is True
|
||||
assert span.status.status_code == StatusCode.UNSET
|
||||
assert span.status.is_ok is False
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
assert span.name == "run/test_celery_functional.BaseTask"
|
||||
assert span.attributes.get("celery.action") == "run"
|
||||
assert span.attributes.get("celery.state") == "FAILURE"
|
||||
|
|
|
|||
|
|
@ -47,9 +47,7 @@ class TestRedisInstrument(TestBase):
|
|||
|
||||
def test_long_command_sanitized(self):
|
||||
RedisInstrumentor().uninstrument()
|
||||
RedisInstrumentor().instrument(
|
||||
tracer_provider=self.tracer_provider, sanitize_query=True
|
||||
)
|
||||
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
|
||||
|
||||
self.redis_client.mget(*range(2000))
|
||||
|
||||
|
|
@ -75,7 +73,7 @@ class TestRedisInstrument(TestBase):
|
|||
self._check_span(span, "MGET")
|
||||
self.assertTrue(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT).startswith(
|
||||
"MGET 0 1 2 3"
|
||||
"MGET ? ? ? ?"
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
|
|
@ -84,9 +82,7 @@ class TestRedisInstrument(TestBase):
|
|||
|
||||
def test_basics_sanitized(self):
|
||||
RedisInstrumentor().uninstrument()
|
||||
RedisInstrumentor().instrument(
|
||||
tracer_provider=self.tracer_provider, sanitize_query=True
|
||||
)
|
||||
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
|
||||
|
||||
self.assertIsNone(self.redis_client.get("cheese"))
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
|
|
@ -105,15 +101,13 @@ class TestRedisInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "GET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
|
||||
|
||||
def test_pipeline_traced_sanitized(self):
|
||||
RedisInstrumentor().uninstrument()
|
||||
RedisInstrumentor().instrument(
|
||||
tracer_provider=self.tracer_provider, sanitize_query=True
|
||||
)
|
||||
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
|
||||
|
||||
with self.redis_client.pipeline(transaction=False) as pipeline:
|
||||
pipeline.set("blah", 32)
|
||||
|
|
@ -144,15 +138,13 @@ class TestRedisInstrument(TestBase):
|
|||
self._check_span(span, "SET RPUSH HGETALL")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT),
|
||||
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
|
||||
"SET ? ?\nRPUSH ? ?\nHGETALL ?",
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
|
||||
|
||||
def test_pipeline_immediate_sanitized(self):
|
||||
RedisInstrumentor().uninstrument()
|
||||
RedisInstrumentor().instrument(
|
||||
tracer_provider=self.tracer_provider, sanitize_query=True
|
||||
)
|
||||
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
|
||||
|
||||
with self.redis_client.pipeline() as pipeline:
|
||||
pipeline.set("a", 1)
|
||||
|
|
@ -182,7 +174,7 @@ class TestRedisInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "SET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "SET b 2"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "SET ? ?"
|
||||
)
|
||||
|
||||
def test_parent(self):
|
||||
|
|
@ -230,7 +222,7 @@ class TestRedisClusterInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "GET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
|
||||
|
||||
|
|
@ -247,7 +239,7 @@ class TestRedisClusterInstrument(TestBase):
|
|||
self._check_span(span, "SET RPUSH HGETALL")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT),
|
||||
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
|
||||
"SET ? ?\nRPUSH ? ?\nHGETALL ?",
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
|
||||
|
||||
|
|
@ -308,7 +300,7 @@ class TestAsyncRedisInstrument(TestBase):
|
|||
self._check_span(span, "MGET")
|
||||
self.assertTrue(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT).startswith(
|
||||
"MGET 0 1 2 3"
|
||||
"MGET ? ? ? ?"
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
|
|
@ -322,7 +314,7 @@ class TestAsyncRedisInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "GET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
|
||||
|
||||
|
|
@ -344,7 +336,7 @@ class TestAsyncRedisInstrument(TestBase):
|
|||
self._check_span(span, "SET RPUSH HGETALL")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT),
|
||||
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
|
||||
"SET ? ?\nRPUSH ? ?\nHGETALL ?",
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
|
||||
|
||||
|
|
@ -364,7 +356,7 @@ class TestAsyncRedisInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "SET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "SET b 2"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "SET ? ?"
|
||||
)
|
||||
|
||||
def test_parent(self):
|
||||
|
|
@ -412,7 +404,7 @@ class TestAsyncRedisClusterInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "GET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
|
||||
|
||||
|
|
@ -434,7 +426,7 @@ class TestAsyncRedisClusterInstrument(TestBase):
|
|||
self._check_span(span, "SET RPUSH HGETALL")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT),
|
||||
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
|
||||
"SET ? ?\nRPUSH ? ?\nHGETALL ?",
|
||||
)
|
||||
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
|
||||
|
||||
|
|
@ -488,5 +480,5 @@ class TestRedisDBIndexInstrument(TestBase):
|
|||
span = spans[0]
|
||||
self._check_span(span, "GET")
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET foo"
|
||||
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
|
||||
)
|
||||
|
|
|
|||
18
tox.ini
18
tox.ini
|
|
@ -84,8 +84,8 @@ envlist =
|
|||
pypy3-test-instrumentation-fastapi
|
||||
|
||||
; opentelemetry-instrumentation-flask
|
||||
py3{7,8,9,10,11}-test-instrumentation-flask
|
||||
pypy3-test-instrumentation-flask
|
||||
py3{7,8,9,10,11}-test-instrumentation-flask{213,220}
|
||||
pypy3-test-instrumentation-flask{213,220}
|
||||
|
||||
; opentelemetry-instrumentation-urllib
|
||||
py3{7,8,9,10,11}-test-instrumentation-urllib
|
||||
|
|
@ -93,7 +93,7 @@ envlist =
|
|||
|
||||
; opentelemetry-instrumentation-urllib3
|
||||
py3{7,8,9,10,11}-test-instrumentation-urllib3
|
||||
pypy3-test-instrumentation-urllib3
|
||||
;pypy3-test-instrumentation-urllib3
|
||||
|
||||
; opentelemetry-instrumentation-requests
|
||||
py3{7,8,9,10,11}-test-instrumentation-requests
|
||||
|
|
@ -258,6 +258,8 @@ deps =
|
|||
falcon1: falcon ==1.4.1
|
||||
falcon2: falcon >=2.0.0,<3.0.0
|
||||
falcon3: falcon >=3.0.0,<4.0.0
|
||||
flask213: Flask ==2.1.3
|
||||
flask220: Flask >=2.2.0
|
||||
grpc: pytest-asyncio
|
||||
sqlalchemy11: sqlalchemy>=1.1,<1.2
|
||||
sqlalchemy14: aiosqlite
|
||||
|
|
@ -275,7 +277,7 @@ deps =
|
|||
httpx18: httpx>=0.18.0,<0.19.0
|
||||
httpx18: respx~=0.17.0
|
||||
httpx21: httpx>=0.19.0
|
||||
httpx21: respx~=0.19.0
|
||||
httpx21: respx~=0.20.1
|
||||
|
||||
; FIXME: add coverage testing
|
||||
; FIXME: add mypy testing
|
||||
|
|
@ -304,7 +306,7 @@ changedir =
|
|||
test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests
|
||||
test-instrumentation-falcon{1,2,3}: instrumentation/opentelemetry-instrumentation-falcon/tests
|
||||
test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests
|
||||
test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests
|
||||
test-instrumentation-flask{213,220}: instrumentation/opentelemetry-instrumentation-flask/tests
|
||||
test-instrumentation-urllib: instrumentation/opentelemetry-instrumentation-urllib/tests
|
||||
test-instrumentation-urllib3: instrumentation/opentelemetry-instrumentation-urllib3/tests
|
||||
test-instrumentation-grpc: instrumentation/opentelemetry-instrumentation-grpc/tests
|
||||
|
|
@ -365,8 +367,8 @@ commands_pre =
|
|||
|
||||
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
|
||||
|
||||
falcon{1,2,3},flask,django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
|
||||
wsgi,falcon{1,2,3},flask,django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
|
||||
falcon{1,2,3},flask{213,220},django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
|
||||
wsgi,falcon{1,2,3},flask{213,220},django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
|
||||
asgi,django{3,4},starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
|
||||
|
||||
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
|
||||
|
|
@ -380,7 +382,7 @@ commands_pre =
|
|||
|
||||
falcon{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
|
||||
|
||||
flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
|
||||
flask{213,220}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
|
||||
|
||||
urllib: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib[test]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue