From a91bea3ccc1a07531a5b290f80ddc2186ee2f903 Mon Sep 17 00:00:00 2001 From: landaudiogo Date: Wed, 15 Jul 2026 12:58:44 +0200 Subject: [PATCH 1/4] Add `end_epoch` for query time range customization Introduce a new `end_epoch` option that specifies the end of the Prometheus query window. When used together with the existing history duration option, users can define an arbitrary time range instead of being limited to querying metrics relative to the current time. --- robusta_krr/core/abstract/metrics.py | 2 +- robusta_krr/core/abstract/strategies.py | 6 ++++++ robusta_krr/core/integrations/prometheus/loader.py | 3 ++- robusta_krr/core/integrations/prometheus/metrics/base.py | 5 ++--- .../prometheus/metrics_service/base_metric_service.py | 1 + .../metrics_service/prometheus_metrics_service.py | 3 ++- robusta_krr/core/runner.py | 1 + 7 files changed, 15 insertions(+), 6 deletions(-) diff --git a/robusta_krr/core/abstract/metrics.py b/robusta_krr/core/abstract/metrics.py index 41881719..29327eff 100644 --- a/robusta_krr/core/abstract/metrics.py +++ b/robusta_krr/core/abstract/metrics.py @@ -16,5 +16,5 @@ class BaseMetric(ABC): @abstractmethod async def load_data( - self, object: K8sObjectData, period: datetime.timedelta, step: datetime.timedelta + self, object: K8sObjectData, period: datetime.timedelta, step: datetime.timedelta, end_time: datetime.datetime ) -> PodsTimeData: ... diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 2ff675b1..97fe52a8 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time import abc import datetime from textwrap import dedent @@ -48,6 +49,7 @@ class StrategySettings(pd.BaseModel): 24 * 7 * 2, ge=1, description="The duration of the history data to use (in hours)." ) timeframe_duration: float = pd.Field(1.25, gt=0, description="The step for the history data (in minutes).") + end_epoch: float = pd.Field(time.time(), ge=0.0, description="...") @property def history_timedelta(self) -> datetime.timedelta: @@ -57,6 +59,10 @@ def history_timedelta(self) -> datetime.timedelta: def timeframe_timedelta(self) -> datetime.timedelta: return datetime.timedelta(minutes=self.timeframe_duration) + @property + def end_datetime(self) -> datetime.datetime: + return datetime.datetime.utcfromtimestamp(self.end_epoch) + def history_range_enough(self, history_range: tuple[datetime.timedelta, datetime.timedelta]) -> bool: """Override this function to check if the history range is enough for the strategy.""" diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index 87c39829..c5fc0242 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -107,6 +107,7 @@ async def gather_data( object: K8sObjectData, strategy: BaseStrategy, period: datetime.timedelta, + end_time: datetime.datetime, *, step: datetime.timedelta = datetime.timedelta(minutes=30), ) -> MetricsPodData: @@ -124,6 +125,6 @@ async def gather_data( """ return { - MetricLoader.__name__: await self.loader.gather_data(object, MetricLoader, period, step) + MetricLoader.__name__: await self.loader.gather_data(object, MetricLoader, period, end_time, step) for MetricLoader in strategy.metrics } diff --git a/robusta_krr/core/integrations/prometheus/metrics/base.py b/robusta_krr/core/integrations/prometheus/metrics/base.py index 347e6b93..adc77556 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base.py @@ -154,7 +154,7 @@ async def query_prometheus(self, data: PrometheusMetricData) -> list[PrometheusS return await loop.run_in_executor(self.executor, lambda: self._query_prometheus_sync(data)) async def load_data( - self, object: K8sObjectData, period: datetime.timedelta, step: datetime.timedelta + self, object: K8sObjectData, period: datetime.timedelta, step: datetime.timedelta, end_time: datetime.datetime ) -> PodsTimeData: """ Asynchronous method that loads metric data for a specific object. @@ -172,14 +172,13 @@ async def load_data( duration_str = self._step_to_string(period) query = self.get_query(object, duration_str, step_str) - end_time = datetime.datetime.utcnow().replace(second=0, microsecond=0) start_time = end_time - period # Here if we split the object into multiple sub-objects, we query each sub-object recursively. if self.pods_batch_size is not None and object.pods_count > self.pods_batch_size: results = await asyncio.gather( *[ - self.load_data(splitted_object, period, step) + self.load_data(splitted_object, period, step, end_time) for splitted_object in object.split_into_batches(self.pods_batch_size) ] ) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py index 6e352388..28fe0f80 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py @@ -43,6 +43,7 @@ async def gather_data( object: K8sObjectData, LoaderClass: type[PrometheusMetric], period: datetime.timedelta, + end_time: datetime.datetime, step: datetime.timedelta = datetime.timedelta(minutes=30), ) -> PodsTimeData: ... diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index bc19dcab..3e72fbf3 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -204,6 +204,7 @@ async def gather_data( object: K8sObjectData, LoaderClass: type[PrometheusMetric], period: timedelta, + end_time: datetime, step: timedelta = timedelta(minutes=30), ) -> PodsTimeData: """ @@ -212,7 +213,7 @@ async def gather_data( logger.debug(f"Gathering {LoaderClass.__name__} metric for {object}") try: metric_loader = LoaderClass(self.get_prometheus(), self.name(), self.executor) - data = await metric_loader.load_data(object, period, step) + data = await metric_loader.load_data(object, period, step, end_time) except Exception: logger.exception("Failed to gather resource history data for %s", object) data = {} diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 23a63fd4..bd895765 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -348,6 +348,7 @@ async def _calculate_object_recommendations(self, object: K8sObjectData) -> Opti object, self._strategy, self._strategy.settings.history_timedelta, + self._strategy.settings.end_datetime, step=self._strategy.settings.timeframe_timedelta, ) From d89aa1a33a5a20c6a90e2dd5a3de789ad1a4f89a Mon Sep 17 00:00:00 2001 From: landaudiogo Date: Wed, 15 Jul 2026 13:23:27 +0200 Subject: [PATCH 2/4] Add missing description to end_epoch --- robusta_krr/core/abstract/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 97fe52a8..176d37e1 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -49,7 +49,7 @@ class StrategySettings(pd.BaseModel): 24 * 7 * 2, ge=1, description="The duration of the history data to use (in hours)." ) timeframe_duration: float = pd.Field(1.25, gt=0, description="The step for the history data (in minutes).") - end_epoch: float = pd.Field(time.time(), ge=0.0, description="...") + end_epoch: float = pd.Field(time.time(), ge=0.0, description="The Unix epoch timestamp marking the end of the query window.") @property def history_timedelta(self) -> datetime.timedelta: From 58ead569e526559e49e88cfbe2614639a547060a Mon Sep 17 00:00:00 2001 From: landaudiogo Date: Wed, 15 Jul 2026 13:36:45 +0200 Subject: [PATCH 3/4] Add end_epoch docstrings --- robusta_krr/core/integrations/prometheus/loader.py | 1 + robusta_krr/core/integrations/prometheus/metrics/base.py | 1 + 2 files changed, 2 insertions(+) diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index c5fc0242..d84bb486 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -118,6 +118,7 @@ async def gather_data( object (K8sObjectData): The Kubernetes object. resource (ResourceType): The resource type. period (datetime.timedelta): The time period for which to gather data. + end_time (datetime.datetime): The timestamp marking the end of the query window. step (datetime.timedelta, optional): The time step between data points. Defaults to 30 minutes. Returns: diff --git a/robusta_krr/core/integrations/prometheus/metrics/base.py b/robusta_krr/core/integrations/prometheus/metrics/base.py index adc77556..c29752ff 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base.py @@ -163,6 +163,7 @@ async def load_data( object (K8sObjectData): The object for which metrics need to be loaded. period (datetime.timedelta): The time period for which metrics need to be loaded. step (datetime.timedelta): The time interval between successive metric values. + end_time (datetime.datetime): The timestamp marking the end of the query window. Returns: ResourceHistoryData: An instance of the ResourceHistoryData class representing the loaded metrics. From e2c3a3e7af2b823027f9fe8e6bb95a3c116c04db Mon Sep 17 00:00:00 2001 From: landaudiogo Date: Wed, 15 Jul 2026 17:01:16 +0200 Subject: [PATCH 4/4] Update deprecated `utcfromtimestamp` Use `fromtimestamp` with utc timezone instead --- robusta_krr/core/abstract/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 176d37e1..0ddfac62 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -61,7 +61,7 @@ def timeframe_timedelta(self) -> datetime.timedelta: @property def end_datetime(self) -> datetime.datetime: - return datetime.datetime.utcfromtimestamp(self.end_epoch) + return datetime.datetime.fromtimestamp(self.end_epoch, tz=datetime.timezone.utc) def history_range_enough(self, history_range: tuple[datetime.timedelta, datetime.timedelta]) -> bool: """Override this function to check if the history range is enough for the strategy."""