Skip to content

Commit

Permalink
feat: add Prometheus monitoring (#120)
Browse files Browse the repository at this point in the history
Co-authored-by: nhnn <[email protected]>
Co-authored-by: Inex Code <[email protected]>
Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api/pulls/120
Co-authored-by: dettlaff <[email protected]>
Co-committed-by: dettlaff <[email protected]>
  • Loading branch information
3 people committed Jul 30, 2024
1 parent 1259c08 commit 4cd90d0
Show file tree
Hide file tree
Showing 15 changed files with 1,416 additions and 4 deletions.
1 change: 1 addition & 0 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pythonPackages.buildPythonPackage rec {
strawberry-graphql
typing-extensions
uvicorn
requests
websockets
httpx
];
Expand Down
120 changes: 120 additions & 0 deletions selfprivacy_api/graphql/queries/monitoring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import strawberry
from typing import Optional
from datetime import datetime
from selfprivacy_api.models.services import ServiceStatus
from selfprivacy_api.services.prometheus import Prometheus
from selfprivacy_api.utils.monitoring import (
MonitoringQueries,
MonitoringQueryError,
MonitoringValuesResult,
MonitoringMetricsResult,
)


@strawberry.type
class CpuMonitoring:
start: Optional[datetime]
end: Optional[datetime]
step: int

@strawberry.field
def overall_usage(self) -> MonitoringValuesResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.cpu_usage_overall(self.start, self.end, self.step)


@strawberry.type
class MemoryMonitoring:
start: Optional[datetime]
end: Optional[datetime]
step: int

@strawberry.field
def overall_usage(self) -> MonitoringValuesResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.memory_usage_overall(self.start, self.end, self.step)

@strawberry.field
def average_usage_by_service(self) -> MonitoringMetricsResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.memory_usage_average_by_slice(self.start, self.end)

@strawberry.field
def max_usage_by_service(self) -> MonitoringMetricsResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.memory_usage_max_by_slice(self.start, self.end)


@strawberry.type
class DiskMonitoring:
start: Optional[datetime]
end: Optional[datetime]
step: int

@strawberry.field
def overall_usage(self) -> MonitoringMetricsResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.disk_usage_overall(self.start, self.end, self.step)


@strawberry.type
class NetworkMonitoring:
start: Optional[datetime]
end: Optional[datetime]
step: int

@strawberry.field
def overall_usage(self) -> MonitoringMetricsResult:
if Prometheus().get_status() != ServiceStatus.ACTIVE:
return MonitoringQueryError(error="Prometheus is not running")

return MonitoringQueries.network_usage_overall(self.start, self.end, self.step)


@strawberry.type
class Monitoring:
@strawberry.field
def cpu_usage(
self,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
step: int = 60,
) -> CpuMonitoring:
return CpuMonitoring(start=start, end=end, step=step)

@strawberry.field
def memory_usage(
self,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
step: int = 60,
) -> MemoryMonitoring:
return MemoryMonitoring(start=start, end=end, step=step)

@strawberry.field
def disk_usage(
self,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
step: int = 60,
) -> DiskMonitoring:
return DiskMonitoring(start=start, end=end, step=step)

@strawberry.field
def network_usage(
self,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
step: int = 60,
) -> NetworkMonitoring:
return NetworkMonitoring(start=start, end=end, step=step)
6 changes: 6 additions & 0 deletions selfprivacy_api/graphql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from selfprivacy_api.graphql.queries.services import Services
from selfprivacy_api.graphql.queries.storage import Storage
from selfprivacy_api.graphql.queries.system import System
from selfprivacy_api.graphql.queries.monitoring import Monitoring

from selfprivacy_api.graphql.subscriptions.jobs import ApiJob
from selfprivacy_api.graphql.subscriptions.jobs import (
Expand Down Expand Up @@ -93,6 +94,11 @@ def backup(self) -> Backup:
"""Backup queries"""
return Backup()

@strawberry.field(permission_classes=[IsAuthenticated])
def monitoring(self) -> Monitoring:
"""Monitoring queries"""
return Monitoring()


@strawberry.type
class Mutation(
Expand Down
2 changes: 2 additions & 0 deletions selfprivacy_api/migrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
CheckForSystemRebuildJobs,
)
from selfprivacy_api.migrations.add_roundcube import AddRoundcube
from selfprivacy_api.migrations.add_monitoring import AddMonitoring

migrations = [
WriteTokenToRedis(),
CheckForSystemRebuildJobs(),
AddMonitoring(),
AddRoundcube(),
]

Expand Down
37 changes: 37 additions & 0 deletions selfprivacy_api/migrations/add_monitoring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from selfprivacy_api.migrations.migration import Migration

from selfprivacy_api.services.flake_service_manager import FlakeServiceManager
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevices


class AddMonitoring(Migration):
"""Adds monitoring service if it is not present."""

def get_migration_name(self) -> str:
return "add_monitoring"

def get_migration_description(self) -> str:
return "Adds the Monitoring if it is not present."

def is_migration_needed(self) -> bool:
with FlakeServiceManager() as manager:
if "monitoring" not in manager.services:
return True
with ReadUserData() as data:
if "monitoring" not in data["modules"]:
return True
return False

def migrate(self) -> None:
with FlakeServiceManager() as manager:
if "monitoring" not in manager.services:
manager.services["monitoring"] = (
"git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/monitoring"
)
with WriteUserData() as data:
if "monitoring" not in data["modules"]:
data["modules"]["monitoring"] = {
"enable": True,
"location": BlockDevices().get_root_block_device().name,
}
2 changes: 2 additions & 0 deletions selfprivacy_api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from selfprivacy_api.services.bitwarden import Bitwarden
from selfprivacy_api.services.forgejo import Forgejo
from selfprivacy_api.services.jitsimeet import JitsiMeet
from selfprivacy_api.services.prometheus import Prometheus
from selfprivacy_api.services.roundcube import Roundcube
from selfprivacy_api.services.mailserver import MailServer
from selfprivacy_api.services.nextcloud import Nextcloud
Expand All @@ -21,6 +22,7 @@
Ocserv(),
JitsiMeet(),
Roundcube(),
Prometheus(),
]


Expand Down
6 changes: 3 additions & 3 deletions selfprivacy_api/services/forgejo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ class Forgejo(Service):
"forgejo-auto",
"forgejo-light",
"forgejo-dark",
"auto",
"gitea",
"arc-green",
"gitea-auto",
"gitea-light",
"gitea-dark",
],
),
}
Expand Down
86 changes: 86 additions & 0 deletions selfprivacy_api/services/prometheus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Class representing Nextcloud service."""

import base64
import subprocess
from typing import Optional, List

from selfprivacy_api.services.owned_path import OwnedPath
from selfprivacy_api.utils.systemd import get_service_status
from selfprivacy_api.services.service import Service, ServiceStatus

from selfprivacy_api.services.prometheus.icon import PROMETHEUS_ICON


class Prometheus(Service):
"""Class representing Prometheus service."""

@staticmethod
def get_id() -> str:
return "monitoring"

@staticmethod
def get_display_name() -> str:
return "Prometheus"

@staticmethod
def get_description() -> str:
return "Prometheus is used for resource monitoring and alerts."

@staticmethod
def get_svg_icon() -> str:
return base64.b64encode(PROMETHEUS_ICON.encode("utf-8")).decode("utf-8")

@staticmethod
def get_url() -> Optional[str]:
"""Return service url."""
return None

@staticmethod
def get_subdomain() -> Optional[str]:
return None

@staticmethod
def is_movable() -> bool:
return False

@staticmethod
def is_required() -> bool:
return True

@staticmethod
def can_be_backed_up() -> bool:
return False

@staticmethod
def get_backup_description() -> str:
return "Backups are not available for Prometheus."

@staticmethod
def get_status() -> ServiceStatus:
return get_service_status("prometheus.service")

@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "prometheus.service"])

@staticmethod
def start():
subprocess.run(["systemctl", "start", "prometheus.service"])

@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "prometheus.service"])

@staticmethod
def get_logs():
return ""

@staticmethod
def get_owned_folders() -> List[OwnedPath]:
return [
OwnedPath(
path="/var/lib/prometheus",
owner="prometheus",
group="prometheus",
),
]
5 changes: 5 additions & 0 deletions selfprivacy_api/services/prometheus/icon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PROMETHEUS_ICON = """
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M64.125 0.51C99.229 0.517 128.045 29.133 128 63.951C127.955 99.293 99.258 127.515 63.392 127.49C28.325 127.466 -0.0249987 98.818 1.26289e-06 63.434C0.0230013 28.834 28.898 0.503 64.125 0.51ZM44.72 22.793C45.523 26.753 44.745 30.448 43.553 34.082C42.73 36.597 41.591 39.022 40.911 41.574C39.789 45.777 38.52 50.004 38.052 54.3C37.381 60.481 39.81 65.925 43.966 71.34L24.86 67.318C24.893 67.92 24.86 68.148 24.925 68.342C26.736 73.662 29.923 78.144 33.495 82.372C33.872 82.818 34.732 83.046 35.372 83.046C54.422 83.084 73.473 83.08 92.524 83.055C93.114 83.055 93.905 82.945 94.265 82.565C98.349 78.271 101.47 73.38 103.425 67.223L83.197 71.185C84.533 68.567 86.052 66.269 86.93 63.742C89.924 55.099 88.682 46.744 84.385 38.862C80.936 32.538 77.754 26.242 79.475 18.619C75.833 22.219 74.432 26.798 73.543 31.517C72.671 36.167 72.154 40.881 71.478 45.6C71.38 45.457 71.258 45.35 71.236 45.227C71.1507 44.7338 71.0919 44.2365 71.06 43.737C70.647 36.011 69.14 28.567 65.954 21.457C64.081 17.275 62.013 12.995 63.946 8.001C62.639 8.694 61.456 9.378 60.608 10.357C58.081 13.277 57.035 16.785 56.766 20.626C56.535 23.908 56.22 27.205 55.61 30.432C54.97 33.824 53.96 37.146 51.678 40.263C50.76 33.607 50.658 27.019 44.722 22.793H44.72ZM93.842 88.88H34.088V99.26H93.842V88.88ZM45.938 104.626C45.889 113.268 54.691 119.707 65.571 119.24C74.591 118.851 82.57 111.756 81.886 104.626H45.938Z" fill="black"/>
</svg>
"""
4 changes: 3 additions & 1 deletion selfprivacy_api/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ def check_if_subdomain_is_taken(subdomain: str) -> bool:
with ReadUserData() as data:
for module in data["modules"]:
if (
data["modules"][module].get("subdomain", DEFAULT_SUBDOMAINS[module])
data["modules"][module].get(
"subdomain", DEFAULT_SUBDOMAINS.get(module, "")
)
== subdomain
):
return True
Expand Down
1 change: 1 addition & 0 deletions selfprivacy_api/utils/default_subdomains.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"pleroma": "social",
"roundcube": "roundcube",
"testservice": "test",
"monitoring": "",
}

RESERVED_SUBDOMAINS = [
Expand Down
Loading

0 comments on commit 4cd90d0

Please sign in to comment.