log info and statistics of every trigger

This commit is contained in:
2025-04-01 00:26:16 +02:00
parent 9e62c1cd6b
commit 230ed62bd0
10 changed files with 114 additions and 125 deletions
+4 -3
View File
@@ -3,6 +3,7 @@
<logLevel>Info</logLevel> <logLevel>Info</logLevel>
<testMode>true</testMode> <testMode>true</testMode>
<defaultGovernor>powersave</defaultGovernor> <defaultGovernor>powersave</defaultGovernor>
<statisticsIntervalInSeconds>5</statisticsIntervalInSeconds>
<triggers> <triggers>
<trigger> <trigger>
<name>test_trigger1</name> <name>test_trigger1</name>
@@ -19,7 +20,7 @@
<trigger> <trigger>
<name>roon</name> <name>roon</name>
<logLevel>Info</logLevel> <logLevel>Info</logLevel>
<intervalInSeconds>2</intervalInSeconds> <intervalInSeconds>1</intervalInSeconds>
<governor>performance</governor> <governor>performance</governor>
<customConfig> <customConfig>
<extensionId>cpu_governor_auto_adjust_for_roon</extensionId> <extensionId>cpu_governor_auto_adjust_for_roon</extensionId>
@@ -43,7 +44,7 @@
<trigger> <trigger>
<name>cpu_load</name> <name>cpu_load</name>
<logLevel>Debug</logLevel> <logLevel>Debug</logLevel>
<intervalInSeconds>5</intervalInSeconds> <intervalInSeconds>1</intervalInSeconds>
<governor>performance</governor> <governor>performance</governor>
<customConfig> <customConfig>
<oneMinuteHighThreshold>0.35</oneMinuteHighThreshold> <oneMinuteHighThreshold>0.35</oneMinuteHighThreshold>
@@ -57,7 +58,7 @@
<trigger> <trigger>
<name>roon_arc</name> <name>roon_arc</name>
<logLevel>Info</logLevel> <logLevel>Info</logLevel>
<intervalInSeconds>5</intervalInSeconds> <intervalInSeconds>1</intervalInSeconds>
<governor>performance</governor> <governor>performance</governor>
<customConfig> <customConfig>
<logFile>/home/martin/dev/roon_arc/RoonServer_log.txt</logFile> <logFile>/home/martin/dev/roon_arc/RoonServer_log.txt</logFile>
+8
View File
@@ -71,6 +71,14 @@ class Config:
raise ValueError("testMode can only be 'true' or 'false") raise ValueError("testMode can only be 'true' or 'false")
return True if value == 'true' else 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 @cached_property
def default_governor(self) -> str: def default_governor(self) -> str:
_default_governor = self._get_single_text_value_from_xpath('defaultGovernor') _default_governor = self._get_single_text_value_from_xpath('defaultGovernor')
+1 -8
View File
@@ -42,16 +42,9 @@ class TriggerScheduler(AppClass):
self.governor_control.set_governor(governor_name) 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: async def start_trigger(self, _trigger: Trigger) -> None:
"""Start a new trigger.""" """Start a new trigger."""
self.loop.create_task(self.async_trigger(_trigger)) self.loop.create_task(_trigger.run())
self.running_triggers.append(_trigger) self.running_triggers.append(_trigger)
async def stop_triggers(self) -> None: async def stop_triggers(self) -> None:
+26 -10
View File
@@ -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.exceptions import MissingConfig, TriggerImportError
from cpu_governor_auto_adjust.governor import Governor, _governor_list from cpu_governor_auto_adjust.governor import Governor, _governor_list
from functools import cached_property from functools import cached_property
from time import monotonic
from datetime import datetime
import asyncio
class Trigger(AppClass): class Trigger(AppClass):
@@ -10,10 +13,21 @@ class Trigger(AppClass):
super().__init__(_config) super().__init__(_config)
self.active: bool = False self.active: bool = False
self.log.setLevel(self.config.loglevel.upper()) self.log.setLevel(self.config.loglevel.upper())
self._timestamp_last_statistics = monotonic()
def __hash__(self): def __hash__(self):
return super().__hash__(hash(self.name)) 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 @property
def trigger_state(self) -> str: def trigger_state(self) -> str:
return "active" if self.active else "not active" return "active" if self.active else "not active"
@@ -41,18 +55,20 @@ class Trigger(AppClass):
raise ValueError("unknown governor: %s", _governor) raise ValueError("unknown governor: %s", _governor)
return _governor return _governor
def run(self) -> None: def trigger_code(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:
# this is the main function of the trigger class and can be used to execute a callback function # this is the main function of the trigger class and can be used to execute a callback function
while True:
raise TriggerImportError( raise TriggerImportError(
"the Trigger class can't used directly, but must be inherited in a trigger specific class" "the Trigger class can't used directly, but must be inherited in a trigger specific class"
) )
async def run(self) -> None:
while True:
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 cpu_governor_auto_adjust.config import Config
from functools import cached_property from functools import cached_property
from os import getloadavg from os import getloadavg
import asyncio
class CpuLoadTrigger(Trigger): class CpuLoadTrigger(Trigger):
@@ -55,8 +54,7 @@ class CpuLoadTrigger(Trigger):
def current_load_average_over_under_threshold(self) -> bool: def current_load_average_over_under_threshold(self) -> bool:
return any(load <= threshold for load, threshold in zip(self.current_load, self.low_threshold)) return any(load <= threshold for load, threshold in zip(self.current_load, self.low_threshold))
async def check_load(self) -> None: def trigger_code(self) -> None:
while True:
current_active = self.active current_active = self.active
new_high_load_active = self.current_load_average_over_high_threshold new_high_load_active = self.current_load_average_over_high_threshold
new_low_load_active = self.current_load_average_over_under_threshold new_low_load_active = self.current_load_average_over_under_threshold
@@ -82,7 +80,3 @@ class CpuLoadTrigger(Trigger):
self.active = False self.active = False
else: else:
self.log.debug("trigger is already inactive, load: %s, threshold: %s", self.current_load, self.high_threshold) 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()
+19 -36
View File
@@ -3,7 +3,6 @@ from cpu_governor_auto_adjust.config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import cached_property from functools import cached_property
from dataclasses import dataclass from dataclasses import dataclass
import asyncio
import os import os
@@ -48,6 +47,8 @@ class LogTrigger(Trigger):
super().__init__(_config) super().__init__(_config)
self.timestamp_last_active_change = datetime.now() - timedelta(minutes=self.timeout_in_minutes + 1) self.timestamp_last_active_change = datetime.now() - timedelta(minutes=self.timeout_in_minutes + 1)
self.line_count = 0 self.line_count = 0
self._file = None
self._current_inode = None
@cached_property @cached_property
def file(self) -> str: def file(self) -> str:
@@ -76,14 +77,11 @@ class LogTrigger(Trigger):
if not self.active: 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.log.info("activating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
self.active = True self.active = True
self.log.info("Statistics: %s", self.get_statistic_info())
def set_inactive(self) -> None: def set_inactive(self) -> None:
if self.active: 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.log.info("deactivating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
self.active = False self.active = False
self.log.info("Statistics: %s", self.get_statistic_info())
def _process_line(self, line: str) -> None: def _process_line(self, line: str) -> None:
for trigger_string in self.trigger_strings: for trigger_string in self.trigger_strings:
@@ -106,45 +104,30 @@ class LogTrigger(Trigger):
self.log.debug("substrings condition not met") self.log.debug("substrings condition not met")
self.log.debug("No trigger strings matched in line: %s", line.strip()) self.log.debug("No trigger strings matched in line: %s", line.strip())
def get_statistic_info(self) -> dict[str, str]: def trigger_code(self) -> None:
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: try:
current_inode = os.stat(self.file).st_ino # Get initial inode 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
with open(self.file, 'r') as _file: # Detect if file has been rotated
_file.seek(0, 2) # Move to the end of the file if os.stat(self.file).st_ino != self._current_inode:
while True: self.log.info("File %s rolled over. Reopening...", self.file)
line = _file.readline() self._file.close()
if line: 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.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) self._process_line(line)
else: line = self._file.readline()
await asyncio.sleep(0.1)
# Check if the trigger has not been triggered for the timeout period # 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): if (datetime.now() - self.timestamp_last_active_change) > timedelta(minutes=self.timeout_in_minutes):
self.set_inactive() 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: except FileNotFoundError:
self.log.debug("file not found: %s", self.file) self.log.debug("file not found: %s", self.file)
await asyncio.sleep(1)
async def async_run(self) -> None:
await self.read_log()
+4 -2
View File
@@ -20,6 +20,7 @@ class RoonTrigger(Trigger):
def __init__(self, _config: Config) -> None: def __init__(self, _config: Config) -> None:
super().__init__(_config) super().__init__(_config)
self.set_status() self.set_status()
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed")
def set_status(self) -> None: def set_status(self) -> None:
zones_state = list() zones_state = list()
@@ -99,5 +100,6 @@ class RoonTrigger(Trigger):
) )
self.set_status() self.set_status()
async def async_run(self) -> None: def trigger_code(self) -> None:
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed") # 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.trigger import Trigger
from cpu_governor_auto_adjust.config import Config from cpu_governor_auto_adjust.config import Config
import random import random
import asyncio
class TestTrigger1(Trigger): class TestTrigger1(Trigger):
def __init__(self, _config: Config) -> None: def __init__(self, _config: Config) -> None:
super().__init__(_config) super().__init__(_config)
async def async_run(self) -> None: def trigger_code(self) -> None:
while True:
choices = [False, True] choices = [False, True]
self.log.debug("run check code of %s", self.__class__.__name__) self.log.debug("run check code of %s", self.__class__.__name__)
self.active = random.choice(choices) self.active = random.choice(choices)
await asyncio.sleep(1)
+1 -6
View File
@@ -1,7 +1,6 @@
from cpu_governor_auto_adjust.trigger import Trigger from cpu_governor_auto_adjust.trigger import Trigger
from cpu_governor_auto_adjust.config import Config from cpu_governor_auto_adjust.config import Config
from datetime import datetime, time from datetime import datetime, time
import asyncio
class TimeTrigger(Trigger): class TimeTrigger(Trigger):
@@ -15,8 +14,7 @@ class TimeTrigger(Trigger):
def end_time(self) -> time: def end_time(self) -> time:
return datetime.strptime(self.config.custom_config['endTime'], '%H:%M').time() return datetime.strptime(self.config.custom_config['endTime'], '%H:%M').time()
async def check_time(self) -> None: def trigger_code(self) -> None:
while True:
active_current = self.active active_current = self.active
active_new = self.start_time() <= datetime.now().time() <= self.end_time() active_new = self.start_time() <= datetime.now().time() <= self.end_time()
if not active_current and active_new: if not active_current and active_new:
@@ -24,7 +22,4 @@ class TimeTrigger(Trigger):
elif active_current and not active_new: 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.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 self.active = active_new
await asyncio.sleep(1)
async def async_run(self) -> None:
await self.check_time()
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "cpu_governor_auto_adjust" 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." description = "This application has been developed to automatically change cpu governor based on certain triggers."
authors = [ authors = [
{ name = "Martin Reurekas", email = "martin@semrks.nl" } { name = "Martin Reurekas", email = "martin@semrks.nl" }