diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 277a4f885..303a11320 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,7 +10,9 @@ ## New Features - +* Many tasks, senders and receivers now have proper names for easier debugging. +* The resample log was improved to show more details. +* The `Sample` class now has a nice `__str__` representation. ## Bug Fixes diff --git a/src/frequenz/sdk/microgrid/_data_pipeline.py b/src/frequenz/sdk/microgrid/_data_pipeline.py index 506d869e8..63b13ce27 100644 --- a/src/frequenz/sdk/microgrid/_data_pipeline.py +++ b/src/frequenz/sdk/microgrid/_data_pipeline.py @@ -479,7 +479,8 @@ def _resampling_request_sender(self) -> Sender[ComponentMetricRequest]: channel_registry=self._channel_registry, data_sourcing_request_sender=self._data_sourcing_request_sender(), resampling_request_receiver=channel.new_receiver( - limit=_REQUEST_RECV_BUFFER_SIZE + limit=_REQUEST_RECV_BUFFER_SIZE, + name=channel.name + " Receiver", ), config=self._resampler_config, ) diff --git a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py index b2d5c29ff..9ceabd456 100644 --- a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py +++ b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py @@ -461,7 +461,8 @@ async def _update_streams( self.comp_data_tasks[comp_id].cancel() self.comp_data_tasks[comp_id] = asyncio.create_task( - run_forever(lambda: self._handle_data_stream(comp_id, category)) + run_forever(lambda: self._handle_data_stream(comp_id, category)), + name=f"{type(self).__name__}._update_stream({comp_id=}, {category.name})", ) async def add_metric(self, request: ComponentMetricRequest) -> None: diff --git a/src/frequenz/sdk/microgrid/_resampling.py b/src/frequenz/sdk/microgrid/_resampling.py index fa403d53c..e16ca8d01 100644 --- a/src/frequenz/sdk/microgrid/_resampling.py +++ b/src/frequenz/sdk/microgrid/_resampling.py @@ -179,7 +179,11 @@ def _log_resampling_task_error(self, resampling_task: asyncio.Task[None]) -> Non resampling_task.result() except ResamplingError as error: for source, source_error in error.exceptions.items(): - _logger.error("Error resampling source %s, removing source...", source) + _logger.error( + "Error resampling source %s, removing source", + source, + exc_info=source_error, + ) removed = self._resampler.remove_timeseries(source) if not removed: _logger.error( diff --git a/src/frequenz/sdk/timeseries/_base_types.py b/src/frequenz/sdk/timeseries/_base_types.py index eff275fa1..8673672f1 100644 --- a/src/frequenz/sdk/timeseries/_base_types.py +++ b/src/frequenz/sdk/timeseries/_base_types.py @@ -34,6 +34,14 @@ class Sample(Generic[QuantityT]): value: QuantityT | None = None """The value of this sample.""" + def __str__(self) -> str: + """Return a string representation of the sample.""" + return f"{type(self).__name__}({self.timestamp}, {self.value})" + + def __repr__(self) -> str: + """Return a string representation of the sample.""" + return f"{type(self).__name__}({self.timestamp=}, {self.value=})" + @dataclass(frozen=True) class Sample3Phase(Generic[QuantityT]): diff --git a/src/frequenz/sdk/timeseries/_resampling.py b/src/frequenz/sdk/timeseries/_resampling.py index c00411601..b2307a926 100644 --- a/src/frequenz/sdk/timeseries/_resampling.py +++ b/src/frequenz/sdk/timeseries/_resampling.py @@ -744,7 +744,8 @@ def resample(self, timestamp: datetime) -> Sample[Quantity]: # resort to some C (or similar) implementation. relevant_samples = list(itertools.islice(self._buffer, min_index, max_index)) if not relevant_samples: - _logger.warning("No relevant samples found for: %s", self._name) + self._log_no_relevant_samples(minimum_relevant_timestamp, timestamp) + value = ( conf.resampling_function(relevant_samples, conf, props) if relevant_samples @@ -752,6 +753,34 @@ def resample(self, timestamp: datetime) -> Sample[Quantity]: ) return Sample(timestamp, None if value is None else Quantity(value)) + def _log_no_relevant_samples( + self, minimum_relevant_timestamp: datetime, timestamp: datetime + ) -> None: + """Log that no relevant samples were found. + + Args: + minimum_relevant_timestamp: Minimum timestamp that was requested + timestamp: Timestamp that was requested + """ + if not _logger.isEnabledFor(logging.WARNING): + return + + if self._buffer: + buffer_info = ( + f"{self._buffer[0].timestamp} - " + f"{self._buffer[-1].timestamp} ({len(self._buffer)} samples)" + ) + else: + buffer_info = "Empty" + + _logger.warning( + "No relevant samples found for: %s\n Requested: %s - %s\n Buffer: %s", + self._name, + minimum_relevant_timestamp, + timestamp, + buffer_info, + ) + class _StreamingHelper: """Resample data coming from a source, sending the results to a sink.""" diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py index f011570c0..055750824 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py @@ -24,7 +24,7 @@ class BatteryPowerFormula(FormulaGenerator[Power]): - """Creates a formula engine from the component graph for calculating grid power.""" + """Creates a formula engine from the component graph for calculating battery power.""" def generate( self,