Compare commits

..

No commits in common. "master" and "r4.02" have entirely different histories.

7 changed files with 7 additions and 199 deletions

72
app.py
View File

@ -5,9 +5,7 @@ import logging
import logging.config import logging.config
import os import os
from collections import defaultdict from flask import (Flask, request, jsonify, abort, render_template)
from flask import (Flask, request, jsonify, abort, render_template, url_for)
from ltsdb_json import LTS from ltsdb_json import LTS
from dashboard import Dashboard from dashboard import Dashboard
@ -154,71 +152,3 @@ def dashboard_index():
def dashboard_file(dashboard): def dashboard_file(dashboard):
d = Dashboard("dashboards/" + dashboard + ".json") d = Dashboard("dashboards/" + dashboard + ".json")
return d.as_html() return d.as_html()
@app.get("/nav")
def nav():
# Start with a list of all dimensions, the number of matching time series
# and a truncated list of series.
# If a dimension is chosen, display a choice of members
# choosing one or more members goes back to the list of
# (remaining) dimensions
with open("data/.index") as fh:
fcntl.flock(fh, fcntl.LOCK_SH)
index = json.load(fh)
timeseries = None
for k, v in request.args.lists():
if k[0] == ".":
continue
log.debug("search: %s -> %s", k, v)
if timeseries is None:
timeseries = set()
log.debug("search: %s: %s", k, index[k])
for m in v:
timeseries |= set(index[k][m])
else:
filter = set()
for m in v:
filter |= set(index[k][m])
timeseries &= filter
if timeseries is None:
timeseries = set()
for mc in index.values():
for tsl in mc.values():
timeseries |= set(tsl)
if d := request.args.get(".m"):
members = []
for m, tsl in index[d].items():
if set(tsl) & timeseries:
members.append(m)
return render_template("nav_member_select.html", dimension=d, members=members)
else:
params = request.args.to_dict(flat=False)
matching_dimensions = defaultdict(int)
for d, mc in index.items():
if d in params:
continue
for m, tsl in mc.items():
mtsl = set(tsl) & timeseries
if mtsl:
matching_dimensions[d] += len(mtsl)
matching_dimensions_list = []
for d in matching_dimensions:
params[".m"] = d
url = url_for("nav", **params)
app.logger.debug(f"{d=} {url=}")
matching_dimensions_list.append(
{"name": d, "count": matching_dimensions[d], "url": url}
)
total_timeseries = len(timeseries)
timeseries = [LTS(id=ts) for ts in list(timeseries)[:100]]
return render_template(
"nav_dimension_list.html",
matching_dimensions=matching_dimensions_list,
timeseries=timeseries, total_timeseries=total_timeseries)
#

View File

@ -27,11 +27,7 @@ class Dashboard:
if w.get("multi"): if w.get("multi"):
ts_list = LTS.find(w["data"][0]) ts_list = LTS.find(w["data"][0])
for ts in ts_list: for ts in ts_list:
try: tso = LTS(id=ts)
tso = LTS(id=ts)
except json.decoder.JSONDecodeError as e:
log.error("%s contains bad data: %s: Skipping", ts, e)
continue
if not tso.data: if not tso.data:
log.warning("%s has no data: Skipping", tso.id) log.warning("%s has no data: Skipping", tso.id)
continue continue
@ -392,14 +388,10 @@ class TimeSeries(Widget):
max_value = max([d[3] if len(d) >= 4 else d[1] for d in self.lts.data]) max_value = max([d[3] if len(d) >= 4 else d[1] for d in self.lts.data])
max_value = max(max_value, 0.001) # ensure positive max_value = max(max_value, 0.001) # ensure positive
unit = self.lts.description["unit"]
if self.yscale == "log": if self.yscale == "log":
try: try:
min_value = min(d[1] for d in self.lts.data if d[1] > 0) min_value = min(d[1] for d in self.lts.data if d[1] > 0)
if unit == "s": self.extra["min"] = "%g" % min_value
self.extra["min"] = "%g" % min_value + " (" + self.format_time(min_value) + ")"
else:
self.extra["min"] = "%g" % min_value
except ValueError: except ValueError:
# no non-negative values # no non-negative values
min_value = max_value / 2 min_value = max_value / 2
@ -413,12 +405,8 @@ class TimeSeries(Widget):
# Make sure min_value is less than max_value # Make sure min_value is less than max_value
min_value /= 2 min_value /= 2
log.debug("min_value = %s, max_value = %s", min_value, max_value) log.debug("min_value = %s, max_value = %s", min_value, max_value)
if unit == "s": self.extra["max"] = "%g" % max_value
self.extra["max"] = "%g" % max_value + " (" + self.format_time(max_value) + ")" self.extra["last"] = "%g" % data[-1][1]
self.extra["last"] = "%g" % data[-1][1] + " (" + self.format_time(data[-1][1]) + ")"
else:
self.extra["max"] = "%g" % max_value
self.extra["last"] = "%g" % data[-1][1]
log.debug("collecting data") log.debug("collecting data")
v_data = [] v_data = []
for i in range(n): for i in range(n):
@ -472,29 +460,6 @@ class TimeSeries(Widget):
log.debug("in as_html") log.debug("in as_html")
return Markup(render_template("timeseries.html", widget=self)) return Markup(render_template("timeseries.html", widget=self))
def format_time(self, seconds):
value = seconds
unit = "s"
if value >= 365.25 * 86400:
value /= 365.25 * 86400
unit = "years"
elif value >= 86400:
value /= 86400
unit = "days"
elif value >= 3600:
value /= 3600
unit = "h"
elif value >= 60:
value /= 60
unit = "m"
elif value >= 1:
pass
elif value >= 0.001:
value *= 1000
unit = "ms"
return f"{value:.2f} {unit}"
class Gauge(Widget): class Gauge(Widget):
def __init__(self, d): def __init__(self, d):
super().__init__(d) super().__init__(d)
@ -536,4 +501,3 @@ class Gauge(Widget):
self.lastvalue_formatted = Markup(f"<span class='value'>{value:.2f}</span><span class='unit'>{unit}</unit>") self.lastvalue_formatted = Markup(f"<span class='value'>{value:.2f}</span><span class='unit'>{unit}</unit>")
return Markup(render_template("gauge.html", widget=self)) return Markup(render_template("gauge.html", widget=self))
# vim: sw=4

