refined grafana and prometheus
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from . import slides_bp
|
from . import slides_bp
|
||||||
from flask import current_app, abort, redirect, url_for, request, send_from_directory
|
from flask import current_app, abort, redirect, url_for, request, send_from_directory
|
||||||
from app.utils import get_cached_deck_info, token_required
|
from app.utils import get_cached_deck_info, token_required
|
||||||
from app.models import Deck, db, Course
|
from app.models import Deck, db, Course, PageView
|
||||||
import os
|
import os
|
||||||
import flask_login
|
import flask_login
|
||||||
import re
|
import re
|
||||||
@@ -115,6 +115,13 @@ def serve_deck(course_folder, deck, token, path="index.html"):
|
|||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"User {token.name} accessed deck {deck_info['deck_name']}"
|
f"User {token.name} accessed deck {deck_info['deck_name']}"
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
pv = PageView(deck_id=deck_info["deck_id"], token_id=token.id)
|
||||||
|
db.session.add(pv)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Failed to log page view: {e}")
|
||||||
return send_from_directory(deck_path, path)
|
return send_from_directory(deck_path, path)
|
||||||
|
|
||||||
log_404("File missing within deck", deck, course_folder, path, token)
|
log_404("File missing within deck", deck, course_folder, path, token)
|
||||||
|
|||||||
117
app/models.py
117
app/models.py
@@ -6,20 +6,29 @@ from typing import List, Optional
|
|||||||
import pytz
|
import pytz
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator, ForeignKey, UniqueConstraint
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
String,
|
||||||
|
TypeDecorator,
|
||||||
|
ForeignKey,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
db : SQLAlchemy = SQLAlchemy()
|
|
||||||
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
|
|
||||||
deck_password = db.Table(
|
deck_password = db.Table(
|
||||||
"deck_password",
|
"deck_password",
|
||||||
Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True),
|
Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True),
|
||||||
Column("password_id", db.Integer, db.ForeignKey("password.id"), primary_key=True)
|
Column("password_id", db.Integer, db.ForeignKey("password.id"), primary_key=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
deck_token = db.Table(
|
deck_token = db.Table(
|
||||||
"deck_token",
|
"deck_token",
|
||||||
Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True),
|
Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True),
|
||||||
Column("token_id", db.Integer, db.ForeignKey("token.id"), primary_key=True)
|
Column("token_id", db.Integer, db.ForeignKey("token.id"), primary_key=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,22 +47,29 @@ class UTCDateTime(TypeDecorator):
|
|||||||
value = value.replace(tzinfo=pytz.utc)
|
value = value.replace(tzinfo=pytz.utc)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Password(db.Model):
|
class Password(db.Model):
|
||||||
__tablename__ = "password"
|
__tablename__ = "password"
|
||||||
|
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
value : Mapped[str] = mapped_column(String(60))
|
value: Mapped[str] = mapped_column(String(60))
|
||||||
expires_at: Mapped[Optional[datetime]] = mapped_column(UTCDateTime()) # Optional expiration
|
expires_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
UTCDateTime()
|
||||||
|
) # Optional expiration
|
||||||
|
|
||||||
decks : Mapped[List[Deck]] = relationship("Deck", secondary=deck_password, back_populates='passwords')
|
decks: Mapped[List[Deck]] = relationship(
|
||||||
|
"Deck", secondary=deck_password, back_populates="passwords"
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, value: str, expires_at: datetime|None) -> None:
|
def __init__(self, value: str, expires_at: datetime | None) -> None:
|
||||||
self.value = value
|
self.value = value
|
||||||
self.expires_at = expires_at
|
self.expires_at = expires_at
|
||||||
|
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
"""Check if the password is expired"""
|
"""Check if the password is expired"""
|
||||||
return self.expires_at is not None and datetime.now(timezone.utc) > self.expires_at
|
return (
|
||||||
|
self.expires_at is not None and datetime.now(timezone.utc) > self.expires_at
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Password(value={self.value}, expires_at={self.expires_at})>"
|
return f"<Password(value={self.value}, expires_at={self.expires_at})>"
|
||||||
@@ -62,24 +78,33 @@ class Password(db.Model):
|
|||||||
class Deck(db.Model):
|
class Deck(db.Model):
|
||||||
__tablename__ = "deck"
|
__tablename__ = "deck"
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (UniqueConstraint("course_id", "deck", name="_course_deck_uc"),)
|
||||||
UniqueConstraint('course_id', 'deck', name='_course_deck_uc'),
|
|
||||||
)
|
|
||||||
|
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name : Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
||||||
deck : Mapped[str] = mapped_column(String(60), nullable=False)
|
deck: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||||
description : Mapped[str] = mapped_column(String(60))
|
description: Mapped[str] = mapped_column(String(60))
|
||||||
index_file: Mapped[str] = mapped_column(String(40), nullable=False)
|
index_file: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||||
export_file: Mapped[Optional[str]] = mapped_column(String(40), nullable=True)
|
export_file: Mapped[Optional[str]] = mapped_column(String(40), nullable=True)
|
||||||
|
|
||||||
passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks')
|
passwords: Mapped[List[Password]] = relationship(
|
||||||
tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, back_populates='decks')
|
"Password", secondary=deck_password, back_populates="decks"
|
||||||
|
)
|
||||||
|
tokens: Mapped[List[Token]] = relationship(
|
||||||
|
"Token", secondary=deck_token, back_populates="decks"
|
||||||
|
)
|
||||||
course_id: Mapped[int] = mapped_column(ForeignKey("course.id"))
|
course_id: Mapped[int] = mapped_column(ForeignKey("course.id"))
|
||||||
course: Mapped['Course'] = relationship("Course", back_populates='decks')
|
course: Mapped["Course"] = relationship("Course", back_populates="decks")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
def __init__(self, name: str, deck: str, course: Course, description: str = "", index_file: str = "index.html", export_file: str | None = None) -> None:
|
self,
|
||||||
|
name: str,
|
||||||
|
deck: str,
|
||||||
|
course: Course,
|
||||||
|
description: str = "",
|
||||||
|
index_file: str = "index.html",
|
||||||
|
export_file: str | None = None,
|
||||||
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
self.description = description
|
self.description = description
|
||||||
@@ -91,30 +116,40 @@ class Deck(db.Model):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Deck(name={self.name}, deck={self.deck}, course={self.course}, index={self.index_file})>"
|
return f"<Deck(name={self.name}, deck={self.deck}, course={self.course}, index={self.index_file})>"
|
||||||
|
|
||||||
|
|
||||||
class Token(db.Model):
|
class Token(db.Model):
|
||||||
__tablename__ = "token"
|
__tablename__ = "token"
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
||||||
active_course_id: Mapped[int] = mapped_column(ForeignKey("course.id"), nullable=True)
|
created_at: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False)
|
||||||
active_course: Mapped[Course] = relationship("Course", foreign_keys=[active_course_id])
|
active_course_id: Mapped[int] = mapped_column(
|
||||||
decks : Mapped[List[Deck]]= relationship("Deck", secondary=deck_token, back_populates='tokens')
|
ForeignKey("course.id"), nullable=True
|
||||||
|
)
|
||||||
|
active_course: Mapped[Course] = relationship(
|
||||||
|
"Course", foreign_keys=[active_course_id]
|
||||||
|
)
|
||||||
|
decks: Mapped[List[Deck]] = relationship(
|
||||||
|
"Deck", secondary=deck_token, back_populates="tokens"
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.created_at = datetime.now(timezone.utc)
|
||||||
self.active_course_id = current_app.config["DEFAULT_COURSE"]
|
self.active_course_id = current_app.config["DEFAULT_COURSE"]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Token(id={self.id}, name={self.name}, active_course_id={self.active_course_id})>"
|
return f"<Token(id={self.id}, name={self.name}, active_course_id={self.active_course_id})>"
|
||||||
|
|
||||||
def is_unlocked(self, deck: str) -> bool :
|
def is_unlocked(self, deck: str) -> bool:
|
||||||
return deck in [deck.deck for deck in self.decks]
|
return deck in [deck.deck for deck in self.decks]
|
||||||
|
|
||||||
|
|
||||||
class Course(db.Model):
|
class Course(db.Model):
|
||||||
__tablename__ = "course"
|
__tablename__ = "course"
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
folder: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
folder: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||||
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
||||||
decks: Mapped[List['Deck']] = relationship(back_populates='course')
|
decks: Mapped[List["Deck"]] = relationship(back_populates="course")
|
||||||
|
|
||||||
def __init__(self, name: str, folder: str) -> None:
|
def __init__(self, name: str, folder: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -126,11 +161,27 @@ class Course(db.Model):
|
|||||||
|
|
||||||
class InfoBanner(db.Model):
|
class InfoBanner(db.Model):
|
||||||
__tablename__ = "info_banner"
|
__tablename__ = "info_banner"
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
content : Mapped[str] = mapped_column(String(280))
|
content: Mapped[str] = mapped_column(String(280))
|
||||||
is_active : Mapped[bool] = mapped_column(Boolean)
|
is_active: Mapped[bool] = mapped_column(Boolean)
|
||||||
|
|
||||||
def __init__(self, content : str) -> None:
|
def __init__(self, content: str) -> None:
|
||||||
self.content = content
|
self.content = content
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
|
|
||||||
|
|
||||||
|
class PageView(db.Model):
|
||||||
|
__tablename__ = "page_view"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
deck_id: Mapped[int] = mapped_column(ForeignKey("deck.id"), nullable=False)
|
||||||
|
token_id: Mapped[int] = mapped_column(ForeignKey("token.id"), nullable=False)
|
||||||
|
viewed_at: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False)
|
||||||
|
|
||||||
|
deck: Mapped["Deck"] = relationship("Deck")
|
||||||
|
token: Mapped["Token"] = relationship("Token")
|
||||||
|
|
||||||
|
def __init__(self, deck_id: int, token_id: int) -> None:
|
||||||
|
self.deck_id = deck_id
|
||||||
|
self.token_id = token_id
|
||||||
|
self.viewed_at = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .models import Deck, InfoBanner, Token, db, Course
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_password_generator():
|
def init_password_generator():
|
||||||
wordfile = xkcd_password.locate_wordfile()
|
wordfile = xkcd_password.locate_wordfile()
|
||||||
global words
|
global words
|
||||||
@@ -225,6 +226,7 @@ def get_cached_deck_info(token_id, course_folder, deck_slug):
|
|||||||
"auth": is_unlocked,
|
"auth": is_unlocked,
|
||||||
"course_exists": True,
|
"course_exists": True,
|
||||||
"deck_exists": True,
|
"deck_exists": True,
|
||||||
|
"deck_id": deck_obj.id,
|
||||||
"deck_name": deck_obj.name,
|
"deck_name": deck_obj.name,
|
||||||
"index_file": deck_obj.index_file
|
"index_file": deck_obj.index_file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,22 @@
|
|||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "course_id",
|
||||||
|
"type": "query",
|
||||||
|
"datasource": "PostgreSQL",
|
||||||
|
"query": "SELECT id AS __value, name AS __text FROM course ORDER BY name",
|
||||||
|
"refresh": 2,
|
||||||
|
"multi": false,
|
||||||
|
"includeAll": true,
|
||||||
|
"allValue": null,
|
||||||
|
"hide": 0,
|
||||||
|
"label": "Course"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"datasource": "Prometheus",
|
"datasource": "Prometheus",
|
||||||
@@ -68,7 +84,6 @@
|
|||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": [
|
||||||
"lastNotNull"
|
"lastNotNull"
|
||||||
@@ -76,183 +91,21 @@
|
|||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
"textMode": "auto",
|
||||||
"wideLayout": true
|
"wideLayout": true
|
||||||
},
|
},
|
||||||
"pluginVersion": "12.3.3",
|
"pluginVersion": "12.3.3",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
|
||||||
"datasource": "Prometheus",
|
|
||||||
"expr": "probe_success{job=\"blackbox_http\"}",
|
|
||||||
"legendFormat": "{{ instance }}",
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"datasource": "Prometheus",
|
"datasource": "Prometheus",
|
||||||
"expr": "probe_success{job=\"blackbox_tcp\"}",
|
"expr": "probe_success{job=\"blackbox_tcp\"}",
|
||||||
"legendFormat": "{{ instance }}",
|
"legendFormat": "{{ instance }}",
|
||||||
"refId": "B"
|
"refId": "A"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Service Health",
|
"title": "Service Health",
|
||||||
"type": "stat"
|
"type": "stat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"datasource": "Prometheus",
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "ms"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [],
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.3.3",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": "Prometheus",
|
|
||||||
"expr": "probe_duration_seconds{job=\"blackbox_http\"} * 1000",
|
|
||||||
"legendFormat": "{{ instance }}",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "HTTP Response Time (ms)",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": "Prometheus",
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"mappings": [],
|
|
||||||
"max": 100,
|
|
||||||
"min": 0,
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "yellow",
|
|
||||||
"value": 95
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 99
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "background",
|
|
||||||
"graphMode": "area",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.3.3",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": "Prometheus",
|
|
||||||
"expr": "avg_over_time(probe_success{job=\"blackbox_http\", instance=\"http://tutortool:5000/grafana/health\"}[24h]) * 100",
|
|
||||||
"legendFormat": "Flask App",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Uptime % (24h)",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"datasource": "PostgreSQL",
|
"datasource": "PostgreSQL",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
@@ -264,10 +117,6 @@
|
|||||||
{
|
{
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"value": 0
|
"value": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -286,7 +135,6 @@
|
|||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": [
|
||||||
"lastNotNull"
|
"lastNotNull"
|
||||||
@@ -294,7 +142,6 @@
|
|||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
"textMode": "auto",
|
||||||
"wideLayout": true
|
"wideLayout": true
|
||||||
},
|
},
|
||||||
@@ -321,10 +168,6 @@
|
|||||||
{
|
{
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"value": 0
|
"value": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -343,7 +186,6 @@
|
|||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": [
|
||||||
"lastNotNull"
|
"lastNotNull"
|
||||||
@@ -351,7 +193,6 @@
|
|||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
"textMode": "auto",
|
||||||
"wideLayout": true
|
"wideLayout": true
|
||||||
},
|
},
|
||||||
@@ -378,10 +219,6 @@
|
|||||||
{
|
{
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"value": 0
|
"value": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -400,7 +237,6 @@
|
|||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": [
|
||||||
"lastNotNull"
|
"lastNotNull"
|
||||||
@@ -408,7 +244,6 @@
|
|||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
"textMode": "auto",
|
||||||
"wideLayout": true
|
"wideLayout": true
|
||||||
},
|
},
|
||||||
@@ -456,7 +291,6 @@
|
|||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": [
|
||||||
"lastNotNull"
|
"lastNotNull"
|
||||||
@@ -464,7 +298,6 @@
|
|||||||
"fields": "",
|
"fields": "",
|
||||||
"values": true
|
"values": true
|
||||||
},
|
},
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
"textMode": "auto",
|
||||||
"wideLayout": true
|
"wideLayout": true
|
||||||
},
|
},
|
||||||
@@ -480,93 +313,6 @@
|
|||||||
"title": "Users per Active Course",
|
"title": "Users per Active Course",
|
||||||
"type": "stat"
|
"type": "stat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"datasource": "PostgreSQL",
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"fillOpacity": 80,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"lineWidth": 1,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 10,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 16
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"barRadius": 0,
|
|
||||||
"barWidth": 0.7,
|
|
||||||
"fullHighlight": false,
|
|
||||||
"groupWidth": 0.7,
|
|
||||||
"legend": {
|
|
||||||
"calcs": [],
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "right",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"orientation": "horizontal",
|
|
||||||
"showValue": "never",
|
|
||||||
"stacking": "none",
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
},
|
|
||||||
"xTickLabelRotation": 0,
|
|
||||||
"xTickLabelSpacing": 0
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.3.3",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": "PostgreSQL",
|
|
||||||
"format": "table",
|
|
||||||
"rawSql": "SELECT d.name AS \"Deck\", COUNT(dt.token_id) AS \"Users\" FROM deck d JOIN course c ON d.course_id = c.id LEFT JOIN deck_token dt ON d.id = dt.deck_id WHERE c.name = 'GRNVS Tutorium SS 25' GROUP BY d.name ORDER BY regexp_replace(d.name, '\\D', '', 'g')::integer ASC NULLS LAST",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Users with Access per Deck — GRNVS Tutorium SS 25",
|
|
||||||
"type": "barchart"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"datasource": "PostgreSQL",
|
"datasource": "PostgreSQL",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
@@ -602,10 +348,6 @@
|
|||||||
{
|
{
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"value": 0
|
"value": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -616,9 +358,12 @@
|
|||||||
"h": 10,
|
"h": 10,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 26
|
"y": 16
|
||||||
},
|
},
|
||||||
"id": 9,
|
"id": 8,
|
||||||
|
"repeat": "course_id",
|
||||||
|
"repeatDirection": "v",
|
||||||
|
"maxPerRow": 1,
|
||||||
"options": {
|
"options": {
|
||||||
"barRadius": 0,
|
"barRadius": 0,
|
||||||
"barWidth": 0.7,
|
"barWidth": 0.7,
|
||||||
@@ -640,46 +385,148 @@
|
|||||||
},
|
},
|
||||||
"xTickLabelRotation": 0,
|
"xTickLabelRotation": 0,
|
||||||
"xTickLabelSpacing": 0
|
"xTickLabelSpacing": 0
|
||||||
},
|
|
||||||
"orientation": "horizontal",
|
|
||||||
"showValue": "auto",
|
|
||||||
"stacking": "none",
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
},
|
|
||||||
"xField": "Deck",
|
|
||||||
"xTickLabelRotation": 0,
|
|
||||||
"xTickLabelSpacing": 0
|
|
||||||
},
|
},
|
||||||
"pluginVersion": "12.3.3",
|
"pluginVersion": "12.3.3",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"datasource": "PostgreSQL",
|
"datasource": "PostgreSQL",
|
||||||
"format": "table",
|
"format": "table",
|
||||||
"rawSql": "SELECT d.name AS \"Deck\", COUNT(dt.token_id) AS \"Users\" FROM deck d JOIN course c ON d.course_id = c.id LEFT JOIN deck_token dt ON d.id = dt.deck_id WHERE c.name = 'IT-Sec Tutorium WS 25/26' GROUP BY d.name ORDER BY regexp_replace(d.name, '\\D', '', 'g')::integer ASC NULLS LAST",
|
"rawSql": "SELECT d.name AS \"Deck\", COUNT(dt.token_id) AS \"Users\" FROM deck d JOIN course c ON d.course_id = c.id LEFT JOIN deck_token dt ON d.id = dt.deck_id WHERE c.id = ${course_id} GROUP BY d.name ORDER BY regexp_replace(d.name, '\\D', '', 'g')::integer ASC NULLS LAST",
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Users with Access per Deck — IT-Sec Tutorium WS 25/26",
|
"title": "Users with Access per Deck \u2014 $course_id",
|
||||||
"type": "barchart"
|
"type": "barchart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "PostgreSQL",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "Views",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 2,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 100
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"repeat": "course_id",
|
||||||
|
"repeatDirection": "v",
|
||||||
|
"maxPerRow": 1,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "12.3.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": "PostgreSQL",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawSql": "SELECT date_trunc('hour', pv.viewed_at) AS time, d.name AS metric, COUNT(*) AS value FROM page_view pv JOIN deck d ON pv.deck_id = d.id JOIN course c ON d.course_id = c.id WHERE c.id = ${course_id} AND $__timeFilter(pv.viewed_at) GROUP BY 1, 2 ORDER BY 1",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Deck Views Over Time \u2014 $course_id",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "User Registrations Over Time",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 300 },
|
||||||
|
"datasource": "PostgreSQL",
|
||||||
|
"options": {
|
||||||
|
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": false },
|
||||||
|
"tooltip": { "mode": "single", "sort": "none" }
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"lineWidth": 1,
|
||||||
|
"showPoints": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": "PostgreSQL",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawSql": "SELECT date_trunc('day', created_at) AS time, COUNT(*) AS value FROM token WHERE $__timeFilter(created_at) GROUP BY 1 ORDER BY 1",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"preload": false,
|
"preload": false,
|
||||||
"refresh": "1m",
|
"refresh": "1m",
|
||||||
"schemaVersion": 42,
|
"schemaVersion": 42,
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"time": {
|
"time": {
|
||||||
"from": "now-6h",
|
"from": "now-7d",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {},
|
"timepicker": {},
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Tutor Tool",
|
"title": "Tutor Tool",
|
||||||
"uid": "tutor-tool",
|
"uid": "tutor-tool",
|
||||||
"version": 3
|
"version": 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
modules:
|
modules:
|
||||||
http_2xx:
|
|
||||||
prober: http
|
|
||||||
timeout: 5s
|
|
||||||
http:
|
|
||||||
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
|
|
||||||
valid_status_codes: [200]
|
|
||||||
method: GET
|
|
||||||
fail_if_ssl: false
|
|
||||||
fail_if_not_ssl: false
|
|
||||||
|
|
||||||
tcp_connect:
|
tcp_connect:
|
||||||
prober: tcp
|
prober: tcp
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
|
|||||||
@@ -3,25 +3,6 @@ global:
|
|||||||
evaluation_interval: 30s
|
evaluation_interval: 30s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
|
|
||||||
# --- Health checks via Blackbox Exporter ---
|
|
||||||
- job_name: 'blackbox_http'
|
|
||||||
metrics_path: /probe
|
|
||||||
params:
|
|
||||||
module: [http_2xx]
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- http://tutortool:5000/grafana/health
|
|
||||||
- http://tutortool:5000/
|
|
||||||
relabel_configs:
|
|
||||||
- source_labels: [__address__]
|
|
||||||
target_label: __param_target
|
|
||||||
- source_labels: [__param_target]
|
|
||||||
target_label: instance
|
|
||||||
- target_label: __address__
|
|
||||||
replacement: blackbox-exporter:9115
|
|
||||||
|
|
||||||
# --- TCP check for Postgres ---
|
|
||||||
- job_name: 'blackbox_tcp'
|
- job_name: 'blackbox_tcp'
|
||||||
metrics_path: /probe
|
metrics_path: /probe
|
||||||
params:
|
params:
|
||||||
@@ -30,6 +11,7 @@ scrape_configs:
|
|||||||
- targets:
|
- targets:
|
||||||
- db:5432
|
- db:5432
|
||||||
- redis:6379
|
- redis:6379
|
||||||
|
- tutortool:5000
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
@@ -37,13 +19,3 @@ scrape_configs:
|
|||||||
target_label: instance
|
target_label: instance
|
||||||
- target_label: __address__
|
- target_label: __address__
|
||||||
replacement: blackbox-exporter:9115
|
replacement: blackbox-exporter:9115
|
||||||
|
|
||||||
# --- Blackbox exporter itself ---
|
|
||||||
- job_name: 'blackbox_exporter'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['blackbox-exporter:9115']
|
|
||||||
|
|
||||||
# --- Prometheus itself ---
|
|
||||||
- job_name: 'prometheus'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['localhost:9090']
|
|
||||||
|
|||||||
Reference in New Issue
Block a user