Skip to content
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

Closed
wgordon17 opened this issue Jan 31, 2025 · 2 comments
Closed

Would a PR for a "NetworkLogger" be accepted? #697

wgordon17 opened this issue Jan 31, 2025 · 2 comments

Comments

@wgordon17
Copy link

I was wondering if it's worth writing up a NetworkLogger and Factory that accepts a python socket for logging, instead of files?

@Finkregh
Copy link

Finkregh commented Feb 11, 2025

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)

@hynek
Copy link
Owner

hynek commented Feb 19, 2025

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 structlog-network or something and I'll happily help promoting it.

@hynek hynek closed this as not planned Won't fix, can't repro, duplicate, stale Feb 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants