##// END OF EJS Templates
vendor: python3 fixes
super-admin -
r5041:dcbf3e8a default
parent child Browse files
Show More
@@ -1,236 +1,237 b''
1 '''
1 '''
2 This library is provided to allow standard python logging
2 This library is provided to allow standard python logging
3 to output log data as JSON formatted strings
3 to output log data as JSON formatted strings
4 '''
4 '''
5 import logging
5 import logging
6 import json
7 import re
6 import re
8 from datetime import date, datetime, time, tzinfo, timedelta
7 from datetime import date, datetime, time, tzinfo, timedelta
9 import traceback
8 import traceback
10 import importlib
9 import importlib
11
10
12 from inspect import istraceback
11 from inspect import istraceback
13
12
14 from collections import OrderedDict
13 from collections import OrderedDict
15 from rhodecode.lib.logging_formatter import _inject_req_id, ExceptionAwareFormatter
14 from rhodecode.lib.logging_formatter import _inject_req_id, ExceptionAwareFormatter
15 from rhodecode.lib.ext_json import sjson as json
16
16
17
17 ZERO = timedelta(0)
18 ZERO = timedelta(0)
18 HOUR = timedelta(hours=1)
19 HOUR = timedelta(hours=1)
19
20
20
21
21 class UTC(tzinfo):
22 class UTC(tzinfo):
22 """UTC"""
23 """UTC"""
23
24
24 def utcoffset(self, dt):
25 def utcoffset(self, dt):
25 return ZERO
26 return ZERO
26
27
27 def tzname(self, dt):
28 def tzname(self, dt):
28 return "UTC"
29 return "UTC"
29
30
30 def dst(self, dt):
31 def dst(self, dt):
31 return ZERO
32 return ZERO
32
33
33 utc = UTC()
34 utc = UTC()
34
35
35
36
36 # skip natural LogRecord attributes
37 # skip natural LogRecord attributes
37 # http://docs.python.org/library/logging.html#logrecord-attributes
38 # http://docs.python.org/library/logging.html#logrecord-attributes
38 RESERVED_ATTRS = (
39 RESERVED_ATTRS = (
39 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
40 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
40 'funcName', 'levelname', 'levelno', 'lineno', 'module',
41 'funcName', 'levelname', 'levelno', 'lineno', 'module',
41 'msecs', 'message', 'msg', 'name', 'pathname', 'process',
42 'msecs', 'message', 'msg', 'name', 'pathname', 'process',
42 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')
43 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')
43
44
44
45
45 def merge_record_extra(record, target, reserved):
46 def merge_record_extra(record, target, reserved):
46 """
47 """
47 Merges extra attributes from LogRecord object into target dictionary
48 Merges extra attributes from LogRecord object into target dictionary
48
49
49 :param record: logging.LogRecord
50 :param record: logging.LogRecord
50 :param target: dict to update
51 :param target: dict to update
51 :param reserved: dict or list with reserved keys to skip
52 :param reserved: dict or list with reserved keys to skip
52 """
53 """
53 for key, value in record.__dict__.items():
54 for key, value in record.__dict__.items():
54 # this allows to have numeric keys
55 # this allows to have numeric keys
55 if (key not in reserved
56 if (key not in reserved
56 and not (hasattr(key, "startswith")
57 and not (hasattr(key, "startswith")
57 and key.startswith('_'))):
58 and key.startswith('_'))):
58 target[key] = value
59 target[key] = value
59 return target
60 return target
60
61
61
62
62 class JsonEncoder(json.JSONEncoder):
63 class JsonEncoder(json.JSONEncoder):
63 """
64 """
64 A custom encoder extending the default JSONEncoder
65 A custom encoder extending the default JSONEncoder
65 """
66 """
66
67
67 def default(self, obj):
68 def default(self, obj):
68 if isinstance(obj, (date, datetime, time)):
69 if isinstance(obj, (date, datetime, time)):
69 return self.format_datetime_obj(obj)
70 return self.format_datetime_obj(obj)
70
71
71 elif istraceback(obj):
72 elif istraceback(obj):
72 return ''.join(traceback.format_tb(obj)).strip()
73 return ''.join(traceback.format_tb(obj)).strip()
73
74
74 elif type(obj) == Exception \
75 elif type(obj) == Exception \
75 or isinstance(obj, Exception) \
76 or isinstance(obj, Exception) \
76 or type(obj) == type:
77 or type(obj) == type:
77 return str(obj)
78 return str(obj)
78
79
79 try:
80 try:
80 return super(JsonEncoder, self).default(obj)
81 return super(JsonEncoder, self).default(obj)
81
82
82 except TypeError:
83 except TypeError:
83 try:
84 try:
84 return str(obj)
85 return str(obj)
85
86
86 except Exception:
87 except Exception:
87 return None
88 return None
88
89
89 def format_datetime_obj(self, obj):
90 def format_datetime_obj(self, obj):
90 return obj.isoformat()
91 return obj.isoformat()
91
92
92
93
93 class JsonFormatter(ExceptionAwareFormatter):
94 class JsonFormatter(ExceptionAwareFormatter):
94 """
95 """
95 A custom formatter to format logging records as json strings.
96 A custom formatter to format logging records as json strings.
96 Extra values will be formatted as str() if not supported by
97 Extra values will be formatted as str() if not supported by
97 json default encoder
98 json default encoder
98 """
99 """
99
100
100 def __init__(self, *args, **kwargs):
101 def __init__(self, *args, **kwargs):
101 """
102 """
102 :param json_default: a function for encoding non-standard objects
103 :param json_default: a function for encoding non-standard objects
103 as outlined in http://docs.python.org/2/library/json.html
104 as outlined in http://docs.python.org/2/library/json.html
104 :param json_encoder: optional custom encoder
105 :param json_encoder: optional custom encoder
105 :param json_serializer: a :meth:`json.dumps`-compatible callable
106 :param json_serializer: a :meth:`json.dumps`-compatible callable
106 that will be used to serialize the log record.
107 that will be used to serialize the log record.
107 :param json_indent: an optional :meth:`json.dumps`-compatible numeric value
108 :param json_indent: an optional :meth:`json.dumps`-compatible numeric value
108 that will be used to customize the indent of the output json.
109 that will be used to customize the indent of the output json.
109 :param prefix: an optional string prefix added at the beginning of
110 :param prefix: an optional string prefix added at the beginning of
110 the formatted string
111 the formatted string
111 :param json_indent: indent parameter for json.dumps
112 :param json_indent: indent parameter for json.dumps
112 :param json_ensure_ascii: ensure_ascii parameter for json.dumps
113 :param json_ensure_ascii: ensure_ascii parameter for json.dumps
113 :param reserved_attrs: an optional list of fields that will be skipped when
114 :param reserved_attrs: an optional list of fields that will be skipped when
114 outputting json log record. Defaults to all log record attributes:
115 outputting json log record. Defaults to all log record attributes:
115 http://docs.python.org/library/logging.html#logrecord-attributes
116 http://docs.python.org/library/logging.html#logrecord-attributes
116 :param timestamp: an optional string/boolean field to add a timestamp when
117 :param timestamp: an optional string/boolean field to add a timestamp when
117 outputting the json log record. If string is passed, timestamp will be added
118 outputting the json log record. If string is passed, timestamp will be added
118 to log record using string as key. If True boolean is passed, timestamp key
119 to log record using string as key. If True boolean is passed, timestamp key
119 will be "timestamp". Defaults to False/off.
120 will be "timestamp". Defaults to False/off.
120 """
121 """
121 self.json_default = self._str_to_fn(kwargs.pop("json_default", None))
122 self.json_default = self._str_to_fn(kwargs.pop("json_default", None))
122 self.json_encoder = self._str_to_fn(kwargs.pop("json_encoder", None))
123 self.json_encoder = self._str_to_fn(kwargs.pop("json_encoder", None))
123 self.json_serializer = self._str_to_fn(kwargs.pop("json_serializer", json.dumps))
124 self.json_serializer = self._str_to_fn(kwargs.pop("json_serializer", json.dumps))
124 self.json_indent = kwargs.pop("json_indent", None)
125 self.json_indent = kwargs.pop("json_indent", None)
125 self.json_ensure_ascii = kwargs.pop("json_ensure_ascii", True)
126 self.json_ensure_ascii = kwargs.pop("json_ensure_ascii", True)
126 self.prefix = kwargs.pop("prefix", "")
127 self.prefix = kwargs.pop("prefix", "")
127 reserved_attrs = kwargs.pop("reserved_attrs", RESERVED_ATTRS)
128 reserved_attrs = kwargs.pop("reserved_attrs", RESERVED_ATTRS)
128 self.reserved_attrs = dict(list(zip(reserved_attrs, reserved_attrs)))
129 self.reserved_attrs = dict(list(zip(reserved_attrs, reserved_attrs)))
129 self.timestamp = kwargs.pop("timestamp", True)
130 self.timestamp = kwargs.pop("timestamp", True)
130
131
131 # super(JsonFormatter, self).__init__(*args, **kwargs)
132 # super(JsonFormatter, self).__init__(*args, **kwargs)
132 logging.Formatter.__init__(self, *args, **kwargs)
133 logging.Formatter.__init__(self, *args, **kwargs)
133 if not self.json_encoder and not self.json_default:
134 if not self.json_encoder and not self.json_default:
134 self.json_encoder = JsonEncoder
135 self.json_encoder = JsonEncoder
135
136
136 self._required_fields = self.parse()
137 self._required_fields = self.parse()
137 self._skip_fields = dict(list(zip(self._required_fields,
138 self._skip_fields = dict(list(zip(self._required_fields,
138 self._required_fields)))
139 self._required_fields)))
139 self._skip_fields.update(self.reserved_attrs)
140 self._skip_fields.update(self.reserved_attrs)
140
141
141 def _str_to_fn(self, fn_as_str):
142 def _str_to_fn(self, fn_as_str):
142 """
143 """
143 If the argument is not a string, return whatever was passed in.
144 If the argument is not a string, return whatever was passed in.
144 Parses a string such as package.module.function, imports the module
145 Parses a string such as package.module.function, imports the module
145 and returns the function.
146 and returns the function.
146
147
147 :param fn_as_str: The string to parse. If not a string, return it.
148 :param fn_as_str: The string to parse. If not a string, return it.
148 """
149 """
149 if not isinstance(fn_as_str, str):
150 if not isinstance(fn_as_str, str):
150 return fn_as_str
151 return fn_as_str
151
152
152 path, _, function = fn_as_str.rpartition('.')
153 path, _, function = fn_as_str.rpartition('.')
153 module = importlib.import_module(path)
154 module = importlib.import_module(path)
154 return getattr(module, function)
155 return getattr(module, function)
155
156
156 def parse(self):
157 def parse(self):
157 """
158 """
158 Parses format string looking for substitutions
159 Parses format string looking for substitutions
159
160
160 This method is responsible for returning a list of fields (as strings)
161 This method is responsible for returning a list of fields (as strings)
161 to include in all log messages.
162 to include in all log messages.
162 """
163 """
163 standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
164 standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
164 return standard_formatters.findall(self._fmt)
165 return standard_formatters.findall(self._fmt)
165
166
166 def add_fields(self, log_record, record, message_dict):
167 def add_fields(self, log_record, record, message_dict):
167 """
168 """
168 Override this method to implement custom logic for adding fields.
169 Override this method to implement custom logic for adding fields.
169 """
170 """
170 for field in self._required_fields:
171 for field in self._required_fields:
171 log_record[field] = record.__dict__.get(field)
172 log_record[field] = record.__dict__.get(field)
172 log_record.update(message_dict)
173 log_record.update(message_dict)
173 merge_record_extra(record, log_record, reserved=self._skip_fields)
174 merge_record_extra(record, log_record, reserved=self._skip_fields)
174
175
175 if self.timestamp:
176 if self.timestamp:
176 key = self.timestamp if type(self.timestamp) == str else 'timestamp'
177 key = self.timestamp if type(self.timestamp) == str else 'timestamp'
177 log_record[key] = datetime.fromtimestamp(record.created, tz=utc)
178 log_record[key] = datetime.fromtimestamp(record.created, tz=utc)
178
179
179 def process_log_record(self, log_record):
180 def process_log_record(self, log_record):
180 """
181 """
181 Override this method to implement custom logic
182 Override this method to implement custom logic
182 on the possibly ordered dictionary.
183 on the possibly ordered dictionary.
183 """
184 """
184 return log_record
185 return log_record
185
186
186 def jsonify_log_record(self, log_record):
187 def jsonify_log_record(self, log_record):
187 """Returns a json string of the log record."""
188 """Returns a json string of the log record."""
188 return self.json_serializer(log_record,
189 return self.json_serializer(log_record,
189 default=self.json_default,
190 default=self.json_default,
190 cls=self.json_encoder,
191 cls=self.json_encoder,
191 indent=self.json_indent,
192 indent=self.json_indent,
192 ensure_ascii=self.json_ensure_ascii)
193 ensure_ascii=self.json_ensure_ascii)
193
194
194 def serialize_log_record(self, log_record):
195 def serialize_log_record(self, log_record):
195 """Returns the final representation of the log record."""
196 """Returns the final representation of the log record."""
196 return "%s%s" % (self.prefix, self.jsonify_log_record(log_record))
197 return "%s%s" % (self.prefix, self.jsonify_log_record(log_record))
197
198
198 def format(self, record):
199 def format(self, record):
199 """Formats a log record and serializes to json"""
200 """Formats a log record and serializes to json"""
200 message_dict = {}
201 message_dict = {}
201 # FIXME: logging.LogRecord.msg and logging.LogRecord.message in typeshed
202 # FIXME: logging.LogRecord.msg and logging.LogRecord.message in typeshed
202 # are always type of str. We shouldn't need to override that.
203 # are always type of str. We shouldn't need to override that.
203 if isinstance(record.msg, dict):
204 if isinstance(record.msg, dict):
204 message_dict = record.msg
205 message_dict = record.msg
205 record.message = None
206 record.message = None
206 else:
207 else:
207 record.message = record.getMessage()
208 record.message = record.getMessage()
208 # only format time if needed
209 # only format time if needed
209 if "asctime" in self._required_fields:
210 if "asctime" in self._required_fields:
210 record.asctime = self.formatTime(record, self.datefmt)
211 record.asctime = self.formatTime(record, self.datefmt)
211
212
212 # Display formatted exception, but allow overriding it in the
213 # Display formatted exception, but allow overriding it in the
213 # user-supplied dict.
214 # user-supplied dict.
214 if record.exc_info and not message_dict.get('exc_info'):
215 if record.exc_info and not message_dict.get('exc_info'):
215 message_dict['exc_info'] = self.formatException(record.exc_info)
216 message_dict['exc_info'] = self.formatException(record.exc_info)
216 if not message_dict.get('exc_info') and record.exc_text:
217 if not message_dict.get('exc_info') and record.exc_text:
217 message_dict['exc_info'] = record.exc_text
218 message_dict['exc_info'] = record.exc_text
218 # Display formatted record of stack frames
219 # Display formatted record of stack frames
219 # default format is a string returned from :func:`traceback.print_stack`
220 # default format is a string returned from :func:`traceback.print_stack`
220 try:
221 try:
221 if record.stack_info and not message_dict.get('stack_info'):
222 if record.stack_info and not message_dict.get('stack_info'):
222 message_dict['stack_info'] = self.formatStack(record.stack_info)
223 message_dict['stack_info'] = self.formatStack(record.stack_info)
223 except AttributeError:
224 except AttributeError:
224 # Python2.7 doesn't have stack_info.
225 # Python2.7 doesn't have stack_info.
225 pass
226 pass
226
227
227 try:
228 try:
228 log_record = OrderedDict()
229 log_record = OrderedDict()
229 except NameError:
230 except NameError:
230 log_record = {}
231 log_record = {}
231
232
232 _inject_req_id(record, with_prefix=False)
233 _inject_req_id(record, with_prefix=False)
233 self.add_fields(log_record, record, message_dict)
234 self.add_fields(log_record, record, message_dict)
234 log_record = self.process_log_record(log_record)
235 log_record = self.process_log_record(log_record)
235
236
236 return self.serialize_log_record(log_record)
237 return self.serialize_log_record(log_record)
@@ -1,52 +1,52 b''
1
1
2
2
3 import logging
3 import logging
4
4
5 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
5 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
6 from .udp import StatsClient # noqa
6 from .udp import StatsClient # noqa
7
7
8 HOST = 'localhost'
8 HOST = 'localhost'
9 PORT = 8125
9 PORT = 8125
10 IPV6 = False
10 IPV6 = False
11 PREFIX = None
11 PREFIX = None
12 MAXUDPSIZE = 512
12 MAXUDPSIZE = 512
13
13
14 log = logging.getLogger('rhodecode.statsd')
14 log = logging.getLogger('rhodecode.statsd')
15
15
16
16
17 def statsd_config(config, prefix='statsd.'):
17 def statsd_config(config, prefix='statsd.'):
18 _config = {}
18 _config = {}
19 for key in config.keys():
19 for key in list(config.keys()):
20 if key.startswith(prefix):
20 if key.startswith(prefix):
21 _config[key[len(prefix):]] = config[key]
21 _config[key[len(prefix):]] = config[key]
22 return _config
22 return _config
23
23
24
24
25 def client_from_config(configuration, prefix='statsd.', **kwargs):
25 def client_from_config(configuration, prefix='statsd.', **kwargs):
26 from pyramid.settings import asbool
26 from pyramid.settings import asbool
27
27
28 _config = statsd_config(configuration, prefix)
28 _config = statsd_config(configuration, prefix)
29 statsd_enabled = asbool(_config.pop('enabled', False))
29 statsd_enabled = asbool(_config.pop('enabled', False))
30 if not statsd_enabled:
30 if not statsd_enabled:
31 log.debug('statsd client not enabled by statsd.enabled = flag, skipping...')
31 log.debug('statsd client not enabled by statsd.enabled = flag, skipping...')
32 return
32 return
33
33
34 host = _config.pop('statsd_host', HOST)
34 host = _config.pop('statsd_host', HOST)
35 port = _config.pop('statsd_port', PORT)
35 port = _config.pop('statsd_port', PORT)
36 prefix = _config.pop('statsd_prefix', PREFIX)
36 prefix = _config.pop('statsd_prefix', PREFIX)
37 maxudpsize = _config.pop('statsd_maxudpsize', MAXUDPSIZE)
37 maxudpsize = _config.pop('statsd_maxudpsize', MAXUDPSIZE)
38 ipv6 = asbool(_config.pop('statsd_ipv6', IPV6))
38 ipv6 = asbool(_config.pop('statsd_ipv6', IPV6))
39 log.debug('configured statsd client %s:%s', host, port)
39 log.debug('configured statsd client %s:%s', host, port)
40
40
41 try:
41 try:
42 client = StatsClient(
42 client = StatsClient(
43 host=host, port=port, prefix=prefix, maxudpsize=maxudpsize, ipv6=ipv6)
43 host=host, port=port, prefix=prefix, maxudpsize=maxudpsize, ipv6=ipv6)
44 except Exception:
44 except Exception:
45 log.exception('StatsD is enabled, but failed to connect to statsd server, fallback: disable statsd')
45 log.exception('StatsD is enabled, but failed to connect to statsd server, fallback: disable statsd')
46 client = None
46 client = None
47
47
48 return client
48 return client
49
49
50
50
51 def get_statsd_client(request):
51 def get_statsd_client(request):
52 return client_from_config(request.registry.settings)
52 return client_from_config(request.registry.settings)
General Comments 0
You need to be logged in to leave comments. Login now