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

Add config option for log_level #217

Merged
merged 9 commits into from
Dec 10, 2024
7 changes: 7 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ config:
Ref: https://grafana.com/docs/agent/latest/static/configuration/flags/#report-information-usage
type: boolean
default: true
log_level:
description: |
Grafana Agent server log level (only log messages with the given severity
or above). Must be one of: [debug, info, warn, error].
If not set, the Grafana Agent default (info) will be used.
type: string
default: info
path_exclude:
description: >
Glob for a set of log files present in `/var/log` that should be ignored by Grafana Agent.
Expand Down
20 changes: 18 additions & 2 deletions src/grafana_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import socket
from collections import namedtuple
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Set, Union
from typing import Any, Callable, Dict, List, Optional, Set, Union, cast

import yaml
from charms.certificate_transfer_interface.v0.certificate_transfer import (
Expand Down Expand Up @@ -150,6 +150,7 @@ def __init__(self, *args):

for rules in [self.loki_rules_paths, self.dashboard_paths]:
if not os.path.isdir(rules.dest):
rules.src.mkdir(parents=True, exist_ok=True)
shutil.copytree(rules.src, rules.dest, dirs_exist_ok=True)

self._remote_write = PrometheusRemoteWriteConsumer(
Expand Down Expand Up @@ -750,7 +751,7 @@ def _server_config(self) -> dict:
Returns:
The dict representing the config
"""
server_config: Dict[str, Any] = {"log_level": "info"}
server_config: Dict[str, Any] = {"log_level": self.log_level}
if self.cert.enabled:
server_config["http_tls_config"] = self.tls_config
server_config["grpc_tls_config"] = self.tls_config
Expand Down Expand Up @@ -1066,6 +1067,21 @@ def _instance_name(self) -> str:

return socket.getfqdn()

@property
def log_level(self) -> str:
"""The log level configured for the charm."""
# Valid upstream log levels in server_config
# https://grafana.com/docs/agent/latest/static/configuration/server-config/#server_config
allowed_log_levels = ["debug", "info", "warn", "error"]
log_level = cast(str, self.config.get("log_level")).lower()

if log_level not in allowed_log_levels:
logging.warning(
f'Invalid loglevel: {log_level} given, {"/".join(allowed_log_levels)} allowed. defaulting to INFO loglevel.'
MichaelThamm marked this conversation as resolved.
Show resolved Hide resolved
)
log_level = "info"
MichaelThamm marked this conversation as resolved.
Show resolved Hide resolved
return log_level

def _reload_config(self, attempts: int = 10) -> None:
"""Reload the config file.

Expand Down
53 changes: 41 additions & 12 deletions tests/scenario/conftest.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
import shutil
from pathlib import Path, PosixPath
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import PropertyMock, patch

import pytest
from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled

from tests.scenario.helpers import CHARM_ROOT

@pytest.fixture
def placeholder_cfg_path(tmp_path):
return tmp_path / "foo.yaml"

class Vroot(PosixPath):
def clean(self) -> None:
shutil.rmtree(self)
shutil.copytree(CHARM_ROOT / "src", self / "src")

@pytest.fixture()
def mock_config_path(placeholder_cfg_path):
with patch("grafana_agent.CONFIG_PATH", placeholder_cfg_path):
yield

@pytest.fixture
def vroot(tmp_path) -> Path:
vroot = Vroot(str(tmp_path.absolute()))
vroot.clean()
return vroot

@pytest.fixture(autouse=True)
def mock_snap():
"""Mock the charm's snap property so we don't access the host."""
with patch("charm.GrafanaAgentMachineCharm.snap", new_callable=PropertyMock):
yield


@pytest.fixture(autouse=True)
def mock_refresh():
"""Mock the refresh call so we don't access the host."""
with patch("snap_management._install_snap", new_callable=PropertyMock):
yield


CONFIG_MATRIX = [
{"classic_snap": True},
{"classic_snap": False},
]


@pytest.fixture(params=CONFIG_MATRIX)
def charm_config(request):
return request.param


@pytest.fixture(autouse=True)
def mock_charm_tracing():
with charm_tracing_disabled():
yield
23 changes: 13 additions & 10 deletions tests/scenario/helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
from pathlib import Path

import yaml

CHARM_ROOT = Path(__file__).parent.parent.parent


def get_charm_meta(charm_type) -> dict:
raw_meta = (CHARM_ROOT / "charmcraft").with_suffix(".yaml").read_text()
return yaml.safe_load(raw_meta)
from unittest.mock import MagicMock


def set_run_out(mock_run, returncode: int = 0, stdout: str = "", stderr: str = ""):
mock_stdout = MagicMock()
mock_stdout.configure_mock(
**{
"returncode": returncode,
"stdout.decode.return_value": stdout,
"stderr.decode.return_value": stderr,
}
)
mock_run.return_value = mock_stdout
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json

import pytest
from scenario import Context, PeerRelation, Relation, State, SubordinateRelation
from ops.testing import Context, PeerRelation, Relation, State, SubordinateRelation

import charm

Expand All @@ -15,7 +15,7 @@ def use_mock_config_path(mock_config_path):
yield


def test_metrics_alert_rule_labels(vroot, charm_config):
def test_metrics_alert_rule_labels(charm_config):
"""Check that metrics alert rules are labeled with principal topology."""
cos_agent_primary_data = {
"config": json.dumps(
Expand Down Expand Up @@ -97,9 +97,8 @@ def test_metrics_alert_rule_labels(vroot, charm_config):
)
remote_write_relation = Relation("send-remote-write", remote_app_name="prometheus")

context = Context(
ctx = Context(
charm_type=charm.GrafanaAgentMachineCharm,
charm_root=vroot,
)
state = State(
leader=True,
Expand All @@ -112,11 +111,13 @@ def test_metrics_alert_rule_labels(vroot, charm_config):
config=charm_config,
)

state_0 = context.run(event=cos_agent_primary_relation.changed_event, state=state)
state_1 = context.run(event=cos_agent_subordinate_relation.changed_event, state=state_0)
state_2 = context.run(event=remote_write_relation.joined_event, state=state_1)
state_0 = ctx.run(ctx.on.relation_changed(relation=cos_agent_primary_relation), state)
state_1 = ctx.run(ctx.on.relation_changed(relation=cos_agent_subordinate_relation), state_0)
state_2 = ctx.run(ctx.on.relation_joined(relation=remote_write_relation), state_1)

alert_rules = json.loads(state_2.relations[2].local_app_data["alert_rules"])
alert_rules = json.loads(
state_2.get_relation(remote_write_relation.id).local_app_data["alert_rules"]
)
for group in alert_rules["groups"]:
for rule in group["rules"]:
if "grafana-agent_alertgroup_alerts" in group["name"]:
Expand Down
51 changes: 51 additions & 0 deletions tests/scenario/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import patch

import pytest
import yaml
from ops.testing import Context, State

import charm


@pytest.fixture(autouse=True)
def patch_all(placeholder_cfg_path):
with patch("grafana_agent.CONFIG_PATH", placeholder_cfg_path):
yield


@pytest.mark.parametrize("log_level", ("debug", "info", "warn", "error"))
def test_config_log_level(placeholder_cfg_path, log_level):
# GIVEN a GrafanaAgentMachineCharm
with patch("charm.GrafanaAgentMachineCharm.is_ready", True):
ctx = Context(charm_type=charm.GrafanaAgentMachineCharm)
# WHEN the config option for log_level is set to a VALID option
ctx.run(ctx.on.start(), State(config={"log_level": log_level}))

# THEN the config file has the correct server:log_level field
yaml_cfg = yaml.safe_load(placeholder_cfg_path.read_text())
assert yaml_cfg["server"]["log_level"] == log_level


def test_default_config_log_level(placeholder_cfg_path):
# GIVEN a GrafanaAgentMachineCharm
with patch("charm.GrafanaAgentMachineCharm.is_ready", True):
ctx = Context(charm_type=charm.GrafanaAgentMachineCharm)
# WHEN the config option for log_level is set to an INVALID option
ctx.run(ctx.on.start(), State(config={"log_level": "foo"}))

# THEN Juju debug-log is created with WARNING level
found = False
for log in ctx.juju_log:
if (
"WARNING" == log.level
and "Invalid loglevel: foo given, debug/info/warn/error allowed." in log.message
):
found = True
break
MichaelThamm marked this conversation as resolved.
Show resolved Hide resolved
assert found is True

# AND the config file defaults the server:log_level field to "info"
yaml_cfg = yaml.safe_load(placeholder_cfg_path.read_text())
assert yaml_cfg["server"]["log_level"] == "info"
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, patch

import pytest
Expand All @@ -14,13 +11,7 @@
)
from ops.charm import CharmBase
from ops.framework import Framework
from scenario import Context, PeerRelation, State, SubordinateRelation


@pytest.fixture
def placeholder_cfg_path(tmp_path):
return tmp_path / "foo.yaml"

from ops.testing import Context, PeerRelation, State, SubordinateRelation

PROVIDER_NAME = "mock-principal"
PROM_RULE = """alert: HostCpuHighIowait
Expand Down Expand Up @@ -59,28 +50,6 @@ def patch_all(placeholder_cfg_path):
yield


@pytest.fixture(autouse=True)
def vroot(placeholder_cfg_path):
with tempfile.TemporaryDirectory() as vroot:
vroot = Path(vroot)
promroot = vroot / "src/prometheus_alert_rules"
lokiroot = vroot / "src/loki_alert_rules"
grafroot = vroot / "src/grafana_dashboards"

promroot.mkdir(parents=True)
lokiroot.mkdir(parents=True)
grafroot.mkdir(parents=True)

(promroot / "prom.rule").write_text(PROM_RULE)
(lokiroot / "loki.rule").write_text(LOKI_RULE)
(grafroot / "grafana_dashboard.json").write_text(GRAFANA_DASH)

old_cwd = os.getcwd()
os.chdir(str(vroot))
yield vroot
os.chdir(old_cwd)


@pytest.fixture(autouse=True)
def snap_is_installed():
with patch(
Expand Down Expand Up @@ -140,22 +109,26 @@ def __init__(self, framework: Framework):


@pytest.fixture
def provider_ctx(provider_charm, vroot):
return Context(charm_type=provider_charm, meta=provider_charm.META, charm_root=vroot)
def provider_ctx(provider_charm):
return Context(charm_type=provider_charm, meta=provider_charm.META)


@pytest.fixture
def requirer_ctx(requirer_charm, vroot):
return Context(charm_type=requirer_charm, meta=requirer_charm.META, charm_root=vroot)
def requirer_ctx(requirer_charm):
return Context(charm_type=requirer_charm, meta=requirer_charm.META)


def test_cos_agent_changed_no_remote_data(provider_ctx):
cos_agent = SubordinateRelation("cos-agent")

state_out = provider_ctx.run(
cos_agent.changed_event(remote_unit_id=1), State(relations=[cos_agent])
provider_ctx.on.relation_changed(relation=cos_agent, remote_unit=1),
State(relations=[cos_agent]),
)

config = json.loads(state_out.relations[0].local_unit_data[CosAgentPeersUnitData.KEY])
config = json.loads(
state_out.get_relation(cos_agent.id).local_unit_data[CosAgentPeersUnitData.KEY]
)
assert config["metrics_alert_rules"] == {}
assert config["log_alert_rules"] == {}
assert len(config["dashboards"]) == 1
Expand Down Expand Up @@ -187,14 +160,15 @@ def test_subordinate_update(requirer_ctx):
remote_unit_data={"config": json.dumps(config)},
)
state_out1 = requirer_ctx.run(
cos_agent1.changed_event(remote_unit_id=0), State(relations=[cos_agent1, peer])
requirer_ctx.on.relation_changed(relation=cos_agent1, remote_unit=0),
State(relations=[cos_agent1, peer]),
)
peer_out = state_out1.get_relations("peers")[0]
peer_out_data = json.loads(
peer_out.local_unit_data[f"{CosAgentPeersUnitData.KEY}-mock-principal/0"]
)
assert peer_out_data["unit_name"] == f"{PROVIDER_NAME}/0"
assert peer_out_data["relation_id"] == str(cos_agent1.relation_id)
assert peer_out_data["relation_id"] == str(cos_agent1.id)
assert peer_out_data["relation_name"] == cos_agent1.endpoint

# passthrough as-is
Expand All @@ -210,19 +184,22 @@ def test_subordinate_update(requirer_ctx):
assert "http://localhost:4318" in urls


def test_cos_agent_wrong_rel_data(vroot, snap_is_installed, provider_ctx):
def test_cos_agent_wrong_rel_data(snap_is_installed, provider_ctx):
# Step 1: principal charm is deployed and ends in "unknown" state
provider_ctx.charm_spec.charm_type._log_slots = (
"charmed:frogs" # Set wrong type, must be a list
)
cos_agent_rel = SubordinateRelation("cos-agent")
state = State(relations=[cos_agent_rel])
state_out = provider_ctx.run(cos_agent_rel.changed_event(remote_unit_id=1), state=state)

state_out = provider_ctx.run(
provider_ctx.on.relation_changed(relation=cos_agent_rel, remote_unit=1), state
)
assert state_out.unit_status.name == "unknown"

found = False
for log in provider_ctx.juju_log:
if "ERROR" in log[0] and "Invalid relation data provided:" in log[1]:
if "ERROR" == log.level and "Invalid relation data provided:" in log.message:
found = True
break

Expand Down
Loading
Loading