-
Notifications
You must be signed in to change notification settings - Fork 0
/
exception_email_client.py
210 lines (177 loc) · 7.61 KB
/
exception_email_client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import json
import os
import smtplib
import ssl
import email.mime.text
import email.mime.multipart
import traceback
from packages.core import types
_dir = os.path.dirname
_PROJECT_DIR = _dir(_dir(_dir(_dir(os.path.abspath(__file__)))))
_PRE_CODE_TAG = '<pre style="background-color: #f1f5f9; color: #334155; padding: 8px 8px 12px 12px; border-radius: 3px; overflow-x: scroll;"><code style="white-space: pre;">'
_POST_CODE_TAG = "</code></pre>"
def _get_pyra_version() -> str:
"""Get the current PYRA version from the UI's package.json file"""
with open(
os.path.join(_PROJECT_DIR, "packages", "ui", "package.json")
) as f:
pyra_version: str = json.load(f)["version"]
assert pyra_version.startswith("4.")
return pyra_version
def _get_current_log_lines() -> list[str]:
"""Get the log line from the current info.log file. Only
returns the log lines from the latest two iterations."""
try:
with open(f"{_PROJECT_DIR}/logs/debug.log") as f:
latest_log_lines = f.readlines()
except FileNotFoundError:
return []
log_lines_in_email: list[str] = []
included_iterations = 0
for l in latest_log_lines[::-1][: 50]:
if ("main - INFO - Starting iteration"
in l) or ("main - INFO - Starting mainloop" in l):
included_iterations += 1
if 'running command "config update" with content:' in l:
l_sections = l.split(
"running command \"config update\" with content:"
)
log_lines_in_email.append(
l_sections[0] +
"running command \"config update\" with content: { REDACTED }\n"
)
else:
log_lines_in_email.append(l)
if included_iterations == 2:
break
return log_lines_in_email[::-1]
class ExceptionEmailClient:
"""Provide functionality to send emails when an exception
occurs/is resolved."""
@staticmethod
def _send_email(
config: types.Config,
text: str,
html: str,
subject: str,
) -> None:
smtp_username = config.error_email.smtp_username
smtp_password = config.error_email.smtp_password
sender_email = config.error_email.sender_address
recipients = config.error_email.recipients.replace(" ", "").split(",")
message = email.mime.multipart.MIMEMultipart("alternative")
message["Subject"] = subject
message["From"] = f"PYRA Technical User <{sender_email}>"
message["To"] = ", ".join(recipients)
# The email client will try to render the last part first
message.attach(email.mime.text.MIMEText(text, "plain"))
message.attach(email.mime.text.MIMEText(html, "html"))
# Create secure connection with server and send email
context = ssl.create_default_context()
if config.error_email.smtp_port == 587:
session = smtplib.SMTP(
config.error_email.smtp_host, config.error_email.smtp_port
)
session.ehlo()
session.starttls()
session.login(
config.error_email.smtp_username,
config.error_email.smtp_password
)
session.sendmail(sender_email, recipients, message.as_string())
session.quit()
else:
with smtplib.SMTP_SSL(
config.error_email.smtp_host,
config.error_email.smtp_port,
context=context
) as server:
server.login(smtp_username, smtp_password)
server.sendmail(
from_addr=sender_email,
to_addrs=recipients,
msg=message.as_string()
)
@staticmethod
def handle_resolved_exception(config: types.Config) -> None:
"""Send out an email that all exceptions have been resolved."""
if not config.error_email.notify_recipients:
return
pyra_version = _get_pyra_version()
current_log_lines = _get_current_log_lines()
logs = "".join(current_log_lines)
text = (
"All exceptions have been resolved.\n\n" +
f"Last 2 iteration's log lines:{logs}\n\n" +
f"This email has been generated by Pyra {pyra_version} automatically."
)
html = "\n".join([
f"<html>",
f' <body style="color: #0f172a;">',
f' <p><strong style="color: #16a34a">All exceptions have been resolved.</strong></p>',
f' <p><strong>Last 2 iteration\'s log lines:</strong></p>',
f' {_PRE_CODE_TAG}{logs}{_POST_CODE_TAG}',
f' <p><em>This email has been generated by Pyra {pyra_version} automatically.</em></p>',
f' </body>',
f'</html>',
])
station_id = config.general.station_id
subject = f'✅ PYRA on system "{station_id}": all exceptions resolved'
ExceptionEmailClient._send_email(config, text, html, subject)
@staticmethod
def handle_occured_exception(
config: types.Config,
exception: Exception,
) -> None:
"""Send out an email that a new exception has occured."""
if not config.error_email.notify_recipients:
return
pyra_version = _get_pyra_version()
current_log_lines = _get_current_log_lines()
tb = "\n".join(traceback.format_exception(exception))
logs = "".join(current_log_lines)
text = (
f"{type(exception).__name__} has occured. Details:\n" +
f"{tb}\nLast 2 iteration's log lines:{logs}\n" +
f"This email has been generated by Pyra {pyra_version} automatically."
)
html = "\n".join([
f"<html>",
f' <body style="color: #0f172a;">',
f' <p><strong><span style="color: #dc2626">{type(exception).__name__}</span> has occured. Details:</strong></p>',
f" {_PRE_CODE_TAG}{tb}{_POST_CODE_TAG}",
f" <p><strong>Last 2 iteration's log lines:</strong></p>",
f" {_PRE_CODE_TAG}{logs}{_POST_CODE_TAG}" +
f" <p><em>This email has been generated by Pyra {pyra_version} automatically.</em></p>",
f" </body>",
f"</html>",
])
station_id = config.general.station_id
subject = f'❗️ PYRA on system "{station_id}": new exception "{type(exception).__name__}"'
ExceptionEmailClient._send_email(config, text, html, subject)
@staticmethod
def send_test_email(config: types.Config) -> None:
"""Send out a test email."""
if not config.error_email.notify_recipients:
return
pyra_version = _get_pyra_version()
current_log_lines = _get_current_log_lines()
logs = "".join(current_log_lines)
text = (
"This is a test email.\n\n" +
f"Last 2 iteration's log lines:{logs}\n\n" +
f"This email has been generated by Pyra {pyra_version} in a test run."
)
html = "\n".join([
f"<html>",
f' <body style="color: #0f172a;">' +
f' <p><strong style="color: #16a34a">This is a test email.</strong></p>',
f" <p><strong>Last 2 iteration's log lines:</strong></p>",
f" {_PRE_CODE_TAG}{logs}{_POST_CODE_TAG}",
f" <p><em>This email has been generated by Pyra {pyra_version} automatically.</em></p>",
f" </body>",
"</html>",
])
station_id = config.general.station_id
subject = f'⚙️ PYRA on system "{station_id}": test email'
ExceptionEmailClient._send_email(config, text, html, subject)