Source code for rialto.runner.tracker

#  Copyright 2022 ABSA Group Limited
#
#  Licensed 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.

__all__ = ["Record", "Tracker"]

import smtplib
from dataclasses import dataclass
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, Optional

from rialto.runner.config_loader import MailConfig


[docs] @dataclass class Record: """Dataclass with information about one run of one pipeline.""" job: str target: str date: datetime.date time: timedelta records: int status: str reason: str exception: Optional[str] = None
[docs] class Tracker: """Collect information about runs and sent them out via email""" def __init__(self): self.records = [] self.last_error = None self.pipeline_start = datetime.now() self.exceptions = []
[docs] def add(self, record: Record) -> None: """Add record for one run""" self.records.append(record)
[docs] def report(self, mail_cfg: MailConfig): """Create and send html report""" if len(self.records) or mail_cfg.sent_empty: report = HTMLMessage.make_report(self.pipeline_start, self.records) for receiver in mail_cfg.to: message = Mailer.create_message( subject=mail_cfg.subject, sender=mail_cfg.sender, receiver=receiver, body=report ) Mailer.send_mail(mail_cfg.smtp, message)
class HTMLMessage: bck_colors = ["#00ded6", "#acfcfa"] borderless_table = 'role="presentation" style="border:0;border-spacing:0;"' bordered_table = ( 'role="presentation" style="background-repeat:no-repeat; margin:0;" cellpadding="1" cellspacing="1" border="1""' ) @staticmethod def _get_status_color(status: str): if status == "Success": return "#398f00" elif status == "Error": return "#ff0000" else: return "#ff8800" @staticmethod def _make_rows(rows): html = "" data_options = 'align="center"' for row, i in zip(rows, range(len(rows))): r = f""" <tr style="height:40px; margin:0; background-color:{HTMLMessage.bck_colors[i % 2]}"> <td {data_options}>{row.job}</td> <td {data_options}>{row.target.split('.')[0]}.<br> {row.target.split('.')[1]}.<br> {row.target.split('.')[2]} </td> <td {data_options}>{row.date}</td> <td {data_options}>{str(row.time).split(".")[0]}</td> <td {data_options}>{f'{row.records:,}'}</td> <td {data_options} style="color: {HTMLMessage._get_status_color(row.status)};"> <b>{row.status}</b> </td> <td {data_options}>{row.reason}</td> </tr> """ html += r return html @staticmethod def _make_overview_header(): return """ <tr style="height:40px; margin:2px; background-color: #286dd4;"> <th>Job</th> <th>Target</th> <th>Date</th> <th>Time elapsed</th> <th>Rows created</th> <th>Status</th> <th>Reason</th> </tr> """ @staticmethod def _make_header(start: datetime): return f""" <div align="center"> <table {HTMLMessage.borderless_table}> <tr> <td align="center"><h3>This is is Rialto Feature Runner report<h3></td> </tr> <tr> <td align="center"> Jobs started <b>{str(start).split('.')[0]}</b> </td> </tr> </table> </div> """ @staticmethod def _make_overview(records: List[Record]): return f""" <table {HTMLMessage.borderless_table}> <tr> <td><h3>Overview</h3></td> </tr> </table> <table {HTMLMessage.bordered_table}> {HTMLMessage._make_overview_header()} {HTMLMessage._make_rows(records)} </table> """ @staticmethod def _head(): return """ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="x-apple-disable-message-reformatting"> <!--[if !mso]><!--> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <!--<![endif]--> <!--[if mso]> <noscript> <xml> <o:OfficeDocumentSettings> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> </noscript> <![endif]--> </head> <style> .foldingcheckbox { float: left; } .foldingcheckbox:not(:checked) + * { display: none } </style> """ @staticmethod def _body_open(): return """ <body style="margin:0;padding:0;word-spacing:normal"> <div role="article" aria-roledescription="email" lang="en" style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;"> <div class="outer" style="width:100%;"> """ @staticmethod def _body_close(): return """ </div> </div> </body> """ @staticmethod def _make_exceptions(records: List[Record]): html = "" for record, i in zip(records, range(len(records))): if record.exception is not None: r = f""" <table {HTMLMessage.bordered_table}> <tr > <td bgcolor={HTMLMessage.bck_colors[0]}>{record.job}</td> <td bgcolor={HTMLMessage.bck_colors[1]}>{record.date}</td> </tr> </table> <input class="foldingcheckbox" type="checkbox">Expand</input> <div> <table {HTMLMessage.borderless_table}> <tr> <td colspan="2">{record.exception}</td> </tr> </table> </div> """ html += r return html @staticmethod def _make_insights(records: List[Record]): return f""" <table {HTMLMessage.borderless_table}> <tr> <td><h3>Exceptions<h3></td> </tr> </table> {HTMLMessage._make_exceptions(records)} """ @staticmethod def make_report(start: datetime, records: List[Record]) -> str: """Create html email report""" html = [ """<!DOCTYPE html> <html lang="en" xmlns="https://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">""", HTMLMessage._head(), HTMLMessage._body_open(), HTMLMessage._make_header(start), HTMLMessage._make_overview(records), HTMLMessage._make_insights(records), HTMLMessage._body_close(), ] return "\n".join(html) class Mailer: @staticmethod def create_message(subject: str, sender: str, receiver: str, body: str) -> MIMEMultipart: msg = MIMEMultipart() msg["Subject"] = subject msg["From"] = sender msg["To"] = receiver body = MIMEText(body, "html") msg.attach(body) return msg @staticmethod def send_mail(smtp: str, message: MIMEMultipart): s = smtplib.SMTP(host=smtp, port=25) s.sendmail(from_addr=message["From"], to_addrs=message["To"], msg=message.as_string()) s.quit()