Skip to content

Commit 16780fa

Browse files
authored
feat: DPE-5656 log rotation new options (#568)
* tune log rotation * rendering config file * test fixes * typing fixes * log rotation setup * revert garbage in commit * better description plus typo fix * fix call on upgrade call * revert typing fix as it needs to fixed at library first * simplify log-rotation test so to avoid decompressing file to test content * remove audit_log_filter * test simplification * If "ensure_all_units_continuous_writes_incrementing" runs to early it breaks the test * typo fix * tiny typo
1 parent a1efa63 commit 16780fa

File tree

13 files changed

+278
-110
lines changed

13 files changed

+278
-110
lines changed

config.yaml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ options:
1212
type: string
1313
profile:
1414
description: |
15-
profile representing the scope of deployment, and used to be able to enable high-level
15+
Profile representing the scope of deployment, and used to be able to enable high-level
1616
customisation of sysconfigs, resource checks/allocation, warning levels, etc.
1717
Allowed values are: “production” and “testing”.
1818
type: string
@@ -37,7 +37,8 @@ options:
3737
description: The database name for the legacy 'mysql' interface (root level access)
3838
type: string
3939
plugin-audit-enabled:
40-
description: Enable the audit plugin
40+
description: |
41+
Audit log plugin state. When the plugin is enabled (default, audit logs will be enabled).
4142
type: boolean
4243
default: true
4344
plugin-audit-strategy:
@@ -50,6 +51,19 @@ options:
5051
description: Number of days for binary logs retention
5152
type: int
5253
default: 7
54+
logs_audit_policy:
55+
description: |
56+
Audit log policy. Allowed values are: "all", "logins" (default), "queries".
57+
Ref. at https://docs.percona.com/percona-server/8.0/audit-log-plugin.html#audit_log_policy
58+
type: string
59+
default: logins
60+
logs_retention_period:
61+
description: |
62+
Specifies the retention period for rotated logs, in days. Accepts an integer value of 3 or
63+
greater, or the special value "auto". When set to "auto" (default), the retention period is
64+
3 days, except when COS-related, where it is 1 day
65+
type: string
66+
default: auto
5367
# Experimental features
5468
experimental-max-connections:
5569
type: int

lib/charms/mysql/v0/mysql.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def wait_until_mysql_connection(self) -> None:
133133
# Increment this major API version when introducing breaking changes
134134
LIBAPI = 0
135135

136-
LIBPATCH = 81
136+
LIBPATCH = 82
137137

138138
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
139139
UNIT_ADD_LOCKNAME = "unit-add"
@@ -930,6 +930,7 @@ def render_mysqld_configuration( # noqa: C901
930930
profile: str,
931931
audit_log_enabled: bool,
932932
audit_log_strategy: str,
933+
audit_log_policy: str,
933934
memory_limit: Optional[int] = None,
934935
experimental_max_connections: Optional[int] = None,
935936
binlog_retention_days: int,
@@ -1000,8 +1001,7 @@ def render_mysqld_configuration( # noqa: C901
10001001
"general_log_file": f"{snap_common}/var/log/mysql/general.log",
10011002
"slow_query_log_file": f"{snap_common}/var/log/mysql/slow.log",
10021003
"binlog_expire_logs_seconds": f"{binlog_retention_seconds}",
1003-
"loose-audit_log_filter": "OFF",
1004-
"loose-audit_log_policy": "LOGINS",
1004+
"loose-audit_log_policy": audit_log_policy.upper(),
10051005
"loose-audit_log_file": f"{snap_common}/var/log/mysql/audit.log",
10061006
}
10071007

@@ -2950,8 +2950,8 @@ def retrieve_backup_with_xbcloud(
29502950
temp_restore_directory: str,
29512951
xbcloud_location: str,
29522952
xbstream_location: str,
2953-
user=None,
2954-
group=None,
2953+
user: Optional[str] = None,
2954+
group: Optional[str] = None,
29552955
) -> Tuple[str, str, str]:
29562956
"""Retrieve the specified backup from S3."""
29572957
nproc_command = ["nproc"]
@@ -3017,8 +3017,8 @@ def prepare_backup_for_restore(
30173017
backup_location: str,
30183018
xtrabackup_location: str,
30193019
xtrabackup_plugin_dir: str,
3020-
user=None,
3021-
group=None,
3020+
user: Optional[str] = None,
3021+
group: Optional[str] = None,
30223022
) -> Tuple[str, str]:
30233023
"""Prepare the backup in the provided dir for restore."""
30243024
try:
@@ -3058,8 +3058,8 @@ def prepare_backup_for_restore(
30583058
def empty_data_files(
30593059
self,
30603060
mysql_data_directory: str,
3061-
user=None,
3062-
group=None,
3061+
user: Optional[str] = None,
3062+
group: Optional[str] = None,
30633063
) -> None:
30643064
"""Empty the mysql data directory in preparation of backup restore."""
30653065
empty_data_files_command = [
@@ -3095,8 +3095,8 @@ def restore_backup(
30953095
defaults_config_file: str,
30963096
mysql_data_directory: str,
30973097
xtrabackup_plugin_directory: str,
3098-
user=None,
3099-
group=None,
3098+
user: Optional[str] = None,
3099+
group: Optional[str] = None,
31003100
) -> Tuple[str, str]:
31013101
"""Restore the provided prepared backup."""
31023102
restore_backup_command = [
@@ -3129,8 +3129,8 @@ def restore_backup(
31293129
def delete_temp_restore_directory(
31303130
self,
31313131
temp_restore_directory: str,
3132-
user=None,
3133-
group=None,
3132+
user: Optional[str] = None,
3133+
group: Optional[str] = None,
31343134
) -> None:
31353135
"""Delete the temp restore directory from the mysql data directory."""
31363136
logger.info(f"Deleting temp restore directory in {temp_restore_directory}")

src/charm.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from charms.mysql.v0.architecture import WrongArchitectureWarningCharm, is_wrong_architecture
88
from ops.main import main
99

10+
from log_rotation_setup import LogRotationSetup
11+
1012
if is_wrong_architecture() and __name__ == "__main__":
1113
main(WrongArchitectureWarningCharm)
1214

@@ -184,6 +186,7 @@ def __init__(self, *args):
184186
self.log_rotate_manager = LogRotateManager(self)
185187
self.log_rotate_manager.start_log_rotate_manager()
186188

189+
self.log_rotate_setup = LogRotationSetup(self)
187190
self.rotate_mysql_logs = RotateMySQLLogs(self)
188191
self.replication_offer = MySQLAsyncReplicationOffer(self)
189192
self.replication_consumer = MySQLAsyncReplicationConsumer(self)
@@ -274,11 +277,22 @@ def is_new_unit(self) -> bool:
274277
}
275278
return self.unit_peer_data.keys() == _default_unit_data_keys
276279

280+
@property
281+
def text_logs(self) -> list:
282+
"""Enabled text logs."""
283+
# slow logs isn't enabled by default
284+
text_logs = ["error"]
285+
286+
if self.config.plugin_audit_enabled:
287+
text_logs.append("audit")
288+
289+
return text_logs
290+
277291
@property
278292
def unit_initialized(self) -> bool:
279293
"""Return whether a unit is started.
280294
281-
Oveerride parent class method to include container accessibility check.
295+
Override parent class method to include container accessibility check.
282296
"""
283297
container = self.unit.get_container(CONTAINER_NAME)
284298
if container.can_connect():
@@ -536,34 +550,24 @@ def _on_config_changed(self, _: EventBase) -> None: # noqa: C901
536550

537551
previous_config_dict = self.mysql_config.custom_config(config_content)
538552

539-
# render the new config
540-
memory_limit_bytes = (self.config.profile_limit_memory or 0) * BYTES_1MB
541-
new_config_content, new_config_dict = self._mysql.render_mysqld_configuration(
542-
profile=self.config.profile,
543-
audit_log_enabled=self.config.plugin_audit_enabled,
544-
audit_log_strategy=self.config.plugin_audit_strategy,
545-
memory_limit=memory_limit_bytes,
546-
experimental_max_connections=self.config.experimental_max_connections,
547-
binlog_retention_days=self.config.binlog_retention_days,
548-
)
553+
# always setup log rotation
554+
self.log_rotate_setup.setup()
549555

556+
logger.info("Persisting configuration changes to file")
557+
new_config_dict = self._write_mysqld_configuration()
550558
changed_config = compare_dictionaries(previous_config_dict, new_config_dict)
551559

552560
if self.mysql_config.keys_requires_restart(changed_config):
553561
# there are static configurations in changed keys
554-
logger.info("Persisting configuration changes to file")
555-
556-
# persist config to file
557-
self._mysql.write_content_to_file(path=MYSQLD_CONFIG_FILE, content=new_config_content)
558562

559563
if self._mysql.is_mysqld_running():
560564
logger.info("Configuration change requires restart")
561565
if "loose-audit_log_format" in changed_config:
562566
# plugins are manipulated running daemon
563567
if self.config.plugin_audit_enabled:
564-
self._mysql.install_plugins(["audit_log", "audit_log_filter"])
568+
self._mysql.install_plugins(["audit_log"])
565569
else:
566-
self._mysql.uninstall_plugins(["audit_log", "audit_log_filter"])
570+
self._mysql.uninstall_plugins(["audit_log"])
567571
# restart the service
568572
self.on[f"{self.restart.name}"].acquire_lock.emit()
569573
return
@@ -572,7 +576,9 @@ def _on_config_changed(self, _: EventBase) -> None: # noqa: C901
572576
# if only dynamic config changed, apply it
573577
logger.info("Configuration does not requires restart")
574578
for config in dynamic_config:
575-
self._mysql.set_dynamic_variable(config, new_config_dict[config])
579+
self._mysql.set_dynamic_variable(
580+
config.removeprefix("loose-"), new_config_dict[config]
581+
)
576582

577583
def _on_leader_elected(self, _) -> None:
578584
"""Handle the leader elected event.
@@ -615,18 +621,20 @@ def _open_ports(self) -> None:
615621
except ops.ModelError:
616622
logger.exception("failed to open port")
617623

618-
def _write_mysqld_configuration(self):
624+
def _write_mysqld_configuration(self) -> dict:
619625
"""Write the mysqld configuration to the file."""
620626
memory_limit_bytes = (self.config.profile_limit_memory or 0) * BYTES_1MB
621-
new_config_content, _ = self._mysql.render_mysqld_configuration(
627+
new_config_content, new_config_dict = self._mysql.render_mysqld_configuration(
622628
profile=self.config.profile,
623629
audit_log_enabled=self.config.plugin_audit_enabled,
624630
audit_log_strategy=self.config.plugin_audit_strategy,
631+
audit_log_policy=self.config.logs_audit_policy,
625632
memory_limit=memory_limit_bytes,
626633
experimental_max_connections=self.config.experimental_max_connections,
627634
binlog_retention_days=self.config.binlog_retention_days,
628635
)
629636
self._mysql.write_content_to_file(path=MYSQLD_CONFIG_FILE, content=new_config_content)
637+
return new_config_dict
630638

631639
def _configure_instance(self, container) -> None:
632640
"""Configure the instance for use in Group Replication."""
@@ -651,7 +659,7 @@ def _configure_instance(self, container) -> None:
651659

652660
if self.config.plugin_audit_enabled:
653661
# Enable the audit plugin
654-
self._mysql.install_plugins(["audit_log", "audit_log_filter"])
662+
self._mysql.install_plugins(["audit_log"])
655663

656664
# Configure instance as a cluster node
657665
self._mysql.configure_instance()
@@ -717,8 +725,7 @@ def _on_mysql_pebble_ready(self, event) -> None:
717725
container = event.workload
718726
self._write_mysqld_configuration()
719727

720-
logger.info("Setting up the logrotate configurations")
721-
self._mysql.setup_logrotate_config()
728+
self.log_rotate_setup.setup()
722729

723730
if self._mysql.is_data_dir_initialised():
724731
# Data directory is already initialised, skip configuration

src/config.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class CharmConfig(BaseConfigModel):
6262
binlog_retention_days: int
6363
plugin_audit_enabled: bool
6464
plugin_audit_strategy: str
65+
logs_audit_policy: str
66+
logs_retention_period: str
6567

6668
@validator("profile")
6769
@classmethod
@@ -152,6 +154,25 @@ def binlog_retention_days_validator(cls, value: int) -> int:
152154
def plugin_audit_strategy_validator(cls, value: str) -> Optional[str]:
153155
"""Check profile config option is one of `testing` or `production`."""
154156
if value not in ["async", "semi-async"]:
155-
raise ValueError("Value not one of 'async' or 'semi-async'")
157+
raise ValueError("plugin_audit_strategy not one of 'async' or 'semi-async'")
158+
159+
return value
160+
161+
@validator("logs_audit_policy")
162+
@classmethod
163+
def logs_audit_policy_validator(cls, value: str) -> Optional[str]:
164+
"""Check values for audit log policy."""
165+
valid_values = ["all", "logins", "queries"]
166+
if value not in valid_values:
167+
raise ValueError(f"logs_audit_policy not one of {', '.join(valid_values)}")
168+
169+
return value
170+
171+
@validator("logs_retention_period")
172+
@classmethod
173+
def logs_retention_period_validator(cls, value: str) -> str:
174+
"""Check logs retention period."""
175+
if not re.match(r"auto|\d{1,3}", value) or value == "0":
176+
raise ValueError("logs_retention_period must be integer greater than 0 or `auto`")
156177

157178
return value

src/constants.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@
3333
MYSQLD_SOCK_FILE = "/var/run/mysqld/mysqld.sock"
3434
MYSQLSH_SCRIPT_FILE = "/tmp/script.py"
3535
MYSQLD_CONFIG_FILE = "/etc/mysql/mysql.conf.d/z-custom.cnf"
36+
MYSQL_LOG_DIR = "/var/log/mysql"
3637
MYSQL_LOG_FILES = [
37-
"/var/log/mysql/error.log",
38-
"/var/log/mysql/audit.log",
39-
"/var/log/mysql/general.log",
38+
f"{MYSQL_LOG_DIR}/error.log",
39+
f"{MYSQL_LOG_DIR}/audit.log",
40+
f"{MYSQL_LOG_DIR}/general.log",
4041
]
4142
MYSQL_SYSTEM_USER = "mysql"
4243
MYSQL_SYSTEM_GROUP = "mysql"
@@ -50,6 +51,7 @@
5051
GR_MAX_MEMBERS = 9
5152
# TODO: should be changed when adopting cos-agent
5253
COS_AGENT_RELATION_NAME = "metrics-endpoint"
54+
COS_LOGGING_RELATION_NAME = "logging"
5355
LOG_ROTATE_CONFIG_FILE = "/etc/logrotate.d/flush_mysql_logs"
5456
ROOT_SYSTEM_USER = "root"
5557
SECRET_KEY_FALLBACKS = {

0 commit comments

Comments
 (0)