Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPE-1799 Add rotation of mysqlrouter logs #143

Merged
merged 14 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ jobs:
build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/[email protected]
with:
charmcraft-snap-revision: 1349 # version 2.3.0
permissions:
actions: write # Needed to manage GitHub Actions cache

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ jobs:
build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/[email protected]
with:
charmcraft-snap-revision: 1349 # version 2.3.0

release:
name: Release charm
Expand Down
1 change: 1 addition & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parts:
prime:
- charm_version
- workload_version
- scripts
build-packages:
- libffi-dev
- libssl-dev
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ resources:
mysql-router-image:
type: oci-image
description: OCI image for mysql-router
upstream-source: ghcr.io/canonical/charmed-mysql@sha256:3b6a4a63971acec3b71a0178cd093014a695ddf7c31d91d56ebb110eec6cdbe1
upstream-source: ghcr.io/canonical/charmed-mysql@sha256:0f5fe7d7679b1881afde24ecfb9d14a9daade790ec787087aa5d8de1d7b00b21
carlcsaposs-canonical marked this conversation as resolved.
Show resolved Hide resolved
assumes:
- k8s-api
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ python = "^3.10"
ops = "^2.6.0"
lightkube = "^0.14.0"
tenacity = "^8.2.3"
jinja2 = "^3.1.2"
poetry-core = "^1.7.0"

[tool.poetry.group.charm-libs.dependencies]
Expand Down Expand Up @@ -57,6 +58,7 @@ pytest-operator-groups = {git = "https://github.com/canonical/data-platform-work
juju = "^2.9.44.1"
mysql-connector-python = "~8.0.33"
pyyaml = "^6.0.1"
tenacity = "^8.2.2"


[tool.coverage.run]
Expand Down
36 changes: 36 additions & 0 deletions scripts/logrotate_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Runs logrotate every minute."""

import subprocess
import time


def main():
"""Main watch and dispatch loop.

Roughly every 60s at the top of the minute, execute logrotate.
"""
# wait till the top of the minute
time.sleep(60 - (time.time() % 60))
start_time = time.monotonic()

while True:
subprocess.run(
[
"logrotate",
"-f",
"-s",
"/tmp/logrotate.status",
"/etc/logrotate.d/flush_mysqlrouter_logs",
],
check=True,
)

# wait again till the top of the next minute
time.sleep(60.0 - ((time.monotonic() - start_time) % 60.0))


if __name__ == "__main__":
main()
9 changes: 8 additions & 1 deletion src/abstract_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import container
import lifecycle
import logrotate
import relations.database_provides
import relations.database_requires
import upgrade
Expand Down Expand Up @@ -70,6 +71,11 @@ def _tls_certificate_saved(self) -> bool:
def _container(self) -> container.Container:
"""Workload container (snap or ROCK)"""

@property
@abc.abstractmethod
def _logrotate(self) -> logrotate.LogRotate:
"""logrotate"""

@property
@abc.abstractmethod
def _upgrade(self) -> typing.Optional[upgrade.Upgrade]:
Expand All @@ -90,10 +96,11 @@ def get_workload(self, *, event):
if connection_info := self._database_requires.get_connection_info(event=event):
return self._authenticated_workload_type(
container_=self._container,
logrotate_=self._logrotate,
connection_info=connection_info,
charm_=self,
)
return self._workload_type(container_=self._container)
return self._workload_type(container_=self._container, logrotate_=self._logrotate)

@staticmethod
# TODO python3.10 min version: Use `list` instead of `typing.List`
Expand Down
6 changes: 6 additions & 0 deletions src/kubernetes_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import ops

import abstract_charm
import kubernetes_logrotate
import kubernetes_upgrade
import logrotate
import relations.tls
import rock
import upgrade
Expand Down Expand Up @@ -53,6 +55,10 @@ def tls_certificate_saved(self) -> bool:
def _container(self) -> rock.Rock:
return rock.Rock(unit=self.unit)

@property
def _logrotate(self) -> logrotate.LogRotate:
return kubernetes_logrotate.LogRotate(container_=self._container)

@property
def _upgrade(self) -> typing.Optional[kubernetes_upgrade.Upgrade]:
try:
Expand Down
45 changes: 45 additions & 0 deletions src/kubernetes_logrotate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""logrotate implementation for k8s"""

import logging
import pathlib

import container
import logrotate

logger = logging.getLogger(__name__)


class LogRotate(logrotate.LogRotate):
shayancanonical marked this conversation as resolved.
Show resolved Hide resolved
"""logrotate implementation for k8s"""

_SYSTEM_USER = "mysql"

def __init__(self, *, container_: container.Container):
super().__init__(container_=container_)
self._logrotate_executor = self._container.path("/logrotate_executor.py")

def enable(self) -> None:
super().enable()

logger.debug("Copying log rotate executor script to workload container")
self._logrotate_executor.write_text(
pathlib.Path("scripts/logrotate_executor.py").read_text()
)
logger.debug("Copied log rotate executor to workload container")

logger.debug("Starting the logrotate executor service")
self._container.update_logrotate_executor_service(enabled=True)
logger.debug("Started the logrotate executor service")

def disable(self) -> None:
logger.debug("Stopping the logrotate executor service")
self._container.update_logrotate_executor_service(enabled=False)
logger.debug("Stopped the logrotate executor service")

logger.debug("Removing logrotate config and executor files")
super().disable()
self._logrotate_executor.unlink()
logger.debug("Removed logrotate config and executor files")
50 changes: 50 additions & 0 deletions src/logrotate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""logrotate

https://manpages.ubuntu.com/manpages/jammy/man8/logrotate.8.html
"""

import abc
import logging
import pathlib

import jinja2

import container

logger = logging.getLogger(__name__)


class LogRotate(abc.ABC):
"""logrotate"""

def __init__(self, *, container_: container.Container):
self._container = container_

self._logrotate_config = self._container.path("/etc/logrotate.d/flush_mysqlrouter_logs")

@property
@abc.abstractmethod
def _SYSTEM_USER(self) -> str: # noqa
"""The system user that mysqlrouter runs as."""

def enable(self) -> None:
"""Enable logrotate."""
logger.debug("Creating logrotate config file")

template = jinja2.Template(pathlib.Path("templates/logrotate.j2").read_text())

log_file_path = self._container.path("/var/log/mysqlrouter/mysqlrouter.log")
rendered = template.render(
log_file_path=str(log_file_path),
system_user=self._SYSTEM_USER,
)
self._logrotate_config.write_text(rendered)

logger.debug("Created logrotate config file")

def disable(self) -> None:
"""Disable logrotate."""
self._logrotate_config.unlink()
35 changes: 34 additions & 1 deletion src/rock.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class Rock(container.Container):
"""Workload ROCK or OCI container"""

_SERVICE_NAME = "mysql_router"
_LOGROTATE_EXECUTOR_SERVICE_NAME = "logrotate_executor"

def __init__(self, *, unit: ops.Unit) -> None:
super().__init__(mysql_router_command="mysqlrouter", mysql_shell_command="mysqlsh")
Expand Down Expand Up @@ -89,7 +90,6 @@ def update_mysql_router_service(self, *, enabled: bool, tls: bool = None) -> Non
startup = ops.pebble.ServiceStartup.DISABLED.value
layer = ops.pebble.Layer(
{
"summary": "MySQL Router layer",
"services": {
self._SERVICE_NAME: {
"override": "replace",
Expand All @@ -110,6 +110,39 @@ def update_mysql_router_service(self, *, enabled: bool, tls: bool = None) -> Non
else:
self._container.stop(self._SERVICE_NAME)

def update_logrotate_executor_service(self, *, enabled: bool) -> None:
"""Update and restart log rotate executor service.

Args:
enabled: Whether log rotate executor service is enabled
"""
startup = (
ops.pebble.ServiceStartup.ENABLED.value
if enabled
else ops.pebble.ServiceStartup.DISABLED.value
)
layer = ops.pebble.Layer(
{
"services": {
self._LOGROTATE_EXECUTOR_SERVICE_NAME: {
"override": "replace",
"summary": "Logrotate executor",
"command": "python3 /logrotate_executor.py",
"startup": startup,
"user": _UNIX_USERNAME,
"group": _UNIX_USERNAME,
},
},
}
)
self._container.add_layer(self._LOGROTATE_EXECUTOR_SERVICE_NAME, layer, combine=True)
# `self._container.replan()` does not stop services that have been disabled
# Use `restart()` and `stop()` instead
if enabled:
self._container.restart(self._LOGROTATE_EXECUTOR_SERVICE_NAME)
else:
self._container.stop(self._LOGROTATE_EXECUTOR_SERVICE_NAME)

# TODO python3.10 min version: Use `list` instead of `typing.List`
def _run_command(self, command: typing.List[str], *, timeout: typing.Optional[int]) -> str:
try:
Expand Down
11 changes: 9 additions & 2 deletions src/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ops

import container
import logrotate
import mysql_shell

if typing.TYPE_CHECKING:
Expand All @@ -25,8 +26,11 @@
class Workload:
"""MySQL Router workload"""

def __init__(self, *, container_: container.Container) -> None:
def __init__(
self, *, container_: container.Container, logrotate_: logrotate.LogRotate
) -> None:
self._container = container_
self._logrotate = logrotate_
self._router_data_directory = self._container.path("/var/lib/mysqlrouter")
self._tls_key_file = self._container.router_config_directory / "custom-key.pem"
self._tls_certificate_file = (
Expand Down Expand Up @@ -56,6 +60,7 @@ def disable(self) -> None:
return
logger.debug("Disabling MySQL Router service")
self._container.update_mysql_router_service(enabled=False)
self._logrotate.disable()
self._container.router_config_directory.rmtree()
self._container.router_config_directory.mkdir()
self._router_data_directory.rmtree()
Expand Down Expand Up @@ -108,10 +113,11 @@ def __init__(
self,
*,
container_: container.Container,
logrotate_: logrotate.LogRotate,
connection_info: "relations.database_requires.ConnectionInformation",
charm_: "abstract_charm.MySQLRouterCharm",
) -> None:
super().__init__(container_=container_)
super().__init__(container_=container_, logrotate_=logrotate_)
self._connection_info = connection_info
self._charm = charm_

Expand Down Expand Up @@ -218,6 +224,7 @@ def enable(self, *, tls: bool, unit_name: str) -> None:
username=self._router_username, router_id=self._router_id, unit_name=unit_name
)
self._container.update_mysql_router_service(enabled=True, tls=tls)
self._logrotate.enable()
logger.debug("Enabled MySQL Router service")
self._charm.wait_until_mysql_router_ready()

Expand Down
29 changes: 29 additions & 0 deletions templates/logrotate.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Use system user
su {{ system_user }} {{ system_user }}

# Create dedicated subdirectory for rotated files
createolddir 770 {{ system_user }} {{ system_user }}

# Frequency of logs rotation
hourly
maxage 7
rotate 10800

# Naming of rotated files should be in the format:
dateext
dateformat -%V_%H%M

# Settings to prevent misconfigurations and unwanted behaviours
ifempty
missingok
nocompress
nomail
nosharedscripts
nocopytruncate

{{ log_file_path }} {
olddir archive_mysqlrouter
postrotate
kill -HUP $(pidof mysqlrouter)
endscript
}
Loading