-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Enable queries using project slug as filter and groupby in Metrics API #69111
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
Merged
shellmayr
merged 24 commits into
master
from
shellmayr/feat/project-to-project-id-draft-2
Apr 18, 2024
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
956b9b3
Add visitor to replace project-filter by project-id-filter in query
shellmayr 2fbf999
Add visitor to replace groupby:project by groupby:project_id
shellmayr ab65551
wip: generalize
shellmayr d53d659
wip: implement first running version of modulator-visitor based on fl…
shellmayr 0518514
add query post-processing steps
shellmayr 08caa89
wip: fix bugs & restructure
shellmayr 5034dbb
wip: everything works before cleanup
shellmayr 164836f
wip: cleanup
shellmayr a8e14a1
wip: remove ModulationMetadata - not needed
shellmayr 7b830af
wip: move things around
shellmayr bc5a402
wip: inject projects directly into the visitor that needs it instead …
shellmayr 2f9f9c8
wip: move groupby modulation into function for ModulatorVisitor
shellmayr a8d757f
add tests & refactor
shellmayr 6a1e9e1
wip: fix mypy errors
shellmayr 0a8142d
add more tests & refactor
shellmayr a414b9a
remove comment
shellmayr 008d7c6
refactor into optimized object structure to reduce shared state
shellmayr 13e6e5c
refactor naming
shellmayr 941dafd
remove find_modulator
shellmayr 2a20d60
rename QueryDemodulationStep
shellmayr c70c225
wip: work review comments into PR
shellmayr 4bf4cc7
wip: add test for groupby:project and filter:project_id
shellmayr 7f3e47f
last adaptations to pacify mypy
shellmayr 0146d86
Merge branch 'master' into shellmayr/feat/project-to-project-id-draft-2
shellmayr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import abc | ||
| from collections.abc import Sequence | ||
| from typing import Any, TypeVar | ||
|
|
||
| from sentry.models.project import Project | ||
|
|
||
|
|
||
| class Mapper(abc.ABC): | ||
| from_key: str = "" | ||
| to_key: str = "" | ||
| applied_on_groupby: bool = False | ||
|
|
||
| def __init__(self): | ||
| # This exists to satisfy mypy, which complains otherwise | ||
| self.map: dict[Any, Any] = {} | ||
shellmayr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __hash__(self): | ||
| return hash((self.from_key, self.to_key)) | ||
|
|
||
| @abc.abstractmethod | ||
| def forward(self, projects: Sequence[Project], value: Any) -> Any: | ||
| return value | ||
|
|
||
| @abc.abstractmethod | ||
| def backward(self, projects: Sequence[Project], value: Any) -> Any: | ||
| return value | ||
|
|
||
|
|
||
| TMapper = TypeVar("TMapper", bound=Mapper) | ||
|
|
||
|
|
||
| class MapperConfig: | ||
| def __init__(self): | ||
| self.mappers: set[type[Mapper]] = set() | ||
|
|
||
| def add(self, mapper: type[Mapper]) -> "MapperConfig": | ||
| self.mappers.add(mapper) | ||
| return self | ||
|
|
||
| def get(self, from_key: str | None = None, to_key: str | None = None) -> type[Mapper] | None: | ||
| for mapper in self.mappers: | ||
| if mapper.from_key == from_key: | ||
| return mapper | ||
| if mapper.to_key == to_key: | ||
| return mapper | ||
| return None | ||
|
|
||
|
|
||
| def get_or_create_mapper( | ||
| mapper_config: MapperConfig, | ||
| mappers: list[Mapper], | ||
| from_key: str | None = None, | ||
| to_key: str | None = None, | ||
| ) -> Mapper | None: | ||
| # retrieve the mapper type that is applicable for the given key | ||
| mapper_class = mapper_config.get(from_key=from_key, to_key=to_key) | ||
| # check if a mapper of the type already exists | ||
| if mapper_class: | ||
| for mapper in mappers: | ||
| if mapper_class == type(mapper): | ||
| # if a mapper already exists, return the existing mapper | ||
| return mapper | ||
| else: | ||
| # if no mapper exists yet, instantiate the object and append it to the mappers list | ||
| mapper_instance = mapper_class() | ||
| mappers.append(mapper_instance) | ||
| return mapper_instance | ||
| else: | ||
| # if no mapper is configured for the key, return None | ||
| return None | ||
|
|
||
|
|
||
| class Project2ProjectIDMapper(Mapper): | ||
| from_key: str = "project" | ||
| to_key: str = "project_id" | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
|
|
||
| def forward(self, projects: Sequence[Project], value: str) -> int: | ||
| if value not in self.map: | ||
| self.map[value] = None | ||
| for project in projects: | ||
| if project.slug == value: | ||
| self.map[value] = project.id | ||
| return self.map[value] | ||
|
|
||
| def backward(self, projects: Sequence[Project], value: int) -> str: | ||
| if value not in self.map: | ||
| for project in projects: | ||
| if project.id == value: | ||
| self.map[value] = project.slug | ||
|
|
||
| return self.map[value] | ||
Empty file.
37 changes: 37 additions & 0 deletions
37
src/sentry/sentry_metrics/querying/data/postprocessing/base.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from abc import ABC, abstractmethod | ||
|
|
||
| from sentry.sentry_metrics.querying.data.execution import QueryResult | ||
|
|
||
|
|
||
| class PostProcessingStep(ABC): | ||
| """ | ||
| Represents an abstract step that post-processes a collection of QueryResult objects. | ||
|
|
||
| The post-processing of these objects might include transforming them or just obtaining some intermediate data that | ||
| is useful to compute other things before returning the results. | ||
| """ | ||
|
|
||
| @abstractmethod | ||
| def run(self, query_results: list[QueryResult]) -> list[QueryResult]: | ||
| """ | ||
| Runs the post-processing steps on a list of query results. | ||
|
|
||
| Returns: | ||
| A list of post-processed query results. | ||
| """ | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| def run_post_processing_steps(query_results: list[QueryResult], *steps) -> list[QueryResult]: | ||
| """ | ||
| Takes a series of query results and steps and runs the post-processing steps one after each other in order they are | ||
| supplied in. | ||
|
|
||
| Returns: | ||
| A list of query results after running the post-processing steps. | ||
| """ | ||
| for step in steps: | ||
| if isinstance(step, PostProcessingStep): | ||
| query_results = step.run(query_results=query_results) | ||
|
|
||
| return query_results |
54 changes: 54 additions & 0 deletions
54
src/sentry/sentry_metrics/querying/data/postprocessing/remapping.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| from collections.abc import Mapping, Sequence | ||
| from copy import deepcopy | ||
| from typing import Any, cast | ||
|
|
||
| from sentry.models.project import Project | ||
| from sentry.sentry_metrics.querying.data.execution import QueryResult | ||
| from sentry.sentry_metrics.querying.data.mapping.mapper import Mapper | ||
| from sentry.sentry_metrics.querying.data.postprocessing.base import PostProcessingStep | ||
|
|
||
|
|
||
| class QueryRemappingStep(PostProcessingStep): | ||
| def __init__(self, projects: Sequence[Project]): | ||
| self.projects = projects | ||
|
|
||
| def run(self, query_results: list[QueryResult]) -> list[QueryResult]: | ||
| for query_result in query_results: | ||
| if ( | ||
| query_result.totals is not None | ||
| and query_result.totals_query is not None | ||
| and len(query_result.totals) > 0 | ||
| ): | ||
| query_result.totals = self._unmap_data( | ||
| query_result.totals, query_result.totals_query.mappers | ||
| ) | ||
| if ( | ||
| query_result.series is not None | ||
| and query_result.series_query is not None | ||
| and len(query_result.series) > 0 | ||
| ): | ||
| query_result.series = self._unmap_data( | ||
| query_result.series, query_result.series_query.mappers | ||
| ) | ||
|
|
||
| return query_results | ||
|
|
||
| def _unmap_data( | ||
| self, data: Sequence[Mapping[str, Any]], mappers: list[Mapper] | ||
| ) -> Sequence[Mapping[str, Any]]: | ||
| unmapped_data: list[dict[str, Any]] = cast(list[dict[str, Any]], deepcopy(data)) | ||
| for element in unmapped_data: | ||
| updated_element = dict() | ||
| keys_to_delete = [] | ||
| for result_key in element.keys(): | ||
| for mapper in mappers: | ||
| if mapper.to_key == result_key and mapper.applied_on_groupby: | ||
| original_value = mapper.backward(self.projects, element[result_key]) | ||
| updated_element[mapper.from_key] = original_value | ||
| keys_to_delete.append(result_key) | ||
|
|
||
| for key in keys_to_delete: | ||
| del element[key] | ||
| element.update(updated_element) | ||
|
|
||
| return cast(Sequence[Mapping[str, Any]], unmapped_data) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/sentry/sentry_metrics/querying/data/preparation/mapping.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| from collections.abc import Sequence | ||
| from dataclasses import replace | ||
|
|
||
| from sentry.models.project import Project | ||
| from sentry.sentry_metrics.querying.data.mapping.mapper import MapperConfig | ||
| from sentry.sentry_metrics.querying.data.preparation.base import IntermediateQuery, PreparationStep | ||
| from sentry.sentry_metrics.querying.visitors.query_expression import MapperVisitor | ||
|
|
||
|
|
||
| class QueryMappingStep(PreparationStep): | ||
| def __init__(self, projects: Sequence[Project], mapper_config: MapperConfig): | ||
| self.projects = projects | ||
| self.mapper_config = mapper_config | ||
|
|
||
| def _get_mapped_intermediate_query( | ||
| self, intermediate_query: IntermediateQuery | ||
| ) -> IntermediateQuery: | ||
| visitor = MapperVisitor(self.projects, self.mapper_config) | ||
| mapped_query = visitor.visit(intermediate_query.metrics_query.query) | ||
|
|
||
| return replace( | ||
| intermediate_query, | ||
| metrics_query=intermediate_query.metrics_query.set_query(mapped_query), | ||
| mappers=visitor.mappers, | ||
| ) | ||
|
|
||
| def run(self, intermediate_queries: list[IntermediateQuery]) -> list[IntermediateQuery]: | ||
| mapped_intermediate_queries = [] | ||
|
|
||
| for intermediate_query in intermediate_queries: | ||
| mapped_intermediate_queries.append( | ||
| self._get_mapped_intermediate_query(intermediate_query) | ||
| ) | ||
|
|
||
| return mapped_intermediate_queries |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.