omdnotificationforwarder 4.7__tar.gz → 4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/.gitignore +0 -1
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/PKG-INFO +1 -1
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/bin/notificationforwarder +49 -3
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/pyproject.toml +1 -1
- omdnotificationforwarder-4.8/src/notificationforwarder/spool_dump.py +259 -0
- omdnotificationforwarder-4.8/tests/test_spool_dump.py +669 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/README.md +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/baseclass.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/component_loader.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/email/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/email/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/example/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/example/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/json/__init__.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/json/logger.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/naemonlog/reporter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/nostr/__init__.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/nostr/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/nostr/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/oauth2webhook/__init__.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/oauth2webhook/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/rabbitmq/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/rabbitmq/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/runtime_config.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/runtime_flow.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/spool.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/syslog/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/syslog/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/telegram/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/text/__init__.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/text/logger.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/util.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/src/notificationforwarder/webhook/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/TESTS.md +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/lib/python/notificationforwarder/split1/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/lib/python/notificationforwarder/split2/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/bayern/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/datadup/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/datapost/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/discard/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split1/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split4/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/split4/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/forwarder.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/reporter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/pythonpath/local/lib/python/notificationforwarder/vong/formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_alertmanager.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_builtin_plugins_focus.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_classes.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_contracts_formatters.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_contracts_forwarders.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_contracts_reporters.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_delivery_resilience.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_discard.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_formatter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_integration_cli.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_lint_gate.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_loading_resolution.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_logger.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_loggers.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_nostr.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_oauth2webhook.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_orchestration_flow.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_package.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_paths.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_reporter.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_resilience_spool.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_runtime_foundation.py +0 -0
- {omdnotificationforwarder-4.7 → omdnotificationforwarder-4.8}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: omdnotificationforwarder
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.8
|
|
4
4
|
Summary: A framework for notification scripts for OMD
|
|
5
5
|
Project-URL: Homepage, https://github.com/lausser/noteventificationforhandlerwarder
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/lausser/noteventificationforhandlerwarder/issues
|
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
#import logging
|
|
9
9
|
from notificationforwarder.util import setup_logging
|
|
10
10
|
from notificationforwarder import baseclass
|
|
11
|
+
from notificationforwarder import spool_dump
|
|
11
12
|
import traceback
|
|
12
13
|
|
|
13
14
|
|
|
@@ -32,8 +33,9 @@ if __name__ == '__main__':
|
|
|
32
33
|
This can be any backend you want, provided
|
|
33
34
|
there is a Python module forwarder.py in the folder
|
|
34
35
|
notificationforwarder/<forwarder> somewhere in your PYTHONPATH.
|
|
35
|
-
Examples are victorops, syslog, servicenow, rabbitmq,...
|
|
36
|
-
|
|
36
|
+
Examples are victorops, syslog, servicenow, rabbitmq,...
|
|
37
|
+
Defaults to syslog when not in dump mode. Required with --dump-spool.''',
|
|
38
|
+
default=argparse.SUPPRESS)
|
|
37
39
|
parser.add_argument('--forwardertag', '--receivertag', action='store',
|
|
38
40
|
dest="forwardertag",
|
|
39
41
|
help='A distinctive string which is added to the logfile',
|
|
@@ -77,13 +79,57 @@ Example for an HTTP-based reporter:
|
|
|
77
79
|
dest="logger",
|
|
78
80
|
help='Logger type: text (default) or json, or custom logger module',
|
|
79
81
|
default='text')
|
|
82
|
+
parser.add_argument('--dump-spool', action='store_true',
|
|
83
|
+
dest="dump_spool",
|
|
84
|
+
help='''Dump the spooled events of the selected forwarder instance and exit.
|
|
85
|
+
The spool is read only: nothing is forwarded, retried, or modified.
|
|
86
|
+
--forwarder is required in this mode; --forwardertag selects the tagged instance.''',
|
|
87
|
+
default=False)
|
|
88
|
+
parser.add_argument('--order', action='store',
|
|
89
|
+
dest="order",
|
|
90
|
+
choices=("oldest", "newest"),
|
|
91
|
+
help='Spool dump order: oldest (default) or newest',
|
|
92
|
+
default="oldest")
|
|
93
|
+
parser.add_argument('--limit', action='store',
|
|
94
|
+
dest="limit",
|
|
95
|
+
help='Spool dump: maximum number of records to display (default: all)',
|
|
96
|
+
default=None)
|
|
97
|
+
parser.add_argument('--dump-format', action='store',
|
|
98
|
+
dest="dump_format",
|
|
99
|
+
choices=("text", "json"),
|
|
100
|
+
help='Spool dump output format: text (default) or json',
|
|
101
|
+
default="text")
|
|
80
102
|
parser.add_argument('--version', action='version',
|
|
81
|
-
version=f'%(prog)s 4.
|
|
103
|
+
version=f'%(prog)s 4.8')
|
|
82
104
|
|
|
83
105
|
args = parser.parse_args()
|
|
106
|
+
if not hasattr(args, 'forwarder'):
|
|
107
|
+
# The default applies only outside dump mode; dump mode needs the
|
|
108
|
+
# instance named explicitly because the spool is one file per
|
|
109
|
+
# (forwarder, tag) pair.
|
|
110
|
+
if args.dump_spool:
|
|
111
|
+
parser.error("--dump-spool requires --forwarder (or --receiver)")
|
|
112
|
+
args.forwarder = "syslog"
|
|
84
113
|
if not hasattr(args, 'formatter'):
|
|
85
114
|
args.formatter = args.forwarder
|
|
86
115
|
|
|
116
|
+
if args.dump_spool:
|
|
117
|
+
try:
|
|
118
|
+
limit = int(args.limit) if args.limit is not None else None
|
|
119
|
+
except ValueError:
|
|
120
|
+
# one usage line naming the value, like the core's validation
|
|
121
|
+
print("error: --limit must be an integer, got '{}'".format(args.limit),
|
|
122
|
+
file=sys.stderr)
|
|
123
|
+
sys.exit(2)
|
|
124
|
+
status = spool_dump.dump_spool(
|
|
125
|
+
args.forwarder,
|
|
126
|
+
args.forwardertag,
|
|
127
|
+
args.order,
|
|
128
|
+
limit,
|
|
129
|
+
args.dump_format,
|
|
130
|
+
)
|
|
131
|
+
sys.exit(status)
|
|
132
|
+
|
|
87
133
|
try:
|
|
88
134
|
forwarder = baseclass.new(args.forwarder, args.forwardertag, args.formatter, args.verbose, args.debug, args.forwarderopt, args.reporter, args.reporteropt, args.logger)
|
|
89
135
|
except Exception as a:
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Read-only inspection of a notificationforwarder spool database.
|
|
2
|
+
|
|
3
|
+
This is a pure read path. It resolves the spool database and table from the
|
|
4
|
+
forwarder name and an optional tag, opens them read-only, and prints the spooled
|
|
5
|
+
records. It never loads a forwarder, formatter, or reporter module, never takes
|
|
6
|
+
the flush lock, and never modifies the spool.
|
|
7
|
+
|
|
8
|
+
The failure reason for a spooled event is not stored in the database, so this
|
|
9
|
+
module does not derive or display one; the spool log remains the source,
|
|
10
|
+
correlated by the record number.
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sqlite3
|
|
16
|
+
import sys
|
|
17
|
+
from urllib.parse import quote
|
|
18
|
+
|
|
19
|
+
# Cap on how long the dump waits on a concurrent writer before failing.
|
|
20
|
+
LOCK_TIMEOUT_MS = 1000
|
|
21
|
+
# How many characters of a corrupt payload to show so the operator has a breadcrumb.
|
|
22
|
+
RAW_PREFIX_LENGTH = 120
|
|
23
|
+
# Upper bound for --limit; a larger value is a usage error.
|
|
24
|
+
LIMIT_MAX = 9223372036854775807
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _forwarder_name(forwarder, tag):
|
|
28
|
+
return forwarder + ("_" + tag if tag else "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _use_utf8(stream):
|
|
32
|
+
# stdout and stderr carry valid UTF-8 independent of the process locale
|
|
33
|
+
# (e.g. LC_ALL=C with locale coercion disabled).
|
|
34
|
+
try:
|
|
35
|
+
stream.reconfigure(encoding="utf-8")
|
|
36
|
+
except (AttributeError, ValueError, OSError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _spool_paths(forwarder, tag):
|
|
41
|
+
forwarder_name = _forwarder_name(forwarder, tag)
|
|
42
|
+
omd_root = os.environ.get("OMD_ROOT", "")
|
|
43
|
+
# Absolute so the read-only URI connect below is valid and the reported
|
|
44
|
+
# path is unambiguous.
|
|
45
|
+
db_file = os.path.abspath(omd_root + "/var/tmp/notificationforwarder_"
|
|
46
|
+
+ forwarder_name + "_notifications.db")
|
|
47
|
+
table = "events_" + forwarder_name
|
|
48
|
+
return db_file, table
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _decode_payload(text):
|
|
52
|
+
"""Return (decodable, value, prefix).
|
|
53
|
+
|
|
54
|
+
On success, (True, decoded_value, None). On failure, (False, None, prefix)
|
|
55
|
+
where prefix is the first RAW_PREFIX_LENGTH characters of the stored text.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
return True, json.loads(text), None
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
if isinstance(text, str):
|
|
61
|
+
prefix = text[:RAW_PREFIX_LENGTH]
|
|
62
|
+
else:
|
|
63
|
+
prefix = str(text)[:RAW_PREFIX_LENGTH]
|
|
64
|
+
return False, None, prefix
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _spool_time(value):
|
|
68
|
+
# Show the stored timestamp verbatim (UTC YYYY-MM-DD HH:MM:SS). No conversion.
|
|
69
|
+
return str(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _header(err, order, db_file, table, total, shown):
|
|
73
|
+
# Header to stderr before the first record so an interrupted dump still
|
|
74
|
+
# shows the counts, and the spool time is labeled UTC.
|
|
75
|
+
err.write("spool: {}\n".format(db_file))
|
|
76
|
+
err.write("table: {}\n".format(table))
|
|
77
|
+
err.write("order: {} (spool time UTC)\n".format(order))
|
|
78
|
+
err.write("total: {}\n".format(total))
|
|
79
|
+
err.write("shown: {}\n".format(shown))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_empty(err, out, fmt, db_file, table, order):
|
|
83
|
+
_header(err, order, db_file, table, 0, 0)
|
|
84
|
+
err.write("spool empty\n")
|
|
85
|
+
if fmt == "json":
|
|
86
|
+
out.write("[]\n")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# U+0085 (NEL), U+2028, U+2029 are line breaks that json.dumps with
|
|
90
|
+
# ensure_ascii=False leaves raw; escape them so one record stays one
|
|
91
|
+
# physical line and stdout stays valid UTF-8 independent of the locale.
|
|
92
|
+
_LINE_BREAKS = re.compile("[\u0085\u2028\u2029]")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _escape_line_breaks(text):
|
|
96
|
+
return _LINE_BREAKS.sub(lambda match: "\\u{:04x}".format(ord(match.group(0))), text)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Lone surrogates (U+D800-U+DFFF) cannot be encoded in UTF-8; JSON carries
|
|
100
|
+
# them only as \u escapes, so escape exactly those code points and leave
|
|
101
|
+
# every other character (including all other non-ASCII) as itself.
|
|
102
|
+
_SURROGATES = re.compile("[\ud800-\udfff]")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _escape_surrogates(text):
|
|
106
|
+
return _SURROGATES.sub(lambda match: "\\u{:04x}".format(ord(match.group(0))), text)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _encode_utf8(value):
|
|
110
|
+
"""Return a UTF-8-encodable JSON string for value.
|
|
111
|
+
|
|
112
|
+
ensure_ascii=False keeps non-ASCII readable; line breaks and lone
|
|
113
|
+
surrogates are escaped so the result stays one line and valid UTF-8.
|
|
114
|
+
Returns None when the value cannot be JSON encoded at all.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
text = _escape_line_breaks(json.dumps(value, ensure_ascii=False))
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
return None
|
|
120
|
+
text = _escape_surrogates(text)
|
|
121
|
+
try:
|
|
122
|
+
text.encode("utf-8")
|
|
123
|
+
except UnicodeEncodeError:
|
|
124
|
+
return None
|
|
125
|
+
return text
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _render(rows, total, order, fmt, db_file, table, out, err):
|
|
129
|
+
shown = len(rows)
|
|
130
|
+
_header(err, order, db_file, table, total, shown)
|
|
131
|
+
|
|
132
|
+
if fmt == "json":
|
|
133
|
+
elements = []
|
|
134
|
+
for row_id, ts, payload_text in rows:
|
|
135
|
+
decodable, value, prefix = _decode_payload(payload_text)
|
|
136
|
+
record = {"id": row_id, "spooled_at": _spool_time(ts)}
|
|
137
|
+
if decodable:
|
|
138
|
+
encoded = _encode_utf8(value)
|
|
139
|
+
if encoded is None:
|
|
140
|
+
record["payload"] = None
|
|
141
|
+
record["undecodable"] = True
|
|
142
|
+
record["raw_prefix"] = prefix
|
|
143
|
+
else:
|
|
144
|
+
record["payload"] = value
|
|
145
|
+
else:
|
|
146
|
+
record["payload"] = None
|
|
147
|
+
record["undecodable"] = True
|
|
148
|
+
record["raw_prefix"] = prefix
|
|
149
|
+
elements.append(record)
|
|
150
|
+
# ensure_ascii=False keeps non-ASCII readable; line breaks and lone
|
|
151
|
+
# surrogates are escaped so the array stays valid UTF-8.
|
|
152
|
+
body = _escape_surrogates(_escape_line_breaks(json.dumps(elements, ensure_ascii=False)))
|
|
153
|
+
try:
|
|
154
|
+
body.encode("utf-8")
|
|
155
|
+
except UnicodeEncodeError:
|
|
156
|
+
# defensive; escaping the surrogates should already be enough
|
|
157
|
+
body = _escape_line_breaks(json.dumps(elements, ensure_ascii=True))
|
|
158
|
+
out.write(body)
|
|
159
|
+
out.write("\n")
|
|
160
|
+
else:
|
|
161
|
+
for row_id, ts, payload_text in rows:
|
|
162
|
+
decodable, value, prefix = _decode_payload(payload_text)
|
|
163
|
+
body = None
|
|
164
|
+
if decodable:
|
|
165
|
+
body = _encode_utf8(value)
|
|
166
|
+
if body is None:
|
|
167
|
+
# Undecodable or unencodable: marker plus raw prefix, escaped
|
|
168
|
+
# so the record stays one physical line and valid UTF-8.
|
|
169
|
+
prefix_body = _encode_utf8(prefix)
|
|
170
|
+
if prefix_body is None:
|
|
171
|
+
prefix_body = json.dumps(prefix, ensure_ascii=True)
|
|
172
|
+
body = "[undecodable payload] {}".format(prefix_body)
|
|
173
|
+
out.write("#{} {} {}\n".format(row_id, _spool_time(ts), body))
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def dump_spool(forwarder, tag=None, order="oldest", limit=None, fmt="text", out=None, err=None):
|
|
178
|
+
"""Dump the spooled events of one forwarder instance. Returns a process exit code."""
|
|
179
|
+
out = sys.stdout if out is None else out
|
|
180
|
+
err = sys.stderr if err is None else err
|
|
181
|
+
_use_utf8(out)
|
|
182
|
+
_use_utf8(err)
|
|
183
|
+
|
|
184
|
+
# Argument validation: a violation is a usage error (non-zero, no record data).
|
|
185
|
+
if not forwarder:
|
|
186
|
+
err.write("error: --dump-spool requires --forwarder (or --receiver); "
|
|
187
|
+
"the syslog default is not applied\n")
|
|
188
|
+
return 2
|
|
189
|
+
if order not in ("oldest", "newest"):
|
|
190
|
+
err.write("error: --order must be 'oldest' or 'newest'\n")
|
|
191
|
+
return 2
|
|
192
|
+
if fmt not in ("text", "json"):
|
|
193
|
+
err.write("error: --dump-format must be 'text' or 'json'\n")
|
|
194
|
+
return 2
|
|
195
|
+
if limit is not None and (not isinstance(limit, int) or isinstance(limit, bool)
|
|
196
|
+
or limit < 1 or limit > LIMIT_MAX):
|
|
197
|
+
err.write("error: --limit must be an integer between 1 and {} (got {})\n".format(
|
|
198
|
+
LIMIT_MAX, limit))
|
|
199
|
+
return 2
|
|
200
|
+
|
|
201
|
+
omd_root = os.environ.get("OMD_ROOT", "")
|
|
202
|
+
if not omd_root:
|
|
203
|
+
err.write("error: OMD_ROOT is not set; cannot resolve the spool database\n")
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
db_file, table = _spool_paths(forwarder, tag)
|
|
207
|
+
|
|
208
|
+
# A missing spool file is a healthy empty spool, not an error.
|
|
209
|
+
if not os.path.exists(db_file):
|
|
210
|
+
_write_empty(err, out, fmt, db_file, table, order)
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
direction = "ASC" if order == "oldest" else "DESC"
|
|
214
|
+
|
|
215
|
+
# Percent-encode the path: an unescaped '#' or '?' in a file or
|
|
216
|
+
# directory name would become a URI fragment/query and drop mode=ro.
|
|
217
|
+
try:
|
|
218
|
+
connection = sqlite3.connect("file:{}?mode=ro".format(quote(db_file, safe="/")), uri=True,
|
|
219
|
+
timeout=LOCK_TIMEOUT_MS / 1000.0, isolation_level=None)
|
|
220
|
+
except sqlite3.Error as exc:
|
|
221
|
+
err.write("error: could not open spool database {}: {}\n".format(db_file, exc))
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
total = 0
|
|
225
|
+
rows = []
|
|
226
|
+
try:
|
|
227
|
+
# One deferred read transaction holds the snapshot across COUNT and
|
|
228
|
+
# SELECT so total and the listed rows describe the same data.
|
|
229
|
+
connection.execute("BEGIN")
|
|
230
|
+
total = connection.execute("SELECT COUNT(*) FROM " + table).fetchone()[0]
|
|
231
|
+
sql = "SELECT id, timestamp, payload FROM {} ORDER BY timestamp {}, id {}".format(
|
|
232
|
+
table, direction, direction)
|
|
233
|
+
params = []
|
|
234
|
+
if limit is not None:
|
|
235
|
+
sql += " LIMIT ?"
|
|
236
|
+
params.append(limit)
|
|
237
|
+
rows = connection.execute(sql, params).fetchall()
|
|
238
|
+
connection.execute("COMMIT")
|
|
239
|
+
except sqlite3.OperationalError as exc:
|
|
240
|
+
message = str(exc).lower()
|
|
241
|
+
if "no such table" in message:
|
|
242
|
+
err.write("error: spool database {} exists but table {} is missing\n".format(db_file, table))
|
|
243
|
+
elif "locked" in message:
|
|
244
|
+
err.write("error: spool database {} stayed locked beyond {} ms: {}\n".format(
|
|
245
|
+
db_file, LOCK_TIMEOUT_MS, exc))
|
|
246
|
+
else:
|
|
247
|
+
err.write("error: could not read spool database {}: {}\n".format(db_file, exc))
|
|
248
|
+
return 1
|
|
249
|
+
except sqlite3.Error as exc:
|
|
250
|
+
err.write("error: could not read spool database {}: {}\n".format(db_file, exc))
|
|
251
|
+
return 1
|
|
252
|
+
finally:
|
|
253
|
+
connection.close()
|
|
254
|
+
|
|
255
|
+
if total == 0:
|
|
256
|
+
_write_empty(err, out, fmt, db_file, table, order)
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
return _render(rows, total, order, fmt, db_file, table, out, err)
|