Show daily repo size changes as graph

This commit is contained in:
George Lacey 2021-05-09 15:58:53 +01:00
parent 5b9c7dfa31
commit 93da0603da
5 changed files with 148 additions and 37 deletions

View File

@ -1,7 +1,7 @@
from django.db import models from django.db import models
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..utility.time import seconds_to_string from ..utility.time import seconds_to_string
from ..utility.data import bytes_to_string from ..utility.data import bytes_to_string, convert_bytes
from . import Label from . import Label
@ -46,12 +46,15 @@ class Repo(models.Model):
def size(self): def size(self):
if self.archive_set.all().exists(): if self.archive_set.all().exists():
cache = self.latest_archive().cache cache = self.latest_archive().cache
return f"{bytes_to_string(cache.unique_csize)}" return cache.unique_csize
else: else:
return "No archives stored" return 0
def recent_errors(self): def size_string(self):
days = 7 size = self.size()
return bytes_to_string(size)
def recent_errors(self, days: int = 7):
days_ago = (datetime.utcnow() - timedelta(days=days)) days_ago = (datetime.utcnow() - timedelta(days=days))
errors = self.label.errors.all().filter(time__gt=days_ago) errors = self.label.errors.all().filter(time__gt=days_ago)
return errors return errors
@ -71,21 +74,57 @@ class Repo(models.Model):
days.append(len(cday_archives) > 0) days.append(len(cday_archives) > 0)
return days return days
def get_archive_hours_dict(self): def daily_dict(self, units, n_hours: int = 24):
if self.archive_set.all().exists(): archives = self.daily_archives(n_hours)
return {"id": self.id, return {
"id": self.id,
"label": self.label.label, "label": self.label.label,
"hours": self.get_archive_hours()} "daily_size": list(reversed(self.series_csize(archives, units)))
else: }
return {"id": self.id,
"label": self.label.label,
"hours": []}
def get_archive_hours(self): @staticmethod
hours = [] def series_times(archives):
for hour in range(24): return [archive.start if archive is not None else None for archive in archives]
chour = datetime.utcnow() - timedelta(hours=hour)
cday_archives = self.archive_set.all().filter(start__date=chour.date()).filter(start__hour=chour.hour) @staticmethod
hours.append(len(cday_archives) > 0) def series_csize(archives, units=None):
hours = ''.join(['H' if hour is True else '-' for hour in hours]) return [convert_bytes(archive.cache.unique_csize, units)[0]
return hours if archive is not None else None for archive in archives]
@staticmethod
def series_csize_pretty(archives):
return [archive.cache.unique_csize if archive is not None else None for archive in archives]
@staticmethod
def series_success_string(archives):
return ''.join(['H' if archive is not None else '-' for archive in archives])
def hourly_archive_string(self):
return ''.join(['H' if archive is not None else '-' for archive in self.hourly_archives(24)])
def daily_archives(self, n_days: int = 24):
archives = []
for day in range(n_days):
current_date = (datetime.utcnow() - timedelta(days=day)).date()
archive_current_date = self.archive_set.all()\
.filter(start__date=current_date)\
.order_by('-start')
if len(archive_current_date) > 0:
archives.append(archive_current_date[0])
else:
archives.append(None)
return archives
def hourly_archives(self, n_hours: int = 24):
archives = []
for hour in range(n_hours):
current_hour = datetime.utcnow() - timedelta(hours=hour)
archives_hour = self.archive_set.all()\
.filter(start__date=current_hour.date())\
.filter(start__hour=current_hour.hour)\
.order_by('-start')
if len(archives_hour) > 0:
archives.append(archives_hour[0])
else:
archives.append(None)
return archives

View File

