Issue #1757 - Update HTTP server/client instrumentation span names (#1759)

Co-authored-by: Shalev Roda <65566801+shalevr@users.noreply.github.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Maciej Nachtygal
2023-06-16 00:21:05 +02:00
committed by GitHub
parent a5ed4da478
commit 743ac64661
30 changed files with 194 additions and 118 deletions

View File

@ -51,6 +51,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
### Added

View File

@ -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)

View File

@ -119,7 +119,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(span_status, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -213,7 +213,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(StatusCode.UNSET, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -247,7 +247,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(expected_status, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -274,7 +274,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(StatusCode.ERROR, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -301,7 +301,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(StatusCode.ERROR, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -338,7 +338,7 @@ class TestAioHttpIntegration(TestBase):
self.assert_spans(
[
(
"HTTP GET",
"GET",
(StatusCode.UNSET, None),
{
SpanAttributes.HTTP_METHOD: "GET",
@ -391,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",

View File

@ -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(

View File

@ -142,12 +142,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 +155,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 +231,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
@ -493,9 +493,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

View File

@ -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):

View File

@ -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")

View File

@ -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")

View File

@ -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 /does-not-exist")
self.assertEqual(span.status.status_code, StatusCode.UNSET)
self.assertSpanHasAttributes(
span,

View File

@ -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

View File

@ -106,7 +106,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 +138,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 +146,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}"
)

View File

@ -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)

View File

@ -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:

View File

@ -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,
@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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,
@ -191,7 +191,7 @@ 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://mock/status/404"

View File

@ -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,

View File

@ -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

View File

@ -93,7 +93,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 +101,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}"
)

View File

@ -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,

View File

@ -136,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)
@ -197,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")
@ -214,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)
@ -230,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}",
},
)
@ -254,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,
@ -291,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,
@ -326,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,
@ -367,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)
@ -408,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)
@ -535,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",
@ -609,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()

View File

@ -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)

View File

@ -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,
@ -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,

View File

@ -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,

View File

@ -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",

View File

@ -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,

View File

@ -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:

View File

@ -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):
@ -455,7 +456,7 @@ class TestWsgiMiddlewareWithTracerProvider(WsgiTestBase):
response,
exporter,
error=None,
span_name="HTTP GET",
span_name="GET /",
http_method="GET",
):
while True:

View File

@ -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