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