From aab602c0dbee6ccb14f17a007095da2728518d28 Mon Sep 17 00:00:00 2001 From: Noctua Date: Fri, 13 Sep 2024 18:04:13 +0200 Subject: [PATCH 1/2] chore: update charm libraries (#182) --- lib/charms/loki_k8s/v1/loki_push_api.py | 81 +++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/lib/charms/loki_k8s/v1/loki_push_api.py b/lib/charms/loki_k8s/v1/loki_push_api.py index 7f8372c..d75cb7e 100644 --- a/lib/charms/loki_k8s/v1/loki_push_api.py +++ b/lib/charms/loki_k8s/v1/loki_push_api.py @@ -480,6 +480,25 @@ def _alert_rules_error(self, event): Units of consumer charm send their alert rules over app relation data using the `alert_rules` key. + +## Charm logging +The `charms.loki_k8s.v0.charm_logging` library can be used in conjunction with this one to configure python's +logging module to forward all logs to Loki via the loki-push-api interface. + +```python +from lib.charms.loki_k8s.v0.charm_logging import log_charm +from lib.charms.loki_k8s.v1.loki_push_api import charm_logging_config, LokiPushApiConsumer + +@log_charm(logging_endpoint="my_endpoints", server_cert="cert_path") +class MyCharm(...): + _cert_path = "/path/to/cert/on/charm/container.crt" + def __init__(self, ...): + self.logging = LokiPushApiConsumer(...) + self.my_endpoints, self.cert_path = charm_logging_config( + self.logging, self._cert_path) +``` + +Do this, and all charm logs will be forwarded to Loki as soon as a relation is formed. """ import json @@ -527,7 +546,7 @@ def _alert_rules_error(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 12 +LIBPATCH = 13 PYDEPS = ["cosl"] @@ -577,7 +596,11 @@ def _alert_rules_error(self, event): GRPC_LISTEN_PORT_START = 9095 # odd start port -class RelationNotFoundError(ValueError): +class LokiPushApiError(Exception): + """Base class for errors raised by this module.""" + + +class RelationNotFoundError(LokiPushApiError): """Raised if there is no relation with the given name.""" def __init__(self, relation_name: str): @@ -587,7 +610,7 @@ def __init__(self, relation_name: str): super().__init__(self.message) -class RelationInterfaceMismatchError(Exception): +class RelationInterfaceMismatchError(LokiPushApiError): """Raised if the relation with the given name has a different interface.""" def __init__( @@ -607,7 +630,7 @@ def __init__( super().__init__(self.message) -class RelationRoleMismatchError(Exception): +class RelationRoleMismatchError(LokiPushApiError): """Raised if the relation with the given name has a different direction.""" def __init__( @@ -2555,7 +2578,7 @@ def _on_pebble_ready(self, event: PebbleReadyEvent): self._update_endpoints(event.workload, loki_endpoints) - def _update_logging(self, _): + def _update_logging(self, event: RelationEvent): """Update the log forwarding to match the active Loki endpoints.""" if not (loki_endpoints := self._retrieve_endpoints_from_relation()): logger.warning("No Loki endpoints available") @@ -2566,6 +2589,8 @@ def _update_logging(self, _): self._update_endpoints(container, loki_endpoints) # else: `_update_endpoints` will be called on pebble-ready anyway. + self._handle_alert_rules(event.relation) + def _retrieve_endpoints_from_relation(self) -> dict: loki_endpoints = {} @@ -2750,3 +2775,49 @@ def _exec(self, cmd) -> str: result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE) output = result.stdout.decode("utf-8").strip() return output + + +def charm_logging_config( + endpoint_requirer: LokiPushApiConsumer, cert_path: Optional[Union[Path, str]] +) -> Tuple[Optional[List[str]], Optional[str]]: + """Utility function to determine the charm_logging config you will likely want. + + If no endpoint is provided: + disable charm logging. + If https endpoint is provided but cert_path is not found on disk: + disable charm logging. + If https endpoint is provided and cert_path is None: + ERROR + Else: + proceed with charm logging (with or without tls, as appropriate) + + Args: + endpoint_requirer: an instance of LokiPushApiConsumer. + cert_path: a path where a cert is stored. + + Returns: + A tuple with (optionally) the values of the endpoints and the certificate path. + + Raises: + LokiPushApiError: if some endpoint are http and others https. + """ + endpoints = [ep["url"] for ep in endpoint_requirer.loki_endpoints] + if not endpoints: + return None, None + + https = tuple(endpoint.startswith("https://") for endpoint in endpoints) + + if all(https): # all endpoints are https + if cert_path is None: + raise LokiPushApiError("Cannot send logs to https endpoints without a certificate.") + if not Path(cert_path).exists(): + # if endpoints is https BUT we don't have a server_cert yet: + # disable charm logging until we do to prevent tls errors + return None, None + return endpoints, str(cert_path) + + if all(not x for x in https): # all endpoints are http + return endpoints, None + + # if there's a disagreement, that's very weird: + raise LokiPushApiError("Some endpoints are http, some others are https. That's not good.") From 80854484cecc8562e6e39891b83372f0c93fb864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20C=2E=20Mass=C3=B3n?= <939888+Abuelodelanada@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:17:46 -0300 Subject: [PATCH 2/2] Move `filename` from label to structured_metadata (#180) * remove filename from regular labels. Add it to structured_metadata * linting * add missing pipeline after merge * update libs --- src/charm.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/charm.py b/src/charm.py index 5787ec1..0eae4d1 100755 --- a/src/charm.py +++ b/src/charm.py @@ -456,6 +456,12 @@ def _additional_log_configs(self) -> List[Dict[str, Any]]: "expression": ".*file is a directory.*", }, }, + { + "structured_metadata": {"filename": "filename"}, + }, + { + "labeldrop": ["filename"], + }, ], "static_configs": [ { @@ -477,6 +483,12 @@ def _additional_log_configs(self) -> List[Dict[str, Any]]: "expression": ".*file is a directory.*", }, }, + { + "structured_metadata": {"filename": "filename"}, + }, + { + "labeldrop": ["filename"], + }, ], }, ] @@ -571,6 +583,12 @@ def _snap_plug_job( "expression": ".*file is a directory.*", }, }, + { + "structured_metadata": {"filename": "filename"}, + }, + { + "labeldrop": ["filename"], + }, ], } @@ -634,7 +652,6 @@ def _snap_plugs_logging_configs(self) -> List[Dict[str, Any]]: self._path_label(path), ) shared_logs_configs.append(job) - else: endpoint_owners = { endpoint.owner: {