View File

@ -41,11 +41,7 @@ class LTS:
with open(self.filename, "x+") as fh: with open(self.filename, "x+") as fh:
fcntl.flock(fh, fcntl.LOCK_EX) fcntl.flock(fh, fcntl.LOCK_EX)
json.dump({"description": self.description, "data": self.data}, fh) json.dump({"description": self.description, "data": self.data}, fh)
log.info(f"Created {self.filename}")
self.rebuild_index() self.rebuild_index()
except json.decoder.JSONDecodeError as e:
log.exception(f"Cannot decode JSON in {self.filename}: {e}")
raise
def pop(self, i): def pop(self, i):
# Pop the element at index i and adjust the min/max values of the # Pop the element at index i and adjust the min/max values of the
@ -131,11 +127,7 @@ class LTS:
(_, _, hash) = fn.rpartition("/") (_, _, hash) = fn.rpartition("/")
with open(fn, "r") as fh: with open(fn, "r") as fh:
fcntl.flock(fh, fcntl.LOCK_SH) fcntl.flock(fh, fcntl.LOCK_SH)
try: d = json.load(fh)
d = json.load(fh)
except json.decoder.JSONDecodeError as e:
log.exception(f"Cannot decode JSON in {fn}: {e}")
raise
for k, v in d["description"].items(): for k, v in d["description"].items():
d1 = index.setdefault(k, {}) d1 = index.setdefault(k, {})
d2 = d1.setdefault(v, []) d2 = d1.setdefault(v, [])

View File

@ -2,7 +2,6 @@
import logging import logging
import logging.config import logging.config
import math
import os import os
import socket import socket
import statistics import statistics
@ -54,29 +53,6 @@ class DiskFullPredictor:
log.info("d = %s, current_used_bytes = %s, current_usable_bytes = %s", m, current_used_bytes, current_usable_bytes) log.info("d = %s, current_used_bytes = %s, current_usable_bytes = %s", m, current_used_bytes, current_usable_bytes)
tuf = now - lts.data[i][0] tuf = now - lts.data[i][0]
break break
else:
# Try always use the minimum of a range.
# We prefer the first datapoint
first_used_bytes = lts.data[0][2] if len(lts.data[0]) >= 4 else lts.data[0][1]
# But if that's not useable we search the whole timeseries for the
# minimum
if first_used_bytes >= current_used_bytes:
first_used_bytes = current_used_bytes
first_i = None
for i in range(len(lts.data)):
used_bytes = lts.data[i][2] if len(lts.data[i]) >= 4 else lts.data[i][1]
if used_bytes < first_used_bytes:
first_used_bytes = used_bytes
first_i = i
else:
first_i = 0
if first_i is not None:
historic_growth = current_used_bytes / first_used_bytes
future_growth = current_usable_bytes / current_used_bytes
tuf = math.log(future_growth) / math.log(historic_growth) * (now - lts.data[first_i][0])
tuf = max(tuf, now - lts.data[first_i][0])
tuf = min(tuf, 1E9)
desc = {**lts.description, desc = {**lts.description,
"measure": "time_until_disk_full", "measure": "time_until_disk_full",
"node": node, "node": node,

View File

@ -8,9 +8,8 @@
body { body {
font-family: sans-serif; font-family: sans-serif;
} }
th, td { th {
text-align: left; text-align: left;
vertical-align: baseline;
} }
main { main {
display: flex; display: flex;

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width; initial-scale=1">
<meta charset="utf-8">
<style>
</style>
</head>
<body>
<ul>
{% for d in matching_dimensions %}
<li><a href="{{d['url']}}">{{d.name}}</a> ({{d.count}})</li>
{% endfor %}
</ul>
{{timeseries|length}}/{{total_timeseries}} timeseries:
<ul>
{% for ts in timeseries %}
<li>
<a href="/v?ts={{ts.id}}">{{ts.description}}</a>
</li>
{% endfor %}
</ul>
</body>
</html>

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width; initial-scale=1">
<meta charset="utf-8">
<style>
</style>
</head>
<body>
<form>
{% for dimension, members in request.args.lists() %}
{% for member in members %}
{% if dimension[0] != "." %}
<input name="{{dimension}}" value="{{member}}" type="hidden">
{% endif %}
{% endfor %}
{% endfor %}
<select name="{{dimension}}" multiple size={{members|length}}>
{% for member in members %}
<option>{{member}}</option>
{% endfor %}
</select>
<input type="submit">
</form>
</body>
</html>