Skip to content

Commit 459b3ed

Browse files
authored
Dispatcher log stdout (librenms#18953)
* Dispatcher service, allow specifying script output destination * Style fixes * style fix * enum cleanup * Add logger support * Fix error if socket_timeout was not included * fix incorrectly remove imports * Only -q when log_output is none * oopsie * Fix fuckup
1 parent d80c46f commit 459b3ed

File tree

5 files changed

+181
-121
lines changed

5 files changed

+181
-121
lines changed

LibreNMS/__init__.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
import json
22
import logging
33
import os
4+
import re
45
import signal
6+
import subprocess
57
import sys
68
import tempfile
79
import threading
810
import timeit
911
from collections import deque
1012
from logging.handlers import RotatingFileHandler
11-
from math import ceil
1213
from queue import Queue
1314
from time import time
1415

16+
from math import ceil
17+
1518
from .command_runner import command_runner
1619
from .queuemanager import (
17-
QueueManager,
18-
TimedQueueManager,
20+
AlertQueueManager,
1921
BillingQueueManager,
22+
DiscoveryQueueManager,
2023
PingQueueManager,
21-
ServicesQueueManager,
22-
AlertQueueManager,
2324
PollerQueueManager,
24-
DiscoveryQueueManager,
25+
ServicesQueueManager,
2526
)
26-
from .service import Service, ServiceConfig
27+
from .service import Service, ServiceConfig, LogOutput
2728

2829
# Hard limit script execution time so we don't get to "hang"
2930
DEFAULT_SCRIPT_TIMEOUT = 3600
@@ -190,13 +191,15 @@ def reset_signals():
190191
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
191192

192193

193-
def call_script(script, args=()):
194+
def call_script(script, args=(), log_dest=None):
194195
"""
195196
Run a LibreNMS script. Captures all output returns exit code.
196197
Blocks parent signals (like SIGINT and SIGTERM).
197198
Kills script if it takes too long
198199
:param script: the name of the executable relative to the base directory
199200
:param args: a tuple of arguments to send to the command
201+
:param log_dest: LogOutput enum or file path
202+
:type log_dest: str | LogOutput | None
200203
:returns the output of the command
201204
"""
202205
if script.endswith(".php"):
@@ -205,17 +208,61 @@ def call_script(script, args=()):
205208
else:
206209
base = ()
207210

211+
# preexec_fn=reset_signals ensures we don't receive signals from children (close_fds=True is default, but may become false in a future release)
212+
kwargs = {
213+
"preexec_fn": reset_signals,
214+
"close_fds": True,
215+
"start_new_session": True,
216+
"timeout": DEFAULT_SCRIPT_TIMEOUT,
217+
}
218+
219+
if isinstance(log_dest, str):
220+
kwargs["stdout"] = log_dest
221+
kwargs["stderr"] = log_dest
222+
elif isinstance(log_dest, LogOutput):
223+
if log_dest is LogOutput.FILE:
224+
raise ValueError(
225+
"For FILE mode, pass a file path string instead of LogOutput.FILE"
226+
)
227+
228+
elif log_dest is LogOutput.NONE:
229+
kwargs["stdout"] = subprocess.DEVNULL
230+
kwargs["stderr"] = subprocess.DEVNULL
231+
232+
elif log_dest is LogOutput.PASSTHROUGH:
233+
kwargs["stdout"] = sys.stdout
234+
kwargs["stderr"] = sys.stderr
235+
208236
base_dir = os.path.realpath(os.path.dirname(__file__) + "/..")
209237
cmd = base + ("{}/{}".format(base_dir, script),) + tuple(map(str, args))
210238
logger.debug("Running {}".format(cmd))
211-
# preexec_fn=reset_signals ensures we don't receive signals from children (close_fds=True is default, but may become false in a future release)
212-
return command_runner(
213-
cmd,
214-
preexec_fn=reset_signals,
215-
close_fds=True,
216-
start_new_session=True,
217-
timeout=DEFAULT_SCRIPT_TIMEOUT,
218-
)
239+
exit_code, output = command_runner(cmd, **kwargs)
240+
241+
if log_dest is LogOutput.LOGGER:
242+
if output:
243+
for line in output.splitlines():
244+
level = infer_log_level(line)
245+
logger.log(level, line)
246+
247+
return exit_code, output
248+
249+
250+
def infer_log_level(line):
251+
"""Infer log level from a log line."""
252+
upper_line = line.upper()
253+
254+
if re.match(r"^\s*(\[)?(CRITICAL|FATAL)(]|:|\s|-)", upper_line):
255+
return logging.CRITICAL
256+
elif re.match(r"^\s*(\[)?(ERROR|ERR)(]|:|\s|-)", upper_line):
257+
return logging.ERROR
258+
elif re.match(r"^\s*(\[)?(WARN|WARNING)(]|:|\s|-)", upper_line):
259+
return logging.WARNING
260+
elif re.match(r"^\s*(\[)?(INFO|INFORMATION)(]|:|\s|-)", upper_line):
261+
return logging.INFO
262+
elif re.match(r"^\s*(\[)?(DEBUG|TRACE)(]|:|\s|-)", upper_line):
263+
return logging.DEBUG
264+
265+
return logging.INFO
219266

220267

221268
class DB:
@@ -472,9 +519,10 @@ def __init__(self, namespace="lock", sentinel_kwargs=None, **redis_kwargs):
472519
self._redis = redis.Redis(**kwargs)
473520
self._redis.ping()
474521
self._namespace = namespace
522+
socket_timeout = redis_kwargs.get("socket_timeout")
475523
logger.debug(
476524
"Created redis lock manager with socket_timeout of {}s".format(
477-
redis_kwargs["socket_timeout"]
525+
socket_timeout if socket_timeout is not None else "default"
478526
)
479527
)
480528

LibreNMS/queuemanager.py

Lines changed: 45 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,6 @@ def do_work(self, run_type, group):
375375
"Error {} in {} billing:\n{}".format(exit_code, run_type, output)
376376
)
377377

