Skip to content

decoupled alerts from Slack#1266

Merged
IDoneShaveIt merged 25 commits into
masterfrom
ele-1852-decouple-alerts-from-slack
Nov 16, 2023
Merged

decoupled alerts from Slack#1266
IDoneShaveIt merged 25 commits into
masterfrom
ele-1852-decouple-alerts-from-slack

Conversation

@IDoneShaveIt
Copy link
Copy Markdown
Contributor

No description provided.

@linear
Copy link
Copy Markdown

linear Bot commented Nov 7, 2023

@IDoneShaveIt IDoneShaveIt marked this pull request as ready for review November 9, 2023 09:23
logger = get_logger(__name__)


ALERT_TABLES = dict(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't is simpler to just create a dict called TABLE_NAME_TO_RESOURCE_TYPE ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might. I actually just moved this part around 🙂
Checking for a simpler solution!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might. I actually just moved this part around 🙂
Checking for a simpler solution!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the need of that const and improved the logic and moved it into the alerts fetcher (this is actually the only place that should be aware of the tables)

self.sent_alert_count = 0
self.send_test_message_on_success = send_test_message_on_success
self.override_meta_slack_channel = override_config
self.alerts_integraion = self._get_integration_client()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be a class method in BaseIntegration? or somewhere over there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the function that determines which integration we should use.
It can't be part of the integration itself, and once we will add more integrations it should be more dynamic (currently it just returns SlackIntegration)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to think about a way to move all its logic to the integrations area, I will think about it when I integrate PD

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can create a method / class (outside of the BaseIntegration) that determines which integration we should use (base on params / config) and we can call it at the data monitoring.
We will still have a method that calls it inside the data monitoring but all the logic won't be there.

],
) -> Dict[str, Any]:
return dict(
channel=(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this also a little slack-specific? I wonder if we can somehow pass on all the param handling to the integration itself

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that currently we pass the params as part of our dbt package.
If I could I would have changed that, but currently users are relaying on it so its a bit problematic.
I will try to think of a way to move it to the integration itself but I a not sure this is that simple

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the get integration params to be part of the integration itself

filter=self.filter.get_filter(),
)

def _format_alert(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the Pending alert and Alert class are pretty much overlapping... wouldn't it make more senes to have just one class, and if there's additional info or methods you can keep them in a helper class or something?
In any case, if you choose not to do that- I don't love this function, I think it can be nicer to create a to_formatted_alert method in every type of pending alert

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my philosophy of code, those are 2 different class (although they are overlapping).
One (Pending alerts schema) is a schema which is used to validate typing and the structure of the data we query from the db using the alerts fetcher.
This will not be used in the SaaS version because we are going to query it from other tables and the fields probably going to be different.

The Alerts models are the "normalized" schema of alerts that is used by the integrations. This is the data structure we "sign" on that is going to contain all the data an integration need on an alert.

Merging the 2 is not a good option IMO because they have a different purpose and use, they first one is changeable depends on the system, and currently they are overlapping but they can easily be changed and don't 🙂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, in that case I would move this method to the pending alerts classes instead of using ifs and isinstance, I think it's cleaner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will move it 🙂
This is scares me a bit because it can create a couple between the 2 in the future (I really like to separate the 2 - fetcher should not be aware of the API that is calling it, and adding the format to the schema kinda makes it coupled).
So if I will see in the future that we are coupling them I will ask to revert it to that change 🙂

) == json.dumps(["alert_id_2", "alert_id_3", "alert_id_4"])
assert json.dumps(
[alert.id for alert in source_freshness_alerts], sort_keys=True
) == json.dumps(["alert_id_2", "alert_id_3", "alert_id_4"])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we want to check more fields are formatted properly? could be nice to add a verification that nothing is omitted in the formatting

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I don't check here that the fields got formatted properly, but that the right alerts got formatted to the relevant format.
Currently the Formatting is straight forward so I didn't add a test for that explicit

Comment thread tests/unit/monitor/data_monitoring/alerts/test_data_monitoring_alerts.py Outdated
filter=self.filter.get_filter(),
)

def _format_alert(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, in that case I would move this method to the pending alerts classes instead of using ifs and isinstance, I think it's cleaner

@@ -0,0 +1,184 @@
from datetime import datetime
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think pending_alert.py is a more suitable name for this file, no?

Comment thread elementary/utils/dicts.py


# Merge dicts attributes into a single list that contains all the values.
def merge_dicts_attribute(dicts: List[Dict], attribute_key: str) -> List:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def merge_dicts_attribute(dicts: List[Dict], attribute_key: str) -> List:
from itertools import chain
def merge_dicts_attribute(dicts: List[Dict], attribute_key: str) -> List:
attributes = [d.get(attribute_key) for d in dicts]
return list(chain.from_iterable(attributes))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I love python magic so couldn't resist myself, you don't have to use this :)

Copy link
Copy Markdown
Contributor Author

@IDoneShaveIt IDoneShaveIt Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will only work if the attributes are lists but we want to support attributes that are single item (like str, int, dict) and merge them into a list as well 🙂
So in order to make it work we will need to have 2 iterations to convert all to lists or have one big inline iteration with inline if statement inside it that uses the .get method on the same item 3 times:

attributes = [
    d.get(attribute_key, []) if isinsatnce(d.get(attribute_key, []), list) else [d.get(attribute_key)]
    for d
    in dicts
]
return list(chain.from_iterable(attributes))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I checked, it works for both cases actually :)

Comment thread elementary/utils/dicts.py
# Flatten a nested dict by a given key, for example:
# nested_dict = {"top1": "one", "nested": {"top1": "One", "top2": "Two"}}, flatten_by_key = "nested" -> {"top1": "One", "top2": "Two"}
def flatten_dict_by_key(nested_dict: Dict, flatten_by_key: str) -> Dict:
flatten_dict = {**nested_dict, **nested_dict.get(flatten_by_key, {})}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pop also returns the value so you can do:

inner_dict = nested_dict.pop(flatten_by_key, {})
return {**nested_dict, **inner_dict}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but pop on nested_dict which is a variable of the method will pop out the key from the origin dict as well (outside of the method) which can cause some big voodoo 😱
(this is actually breaking the test, because we run the same dict with different scenarios and this is editing the dict outside of the method calls)

We can do:

def flatten_dict_by_key(nested_dict: Dict, flatten_by_key: str) -> Dict:
    nested_dict_copy = {**nested_dict}
    inner_dict = nested_dict_copy.pop(flatten_by_key, {})
    return {**nested_dict_copy, **inner_dict}

Which IMO is quite the same with a different order 🙂

I can change to that if you prefer it though



def test_get_alert_meta_attrs():
base_alert = BasePendingAlertSchema(**{**BASE_ALERT, "model_meta": dict(attr="a")})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use Parametrize here :)

self.sent_alert_count = 0
self.send_test_message_on_success = send_test_message_on_success
self.override_meta_slack_channel = override_config
self.alerts_integraion = self._get_integration_client()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to think about a way to move all its logic to the integrations area, I will think about it when I integrate PD

@IDoneShaveIt IDoneShaveIt merged commit abf61d7 into master Nov 16, 2023
@IDoneShaveIt IDoneShaveIt deleted the ele-1852-decouple-alerts-from-slack branch November 16, 2023 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants