-
Notifications
You must be signed in to change notification settings - Fork 17
Logging improvements #1132
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
base: v1.x.x
Are you sure you want to change the base?
Logging improvements #1132
Changes from all commits
374665f
63e0b89
4b41848
d1fd598
8f497e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,300 @@ | ||||||||||||||||||||||||
# License: MIT | ||||||||||||||||||||||||
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
"""Logging utilities for the SDK.""" | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import logging | ||||||||||||||||||||||||
from collections.abc import Mapping | ||||||||||||||||||||||||
from datetime import datetime, timedelta | ||||||||||||||||||||||||
from types import TracebackType | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
_ExcInfoType = ( | ||||||||||||||||||||||||
bool | ||||||||||||||||||||||||
| BaseException | ||||||||||||||||||||||||
| tuple[None, None, None] | ||||||||||||||||||||||||
| tuple[type[BaseException], BaseException, TracebackType | None] | ||||||||||||||||||||||||
| None | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
DEFAULT_RATE_LIMIT = timedelta(minutes=15) | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably just put the literal |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
# The standard logging.py file uses variadic arguments in the logging methods, but the | ||||||||||||||||||||||||
# type hints file has more specific parameters. pylint is not able to handle this, so | ||||||||||||||||||||||||
# we need to suppress the warning. | ||||||||||||||||||||||||
# | ||||||||||||||||||||||||
# pylint: disable=arguments-differ | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
class RateLimitedLogger: | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to inherit from And maybe this could be implemented as a This looks quite interesting: https://github.com/samuller/log-rate-limit, it is a filter but allows overriding on every log call too, and allows grouping messages in streams, and individual streams can be rate-limited individually. |
||||||||||||||||||||||||
"""Logger that limits the rate of logging messages. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
The first message is logged immediately. Subsequent messages are ignored until the | ||||||||||||||||||||||||
rate limit interval has elapsed. After that the next request goes through, and so on. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
This allows a new outage to be reported immediately and subsequent logs to be | ||||||||||||||||||||||||
rate-limited. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
When an outage has been resolved, the `reset()` method may be used to reset the | ||||||||||||||||||||||||
logger and the next message will get logged immediately. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def __init__( | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
logger: logging.Logger, | ||||||||||||||||||||||||
rate_limit: timedelta = DEFAULT_RATE_LIMIT, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Initialize the logger. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
logger: Logger to rate-limit. | ||||||||||||||||||||||||
rate_limit: Time interval between two log messages. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self._logger = logger | ||||||||||||||||||||||||
self._started: bool = False | ||||||||||||||||||||||||
self._last_log_time: datetime | None = None | ||||||||||||||||||||||||
self._rate_limit: timedelta = rate_limit | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def set_rate_limit(self, rate_limit: timedelta) -> None: | ||||||||||||||||||||||||
"""Set the rate limit for the logger. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
rate_limit: Time interval between two log messages. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self._rate_limit = rate_limit | ||||||||||||||||||||||||
Comment on lines
+55
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just making it public?
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
def is_limiting(self) -> bool: | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
"""Return whether rate limiting is active. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
This is true when a previous message has already been logged, and can be reset | ||||||||||||||||||||||||
by calling the `reset()` method. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
return self._started | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def reset(self) -> None: | ||||||||||||||||||||||||
"""Reset the logger to healthy state.""" | ||||||||||||||||||||||||
self._started = False | ||||||||||||||||||||||||
self._last_log_time = None | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def log( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
level: int, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log a message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
level: Log level. | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
if self._rate_limit is None: | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the check? It can't be |
||||||||||||||||||||||||
self._logger.log( | ||||||||||||||||||||||||
level, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
current_time = datetime.now() | ||||||||||||||||||||||||
if ( | ||||||||||||||||||||||||
not self._started | ||||||||||||||||||||||||
or self._last_log_time is None | ||||||||||||||||||||||||
or (current_time - self._last_log_time) >= self._rate_limit | ||||||||||||||||||||||||
): | ||||||||||||||||||||||||
self._logger.log( | ||||||||||||||||||||||||
level, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
self._last_log_time = current_time | ||||||||||||||||||||||||
self._started = True | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def info( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log an info message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.log( | ||||||||||||||||||||||||
logging.INFO, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def debug( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log a debug message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.log( | ||||||||||||||||||||||||
logging.DEBUG, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def warning( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log a warning message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.log( | ||||||||||||||||||||||||
logging.WARNING, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def critical( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log a critical message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.log( | ||||||||||||||||||||||||
logging.CRITICAL, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def error( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = None, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log an error message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.log( | ||||||||||||||||||||||||
logging.ERROR, | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def exception( # pylint: disable=too-many-arguments | ||||||||||||||||||||||||
self, | ||||||||||||||||||||||||
msg: object, | ||||||||||||||||||||||||
*args: object, | ||||||||||||||||||||||||
exc_info: _ExcInfoType = True, | ||||||||||||||||||||||||
stack_info: bool = False, | ||||||||||||||||||||||||
stacklevel: int = 1, | ||||||||||||||||||||||||
extra: Mapping[str, object] | None = None, | ||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||
"""Log an exception message. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||
msg: Log message. | ||||||||||||||||||||||||
*args: Arguments for the log message. | ||||||||||||||||||||||||
exc_info: Exception information. | ||||||||||||||||||||||||
stack_info: Stack information. | ||||||||||||||||||||||||
stacklevel: Stack level. | ||||||||||||||||||||||||
extra: Extra information. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
self.error( | ||||||||||||||||||||||||
msg, | ||||||||||||||||||||||||
*args, | ||||||||||||||||||||||||
exc_info=exc_info, | ||||||||||||||||||||||||
stack_info=stack_info, | ||||||||||||||||||||||||
stacklevel=stacklevel, | ||||||||||||||||||||||||
extra=extra, | ||||||||||||||||||||||||
) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -10,7 +10,7 @@ | |||||||||||||||||||
import math | ||||||||||||||||||||
from abc import ABC, abstractmethod | ||||||||||||||||||||
from collections.abc import Iterable | ||||||||||||||||||||
from datetime import datetime, timezone | ||||||||||||||||||||
from datetime import datetime, timedelta, timezone | ||||||||||||||||||||
from typing import Any, Generic, Self, TypeVar | ||||||||||||||||||||
|
||||||||||||||||||||
from frequenz.channels import ChannelClosedError, Receiver | ||||||||||||||||||||
|
@@ -22,6 +22,7 @@ | |||||||||||||||||||
InverterData, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
from ..._internal import _logging | ||||||||||||||||||||
from ..._internal._asyncio import AsyncConstructible | ||||||||||||||||||||
from ..._internal._constants import MAX_BATTERY_DATA_AGE_SEC | ||||||||||||||||||||
from ...microgrid import connection_manager | ||||||||||||||||||||
|
@@ -33,6 +34,11 @@ | |||||||||||||||||||
|
||||||||||||||||||||
_logger = logging.getLogger(__name__) | ||||||||||||||||||||
|
||||||||||||||||||||
_missing_data_logger = _logging.RateLimitedLogger( | ||||||||||||||||||||
_logger, | ||||||||||||||||||||
timedelta(minutes=5), | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
T = TypeVar("T", bound=ComponentData) | ||||||||||||||||||||
"""Type variable for component data.""" | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -120,6 +126,12 @@ async def fetch_next(self) -> ComponentMetricsData | None: | |||||||||||||||||||
data = await asyncio.wait_for( | ||||||||||||||||||||
self._receiver.receive(), self._max_waiting_time | ||||||||||||||||||||
) | ||||||||||||||||||||
if _missing_data_logger.is_limiting(): | ||||||||||||||||||||
_missing_data_logger.reset() | ||||||||||||||||||||
_missing_data_logger.debug( | ||||||||||||||||||||
"Component %d has started sending data.", self._component_id | ||||||||||||||||||||
) | ||||||||||||||||||||
_missing_data_logger.reset() | ||||||||||||||||||||
Comment on lines
+130
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't you just log using the regular
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
except ChannelClosedError: | ||||||||||||||||||||
_logger.exception( | ||||||||||||||||||||
|
@@ -128,7 +140,9 @@ async def fetch_next(self) -> ComponentMetricsData | None: | |||||||||||||||||||
return None | ||||||||||||||||||||
except asyncio.TimeoutError: | ||||||||||||||||||||
# Next time wait infinitely until we receive any message. | ||||||||||||||||||||
_logger.debug("Component %d stopped sending data.", self._component_id) | ||||||||||||||||||||
_missing_data_logger.debug( | ||||||||||||||||||||
"Component %d stopped sending data.", self._component_id | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rephrase because when it is repeated, one might think that it just stopped sending data now. Also since we have instances where the problem was in the data pipeline and not the component itself, maybe is more accurate to say we are not receiving data.
Suggested change
|
||||||||||||||||||||
) | ||||||||||||||||||||
return ComponentMetricsData( | ||||||||||||||||||||
self._component_id, datetime.now(tz=timezone.utc), {} | ||||||||||||||||||||
) | ||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be removed, marshmallow-code/marshmallow#2739 is fixed.