Skip to content

Commit 375c140

Browse files
committed
feat: Add subprocess integration
1 parent 44508c5 commit 375c140

5 files changed

Lines changed: 108 additions & 17 deletions

File tree

sentry_sdk/integrations/stdlib.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import os
2+
import subprocess
13
import sys
24

35
from sentry_sdk.hub import Hub
46
from sentry_sdk.integrations import Integration
5-
from sentry_sdk.tracing import record_http_request
7+
from sentry_sdk.tracing import EnvironHeaders, record_http_request
68

79

810
try:
@@ -17,10 +19,11 @@ class StdlibIntegration(Integration):
1719
@staticmethod
1820
def setup_once():
1921
# type: () -> None
20-
install_httplib()
22+
_install_httplib()
23+
_install_subprocess()
2124

2225

23-
def install_httplib():
26+
def _install_httplib():
2427
# type: () -> None
2528
real_putrequest = HTTPConnection.putrequest
2629
real_getresponse = HTTPConnection.getresponse
@@ -90,3 +93,47 @@ def getresponse(self, *args, **kwargs):
9093

9194
HTTPConnection.putrequest = putrequest
9295
HTTPConnection.getresponse = getresponse
96+
97+
98+
def _get_argument(args, kwargs, name, position, setdefault=None):
99+
if name in kwargs:
100+
rv = kwargs[name]
101+
if rv is None and setdefault is not None:
102+
rv = kwargs[name] = setdefault
103+
elif position < len(args):
104+
rv = args[position]
105+
if rv is None and setdefault is not None:
106+
rv = args[position] = setdefault
107+
else:
108+
rv = kwargs[name] = setdefault
109+
110+
return rv
111+
112+
113+
def _install_subprocess():
114+
old_popen_init = subprocess.Popen.__init__
115+
116+
def sentry_patched_popen_init(self, *a, **kw):
117+
hub = Hub.current
118+
if hub.get_integration(StdlibIntegration) is None:
119+
return old_popen_init(self, *a, **kw)
120+
121+
# do not setdefault! args is required by Popen, doing setdefault would
122+
# make invalid calls valid
123+
args = _get_argument(a, kw, "args", 0) or []
124+
cwd = _get_argument(a, kw, "cwd", 10)
125+
126+
for k, v in hub.iter_trace_propagation_headers():
127+
env = _get_argument(a, kw, "env", 11, {})
128+
env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
129+
130+
with hub.span(op="subprocess", description=" ".join(map(str, args))) as span:
131+
span.set_tag("subprocess.cwd", cwd)
132+
133+
return old_popen_init(self, *a, **kw)
134+
135+
subprocess.Popen.__init__ = sentry_patched_popen_init
136+
137+
138+
def get_subprocess_traceparent_headers():
139+
return EnvironHeaders(os.environ, prefix="SUBPROCESS_")

sentry_sdk/tracing.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
)
2323

2424

25-
class _EnvironHeaders(object):
26-
def __init__(self, environ):
25+
class EnvironHeaders(object):
26+
def __init__(self, environ, prefix="HTTP_"):
2727
self.environ = environ
28+
self.prefix = prefix
2829

2930
def get(self, key):
30-
return self.environ.get("HTTP_" + key.replace("-", "_").upper())
31+
return self.environ.get(self.prefix + key.replace("-", "_").upper())
3132

3233

3334
class Span(object):
@@ -107,7 +108,7 @@ def new_span(self, **kwargs):
107108

108109
@classmethod
109110
def continue_from_environ(cls, environ):
110-
return cls.continue_from_headers(_EnvironHeaders(environ))
111+
return cls.continue_from_headers(EnvironHeaders(environ))
111112

112113
@classmethod
113114
def continue_from_headers(cls, headers):
@@ -228,3 +229,10 @@ def maybe_create_breadcrumbs_from_span(hub, span):
228229
data=span._data,
229230
hint={"httplib_response": span._data.get("httplib_response")},
230231
)
232+
elif span.op == "subprocess":
233+
hub.add_breadcrumb(
234+
type="subprocess",
235+
category="subprocess",
236+
data=span._data,
237+
hint={"popen_instance": span._data.get("popen_instance")},
238+
)

tests/conftest.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,19 @@ def assert_semaphore_acceptance(tmpdir):
9191
def inner(event):
9292
if not SEMAPHORE:
9393
return
94-
# not dealing with the subprocess API right now
95-
file = tmpdir.join("event")
96-
file.write(json.dumps(dict(event)))
97-
output = json.loads(
98-
subprocess.check_output(
99-
[SEMAPHORE, "process-event"], stdin=file.open()
100-
).decode("utf-8")
101-
)
102-
_no_errors_in_semaphore_response(output)
103-
output.pop("_meta", None)
94+
95+
# Disable subprocess integration
96+
with sentry_sdk.Hub(None):
97+
# not dealing with the subprocess API right now
98+
file = tmpdir.join("event")
99+
file.write(json.dumps(dict(event)))
100+
output = json.loads(
101+
subprocess.check_output(
102+
[SEMAPHORE, "process-event"], stdin=file.open()
103+
).decode("utf-8")
104+
)
105+
_no_errors_in_semaphore_response(output)
106+
output.pop("_meta", None)
104107

105108
return inner
106109

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pytest.importorskip("redis")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import subprocess
2+
3+
from sentry_sdk import Hub, capture_message
4+
from sentry_sdk.integrations.stdlib import StdlibIntegration
5+
6+
7+
def test_subprocess_basic(sentry_init, capture_events):
8+
sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0)
9+
10+
with Hub.current.trace(transaction="foo", op="foo") as span:
11+
output = subprocess.check_output(
12+
"python -c '"
13+
"import sentry_sdk; "
14+
"from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
15+
"sentry_sdk.init(); "
16+
"print(dict(get_subprocess_traceparent_headers()))"
17+
"'",
18+
shell=True,
19+
)
20+
21+
assert span.trace_id in output
22+
23+
events = capture_events()
24+
25+
capture_message("hi")
26+
27+
event, = events
28+
29+
crumb, = event["breadcrumbs"]
30+
assert crumb == {}

0 commit comments

Comments
 (0)