-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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(suspect-spans): Add new endpoint for suspect spans #29357
Conversation
This introduced a new endpoint for suspect spans.
|
||
self.min_ago = before_now(minutes=1).replace(microsecond=0) | ||
|
||
self.update_snuba_config_ensure({"write_span_columns_projects": f"[{self.project.id}]"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds nearly 10s per test. If there's not an easy way to make this faster in CI, we may want to xfail
these tests for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM;
- small question around moving some of the classes and functions out of the endpoint to make it smaller
- Also I think if these tests do end up taking 10s each, we likely want to skip instead of xfail cause I think pytest still runs the tests
- Also should leave 1 in as a general smoke test IMO
data.get("spans", []), | ||
) | ||
] | ||
) | ||
|
||
return ExampleTransaction( | ||
id=transaction_id, | ||
description=description, | ||
start_timestamp=data["start_timestamp"], | ||
finish_timestamp=data["timestamp"], | ||
non_overlapping_exclusive_time=sum( | ||
window.duration_ms for window in non_overlapping_exclusive_time_windows | ||
), | ||
spans=spans, | ||
) | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class TimeWindow: | ||
# Timestamps are in seconds | ||
start: float | ||
end: float | ||
|
||
def as_tuple(self) -> Tuple[float, float]: | ||
return (self.start, self.end) | ||
|
||
@property | ||
def duration_ms(self) -> float: | ||
return (self.end - self.start) * 1000 | ||
|
||
def __add__(self, other: "TimeWindow") -> Tuple[Optional["TimeWindow"], "TimeWindow"]: | ||
if self.start < other.start: | ||
if self.end < other.start: | ||
return self, other | ||
return None, TimeWindow(start=self.start, end=max(self.end, other.end)) | ||
else: | ||
if self.start > other.end: | ||
return other, self | ||
return None, TimeWindow(start=other.start, end=max(self.end, other.end)) | ||
|
||
def __sub__(self, other: "TimeWindow") -> Tuple[Optional["TimeWindow"], "TimeWindow"]: | ||
if self.start < other.start: | ||
if self.end > other.end: | ||
return ( | ||
TimeWindow(start=self.start, end=other.start), | ||
TimeWindow(start=other.end, end=self.end), | ||
) | ||
return None, TimeWindow(start=self.start, end=min(self.end, other.start)) | ||
else: | ||
if self.end < other.end: | ||
return None, TimeWindow(start=self.end, end=self.end) | ||
return None, TimeWindow(start=max(self.start, other.end), end=self.end) | ||
|
||
|
||
def union_time_windows(time_windows: List[TimeWindow]) -> List[TimeWindow]: | ||
if not time_windows: | ||
return [] | ||
|
||
previous, *time_windows = sorted(time_windows, key=lambda window: window.as_tuple()) | ||
|
||
unioned: List[TimeWindow] = [] | ||
|
||
for current in time_windows: | ||
window, previous = previous + current | ||
if window: | ||
unioned.append(window) | ||
|
||
unioned.append(previous) | ||
|
||
return unioned | ||
|
||
|
||
def remove_time_windows(source: TimeWindow, time_windows: List[TimeWindow]) -> List[TimeWindow]: | ||
if not time_windows: | ||
return [source] | ||
|
||
removed: List[TimeWindow] = [] | ||
|
||
for current in time_windows: | ||
window, source = source - current | ||
if window: | ||
removed.append(window) | ||
|
||
removed.append(source) | ||
|
||
return removed | ||
|
||
|
||
def get_exclusive_time_windows(span: ExampleSpan, spans: List[Any]) -> List[TimeWindow]: | ||
non_overlapping_children_time_windows = union_time_windows( | ||
[ | ||
TimeWindow(start=child["start_timestamp"], end=child["timestamp"]) | ||
for child in spans | ||
if child["parent_span_id"] == span.id | ||
] | ||
) | ||
return remove_time_windows( | ||
TimeWindow(start=span.start_timestamp, end=span.finish_timestamp), | ||
non_overlapping_children_time_windows, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on moving these to utils.py?
# pagination will try to get 1 extra result to decide if there is a next page, | ||
# drop the extra result, if present, to fetch less events | ||
alias = get_function_alias( | ||
SPAN_PERFORMANCE_COLUMNS[orderby_column].suspect_example_column | ||
) | ||
orderby = direction + alias | ||
transaction_ids = query_example_transactions( | ||
params, query, orderby, suspects[: limit - 1] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 won't this break pagination?
If we don't want pagination shouldn't we disable it entirely like we do with the noPagination
param?
Or is this for another reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is done intentionally. Pagination is done by checking the existence of an extra result. What this is doing is skipping the nodestore calls + any calculation done on the event for this extra result. However, when serializing, the extra result will be present but without any example events attached. This way pagination still works as expected but we don't need to make the extra nodestore calls for this extra result.
This introduced a new endpoint for suspect spans.