378-
if self.config.log_output:
379-
with open(
380-
"{}/dispatch_billing-{}.log".format(self.config.logdir, run_type), "a"
381-
) as log_file:
382-
log_file.write(output)
383-
384378

385379
class PingQueueManager(TimedQueueManager):
386380
def __init__(self, config, lock_manager):
@@ -415,15 +409,6 @@ def do_work(self, context, group):
415409
)
416410
exit_code, output = LibreNMS.call_script("lnms", args)
417411

418-
if self.config.log_output:
419-
with open(
420-
"{}/dispatch_group_{}_ping.log".format(
421-
self.config.logdir, group
422-
),
423-
"a",
424-
) as log_file:
425-
log_file.write(output)
426-
427412
if exit_code != 0:
428413
logger.warning(
429414
"Running fast ping for {} failed with error code {}: {}".format(
@@ -461,17 +446,17 @@ def do_dispatch(self):
461446
def do_work(self, device_id, group):
462447
if self.lock(device_id, timeout=self.config.services.frequency):
463448
logger.info("Checking services on device {}".format(device_id))
464-
args = ("-d", "-h", device_id) if self.config.debug else ("-h", device_id)
465-
exit_code, output = LibreNMS.call_script("check-services.php", args)
466449

467-
if self.config.log_output:
468-
with open(
469-
"{}/dispatch_device_{}_services.log".format(
470-
self.config.logdir, device_id
471-
),
472-
"a",
473-
) as log_file:
474-
log_file.write(output)
450+
output = (
451+
"{}/dispatch_device_{}_services.log".format(
452+
self.config.logdir, device_id
453+
)
454+
if self.config.log_output == LibreNMS.LogOutput.FILE
455+
else self.config.log_output
456+
)
457+
458+
args = ("-d", "-h", device_id) if self.config.debug else ("-h", device_id)
459+
exit_code, output = LibreNMS.call_script("check-services.php", args, output)
475460

476461
if exit_code == 0:
477462
self.unlock(device_id)
@@ -511,15 +496,15 @@ def do_dispatch(self):
511496

512497
def do_work(self, device_id, group):
513498
logger.info("Checking alerts")
514-
args = ("-d", "-f") if self.config.debug else ("-f",)
515-
exit_code, output = LibreNMS.call_script("alerts.php", args)
516499

517-
if self.config.log_output:
518-
with open(
519-
"{}/dispatch_alerts.log".format(self.config.logdir),
520-
"a",
521-
) as log_file:
522-
log_file.write(output)
500+
output = (
501+
"{}/dispatch_alerts.log".format(self.config.logdir)
502+
if self.config.log_output == LibreNMS.LogOutput.FILE
503+
else self.config.log_output
504+
)
505+
506+
args = ("-d", "-f") if self.config.debug else ("-f",)
507+
exit_code, output = LibreNMS.call_script("alerts.php", args, output)
523508

524509
if exit_code != 0:
525510
if exit_code == 1:
@@ -544,21 +529,20 @@ def do_work(self, device_id, group):
544529
if self.lock(device_id, timeout=self.config.poller.frequency):
545530
logger.info("Polling device {}".format(device_id))
546531

547-
args = (
548-
("device:poll", device_id, "-vv")
549-
if self.config.debug
550-
else ("device:poll", device_id, "-q")
532+
output = (
533+
"{}/dispatch_device_{}_poller.log".format(self.config.logdir, device_id)
534+
if self.config.log_output == LibreNMS.LogOutput.FILE
535+
else self.config.log_output
551536
)
552-
exit_code, output = LibreNMS.call_script("lnms", args)
553537

554-
if self.config.log_output:
555-
with open(
556-
"{}/dispatch_device_{}_poller.log".format(
557-
self.config.logdir, device_id
558-
),
559-
"a",
560-
) as log_file:
561-
log_file.write(output)
538+
args_list = ["device:poll", device_id]
539+
if self.config.debug:
540+
args_list.append("-vv")
541+
elif self.config.log_output is LibreNMS.LogOutput.NONE:
542+
args_list.append("-q")
543+
args = tuple(args_list)
544+
545+
exit_code, output = LibreNMS.call_script("lnms", args, output)
562546

563547
if exit_code == 0:
564548
self.unlock(device_id)
@@ -613,21 +597,22 @@ def do_work(self, device_id, group):
613597
):
614598
logger.info("Discovering device {}".format(device_id))
615599

616-
args = (
617-
("device:discover", device_id, "-vv")
618-
if self.config.debug
619-
else ("device:discover", device_id, "-q")
600+
output = (
601+
"{}/dispatch_device_{}_discovery.log".format(
602+
self.config.logdir, device_id
603+
)
604+
if self.config.log_output == LibreNMS.LogOutput.FILE
605+
else self.config.log_output
620606
)
621-
exit_code, output = LibreNMS.call_script("lnms", args)
622-
623-
if self.config.log_output:
624-
with open(
625-
"{}/dispatch_device_{}_discovery.log".format(
626-
self.config.logdir, device_id
627-
),
628-
"a",
629-
) as log_file:
630-
log_file.write(output)
607+
608+
args_list = ["device:discover", device_id]
609+
if self.config.debug:
610+
args_list.append("-vv")
611+
elif self.config.log_output is LibreNMS.LogOutput.NONE:
612+
args_list.append("-q")
613+
args = tuple(args_list)
614+
615+
exit_code, output = LibreNMS.call_script("lnms", args, output)
631616

632617
if exit_code == 0:
633618
self.unlock(device_id)

LibreNMS/service.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import logging
22
import os
3-
import pymysql # pylint: disable=import-error
43
import sys
54
import threading
65
import time
76

7+
import pymysql # pylint: disable=import-error
8+
89
import LibreNMS
910
from LibreNMS.config import DBConfig
1011

@@ -15,6 +16,7 @@
1516

1617
from datetime import timedelta
1718
from datetime import datetime
19+
from enum import Enum
1820
from platform import python_version
1921
from time import sleep
2022
from socket import gethostname
@@ -38,6 +40,13 @@ class RedisConnectionError(Exception):
3840
logger = logging.getLogger(__name__)
3941

4042

43+
class LogOutput(Enum):
44+
NONE = "none"
45+
PASSTHROUGH = "passthrough"
46+
LOGGER = "logger"
47+
FILE = "file"
48+
49+
4150
class ServiceConfig(DBConfig):
4251
def __init__(self):
4352
"""
@@ -101,7 +110,7 @@ def __init__(self, workers, frequency, calculate=None):
101110
redis_sentinel_service = None
102111
redis_timeout = 60
103112

104-
log_output = False
113+
log_output = LogOutput.NONE
105114
logdir = "logs"
106115

107116
watchdog_enabled = False
@@ -246,9 +255,11 @@ def populate(self):
246255
self.redis_timeout = int(
247256
os.getenv(
248257
"REDIS_TIMEOUT",
249-
self.alerting.frequency
250-
if self.alerting.frequency != 0
251-
else self.redis_timeout,
258+
(
259+
self.alerting.frequency
260+
if self.alerting.frequency != 0
261+
else self.redis_timeout
262+
),
252263
)
253264
)
254265

@@ -513,15 +524,21 @@ def start(self):
513524
)
514525
logger.info(
515526
"Queue Workers: Discovery={} Poller={} Services={} Alerting={} Billing={} Ping={}".format(
516-
self.config.discovery.workers
517-
if self.config.discovery.enabled
518-
else "disabled",
519-
self.config.poller.workers
520-
if self.config.poller.enabled
521-
else "disabled",
522-
self.config.services.workers
523-
if self.config.services.enabled
524-
else "disabled",
527+
(
528+
self.config.discovery.workers
529+
if self.config.discovery.enabled
530+
else "disabled"
531+
),
532+
(
533+
self.config.poller.workers
534+
if self.config.poller.enabled
535+
else "disabled"
536+
),
537+
(
538+
self.config.services.workers
539+
if self.config.services.enabled
540+
else "disabled"
541+
),
525542
"enabled" if self.config.alerting.enabled else "disabled",
526543
"enabled" if self.config.billing.enabled else "disabled",
527544
"enabled" if self.config.ping.enabled else "disabled",

0 commit comments

Comments
 (0)