Skip to content

feat: Add twisted instrumentation and unit/integration tests#888

Open
CagriYonca wants to merge 1 commit into
mainfrom
twisted
Open

feat: Add twisted instrumentation and unit/integration tests#888
CagriYonca wants to merge 1 commit into
mainfrom
twisted

Conversation

@CagriYonca

Copy link
Copy Markdown
Contributor

Added twisted webserver instrumentation and related tests.

@CagriYonca CagriYonca self-assigned this Jul 13, 2026
@CagriYonca
CagriYonca force-pushed the twisted branch 4 times, most recently from 1585ae8 to b116c82 Compare July 13, 2026 11:12
Signed-off-by: Cagri Yonca <cagri@ibm.com>
@CagriYonca
CagriYonca marked this pull request as ready for review July 13, 2026 12:10
@CagriYonca
CagriYonca requested a review from a team as a code owner July 13, 2026 12:10
@sonarqubecloud

Copy link
Copy Markdown

@pvital pvital added the feature label Jul 13, 2026
Comment on lines +85 to +329
def test_get(self) -> None:
with self.tracer.start_as_current_span("test"):
response = requests.get(testenv["twisted_server"] + "/")

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert not get_current_span().is_recording()

# Same traceId
traceId = test_span.t
assert traceId == client_span.t
assert traceId == server_span.t

# Parent relationships
assert client_span.p == test_span.s
assert server_span.p == client_span.s

# Error logging
assert not test_span.ec
assert not client_span.ec
assert not server_span.ec

assert server_span.data["http"]["status"] == 200
assert testenv["twisted_server"] + "/" == server_span.data["http"]["url"]
assert not server_span.data["http"].get("params")
assert server_span.data["http"]["method"] == "GET"

assert "X-INSTANA-T" in response.headers
assert response.headers["X-INSTANA-T"] == hex_id(traceId)
assert "X-INSTANA-S" in response.headers
assert response.headers["X-INSTANA-S"] == hex_id(server_span.s)
assert "X-INSTANA-L" in response.headers
assert response.headers["X-INSTANA-L"] == "1"
assert "Server-Timing" in response.headers
assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}"

def test_post(self) -> None:
with self.tracer.start_as_current_span("test"):
_ = requests.post(testenv["twisted_server"] + "/", data={"hello": "post"})

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert not get_current_span().is_recording()

# Same traceId
traceId = test_span.t
assert traceId == client_span.t
assert traceId == server_span.t

# Parent relationships
assert client_span.p == test_span.s
assert server_span.p == client_span.s

# Error logging
assert not test_span.ec
assert not client_span.ec
assert not server_span.ec

assert server_span.data["http"]["status"] == 200
assert testenv["twisted_server"] + "/" == server_span.data["http"]["url"]
assert not server_span.data["http"].get("params")
assert server_span.data["http"]["method"] == "POST"

def test_get_301(self) -> None:
with self.tracer.start_as_current_span("test"):
_ = requests.get(testenv["twisted_server"] + "/301", allow_redirects=False)

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert not get_current_span().is_recording()

# Same traceId
traceId = test_span.t
assert traceId == client_span.t
assert traceId == server_span.t

# Parent relationships
assert client_span.p == test_span.s
assert server_span.p == client_span.s

assert server_span.data["http"]["status"] == 301
assert testenv["twisted_server"] + "/301" == server_span.data["http"]["url"]
assert server_span.data["http"]["method"] == "GET"

def test_get_404(self) -> None:
with self.tracer.start_as_current_span("test"):
_ = requests.get(testenv["twisted_server"] + "/404")

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert server_span.data["http"]["status"] == 404
assert testenv["twisted_server"] + "/404" == server_span.data["http"]["url"]

def test_get_500(self) -> None:
with self.tracer.start_as_current_span("test"):
_ = requests.get(testenv["twisted_server"] + "/500")

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert not get_current_span().is_recording()

# Same traceId
traceId = test_span.t
assert traceId == client_span.t
assert traceId == server_span.t