@ -1,7 +1,46 @@
window.addEventListener("DOMContentLoaded", function () { window.addEventListener("DOMContentLoaded", function () {
const repoDict = JSON.parse(document.getElementById('hour_list').textContent);
const hour_json = JSON.parse(document.getElementById('hour_list').textContent); set_daily_graph(repoDict)
hour_json.forEach(function (repo) {
console.log(repo.hours);
})
}, false); }, false);
function set_daily_graph(repoDict) {
const labels = repoDict.date_labels;
const y_units = repoDict.units
var datasets = []
repoDict.repos.forEach(function (repo) {
datasets.push({label: repo.label,
data: repo.daily_size,
fill: false,
tension: 0.1,
borderColor: 'rgb(75, 192, 192)'});
})
const data = {
labels: labels,
datasets: datasets
};
const config = {
type: 'line',
data,
options: {
scales: {
y: {
ticks: {
display: true,
grid: false,
callback: function(value, index, values) {
return value + " " + y_units;
}
}
}
}
}
};
var myChart = new Chart(
document.getElementById('backup_csize_hourly'),
config
);
}

View File

@ -31,11 +31,11 @@
</dl> </dl>
<dl class="row"> <dl class="row">
<dt class="col-sm-3">Hourly backups:</dt> <dt class="col-sm-3">Hourly backups:</dt>
<dd class="col-sm-9">{{ repo.get_archive_hours }}</dd> <dd class="col-sm-9">{{ repo.hourly_archive_string }}</dd>
</dl> </dl>
<dl class="row"> <dl class="row">
<dt class="col-sm-3">Size:</dt> <dt class="col-sm-3">Size:</dt>
<dd class="col-sm-9">{{ repo.size }}</dd> <dd class="col-sm-9">{{ repo.size_string }}</dd>
</dl> </dl>
{% if repo.recent_errors|length > 0 %} {% if repo.recent_errors|length > 0 %}
<dl class="row"> <dl class="row">
@ -53,6 +53,9 @@
{% else %} {% else %}
<p>No repos found.</p> <p>No repos found.</p>
{% endif %} {% endif %}
<div class="col-md-4 rounded float-xl-left">
<canvas id="backup_csize_hourly" width="200" height="100"></canvas>
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,11 +1,24 @@
from math import floor, log from math import floor, log
bytes_in_unit = 1024 # Kibibyte
units = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "HiB")
# bytes_in_unit = 1000 # Kilobyte
# units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "HB")
def bytes_to_string(bytes: int):
suffixes = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "HB") def convert_bytes(c_bytes: int, unit: str = None) -> (float, str):
if bytes == 0: if c_bytes == 0:
return f"0{suffixes[0]}" return 0, units[0] if unit is None else unit
if unit is None:
index = int(floor(log(c_bytes, bytes_in_unit)))
else: else:
index = int(floor(log(bytes, 1024))) index = units.index(unit)
s = round(bytes / pow(1024, index), 2)
return f"{s}{suffixes[index]}" result = round(c_bytes / pow(bytes_in_unit, index), 2)
return result, units[index]
def bytes_to_string(c_bytes: int, unit: str = None) -> str:
n_bytes, unit = convert_bytes(c_bytes, unit)
return f"{n_bytes} {unit}"

View File

@ -5,19 +5,36 @@ from .models import Repo, Label, Archive, Cache, Error
from django.urls import reverse from django.urls import reverse
from .forms import RepoForm, ArchiveForm, ErrorForm from .forms import RepoForm, ArchiveForm, ErrorForm
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from .utility import data
from datetime import datetime, timedelta
def index(request): def index(request):
repo_list = Repo.objects.all() repo_list = Repo.objects.all()
hour_list = [repo.get_archive_hours_dict() for repo in repo_list] repo_dict = repo_daily_dict(repo_list, 24)
context = { context = {
'repo_list': repo_list, 'repo_list': repo_list,
'hour_list': hour_list 'hour_list': repo_dict
} }
return render(request, 'borg/index.html', context) return render(request, 'borg/index.html', context)
def repo_daily_dict(repo_list, n_days=14):
date_labels = list(reversed([(datetime.utcnow() - timedelta(days=day)).strftime("%d %b") for day in range(n_days)]))
max_repo_size = max(repo.latest_archive().cache.unique_csize for repo in repo_list)
_, max_unit = data.convert_bytes(max_repo_size)
repo_dicts = [repo.daily_dict(max_unit, n_days) for repo in repo_list]
return {
"date_labels": date_labels,
"repos": repo_dicts,
"units": max_unit
}
@permission_required("borg.add_repo") @permission_required("borg.add_repo")
def get_repo(request): def get_repo(request):
if request.method == 'POST': if request.method == 'POST':