WWW service status tracker

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

View raw Download
text/x-script.python • 14.19 kiB
Python script, ASCII text executable
        
            
1
import datetime
2
3
import celery
4
import flask
5
import sqlalchemy
6
from flask_sqlalchemy import SQLAlchemy
7
from flask_bcrypt import Bcrypt
8
from flask_migrate import Migrate
9
10
from sqlalchemy.orm import declarative_base
11
from celery import shared_task
12
import httpx
13
14
app = flask.Flask(__name__)
15
from celery import Celery, Task
16
17
18
def celery_init_app(app_) -> Celery:
19
class FlaskTask(Task):
20
def __call__(self, *args: object, **kwargs: object) -> object:
21
with app_.app_context():
22
return self.run(*args, **kwargs)
23
24
celery_app = Celery(app_.name, task_cls=FlaskTask)
25
celery_app.config_from_object(app_.config["CELERY"])
26
celery_app.set_default()
27
app_.extensions["celery"] = celery_app
28
return celery_app
29
30
31
app.config.from_mapping(
32
CELERY=dict(
33
broker_url="redis://localhost:6379",
34
result_backend="redis://localhost:6379"
35
),
36
)
37
celery_app = celery_init_app(app)
38
39
app.config["SQLALCHEMY_DATABASE_URI"] = \
40
"postgresql://echo:1234@localhost:5432/echo"
41
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
42
"connect_args": {
43
"options": "-c timezone=utc"
44
}
45
}
46
db = SQLAlchemy(app)
47
bcrypt = Bcrypt(app)
48
migrate = Migrate(app, db)
49
app.config["SESSION_TYPE"] = "filesystem"
50
app.config["SECRET_KEY"] = "super secret"
51
52
with (app.app_context()):
53
class User(db.Model):
54
username = db.Column(db.String(64), unique=True, nullable=False, primary_key=True)
55
password = db.Column(db.String(72), nullable=False)
56
admin = db.Column(db.Boolean, nullable=False, default=False)
57
58
applications = db.relationship("Application", back_populates="owner")
59
60
def __init__(self, username, password, admin=False):
61
self.username = username
62
self.password = bcrypt.generate_password_hash(password).decode("utf-8")
63
self.admin = admin
64
65
class Application(db.Model):
66
id = db.Column(db.Integer, primary_key=True, autoincrement=True, unique=True)
67
name = db.Column(db.String(64), unique=True, nullable=False)
68
owner_name = db.Column(db.String(64), db.ForeignKey("user.username"), nullable=False)
69
70
owner = db.relationship("User", back_populates="applications")
71
72
endpoints = db.relationship("Endpoint", back_populates="application")
73
stability_threshold = db.Column(db.Integer, default=15)
74
75
def __init__(self, name, owner):
76
self.name = name
77
self.owner_name = owner.username
78
79
class Endpoint(db.Model):
80
id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
81
application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False)
82
address = db.Column(db.String(2048), nullable=False)
83
name = db.Column(db.String(64), nullable=False)
84
comment = db.Column(db.String(2048), nullable=True)
85
ping_interval = db.Column(db.Integer, default=300, nullable=False)
86
buggy = db.Column(db.Boolean, default=False)
87
create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
88
89
application = db.relationship("Application", back_populates="endpoints")
90
statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic")
91
92
def __init__(self, application, name, address, ping_interval, comment=""):
93
self.application_id = application.id
94
self.name = name
95
self.address = address
96
self.comment = comment
97
self.ping_interval = ping_interval
98
99
100
class Status(db.Model):
101
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
102
endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False)
103
time = db.Column(db.DateTime, default=datetime.datetime.utcnow)
104
105
status = db.Column(db.SmallInteger, nullable=False)
106
buggy = db.Column(db.Boolean, default=False)
107
endpoint = db.relationship("Endpoint", back_populates="statuses")
108
109
def __init__(self, endpoint_id, status, buggy):
110
self.endpoint_id = endpoint_id
111
self.status = status
112
self.buggy = buggy
113
114
115
@celery.shared_task(name="ping")
116
def ping(id, address, next_ping):
117
if not db.session.get(Endpoint, id):
118
return
119
elif db.session.get(Endpoint, id).buggy:
120
buggy = True
121
else:
122
buggy = False
123
url = address
124
print(f"Pinging {url}")
125
response = httpx.get(url, verify=False)
126
reading = Status(id, response.status_code, buggy)
127
last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first()
128
db.session.add(reading)
129
db.session.commit()
130
131
# Schedule the next ping
132
ping.apply_async(args=(id, address, next_ping), countdown=next_ping)
133
134
@celery.shared_task(name="ping_all")
135
def ping_all():
136
endpoints = Endpoint.query.all()
137
for endpoint in endpoints:
138
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
139
140
141
task = ping_all.delay()
142
143
print()
144
print()
145
print(task)
146
print()
147
print()
148
149
150
@app.context_processor
151
def default():
152
return {
153
"session": flask.session,
154
}
155
156
157
@app.route("/")
158
def dashboard():
159
current_user = db.session.get(User, flask.session.get("username"))
160
return flask.render_template("dashboard.html", apps=Application.query.all(),
161
bugs=Endpoint.query.filter_by(buggy=True).filter(Application.owner_name == current_user.username).all() if current_user else [])
162
163
164
@app.route("/my")
165
def my_apps():
166
if not flask.session.get("username"):
167
return flask.redirect("/login", code=303)
168
return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all())
169
170
171
@app.route("/login", methods=["GET"])
172
def login():
173
return flask.render_template("login.html")
174
175
176
@app.route("/signup", methods=["GET"])
177
def signup():
178
return flask.render_template("signup.html")
179
180
181
@app.route("/new-app", methods=["GET"])
182
def new_app():
183
if not flask.session.get("username"):
184
return flask.redirect("/login", code=303)
185
return flask.render_template("new-app.html")
186
187
188
@app.route("/new-app", methods=["POST"])
189
def new_app_post():
190
if not flask.session.get("username"):
191
return flask.redirect("/login", code=303)
192
if Application.query.filter_by(name=flask.request.form["name"]).first():
193
flask.flash("Application already exists")
194
return flask.redirect("/new-app", code=303)
195
196
new_app_ = Application(
197
flask.request.form["name"],
198
db.session.get(User, flask.session["username"]),
199
)
200
db.session.add(new_app_)
201
db.session.commit()
202
return flask.redirect("/", code=303)
203
204
205
@app.route("/login", methods=["POST"])
206
def login_post():
207
user = db.session.get(User, flask.request.form["username"])
208
if not user:
209
flask.flash("Username doesn't exist")
210
return flask.redirect("/signup", code=303)
211
if not bcrypt.check_password_hash(user.password, flask.request.form["password"]):
212
flask.flash("Wrong password")
213
return flask.redirect("/signup", code=303)
214
215
flask.session["username"] = user.username
216
return flask.redirect("/", code=303)
217
218
219
@app.route("/logout")
220
def logout():
221
flask.session.pop("username", None)
222
return flask.redirect("/", code=303)
223
224
225
@app.route("/signup", methods=["POST"])
226
def signup_post():
227
if flask.request.form["password"] != flask.request.form["password2"]:
228
flask.flash("Passwords do not match")
229
return flask.redirect("/signup", code=303)
230
if db.session.get(User, flask.request.form["username"]):
231
flask.flash("Username already exists")
232
return flask.redirect("/signup", code=303)
233
if len(flask.request.form["password"]) < 8:
234
flask.flash("Password must be at least 8 characters")
235
return flask.redirect("/signup", code=303)
236
if len(flask.request.form["username"]) < 4:
237
flask.flash("Username must be at least 4 characters")
238
return flask.redirect("/signup", code=303)
239
240
new_user = User(
241
flask.request.form["username"],
242
flask.request.form["password"],
243
)
244
db.session.add(new_user)
245
db.session.commit()
246
flask.session["username"] = new_user.username
247
return flask.redirect("/", code=303)
248
249
250
# UTC filter
251
@app.template_filter("utc")
252
def utc_filter(timestamp):
253
return datetime.datetime.utcfromtimestamp(timestamp)
254
255
256
@app.route("/app/<int:app_id>/")
257
def app_info(app_id):
258
app_ = db.session.get(Application, app_id)
259
260
time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)),
261
datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * i))
262
for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)]
263
264
slice_results = {}
265
all_results = []
266
267
for endpoint in app_.endpoints:
268
slice_results[endpoint.id] = []
269
270
for slice_ in time_slices:
271
slice_results[endpoint.id].append(
272
(
273
db.session.query(Status).filter(
274
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
275
Status.time >= slice_[0],
276
Status.time < slice_[1])).all(),
277
slice_
278
)
279
)
280
281
all_results.extend(db.session.query(Status).filter(
282
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
283
Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=app_.stability_threshold),
284
Status.time < datetime.datetime.utcnow())).all())
285
286
return flask.render_template("app.html", app=app_, sorted=sorted, list=list,
287
sorting=lambda x: x.time, reverse=True,
288
is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
289
for status in x), and_=sqlalchemy.and_,
290
is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
291
for status in x),
292
bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status,
293
time_period=int(flask.request.args.get("time_period", 1440)),
294
now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func,
295
reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp,
296
slices=slice_results, bugs=lambda x: any(status.buggy for status in x),
297
all_results=all_results)
298
299
300
@app.route("/app/<int:app_id>/edit/")
301
def app_editor(app_id):
302
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
303
flask.abort(403)
304
app_ = db.session.get(Application, app_id)
305
return flask.render_template("app-editor.html", app=app_)
306
307
308
@app.route("/app/<int:app_id>/edit/", methods=["POST"])
309
def app_editor_post(app_id):
310
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
311
flask.abort(403)
312
app_ = db.session.get(Application, app_id)
313
if flask.request.form.get("delete") == "delete":
314
endpoints = db.session.query(Endpoint).filter_by(application_id=app_id).all()
315
for endpoint in endpoints:
316
statuses = db.session.query(Status).filter_by(endpoint_id=endpoint.id).all()
317
for status in statuses:
318
db.session.delete(status)
319
db.session.delete(endpoint)
320
db.session.delete(app_)
321
db.session.commit()
322
else:
323
app_.name = flask.request.form["name"]
324
app_.stability_threshold = min(300, int(flask.request.form["threshold"]))
325
db.session.commit()
326
return flask.redirect("/", code=303)
327
328
329
@app.route("/app/<int:app_id>/edit/<int:endpoint_id>", methods=["POST"])
330
def endpoint_edit(app_id, endpoint_id):
331
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
332
flask.abort(403)
333
endpoint = db.session.get(Endpoint, endpoint_id)
334
if flask.request.form.get("delete") == "delete":
335
statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all()
336
for status in statuses:
337
db.session.delete(status)
338
db.session.delete(endpoint)
339
db.session.commit()
340
else:
341
endpoint.name = flask.request.form["name"]
342
endpoint.address = flask.request.form["url"]
343
endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"]))
344
endpoint.comment = flask.request.form["comment"]
345
db.session.commit()
346
return flask.redirect(f"/app/{app_id}/edit", code=303)
347
348
349
@app.route("/app/<int:app_id>/report/<int:endpoint_id>")
350
def endpoint_report(app_id, endpoint_id):
351
endpoint = db.session.get(Endpoint, endpoint_id)
352
endpoint.buggy = True
353
db.session.commit()
354
return flask.redirect(f"/app/{app_id}", code=303)
355
356
357
@app.route("/app/<int:app_id>/fix/<int:endpoint_id>")
358
def endpoint_fix(app_id, endpoint_id):
359
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
360
flask.abort(403)
361
endpoint = db.session.get(Endpoint, endpoint_id)
362
endpoint.buggy = False
363
db.session.commit()
364
return flask.redirect(f"/app/{app_id}", code=303)
365
366
367
@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"])
368
def app_add_endpoint(app_id):
369
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
370
flask.abort(403)
371
app_ = db.session.get(Application, app_id)
372
endpoint = Endpoint(app_,
373
flask.request.form["name"],
374
flask.request.form["url"],
375
max(15, int(flask.request.form["ping_interval"])),
376
flask.request.form["comment"])
377
db.session.add(endpoint)
378
db.session.commit()
379
380
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
381
382
return flask.redirect(f"/app/{app_id}/edit", code=303)
383
384