From d02e2d3ff46cdb766d0eb636fc94375ef21aebd6 Mon Sep 17 00:00:00 2001 From: renakas <107467323+renakas@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:15:28 +0200 Subject: [PATCH 1/4] Added log rotation with python --- agent360/agent360.py | 94 +++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/agent360/agent360.py b/agent360/agent360.py index e20b626..614c19b 100755 --- a/agent360/agent360.py +++ b/agent360/agent360.py @@ -42,6 +42,9 @@ import time import types from optparse import OptionParser +from logging import Formatter, getLogger, StreamHandler +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler + try: from urllib.parse import urlparse, urlencode @@ -366,38 +369,83 @@ def _config_section_create(self, section): if not self.config.has_section(section): self.config.add_section(section) - def _logging_init(self): - ''' - Initialize logging faculty - ''' - level = self.config.getint('agent', 'logging_level') - if os.name == 'nt': - log_file = os.path.expandvars(self.config.get('agent', 'log_file')) - else: - log_file = self.config.get('agent', 'log_file') +def _logging_init(self): + """ + Initialize logging with optional in-app log rotation. + Supported rotation modes: + - size (default): RotatingFileHandler + - time: TimedRotatingFileHandler + - none: plain FileHandler + """ + level = self.config.getint('agent', 'logging_level') + + # Resolve target log file path + if os.name == 'nt': + log_file = os.path.expandvars(self.config.get('agent', 'log_file')) + else: + log_file = self.config.get('agent', 'log_file') - log_file_mode = self.config.get('agent', 'log_file_mode') - if log_file_mode in ('w', 'a'): - pass - elif log_file_mode == 'truncate': + # Normalize legacy 'log_file_mode' (kept for compatibility) + log_file_mode = self.config.get('agent', 'log_file_mode') + if log_file_mode not in ('w', 'a'): + if log_file_mode == 'truncate': log_file_mode = 'w' - elif log_file_mode == 'append': - log_file_mode = 'a' else: log_file_mode = 'a' + # Rotation config (all optional) + rotation = self.config.get('agent', 'rotation') if self.config.has_option('agent', 'rotation') else 'size' + # Size-based defaults (10MB, keep 7 files) + rotation_max_bytes = self.config.getint('agent', 'rotation_max_bytes') if self.config.has_option('agent', 'rotation_max_bytes') else 10 * 1024 * 1024 + rotation_backup_count = self.config.getint('agent', 'rotation_backup_count') if self.config.has_option('agent', 'rotation_backup_count') else 7 + # Time-based defaults (roll at midnight, keep 7 files) + rotation_when = self.config.get('agent', 'rotation_when') if self.config.has_option('agent', 'rotation_when') else 'midnight' + rotation_interval = self.config.getint('agent', 'rotation_interval') if self.config.has_option('agent', 'rotation_interval') else 1 + rotation_utc = self.config.getboolean('agent', 'rotation_utc') if self.config.has_option('agent', 'rotation_utc') else False + + # Configure root logger explicitly (avoid basicConfig) + root = getLogger() + root.setLevel(level) + fmt = Formatter("%(asctime)-15s %(levelname)s %(message)s") + + try: if log_file == '-': - logging.basicConfig(level=level) # Log to sys.stderr by default + # Container/journald-friendly: write to stderr + handler = StreamHandler() else: - try: - logging.basicConfig(filename=log_file, filemode=log_file_mode, level=level, format="%(asctime)-15s %(levelname)s %(message)s") - except IOError as e: - logging.basicConfig(level=level) - logging.info('IOError: %s', e) - logging.info('Drop logging to stderr') + if rotation == 'none': + # No rotation at all + handler = logging.FileHandler(log_file, mode=log_file_mode) + elif rotation == 'time': + # Time-based rotation + handler = TimedRotatingFileHandler( + log_file, + when=rotation_when, # e.g., 'S', 'M', 'H', 'D', 'midnight', 'W0' + interval=rotation_interval, + backupCount=rotation_backup_count, + utc=rotation_utc + ) + else: + # Default: size-based rotation + handler = RotatingFileHandler( + log_file, + mode=log_file_mode, + maxBytes=rotation_max_bytes, + backupCount=rotation_backup_count + ) + + handler.setFormatter(fmt) + root.handlers = [handler] + except IOError as e: + # Fall back to stderr if file cannot be opened + root.handlers = [StreamHandler()] + root.handlers[0].setFormatter(fmt) + logging.info('IOError: %s', e) + logging.info('Drop logging to stderr') + + logging.info('Agent logging_level %i', level) - logging.info('Agent logging_level %i', level) def _plugins_init(self): ''' From c62f96989ad45be97bef337b7cec0583ccd99037 Mon Sep 17 00:00:00 2001 From: Renat Kasimov Date: Thu, 15 Jan 2026 10:05:11 +0100 Subject: [PATCH 2/4] Update mysql.py --- agent360/plugins/mysql.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/agent360/plugins/mysql.py b/agent360/plugins/mysql.py index 468e86c..b213e63 100644 --- a/agent360/plugins/mysql.py +++ b/agent360/plugins/mysql.py @@ -129,29 +129,37 @@ def run(self, config): pass cursor = db.cursor(MySQLdb.cursors.DictCursor) - cursor.execute('SHOW SLAVE STATUS') - query_result_slave = cursor.fetchone() + # Try MySQL 8.4+ syntax first, fall back to older syntax for backward compatibility + try: + cursor.execute('SHOW REPLICA STATUS') + query_result_slave = cursor.fetchone() + except MySQLdb.OperationalError: + cursor.execute('SHOW SLAVE STATUS') + query_result_slave = cursor.fetchone() + + # Support both old and new column names non_delta_slave = ( - 'slave_io_state', - 'master_host', - 'seconds_behind_master', - 'read_master_log_pos', + 'slave_io_state', 'replica_io_state', + 'master_host', 'source_host', + 'seconds_behind_master', 'seconds_behind_source', + 'read_master_log_pos', 'read_source_log_pos', 'relay_log_pos', - 'slave_io_running', - 'slave_sql_running', + 'slave_io_running', 'replica_io_running', + 'slave_sql_running', 'replica_sql_running', 'last_error', - 'exec_master_log_pos', + 'exec_master_log_pos', 'exec_source_log_pos', 'relay_log_space', - 'slave_sql_running_state', - 'master_retry_count' + 'slave_sql_running_state', 'replica_sql_running_state', + 'master_retry_count', 'source_retry_count' ) if query_result_slave is None: query_result_slave = dict() for key, value in query_result_slave.items(): key = key.lower().strip() - if key == 'slave_sql_running': + # Handle both old and new column names + if key in ('slave_sql_running', 'replica_sql_running'): value = 1 if value == 'Yes' else 0 - if key == 'slave_io_running': + if key in ('slave_io_running', 'replica_io_running'): value = 1 if value == 'Yes' else 0 for c in constructors: From 077eb926c38b834b4cd89febebccdd9e48272705 Mon Sep 17 00:00:00 2001 From: Renat Kasimov Date: Wed, 21 Jan 2026 10:33:25 +0100 Subject: [PATCH 3/4] Revert "Added log rotation with python" This reverts commit d02e2d3ff46cdb766d0eb636fc94375ef21aebd6. --- agent360/agent360.py | 94 +++++++++++--------------------------------- 1 file changed, 23 insertions(+), 71 deletions(-) diff --git a/agent360/agent360.py b/agent360/agent360.py index 614c19b..e20b626 100755 --- a/agent360/agent360.py +++ b/agent360/agent360.py @@ -42,9 +42,6 @@ import time import types from optparse import OptionParser -from logging import Formatter, getLogger, StreamHandler -from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler - try: from urllib.parse import urlparse, urlencode @@ -369,83 +366,38 @@ def _config_section_create(self, section): if not self.config.has_section(section): self.config.add_section(section) + def _logging_init(self): + ''' + Initialize logging faculty + ''' + level = self.config.getint('agent', 'logging_level') -def _logging_init(self): - """ - Initialize logging with optional in-app log rotation. - Supported rotation modes: - - size (default): RotatingFileHandler - - time: TimedRotatingFileHandler - - none: plain FileHandler - """ - level = self.config.getint('agent', 'logging_level') - - # Resolve target log file path - if os.name == 'nt': - log_file = os.path.expandvars(self.config.get('agent', 'log_file')) - else: - log_file = self.config.get('agent', 'log_file') + if os.name == 'nt': + log_file = os.path.expandvars(self.config.get('agent', 'log_file')) + else: + log_file = self.config.get('agent', 'log_file') - # Normalize legacy 'log_file_mode' (kept for compatibility) - log_file_mode = self.config.get('agent', 'log_file_mode') - if log_file_mode not in ('w', 'a'): - if log_file_mode == 'truncate': + log_file_mode = self.config.get('agent', 'log_file_mode') + if log_file_mode in ('w', 'a'): + pass + elif log_file_mode == 'truncate': log_file_mode = 'w' + elif log_file_mode == 'append': + log_file_mode = 'a' else: log_file_mode = 'a' - # Rotation config (all optional) - rotation = self.config.get('agent', 'rotation') if self.config.has_option('agent', 'rotation') else 'size' - # Size-based defaults (10MB, keep 7 files) - rotation_max_bytes = self.config.getint('agent', 'rotation_max_bytes') if self.config.has_option('agent', 'rotation_max_bytes') else 10 * 1024 * 1024 - rotation_backup_count = self.config.getint('agent', 'rotation_backup_count') if self.config.has_option('agent', 'rotation_backup_count') else 7 - # Time-based defaults (roll at midnight, keep 7 files) - rotation_when = self.config.get('agent', 'rotation_when') if self.config.has_option('agent', 'rotation_when') else 'midnight' - rotation_interval = self.config.getint('agent', 'rotation_interval') if self.config.has_option('agent', 'rotation_interval') else 1 - rotation_utc = self.config.getboolean('agent', 'rotation_utc') if self.config.has_option('agent', 'rotation_utc') else False - - # Configure root logger explicitly (avoid basicConfig) - root = getLogger() - root.setLevel(level) - fmt = Formatter("%(asctime)-15s %(levelname)s %(message)s") - - try: if log_file == '-': - # Container/journald-friendly: write to stderr - handler = StreamHandler() + logging.basicConfig(level=level) # Log to sys.stderr by default else: - if rotation == 'none': - # No rotation at all - handler = logging.FileHandler(log_file, mode=log_file_mode) - elif rotation == 'time': - # Time-based rotation - handler = TimedRotatingFileHandler( - log_file, - when=rotation_when, # e.g., 'S', 'M', 'H', 'D', 'midnight', 'W0' - interval=rotation_interval, - backupCount=rotation_backup_count, - utc=rotation_utc - ) - else: - # Default: size-based rotation - handler = RotatingFileHandler( - log_file, - mode=log_file_mode, - maxBytes=rotation_max_bytes, - backupCount=rotation_backup_count - ) - - handler.setFormatter(fmt) - root.handlers = [handler] - except IOError as e: - # Fall back to stderr if file cannot be opened - root.handlers = [StreamHandler()] - root.handlers[0].setFormatter(fmt) - logging.info('IOError: %s', e) - logging.info('Drop logging to stderr') - - logging.info('Agent logging_level %i', level) + try: + logging.basicConfig(filename=log_file, filemode=log_file_mode, level=level, format="%(asctime)-15s %(levelname)s %(message)s") + except IOError as e: + logging.basicConfig(level=level) + logging.info('IOError: %s', e) + logging.info('Drop logging to stderr') + logging.info('Agent logging_level %i', level) def _plugins_init(self): ''' From ad99bc971c459250021b534ee147cd05b9544dda Mon Sep 17 00:00:00 2001 From: Renat Kasimov Date: Fri, 9 Oct 2026 19:28:37 +0200 Subject: [PATCH 4/4] [CPCLOUD-5995] Add support MySQL 84 --- agent360/plugins/mysql.py | 48 ++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/agent360/plugins/mysql.py b/agent360/plugins/mysql.py index b213e63..19e9887 100644 --- a/agent360/plugins/mysql.py +++ b/agent360/plugins/mysql.py @@ -129,37 +129,49 @@ def run(self, config): pass cursor = db.cursor(MySQLdb.cursors.DictCursor) - # Try MySQL 8.4+ syntax first, fall back to older syntax for backward compatibility + # SHOW REPLICA STATUS: MySQL 8.0.22+ / MariaDB 10.5.1+. Older servers + # reject it with a syntax error (ProgrammingError), so fall back. try: cursor.execute('SHOW REPLICA STATUS') - query_result_slave = cursor.fetchone() - except MySQLdb.OperationalError: + except MySQLdb.Error: cursor.execute('SHOW SLAVE STATUS') - query_result_slave = cursor.fetchone() - - # Support both old and new column names + query_result_slave = cursor.fetchone() + + # Newer servers renamed columns; map them back to the legacy names + # so metric keys stay the same across server versions. + replica_key_map = { + 'replica_io_state': 'slave_io_state', + 'source_host': 'master_host', + 'seconds_behind_source': 'seconds_behind_master', + 'read_source_log_pos': 'read_master_log_pos', + 'replica_io_running': 'slave_io_running', + 'replica_sql_running': 'slave_sql_running', + 'exec_source_log_pos': 'exec_master_log_pos', + 'replica_sql_running_state': 'slave_sql_running_state', + 'source_retry_count': 'master_retry_count', + } non_delta_slave = ( - 'slave_io_state', 'replica_io_state', - 'master_host', 'source_host', - 'seconds_behind_master', 'seconds_behind_source', - 'read_master_log_pos', 'read_source_log_pos', + 'slave_io_state', + 'master_host', + 'seconds_behind_master', + 'read_master_log_pos', 'relay_log_pos', - 'slave_io_running', 'replica_io_running', - 'slave_sql_running', 'replica_sql_running', + 'slave_io_running', + 'slave_sql_running', 'last_error', - 'exec_master_log_pos', 'exec_source_log_pos', + 'exec_master_log_pos', 'relay_log_space', - 'slave_sql_running_state', 'replica_sql_running_state', - 'master_retry_count', 'source_retry_count' + 'slave_sql_running_state', + 'master_retry_count' ) if query_result_slave is None: query_result_slave = dict() for key, value in query_result_slave.items(): key = key.lower().strip() - # Handle both old and new column names - if key in ('slave_sql_running', 'replica_sql_running'): + key = replica_key_map.get(key, key) + if key == 'slave_sql_running': value = 1 if value == 'Yes' else 0 - if key in ('slave_io_running', 'replica_io_running'): + if key == 'slave_io_running': value = 1 if value == 'Yes' else 0 for c in constructors: