omdnotificationforwarder 2.9__tar.gz → 4.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.
Files changed (49) hide show
  1. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/PKG-INFO +91 -2
  2. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/README.md +90 -1
  3. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/bin/notificationforwarder +6 -2
  4. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/pyproject.toml +1 -1
  5. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/baseclass.py +205 -38
  6. omdnotificationforwarder-4.0/src/notificationforwarder/json/__init__.py +1 -0
  7. omdnotificationforwarder-4.0/src/notificationforwarder/json/logger.py +230 -0
  8. omdnotificationforwarder-4.0/src/notificationforwarder/text/__init__.py +1 -0
  9. omdnotificationforwarder-4.0/src/notificationforwarder/text/logger.py +163 -0
  10. omdnotificationforwarder-4.0/tests/test_logger.py +239 -0
  11. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/.gitignore +0 -0
  12. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/email/formatter.py +0 -0
  13. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/email/forwarder.py +0 -0
  14. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/example/formatter.py +0 -0
  15. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/example/forwarder.py +0 -0
  16. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/naemonlog/reporter.py +0 -0
  17. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/rabbitmq/formatter.py +0 -0
  18. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/rabbitmq/forwarder.py +0 -0
  19. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/syslog/formatter.py +0 -0
  20. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/syslog/forwarder.py +0 -0
  21. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/telegram/forwarder.py +0 -0
  22. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/src/notificationforwarder/webhook/forwarder.py +0 -0
  23. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/lib/python/notificationforwarder/split1/forwarder.py +0 -0
  24. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/lib/python/notificationforwarder/split2/formatter.py +0 -0
  25. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/lib/python/notificationforwarder/split2/forwarder.py +0 -0
  26. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/lib/python/notificationforwarder/split3/formatter.py +0 -0
  27. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/lib/python/notificationforwarder/split3/forwarder.py +0 -0
  28. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py +0 -0
  29. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/bayern/formatter.py +0 -0
  30. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/datadup/formatter.py +0 -0
  31. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/datapost/formatter.py +0 -0
  32. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/discard/formatter.py +0 -0
  33. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split1/formatter.py +0 -0
  34. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split2/forwarder.py +0 -0
  35. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split3/formatter.py +0 -0
  36. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split3/forwarder.py +0 -0
  37. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split4/formatter.py +0 -0
  38. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/split4/forwarder.py +0 -0
  39. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/forwarder.py +0 -0
  40. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/reporter.py +0 -0
  41. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/pythonpath/local/lib/python/notificationforwarder/vong/formatter.py +0 -0
  42. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_alertmanager.py +0 -0
  43. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_classes.py +0 -0
  44. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_discard.py +0 -0
  45. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_formatter.py +0 -0
  46. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_package.py +0 -0
  47. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_paths.py +0 -0
  48. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_reporter.py +0 -0
  49. {omdnotificationforwarder-2.9 → omdnotificationforwarder-4.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdnotificationforwarder
3
- Version: 2.9
3
+ Version: 4.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
@@ -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 2.9')
81
+ version=f'%(prog)s 4.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)
@@ -21,7 +21,7 @@ packages = ["src/notificationforwarder"]
21
21
 
22
22
  [project]
23
23
  name = "omdnotificationforwarder"
24
- version = "2.9"
24
+ version = "4.0"
25
25
  authors = [
26
26
  { name="Gerhard Lausser", email="lausser@yahoo.com" },
27
27
  ]
@@ -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
- logger = logging.getLogger(logger_name)
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
- # so we can use logger.info(...) in the single modules
83
- forwarder_module.logger = logging.getLogger(logger_name)
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 = logging.getLogger(logger_name)
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 {}: {}".format(self.db_file, str(e)))
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 {}".format(module_name))
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 error in formatter instantiation: {}".format(e))
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 {}".format(module_name))
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 error in reporter instantiation: {}".format(e))
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 this {} with this {} there was an error <{}>".format(str(raw_event), instance.__class__.__name__+"@"+instance.__module_file__, str(e)))
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 this {} with this {} there was an error <{}>".format(str(formatted_event.eventopts), instance.__class__.__name__+"@"+instance.__module_file__, str(e)))
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 a {} reporter instance with {}".format(self.reporter_name, self.reporter_opts))
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 {}".format(formatted_event.summary))
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("a formatted event {} must have the attributes payload and summary".format(formatted_event.__class__.__name__))
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 {} caused error {}".format(str(raw_event), str(e)))
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(f"received a payload with {len(raw_event_list)} single events")
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(f"error split_events failed for {raw_event}")
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 with exception <{}>".format(str(e)))
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 {}".format(formatted_event.summary))
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 with exception <{}>, spooled <{}>".format(format_exception_msg, formatted_event.summary))
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, spooling {}".format(formatted_event.summary))
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 "+str(e))
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 is {}".format(spooled_events))
445
+ logger.warning("spooling queue length", {
446
+ 'queue_length': spooled_events
447
+ })
385
448
  except Exception as e:
386
- logger.critical("database error "+str(e))
387
- logger.info(raw_event)
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(f"flush lock failed (attempt {attempt + 1}): {str(e)}")
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 {} outdated events".format(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("there are {} spooled events to be re-sent".format(events_to_flush))
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("{} spooled events could not be submitted".format(last_events_to_flush))
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 {}".format(id))
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 {} stays in spool".format(id))
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 {}. sorry, but i will delete this garbage with id {}".format(raw_event, id))
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 {}".format(id))
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(f"database flush+resubmit failed: {e}")
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
+
@@ -0,0 +1 @@
1
+ # JSON logger module