diff --git a/src/sentry/api/endpoints/organization_events.py b/src/sentry/api/endpoints/organization_events.py index 556e2056ac9642..52bd33362926ce 100644 --- a/src/sentry/api/endpoints/organization_events.py +++ b/src/sentry/api/endpoints/organization_events.py @@ -40,6 +40,7 @@ ) from sentry.snuba.metrics.extraction import MetricSpecType from sentry.snuba.ourlogs import OurLogs +from sentry.snuba.profile_functions import ProfileFunctions from sentry.snuba.referrer import Referrer, is_valid_referrer from sentry.snuba.spans_rpc import Spans from sentry.snuba.trace_metrics import TraceMetrics @@ -532,6 +533,13 @@ def get_rpc_config(): auto_fields=True, disable_aggregate_extrapolation=disable_aggregate_extrapolation, ) + elif scoped_dataset == ProfileFunctions: + # profile_functions uses aggregate conditions + return SearchResolverConfig( + use_aggregate_conditions=use_aggregate_conditions, + auto_fields=True, + disable_aggregate_extrapolation=disable_aggregate_extrapolation, + ) elif scoped_dataset == uptime_results.UptimeResults: return SearchResolverConfig( use_aggregate_conditions=use_aggregate_conditions, @@ -600,7 +608,7 @@ def data_fn(offset, limit): paginator, cursor_cls = paginator_factory(dataset) - max_per_page = 9999 if dataset in (OurLogs, TraceMetrics) else None + max_per_page = 9999 if dataset in (OurLogs, TraceMetrics, ProfileFunctions) else None def _handle_results(results): # Apply error upsampling for regular Events API diff --git a/src/sentry/api/endpoints/organization_events_stats.py b/src/sentry/api/endpoints/organization_events_stats.py index e1286042e5738e..ca2f0aa98f8e93 100644 --- a/src/sentry/api/endpoints/organization_events_stats.py +++ b/src/sentry/api/endpoints/organization_events_stats.py @@ -33,6 +33,7 @@ ) from sentry.snuba.metrics.extraction import MetricSpecType from sentry.snuba.ourlogs import OurLogs +from sentry.snuba.profile_functions import ProfileFunctions from sentry.snuba.query_sources import QuerySource from sentry.snuba.referrer import Referrer, is_valid_referrer from sentry.snuba.spans_rpc import Spans @@ -192,6 +193,7 @@ def get(self, request: Request, organization: Organization) -> Response: spans_metrics, Spans, OurLogs, + ProfileFunctions, TraceMetrics, errors, transactions, diff --git a/src/sentry/replays/lib/eap/snuba_transpiler.py b/src/sentry/replays/lib/eap/snuba_transpiler.py index 5335f4ae61da42..6631e7c71e97cf 100644 --- a/src/sentry/replays/lib/eap/snuba_transpiler.py +++ b/src/sentry/replays/lib/eap/snuba_transpiler.py @@ -190,9 +190,12 @@ "uptime_result": TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT, "replay": TraceItemType.TRACE_ITEM_TYPE_REPLAY, "metric": TraceItemType.TRACE_ITEM_TYPE_METRIC, + "profile_functions": TraceItemType.TRACE_ITEM_TYPE_PROFILE_FUNCTION, } -TRACE_ITEM_TYPES = TLiteral["span", "error", "log", "uptime_result", "replay", "metric"] # noqa +TRACE_ITEM_TYPES = TLiteral[ + "span", "error", "log", "uptime_result", "replay", "metric", "profile_functions" # noqa +] class RequestMeta(TypedDict): diff --git a/src/sentry/search/eap/constants.py b/src/sentry/search/eap/constants.py index 665e29694e20dd..264977ef408222 100644 --- a/src/sentry/search/eap/constants.py +++ b/src/sentry/search/eap/constants.py @@ -16,6 +16,7 @@ SupportedTraceItemType.SPANS: TraceItemType.TRACE_ITEM_TYPE_SPAN, SupportedTraceItemType.UPTIME_RESULTS: TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT, SupportedTraceItemType.TRACEMETRICS: TraceItemType.TRACE_ITEM_TYPE_METRIC, + SupportedTraceItemType.PROFILE_FUNCTIONS: TraceItemType.TRACE_ITEM_TYPE_PROFILE_FUNCTION, } OPERATOR_MAP = { diff --git a/src/sentry/search/eap/profile_functions/aggregates.py b/src/sentry/search/eap/profile_functions/aggregates.py new file mode 100644 index 00000000000000..d24da55a7bc993 --- /dev/null +++ b/src/sentry/search/eap/profile_functions/aggregates.py @@ -0,0 +1,5 @@ +from sentry.search.eap.ourlogs.aggregates import LOG_AGGREGATE_DEFINITIONS + +# For now, profile functions uses the same aggregates as logs +# This can be extended in the future with trace metrics specific aggregates +PROFILE_FUNCTIONS_AGGREGATE_DEFINITIONS = LOG_AGGREGATE_DEFINITIONS diff --git a/src/sentry/search/eap/profile_functions/attributes.py b/src/sentry/search/eap/profile_functions/attributes.py new file mode 100644 index 00000000000000..36445a37668ab5 --- /dev/null +++ b/src/sentry/search/eap/profile_functions/attributes.py @@ -0,0 +1,198 @@ +from typing import Literal + +from sentry.search.eap import constants +from sentry.search.eap.columns import ( + ResolvedAttribute, + VirtualColumnDefinition, + project_context_constructor, + project_term_resolver, +) +from sentry.search.eap.common_columns import COMMON_COLUMNS +from sentry.utils.validators import is_event_id_or_list + +PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS = { + column.public_alias: column + for column in COMMON_COLUMNS + + [ + ResolvedAttribute( + public_alias="id", + internal_name="sentry.item_id", + search_type="string", + validator=is_event_id_or_list, + ), + ResolvedAttribute( + public_alias=constants.TRACE_ALIAS, + internal_name="sentry.trace_id", + search_type="string", + validator=is_event_id_or_list, + ), + ResolvedAttribute( + public_alias="environment", + internal_name="environment", + search_type="string", + ), + ResolvedAttribute( + public_alias="release", + internal_name="release", + search_type="string", + ), + ResolvedAttribute( + public_alias="transaction", + internal_name="transaction_name", + search_type="string", + ), + ResolvedAttribute( + public_alias="function", + internal_name="name", + search_type="string", + ), + ResolvedAttribute( + public_alias="_fingerprint", + internal_name="fingerprint", + search_type="integer", + ), + ResolvedAttribute( + public_alias="stack_fingerprint", + internal_name="stack_fingerprint", + search_type="integer", + ), + ResolvedAttribute( + public_alias="parent_fingerprint", + internal_name="parent_fingerprint", + search_type="integer", + ), + # will contain profile_id for tnx profiles and profiler_id for continuous profiles + ResolvedAttribute( + public_alias="profile_id", + internal_name="profile_id", + search_type="string", + ), + # enum ("transaction" | "continuous") + ResolvedAttribute( + public_alias="profiling_type", + internal_name="profiling_type", + search_type="string", + ), + ResolvedAttribute( + public_alias="function", + internal_name="name", + search_type="string", + ), + ResolvedAttribute( + public_alias="package", + internal_name="package", + search_type="string", + ), + ResolvedAttribute( + public_alias="is_application", + internal_name="is_application", + search_type="string", + ), + ResolvedAttribute( + public_alias="platform.name", + internal_name="platform", + search_type="string", + ), + ResolvedAttribute( + public_alias="function.self_duration", + internal_name="self_time_ns", + search_type="integer", + ), + ResolvedAttribute( + public_alias="depth", + internal_name="depth", + search_type="integer", + ), + ResolvedAttribute( + public_alias="function.total_duration", + internal_name="total_time_ns", + search_type="integer", + ), + ResolvedAttribute( + public_alias="thread_id", + internal_name="thread_id", + search_type="string", + ), + # only for continuous profiles + ResolvedAttribute( + public_alias="start_timestamp", + internal_name="start_timestamp", + search_type="number", + ), + # only for continuous profiles + ResolvedAttribute( + public_alias="end_timestamp", + internal_name="end_timestamp", + search_type="number", + ), + ] +} + + +# Ensure that required fields are defined at runtime +for field in {constants.TIMESTAMP_ALIAS, constants.TRACE_ALIAS}: + assert ( + field in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS + ), f"{field} must be defined for profile functions" + +PROFILE_FUNCTIONS_VIRTUAL_CONTEXTS = { + key: VirtualColumnDefinition( + constructor=project_context_constructor(key), + term_resolver=project_term_resolver, + filter_column="project.id", + ) + for key in constants.PROJECT_FIELDS +} + +PROFILE_FUNCTIONS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS: dict[ + Literal["string", "number"], dict[str, str] +] = { + "string": { + definition.internal_name: definition.public_alias + for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values() + if not definition.secondary_alias and definition.search_type == "string" + } + | { + # sentry.service is the project id as a string, but map to project for convenience + "sentry.service": "project", + }, + "number": { + definition.internal_name: definition.public_alias + for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values() + if not definition.secondary_alias and definition.search_type != "string" + }, +} + +PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTES: set[str] = { + definition.internal_name + for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values() + if definition.private +} + +# For dynamic internal attributes (eg. meta information for attributes) we match by the beginning of the key. +PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTE_PREFIXES: set[str] = {constants.META_PREFIX} + +PROFILE_FUNCTIONS_REPLACEMENT_ATTRIBUTES: set[str] = { + definition.replacement + for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values() + if definition.replacement +} + +PROFILE_FUNCTIONS_REPLACEMENT_MAP: dict[str, str] = { + definition.public_alias: definition.replacement + for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values() + if definition.replacement +} +PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING: dict[str, set[str]] = {} + +for definition in PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS.values(): + if not definition.secondary_alias: + continue + + secondary_aliases = PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING.get( + definition.internal_name, set() + ) + secondary_aliases.add(definition.public_alias) + PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING[definition.internal_name] = ( + secondary_aliases + ) diff --git a/src/sentry/search/eap/profile_functions/definitions.py b/src/sentry/search/eap/profile_functions/definitions.py new file mode 100644 index 00000000000000..ca60342183f4f3 --- /dev/null +++ b/src/sentry/search/eap/profile_functions/definitions.py @@ -0,0 +1,20 @@ +from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType + +from sentry.search.eap.columns import ColumnDefinitions +from sentry.search.eap.profile_functions.aggregates import PROFILE_FUNCTIONS_AGGREGATE_DEFINITIONS +from sentry.search.eap.profile_functions.attributes import ( + PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS, + PROFILE_FUNCTIONS_VIRTUAL_CONTEXTS, +) + +PROFILE_FUNCTIONS_DEFINITIONS = ColumnDefinitions( + aggregates=PROFILE_FUNCTIONS_AGGREGATE_DEFINITIONS, + conditional_aggregates={}, + formulas={}, + columns=PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS, + contexts=PROFILE_FUNCTIONS_VIRTUAL_CONTEXTS, + trace_item_type=TraceItemType.TRACE_ITEM_TYPE_PROFILE_FUNCTION, + filter_aliases={}, + column_to_alias=None, + alias_to_column=None, +) diff --git a/src/sentry/search/eap/types.py b/src/sentry/search/eap/types.py index 7934034799e9fd..763c65eebf4480 100644 --- a/src/sentry/search/eap/types.py +++ b/src/sentry/search/eap/types.py @@ -45,6 +45,7 @@ class SupportedTraceItemType(str, Enum): SPANS = "spans" UPTIME_RESULTS = "uptime_results" TRACEMETRICS = "tracemetrics" + PROFILE_FUNCTIONS = "profile_functions" class AttributeSourceType(str, Enum): diff --git a/src/sentry/search/eap/utils.py b/src/sentry/search/eap/utils.py index 42a55f4a33d2ae..9f890f4329f452 100644 --- a/src/sentry/search/eap/utils.py +++ b/src/sentry/search/eap/utils.py @@ -16,6 +16,16 @@ OURLOG_ATTRIBUTE_DEFINITIONS, ) from sentry.search.eap.ourlogs.definitions import OURLOG_DEFINITIONS +from sentry.search.eap.profile_functions.attributes import ( + PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS, + PROFILE_FUNCTIONS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS, + PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, + PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTE_PREFIXES, + PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTES, + PROFILE_FUNCTIONS_REPLACEMENT_ATTRIBUTES, + PROFILE_FUNCTIONS_REPLACEMENT_MAP, +) +from sentry.search.eap.profile_functions.definitions import PROFILE_FUNCTIONS_DEFINITIONS from sentry.search.eap.spans.attributes import ( SPAN_ATTRIBUTE_DEFINITIONS, SPAN_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, @@ -58,12 +68,14 @@ def add_start_end_conditions( SupportedTraceItemType.SPANS: SPANS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS, SupportedTraceItemType.LOGS: LOGS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS, } PUBLIC_ALIAS_TO_INTERNAL_MAPPING: dict[SupportedTraceItemType, dict[str, ResolvedAttribute]] = { SupportedTraceItemType.SPANS: SPAN_ATTRIBUTE_DEFINITIONS, SupportedTraceItemType.LOGS: OURLOG_ATTRIBUTE_DEFINITIONS, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_ATTRIBUTE_DEFINITIONS, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS, } @@ -71,24 +83,28 @@ def add_start_end_conditions( SupportedTraceItemType.SPANS: SPANS_PRIVATE_ATTRIBUTES, SupportedTraceItemType.LOGS: LOGS_PRIVATE_ATTRIBUTES, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_PRIVATE_ATTRIBUTES, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTES, } PRIVATE_ATTRIBUTE_PREFIXES: dict[SupportedTraceItemType, set[str]] = { SupportedTraceItemType.SPANS: SPANS_PRIVATE_ATTRIBUTE_PREFIXES, SupportedTraceItemType.LOGS: LOGS_PRIVATE_ATTRIBUTE_PREFIXES, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_PRIVATE_ATTRIBUTE_PREFIXES, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTE_PREFIXES, } SENTRY_CONVENTIONS_REPLACEMENT_ATTRIBUTES: dict[SupportedTraceItemType, set[str]] = { SupportedTraceItemType.SPANS: SPANS_REPLACEMENT_ATTRIBUTES, SupportedTraceItemType.LOGS: LOGS_REPLACEMENT_ATTRIBUTES, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_REPLACEMENT_ATTRIBUTES, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_REPLACEMENT_ATTRIBUTES, } SENTRY_CONVENTIONS_REPLACEMENT_MAPPINGS: dict[SupportedTraceItemType, dict[str, str]] = { SupportedTraceItemType.SPANS: SPANS_REPLACEMENT_MAP, SupportedTraceItemType.LOGS: LOGS_REPLACEMENT_MAP, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_REPLACEMENT_MAP, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_REPLACEMENT_MAP, } @@ -96,12 +112,14 @@ def add_start_end_conditions( SupportedTraceItemType.SPANS: SPAN_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, SupportedTraceItemType.LOGS: LOGS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, } TRACE_ITEM_TYPE_DEFINITIONS: dict[SupportedTraceItemType, ColumnDefinitions] = { SupportedTraceItemType.SPANS: SPAN_DEFINITIONS, SupportedTraceItemType.LOGS: OURLOG_DEFINITIONS, SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_DEFINITIONS, + SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_DEFINITIONS, } diff --git a/src/sentry/snuba/profile_functions.py b/src/sentry/snuba/profile_functions.py new file mode 100644 index 00000000000000..d7f7ba99ead307 --- /dev/null +++ b/src/sentry/snuba/profile_functions.py @@ -0,0 +1,121 @@ +import logging +from datetime import timedelta + +import sentry_sdk +from sentry_protos.snuba.v1.request_common_pb2 import PageToken + +from sentry.search.eap.profile_functions.definitions import PROFILE_FUNCTIONS_DEFINITIONS +from sentry.search.eap.resolver import SearchResolver +from sentry.search.eap.sampling import events_meta_from_rpc_request_meta +from sentry.search.eap.types import AdditionalQueries, EAPResponse, SearchResolverConfig +from sentry.search.events.types import SAMPLING_MODES, EventsMeta, SnubaParams +from sentry.snuba import rpc_dataset_common +from sentry.snuba.discover import zerofill +from sentry.utils import snuba_rpc +from sentry.utils.snuba import SnubaTSResult + +logger = logging.getLogger("sentry.snuba.profile_functions") + + +class ProfileFunctions(rpc_dataset_common.RPCBase): + + DEFINITIONS = PROFILE_FUNCTIONS_DEFINITIONS + + @classmethod + @sentry_sdk.trace + def run_table_query( + cls, + *, + params: SnubaParams, + query_string: str, + selected_columns: list[str], + orderby: list[str] | None, + offset: int, + limit: int, + referrer: str, + config: SearchResolverConfig, + sampling_mode: SAMPLING_MODES | None = None, + equations: list[str] | None = None, + search_resolver: SearchResolver | None = None, + page_token: PageToken | None = None, + debug: bool = False, + additional_queries: AdditionalQueries | None = None, + ) -> EAPResponse: + return cls._run_table_query( + rpc_dataset_common.TableQuery( + query_string=query_string, + selected_columns=selected_columns, + orderby=orderby, + offset=offset, + limit=limit, + referrer=referrer, + sampling_mode=sampling_mode, + resolver=search_resolver + or cls.get_resolver( + params=params, + config=config, + ), + page_token=page_token, + additional_queries=additional_queries, + ), + debug=debug, + ) + + @classmethod + @sentry_sdk.trace + def run_timeseries_query( + cls, + *, + params: SnubaParams, + query_string: str, + y_axes: list[str], + referrer: str, + config: SearchResolverConfig, + sampling_mode: SAMPLING_MODES | None, + comparison_delta: timedelta | None = None, + ) -> SnubaTSResult: + cls.validate_granularity(params) + search_resolver = cls.get_resolver(params, config) + rpc_request, aggregates, groupbys = cls.get_timeseries_query( + search_resolver=search_resolver, + params=params, + query_string=query_string, + y_axes=y_axes, + groupby=[], + referrer=referrer, + sampling_mode=sampling_mode, + ) + + """Run the query""" + rpc_response = snuba_rpc.timeseries_rpc([rpc_request])[0] + + """Process the results""" + result = rpc_dataset_common.ProcessedTimeseries() + final_meta: EventsMeta = events_meta_from_rpc_request_meta(rpc_response.meta) + for resolved_field in aggregates + groupbys: + final_meta["fields"][resolved_field.public_alias] = resolved_field.search_type + + for timeseries in rpc_response.result_timeseries: + processed = cls.process_timeseries_list([timeseries]) + if len(result.timeseries) == 0: + result = processed + else: + for attr in ["timeseries", "confidence", "sample_count", "sampling_rate"]: + for existing, new in zip(getattr(result, attr), getattr(processed, attr)): + existing.update(new) + if len(result.timeseries) == 0: + # The rpc only zerofills for us when there are results, if there aren't any we have to do it ourselves + result.timeseries = zerofill( + [], + params.start_date, + params.end_date, + params.timeseries_granularity_secs, + ["time"], + ) + + return SnubaTSResult( + {"data": result.timeseries, "processed_timeseries": result, "meta": final_meta}, + params.start, + params.end, + params.granularity_secs, + ) diff --git a/src/sentry/snuba/utils.py b/src/sentry/snuba/utils.py index 877d6e3d96d6de..6449bb9f1cc336 100644 --- a/src/sentry/snuba/utils.py +++ b/src/sentry/snuba/utils.py @@ -17,6 +17,7 @@ ) from sentry.snuba.models import QuerySubscription, SnubaQuery from sentry.snuba.ourlogs import OurLogs +from sentry.snuba.profile_functions import ProfileFunctions from sentry.snuba.spans_rpc import Spans from sentry.snuba.trace_metrics import TraceMetrics from sentry.snuba.uptime_results import UptimeResults @@ -37,6 +38,7 @@ "profiles": profiles, "issuePlatform": issue_platform, "profileFunctions": functions, + "profile_functions": ProfileFunctions, "spans": Spans, "spansIndexed": spans_indexed, "spansMetrics": spans_metrics, @@ -45,6 +47,7 @@ } DEPRECATED_LABELS = {"ourlogs"} RPC_DATASETS = { + ProfileFunctions, Spans, TraceMetrics, OurLogs,