-
-
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
ref(metrics): Release health service INGEST-376 #28598
Conversation
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.
I think after basic review this is ready to merge and deploy. I'd rather deploy in small steps instead of later having to merge a large refactor branch
The existing API endpoint /tests/<dataset:dataset>/insert allows writing data to the default entity of each dataset. In the case of metrics, this means that only metrics_sets can be written. This PR introduces an additional endpoint /tests/entities/<entity:entity>/insert, which allows the client to define the target entity explicitly. This is required to run tests in getsentry/sentry#28598.
from sentry.utils.services import Service | ||
|
||
|
||
class ReleaseHealthBackend(Service): # type: ignore |
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.
Why is ReleaseHealthBackend
inheriting from Service
, Service
has two functions that are validate
and setup
, that none of the ReleaseHealthBackend
child classes define so I feel there is no point in this inheritance, and probably makes sense to just remove # type: ignore
here
Also should the ReleaseHealthBackend
class be an abstractclass
?
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.
Service
is a special class which, in combination with LazyServiceWrapper
, lets you expose the methods of the configured implementation as locals in the module. It's widely used in our code base, which is why we chose the same pattern for this abstraction. AFAIK nobody has gotten it to play nice with mypy yet, which is why we need all the #type: ignore
comments.
I tried making this an abc.ABC
initially, but ran into an issue I don't remember right now.
|
||
|
||
def metric_id(org_id: int, name: str) -> int: | ||
index = indexer.resolve(org_id, UseCase.TAG_KEY, name) # type: ignore |
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.
@untitaker This is what I meant by places where # type: ignore
can be avoided. Seems to me that indexer is of type StringIndexer
, and resolve
always returns int
typing the indexer imported should be enough to eliminate a few # type: ignore
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.
indexer
is a module. This is a problem of our lazy service abstraction which does too much magic for mypy to comprehend:
backend.expose(locals()) |
We're certainly not going to type the indexer. That module is under active development by another team.
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.
What I meant by typing the indexer is string_indexer: StringIndexer = indexer
in this class given that this uses an instance of StringIndexer
, no? and StringIndexer.resolve
returns an int
Isn't that a potential way to eliminate this # type: ignore
?
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.
right but I believe we need a more generic solution to the service problem, I played around with this in hackweek and didn't come up with something satisfactory.
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.
Just to be completely clear on this, I do believe some cases do require a # type: ignore
but what I am advocating here is that we investigate prior to falling back on this
Imo type casting the instance of indexer
imported here is a satisfactory alternative to ignoring the type
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.
I understand, but we have investigated solutions to give service methods the correct types, I just don't agree with your suggestion as I don't think the time is well-spent in coming up with usage-specific workarounds (per usage of indexer)
return str_value # type: ignore | ||
|
||
|
||
class MetricsReleaseHealthBackend(ReleaseHealthBackend): |
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.
Nit: naming this just MetricsBackend
should be enough given that it inherits from ReleaseHealthBackend
so probably not necessary to include ReleaseHealth
in every child class
previous_end: datetime, | ||
rollup: int, | ||
org_id: Optional[int] = None, | ||
) -> ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates: |
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.
Why are doing this kind of type inheritance ? I would just move the types outside of the class.. Surely they will be used in other areas of the code that use this function
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 has been fixed in master.
if org_id is None: | ||
org_id = self._get_org_id(project_ids) | ||
|
||
projects_crash_free_rate_dict: ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates = { |
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.
Same here as above comment
|
||
count_query = Query( | ||
dataset=Dataset.Metrics.value, | ||
match=Entity("metrics_counters"), |
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.
Would use an enum for metrics_counter
.. this is error prone
instead
EntityKey.METRICS_COUNTER.value
rollup: int, | ||
org_id: Optional[int] = None, | ||
) -> ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates: | ||
return get_current_and_previous_crash_free_rates( # type: ignore |
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.
Defining a type for get_current_and_previous_crash_free_rates
will prevent us from needing to add # type: ignore
@@ -1779,6 +1789,10 @@ def test_get_current_and_previous_crash_free_rates_with_zero_sessions(self): | |||
} | |||
|
|||
|
|||
class GetCrashFreeRateTestCaseMetrics(ReleaseHealthMetricsTestCase, GetCrashFreeRateTestCase): | |||
"""Repeat tests with metrics backend""" | |||
|
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.
Please Add a ToDo
comment
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.
Deriving from both ReleaseHealthMetricsTestCase
and GetCrashFreeRateTestCase
already repeats the tests, so there is nothing to do here. But I agree that that should be clearer from reading the code.
Provide a common interface for release health queries, so we can easily switch between the "sessions" backend and the metrics backend.
Note: The service class in this PR only features a single release health related function. More functions will be ported in future PRs.
https://getsentry.atlassian.net/browse/INGEST-376
Workflow to port a function:
sentry/snuba/sessions.py
and add the same signature tosentry/releasehealth/base.py
.sentry/releasehealth/sessions.py
which calls the original function.sentry/releasehealth/metrics.py
which retrieves the same information from the metrics dataset.releasehealth.
tests/snuba/sessions/test_sessions.py
and identify the test case corresponding to your function.releasehealth.
Xx
), add a classNote: The metrics-based test will be skipped unless you have a snuba instance with metrics enabled:
devservices
,export SNUBA_SETTINGS=/path/to/settings_metrics.py snuba bootstrap --force snuba api
settings_metrics.py
looks like this: