log info and statistics of every trigger
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<logLevel>Info</logLevel>
|
||||
<testMode>true</testMode>
|
||||
<defaultGovernor>powersave</defaultGovernor>
|
||||
<statisticsIntervalInSeconds>5</statisticsIntervalInSeconds>
|
||||
<triggers>
|
||||
<trigger>
|
||||
<name>test_trigger1</name>
|
||||
@@ -19,7 +20,7 @@
|
||||
<trigger>
|
||||
<name>roon</name>
|
||||
<logLevel>Info</logLevel>
|
||||
<intervalInSeconds>2</intervalInSeconds>
|
||||
<intervalInSeconds>1</intervalInSeconds>
|
||||
<governor>performance</governor>
|
||||
<customConfig>
|
||||
<extensionId>cpu_governor_auto_adjust_for_roon</extensionId>
|
||||
@@ -43,7 +44,7 @@
|
||||
<trigger>
|
||||
<name>cpu_load</name>
|
||||
<logLevel>Debug</logLevel>
|
||||
<intervalInSeconds>5</intervalInSeconds>
|
||||
<intervalInSeconds>1</intervalInSeconds>
|
||||
<governor>performance</governor>
|
||||
<customConfig>
|
||||
<oneMinuteHighThreshold>0.35</oneMinuteHighThreshold>
|
||||
@@ -57,7 +58,7 @@
|
||||
<trigger>
|
||||
<name>roon_arc</name>
|
||||
<logLevel>Info</logLevel>
|
||||
<intervalInSeconds>5</intervalInSeconds>
|
||||
<intervalInSeconds>1</intervalInSeconds>
|
||||
<governor>performance</governor>
|
||||
<customConfig>
|
||||
<logFile>/home/martin/dev/roon_arc/RoonServer_log.txt</logFile>
|
||||
|
||||
@@ -71,6 +71,14 @@ class Config:
|
||||
raise ValueError("testMode can only be 'true' or 'false")
|
||||
return True if value == 'true' else False
|
||||
|
||||
@cached_property
|
||||
def statistics_interval_in_seconds(self) -> int:
|
||||
_statistics_interval = self._get_single_text_value_from_xpath('statisticsIntervalInSeconds')
|
||||
value = int(_statistics_interval)
|
||||
if value < 1:
|
||||
raise ValueError("statisticsIntervalInSeconds must be greater than 0")
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def default_governor(self) -> str:
|
||||
_default_governor = self._get_single_text_value_from_xpath('defaultGovernor')
|
||||
|
||||
@@ -41,17 +41,10 @@ class TriggerScheduler(AppClass):
|
||||
return
|
||||
|
||||
self.governor_control.set_governor(governor_name)
|
||||
|
||||
async def async_trigger(self, _trigger: Trigger) -> None:
|
||||
"""Run a callback_trigger with a specific name and check its status at a given interval."""
|
||||
await _trigger.async_run()
|
||||
while True:
|
||||
self.log.debug("callback trigger %s, state: %s", _trigger.name, _trigger.trigger_state)
|
||||
await asyncio.sleep(_trigger.config.interval_in_seconds)
|
||||
|
||||
async def start_trigger(self, _trigger: Trigger) -> None:
|
||||
"""Start a new trigger."""
|
||||
self.loop.create_task(self.async_trigger(_trigger))
|
||||
self.loop.create_task(_trigger.run())
|
||||
self.running_triggers.append(_trigger)
|
||||
|
||||
async def stop_triggers(self) -> None:
|
||||
|
||||
@@ -3,6 +3,9 @@ from cpu_governor_auto_adjust.config import Config, TriggerTuple
|
||||
from cpu_governor_auto_adjust.exceptions import MissingConfig, TriggerImportError
|
||||
from cpu_governor_auto_adjust.governor import Governor, _governor_list
|
||||
from functools import cached_property
|
||||
from time import monotonic
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
|
||||
class Trigger(AppClass):
|
||||
@@ -10,10 +13,21 @@ class Trigger(AppClass):
|
||||
super().__init__(_config)
|
||||
self.active: bool = False
|
||||
self.log.setLevel(self.config.loglevel.upper())
|
||||
self._timestamp_last_statistics = monotonic()
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__(hash(self.name))
|
||||
|
||||
def log_statistics(self):
|
||||
instance_vars = {}
|
||||
|
||||
for key, value in self.__dict__.items():
|
||||
if not key.startswith("_") and isinstance(value, (str, int, float, bool, list, dict, datetime, Governor)):
|
||||
if not isinstance(value, (int, float, Governor)):
|
||||
value = str(value)
|
||||
instance_vars[key] = value
|
||||
self.log.info("info and statistics: %s", instance_vars)
|
||||
|
||||
@property
|
||||
def trigger_state(self) -> str:
|
||||
return "active" if self.active else "not active"
|
||||
@@ -41,18 +55,20 @@ class Trigger(AppClass):
|
||||
raise ValueError("unknown governor: %s", _governor)
|
||||
return _governor
|
||||
|
||||
def run(self) -> None:
|
||||
# this is the main function of the trigger class and has to stay active by using a while True loop
|
||||
|
||||
while True:
|
||||
raise TriggerImportError(
|
||||
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
||||
)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
def trigger_code(self) -> None:
|
||||
# this is the main function of the trigger class and can be used to execute a callback function
|
||||
|
||||
raise TriggerImportError(
|
||||
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
||||
)
|
||||
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
raise TriggerImportError(
|
||||
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
||||
)
|
||||
self.log.debug("run trigger code of %s", self.__class__.__name__)
|
||||
if self._timestamp_last_statistics + int(self._config.statistics_interval_in_seconds) < monotonic():
|
||||
self.log_statistics()
|
||||
self._timestamp_last_statistics = monotonic()
|
||||
self.trigger_code()
|
||||
self.log.debug("trigger state: %s", self.trigger_state)
|
||||
await asyncio.sleep(self.config.interval_in_seconds)
|
||||
|
||||
@@ -2,7 +2,6 @@ from cpu_governor_auto_adjust.trigger import Trigger
|
||||
from cpu_governor_auto_adjust.config import Config
|
||||
from functools import cached_property
|
||||
from os import getloadavg
|
||||
import asyncio
|
||||
|
||||
|
||||
class CpuLoadTrigger(Trigger):
|
||||
@@ -55,34 +54,29 @@ class CpuLoadTrigger(Trigger):
|
||||
def current_load_average_over_under_threshold(self) -> bool:
|
||||
return any(load <= threshold for load, threshold in zip(self.current_load, self.low_threshold))
|
||||
|
||||
async def check_load(self) -> None:
|
||||
while True:
|
||||
current_active = self.active
|
||||
new_high_load_active = self.current_load_average_over_high_threshold
|
||||
new_low_load_active = self.current_load_average_over_under_threshold
|
||||
def trigger_code(self) -> None:
|
||||
current_active = self.active
|
||||
new_high_load_active = self.current_load_average_over_high_threshold
|
||||
new_low_load_active = self.current_load_average_over_under_threshold
|
||||
|
||||
if not current_active and new_high_load_active:
|
||||
self.log.info(
|
||||
"activating trigger, load: %s, high threshold: %s, governor: %s",
|
||||
self.current_load, self.high_threshold, self.governor.name
|
||||
)
|
||||
self.active = True
|
||||
|
||||
if not current_active and new_high_load_active:
|
||||
self.log.info(
|
||||
"activating trigger, load: %s, high threshold: %s, governor: %s",
|
||||
self.current_load, self.high_threshold, self.governor.name
|
||||
)
|
||||
self.active = True
|
||||
|
||||
elif current_active and new_high_load_active:
|
||||
self.log.debug(
|
||||
"trigger is already active, load: %s, high threshold: %s",
|
||||
self.current_load, self.high_threshold
|
||||
)
|
||||
|
||||
elif current_active and new_low_load_active:
|
||||
self.log.info(
|
||||
"deactivating trigger, load: %s, low threshold: %s, governor: %s",
|
||||
self.current_load, self.low_threshold, self.governor.name
|
||||
)
|
||||
self.active = False
|
||||
else:
|
||||
self.log.debug("trigger is already inactive, load: %s, threshold: %s", self.current_load, self.high_threshold)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
await self.check_load()
|
||||
elif current_active and new_high_load_active:
|
||||
self.log.debug(
|
||||
"trigger is already active, load: %s, high threshold: %s",
|
||||
self.current_load, self.high_threshold
|
||||
)
|
||||
|
||||
elif current_active and new_low_load_active:
|
||||
self.log.info(
|
||||
"deactivating trigger, load: %s, low threshold: %s, governor: %s",
|
||||
self.current_load, self.low_threshold, self.governor.name
|
||||
)
|
||||
self.active = False
|
||||
else:
|
||||
self.log.debug("trigger is already inactive, load: %s, threshold: %s", self.current_load, self.high_threshold)
|
||||
|
||||
@@ -3,7 +3,6 @@ from cpu_governor_auto_adjust.config import Config
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
from dataclasses import dataclass
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
|
||||
@@ -48,6 +47,8 @@ class LogTrigger(Trigger):
|
||||
super().__init__(_config)
|
||||
self.timestamp_last_active_change = datetime.now() - timedelta(minutes=self.timeout_in_minutes + 1)
|
||||
self.line_count = 0
|
||||
self._file = None
|
||||
self._current_inode = None
|
||||
|
||||
@cached_property
|
||||
def file(self) -> str:
|
||||
@@ -75,15 +76,12 @@ class LogTrigger(Trigger):
|
||||
self.timestamp_last_active_change = datetime.now()
|
||||
if not self.active:
|
||||
self.log.info("activating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
||||
self.active = True
|
||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
||||
|
||||
self.active = True
|
||||
|
||||
def set_inactive(self) -> None:
|
||||
if self.active:
|
||||
self.log.info("deactivating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
||||
self.active = False
|
||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
||||
|
||||
def _process_line(self, line: str) -> None:
|
||||
for trigger_string in self.trigger_strings:
|
||||
@@ -105,46 +103,31 @@ class LogTrigger(Trigger):
|
||||
else:
|
||||
self.log.debug("substrings condition not met")
|
||||
self.log.debug("No trigger strings matched in line: %s", line.strip())
|
||||
|
||||
def get_statistic_info(self) -> dict[str, str]:
|
||||
return {
|
||||
'file': self.file,
|
||||
'active': str(self.active),
|
||||
'line_count': str(self.line_count),
|
||||
'timestamp_last_active_change': str(self.timestamp_last_active_change),
|
||||
'timeout_in_minutes': str(self.timeout_in_minutes),
|
||||
}
|
||||
|
||||
async def read_log(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
current_inode = os.stat(self.file).st_ino # Get initial inode
|
||||
def trigger_code(self) -> None:
|
||||
try:
|
||||
if self._file is None:
|
||||
self.log.info("Opening file: %s", self.file)
|
||||
self._file = open(self.file, 'r')
|
||||
self._current_inode = os.stat(self.file).st_ino
|
||||
self._file.seek(0, 2) # Move to the end of the file
|
||||
|
||||
# Detect if file has been rotated
|
||||
if os.stat(self.file).st_ino != self._current_inode:
|
||||
self.log.info("File %s rolled over. Reopening...", self.file)
|
||||
self._file.close()
|
||||
self._file = open(self.file, 'r')
|
||||
self._current_inode = os.stat(self.file).st_ino
|
||||
|
||||
line = self._file.readline()
|
||||
while line:
|
||||
self.line_count += 1
|
||||
self._process_line(line)
|
||||
line = self._file.readline()
|
||||
|
||||
# Check if the trigger has not been triggered for the timeout period
|
||||
if (datetime.now() - self.timestamp_last_active_change) > timedelta(minutes=self.timeout_in_minutes):
|
||||
self.set_inactive()
|
||||
|
||||
with open(self.file, 'r') as _file:
|
||||
_file.seek(0, 2) # Move to the end of the file
|
||||
while True:
|
||||
line = _file.readline()
|
||||
if line:
|
||||
self.line_count += 1
|
||||
if self.line_count % 100 == 0:
|
||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
||||
else:
|
||||
self.log.debug("Statistics: %s", self.get_statistic_info())
|
||||
|
||||
self._process_line(line)
|
||||
else:
|
||||
await asyncio.sleep(0.1)
|
||||
# Check if the trigger has not been triggered for the timeout period
|
||||
if (datetime.now() - self.timestamp_last_active_change) > timedelta(minutes=self.timeout_in_minutes):
|
||||
self.set_inactive()
|
||||
|
||||
# Detect if file has been rotated
|
||||
if os.stat(self.file).st_ino != current_inode:
|
||||
self.log.info("File %s rolled over. Reopening...", self.file)
|
||||
break # Exit inner loop to reopen file
|
||||
except FileNotFoundError:
|
||||
self.log.debug("file not found: %s", self.file)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
await self.read_log()
|
||||
except FileNotFoundError:
|
||||
self.log.debug("file not found: %s", self.file)
|
||||
|
||||
@@ -20,7 +20,8 @@ class RoonTrigger(Trigger):
|
||||
def __init__(self, _config: Config) -> None:
|
||||
super().__init__(_config)
|
||||
self.set_status()
|
||||
|
||||
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed")
|
||||
|
||||
def set_status(self) -> None:
|
||||
zones_state = list()
|
||||
|
||||
@@ -99,5 +100,6 @@ class RoonTrigger(Trigger):
|
||||
)
|
||||
self.set_status()
|
||||
|
||||
async def async_run(self) -> None:
|
||||
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed")
|
||||
def trigger_code(self) -> None:
|
||||
# this check works with the callback function of roonapi
|
||||
pass
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from cpu_governor_auto_adjust.trigger import Trigger
|
||||
from cpu_governor_auto_adjust.config import Config
|
||||
import random
|
||||
import asyncio
|
||||
|
||||
class TestTrigger1(Trigger):
|
||||
def __init__(self, _config: Config) -> None:
|
||||
super().__init__(_config)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
while True:
|
||||
choices = [False, True]
|
||||
self.log.debug("run check code of %s", self.__class__.__name__)
|
||||
self.active = random.choice(choices)
|
||||
await asyncio.sleep(1)
|
||||
def trigger_code(self) -> None:
|
||||
choices = [False, True]
|
||||
self.log.debug("run check code of %s", self.__class__.__name__)
|
||||
self.active = random.choice(choices)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from cpu_governor_auto_adjust.trigger import Trigger
|
||||
from cpu_governor_auto_adjust.config import Config
|
||||
from datetime import datetime, time
|
||||
import asyncio
|
||||
|
||||
|
||||
class TimeTrigger(Trigger):
|
||||
@@ -15,16 +14,12 @@ class TimeTrigger(Trigger):
|
||||
def end_time(self) -> time:
|
||||
return datetime.strptime(self.config.custom_config['endTime'], '%H:%M').time()
|
||||
|
||||
async def check_time(self) -> None:
|
||||
while True:
|
||||
active_current = self.active
|
||||
active_new = self.start_time() <= datetime.now().time() <= self.end_time()
|
||||
if not active_current and active_new:
|
||||
self.log.info("activating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||
elif active_current and not active_new:
|
||||
self.log.info("deactivating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||
self.active = active_new
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
await self.check_time()
|
||||
def trigger_code(self) -> None:
|
||||
active_current = self.active
|
||||
active_new = self.start_time() <= datetime.now().time() <= self.end_time()
|
||||
if not active_current and active_new:
|
||||
self.log.info("activating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||
elif active_current and not active_new:
|
||||
self.log.info("deactivating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||
self.active = active_new
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "cpu_governor_auto_adjust"
|
||||
version = "0.4.0"
|
||||
version = "0.5.6"
|
||||
description = "This application has been developed to automatically change cpu governor based on certain triggers."
|
||||
authors = [
|
||||
{ name = "Martin Reurekas", email = "martin@semrks.nl" }
|
||||
|
||||
Reference in New Issue
Block a user