|
4 | 4 |
|
5 | 5 | import logging
|
6 | 6 | from logging.handlers import RotatingFileHandler, SysLogHandler
|
7 |
| -from . import app_qpylib |
8 |
| -from . import util_qpylib |
| 7 | +import re |
| 8 | +from qpylib.encryption.cryptoutil import derive_key |
| 9 | +from . import app_qpylib, util_qpylib |
9 | 10 |
|
10 |
| -APP_FILE_LOG_FORMAT = '%(asctime)s [%(module)s.%(funcName)s] [%(threadName)s] [%(levelname)s] - %(message)s' |
11 |
| -APP_CONSOLE_LOG_FORMAT = '%(asctime)s %(module)s.%(funcName)s: %(message)s' |
| 11 | +# Log format for local file logging. |
| 12 | +# Uses default date/time formatting from logging module. |
| 13 | +# Before the logging formatter is created, APPID is substituted with the actual app ID, |
| 14 | +# which is constant for all logs generated within an app container. |
| 15 | +# Example: 2020-08-19 12:48:11,423 [Thread-4] [INFO] [APP_ID:1005] [NOT:0000006000] hello |
12 | 16 |
|
13 |
| -QLOGGER = 0 |
| 17 | +APP_FILE_LOG_FORMAT = '%(asctime)s [%(threadName)s] [%(levelname)s] [APP_ID:APPID] [NOT:%(ncode)s] %(message)s' |
14 | 18 |
|
15 |
| -def log(message, level): |
16 |
| - log_fn = _choose_log_fn(level) |
17 |
| - log_fn("[APP_ID/{0}][NOT:{1}] {2}" |
18 |
| - .format(app_qpylib.get_app_id(), _map_notification_code(level), message)) |
| 19 | +# Log format for Syslog. |
| 20 | +# See https://tools.ietf.org/html/rfc5424 for more details on the following specification: |
| 21 | +# SYSLOG-MSG = HEADER SP STRUCTURED-DATA SP MSG |
| 22 | +# HEADER = PRI VERSION SP TIMESTAMP SP HOSTNAME SP APPNAME SP PROCID SP MSGID |
| 23 | +# PRI is taken care of by SysLogHandler and not included in the log format below. |
| 24 | +# VERSION is hard-coded to 1. |
| 25 | +# TIMESTAMP in the specification is e.g. 2020-04-12T19:20:50.345678+01:00 |
| 26 | +# Seconds fraction is not supported in "time" module, which is what the logging |
| 27 | +# module uses. SYSLOG_LOG_FORMAT uses asctime formatted with SYSLOG_TIME_FORMAT. |
| 28 | +# HOSTNAME is set to a hash generated using the app ID and QRADAR_APP_UUID. |
| 29 | +# The reason for not using the container host name or IP address is that for a |
| 30 | +# given app instance, both of those values can change across a reboot. |
| 31 | +# APPNAME is set to a sanitised copy of the app manifest name field. |
| 32 | +# PROCID is set to the app ID. |
| 33 | +# HOSTNAME, APPNAME and PROCID are all constants, replaced in the format string with their values. |
| 34 | +# MSGID and STRUCTURED-DATA are set to the Syslog nil value '-'. |
| 35 | +# Example: <14>1 2020-08-21T14:16:51+0100 d15fbd9fdd86b30e MyExampleApp 1005 - - [NOT:0000006000] hello |
| 36 | + |
| 37 | +SYSLOG_LOG_FORMAT = '1 %(asctime)s HOSTNAME APPNAME PROCID - - [NOT:%(ncode)s] %(message)s' |
| 38 | +SYSLOG_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%z' |
| 39 | + |
| 40 | +# Globals related to logging.Logger instance. |
| 41 | +QLOGGER = None |
| 42 | +LOG_LEVEL_TO_FUNCTION = None |
19 | 43 |
|
20 | 44 | def create_log():
|
21 | 45 | global QLOGGER
|
22 | 46 | QLOGGER = logging.getLogger('com.ibm.applicationLogger')
|
23 |
| - QLOGGER.setLevel(default_log_level()) |
| 47 | + QLOGGER.setLevel(_default_log_level()) |
| 48 | + QLOGGER.addFilter(NotificationCodeFilter()) |
24 | 49 |
|
25 |
| - handler = RotatingFileHandler(_log_file_location(), maxBytes=2*1024*1024, backupCount=5) |
26 |
| - handler.setFormatter(logging.Formatter(APP_FILE_LOG_FORMAT)) |
27 |
| - QLOGGER.addHandler(handler) |
| 50 | + for handler in _generate_handlers(): |
| 51 | + QLOGGER.addHandler(handler) |
28 | 52 |
|
29 |
| - if not util_qpylib.is_sdk(): |
30 |
| - console_ip = app_qpylib.get_console_ip() |
31 |
| - if util_qpylib.is_ipv6_address(console_ip): |
32 |
| - console_ip = console_ip[1:-1] |
33 |
| - syslog_handler = SysLogHandler(address=(console_ip, 514)) |
34 |
| - syslog_handler.setFormatter(logging.Formatter(APP_CONSOLE_LOG_FORMAT)) |
35 |
| - QLOGGER.addHandler(syslog_handler) |
| 53 | + global LOG_LEVEL_TO_FUNCTION |
| 54 | + LOG_LEVEL_TO_FUNCTION = { |
| 55 | + 'DEBUG': QLOGGER.debug, |
| 56 | + 'INFO': QLOGGER.info, |
| 57 | + 'WARNING': QLOGGER.warning, |
| 58 | + 'ERROR': QLOGGER.error, |
| 59 | + 'EXCEPTION': QLOGGER.exception, |
| 60 | + 'CRITICAL': QLOGGER.critical |
| 61 | + } |
| 62 | + |
| 63 | +def log(message, level): |
| 64 | + global LOG_LEVEL_TO_FUNCTION |
| 65 | + if not LOG_LEVEL_TO_FUNCTION: |
| 66 | + raise RuntimeError('You cannot use log before logging has been initialised') |
| 67 | + log_function = LOG_LEVEL_TO_FUNCTION.get(level.upper()) |
| 68 | + if not log_function: |
| 69 | + raise ValueError("Unknown level: '{0}'".format(level)) |
| 70 | + log_function(message) |
36 | 71 |
|
37 | 72 | def set_log_level(level='INFO'):
|
38 | 73 | global QLOGGER
|
39 |
| - QLOGGER.setLevel(_map_log_level(level)) |
| 74 | + if not QLOGGER: |
| 75 | + raise RuntimeError('You cannot use set_log_level before logging has been initialised') |
| 76 | + QLOGGER.setLevel(level.upper()) |
40 | 77 |
|
41 |
| -def default_log_level(): |
42 |
| - return _map_log_level(app_qpylib.get_manifest_field_value('log_level', 'INFO')) |
| 78 | +def _default_log_level(): |
| 79 | + return app_qpylib.get_manifest_field_value('log_level', 'INFO').upper() |
43 | 80 |
|
44 | 81 | def _log_file_location():
|
45 | 82 | return app_qpylib.get_log_path('app.log')
|
46 | 83 |
|
47 |
| -def _choose_log_fn(level): |
48 |
| - global QLOGGER |
49 |
| - if QLOGGER == 0: |
50 |
| - raise RuntimeError('You cannot use log before logging has been initialised') |
51 |
| - return { |
52 |
| - 'INFO': QLOGGER.info, |
53 |
| - 'DEBUG': QLOGGER.debug, |
54 |
| - 'WARNING': QLOGGER.warning, |
55 |
| - 'ERROR': QLOGGER.error, |
56 |
| - 'EXCEPTION': QLOGGER.exception, |
57 |
| - 'CRITICAL': QLOGGER.critical |
58 |
| - }.get(level.upper(), QLOGGER.info) |
59 |
| - |
60 |
| -def _map_notification_code(level): |
61 |
| - return { |
62 |
| - 'INFO': "0000006000", |
63 |
| - 'DEBUG': "0000006000", |
64 |
| - 'WARNING': "0000004000", |
65 |
| - 'ERROR': "0000003000", |
66 |
| - 'EXCEPTION': "0000003000", |
67 |
| - 'CRITICAL': "0000003000" |
68 |
| - }.get(level.upper(), "0000006000") |
69 |
| - |
70 |
| -def _map_log_level(level): |
71 |
| - return { |
72 |
| - 'INFO': logging.INFO, |
73 |
| - 'DEBUG': logging.DEBUG, |
74 |
| - 'WARNING': logging.WARNING, |
75 |
| - 'ERROR': logging.ERROR, |
76 |
| - 'CRITICAL': logging.CRITICAL |
77 |
| - }.get(level.upper(), logging.INFO) |
| 84 | +def _generate_handlers(): |
| 85 | + handlers = [] |
| 86 | + |
| 87 | + app_id = str(app_qpylib.get_app_id()) |
| 88 | + handlers.append(_create_file_handler(app_id)) |
| 89 | + |
| 90 | + address = None |
| 91 | + qradar_app_uuid = None |
| 92 | + try: |
| 93 | + address = _get_address_for_syslog() |
| 94 | + qradar_app_uuid = app_qpylib.get_env_var('QRADAR_APP_UUID') |
| 95 | + except KeyError: |
| 96 | + pass |
| 97 | + if address and qradar_app_uuid: |
| 98 | + handlers.append(_create_syslog_handler(address, app_id, qradar_app_uuid)) |
| 99 | + |
| 100 | + return handlers |
| 101 | + |
| 102 | +def _create_file_handler(app_id): |
| 103 | + handler = RotatingFileHandler(_log_file_location(), maxBytes=2*1024*1024, backupCount=5) |
| 104 | + log_format = APP_FILE_LOG_FORMAT.replace('APPID', app_id) |
| 105 | + handler.setFormatter(logging.Formatter(log_format)) |
| 106 | + return handler |
| 107 | + |
| 108 | +def _create_syslog_handler(syslog_address, app_id, qradar_app_uuid): |
| 109 | + log_format = _create_syslog_log_format(app_id, qradar_app_uuid) |
| 110 | + handler = SysLogHandler(address=syslog_address) |
| 111 | + handler.setFormatter(logging.Formatter(log_format, SYSLOG_TIME_FORMAT)) |
| 112 | + return handler |
| 113 | + |
| 114 | +def _get_address_for_syslog(): |
| 115 | + console_ip = app_qpylib.get_console_ip() |
| 116 | + if util_qpylib.is_ipv6_address(console_ip): |
| 117 | + console_ip = console_ip[1:-1] |
| 118 | + return (console_ip, 514) |
| 119 | + |
| 120 | +def _create_syslog_log_format(app_id, qradar_app_uuid): |
| 121 | + pseudo_hostname = _create_pseudo_hostname(app_id, qradar_app_uuid) |
| 122 | + sanitized_app_name = _create_sanitized_app_name() |
| 123 | + return SYSLOG_LOG_FORMAT.replace('HOSTNAME', pseudo_hostname) \ |
| 124 | + .replace('APPNAME', sanitized_app_name) \ |
| 125 | + .replace('PROCID', app_id) |
| 126 | + |
| 127 | +def _create_pseudo_hostname(app_id, qradar_app_uuid): |
| 128 | + # This uses a key derivation function rather than a straightforward hash |
| 129 | + # function so that the unique value generated has 16 characters (hex). |
| 130 | + # Anything longer would occupy too much space in each log record. |
| 131 | + key = derive_key(app_id.encode('utf-8'), |
| 132 | + qradar_app_uuid.encode('utf-8'), |
| 133 | + length=8) |
| 134 | + return key.hex() |
| 135 | + |
| 136 | +def _create_sanitized_app_name(): |
| 137 | + ''' Extracts app name from manifest, strips unwanted characters, |
| 138 | + and truncates to max length 48, as per RFC5424. |
| 139 | + ''' |
| 140 | + return re.sub(r'\W+', '', app_qpylib.get_app_name())[:48] |
| 141 | + |
| 142 | +class NotificationCodeFilter(logging.Filter): |
| 143 | + ''' Filter which adds a field named ncode to each log record. |
| 144 | + Allows notification code to be specified in log handler |
| 145 | + format strings. |
| 146 | + ''' |
| 147 | + # These are standard QRadar codes for identifying log levels. |
| 148 | + Q_INFO_CODE = '0000006000' |
| 149 | + Q_WARNING_CODE = '0000004000' |
| 150 | + Q_ERROR_CODE = '0000003000' |
| 151 | + |
| 152 | + def filter(self, record): |
| 153 | + record.ncode = self._log_level_to_notification_code.get( |
| 154 | + record.levelname.upper(), self.Q_INFO_CODE) |
| 155 | + return True |
| 156 | + |
| 157 | + _log_level_to_notification_code = { |
| 158 | + 'DEBUG': Q_INFO_CODE, |
| 159 | + 'INFO': Q_INFO_CODE, |
| 160 | + 'WARNING': Q_WARNING_CODE, |
| 161 | + 'ERROR': Q_ERROR_CODE, |
| 162 | + 'CRITICAL': Q_ERROR_CODE |
| 163 | + } |
0 commit comments