| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| """Models for scheduled execution of jobs""" |
| import json |
| import textwrap |
| from datetime import datetime |
| from typing import Any, Optional |
| |
| from flask_appbuilder import Model |
| from sqlalchemy import ( |
| Boolean, |
| Column, |
| DateTime, |
| Float, |
| ForeignKey, |
| Integer, |
| String, |
| Table, |
| Text, |
| ) |
| from sqlalchemy.ext.declarative import declared_attr |
| from sqlalchemy.orm import backref, relationship, RelationshipProperty |
| |
| from superset import db, security_manager |
| from superset.models.helpers import AuditMixinNullable |
| |
| metadata = Model.metadata # pylint: disable=no-member |
| |
| |
| alert_owner = Table( |
| "alert_owner", |
| metadata, |
| Column("id", Integer, primary_key=True), |
| Column("user_id", Integer, ForeignKey("ab_user.id")), |
| Column("alert_id", Integer, ForeignKey("alerts.id")), |
| ) |
| |
| |
| class Alert(Model, AuditMixinNullable): |
| |
| """Schedules for emailing slices / dashboards""" |
| |
| __tablename__ = "alerts" |
| |
| id = Column(Integer, primary_key=True) |
| label = Column(String(150), nullable=False) |
| active = Column(Boolean, default=True, index=True) |
| # TODO(bkyryliuk): enforce minimal supported frequency |
| crontab = Column(String(50), nullable=False) |
| |
| alert_type = Column(String(50)) |
| owners = relationship(security_manager.user_model, secondary=alert_owner) |
| recipients = Column(Text) |
| slack_channel = Column(Text) |
| |
| # TODO(bkyryliuk): implement log_retention |
| log_retention = Column(Integer, default=90) |
| grace_period = Column(Integer, default=60 * 60 * 24) |
| |
| slice_id = Column(Integer, ForeignKey("slices.id")) |
| slice = relationship("Slice", backref="alerts", foreign_keys=[slice_id]) |
| |
| dashboard_id = Column(Integer, ForeignKey("dashboards.id")) |
| dashboard = relationship("Dashboard", backref="alert", foreign_keys=[dashboard_id]) |
| |
| last_eval_dttm = Column(DateTime, default=datetime.utcnow) |
| last_state = Column(String(10)) |
| |
| # Observation related columns |
| sql = Column(Text, nullable=False) |
| |
| # Validation related columns |
| validator_type = Column(String(100), nullable=False) |
| validator_config = Column( |
| Text, |
| default=textwrap.dedent( |
| """ |
| { |
| |
| } |
| """ |
| ), |
| ) |
| |
| @declared_attr |
| def database_id(self) -> int: |
| return Column(Integer, ForeignKey("dbs.id"), nullable=False) |
| |
| @declared_attr |
| def database(self) -> RelationshipProperty: |
| return relationship( |
| "Database", |
| foreign_keys=[self.database_id], |
| backref=backref("sql_observers", cascade="all, delete-orphan"), |
| ) |
| |
| def get_last_observation(self) -> Optional[Any]: |
| observations = list( |
| db.session.query(SQLObservation) |
| .filter_by(alert_id=self.id) |
| .order_by(SQLObservation.dttm.desc()) |
| .limit(1) |
| ) |
| |
| if observations: |
| return observations[0] |
| |
| return None |
| |
| def __str__(self) -> str: |
| return f"<{self.id}:{self.label}>" |
| |
| @property |
| def pretty_config(self) -> str: |
| """ String representing the comparison that will trigger a validator """ |
| config = json.loads(self.validator_config) |
| |
| if self.validator_type.lower() == "operator": |
| return f"{config['op']} {config['threshold']}" |
| |
| if self.validator_type.lower() == "not null": |
| return "!= Null or 0" |
| |
| return "" |
| |
| |
| class AlertLog(Model): |
| """Keeps track of alert-related operations""" |
| |
| __tablename__ = "alert_logs" |
| |
| id = Column(Integer, primary_key=True) |
| scheduled_dttm = Column(DateTime) |
| dttm_start = Column(DateTime, default=datetime.utcnow) |
| dttm_end = Column(DateTime, default=datetime.utcnow) |
| alert_id = Column(Integer, ForeignKey("alerts.id")) |
| alert = relationship("Alert", backref="logs", foreign_keys=[alert_id]) |
| state = Column(String(10)) |
| |
| @property |
| def duration(self) -> int: |
| return (self.dttm_end - self.dttm_start).total_seconds() |
| |
| |
| # TODO: Currently SQLObservation table will constantly grow with no limit, |
| # add some retention restriction or more to a more scalable db e.g. |
| # https://github.com/apache/superset/blob/master/superset/utils/log.py#L32 |
| class SQLObservation(Model): # pylint: disable=too-few-public-methods |
| """Keeps track of the collected observations for alerts.""" |
| |
| __tablename__ = "sql_observations" |
| |
| id = Column(Integer, primary_key=True) |
| dttm = Column(DateTime, default=datetime.utcnow, index=True) |
| alert_id = Column(Integer, ForeignKey("alerts.id")) |
| alert = relationship( |
| "Alert", |
| foreign_keys=[alert_id], |
| backref=backref("observations", cascade="all, delete-orphan"), |
| ) |
| value = Column(Float) |
| error_msg = Column(String(500)) |