diff --git a/charmcraft.yaml b/charmcraft.yaml index f9f3a772..9c65140d 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -254,6 +254,14 @@ config: Otherwise, the value is set to the greater of the setpoint or 100,000. Ref: https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage type: int + out_of_order_time_window: + default: "0s" + description: | + The time window within which out-of-order samples are accepted for ingestion. + Supported units: s, m, h (Prometheus duration format; "m" is minutes). + Set to "0s" to disable (default). + See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tsdb + type: string actions: validate-configuration: diff --git a/src/charm.py b/src/charm.py index 9a35e664..ecf9ddcc 100755 --- a/src/charm.py +++ b/src/charm.py @@ -339,6 +339,10 @@ def _on_collect_unit_status(self, event: CollectStatusEvent): if not is_valid_timespec(cast(str, retention_time)): event.add_status(BlockedStatus(f"Invalid time spec : {retention_time}")) + out_of_order_time_window = self.model.config.get("out_of_order_time_window", "") + if not is_valid_timespec(cast(str, out_of_order_time_window)): + event.add_status(BlockedStatus(f"Invalid time spec : {out_of_order_time_window}")) + # "Push" statuses for status in self._stored.status.values(): event.add_status(to_status(status)) @@ -1131,8 +1135,15 @@ def _generate_prometheus_config(self) -> bool: web_config = self._web_config() + storage_config = {} if self._exemplars: - prometheus_config["storage"] = {"exemplars": {"max_exemplars": self._exemplars}} + storage_config["exemplars"] = {"max_exemplars": self._exemplars} + if is_valid_timespec( + ooo := cast(str, self.model.config.get("out_of_order_time_window", "")) + ): + storage_config.setdefault("tsdb", {})["out_of_order_time_window"] = ooo + if storage_config: + prometheus_config["storage"] = storage_config if self.workload_tracing_endpoint: prometheus_config["tracing"] = self._tracing_config() diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index d98c7184..b7160206 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -175,7 +175,26 @@ def test_invalid_metrics_retention_times_can_not_be_set(self, *unused): @k8s_resource_multipatch @patch("lightkube.core.client.GenericSyncClient") - @prom_multipatch + def test_valid_out_of_order_time_window_can_be_set(self, *unused): + acceptable = ["5m", "1h", "30s", "0s"] + for value in acceptable: + self.harness.update_config({"out_of_order_time_window": value}) + container = self.harness.charm.unit.get_container(self.harness.charm._name) + config = yaml.safe_load(container.pull(PROMETHEUS_CONFIG)) + self.assertEqual( + config.get("storage", {}).get("tsdb", {}).get("out_of_order_time_window"), value + ) + + @k8s_resource_multipatch + @patch("lightkube.core.client.GenericSyncClient") + def test_invalid_out_of_order_time_window_can_not_be_set(self, *unused): + self.harness.update_config({"out_of_order_time_window": "5min"}) + container = self.harness.charm.unit.get_container(self.harness.charm._name) + config = yaml.safe_load(container.pull(PROMETHEUS_CONFIG)) + self.assertIsNone(config.get("storage", {}).get("tsdb")) + + @k8s_resource_multipatch + @patch("lightkube.core.client.GenericSyncClient") def test_global_evaluation_interval_can_be_set(self, *unused): evalint_config = {} acceptable_units = ["y", "w", "d", "h", "m", "s"]