-
-
Notifications
You must be signed in to change notification settings - Fork 232
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
Would a PR for a "NetworkLogger" be accepted? #697
Comments
Related: https://www.elastic.co/guide/en/ecs-logging/python/master/installation.html#structlog :) Logging config in that direction: logging_config["handlers"]["socket"] = {
"level": log_level_int,
"class": "TcpPlainHandler",
"host": tcp_host,
"port": tcp_port,
"initial_backoff_seconds": tcp_initial_backoff_seconds,
"retry_scale_factor": tcp_retry_scale_factor,
"retry_max_seconds": tcp_retry_max_seconds,
"print_log_on_retry": tcp_print_log_on_retry,
"formatter": tcp_formatter,
}
logging_config["loggers"][""]["handlers"] = [
*logging_config["loggers"][""]["handlers"],
"socket",
] handler class import itertools
import logging
import sys
import time
from logging import NOTSET, LogRecord
from logging.handlers import SocketHandler
class TcpPlainHandler(SocketHandler):
"""Plain logging to a TCP destination.
Added functionality:
- Retry on connection issue with backoff
- Raise a custom exception on connection error
Example:
>>> TcpPlainHandler("localhost", 5170)
"""
def __init__(
self,
host: str,
port: int | None = None,
*,
initial_backoff_seconds: float = 1.0,
retry_scale_factor: float = 2.0,
retry_max_seconds: float = 16.0,
print_log_on_retry: bool = False,
) -> None:
"""Initialize the handler.
Args:
host(str): The host to connect to.
port(int, optional): The port to connect to.
initial_backoff_seconds(float, optional): The initial backoff time
in seconds when the initial connection fails. Defaults to 1.0.
retry_scale_factor(float, optional): The factor by which the backoff time
is multiplied with on each retry. Defaults to 2.0.
retry_max_seconds(float, optional): The maximal summarized
backoff time in seconds. Defaults to 6.0.
print_log_on_retry(bool, optional): Print a log message on each retry.
Defaults to False.
"""
# ruff: noqa: N803 # function args should be lowercase # we have to override the ones from the parent class
super().__init__(host, port)
self.initial_backoff_seconds: float = initial_backoff_seconds
self.retry_scale_factor: float = retry_scale_factor
self.retry_max_seconds: float = retry_max_seconds
self.print_log_on_retry: bool = print_log_on_retry
def createSocket(self) -> None:
"""Try to create a socket.
This is based on the stdlib function from logging.handlers.SocketHandler but
has been extended that the retry/backoff logic is actually working.
"""
# ruff: noqa: N802 # function name should be lowercase # we have to override the ones from the parent class
accumulated_sleep_time = 0.0
current_backoff_seconds = self.initial_backoff_seconds
for try_count in itertools.count(1):
try:
self.sock = self.makeSocket()
break # Exit the loop if socket creation is successful
except OSError as _e:
# Creation failed, so set the retry time and return.
accumulated_sleep_time += current_backoff_seconds
if accumulated_sleep_time > self.retry_max_seconds:
raise LogSocketConnectionError(
parent_exception=_e,
host=self.host,
port=self.port,
retry_time=accumulated_sleep_time - current_backoff_seconds,
try_count=try_count,
) from None
if self.print_log_on_retry:
print( # noqa: T201 # we can not properly log inside the logger
(
f"[WARNING] logging to {self.address} failed ({_e.errno}, "
f"{_e.strerror}); retrying in {current_backoff_seconds} s."
),
file=sys.stderr,
flush=True,
)
time.sleep(current_backoff_seconds)
current_backoff_seconds *= self.retry_scale_factor
def emit(self, record: LogRecord) -> None:
"""Emit a log record.
Required to only send the json as bytes, without any other prefix.
Source: <https://github.com/fluent/fluent-bit/discussions/5823#discussioncomment-4674134>
"""
try:
self.send((self.format(record)).encode())
except Exception: # noqa: BLE001 # handled with handleError()
self.handleError(record)
def handleError(self, record: LogRecord) -> None:
"""Raise a normal exception on error."""
if logging.raiseExceptions:
raise # noqa: PLE0704 # the exception handler is some levels above 🙄
print( # noqa: T201 # we can not properly log inside the logger
(
f"[ERROR] Failed to send log message: "
f"record: {record.msg}; args: {record.args}"
),
file=sys.stderr,
flush=True,
)
class LogSocketConnectionError(ConnectionError):
"""Log host connection error."""
def __init__(
self,
host: str,
retry_time: float,
try_count: int,
port: int | None = None,
*,
parent_exception: Exception | None = None,
) -> None:
"""Log host connection error.
Args:
host(str): The host we tried to connect to.
retry_time(float): The total time we tried to connect.
try_count(int): The number of times we tried to connect.
port(int, optional): The port we tried to connect to. Defaults to None.
parent_exception(Exception): The parent exception.
"""
self.message = (
f"Failed to connect to {host} at {port}, "
f"tried {try_count} times within {retry_time} seconds, "
f"final exception: {parent_exception}"
)
super().__init__(self.message) and python/cpython#127398 x) |
hey, that doesn't seem like something I'd like to maintain in addition to the existing pile. It would be great if y'all could team up and create |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I was wondering if it's worth writing up a NetworkLogger and Factory that accepts a python socket for logging, instead of files?
The text was updated successfully, but these errors were encountered: