Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): Attach thread metadata to profiles #1660

Merged
merged 3 commits into from Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 4 additions & 8 deletions sentry_sdk/client.py
Expand Up @@ -357,6 +357,8 @@ def capture_event(
if not self._should_capture(event, hint, scope):
return None

profile = event.pop("profile", None)

event_opt = self._prepare_event(event, hint, scope)
if event_opt is None:
return None
Expand Down Expand Up @@ -409,14 +411,8 @@ def capture_event(
envelope = Envelope(headers=headers)

if is_transaction:
if "profile" in event_opt:
event_opt["profile"]["environment"] = event_opt.get("environment")
event_opt["profile"]["release"] = event_opt.get("release", "")
event_opt["profile"]["timestamp"] = event_opt.get("timestamp", "")
event_opt["profile"]["transactions"][0]["id"] = event_opt[
"event_id"
]
envelope.add_profile(event_opt.pop("profile"))
if profile is not None:
envelope.add_profile(profile.to_json(event_opt))
envelope.add_transaction(event_opt)
else:
envelope.add_event(event_opt)
Expand Down
42 changes: 29 additions & 13 deletions sentry_sdk/profiler.py
Expand Up @@ -56,7 +56,7 @@ def setup_profiler(options):
`buffer_secs` determines the max time a sample will be buffered for
`frequency` determines the number of samples to take per second (Hz)
"""
buffer_secs = 60
buffer_secs = 30
frequency = 101

if not PY33:
Expand Down Expand Up @@ -163,6 +163,8 @@ def __init__(self, transaction, hub=None):
self._start_ns = None # type: Optional[int]
self._stop_ns = None # type: Optional[int]

transaction._profile = self

def __enter__(self):
# type: () -> None
assert _scheduler is not None
Expand All @@ -175,23 +177,19 @@ def __exit__(self, ty, value, tb):
_scheduler.stop_profiling()
self._stop_ns = nanosecond_time()

# Now that we've collected all the data, attach it to the
# transaction so that it can be sent in the same envelope
self.transaction._profile = self.to_json()

def to_json(self):
# type: () -> Dict[str, Any]
def to_json(self, event_opt):
# type: (Any) -> Dict[str, Any]
assert _sample_buffer is not None
assert self._start_ns is not None
assert self._stop_ns is not None

return {
"environment": None, # Gets added in client.py
"environment": event_opt.get("environment"),
"event_id": uuid.uuid4().hex,
"platform": "python",
"profile": _sample_buffer.slice_profile(self._start_ns, self._stop_ns),
"release": None, # Gets added in client.py
"timestamp": None, # Gets added in client.py
"release": event_opt.get("release", ""),
"timestamp": event_opt["timestamp"],
"version": "1",
"device": {
"architecture": platform.machine(),
Expand All @@ -206,7 +204,7 @@ def to_json(self):
},
"transactions": [
{
"id": None, # Gets added in client.py
"id": event_opt["event_id"],
"name": self.transaction.name,
# we start the transaction before the profile and this is
# the transaction start time relative to the profile, so we
Expand Down Expand Up @@ -304,7 +302,22 @@ def slice_profile(self, start_ns, stop_ns):
sample["stack_id"] = stacks[current_stack]
samples.append(sample)

return {"stacks": stacks_list, "frames": frames_list, "samples": samples}
# This collects the thread metadata at the end of a profile. Doing it
# this way means that any threads that terminate before the profile ends
# will not have any metadata associated with it.
thread_metadata = {
str(thread.ident): {
"name": thread.name,
}
for thread in threading.enumerate()
}

return {
"stacks": stacks_list,
"frames": frames_list,
"samples": samples,
"thread_metadata": thread_metadata,
}


class _Scheduler(object):
Expand Down Expand Up @@ -344,6 +357,7 @@ class _ThreadScheduler(_Scheduler):
"""

mode = "thread"
name = None # type: Optional[str]

def __init__(self, frequency):
# type: (int) -> None
Expand All @@ -368,7 +382,7 @@ def start_profiling(self):
# make sure the thread is a daemon here otherwise this
# can keep the application running after other threads
# have exited
thread = threading.Thread(target=self.run, daemon=True)
thread = threading.Thread(name=self.name, target=self.run, daemon=True)
thread.start()
return True
return False
Expand All @@ -394,6 +408,7 @@ class _SleepScheduler(_ThreadScheduler):
"""

mode = "sleep"
name = "sentry.profiler.SleepScheduler"

def run(self):
# type: () -> None
Expand Down Expand Up @@ -424,6 +439,7 @@ class _EventScheduler(_ThreadScheduler):
"""

mode = "event"
name = "sentry.profiler.EventScheduler"

def run(self):
# type: () -> None
Expand Down
7 changes: 4 additions & 3 deletions sentry_sdk/tracing.py
Expand Up @@ -21,7 +21,8 @@
from typing import Tuple
from typing import Iterator

from sentry_sdk._types import SamplingContext, MeasurementUnit
import sentry_sdk.profiler
from sentry_sdk._types import Event, SamplingContext, MeasurementUnit


# Transaction source
Expand Down Expand Up @@ -579,7 +580,7 @@ def __init__(
self._sentry_tracestate = sentry_tracestate
self._third_party_tracestate = third_party_tracestate
self._measurements = {} # type: Dict[str, Any]
self._profile = None # type: Optional[Dict[str, Any]]
self._profile = None # type: Optional[sentry_sdk.profiler.Profile]
self._baggage = baggage
# for profiling, we want to know on which thread a transaction is started
# to accurately show the active thread in the UI
Expand Down Expand Up @@ -675,7 +676,7 @@ def finish(self, hub=None):
"timestamp": self.timestamp,
"start_timestamp": self.start_timestamp,
"spans": finished_spans,
}
} # type: Event

if hub.client is not None and self._profile is not None:
event["profile"] = self._profile
Expand Down