assert server_span.data["http"]["status"] == 500
assert testenv["twisted_server"] + "/500" == server_span.data["http"]["url"]

# Error logging
assert not test_span.ec
assert client_span.ec == 1
assert server_span.ec == 1

def test_get_with_params_to_scrub(self) -> None:
with self.tracer.start_as_current_span("test"):
_ = requests.get(testenv["twisted_server"] + "/", params={"secret": "yeah"})

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert not get_current_span().is_recording()

# Same traceId
traceId = test_span.t
assert traceId == client_span.t
assert traceId == server_span.t

assert server_span.data["http"]["status"] == 200
assert testenv["twisted_server"] + "/" == server_span.data["http"]["url"]
assert server_span.data["http"]["params"] == "secret=<redacted>"

def test_request_header_capture(self) -> None:
original_extra_http_headers = agent.options.extra_http_headers
agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"]

with self.tracer.start_as_current_span("test"):
_ = requests.get(
testenv["twisted_server"] + "/",
headers={"X-Capture-This": "this", "X-Capture-That": "that"},
)

agent.options.extra_http_headers = original_extra_http_headers

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert "X-Capture-This" in server_span.data["http"]["header"]
assert server_span.data["http"]["header"]["X-Capture-This"] == "this"
assert "X-Capture-That" in server_span.data["http"]["header"]
assert server_span.data["http"]["header"]["X-Capture-That"] == "that"

def test_response_header_capture(self) -> None:
original_extra_http_headers = agent.options.extra_http_headers
agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"]

with self.tracer.start_as_current_span("test"):
_ = requests.get(testenv["twisted_server"] + "/response_headers")

agent.options.extra_http_headers = original_extra_http_headers

time.sleep(0.5)
spans = self.recorder.queued_spans()
assert len(spans) == 3

server_span = get_first_span_by_name(spans, "twisted-server")
client_span = get_first_span_by_name(spans, "urllib3")
test_span = get_first_span_by_name(spans, "sdk")

assert server_span
assert client_span
assert test_span

assert "X-Capture-This-Too" in server_span.data["http"]["header"]
assert server_span.data["http"]["header"]["X-Capture-This-Too"] == "this too"
assert "X-Capture-That-Too" in server_span.data["http"]["header"]
assert server_span.data["http"]["header"]["X-Capture-That-Too"] == "that too"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first 8 test methods in TestTwistedClient are functionally identical to the 8 tests of the same name in TestTwistedServer. Every one of them:

  • Looks up get_first_span_by_name(spans, "urllib3") — the variable is even named client_span, but it holds the urllib3 span, not a twisted-client span
  • Never checks for a span named "twisted-client" at all

span.set_attribute(SpanAttributes.HTTP_METHOD, method_str)

# Build / augment headers with trace correlation
from twisted.web.http_headers import Headers as TwistedHeaders

@GSVarsha GSVarsha Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to the top, in the outermost try block

def _finish_tracing(result: Any, span: "InstanaSpan") -> Any:
"""Callback/errback attached to the Agent.request Deferred."""
try:
from twisted.python.failure import Failure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here


# Capture outgoing request headers
headers_dict = {
k.decode("latin-1"): v[0].decode("latin-1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the official twisted doc here, header names are encoded using ISO-8859-1 (latin-1) and header values are encoded using UTF-8.

getAllRawHeaders() returns raw bytes in both cases, so the caller must decode each correctly:

Suggested change
k.decode("latin-1"): v[0].decode("latin-1")
k.decode("latin-1"): v[0].decode("utf-8")


# Capture response headers
headers_dict = {
k.decode("latin-1"): v[0].decode("latin-1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here

parent_context = None
if request.requestHeaders:
headers_dict: dict[str, str] = {
k.decode("latin-1"): v[0].decode("latin-1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here


# Capture response headers
response_hdrs = {
k.decode("latin-1"): v[0].decode("latin-1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here

@GSVarsha GSVarsha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few requests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants