Skip to content

Commit ef43fd7

Browse files
committed
feat(configuration): Mark 4xx calls as erroneous spans and adapt current instrumentations.
Signed-off-by: Cagri Yonca <cagri@ibm.com>
1 parent f7a81ae commit ef43fd7

23 files changed

Lines changed: 1160 additions & 72 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ uv.lock
106106
# Sandbox
107107
sandbox/
108108
.bob/
109+
.serena/

src/instana/instrumentation/aiohttp/client.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22
# (c) Copyright Instana Inc. 2019
33

44

5-
from types import SimpleNamespace
6-
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple
7-
8-
import wrapt
9-
from opentelemetry.semconv.trace import SpanAttributes
10-
11-
from instana.log import logger
12-
from instana.propagators.format import Format
13-
from instana.singletons import agent
14-
from instana.util.secrets import strip_secrets_from_query
15-
from instana.util.traceutils import extract_custom_headers, get_tracer_tuple
16-
175
try:
6+
from collections.abc import Awaitable, Callable
7+
from types import SimpleNamespace
8+
from typing import TYPE_CHECKING, Any
9+
1810
import aiohttp
11+
import wrapt
1912
from opentelemetry.context import get_current
13+
from opentelemetry.semconv.trace import SpanAttributes
14+
15+
from instana.log import logger
16+
from instana.propagators.format import Format
17+
from instana.singletons import agent
18+
from instana.util.http import should_mark_http_exit_as_error
19+
from instana.util.secrets import strip_secrets_from_query
20+
from instana.util.traceutils import extract_custom_headers, get_tracer_tuple
2021

