omdnotificationforwarder 2.8__tar.gz → 3.0__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-2.8 → omdnotificationforwarder-3.0}/PKG-INFO +92 -3
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/README.md +90 -1
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/bin/notificationforwarder +11 -3
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/pyproject.toml +2 -2
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/baseclass.py +205 -38
- omdnotificationforwarder-3.0/src/notificationforwarder/json/__init__.py +1 -0
- omdnotificationforwarder-3.0/src/notificationforwarder/json/logger.py +230 -0
- omdnotificationforwarder-3.0/src/notificationforwarder/text/__init__.py +1 -0
- omdnotificationforwarder-3.0/src/notificationforwarder/text/logger.py +163 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/webhook/forwarder.py +28 -7
- omdnotificationforwarder-3.0/tests/pythonpath/local/lib/python/notificationforwarder/datadup/formatter.py +27 -0
- omdnotificationforwarder-3.0/tests/pythonpath/local/lib/python/notificationforwarder/datapost/formatter.py +13 -0
- omdnotificationforwarder-3.0/tests/test_logger.py +239 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_webhook.py +147 -12
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/.gitignore +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/email/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/email/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/example/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/example/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/naemonlog/reporter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/rabbitmq/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/rabbitmq/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/syslog/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/syslog/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/telegram/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/lib/python/notificationforwarder/split1/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/lib/python/notificationforwarder/split2/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/bayern/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/discard/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split1/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split4/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/split4/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/forwarder.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/reporter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/pythonpath/local/lib/python/notificationforwarder/vong/formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_alertmanager.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_classes.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_discard.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_formatter.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_package.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_paths.py +0 -0
- {omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/tests/test_reporter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: omdnotificationforwarder
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0
|
|
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,7 +8,7 @@ Author-email: Gerhard Lausser <lausser@yahoo.com>
|
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Requires-Python: >=3.6
|
|
11
|
+
Requires-Python: >=3.6.1
|
|
12
12
|
Requires-Dist: coshsh
|
|
13
13
|
Requires-Dist: jinja2
|
|
14
14
|
Provides-Extra: test
|
|
@@ -260,9 +260,98 @@ There is also a SyslogFormatter, which creates the log line as:
|
|
|
260
260
|
|
|
261
261
|
If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
|
|
262
262
|
|
|
263
|
+
## Loggers
|
|
264
|
+
|
|
265
|
+
The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
|
|
266
|
+
|
|
267
|
+
### Why JSON Logging?
|
|
268
|
+
|
|
269
|
+
In enterprise environments, the gateway from monitoring systems to incident management platforms like Remedy, ServiceNow, or other ITSM tools is crucial for operational reliability. For comprehensive monitoring and troubleshooting of this critical path, logs need to be ingested into log aggregation systems like Splunk for analysis, alerting, and correlation.
|
|
270
|
+
|
|
271
|
+
The JSON logger provides structured logging optimized for ingestion into Splunk and other log management systems. It outputs single-line JSON with:
|
|
272
|
+
- Splunk-friendly underscore field naming (e.g., `event_host_name`, `event_service_name`)
|
|
273
|
+
- Complete event details including state, output, and summary
|
|
274
|
+
- Operational metrics (queue length, spool counts, retry attempts)
|
|
275
|
+
- Structured exception traces
|
|
276
|
+
- Timezone-aware timestamps
|
|
277
|
+
|
|
278
|
+
### Usage
|
|
279
|
+
|
|
280
|
+
**Default (text logging):**
|
|
281
|
+
```bash
|
|
282
|
+
$USER1$/notificationforwarder \
|
|
283
|
+
--forwarder webhook \
|
|
284
|
+
--forwarderopt url=https://api.example.com/tickets \
|
|
285
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
286
|
+
--eventopt SERVICESTATE='$SERVICESTATE$'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**JSON logging for Splunk ingestion:**
|
|
290
|
+
```bash
|
|
291
|
+
$USER1$/notificationforwarder \
|
|
292
|
+
--forwarder webhook \
|
|
293
|
+
--forwarderopt url=https://api.example.com/tickets \
|
|
294
|
+
--logger json \
|
|
295
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
296
|
+
--eventopt SERVICESTATE='$SERVICESTATE$'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Custom logger:**
|
|
300
|
+
```bash
|
|
301
|
+
$USER1$/notificationforwarder \
|
|
302
|
+
--forwarder webhook \
|
|
303
|
+
--logger mycustomlogger \
|
|
304
|
+
--eventopt HOSTNAME='$HOSTNAME$'
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Example Log Output
|
|
308
|
+
|
|
309
|
+
**Text format (default):**
|
|
310
|
+
```
|
|
311
|
+
2025-11-13 17:00:57,987 3468977 - INFO - forwarded dbserver02.example.com/MySQL: WARNING - Slow queries
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**JSON format:**
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"timestamp": "2025-11-13T17:00:57.987487+01:00",
|
|
318
|
+
"host_name": "oasch.example.com",
|
|
319
|
+
"version": "2.9",
|
|
320
|
+
"level": "INFO",
|
|
321
|
+
"logger": "notificationforwarder_webhook",
|
|
322
|
+
"omd_site": "demo_site",
|
|
323
|
+
"event_host_name": "dbserver02.example.com",
|
|
324
|
+
"event_service_name": "MySQL",
|
|
325
|
+
"event_state": "WARNING",
|
|
326
|
+
"event_notification_type": "PROBLEM",
|
|
327
|
+
"event_service_output": "MySQL WARNING - Slow queries detected",
|
|
328
|
+
"event_summary": "dbserver02.example.com/MySQL: WARNING - Slow queries",
|
|
329
|
+
"msg": {
|
|
330
|
+
"message": "forwarded",
|
|
331
|
+
"status": "success"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Custom Loggers
|
|
337
|
+
|
|
338
|
+
You can create custom loggers by:
|
|
339
|
+
1. Creating `~/local/lib/python/notificationforwarder/mylogger/logger.py`
|
|
340
|
+
2. Inheriting from `NotificationLogger` base class
|
|
341
|
+
3. Implementing the `log(level, message, context)` method
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from notificationforwarder.baseclass import NotificationLogger
|
|
345
|
+
|
|
346
|
+
class MyloggerLogger(NotificationLogger):
|
|
347
|
+
def log(self, level, message, context=None):
|
|
348
|
+
# Custom logging implementation
|
|
349
|
+
pass
|
|
350
|
+
```
|
|
351
|
+
|
|
263
352
|
## Reporters
|
|
264
353
|
|
|
265
|
-
Like *forwarder* and *formatter*, a *reporter* is an instance of a *NotificationReporter* class defined in a file named *reporter.py*. There is one class coming with notificationforwarder, the *NaemonlogReporter*. It's purpose it to write a message to the Naemon logfile. When notificationforwarder is run as a standalone script (and not triggered as a notificationhandler by Naemon), the *NaemonlogReporter* can nevertheless leave a line in the Naemon log.
|
|
354
|
+
Like *forwarder* and *formatter*, a *reporter* is an instance of a *NotificationReporter* class defined in a file named *reporter.py*. There is one class coming with notificationforwarder, the *NaemonlogReporter*. It's purpose it to write a message to the Naemon logfile. When notificationforwarder is run as a standalone script (and not triggered as a notificationhandler by Naemon), the *NaemonlogReporter* can nevertheless leave a line in the Naemon log.
|
|
266
355
|
Or you can write an extra log showing success or failure of the notification delivery.
|
|
267
356
|
|
|
268
357
|
```
|
|
@@ -240,9 +240,98 @@ There is also a SyslogFormatter, which creates the log line as:
|
|
|
240
240
|
|
|
241
241
|
If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
|
|
242
242
|
|
|
243
|
+
## Loggers
|
|
244
|
+
|
|
245
|
+
The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
|
|
246
|
+
|
|
247
|
+
### Why JSON Logging?
|
|
248
|
+
|
|
249
|
+
In enterprise environments, the gateway from monitoring systems to incident management platforms like Remedy, ServiceNow, or other ITSM tools is crucial for operational reliability. For comprehensive monitoring and troubleshooting of this critical path, logs need to be ingested into log aggregation systems like Splunk for analysis, alerting, and correlation.
|
|
250
|
+
|
|
251
|
+
The JSON logger provides structured logging optimized for ingestion into Splunk and other log management systems. It outputs single-line JSON with:
|
|
252
|
+
- Splunk-friendly underscore field naming (e.g., `event_host_name`, `event_service_name`)
|
|
253
|
+
- Complete event details including state, output, and summary
|
|
254
|
+
- Operational metrics (queue length, spool counts, retry attempts)
|
|
255
|
+
- Structured exception traces
|
|
256
|
+
- Timezone-aware timestamps
|
|
257
|
+
|
|
258
|
+
### Usage
|
|
259
|
+
|
|
260
|
+
**Default (text logging):**
|
|
261
|
+
```bash
|
|
262
|
+
$USER1$/notificationforwarder \
|
|
263
|
+
--forwarder webhook \
|
|
264
|
+
--forwarderopt url=https://api.example.com/tickets \
|
|
265
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
266
|
+
--eventopt SERVICESTATE='$SERVICESTATE$'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**JSON logging for Splunk ingestion:**
|
|
270
|
+
```bash
|
|
271
|
+
$USER1$/notificationforwarder \
|
|
272
|
+
--forwarder webhook \
|
|
273
|
+
--forwarderopt url=https://api.example.com/tickets \
|
|
274
|
+
--logger json \
|
|
275
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
276
|
+
--eventopt SERVICESTATE='$SERVICESTATE$'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Custom logger:**
|
|
280
|
+
```bash
|
|
281
|
+
$USER1$/notificationforwarder \
|
|
282
|
+
--forwarder webhook \
|
|
283
|
+
--logger mycustomlogger \
|
|
284
|
+
--eventopt HOSTNAME='$HOSTNAME$'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Example Log Output
|
|
288
|
+
|
|
289
|
+
**Text format (default):**
|
|
290
|
+
```
|
|
291
|
+
2025-11-13 17:00:57,987 3468977 - INFO - forwarded dbserver02.example.com/MySQL: WARNING - Slow queries
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**JSON format:**
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"timestamp": "2025-11-13T17:00:57.987487+01:00",
|
|
298
|
+
"host_name": "oasch.example.com",
|
|
299
|
+
"version": "2.9",
|
|
300
|
+
"level": "INFO",
|
|
301
|
+
"logger": "notificationforwarder_webhook",
|
|
302
|
+
"omd_site": "demo_site",
|
|
303
|
+
"event_host_name": "dbserver02.example.com",
|
|
304
|
+
"event_service_name": "MySQL",
|
|
305
|
+
"event_state": "WARNING",
|
|
306
|
+
"event_notification_type": "PROBLEM",
|
|
307
|
+
"event_service_output": "MySQL WARNING - Slow queries detected",
|
|
308
|
+
"event_summary": "dbserver02.example.com/MySQL: WARNING - Slow queries",
|
|
309
|
+
"msg": {
|
|
310
|
+
"message": "forwarded",
|
|
311
|
+
"status": "success"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Custom Loggers
|
|
317
|
+
|
|
318
|
+
You can create custom loggers by:
|
|
319
|
+
1. Creating `~/local/lib/python/notificationforwarder/mylogger/logger.py`
|
|
320
|
+
2. Inheriting from `NotificationLogger` base class
|
|
321
|
+
3. Implementing the `log(level, message, context)` method
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from notificationforwarder.baseclass import NotificationLogger
|
|
325
|
+
|
|
326
|
+
class MyloggerLogger(NotificationLogger):
|
|
327
|
+
def log(self, level, message, context=None):
|
|
328
|
+
# Custom logging implementation
|
|
329
|
+
pass
|
|
330
|
+
```
|
|
331
|
+
|
|
243
332
|
## Reporters
|
|
244
333
|
|
|
245
|
-
Like *forwarder* and *formatter*, a *reporter* is an instance of a *NotificationReporter* class defined in a file named *reporter.py*. There is one class coming with notificationforwarder, the *NaemonlogReporter*. It's purpose it to write a message to the Naemon logfile. When notificationforwarder is run as a standalone script (and not triggered as a notificationhandler by Naemon), the *NaemonlogReporter* can nevertheless leave a line in the Naemon log.
|
|
334
|
+
Like *forwarder* and *formatter*, a *reporter* is an instance of a *NotificationReporter* class defined in a file named *reporter.py*. There is one class coming with notificationforwarder, the *NaemonlogReporter*. It's purpose it to write a message to the Naemon logfile. When notificationforwarder is run as a standalone script (and not triggered as a notificationhandler by Naemon), the *NaemonlogReporter* can nevertheless leave a line in the Naemon log.
|
|
246
335
|
Or you can write an extra log showing success or failure of the notification delivery.
|
|
247
336
|
|
|
248
337
|
```
|
|
@@ -73,15 +73,19 @@ Example for an HTTP-based reporter:
|
|
|
73
73
|
dest="debug",
|
|
74
74
|
help='Increase the log level to DEBUG',
|
|
75
75
|
default=False)
|
|
76
|
+
parser.add_argument('--logger', action='store',
|
|
77
|
+
dest="logger",
|
|
78
|
+
help='Logger type: text (default) or json, or custom logger module',
|
|
79
|
+
default='text')
|
|
76
80
|
parser.add_argument('--version', action='version',
|
|
77
|
-
version=f'%(prog)s
|
|
81
|
+
version=f'%(prog)s 3.0')
|
|
78
82
|
|
|
79
83
|
args = parser.parse_args()
|
|
80
84
|
if not hasattr(args, 'formatter'):
|
|
81
85
|
args.formatter = args.forwarder
|
|
82
86
|
|
|
83
87
|
try:
|
|
84
|
-
forwarder = baseclass.new(args.forwarder, args.forwardertag, args.formatter, args.verbose, args.debug, args.forwarderopt, args.reporter, args.reporteropt)
|
|
88
|
+
forwarder = baseclass.new(args.forwarder, args.forwardertag, args.formatter, args.verbose, args.debug, args.forwarderopt, args.reporter, args.reporteropt, args.logger)
|
|
85
89
|
except Exception as a:
|
|
86
90
|
logger_name = "notificationforwarder_"+args.forwarder+("_"+args.forwardertag if args.forwardertag else "")
|
|
87
91
|
logger = logging.getLogger(logger_name)
|
|
@@ -90,5 +94,9 @@ Example for an HTTP-based reporter:
|
|
|
90
94
|
logger.critical("there is no class for forwarder {} and formatter {}".format(args.forwarder, args.formatter))
|
|
91
95
|
sys.exit(1)
|
|
92
96
|
|
|
93
|
-
forwarder.
|
|
97
|
+
formatter_instance = forwarder.new_formatter()
|
|
98
|
+
if hasattr(formatter_instance, 'split_events'):
|
|
99
|
+
forwarder.forward_multiple(args.eventopt)
|
|
100
|
+
else:
|
|
101
|
+
forwarder.forward(args.eventopt)
|
|
94
102
|
|
|
@@ -21,13 +21,13 @@ packages = ["src/notificationforwarder"]
|
|
|
21
21
|
|
|
22
22
|
[project]
|
|
23
23
|
name = "omdnotificationforwarder"
|
|
24
|
-
version = "
|
|
24
|
+
version = "3.0"
|
|
25
25
|
authors = [
|
|
26
26
|
{ name="Gerhard Lausser", email="lausser@yahoo.com" },
|
|
27
27
|
]
|
|
28
28
|
description = "A framework for notification scripts for OMD"
|
|
29
29
|
readme = "README.md"
|
|
30
|
-
requires-python = ">=3.6"
|
|
30
|
+
requires-python = ">=3.6.1"
|
|
31
31
|
classifiers = [
|
|
32
32
|
"Programming Language :: Python :: 3",
|
|
33
33
|
"License :: OSI Approved :: MIT License",
|
{omdnotificationforwarder-2.8 → omdnotificationforwarder-3.0}/src/notificationforwarder/baseclass.py
RENAMED
|
@@ -25,7 +25,7 @@ from coshsh.util import setup_logging
|
|
|
25
25
|
|
|
26
26
|
logger = None
|
|
27
27
|
|
|
28
|
-
def new(target_name, tag, formatter_name, verbose, debug, forwarder_opts, reporter_name=None, reporter_opts={}):
|
|
28
|
+
def new(target_name, tag, formatter_name, verbose, debug, forwarder_opts, reporter_name=None, reporter_opts={}, logger_type='text'):
|
|
29
29
|
|
|
30
30
|
forwarder_name = target_name + ("_"+tag if tag else "")
|
|
31
31
|
if verbose:
|
|
@@ -55,8 +55,27 @@ def new(target_name, tag, formatter_name, verbose, debug, forwarder_opts, report
|
|
|
55
55
|
max_spool_minutes = 5
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
# Setup Python logging infrastructure (same for all logger types)
|
|
59
|
+
# Use simple format, actual formatting is done by logger class
|
|
58
60
|
setup_logging(logdir=os.environ["OMD_ROOT"]+"/var/log", logfile=logger_name+".log", scrnloglevel=scrnloglevel, txtloglevel=txtloglevel, format="%(asctime)s %(process)d - %(levelname)s - %(message)s", backup_count=backup_count)
|
|
59
|
-
|
|
61
|
+
python_logger = logging.getLogger(logger_name)
|
|
62
|
+
|
|
63
|
+
# Instantiate application logger (text or json)
|
|
64
|
+
try:
|
|
65
|
+
if '.' in logger_type:
|
|
66
|
+
module_name, class_name = logger_type.rsplit('.', 1)
|
|
67
|
+
else:
|
|
68
|
+
module_name = logger_type
|
|
69
|
+
class_name = "".join([x.title() for x in logger_type.split("_")])+"Logger"
|
|
70
|
+
logger_module = import_module('notificationforwarder.'+module_name+'.logger',
|
|
71
|
+
package='notificationforwarder.'+module_name)
|
|
72
|
+
logger_class = getattr(logger_module, class_name)
|
|
73
|
+
logger = logger_class(logger_name, python_logger)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
# Fallback to text logger
|
|
76
|
+
from notificationforwarder.text.logger import TextLogger
|
|
77
|
+
logger = TextLogger(logger_name, python_logger)
|
|
78
|
+
logger.warning("Could not load logger type, falling back to text", {'exception': e})
|
|
60
79
|
try:
|
|
61
80
|
if '.' in target_name:
|
|
62
81
|
module_name, class_name = target_name.rsplit('.', 1)
|
|
@@ -79,10 +98,10 @@ def new(target_name, tag, formatter_name, verbose, debug, forwarder_opts, report
|
|
|
79
98
|
instance.init_paths()
|
|
80
99
|
instance.init_db()
|
|
81
100
|
|
|
82
|
-
#
|
|
83
|
-
forwarder_module.logger =
|
|
101
|
+
# Make app_logger available to modules
|
|
102
|
+
forwarder_module.logger = logger
|
|
84
103
|
base_module = import_module('.baseclass', package='notificationforwarder')
|
|
85
|
-
base_module.logger =
|
|
104
|
+
base_module.logger = logger
|
|
86
105
|
|
|
87
106
|
except Exception as e:
|
|
88
107
|
raise ImportError('{} is not part of our forwarder collection!'.format(target_name))
|
|
@@ -175,7 +194,7 @@ class NotificationForwarder(object):
|
|
|
175
194
|
self.dbcurs.execute(sql_create)
|
|
176
195
|
self.dbconn.commit()
|
|
177
196
|
except Exception as e:
|
|
178
|
-
logger.info("error initializing database {
|
|
197
|
+
logger.info("error initializing database", {'db_file': self.db_file, 'exception': e})
|
|
179
198
|
|
|
180
199
|
def new_formatter(self):
|
|
181
200
|
try:
|
|
@@ -188,10 +207,10 @@ class NotificationForwarder(object):
|
|
|
188
207
|
instance.__module_file__ = formatter_module.__file__
|
|
189
208
|
return instance
|
|
190
209
|
except ImportError:
|
|
191
|
-
logger.critical("found no formatter module {}
|
|
210
|
+
logger.critical("found no formatter module", {'module_name': module_name})
|
|
192
211
|
return None
|
|
193
212
|
except Exception as e:
|
|
194
|
-
logger.critical("unknown error
|
|
213
|
+
logger.critical("unknown error in formatter instantiation", {'exception': e})
|
|
195
214
|
return None
|
|
196
215
|
|
|
197
216
|
def new_reporter(self, opts):
|
|
@@ -205,10 +224,10 @@ class NotificationForwarder(object):
|
|
|
205
224
|
instance.__module_file__ = reporter_module.__file__
|
|
206
225
|
return instance
|
|
207
226
|
except ImportError:
|
|
208
|
-
logger.critical("found no reporter module {}
|
|
227
|
+
logger.critical("found no reporter module", {'module_name': module_name})
|
|
209
228
|
return None
|
|
210
229
|
except Exception as e:
|
|
211
|
-
logger.critical("unknown error
|
|
230
|
+
logger.critical("unknown error in reporter instantiation", {'exception': e})
|
|
212
231
|
return None
|
|
213
232
|
|
|
214
233
|
def format_event(self, raw_event):
|
|
@@ -224,7 +243,12 @@ class NotificationForwarder(object):
|
|
|
224
243
|
instance.format_event(formatted_event)
|
|
225
244
|
return formatted_event
|
|
226
245
|
except Exception as e:
|
|
227
|
-
logger.critical("when formatting
|
|
246
|
+
logger.critical("when formatting event there was an error", {
|
|
247
|
+
'event_data': str(raw_event),
|
|
248
|
+
'formatter_instance': instance,
|
|
249
|
+
'exception': e,
|
|
250
|
+
'exc_info': sys.exc_info()
|
|
251
|
+
})
|
|
228
252
|
return None
|
|
229
253
|
|
|
230
254
|
def report_event(self, formatted_event):
|
|
@@ -233,9 +257,17 @@ class NotificationForwarder(object):
|
|
|
233
257
|
instance.report_event(formatted_event)
|
|
234
258
|
except Exception as e:
|
|
235
259
|
if instance:
|
|
236
|
-
logger.critical("when reporting
|
|
260
|
+
logger.critical("when reporting event there was an error", {
|
|
261
|
+
'event_data': str(formatted_event.eventopts),
|
|
262
|
+
'reporter_instance': instance,
|
|
263
|
+
'exception': e,
|
|
264
|
+
'exc_info': sys.exc_info()
|
|
265
|
+
})
|
|
237
266
|
else:
|
|
238
|
-
logger.critical("could not create
|
|
267
|
+
logger.critical("could not create reporter instance", {
|
|
268
|
+
'reporter_name': self.reporter_name,
|
|
269
|
+
'reporter_opts': self.reporter_opts
|
|
270
|
+
})
|
|
239
271
|
return None
|
|
240
272
|
|
|
241
273
|
def forward(self, raw_event):
|
|
@@ -246,16 +278,22 @@ class NotificationForwarder(object):
|
|
|
246
278
|
if not formatted_event.is_discarded_silently:
|
|
247
279
|
if not formatted_event.summary:
|
|
248
280
|
formatted_event.summary = str(raw_event)
|
|
249
|
-
logger.info("discarded {}
|
|
281
|
+
logger.info("discarded", {'formatted_event': formatted_event})
|
|
250
282
|
formatted_event = None
|
|
251
283
|
elif formatted_event and not formatted_event.is_complete():
|
|
252
|
-
logger.critical("
|
|
284
|
+
logger.critical("formatted event incomplete", {
|
|
285
|
+
'event_class': formatted_event.__class__.__name__
|
|
286
|
+
})
|
|
253
287
|
formatted_event = None
|
|
254
288
|
except Exception as e:
|
|
255
289
|
try:
|
|
256
290
|
formatted_event
|
|
257
291
|
except NameError:
|
|
258
|
-
logger.critical("raw event
|
|
292
|
+
logger.critical("raw event caused error", {
|
|
293
|
+
'raw_event': str(raw_event),
|
|
294
|
+
'exception': e,
|
|
295
|
+
'exc_info': sys.exc_info()
|
|
296
|
+
})
|
|
259
297
|
formatted_event = None
|
|
260
298
|
|
|
261
299
|
if formatted_event:
|
|
@@ -288,11 +326,17 @@ class NotificationForwarder(object):
|
|
|
288
326
|
try:
|
|
289
327
|
raw_event_list = instance.split_events(raw_event)
|
|
290
328
|
instance = None
|
|
291
|
-
logger.debug(
|
|
329
|
+
logger.debug("received payload with multiple events", {
|
|
330
|
+
'split_count': len(raw_event_list)
|
|
331
|
+
})
|
|
292
332
|
for raw_event in raw_event_list:
|
|
293
333
|
self.forward(raw_event)
|
|
294
334
|
except Exception as e:
|
|
295
|
-
logger.critical(
|
|
335
|
+
logger.critical("split_events failed", {
|
|
336
|
+
'raw_event': raw_event,
|
|
337
|
+
'split_error': True,
|
|
338
|
+
'exception': e
|
|
339
|
+
})
|
|
296
340
|
|
|
297
341
|
def enrich_raw_event(self, raw_event):
|
|
298
342
|
if not "omd_site" in raw_event:
|
|
@@ -326,7 +370,10 @@ class NotificationForwarder(object):
|
|
|
326
370
|
if self.num_spooled_events() and (not hasattr(self, "probe") or self.probe()):
|
|
327
371
|
self.flush()
|
|
328
372
|
except Exception as e:
|
|
329
|
-
logger.critical("flush probe failed
|
|
373
|
+
logger.critical("flush probe failed", {
|
|
374
|
+
'exception': e,
|
|
375
|
+
'exc_info': sys.exc_info()
|
|
376
|
+
})
|
|
330
377
|
|
|
331
378
|
format_exception_msg = None
|
|
332
379
|
try:
|
|
@@ -353,13 +400,24 @@ class NotificationForwarder(object):
|
|
|
353
400
|
|
|
354
401
|
if success:
|
|
355
402
|
if self.baseclass_logs_summary:
|
|
356
|
-
logger.info("forwarded {
|
|
403
|
+
logger.info("forwarded", {
|
|
404
|
+
'formatted_event': formatted_event,
|
|
405
|
+
'status': 'success'
|
|
406
|
+
})
|
|
357
407
|
return success
|
|
358
408
|
else:
|
|
359
409
|
if format_exception_msg:
|
|
360
|
-
logger.critical("forward failed
|
|
410
|
+
logger.critical("forward failed", {
|
|
411
|
+
'exception': format_exception_msg,
|
|
412
|
+
'formatted_event': formatted_event,
|
|
413
|
+
'spooled': True
|
|
414
|
+
})
|
|
361
415
|
elif self.baseclass_logs_summary:
|
|
362
|
-
logger.warning("forward failed,
|
|
416
|
+
logger.warning("forward failed", {
|
|
417
|
+
'formatted_event': formatted_event,
|
|
418
|
+
'spooled': True,
|
|
419
|
+
'status': 'failed'
|
|
420
|
+
})
|
|
363
421
|
return False
|
|
364
422
|
|
|
365
423
|
|
|
@@ -370,7 +428,10 @@ class NotificationForwarder(object):
|
|
|
370
428
|
self.dbcurs.execute(sql_count)
|
|
371
429
|
spooled_events = self.dbcurs.fetchone()[0]
|
|
372
430
|
except Exception as e:
|
|
373
|
-
logger.critical("database error
|
|
431
|
+
logger.critical("database error", {
|
|
432
|
+
'database_error': True,
|
|
433
|
+
'exception': e
|
|
434
|
+
})
|
|
374
435
|
return spooled_events
|
|
375
436
|
|
|
376
437
|
|
|
@@ -381,19 +442,27 @@ class NotificationForwarder(object):
|
|
|
381
442
|
self.dbcurs.execute(sql_insert, (text,))
|
|
382
443
|
self.dbconn.commit()
|
|
383
444
|
spooled_events = self.num_spooled_events()
|
|
384
|
-
logger.warning("spooling queue length
|
|
445
|
+
logger.warning("spooling queue length", {
|
|
446
|
+
'queue_length': spooled_events
|
|
447
|
+
})
|
|
385
448
|
except Exception as e:
|
|
386
|
-
logger.critical("database error
|
|
387
|
-
|
|
449
|
+
logger.critical("database error", {
|
|
450
|
+
'database_error': True,
|
|
451
|
+
'exception': e
|
|
452
|
+
})
|
|
453
|
+
logger.info("raw event details", {'raw_event': raw_event})
|
|
388
454
|
|
|
389
455
|
def acquire_lock_with_retry(self, lock_file, max_attempts=3, base_delay=0.1):
|
|
390
456
|
for attempt in range(max_attempts):
|
|
391
457
|
try:
|
|
392
458
|
fcntl.lockf(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
393
|
-
logger.debug("flush lock set")
|
|
459
|
+
logger.debug("flush lock set", {})
|
|
394
460
|
return True
|
|
395
461
|
except IOError as e:
|
|
396
|
-
logger.debug(
|
|
462
|
+
logger.debug("flush lock failed", {
|
|
463
|
+
'attempt': attempt + 1,
|
|
464
|
+
'exception': e
|
|
465
|
+
})
|
|
397
466
|
if attempt < max_attempts - 1:
|
|
398
467
|
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.1)
|
|
399
468
|
time.sleep(delay)
|
|
@@ -412,18 +481,27 @@ class NotificationForwarder(object):
|
|
|
412
481
|
self.dbcurs.execute(sql_delete, (outdated,))
|
|
413
482
|
dropped = self.dbcurs.rowcount
|
|
414
483
|
if dropped:
|
|
415
|
-
logger.info("dropped
|
|
484
|
+
logger.info("dropped outdated events", {
|
|
485
|
+
'spooled_count': dropped,
|
|
486
|
+
'action': 'dropped'
|
|
487
|
+
})
|
|
416
488
|
last_events_to_flush = 0
|
|
417
489
|
while True:
|
|
418
490
|
events_to_flush = self.num_spooled_events()
|
|
419
491
|
if events_to_flush:
|
|
420
|
-
logger.info("
|
|
492
|
+
logger.info("spooled events to be re-sent", {
|
|
493
|
+
'spooled_count': events_to_flush,
|
|
494
|
+
'action': 'resend'
|
|
495
|
+
})
|
|
421
496
|
else:
|
|
422
|
-
logger.debug("nothing left to flush")
|
|
497
|
+
logger.debug("nothing left to flush", {})
|
|
423
498
|
break
|
|
424
499
|
if last_events_to_flush == events_to_flush:
|
|
425
500
|
if events_to_flush != 0:
|
|
426
|
-
logger.critical("
|
|
501
|
+
logger.critical("spooled events could not be submitted", {
|
|
502
|
+
'spooled_count': last_events_to_flush,
|
|
503
|
+
'action': 'could_not_submit'
|
|
504
|
+
})
|
|
427
505
|
break
|
|
428
506
|
else:
|
|
429
507
|
self.dbcurs.execute(sql_select)
|
|
@@ -436,22 +514,40 @@ class NotificationForwarder(object):
|
|
|
436
514
|
success = self.submit(formatted_event)
|
|
437
515
|
if success:
|
|
438
516
|
self.dbcurs.execute(sql_delete_id, (id, ))
|
|
439
|
-
logger.info("delete spooled event {
|
|
517
|
+
logger.info("delete spooled event", {
|
|
518
|
+
'spooled_count': 1,
|
|
519
|
+
'event_id': id,
|
|
520
|
+
'action': 'delete'
|
|
521
|
+
})
|
|
440
522
|
self.dbconn.commit()
|
|
441
523
|
else:
|
|
442
|
-
logger.critical("event
|
|
524
|
+
logger.critical("event stays in spool", {
|
|
525
|
+
'event_id': id,
|
|
526
|
+
'action': 'stays_in_spool'
|
|
527
|
+
})
|
|
443
528
|
else:
|
|
444
|
-
logger.critical("could not format spooled
|
|
529
|
+
logger.critical("could not format spooled event", {
|
|
530
|
+
'raw_event': raw_event,
|
|
531
|
+
'event_id': id,
|
|
532
|
+
'spooled_count': 1,
|
|
533
|
+
'action': 'could_not_format'
|
|
534
|
+
})
|
|
445
535
|
self.dbcurs.execute(sql_delete_id, (id, ))
|
|
446
|
-
logger.info("delete trash event {
|
|
536
|
+
logger.info("delete trash event", {
|
|
537
|
+
'event_id': id,
|
|
538
|
+
'action': 'delete_trash'
|
|
539
|
+
})
|
|
447
540
|
self.dbconn.commit()
|
|
448
541
|
last_events_to_flush = events_to_flush
|
|
449
542
|
self.dbconn.commit()
|
|
450
543
|
except Exception as e:
|
|
451
|
-
logger.critical(
|
|
544
|
+
logger.critical("database flush+resubmit failed", {
|
|
545
|
+
'database_error': True,
|
|
546
|
+
'exception': e
|
|
547
|
+
})
|
|
452
548
|
fcntl.lockf(lock_file, fcntl.LOCK_UN)
|
|
453
549
|
else:
|
|
454
|
-
logger.debug("missed the flush lock")
|
|
550
|
+
logger.debug("missed the flush lock", {})
|
|
455
551
|
|
|
456
552
|
def no_more_logging(self):
|
|
457
553
|
# this is called in the forwarder. If the forwarder already wrote
|
|
@@ -557,3 +653,74 @@ class NotificationReporter(metaclass=ABCMeta):
|
|
|
557
653
|
pass
|
|
558
654
|
|
|
559
655
|
|
|
656
|
+
class NotificationLogger(metaclass=ABCMeta):
|
|
657
|
+
"""
|
|
658
|
+
Abstract base class for loggers
|
|
659
|
+
|
|
660
|
+
Loggers receive structured context and format log entries appropriately.
|
|
661
|
+
This allows switching between text and JSON formats without changing
|
|
662
|
+
logging call sites.
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
def __init__(self, logger_name, python_logger):
|
|
666
|
+
"""
|
|
667
|
+
Initialize logger
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
logger_name: Name of the logger (e.g., "notificationforwarder_webhook")
|
|
671
|
+
python_logger: Underlying Python logging.Logger instance
|
|
672
|
+
"""
|
|
673
|
+
self.logger_name = logger_name
|
|
674
|
+
self.python_logger = python_logger
|
|
675
|
+
self.omd_site = os.environ.get("OMD_SITE", "")
|
|
676
|
+
self.originating_host = socket.gethostname()
|
|
677
|
+
self.originating_fqdn = socket.getfqdn()
|
|
678
|
+
|
|
679
|
+
@abstractmethod
|
|
680
|
+
def log(self, level, message, context=None):
|
|
681
|
+
"""
|
|
682
|
+
Log a message with structured context
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
level: Log level ('debug', 'info', 'warning', 'error', 'critical')
|
|
686
|
+
message: Human-readable message
|
|
687
|
+
context: Dict with structured context:
|
|
688
|
+
- event: Raw event dict (eventopts)
|
|
689
|
+
- formatted_event: FormattedEvent instance
|
|
690
|
+
- exception: Exception object
|
|
691
|
+
- exc_info: sys.exc_info() tuple for traceback
|
|
692
|
+
- spooled: Boolean if event was spooled
|
|
693
|
+
- forwarder_name: Name of forwarder
|
|
694
|
+
- formatter_name: Name of formatter
|
|
695
|
+
- reporter_name: Name of reporter
|
|
696
|
+
- formatter_instance: Formatter instance
|
|
697
|
+
- reporter_instance: Reporter instance
|
|
698
|
+
- spooled_count: Number of spooled events
|
|
699
|
+
- queue_length: Queue length
|
|
700
|
+
- dropped_count: Number of dropped events
|
|
701
|
+
- event_data: Raw event data
|
|
702
|
+
- status: Status string
|
|
703
|
+
"""
|
|
704
|
+
pass
|
|
705
|
+
|
|
706
|
+
def debug(self, message, context=None):
|
|
707
|
+
"""Convenience method for debug level"""
|
|
708
|
+
self.log('debug', message, context)
|
|
709
|
+
|
|
710
|
+
def info(self, message, context=None):
|
|
711
|
+
"""Convenience method for info level"""
|
|
712
|
+
self.log('info', message, context)
|
|
713
|
+
|
|
714
|
+
def warning(self, message, context=None):
|
|
715
|
+
"""Convenience method for warning level"""
|
|
716
|
+
self.log('warning', message, context)
|
|
717
|
+
|
|
718
|
+
def error(self, message, context=None):
|
|
719
|
+
"""Convenience method for error level"""
|
|
720
|
+
self.log('error', message, context)
|
|
721
|
+
|
|
722
|
+
def critical(self, message, context=None):
|
|
723
|
+
"""Convenience method for critical level"""
|
|
724
|
+
self.log('critical', message, context)
|
|
725
|
+
|
|
726
|
+
|