8 Commits
Author SHA1 Message Date
feffe 17241e0d80 fix clearing and listing 2026-08-02 08:05:58 +03:00
feffe 239721a8f6 remove deprecated classifier 2026-08-02 07:57:09 +03:00
feffe 7ac825b59d fix syntax and license expression 2026-08-02 07:53:41 +03:00
feffe 1aff092d62 fix usage and listing 2026-08-02 07:49:16 +03:00
feffe 85c97bcf12 add cli to list/clear failures and fix if-alerted alerting 2026-08-02 07:09:30 +03:00
feffe fb33fa7596 remove timestamp, it seems it's not present for all messages? 2024-07-14 16:41:49 +02:00
feffe f759b8bb1e add example config for service specific override 2024-07-14 16:12:30 +02:00
feffe 93f5a9aaad add timestamp to mailed journal log.
Also restrict to 200 rows to prevent unreasonable large emails...
2024-07-14 16:07:11 +02:00
5 changed files with 86 additions and 7 deletions
+7
View File
@@ -29,3 +29,10 @@ alert_method=sysalert.email
#mail_to=root <root@localhost> #mail_to=root <root@localhost>
smtp_host=localhost smtp_host=localhost
# Specific services can have their own configuration
# If you have an unstable service that sometimes fails three times
# before it recovers you can override max_failures for this service
# by adding a section named after that service:
#
#[unstable.service]
#max_failures=3
+1 -2
View File
@@ -11,12 +11,11 @@ dependencies = [
] ]
description = "Generic OnFailure= and OnSuccess= handler for systemd" description = "Generic OnFailure= and OnSuccess= handler for systemd"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = "MIT"
keywords = [ "systemd" ] keywords = [ "systemd" ]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: System Administrators", "Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
"Topic :: System :: Monitoring", "Topic :: System :: Monitoring",
] ]
+15 -1
View File
@@ -2,7 +2,7 @@ import datetime
import os import os
import sqlite3 import sqlite3
def register_success(name, db): def clear_failure(name, db):
cur = db.cursor() cur = db.cursor()
cur.execute(''' cur.execute('''
DELETE FROM alert DELETE FROM alert
@@ -86,6 +86,20 @@ def get_failures(name, db):
failures.sort(key=lambda x: x['timestamp']) failures.sort(key=lambda x: x['timestamp'])
return failures return failures
def get_services(db):
cur = db.cursor()
cur.execute('''SELECT
service.name, COUNT(*)
FROM service
JOIN failure ON
failure.service=service.id
GROUP BY
service.name;
''')
ret = {name: failures for (name, failures) in cur.fetchall()}
db.commit()
return ret
def init(path): def init(path):
con = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) con = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
+5 -1
View File
@@ -50,7 +50,11 @@ def failure(name, failures, config):
reader.add_match(MONITOR_INVOCATION_ID=latest_inv_id) reader.add_match(MONITOR_INVOCATION_ID=latest_inv_id)
reader.add_disjunction() reader.add_disjunction()
reader.add_match(_SYSTEMD_INVOCATION_ID=latest_inv_id) reader.add_match(_SYSTEMD_INVOCATION_ID=latest_inv_id)
journal_txt = "\n".join([entry['MESSAGE'] for entry in reader]) journal_txt = "\n".join(
[
entry["MESSAGE"]
for entry in list(reader)[-200:]
])
if nr_failures <= 1: if nr_failures <= 1:
subject=f"{hostname} - {name}: failure" subject=f"{hostname} - {name}: failure"
+58 -3
View File
@@ -1,4 +1,4 @@
import argparse
import configparser import configparser
import datetime import datetime
import importlib import importlib
@@ -37,11 +37,12 @@ def register_exit(config, db):
if os.environ['MONITOR_SERVICE_RESULT'] == 'success': if os.environ['MONITOR_SERVICE_RESULT'] == 'success':
# exit with success status # exit with success status
failures = sysalert.db.get_failures(service_name, db) failures = sysalert.db.get_failures(service_name, db)
sysalert.db.register_success(service_name, db) sysalert.db.clear_failure(service_name, db)
try: try:
do_alert = config.getboolean(section_name, 'recovery_alert') do_alert = config.getboolean(section_name, 'recovery_alert')
except ValueError: except ValueError:
if config.get(section_name, 'recovery_alert') == 'if-alerted' and failures: if config.get(section_name, 'recovery_alert') == 'if-alerted' and \
any([f['alert_method'] for f in failures]):
do_alert = True do_alert = True
else: else:
do_alert = False do_alert = False
@@ -75,6 +76,31 @@ def register_exit(config, db):
alert.failure(service_name, failures, alert_config) alert.failure(service_name, failures, alert_config)
return 0 return 0
def cli_args():
parser = argparse.ArgumentParser(
description='Manage active sysalerts',
epilog='''Note that when called from systemd this argument parser
is not used. Use the provided sysalert-* services and see
documentation for details.''')
parser.add_argument('--service', '-s',
help='Only operate on specific service')
subparsers = parser.add_subparsers()
parser_list = subparsers.add_parser('list')
parser_list.set_defaults(cmd='list')
parser_clear = subparsers.add_parser('clear')
parser_clear.set_defaults(cmd='clear')
parser_clear.add_argument('--force', '-f',
action='store_true',
help='''Force clear for all services if service is
not specified''')
args = parser.parse_args()
if 'cmd' not in args:
parser.print_help()
sys.exit(1)
return args
def cli(): def cli():
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -85,9 +111,38 @@ def cli():
if _test_env(): if _test_env():
# invoked by systemd # invoked by systemd
ret = register_exit(config, db) ret = register_exit(config, db)
else:
args = cli_args()
if args.service and not args.service.endswith('.service'):
args.service = f'{args.service}.service'
if args.cmd == 'list':
cli_list(args, db)
elif args.cmd == 'clear':
cli_clear(args, db)
sysalert.db.close(db) sysalert.db.close(db)
return ret return ret
def cli_list(args, db):
for service, failures in sysalert.db.get_services(db).items():
if not args.service or (args.service and args.service == service):
print(f'{service}: {failures} failures')
return 0
def cli_clear(args, db):
if not args.service and not args.force:
print(f'No service specified, use "--force" to clear all services',
file=sys.stderr)
return 1
if args.service:
sysalert.db.clear_failure(args.service, db)
print(f'Cleared failures for {args.service}')
else:
for service, failures in sysalert.db.get_services(db).items():
sysalert.db.clear_failure(service, db)
print(f'Cleared all failures')
return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(cli()) sys.exit(cli())