Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devel #29

Merged
merged 20 commits into from
Apr 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__pycache__/
*/__pycache__/
__pycache__
*.pyc
monitor-agent.log
monitor.log
2 changes: 0 additions & 2 deletions monitor_agent/core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ def _executeCommand(command: str, timeout: int) -> typing.Tuple[str, str, int, f
# https://stackoverflow.com/questions/64948722/error-unicodedecodeerror-utf-8-codec-cant-decode-byte-0xbe-in-position-2-in
# shell=True MUST NOT BE USED:
# https://stackoverflow.com/questions/48763362/python-subprocess-kill-with-timeout
# print("Windows CMD built-in.", command)
process = subprocess.run(
command,
capture_output=True,
Expand All @@ -92,7 +91,6 @@ def _executeCommand(command: str, timeout: int) -> typing.Tuple[str, str, int, f
check=True,
)
else:
# print("Linux or Windows command:", command)
process = subprocess.run(
command.split(),
capture_output=True,
Expand Down
46 changes: 36 additions & 10 deletions monitor_agent/core/helper.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import sys
from datetime import datetime
import logging

LOG_FILE = "monitor-agent.log"
MODE = "a+"

def getLogger(level: str, filename: str):
log_level = ""
translation = {
"debug": logging.DEBUG,
"info": logging.INFO,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
}
try:
if level in translation.keys():
log_level = translation[level]
else:
logging.warning("Level not established in Settings.json")
log_level = logging.info
except AttributeError as e:
logging.warning(
"Level not established in Settings.json.\nDefault Info level will be used.",
exc_info=True,
)
log_level = logging.info

def save2log(filename: str = LOG_FILE, mode: str = MODE, data="", type=""):
log_filename = ""
try:
with open(filename, mode) as f:
timestamp = datetime.now().strftime("%d/%m/%Y:%H:%M:%S")
f.write(f"{type} - - [{timestamp}] : {data}\n")
except OSError as msg:
print(f"ERROR: Couldn't open file {filename} - {msg}", file=sys.stderr)
log_filename = filename
except AttributeError as e:
logging.warning(
'Log filename not established in Settings.json.\nDefault "monitor.log" file will be used.',
exc_info=True,
)
log_filename = "monitor.log"

logging.basicConfig(
level=log_level,
filename=log_filename,
format="%(asctime)s - %(levelname)s - %(message)s",
)
60 changes: 48 additions & 12 deletions monitor_agent/core/metricFunctions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import time
import json
import time
import typing
import requests
import logging
from requests.exceptions import ConnectionError, InvalidSchema
from urllib3.exceptions import MaxRetryError, NewConnectionError

from monitor_agent.core.helper import save2log
from .models.metricModel import Status, MetricDynamic, MetricStatic

# Typing Tuple is used to overcome Python ^3.6 until Python 3.10 problem
# with Tuples not being a standard type
from monitor_agent.core.models import Status, MetricDynamic, MetricStatic


def execution_time_decorator(function) -> typing.Tuple[float, dict]:
Expand Down Expand Up @@ -36,19 +35,56 @@ def send_metrics_adapter(function_list: list) -> typing.Tuple[dict, dict]:
elapsed_time.update(f_time)
data.update(f_data)
except TypeError as msg:
save2log(type="WARNING", data=f"TypeError: {msg}")
logging.warning(f"TypeError: {msg}", exc_info=True)
continue
return elapsed_time, data


def send_metrics(
url: str, elapsed_time: dict, data: dict, file_enabled: bool, file_path: str
elapsed_time: dict,
metrics: dict,
file_enabled: bool,
file_path: str,
metric_endpoint: str,
agent_endpoint: str,
user_token: str,
agent_token: str,
name: str,
):
status = Status(elapsed=elapsed_time).__dict__
json_request = {"data": data, "status": status}
r = requests.post(url, json=json_request)
# DEBUG
print(r.status_code)
if not agent_endpoint.endswith("/"):
agent_endpoint = agent_endpoint + "/"
if not metric_endpoint.endswith("/"):
metric_endpoint = metric_endpoint + "/"

agent_token = f"{agent_endpoint}{agent_token}/"
json_request = {
"agent": agent_token,
"name": name,
"metrics": metrics,
"status": status,
}
logging.debug(f"Agent token: {agent_token}")
logging.debug(f"Metric endpoint: {metric_endpoint}")

try:
r = requests.post(
metric_endpoint,
json=json_request,
headers={"Authorization": f"Token {user_token}"},
)
logging.debug(f"Response: {r.text}")
logging.debug(f"Status Code: {r.status_code}")
except (
MaxRetryError,
NewConnectionError,
ConnectionRefusedError,
ConnectionError,
InvalidSchema,
) as e:
logging.critical(
f"Agent could not send metrics to server {metric_endpoint}", exc_info=True
)
if file_enabled:
with open(file_path, "w") as f:
f.write(json.dumps(json_request, indent=4, sort_keys=True))
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import platform
import time
import datetime
import os

from monitor_agent.core.helper import save2log
from monitor_agent.main import logging


class Status:
Expand Down Expand Up @@ -116,9 +115,8 @@ def _process(ram: int, pc_cpu_percent):
# Requires elevated permissions
process[p.pid]["path"] = p.exe()
except (PermissionError, psutil.AccessDenied):
save2log(
type="WARNING",
data=f"Could not get Username or Path for process {p.name()}",
logging.warning(
f"Could not get Username or Path for process {p.name()}"
)
return process

Expand Down
Empty file.
Empty file.
51 changes: 34 additions & 17 deletions monitor_agent/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import re
import sys
import json
import uvicorn
import logging
Expand All @@ -8,15 +6,19 @@
from .core.command import Command
from fastapi import FastAPI, UploadFile
from fastapi_utils.tasks import repeat_every
from .core.metricFunctions import send_metrics, send_metrics_adapter, static, dynamic
from monitor_agent.core.helper import getLogger


try:
CONFIG = Settings()
except json.decoder.JSONDecodeError as msg:
print('Error in "settings.json".', msg, file=sys.stderr)
logging.critical(f'Error in "settings.json". {msg}')
exit()

logger = logging.getLogger(__name__)
getLogger(CONFIG.logging.level, CONFIG.logging.filename)

from .core.metricFunctions import send_metrics, send_metrics_adapter, static, dynamic

api = FastAPI()

endpoints = {
Expand Down Expand Up @@ -62,33 +64,45 @@ async def mod_settings(settings: UploadFile):
return msg


logging.debug(f"POST Interval: {CONFIG.metrics.post_interval}")


@api.on_event("startup")
@repeat_every(seconds=CONFIG.metrics.post_interval, logger=logger, wait_first=True)
@repeat_every(seconds=CONFIG.metrics.post_interval, logger=logging, wait_first=True)
def periodic():
# https://github.com/tiangolo/fastapi/issues/520
# https://fastapi-utils.davidmontague.xyz/user-guide/repeated-tasks/#the-repeat_every-decorator
# Changed Timeloop for this
elapsed_time, data = send_metrics_adapter([static, dynamic])
elapsed_time, metrics = send_metrics_adapter([static, dynamic])
send_metrics(
url=CONFIG.metrics.post_url,
elapsed_time=elapsed_time,
data=data,
metrics=metrics,
file_enabled=CONFIG.metrics.enable_logfile,
file_path=CONFIG.metrics.log_filename,
metric_endpoint=CONFIG.endpoints.metric_endpoint,
agent_endpoint=CONFIG.endpoints.agent_endpoint,
agent_token=CONFIG.auth.agent_token,
user_token=CONFIG.auth.user_token,
name=CONFIG.auth.name,
)

alert = {}
if data["cpu_percent"] >= thresholds_dict["cpu_percent"]:
alert["cpu_percent"] = data["cpu_percent"]
if data["ram"]["percent"] >= thresholds_dict["ram_percent"]:
alert["ram_percent"] = data["ram"]["percent"]
if metrics["cpu_percent"] >= CONFIG.thresholds.cpu_percent:
alert["cpu_percent"] = metrics["cpu_percent"]
if metrics["ram"]["percent"] >= CONFIG.thresholds.ram_percent:
alert["ram_percent"] = metrics["ram"]["percent"]
try:
if data["process"]:
alert["processes"] = data["process"]
if metrics["process"]:
alert["processes"] = metrics["process"]
except KeyError as msg:
pass
if alert:
r = requests.post(CONFIG.alerts.url, json={"alert": alert})
try:
r = requests.post(CONFIG.alerts.url, json={"alert": alert})
except requests.exceptions.InvalidSchema as e:
logging.error(
f"Agent could not send an alert to {CONFIG.alerts.url}", exc_info=True
)


def start():
Expand All @@ -99,4 +113,7 @@ def start():
uviconfig.pop("__dict__", None)
uviconfig.pop("__weakref__", None)
uviconfig.pop("__doc__", None)
uvicorn.run(**uviconfig)
try:
uvicorn.run(**uviconfig)
except:
logging.critical("Unable to run server.", exc_info=True)
24 changes: 18 additions & 6 deletions monitor_agent/settings.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
{
"alerts": {
"url": "127.0.0.1:8000/alerts"
"url": "http://localhost:8000/api/alerts/"
},
"auth": {
"agent_token": "f214030c663a2d4b1dc7e133d51635184b006da6",
"name": "Win10",
"user_token": "dca6ce1b19832db09b636844c85dbb3295f1a469"
},
"endpoints": {
"agent_endpoint": "http://localhost:8000/api/agents/",
"metric_endpoint": "http://localhost:8000/api/metrics/"
},
"logging": {
"filename": "monitor.log",
"level": "info"
},
"metrics": {
"enable_logfile": false,
"get_endpoint": true,
"get_endpoint": false,
"log_filename": "metrics.json",
"post_interval": 60,
"post_url": "http://httpbin.org/post"
"post_interval": 60
},
"thresholds": {
"cpu_percent": 50,
"ram_percent": 30
},
"uvicorn": {
"backlog": 2048,
"debug": true,
"debug": false,
"host": "0.0.0.0",
"log_level": "trace",
"port": 8000,
"port": 8080,
"reload": true,
"timeout_keep_alive": 5,
"workers": 4
Expand Down
19 changes: 10 additions & 9 deletions monitor_agent/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import sys
import json
from monitor_agent.core.helper import save2log
import logging


rel_path = "settings.json"
Expand All @@ -12,11 +11,14 @@
class Settings:
def __init__(self):
self.as_dict: dict = self._read_settings_file()
obj = toObj(self.as_dict)
self.alerts = obj.alerts
self.metrics = obj.metrics
self.thresholds = obj.thresholds
self.uvicorn = obj.uvicorn
_obj = toObj(self.as_dict)
self.alerts = _obj.alerts
self.auth = _obj.auth
self.logging = _obj.logging
self.metrics = _obj.metrics
self.endpoints = _obj.endpoints
self.thresholds = _obj.thresholds
self.uvicorn = _obj.uvicorn

def _read_settings_file(self):
try:
Expand All @@ -25,8 +27,7 @@ def _read_settings_file(self):
data_dict = json.loads(data)
data_str, data_dict = _write_file(data_dict, abs_file_path)
except (json.JSONDecodeError, ValueError, FileNotFoundError) as msg:
print(f"ERROR: Invalid JSON settings file - {msg}", file=sys.stderr)
save2log(type="ERROR", data=f"Invalid JSON file - {msg}")
logging.error(f"Invalid JSON settings file.", exc_info=True)
exit()

return data_dict
Expand Down
25 changes: 0 additions & 25 deletions monitor_agent/settings_localhost.json

This file was deleted.