2122
if TYPE_CHECKING:
2223
from aiohttp.client import ClientSession
@@ -65,8 +66,9 @@ async def stan_request_end(
6566

6667
extract_custom_headers(span, params.response.headers)
6768

68-
if params.response.status >= 500:
69-
span.mark_as_errored({"http.error": params.response.reason})
69+
if should_mark_http_exit_as_error(params.response.status, agent.options):
70+
error_msg = f"{params.response.status} {params.response.reason}"
71+
span.mark_as_errored({"http.error": error_msg})
7072

7173
if span.is_recording():
7274
span.end()
@@ -92,8 +94,8 @@ async def stan_request_exception(
9294
def init_with_instana(
9395
wrapped: Callable[..., Awaitable["ClientSession"]],
9496
instance: aiohttp.client.ClientSession,
95-
args: Tuple[int, str, Tuple[object, ...]],
96-
kwargs: Dict[str, Any],
97+
args: tuple[int, str, tuple[object, ...]],
98+
kwargs: dict[str, Any],
9799
) -> object:
98100
instana_trace_config = aiohttp.TraceConfig()
99101
instana_trace_config.on_request_start.append(stan_request_start)

src/instana/instrumentation/httpx.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# (c) Copyright IBM Corp. 2025
22

33
try:
4-
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple
4+
from typing import TYPE_CHECKING, Any, Callable, Optional
55

66
import httpx
77
import wrapt
@@ -12,6 +12,7 @@
1212
from instana.log import logger
1313
from instana.propagators.format import Format
1414
from instana.singletons import agent
15+
from instana.util.http import should_mark_http_exit_as_error
1516
from instana.util.secrets import strip_secrets_from_query
1617
from instana.util.traceutils import extract_custom_headers, get_tracer_tuple
1718

@@ -50,25 +51,26 @@ def _set_request_span_attributes(
5051

5152
def _set_response_span_attributes(
5253
span: "InstanaSpan",
53-
response: Optional[httpx.Response] = None,
54+
response: "Optional[httpx.Response]" = None,
5455
) -> None:
5556
try:
5657
if response.headers:
5758
extract_custom_headers(span, response.headers)
5859

5960
status_code = response.status_code
6061
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
61-
if status_code >= 500:
62-
span.mark_as_errored()
62+
if should_mark_http_exit_as_error(status_code, agent.options):
63+
error_msg = f"{status_code} {response.reason_phrase}"
64+
span.mark_as_errored({"http.error": error_msg})
6365
except Exception:
6466
logger.debug("httpx _set_request_span_attributes error: ", exc_info=True)
6567

6668
@wrapt.patch_function_wrapper("httpx", "HTTPTransport.handle_request")
6769
def handle_request_with_instana(
6870
wrapped: Callable[..., "httpx.HTTPTransport.handle_request"],
6971
instance: httpx.HTTPTransport,
70-
args: Tuple[int, str, Tuple[Any, ...]],
71-
kwargs: Dict[str, Any],
72+
args: tuple[int, str, tuple[Any, ...]],
73+
kwargs: dict[str, Any],
7274
) -> httpx.Response:
7375
tracer, _, _ = get_tracer_tuple()
7476
# If we're not tracing, just return
@@ -102,8 +104,8 @@ def handle_request_with_instana(
102104
async def handle_async_request_with_instana(
103105
wrapped: Callable[..., "httpx.AsyncHTTPTransport.handle_async_request"],
104106
instance: httpx.AsyncHTTPTransport,
105-
args: Tuple[int, str, Tuple[Any, ...]],
106-
kwargs: Dict[str, Any],
107+
args: tuple[int, str, tuple[Any, ...]],
108+
kwargs: dict[str, Any],
107109
) -> httpx.Response:
108110
tracer, _, _ = get_tracer_tuple()
109111
# If we're not tracing, just return

src/instana/instrumentation/tornado/client.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
try:
66
import functools
7-
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
7+
from typing import TYPE_CHECKING, Any, Callable
88

99
import tornado
1010
import wrapt
@@ -23,15 +23,16 @@
2323
from instana.propagators.format import Format
2424
from instana.singletons import agent, get_tracer
2525
from instana.span.span import get_current_span
26+
from instana.util.http import should_mark_http_exit_as_error
2627
from instana.util.secrets import strip_secrets_from_query
2728
from instana.util.traceutils import extract_custom_headers
2829

2930
@wrapt.patch_function_wrapper("tornado.httpclient", "AsyncHTTPClient.fetch")
3031
def fetch_with_instana(
3132
wrapped: Callable[..., object],
3233
instance: "AsyncHTTPClient",
33-
argv: Tuple[object, ...],
34-
kwargs: Dict[str, Any],
34+
argv: tuple[object, ...],
35+
kwargs: dict[str, Any],
3536
) -> "Future":
3637
try:
3738
parent_span = get_current_span()
@@ -47,13 +48,12 @@ def fetch_with_instana(
4748
# To modify request headers, we have to preemptively create an HTTPRequest object if a
4849
# URL string was passed.
4950
if not isinstance(request, tornado.httpclient.HTTPRequest):
51+
# "callback" and "raise_error" are fetch()-level kwargs, not
52+
# HTTPRequest constructor params — extract them first so they
53+
# are not forwarded to HTTPRequest.__init__.
54+
fetch_only_params = ("callback", "raise_error")
55+
new_kwargs = {p: kwargs.pop(p) for p in fetch_only_params if p in kwargs}
5056
request = tornado.httpclient.HTTPRequest(url=request, **kwargs)
51-
52-
new_kwargs = {}
53-
for param in ("callback", "raise_error"):
54-
# if not in instead and pop
55-
if param in kwargs:
56-
new_kwargs[param] = kwargs.pop(param)
5757
kwargs = new_kwargs
5858

5959
parent_context = get_current()
@@ -89,8 +89,10 @@ def finish_tracing(future: "Future", span: "InstanaSpan") -> None:
8989
try:
9090
response = future.result()
9191
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.code)
92-
9392
extract_custom_headers(span, response.headers)
93+
if should_mark_http_exit_as_error(response.code, agent.options):
94+
error_msg = f"{response.code} {response.reason}"
95+
span.mark_as_errored({"http.error": error_msg})
9496
except tornado.httpclient.HTTPClientError as e:
9597
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, e.code)
9698
span.record_exception(e)

src/instana/instrumentation/tornado/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def set_default_headers_with_instana(
7777
span = instance.request._instana
7878
tracer = get_tracer()
7979
tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers)
80+
return wrapped(*argv, **kwargs)
8081

8182
@wrapt.patch_function_wrapper("tornado.web", "RequestHandler.on_finish")
8283
def on_finish_with_instana(

src/instana/instrumentation/twisted/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from instana.propagators.format import Format
2020
from instana.singletons import agent, get_tracer
2121
from instana.span.span import get_current_span
22+
from instana.util.http import should_mark_http_exit_as_error
2223
from instana.util.secrets import strip_secrets_from_query
2324
from instana.util.traceutils import extract_custom_headers
2425

@@ -133,9 +134,10 @@ def finish_tracing(
133134
}
134135
extract_custom_headers(span, headers_dict)
135136

136-
if status_code >= 500:
137+
if should_mark_http_exit_as_error(status_code, agent.options):
138+
phrase = result.phrase.decode("latin-1")
137139
span.mark_as_errored({
138-
"http.error": result.phrase.decode("latin-1")
140+
"http.error": f"{status_code} {phrase}"
139141
})
140142
except Exception:
141143
logger.debug("twisted client finish_tracing", exc_info=True)

src/instana/instrumentation/twisted/server.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,20 @@ def render_with_instana(
6262
span = tracer.start_span(
6363
"twisted-server", context=parent_context)
6464

65-
# Set span as current so downstream code
66-
# (e.g. twisted-client) can find it during the synchronous
67-
# wrapped() call. We detach unconditionally in the finally
68-
# block below once wrapped() has returned.
65+
# Set span as current so that any async work started during
66+
# wrapped() (e.g. outgoing Agent.request Deferreds) can find
67+
# this span as their parent after the event loop resumes.
68+
#
69+
# IMPORTANT: we do NOT detach the token here in the `finally`
70+
# block. Twisted's render() returns NOT_DONE_YET for async
71+
# handlers, and the event loop only fires pending Deferreds
72+
# after render() has returned — by which point a `finally`
73+
# detach would have already removed the context, leaving
74+
# downstream spans (e.g. twisted-client) with no active parent.
75+
#
76+
# Instead the token is stored on the request object and detached
77+
# inside finish_tracing(), which is called by notifyFinish() only
78+
# after the full async response lifecycle has completed.
6979
ctx = trace.set_span_in_context(span)
7080
token = context.attach(ctx)
7181

@@ -119,21 +129,24 @@ def render_with_instana(
119129
for key, value in response_headers.items():
120130
request.setHeader(key.encode("latin-1"), value.encode("utf-8"))
121131

122-
# Store span on request for later retrieval
132+
# Store span and context token on the request so finish_tracing
133+
# can detach the token after the full async lifecycle completes.
123134
request._instana = span
135+
request._instana_token = token
124136
request._instana_finished = False
125137

126138
finish_deferred = request.notifyFinish()
127139
finish_deferred.addBoth(finish_tracing, request)
128140

129141
return wrapped(*argv, **kwargs)
130142
except Exception:
143+
# On instrumentation error detach immediately (we never reach
144+
# finish_tracing in this path) and fall through to the bare call.
145+
if token is not None:
146+
context.detach(token)
131147
if span is not None and span.is_recording():
132148
span.end()
133149
logger.debug("twisted server render_with_instana", exc_info=True)
134-
finally:
135-
if token is not None:
136-
context.detach(token)
137150

138151
return wrapped(*argv, **kwargs)
139152

@@ -146,6 +159,7 @@ def finish_tracing(
146159

147160
request._instana_finished = True
148161
span = request._instana
162+
token = getattr(request, "_instana_token", None)
149163
try:
150164
status_code = request.code
151165
if isinstance(status_code, int):
@@ -159,12 +173,19 @@ def finish_tracing(
159173
extract_custom_headers(span, response_hdrs)
160174

161175
if isinstance(status_code, int) and status_code >= 500:
176+
phrase = request.code_message.decode("latin-1")
162177
span.mark_as_errored({
163-
"http.error": request.code_message.decode("latin-1")
178+
"http.error": f"{status_code} {phrase}"
164179
})
165180
except Exception:
166181
logger.debug("twisted server finish_tracing", exc_info=True)
167182
finally:
183+
# Detach the OTel context token here — after the full async
184+
# response lifecycle — instead of in render_with_instana's
185+
# finally block. This ensures the server span remains the
186+
# active context for any Deferreds started during render().
187+
if token is not None:
188+
context.detach(token)
168189
if span.is_recording():
169190
span.end()
170191

src/instana/instrumentation/urllib3.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,33 @@
22
# (c) Copyright Instana Inc. 2017
33

44

5-
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union
5+
try:
6+
from typing import TYPE_CHECKING, Any, Callable, Union
67

7-
import wrapt
8-
from opentelemetry.context import get_current
9-
from opentelemetry.semconv.trace import SpanAttributes
8+
import wrapt
9+
from opentelemetry.context import get_current
10+
from opentelemetry.semconv.trace import SpanAttributes
1011

11-
from instana.log import logger
12-
from instana.propagators.format import Format
13-
from instana.singletons import agent
14-
from instana.util.secrets import strip_secrets_from_query
15-
from instana.util.traceutils import extract_custom_headers, get_tracer_tuple
12+
from instana.log import logger
13+
from instana.propagators.format import Format
14+
from instana.singletons import agent
15+
from instana.util.http import should_mark_http_exit_as_error
16+
from instana.util.secrets import strip_secrets_from_query
17+
from instana.util.traceutils import extract_custom_headers, get_tracer_tuple
1618

17-
if TYPE_CHECKING:
18-
from instana.span.span import InstanaSpan
19+
if TYPE_CHECKING:
20+
from instana.span.span import InstanaSpan
1921

20-
try:
2122
import urllib3
2223

2324
def _collect_kvs(
2425
instance: Union[
2526
urllib3.connectionpool.HTTPConnectionPool,
2627
urllib3.connectionpool.HTTPSConnectionPool,
2728
],
28-
args: Tuple[int, str, Tuple[Any, ...]],
29-
kwargs: Dict[str, Any],
30-
) -> Dict[str, Any]:
29+
args: tuple[int, str, tuple[Any, ...]],
30+
kwargs: dict[str, Any],
31+
) -> dict[str, Any]:
3132
kvs = dict()
3233
try:
3334
kvs["host"] = instance.host
@@ -74,8 +75,9 @@ def collect_response(
7475

7576
extract_custom_headers(span, response.headers)
7677

77-
if response.status >= 500:
78-
span.mark_as_errored()
78+
if should_mark_http_exit_as_error(response.status, agent.options):
79+
error_msg = f"{response.status} {response.reason}"
80+
span.mark_as_errored({"http.error": error_msg})
7981
except Exception:
8082
logger.debug("urllib3 collect_response error: ", exc_info=True)
8183

@@ -88,8 +90,8 @@ def urlopen_with_instana(
8890
urllib3.connectionpool.HTTPConnectionPool,
8991
urllib3.connectionpool.HTTPSConnectionPool,
9092
],
91-
args: Tuple[int, str, Tuple[Any, ...]],
92-
kwargs: Dict[str, Any],
93+
args: tuple[int, str, tuple[Any, ...]],
94+
kwargs: dict[str, Any],
9395
) -> urllib3.response.HTTPResponse:
9496
tracer, _, span_name = get_tracer_tuple()
9597

0 commit comments

Comments
 (0)