Skip to content

Commit 4db46c3

Browse files
authored
New transport element (#52)
New transport element Add script for sending notifications instead of Grafana Alertmanager Fix requirements Add scheduler.py to README Reviewed-by: Vladimir Vshivkov
1 parent 8e2dd7a commit 4db46c3

File tree

3 files changed

+198
-1
lines changed

3 files changed

+198
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ Scripts gathering HC3.0 PRs, issues and docs info from Github and Gitea. Scripts
88
4) **failed_zuul.py:** collecting info about PRs which checks in Zuul has been failed
99
5) **open_issues.py:** this script gather info regarding all open issues both from Gitea and Gitnub
1010
6) **last_commit_info.py:** this script gather and calculate date of the last update of a prospective doc for all the services and types of docs (user manual, API reference and so on)
11-
11+
7) **scheduler.py:** this script checking postgres for orphans, unattended issues and outdated docs, and send notifications to Zulip, via OTC Bot. Runs as cronjob (see 'system-config' repo)
1212
Postgres database names, table names, Gitea & Github organization names and access tokens are store in environment variables.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ urllib3==2.0.7
1414
wrapt==1.15.0
1515
pyyaml==6.0.1
1616
gitpython==3.1.37
17+
zulip==0.9.0

scheduler.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import os
2+
import psycopg2
3+
import time
4+
import logging
5+
from psycopg2.extras import DictCursor
6+
import zulip
7+
8+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
9+
10+
# timer for counting script time execution
11+
start_time = time.time()
12+
13+
logging.info("**SCHEDULER IS RUNNING**")
14+
15+
# Database and Zulip configuration, environment vars are used
16+
db_host = os.getenv("DB_HOST")
17+
db_port = os.getenv("DB_PORT")
18+
db_csv = os.getenv("DB_CSV")
19+
db_orph = os.getenv("DB_ORPH")
20+
db_user = os.getenv("DB_USER")
21+
db_password = os.getenv("DB_PASSWORD")
22+
api_key = os.getenv("OTC_BOT_API")
23+
24+
# Zulip stream and topic mapping for each squad
25+
squad_streams = {
26+
"Database Squad": {"stream": "Database Squad", "topic": "Doc alerts"},
27+
"Big Data and AI Squad": {"stream": "bigdata & ai", "topic": "helpcenter_alerts"},
28+
"Compute Squad": {"stream": "compute", "topic": "hc_alerts topic"},
29+
"Network Squad": {"stream": "network", "topic": "Alerts_HelpCenter"}
30+
}
31+
32+
33+
def check_env_variables():
34+
required_env_vars = [
35+
"DB_HOST", "DB_PORT",
36+
"DB_CSV", "DB_ORPH", "DB_USER", "DB_PASSWORD", "OTC_BOT_API"
37+
]
38+
for var in required_env_vars:
39+
if os.getenv(var) is None:
40+
raise Exception(f"Missing environment variable: {var}")
41+
42+
43+
def connect_to_db(db_name):
44+
logging.info(f"Connecting to Postgres ({db_name})...")
45+
try:
46+
return psycopg2.connect(
47+
host=db_host,
48+
port=db_port,
49+
dbname=db_name,
50+
user=db_user,
51+
password=db_password,
52+
cursor_factory=DictCursor
53+
)
54+
except psycopg2.Error as e:
55+
logging.error(f"Connecting to Postgres: an error occurred while trying to connect to the database: {e}")
56+
return None
57+
58+
59+
def check_orphans(conn_orph, squad_name, stream_name, topic_name):
60+
cur_orph = conn_orph.cursor()
61+
tables = ["open_prs", "open_prs_swiss"]
62+
for table in tables:
63+
# here each query marked with zone marker (Public or Hybrid) and type for bringing it into message
64+
if table == "open_prs":
65+
logging.info(f"Looking for orphaned PRs for {squad_name} in {table}...")
66+
query = f"""SELECT *, 'Public' as zone, 'orphan' as type FROM {table} WHERE "Squad" = '{squad_name}';"""
67+
cur_orph.execute(query, (squad_name,))
68+
results = cur_orph.fetchall()
69+
elif table == "open_prs_swiss":
70+
logging.info(f"Looking for orphaned PRs for {squad_name} in {table}...")
71+
query = f"""SELECT *, 'Hybrid' as zone, 'orphan' as type FROM {table} WHERE "Squad" = '{squad_name}';"""
72+
cur_orph.execute(query, (squad_name,))
73+
results = cur_orph.fetchall()
74+
if results:
75+
for row in results:
76+
send_zulip_notification(row, api_key, stream_name, topic_name)
77+
78+
79+
def check_open_issues(conn, squad_name, stream_name, topic_name):
80+
cur = conn.cursor()
81+
tables = ["open_issues", "open_issues_swiss"]
82+
for table in tables:
83+
# here each query marked with zone marker (Public or Hybrid) and type for bringing it into message
84+
if table == "open_issues":
85+
logging.info(f"Checking {table} for {squad_name}")
86+
query = f"""SELECT *, 'Public' as zone, 'issue' as type FROM {table} WHERE "Squad" = '{squad_name}' AND "Environment" = 'Github' AND "Assignees" = '' AND "Duration" > '7' ;"""
87+
cur.execute(query, (squad_name,))
88+
results = cur.fetchall()
89+
elif table == "open_issues_swiss":
90+
logging.info(f"Checking {table} for {squad_name}")
91+
query = f"""SELECT *, 'Hybrid' as zone, 'issue' as type FROM {table} WHERE "Squad" = '{squad_name}' AND "Environment" = 'Github' AND "Assignees" = '' AND "Duration" > '7' ;"""
92+
cur.execute(query, (squad_name,))
93+
results = cur.fetchall()
94+
if results:
95+
for row in results:
96+
send_zulip_notification(row, api_key, stream_name, topic_name)
97+
98+
99+
def check_outdated_docs(conn, squad_name, stream_name, topic_name):
100+
cur = conn.cursor()
101+
tables = ["last_update_commit", "last_update_commit_swiss"]
102+
for table in tables:
103+
# here each query marked with zone marker (Public or Hybrid) and type for bringing it into message
104+
if table == "last_update_commit":
105+
logging.info(f"Checking {table} table for {squad_name}...")
106+
query = f"""SELECT *, 'Public' as zone, 'doc' as type FROM {table} WHERE "Squad" = %s;"""
107+
cur.execute(query, (squad_name,))
108+
results = cur.fetchall()
109+
elif table == "last_update_commit_swiss":
110+
logging.info(f"Checking {table} table for {squad_name}...")
111+
query = f"""SELECT *, 'Hybrid' as zone, 'doc' as type FROM {table} WHERE "Squad" = %s;"""
112+
cur.execute(query, (squad_name,))
113+
results = cur.fetchall()
114+
if results:
115+
for row in results:
116+
send_zulip_notification(row, api_key, stream_name, topic_name)
117+
118+
119+
def send_zulip_notification(row, api_key, stream_name, topic_name):
120+
client = zulip.Client(email="[email protected]", api_key=api_key, site="https://zulip.tsi-vc.otc-service.com")
121+
if row["type"] == "doc":
122+
squad_name = row[3]
123+
service_name = row[1]
124+
commit_url = row[6]
125+
days_passed = int(row[5])
126+
if days_passed == 344:
127+
weeks_to_threshold = 3
128+
message = f":notifications: **Outdated Documents Alert** :notifications:\n\nThis document's last relea" \
129+
f"se date will break the **1-year threshold after {weeks_to_threshold} weeks.**\n"
130+
elif days_passed == 351:
131+
weeks_to_threshold = 2
132+
message = f":notifications::notifications: **Outdated Documents Alert** :notifications::notifications:" \
133+
f"\n\nThis document's last release date will break the **1-year threshold after {weeks_to_threshold} weeks.**"
134+
elif days_passed == 358:
135+
weeks_to_threshold = 1
136+
message = f":notifications::notifications::notifications: **Outdated Documents Alert** :notifications::" \
137+
f"notifications::notifications:\n\nThis document's last release date will break the **1-year threshold after {weeks_to_threshold} weeks.**"
138+
elif days_passed >= 365:
139+
message = f":exclamation: **Outdated Documents Alert** :exclamation:\n\nThis document's release date breaks 1-year threshold!"
140+
else:
141+
return
142+
143+
message += f"\n\n**Squad name:** {squad_name}\n**Service name:** {service_name}\n**Zone:** {row[-2]}\n\n**Commit" \
144+
f" URL:** {commit_url}\n**Dashboard URL:** https://dashboard.tsi-dev.otc-service.com/d/c67f0f4b-b31c-" \
145+
f"4433-b530-a18896470d49/last-docs-commit?orgId=1\n\n---------------------------------------------------------"
146+
elif row["type"] == "issue":
147+
squad_name = row[3]
148+
service_name = row[2]
149+
issue_url = row[5]
150+
message = f":point_right: **Unattended Issues Alert** :point_left:\n\nYou have an issue which has no assignees for more than 7 days\n\n" \
151+
f"**Squad name:** {squad_name}\n**Service name:** {service_name}\n**Zone:** {row[-2]}\n\n**Issue URL:" \
152+
f"** {issue_url}\n**Dashboard URL:** https://dashboard.tsi-dev.otc-service.com/d/I-YJAuBVk/open-issues" \
153+
f"-dashboard?orgId=1&var-squad_issues=All&var-env_issues=All&var-sort_duration=DESC&var-zone=open_issues\n\n---------------------------------------------------------"
154+
elif row["type"] == "orphan":
155+
squad_name = row[3]
156+
service_name = row[2]
157+
orphan_url = row[4]
158+
message = f":boom: **Orphaned PRs Alert** :boom:\n\nYou have orphaned PR here!\n\n**Squad name:** {squad_name}\n**Service name:** {service_name}\n**Zone:** {row[-2]}\n\n" \
159+
f"**Orphan URL:** {orphan_url}\n**Dashboard URL:** https://dashboard.tsi-dev.otc-service.com/d/4vLGLDB" \
160+
f"4z/open-prs-dashboard?orgId=1\n\n---------------------------------------------------------"
161+
result = client.send_message({
162+
"type": "stream",
163+
"to": stream_name,
164+
"subject": topic_name,
165+
"content": message
166+
})
167+
168+
if result["result"] == "success":
169+
logging.info(f"Notification sent successfully for {row[-1]}")
170+
else:
171+
logging.error(f"Failed to send notification for {row[-1]}: {result['msg']}")
172+
173+
174+
def main():
175+
check_env_variables()
176+
conn = connect_to_db(db_csv)
177+
conn_orph = connect_to_db(db_orph)
178+
179+
for squad_name, channel in squad_streams.items():
180+
stream_name = channel["stream"]
181+
topic_name = channel["topic"]
182+
check_orphans(conn_orph, squad_name, stream_name, topic_name)
183+
check_open_issues(conn, squad_name, stream_name, topic_name)
184+
check_outdated_docs(conn, squad_name, stream_name, topic_name)
185+
186+
conn.close()
187+
conn_orph.close()
188+
189+
190+
if __name__ == "__main__":
191+
main()
192+
end_time = time.time()
193+
execution_time = end_time - start_time
194+
minutes, seconds = divmod(execution_time, 60)
195+
logging.info(f"Script executed in {int(minutes)} minutes {int(seconds)} seconds! Let's go drink some beer :)")
196+

0 commit comments

Comments
 (0)