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 • 13.05 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, default=0)
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
74
def __init__(self, name, owner):
75
self.name = name
76
self.owner_name = owner.username
77
78
class Endpoint(db.Model):
79
id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
80
application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False)
81
address = db.Column(db.String(2048), nullable=False)
82
name = db.Column(db.String(64), nullable=False)
83
comment = db.Column(db.String(2048), nullable=True)
84
ping_interval = db.Column(db.Integer, default=300, nullable=False)
85
buggy = db.Column(db.Boolean, default=False)
86
create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
87
88
application = db.relationship("Application", back_populates="endpoints")
89
statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic")
90
91
def __init__(self, application, name, address, ping_interval, comment=""):
92
self.application_id = application.id
93
self.name = name
94
self.address = address
95
self.comment = comment
96
self.ping_interval = ping_interval
97
98
99
class Status(db.Model):
100
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
101
endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False)
102
time = db.Column(db.DateTime, default=datetime.datetime.utcnow)
103
104
status = db.Column(db.SmallInteger, nullable=False)
105
buggy = db.Column(db.Boolean, default=False)
106
endpoint = db.relationship("Endpoint", back_populates="statuses")
107
108
def __init__(self, endpoint_id, status, buggy):
109
self.endpoint_id = endpoint_id
110
self.status = status
111
self.buggy = buggy
112
113
114
@celery.shared_task(name="ping")
115
def ping(id, address, next_ping):
116
if not db.session.get(Endpoint, id):
117
return
118
elif db.session.get(Endpoint, id).buggy:
119
buggy = True
120
else:
121
buggy = False
122
url = address
123
print(f"Pinging {url}")
124
response = httpx.get(url, verify=False)
125
reading = Status(id, response.status_code, buggy)
126
last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first()
127
db.session.add(reading)
128
db.session.commit()
129
130
# Schedule the next ping
131
ping.apply_async(args=(id, address, next_ping), countdown=next_ping)
132
133
@celery.shared_task(name="ping_all")
134
def ping_all():
135
endpoints = Endpoint.query.all()
136
for endpoint in endpoints:
137
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
138
139
140
task = ping_all.delay()
141
142
print()
143
print()
144
print(task)
145
print()
146
print()
147
148
149
@app.context_processor
150
def default():
151
return {
152
"session": flask.session,
153
}
154
155
156
@app.route("/")
157
def dashboard():
158
return flask.render_template("dashboard.html", apps=Application.query.all())
159
160
161
@app.route("/my")
162
def my_apps():
163
if not flask.session.get("username"):
164
return flask.redirect("/login", code=303)
165
return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all())
166
167
168
@app.route("/login", methods=["GET"])
169
def login():
170
return flask.render_template("login.html")
171
172
173
@app.route("/signup", methods=["GET"])
174
def signup():
175
return flask.render_template("signup.html")
176
177
178
@app.route("/new-app", methods=["GET"])
179
def new_app():
180
if not flask.session.get("username"):
181
return flask.redirect("/login", code=303)
182
return flask.render_template("new-app.html")
183
184
185
@app.route("/new-app", methods=["POST"])
186
def new_app_post():
187
if not flask.session.get("username"):
188
return flask.redirect("/login", code=303)
189
if Application.query.filter_by(name=flask.request.form["name"]).first():
190
flask.flash("Application already exists")
191
return flask.redirect("/new-app", code=303)
192
193
new_app_ = Application(
194
flask.request.form["name"],
195
db.session.get(User, flask.session["username"]),
196
)
197
db.session.add(new_app_)
198
db.session.commit()
199
return flask.redirect("/", code=303)
200
201
202
@app.route("/login", methods=["POST"])
203
def login_post():
204
user = db.session.get(User, flask.request.form["username"])
205
if not user:
206
flask.flash("Username doesn't exist")
207
return flask.redirect("/signup", code=303)
208
if not bcrypt.check_password_hash(user.password, flask.request.form["password"]):
209
flask.flash("Wrong password")
210
return flask.redirect("/signup", code=303)
211
212
flask.session["username"] = user.username
213
return flask.redirect("/", code=303)
214
215
216
@app.route("/logout")
217
def logout():
218
flask.session.pop("username", None)
219
return flask.redirect("/", code=303)
220
221
222
@app.route("/signup", methods=["POST"])
223
def signup_post():
224
if flask.request.form["password"] != flask.request.form["password2"]:
225
flask.flash("Passwords do not match")
226
return flask.redirect("/signup", code=303)
227
if db.session.get(User, flask.request.form["username"]):
228
flask.flash("Username already exists")
229
return flask.redirect("/signup", code=303)
230
if len(flask.request.form["password"]) < 8:
231
flask.flash("Password must be at least 8 characters")
232
return flask.redirect("/signup", code=303)
233
if len(flask.request.form["username"]) < 4:
234
flask.flash("Username must be at least 4 characters")
235
return flask.redirect("/signup", code=303)
236
237
new_user = User(
238
flask.request.form["username"],
239
flask.request.form["password"],
240
)
241
db.session.add(new_user)
242
db.session.commit()
243
flask.session["username"] = new_user.username
244
return flask.redirect("/", code=303)
245
246
247
# UTC filter
248
@app.template_filter("utc")
249
def utc_filter(timestamp):
250
return datetime.datetime.utcfromtimestamp(timestamp)
251
252
253
@app.route("/app/<int:app_id>/")
254
def app_info(app_id):
255
app_ = db.session.get(Application, app_id)
256
257
time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)),
258
datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * i))
259
for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)]
260
261
slice_results = {}
262
all_results = []
263
264
for endpoint in app_.endpoints:
265
slice_results[endpoint.id] = []
266
267
for slice_ in time_slices:
268
slice_results[endpoint.id].append(
269
(
270
db.session.query(Status).filter(
271
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
272
Status.time >= slice_[0],
273
Status.time < slice_[1])).all(),
274
slice_
275
)
276
)
277
278
for endpoint in app_.endpoints:
279
all_results.extend(db.session.query(Status).filter(
280
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
281
Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=10),
282
Status.time < datetime.datetime.utcnow())).all())
283
284
return flask.render_template("app.html", app=app_, sorted=sorted, list=list,
285
sorting=lambda x: x.time, reverse=True,
286
is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
287
for status in x), and_=sqlalchemy.and_,
288
is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
289
for status in x),
290
bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status,
291
time_period=int(flask.request.args.get("time_period", 1440)),
292
now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func,
293
reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp,
294
slices=slice_results, bugs=lambda x: any(status.buggy for status in x),
295
all_results=all_results)
296
297
298
@app.route("/app/<int:app_id>/edit/")
299
def app_editor(app_id):
300
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
301
flask.abort(403)
302
app_ = db.session.get(Application, app_id)
303
return flask.render_template("app-editor.html", app=app_)
304
305
306
@app.route("/app/<int:app_id>/edit/<int:endpoint_id>", methods=["POST"])
307
def endpoint_edit(app_id, endpoint_id):
308
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
309
flask.abort(403)
310
endpoint = db.session.get(Endpoint, endpoint_id)
311
if flask.request.form.get("delete") == "delete":
312
statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all()
313
for status in statuses:
314
db.session.delete(status)
315
db.session.delete(endpoint)
316
db.session.commit()
317
else:
318
endpoint.name = flask.request.form["name"]
319
endpoint.address = flask.request.form["url"]
320
endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"]))
321
endpoint.comment = flask.request.form["comment"]
322
db.session.commit()
323
return flask.redirect(f"/app/{app_id}/edit", code=303)
324
325
326
@app.route("/app/<int:app_id>/report/<int:endpoint_id>")
327
def endpoint_report(app_id, endpoint_id):
328
endpoint = db.session.get(Endpoint, endpoint_id)
329
endpoint.buggy = True
330
db.session.commit()
331
return flask.redirect(f"/app/{app_id}", code=303)
332
333
334
@app.route("/app/<int:app_id>/fix/<int:endpoint_id>")
335
def endpoint_fix(app_id, endpoint_id):
336
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
337
flask.abort(403)
338
endpoint = db.session.get(Endpoint, endpoint_id)
339
endpoint.buggy = False
340
db.session.commit()
341
return flask.redirect(f"/app/{app_id}", code=303)
342
343
344
@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"])
345
def app_add_endpoint(app_id):
346
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
347
flask.abort(403)
348
app_ = db.session.get(Application, app_id)
349
endpoint = Endpoint(app_,
350
flask.request.form["name"],
351
flask.request.form["url"],
352
max(15, int(flask.request.form["ping_interval"])),
353
flask.request.form["comment"])
354
db.session.add(endpoint)
355
db.session.commit()
356
357
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
358
359
return flask.redirect(f"/app/{app_id}/edit", code=303)
360
361