Skip to content

Commit

Permalink
feat: syslog
Browse files Browse the repository at this point in the history
  • Loading branch information
devmaxde committed Dec 11, 2024
1 parent 61dd594 commit e317507
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/api/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Decorators
----------

.. automodule:: metricq.cli.decorator
:members: metricq_command, metricq_metric_option, metricq_server_option, metricq_token_option,
:members: metricq_command, metricq_metric_option, metricq_server_option, metricq_syslog_option, metricq_token_option,


Parameter
Expand Down
2 changes: 2 additions & 0 deletions metricq/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
metricq_command,
metricq_metric_option,
metricq_server_option,
metricq_syslog_option,
metricq_token_option,
)
from .params import (
Expand All @@ -23,5 +24,6 @@
"metricq_command",
"metricq_metric_option",
"metricq_server_option",
"metricq_syslog_option",
"metricq_token_option",
]
60 changes: 54 additions & 6 deletions metricq/cli/decorator.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
from typing import Callable, Optional, cast, TypeVar, Union, Any
from typing import Any, Callable, Optional, TypeVar, Union, cast

import click
import click_log # type: ignore
from click import option
from click import Context, option
from dotenv import find_dotenv, load_dotenv

from .. import get_logger
from .params import MetricParam, TemplateStringParam
from .syslog import SyslogFormatter, get_syslog_handler

# We do not interpolate (i.e. replace ${VAR} with corresponding environment variables).
# That is because we want to be able to interpolate ourselves for metrics and tokens
Expand All @@ -22,6 +23,41 @@
FC = TypeVar("FC", bound=Union[Callable[..., Any], click.Command])


def metricq_syslog_option() -> Callable[[FC], FC]:
"""
Exposes the -\\-syslog option as a click param.
The program will try read the 'token' from the click params.
if the token is not set, the default value of 'metricq.program' will be used.
That's why the @metricq_syslog_option should be the 2nd decorator in the chain.
It is recommended to use the :py:func:`~metricq.cli.decorator.metricq_command` decorator instead of using this
function directly.
"""

def enable_syslog(ctx: Context, param: Any | None, value: Optional[str]) -> None:
if value is not None:
logger = get_logger()
if value == "":
value = None

program_name = ctx.params.get("token", "metricq.program")

handler = get_syslog_handler(value)
handler.setFormatter(SyslogFormatter(name=program_name))
logger.addHandler(handler)

return option(
"--syslog",
help="Enable syslog logging by specifying the a Unix socket or host:port for the logger. If --syslog is set "
"but no value is specified, the default of localhost:514 will be used.",
callback=enable_syslog,
expose_value=False,
is_flag=False,
flag_value="",
)


def metricq_server_option() -> Callable[[FC], FC]:
"""
Allows the User to provide a -\\-server option. This option has no input validation and therefore can be any string.
Expand Down Expand Up @@ -144,10 +180,20 @@ def metricq_command(
- -\\-token:
The Token is used to identify each program on the metricq network. for example: sink-py-dummy
The token param can be set using the environment variable METRICQ_TOKEN or adding the --token {value} option to the cli command
The token param can be set using the environment variable METRICQ_TOKEN or adding the --token {value} option
to the cli command
- -\\-syslog:
The Syslog param is used to enable syslog. It can be used with or without parameter.
If used without parameter (for example: ``metricq-check --syslog`` ) the Syslog will default to localhost:514.
You can also specify a Unix socket (for example: /dev/log) or a custom host (for example: example.com:5114)
by adding the value to the syslog flag (for example: ``metricq-check --syslog example.com:5114``)
Full example:
``metricq-check --server amqp://localhost/ --token sink-py-dummy``
``metricq-check --server amqp://localhost/ --token sink-py-dummy --syslog``
**Example**::
Expand Down Expand Up @@ -185,8 +231,10 @@ def decorator(func: FC) -> click.Command:
log_decorator(
metricq_token_option(default_token)(
metricq_server_option()(
click.command(context_settings=context_settings, epilog=epilog)(
func
metricq_syslog_option()(
click.command(
context_settings=context_settings, epilog=epilog
)(func)
)
)
)
Expand Down
32 changes: 32 additions & 0 deletions metricq/cli/syslog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
import socket
import time
from logging.handlers import SysLogHandler


class SyslogFormatter(logging.Formatter):
def __init__(self, *args, name: str = "metricq", **kwargs): # type: ignore
super().__init__(*args, **kwargs)
self.program = name

def format(self, record: logging.LogRecord) -> str:
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
hostname = socket.gethostname()
pid = record.process
program = self.program
# Custom Formatter based on rfc3164
# Format the header as "<PRI> TIMESTAMP HOSTNAME PROGRAM[PID]: MESSAGE"
# <PRI> is already being set by the SysLogHandler, we only need to add the rest
syslog_header = f"{timestamp} {hostname} {program}[{pid}]: "
message = super().format(record)
return syslog_header + message


def get_syslog_handler(address: str | None) -> SysLogHandler:
if address is None:
return SysLogHandler()
elif ":" in address:
ip, port = address.split(":")
return SysLogHandler(address=(ip, int(port)))
else:
return SysLogHandler(address=address)

0 comments on commit e317507

Please sign in to comment.