-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DPE-5601] Add pgBackRest logrotate configuration (#645)
* Add pgBackRest logrotate configuration Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Update how logrotate starts Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Refactor method name and add unit tests Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Merge linting changes --------- Signed-off-by: Marcelo Henrique Neppel <[email protected]> Co-authored-by: Dragomir Penev <[email protected]>
- Loading branch information
1 parent
037d1bb
commit 685d78c
Showing
9 changed files
with
221 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Copyright 2024 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
|
||
"""Background process for rotating logs.""" | ||
|
||
import logging | ||
import os | ||
import subprocess | ||
from time import sleep | ||
|
||
from ops.charm import CharmBase | ||
from ops.framework import Object | ||
from ops.model import ActiveStatus | ||
|
||
from constants import PGBACKREST_LOGROTATE_FILE | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
# File path for the spawned rotate logs process to write logs. | ||
LOG_FILE_PATH = "/var/log/rotate_logs.log" | ||
|
||
|
||
class RotateLogs(Object): | ||
"""Rotate logs every minute.""" | ||
|
||
def __init__(self, charm: CharmBase): | ||
super().__init__(charm, "rotate-logs") | ||
self._charm = charm | ||
|
||
def start_log_rotation(self): | ||
"""Start the rotate logs running in a new process.""" | ||
if ( | ||
not isinstance(self._charm.unit.status, ActiveStatus) | ||
or self._charm._peers is None | ||
or not os.path.exists(PGBACKREST_LOGROTATE_FILE) | ||
): | ||
return | ||
if "rotate-logs-pid" in self._charm.unit_peer_data: | ||
# Double check that the PID exists. | ||
pid = int(self._charm.unit_peer_data["rotate-logs-pid"]) | ||
try: | ||
os.kill(pid, 0) | ||
return | ||
except OSError: | ||
pass | ||
|
||
logging.info("Starting rotate logs process") | ||
|
||
# Input is generated by the charm | ||
pid = subprocess.Popen( # noqa: S603 | ||
["/usr/bin/python3", "src/rotate_logs.py"], | ||
# File should not close | ||
stdout=open(LOG_FILE_PATH, "a"), # noqa: SIM115 | ||
stderr=subprocess.STDOUT, | ||
).pid | ||
|
||
self._charm.unit_peer_data.update({"rotate-logs-pid": f"{pid}"}) | ||
logging.info(f"Started rotate logs process with PID {pid}") | ||
|
||
|
||
def main(): | ||
"""Main loop that calls logrotate.""" | ||
while True: | ||
# Input is constant | ||
subprocess.run(["/usr/sbin/logrotate", "-f", PGBACKREST_LOGROTATE_FILE]) # noqa: S603 | ||
|
||
# Wait 60 seconds before executing logrotate again. | ||
sleep(60) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/var/snap/charmed-postgresql/common/var/log/pgbackrest/*.log { | ||
rotate 10 | ||
missingok | ||
notifempty | ||
nocompress | ||
daily | ||
create 0600 snap_daemon snap_daemon | ||
dateext | ||
dateformat -%Y%m%d_%H:%M.log | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Copyright 2024 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
from unittest.mock import Mock, PropertyMock, patch | ||
|
||
import pytest | ||
from ops.charm import CharmBase | ||
from ops.model import ActiveStatus, Relation, WaitingStatus | ||
from ops.testing import Harness | ||
|
||
from rotate_logs import RotateLogs | ||
|
||
|
||
class MockCharm(CharmBase): | ||
def __init__(self, *args): | ||
super().__init__(*args) | ||
|
||
self.rotate_logs = RotateLogs(self) | ||
|
||
@property | ||
def _peers(self) -> Relation | None: | ||
return None | ||
|
||
@property | ||
def unit_peer_data(self) -> dict: | ||
"""Unit peer relation data object.""" | ||
if self._peers is None: | ||
return {} | ||
|
||
return self._peers.data[self.unit] | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def harness(): | ||
harness = Harness(MockCharm, meta="name: test-charm") | ||
harness.begin() | ||
yield harness | ||
harness.cleanup() | ||
|
||
|
||
def test_start_log_rotation(harness): | ||
with ( | ||
patch("builtins.open") as _open, | ||
patch("subprocess.Popen") as _popen, | ||
patch("os.path.exists") as _exists, | ||
patch.object(MockCharm, "_peers", new_callable=PropertyMock) as _peers, | ||
): | ||
# Test that nothing is done if there is already a running process. | ||
_peers.return_value = Mock(data={harness.charm.unit: {"rotate-logs-pid": "1"}}) | ||
_exists.return_value = True | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_popen.assert_not_called() | ||
|
||
# Test that nothing is done if the charm is not in an active status. | ||
harness.charm.unit.status = WaitingStatus() | ||
_peers.return_value = Mock(data={harness.charm.unit: {}}) | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_popen.assert_not_called() | ||
|
||
# Test that nothing is done if peer relation is not available yet. | ||
harness.charm.unit.status = ActiveStatus() | ||
_peers.return_value = None | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_popen.assert_not_called() | ||
|
||
# Test that nothing is done if the logrotate file does not exist. | ||
_peers.return_value = Mock(data={harness.charm.unit: {}}) | ||
_exists.return_value = False | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_popen.assert_not_called() | ||
|
||
# Test that nothing is done if there is already a running process. | ||
_popen.return_value = Mock(pid=1) | ||
_exists.return_value = True | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_popen.assert_called_once() | ||
|
||
|
||
def test_start_log_rotation_already_running(harness): | ||
with ( | ||
patch("builtins.open") as _open, | ||
patch("subprocess.Popen") as _popen, | ||
patch("os.kill") as _kill, | ||
patch("os.path.exists") as _exists, | ||
patch.object(MockCharm, "_peers", new_callable=PropertyMock) as _peers, | ||
): | ||
harness.charm.unit.status = ActiveStatus() | ||
_peers.return_value = Mock(data={harness.charm.unit: {"rotate-logs-pid": "1234"}}) | ||
_exists.return_value = True | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_kill.assert_called_once_with(1234, 0) | ||
assert not _popen.called | ||
_kill.reset_mock() | ||
|
||
# If process is already dead, it should restart. | ||
_kill.side_effect = OSError | ||
harness.charm.rotate_logs.start_log_rotation() | ||
_kill.assert_called_once_with(1234, 0) | ||
_popen.assert_called_once() |