Skip to content

Commit a0def2a

Browse files
feat: support for monitor external api/file added
1 parent b021d11 commit a0def2a

8 files changed

Lines changed: 141 additions & 9 deletions

File tree

app.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from src.config import app
22
from src import routes
3-
from src.background_task import start_website_monitoring, monitor_settings
3+
from src.background_task import start_website_monitoring, monitor_settings, fetch_file_metrics_task
44
import os
55

66
# background thread to monitor system settings changes
77
print("FLASK_ENV: ", os.getenv('FLASK_ENV'))
8-
if os.getenv('FLASK_ENV') == 'production':
9-
monitor_settings() # Starts monitoring for system logging changes
10-
start_website_monitoring() # Starts pinging active websites
8+
# if os.getenv('FLASK_ENV') == 'production':
9+
start_website_monitoring() # Starts pinging active websites
10+
fetch_file_metrics_task()
11+
monitor_settings() # Starts monitoring for system logging changes
1112

1213
if __name__ == "__main__":
1314
app.run(host="0.0.0.0", port=5000, debug=True)

src/background_task/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from src.background_task.monitor_website import start_website_monitoring
22
from src.background_task.log_system_info import monitor_settings
3+
from src.background_task.prometheus_helper import fetch_file_metrics_task
34

4-
__all__ = ["start_website_monitoring", "monitor_settings"]
5+
__all__ = ["start_website_monitoring", "monitor_settings", "fetch_file_metrics_task"]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from flask import Flask, Response
2+
from prometheus_client import Gauge, generate_latest
3+
import os
4+
import time
5+
from src.models import ExternalMonitornig
6+
from src.config import app
7+
import threading
8+
from src.logger import logger
9+
10+
# Define a Gauge metric with a label 'key' to store multiple values from the file
11+
file_metric = Gauge('external_metrics', 'Value read from file for each key', ['key'])
12+
13+
def read_file_and_update_metric(file_path: str) -> None:
14+
"""Reads a file and updates metrics based on its content."""
15+
if os.path.exists(file_path):
16+
with open(file_path, 'r') as file:
17+
for line in file:
18+
try:
19+
key, value = line.strip().split(':')
20+
file_metric.labels(key.strip()).set(float(value.strip()))
21+
except ValueError as ve:
22+
logger.error(f"Value error processing line '{line}': {ve}")
23+
except Exception as e:
24+
logger.error(f"Error processing line '{line}': {e}")
25+
else:
26+
logger.warning(f"File {file_path} does not exist")
27+
28+
def fetch_file_metrics(sleep_duration: int = 5) -> None:
29+
"""Background task to read file paths from the database and update metrics."""
30+
while True:
31+
with app.app_context():
32+
file_paths = ExternalMonitornig.query.all()
33+
current_keys = {sample.labels['key'] for sample in file_metric.collect()[0].samples}
34+
new_keys = set()
35+
36+
for file_path in file_paths:
37+
logger.info(f"Reading file: {file_path.file_path}")
38+
read_file_and_update_metric(file_path.file_path)
39+
new_keys.update({key.strip() for line in open(file_path.file_path) for key in line.split(':')[0].strip()})
40+
41+
# Remove metrics for keys that are no longer in the database
42+
for key in current_keys:
43+
if key not in new_keys:
44+
file_metric.remove(key)
45+
46+
time.sleep(sleep_duration)
47+
48+
def fetch_file_metrics_task() -> None:
49+
"""Starts the background task in a separate thread."""
50+
thread = threading.Thread(target=fetch_file_metrics)
51+
thread.daemon = True
52+
thread.start()

src/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
os.makedirs(DB_DIR, exist_ok=True)
2727

2828
# Configure the SQLite database
29-
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db"
29+
# app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db"
30+
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///systemguard.db"
3031
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
3132
app.config['SECRET_KEY'] = 'secret'
3233

src/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from src.models.system_information import SystemInformation
1010
from src.models.user_profile import UserProfile
1111
from src.models.monitored_website import MonitoredWebsite
12+
from src.models.prometheus_model import ExternalMonitornig
1213
from flask_login import current_user
1314
from src.logger import logger
1415
from werkzeug.security import generate_password_hash

src/models/prometheus_model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from src.config import db
2+
3+
4+
class ExternalMonitornig(db.Model):
5+
id = db.Column(db.Integer, primary_key=True)
6+
file_path = db.Column(db.String(100), nullable=False)
7+
is_active = db.Column(db.Boolean, default=True)

src/routes/prometheus.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
1-
from flask import Blueprint, Response
1+
from flask import Blueprint, Response, request, jsonify, render_template, flash, redirect, url_for
22
from prometheus_client import generate_latest
3-
from src.config import app
3+
4+
from src.config import app, db
5+
from src.models import ExternalMonitornig
6+
47

58
# Define the Prometheus Blueprint
69
prometheus_bp = Blueprint('prometheus', __name__)
710

811
# Define a route to serve Prometheus metrics
912
@app.route('/metrics')
1013
def metrics():
11-
return Response(generate_latest(), mimetype='text/plain')
14+
output = generate_latest()
15+
output = '\n'.join([line for line in output.decode().split('\n') if not line.startswith('#') and line])
16+
return Response(output, mimetype='text/plain')
17+
18+
# post request to add file path
19+
@app.route('/prometheus/add_file_path', methods=['GET', 'POST'])
20+
def add_file_path():
21+
if request.method == 'POST':
22+
23+
file_path = request.form.get('file_path')
24+
# save into the ExternalMonitornig table
25+
new_task = ExternalMonitornig(file_path=file_path)
26+
# commit the changes
27+
db.session.add(new_task)
28+
db.session.commit()
29+
30+
# read_file_and_update_metric(file_path=file_path)
31+
return redirect(url_for('add_file_path'))
32+
33+
data = ExternalMonitornig.query.all()
34+
return render_template('prometheus/add_file_path.html', data=data)
35+
36+
37+
# post request to delete file path
38+
@app.route('/prometheus/delete_file_path/<int:id>', methods=['POST'])
39+
def delete_file_path(id):
40+
file_path = ExternalMonitornig.query.get_or_404(id)
41+
db.session.delete(file_path)
42+
db.session.commit()
43+
flash('File path deleted successfully!', 'success')
44+
return redirect(url_for('add_file_path'))
45+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>External File for Monitoring Prometheus</title>
8+
</head>
9+
10+
<body>
11+
<div class="container">
12+
<h1>Add File Path</h1>
13+
<form method="POST" action="{{ url_for('add_file_path') }}">
14+
<label for="file-path">File Path</label>
15+
<input type="text" id="file-path" name="file_path" placeholder="Enter file path" required>
16+
<button type="submit">Submit</button>
17+
</form>
18+
<div class="message" id="message"></div>
19+
</div>
20+
21+
<!-- show a list of external monitoring file path from db -->
22+
<!-- /prometheus/delete_file_path/<int:id> -->
23+
{% if data %}
24+
{% for item in data %}
25+
<div>
26+
<p>{{ item.file_path }}</p>
27+
<form method="POST" action="{{ url_for('delete_file_path', id=item.id) }}">
28+
<button type="submit">Delete</button>
29+
</form>
30+
</div>
31+
{% endfor %}
32+
{% endif %}
33+
</body>
34+
35+
</html>

0 commit comments

Comments
 (0)