mirror of
https://forgejo.altau.su/lego/lego-monitoring.git
synced 2026-03-09 20:31:10 +00:00
network monitoring
This commit is contained in:
parent
8af7b683b6
commit
8b18d407d7
21 changed files with 434 additions and 53 deletions
|
|
@ -33,7 +33,7 @@ List of enabled check sets\. Each check set is a module which checks something a
|
|||
|
||||
|
||||
*Type:*
|
||||
list of (one of “start”, “stop”, “cpu”, “ram”, “temp”, “vulnix”)
|
||||
list of (one of “start”, “stop”, “remind”, “cpu”, “ram”, “temp”, “net”, “vulnix”)
|
||||
|
||||
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ CPU load percentage for a critical alert to be sent\. Null means never generate
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ CPU load percentage for a warning alert to be sent\. Null means never generate a
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
@ -85,6 +85,166 @@ null or floating point number
|
|||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces
|
||||
|
||||
|
||||
|
||||
Per-interface configuration of IO byte thresholds\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
attribute set of (submodule)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` { } `
|
||||
|
||||
|
||||
|
||||
*Example:*
|
||||
|
||||
```
|
||||
{
|
||||
br0 = {
|
||||
warningThresholdCombBytes = 700 * 1024 * 128; # 700 Megabits
|
||||
criticalThresholdCombBytes = 1 * 1024 * 1024 * 128; # 1 Gigabit
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.criticalThresholdCombBytes
|
||||
|
||||
|
||||
|
||||
Combined (sent + received) bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.criticalThresholdRecvBytes
|
||||
|
||||
|
||||
|
||||
Received bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.criticalThresholdSentBytes
|
||||
|
||||
|
||||
|
||||
Sent bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.warningThresholdCombBytes
|
||||
|
||||
|
||||
|
||||
Combined (sent + received) bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.warningThresholdRecvBytes
|
||||
|
||||
|
||||
|
||||
Received bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.net\.interfaces\.\<name>\.warningThresholdSentBytes
|
||||
|
||||
|
||||
|
||||
Sent bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or (positive integer, meaning >0)
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
*Declared by:*
|
||||
- [modules/options\.nix](../modules/options.nix)
|
||||
|
||||
|
||||
|
||||
## services\.lego-monitoring\.checks\.ram\.criticalPercentage
|
||||
|
||||
|
||||
|
|
@ -94,7 +254,7 @@ RAM usage percentage for a critical alert to be sent\. Null means never generate
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
@ -115,7 +275,7 @@ RAM usage percentage for a warning alert to be sent\. Null means never generate
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
@ -265,7 +425,7 @@ Critical temperature threshold\.
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
@ -307,7 +467,7 @@ Warning temperature threshold\.
|
|||
|
||||
|
||||
*Type:*
|
||||
null or floating point number
|
||||
null or (positive integer or floating point number, meaning >0)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
23
flake.lock
generated
23
flake.lock
generated
|
|
@ -16,6 +16,22 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-29335f": {
|
||||
"locked": {
|
||||
"lastModified": 1745804731,
|
||||
"narHash": "sha256-v/sK3AS0QKu/Tu5sHIfddiEHCvrbNYPv8X10Fpux68g=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "29335f23bea5e34228349ea739f31ee79e267b88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "29335f23bea5e34228349ea739f31ee79e267b88",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pyproject-build-systems": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
|
|
@ -65,6 +81,7 @@
|
|||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-29335f": "nixpkgs-29335f",
|
||||
"pyproject-build-systems": "pyproject-build-systems",
|
||||
"pyproject-nix": "pyproject-nix",
|
||||
"uv2nix": "uv2nix"
|
||||
|
|
@ -80,11 +97,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745697651,
|
||||
"narHash": "sha256-r4A/fkiCenEapHkjJWPiNUZEfviuXMCr6mRozJ5dC4o=",
|
||||
"lastModified": 1749170547,
|
||||
"narHash": "sha256-zOptuFhTr9P0A+unFaOBFx5E5T6yx0qE8VrUGVrM96U=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "cb6508484d534dafd097713b575f2aebc3417de0",
|
||||
"rev": "7ae60727d4fc2e41aefd30da665e4f92ba8298f1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
# this is for uv 0.6.17, 0.7.0 has a change uv2nix doesn't yet support: https://github.com/astral-sh/uv/pull/13176
|
||||
nixpkgs-29335f.url = "github:nixos/nixpkgs/29335f23bea5e34228349ea739f31ee79e267b88";
|
||||
|
||||
pyproject-nix = {
|
||||
url = "github:pyproject-nix/pyproject.nix";
|
||||
|
|
@ -25,6 +27,7 @@
|
|||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nixpkgs-29335f,
|
||||
uv2nix,
|
||||
pyproject-nix,
|
||||
pyproject-build-systems,
|
||||
|
|
@ -78,6 +81,7 @@
|
|||
|
||||
# This example is only using x86_64-linux
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
pkgs-29335f = nixpkgs-29335f.legacyPackages.x86_64-linux;
|
||||
|
||||
# Use Python 3.12 from nixpkgs
|
||||
python = pkgs.python312;
|
||||
|
|
@ -125,7 +129,7 @@
|
|||
impure = pkgs.mkShell {
|
||||
packages = [
|
||||
python
|
||||
pkgs.uv
|
||||
pkgs-29335f.uv
|
||||
];
|
||||
env =
|
||||
{
|
||||
|
|
@ -205,7 +209,7 @@
|
|||
pkgs.mkShell {
|
||||
packages = [
|
||||
virtualenv
|
||||
pkgs.uv
|
||||
pkgs-29335f.uv
|
||||
];
|
||||
|
||||
env = {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ package:
|
|||
warning_percentage = warningPercentage;
|
||||
critical_percentage = criticalPercentage;
|
||||
};
|
||||
|
||||
net.interfaces = lib.mapAttrs (_: interfaceCfg: {
|
||||
warning_threshold_sent_bytes = interfaceCfg.warningThresholdSentBytes;
|
||||
critical_threshold_sent_bytes = interfaceCfg.criticalThresholdSentBytes;
|
||||
warning_threshold_recv_bytes = interfaceCfg.warningThresholdRecvBytes;
|
||||
critical_threshold_recv_bytes = interfaceCfg.warningThresholdRecvBytes;
|
||||
warning_threshold_comb_bytes = interfaceCfg.warningThresholdCombBytes;
|
||||
critical_threshold_comb_bytes = interfaceCfg.criticalThresholdCombBytes;
|
||||
}) cfg.checks.net.interfaces;
|
||||
};
|
||||
};
|
||||
in lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
let
|
||||
tempSensorOptions = (import ./suboptions/tempSensorOptions.nix) { inherit lib; };
|
||||
vulnixWhitelistRule = (import ./suboptions/vulnixWhitelistRule.nix) { inherit lib; };
|
||||
netInterfaceOptions = (import ./suboptions/netInterfaceOptions.nix) { inherit lib; };
|
||||
in
|
||||
{
|
||||
options.services.lego-monitoring = {
|
||||
|
|
@ -32,6 +33,7 @@ in
|
|||
"cpu"
|
||||
"ram"
|
||||
"temp"
|
||||
"net"
|
||||
|
||||
"vulnix"
|
||||
]);
|
||||
|
|
@ -96,12 +98,12 @@ in
|
|||
|
||||
cpu = {
|
||||
warningPercentage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = 80.0;
|
||||
description = "CPU load percentage for a warning alert to be sent. Null means never generate a CPU warning alert.";
|
||||
};
|
||||
criticalPercentage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = 90.0;
|
||||
description = "CPU load percentage for a critical alert to be sent. Null means never generate a CPU critical alert.";
|
||||
};
|
||||
|
|
@ -109,16 +111,31 @@ in
|
|||
|
||||
ram = {
|
||||
warningPercentage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = 80.0;
|
||||
description = "RAM usage percentage for a warning alert to be sent. Null means never generate a RAM warning alert.";
|
||||
};
|
||||
criticalPercentage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = 90.0;
|
||||
description = "RAM usage percentage for a critical alert to be sent. Null means never generate a RAM critical alert.";
|
||||
};
|
||||
};
|
||||
|
||||
net = {
|
||||
interfaces = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule netInterfaceOptions);
|
||||
default = { };
|
||||
description = "Per-interface configuration of IO byte thresholds.";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
br0 = {
|
||||
warningThresholdCombBytes = 700 * 1024 * 128; # 700 Megabits
|
||||
criticalThresholdCombBytes = 1 * 1024 * 1024 * 128; # 1 Gigabit
|
||||
};
|
||||
}'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
39
modules/suboptions/netInterfaceOptions.nix
Normal file
39
modules/suboptions/netInterfaceOptions.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
options = {
|
||||
warningThresholdSentBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Sent bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
criticalThresholdSentBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Sent bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
warningThresholdRecvBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Received bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
criticalThresholdRecvBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Received bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
warningThresholdCombBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Combined (sent + received) bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
criticalThresholdCombBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
description = "Combined (sent + received) bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked.";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -16,12 +16,12 @@ let
|
|||
description = "Whether this reading is enabled.";
|
||||
};
|
||||
warningTemp = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = null;
|
||||
description = "Warning temperature threshold.";
|
||||
};
|
||||
criticalTemp = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.float;
|
||||
type = lib.types.nullOr lib.types.numbers.positive;
|
||||
default = null;
|
||||
description = "Critical temperature threshold.";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ readme = "README.md"
|
|||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"alt-utils>=0.0.8",
|
||||
"humanize>=4.12.3",
|
||||
"psutil>=7.0.0",
|
||||
"telethon>=1.40.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ async def async_main():
|
|||
is_reminder=True,
|
||||
)
|
||||
],
|
||||
check_sets.NET: [
|
||||
IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True)
|
||||
],
|
||||
}
|
||||
|
||||
checkers = []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from .enum import AlertType, Severity
|
||||
|
||||
|
|
@ -8,3 +9,4 @@ class Alert:
|
|||
alert_type: AlertType
|
||||
message: str
|
||||
severity: Severity
|
||||
created: datetime = field(default_factory=datetime.now)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@ from enum import IntEnum, StrEnum
|
|||
|
||||
class AlertType(StrEnum):
|
||||
BOOT = "BOOT"
|
||||
CPU = "CPU"
|
||||
ERROR = "ERROR"
|
||||
TEST = "TEST"
|
||||
REMIND = "REMIND"
|
||||
|
||||
CPU = "CPU"
|
||||
NET = "NET"
|
||||
RAM = "RAM"
|
||||
TEMP = "TEMP"
|
||||
TEST = "TEST"
|
||||
|
||||
VULN = "VULN"
|
||||
REMIND = "REMIND"
|
||||
# LOGIN = "LOGIN"
|
||||
# SMART = "SMART" # TODO
|
||||
# RAID = "RAID"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ async def get_client() -> TelegramClient:
|
|||
|
||||
def format_message(alert: Alert, note: str) -> str:
|
||||
severity_emoji = SEVERITY_TO_EMOJI[alert.severity]
|
||||
note_formatted = f" - <i>{note}</i>" if note else ""
|
||||
message = f"{severity_emoji} {alert.alert_type} Alert{note_formatted}\n{alert.message}"
|
||||
note_formatted = f"{note}, " if note else ""
|
||||
if "ongoing" in note_formatted:
|
||||
note_formatted += f"since {alert.created.isoformat()}"
|
||||
else:
|
||||
note_formatted += f"at {alert.created.isoformat()}"
|
||||
message = f"{severity_emoji} {alert.alert_type} Alert - <i>{note_formatted}</i>\n{alert.message}"
|
||||
return message
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from .cpu import cpu_check
|
||||
from .net import NetIOTracker
|
||||
from .ram import ram_check
|
||||
from .remind import remind_check
|
||||
from .temp import temp_check
|
||||
|
|
|
|||
|
|
@ -10,19 +10,19 @@ IS_TESTING = False
|
|||
def cpu_check() -> list[Alert]:
|
||||
percentage = cpu_percent()
|
||||
config = cvars.config.get().checks.cpu
|
||||
if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage):
|
||||
if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage):
|
||||
return [
|
||||
Alert(
|
||||
alert_type=AlertType.CPU,
|
||||
message=f"CPU load: {percentage:.2f}% > {config.critical_percentage:.2f}%",
|
||||
message=f"CPU load: {percentage:.2f}% >= {config.critical_percentage:.2f}%",
|
||||
severity=Severity.CRITICAL,
|
||||
)
|
||||
]
|
||||
elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage):
|
||||
elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage):
|
||||
return [
|
||||
Alert(
|
||||
alert_type=AlertType.CPU,
|
||||
message=f"CPU load: {percentage:.2f}% > {config.warning_percentage:.2f}%",
|
||||
message=f"CPU load: {percentage:.2f}% >= {config.warning_percentage:.2f}%",
|
||||
severity=Severity.WARNING,
|
||||
)
|
||||
]
|
||||
|
|
|
|||
87
src/lego_monitoring/checks/net.py
Normal file
87
src/lego_monitoring/checks/net.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from humanize import naturalsize
|
||||
from psutil import net_io_counters
|
||||
|
||||
from lego_monitoring.alerting.alert import Alert
|
||||
from lego_monitoring.alerting.enum import AlertType, Severity
|
||||
from lego_monitoring.core import cvars
|
||||
|
||||
IS_TESTING = False
|
||||
SECONDS_BETWEEN_CHECKS = 5 * 60
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetIOTracker:
|
||||
sent_per_interface: dict[str, int] = field(default_factory=dict, init=False)
|
||||
recv_per_interface: dict[str, int] = field(default_factory=dict, init=False)
|
||||
|
||||
@staticmethod
|
||||
def check_threshold(
|
||||
current_stat_bytes_per_sec: float,
|
||||
critical_threshold: Optional[int],
|
||||
warning_threshold: Optional[int],
|
||||
stat_name: str,
|
||||
interface: str,
|
||||
) -> Optional[Alert]:
|
||||
if critical_threshold and (IS_TESTING or current_stat_bytes_per_sec >= critical_threshold):
|
||||
current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True)
|
||||
critical_threshold_natural = naturalsize(critical_threshold, binary=True)
|
||||
return Alert(
|
||||
alert_type=AlertType.NET,
|
||||
message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {critical_threshold_natural}/s",
|
||||
severity=Severity.CRITICAL,
|
||||
)
|
||||
elif warning_threshold and (IS_TESTING or current_stat_bytes_per_sec >= warning_threshold):
|
||||
current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True)
|
||||
warning_threshold_natural = naturalsize(warning_threshold, binary=True)
|
||||
return Alert(
|
||||
alert_type=AlertType.NET,
|
||||
message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {warning_threshold_natural}/s",
|
||||
severity=Severity.WARNING,
|
||||
)
|
||||
|
||||
def net_check(self) -> list[Alert]:
|
||||
alerts = []
|
||||
current_stats = net_io_counters(pernic=True)
|
||||
config = cvars.config.get().checks.net
|
||||
for interface, thresholds in config.interfaces.items():
|
||||
if interface in self.sent_per_interface and interface in self.recv_per_interface:
|
||||
sent_since_last_check_per_sec = (
|
||||
current_stats[interface].bytes_sent - self.sent_per_interface[interface]
|
||||
) / SECONDS_BETWEEN_CHECKS
|
||||
recv_since_last_check_per_sec = (
|
||||
current_stats[interface].bytes_recv - self.recv_per_interface[interface]
|
||||
) / SECONDS_BETWEEN_CHECKS
|
||||
comb_since_last_check_per_sec = sent_since_last_check_per_sec + recv_since_last_check_per_sec
|
||||
|
||||
if alert := self.check_threshold(
|
||||
sent_since_last_check_per_sec,
|
||||
thresholds.critical_threshold_sent_bytes,
|
||||
thresholds.warning_threshold_sent_bytes,
|
||||
"sent",
|
||||
interface,
|
||||
):
|
||||
alerts.append(alert)
|
||||
if alert := self.check_threshold(
|
||||
recv_since_last_check_per_sec,
|
||||
thresholds.critical_threshold_recv_bytes,
|
||||
thresholds.warning_threshold_recv_bytes,
|
||||
"recv",
|
||||
interface,
|
||||
):
|
||||
alerts.append(alert)
|
||||
if alert := self.check_threshold(
|
||||
comb_since_last_check_per_sec,
|
||||
thresholds.critical_threshold_comb_bytes,
|
||||
thresholds.warning_threshold_comb_bytes,
|
||||
"comb",
|
||||
interface,
|
||||
):
|
||||
alerts.append(alert)
|
||||
|
||||
self.sent_per_interface[interface] = current_stats[interface].bytes_sent
|
||||
self.recv_per_interface[interface] = current_stats[interface].bytes_recv
|
||||
|
||||
return alerts
|
||||
|
|
@ -10,19 +10,19 @@ IS_TESTING = False
|
|||
def ram_check() -> list[Alert]:
|
||||
percentage = virtual_memory().percent
|
||||
config = cvars.config.get().checks.ram
|
||||
if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage):
|
||||
if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage):
|
||||
return [
|
||||
Alert(
|
||||
alert_type=AlertType.RAM,
|
||||
message=f"RAM usage: {percentage:.2f}% > {config.critical_percentage:.2f}%",
|
||||
message=f"RAM usage: {percentage:.2f}% >= {config.critical_percentage:.2f}%",
|
||||
severity=Severity.CRITICAL,
|
||||
)
|
||||
]
|
||||
elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage):
|
||||
elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage):
|
||||
return [
|
||||
Alert(
|
||||
alert_type=AlertType.RAM,
|
||||
message=f"RAM usage: {percentage:.2f}% > {config.warning_percentage:.2f}%",
|
||||
message=f"RAM usage: {percentage:.2f}% >= {config.warning_percentage:.2f}%",
|
||||
severity=Severity.WARNING,
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ def temp_check() -> list[Alert]:
|
|||
temps = sensors.get_readings()
|
||||
for sensor, readings in temps.items():
|
||||
for r in readings:
|
||||
if r.critical_temp is not None and (IS_TESTING or r.current_temp > r.critical_temp):
|
||||
if r.critical_temp is not None and (IS_TESTING or r.current_temp >= r.critical_temp):
|
||||
alert = Alert(
|
||||
alert_type=AlertType.TEMP,
|
||||
message=f"{sensor} {r.label}: {r.current_temp}°C > {r.critical_temp}°C",
|
||||
message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.critical_temp}°C",
|
||||
severity=Severity.CRITICAL,
|
||||
)
|
||||
elif r.warning_temp is not None and (IS_TESTING or r.current_temp > r.warning_temp):
|
||||
elif r.warning_temp is not None and (IS_TESTING or r.current_temp >= r.warning_temp):
|
||||
alert = Alert(
|
||||
alert_type=AlertType.TEMP,
|
||||
message=f"{sensor} {r.label}: {r.current_temp}°C > {r.warning_temp}°C",
|
||||
message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.warning_temp}°C",
|
||||
severity=Severity.WARNING,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from alt_utils import NestedDeserializableDataclass
|
|||
|
||||
from . import enums
|
||||
from .checks.cpu import CpuCheckConfig
|
||||
from .checks.net import NetCheckConfig
|
||||
from .checks.ram import RamCheckConfig
|
||||
from .checks.temp import TempCheckConfig
|
||||
from .checks.vulnix import VulnixCheckConfig
|
||||
|
|
@ -17,6 +18,7 @@ class ChecksConfig(NestedDeserializableDataclass):
|
|||
ram: RamCheckConfig = field(default_factory=RamCheckConfig)
|
||||
temp: TempCheckConfig = field(default_factory=TempCheckConfig)
|
||||
vulnix: Optional[VulnixCheckConfig] = None # vulnix check WILL raise if this config section is None
|
||||
net: NetCheckConfig = field(default_factory=NetCheckConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
19
src/lego_monitoring/config/checks/net.py
Normal file
19
src/lego_monitoring/config/checks/net.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from alt_utils import NestedDeserializableDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetInterfaceConfig:
|
||||
warning_threshold_sent_bytes: Optional[int] = None
|
||||
critical_threshold_sent_bytes: Optional[int] = None
|
||||
warning_threshold_recv_bytes: Optional[int] = None
|
||||
critical_threshold_recv_bytes: Optional[int] = None
|
||||
warning_threshold_comb_bytes: Optional[int] = None
|
||||
critical_threshold_comb_bytes: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetCheckConfig(NestedDeserializableDataclass):
|
||||
interfaces: dict[str, NetInterfaceConfig] = field(default_factory=dict)
|
||||
|
|
@ -9,6 +9,7 @@ class CheckSet(StrEnum):
|
|||
CPU = "cpu"
|
||||
RAM = "ram"
|
||||
TEMP = "temp"
|
||||
NET = "net"
|
||||
|
||||
VULNIX = "vulnix"
|
||||
|
||||
|
|
|
|||
46
uv.lock
generated
46
uv.lock
generated
|
|
@ -1,13 +1,23 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "alt-utils"
|
||||
version = "0.0.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103, upload_time = "2025-05-10T19:36:49.187Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378, upload_time = "2025-05-10T19:36:47.954Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humanize"
|
||||
version = "4.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload_time = "2025-04-30T11:51:07.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload_time = "2025-04-30T11:51:06.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -16,6 +26,7 @@ version = "0.1.0"
|
|||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "alt-utils" },
|
||||
{ name = "humanize" },
|
||||
{ name = "psutil" },
|
||||
{ name = "telethon" },
|
||||
]
|
||||
|
|
@ -23,6 +34,7 @@ dependencies = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "alt-utils", specifier = ">=0.0.8" },
|
||||
{ name = "humanize", specifier = ">=4.12.3" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "telethon", specifier = ">=1.40.0" },
|
||||
]
|
||||
|
|
@ -31,30 +43,30 @@ requires-dist = [
|
|||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload_time = "2025-02-13T21:54:07.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload_time = "2025-02-13T21:54:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload_time = "2025-02-13T21:54:16.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload_time = "2025-02-13T21:54:18.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload_time = "2025-02-13T21:54:21.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload_time = "2025-02-13T21:54:24.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload_time = "2025-02-13T21:54:34.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyaes"
|
||||
version = "1.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload_time = "2017-09-20T21:17:54.23Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload_time = "2024-09-10T22:41:42.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload_time = "2024-09-11T16:00:36.122Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -64,9 +76,9 @@ source = { registry = "https://pypi.org/simple" }
|
|||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload_time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -77,7 +89,7 @@ dependencies = [
|
|||
{ name = "pyaes" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723, upload_time = "2025-04-21T09:12:10.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013, upload_time = "2025-04-21T09:12:08.399Z" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue