##// END OF EJS Templates
core: synced vendor/ext_json with ce for better compatability
super-admin -
r1250:2c57bb5b default
parent child Browse files
Show More
@@ -1,243 +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
14
16
15 from ...logging_formatter import _inject_req_id, ExceptionAwareFormatter
17 def _inject_req_id(record, *args, **kwargs):
16 from ...ext_json import sjson as json
18 return record
19
20
21 ExceptionAwareFormatter = logging.Formatter
22
23
17
24 ZERO = timedelta(0)
18 ZERO = timedelta(0)
25 HOUR = timedelta(hours=1)
19 HOUR = timedelta(hours=1)
26
20
27
21
28 class UTC(tzinfo):
22 class UTC(tzinfo):
29 """UTC"""
23 """UTC"""
30
24
31 def utcoffset(self, dt):
25 def utcoffset(self, dt):
32 return ZERO
26 return ZERO
33
27
34 def tzname(self, dt):
28 def tzname(self, dt):
35 return "UTC"
29 return "UTC"
36
30
37 def dst(self, dt):
31 def dst(self, dt):
38 return ZERO
32 return ZERO
39
33
40 utc = UTC()
34 utc = UTC()
41
35
42
36
43 # skip natural LogRecord attributes
37 # skip natural LogRecord attributes
44 # http://docs.python.org/library/logging.html#logrecord-attributes
38 # http://docs.python.org/library/logging.html#logrecord-attributes
45 RESERVED_ATTRS = (
39 RESERVED_ATTRS = (
46 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
40 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
47 'funcName', 'levelname', 'levelno', 'lineno', 'module',
41 'funcName', 'levelname', 'levelno', 'lineno', 'module',
48 'msecs', 'message', 'msg', 'name', 'pathname', 'process',
42 'msecs', 'message', 'msg', 'name', 'pathname', 'process',
49 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')
43 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')
50
44
51
45
52 def merge_record_extra(record, target, reserved):
46 def merge_record_extra(record, target, reserved):
53 """
47 """
54 Merges extra attributes from LogRecord object into target dictionary
48 Merges extra attributes from LogRecord object into target dictionary
55
49
56 :param record: logging.LogRecord
50 :param record: logging.LogRecord
57 :param target: dict to update
51 :param target: dict to update
58 :param reserved: dict or list with reserved keys to skip
52 :param reserved: dict or list with reserved keys to skip
59 """
53 """
60 for key, value in record.__dict__.items():
54 for key, value in record.__dict__.items():
61 # this allows to have numeric keys
55 # this allows to have numeric keys
62 if (key not in reserved
56 if (key not in reserved
63 and not (hasattr(key, "startswith")
57 and not (hasattr(key, "startswith")
64 and key.startswith('_'))):
58 and key.startswith('_'))):
65 target[key] = value
59 target[key] = value
66 return target
60 return target
67
61
68
62
69 class JsonEncoder(json.JSONEncoder):
63 class JsonEncoder(json.JSONEncoder):
70 """
64 """
71 A custom encoder extending the default JSONEncoder
65 A custom encoder extending the default JSONEncoder
72 """
66 """
73
67
74 def default(self, obj):
68 def default(self, obj):
75 if isinstance(obj, (date, datetime, time)):
69 if isinstance(obj, (date, datetime, time)):
76 return self.format_datetime_obj(obj)
70 return self.format_datetime_obj(obj)
77
71
78 elif istraceback(obj):
72 elif istraceback(obj):
79 return ''.join(traceback.format_tb(obj)).strip()
73 return ''.join(traceback.format_tb(obj)).strip()
80
74
81 elif type(obj) == Exception \
75 elif type(obj) == Exception \
82 or isinstance(obj, Exception) \
76 or isinstance(obj, Exception) \
83 or type(obj) == type:
77 or type(obj) == type:
84 return str(obj)
78 return str(obj)
85
79
86 try:
80 try:
87 return super().default(obj)
81 return super().default(obj)
88
82
89 except TypeError:
83 except TypeError:
90 try:
84 try:
91 return str(obj)
85 return str(obj)
92
86
93 except Exception:
87 except Exception:
94 return None
88 return None
95
89
96 def format_datetime_obj(self, obj):
90 def format_datetime_obj(self, obj):
97 return obj.isoformat()
91 return obj.isoformat()
98
92
99
93
100 class JsonFormatter(ExceptionAwareFormatter):
94 class JsonFormatter(ExceptionAwareFormatter):
101 """
95 """
102 A custom formatter to format logging records as json strings.
96 A custom formatter to format logging records as json strings.
103 Extra values will be formatted as str() if not supported by
97 Extra values will be formatted as str() if not supported by
104 json default encoder
98 json default encoder
105 """
99 """
106
100
107 def __init__(self, *args, **kwargs):
101 def __init__(self, *args, **kwargs):
108 """
102 """
109 :param json_default: a function for encoding non-standard objects
103 :param json_default: a function for encoding non-standard objects
110 as outlined in http://docs.python.org/2/library/json.html
104 as outlined in http://docs.python.org/2/library/json.html
111 :param json_encoder: optional custom encoder
105 :param json_encoder: optional custom encoder
112 :param json_serializer: a :meth:`json.dumps`-compatible callable
106 :param json_serializer: a :meth:`json.dumps`-compatible callable
113 that will be used to serialize the log record.
107 that will be used to serialize the log record.
114 :param json_indent: an optional :meth:`json.dumps`-compatible numeric value
108 :param json_indent: an optional :meth:`json.dumps`-compatible numeric value
115 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.
116 :param prefix: an optional string prefix added at the beginning of
110 :param prefix: an optional string prefix added at the beginning of
117 the formatted string
111 the formatted string
118 :param json_indent: indent parameter for json.dumps
112 :param json_indent: indent parameter for json.dumps
119 :param json_ensure_ascii: ensure_ascii parameter for json.dumps
113 :param json_ensure_ascii: ensure_ascii parameter for json.dumps
120 :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
121 outputting json log record. Defaults to all log record attributes:
115 outputting json log record. Defaults to all log record attributes:
122 http://docs.python.org/library/logging.html#logrecord-attributes
116 http://docs.python.org/library/logging.html#logrecord-attributes
123 :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
124 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
125 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
126 will be "timestamp". Defaults to False/off.
120 will be "timestamp". Defaults to False/off.
127 """
121 """
128 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))
129 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))
130 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))
131 self.json_indent = kwargs.pop("json_indent", None)
125 self.json_indent = kwargs.pop("json_indent", None)
132 self.json_ensure_ascii = kwargs.pop("json_ensure_ascii", True)
126 self.json_ensure_ascii = kwargs.pop("json_ensure_ascii", True)
133 self.prefix = kwargs.pop("prefix", "")
127 self.prefix = kwargs.pop("prefix", "")
134 reserved_attrs = kwargs.pop("reserved_attrs", RESERVED_ATTRS)
128 reserved_attrs = kwargs.pop("reserved_attrs", RESERVED_ATTRS)
135 self.reserved_attrs = dict(list(zip(reserved_attrs, reserved_attrs)))
129 self.reserved_attrs = dict(list(zip(reserved_attrs, reserved_attrs)))
136 self.timestamp = kwargs.pop("timestamp", True)
130 self.timestamp = kwargs.pop("timestamp", True)
137
131
138 # super(JsonFormatter, self).__init__(*args, **kwargs)
132 # super(JsonFormatter, self).__init__(*args, **kwargs)
139 logging.Formatter.__init__(self, *args, **kwargs)
133 logging.Formatter.__init__(self, *args, **kwargs)
140 if not self.json_encoder and not self.json_default:
134 if not self.json_encoder and not self.json_default:
141 self.json_encoder = JsonEncoder
135 self.json_encoder = JsonEncoder
142
136
143 self._required_fields = self.parse()
137 self._required_fields = self.parse()
144 self._skip_fields = dict(list(zip(self._required_fields,
138 self._skip_fields = dict(list(zip(self._required_fields,
145 self._required_fields)))
139 self._required_fields)))
146 self._skip_fields.update(self.reserved_attrs)
140 self._skip_fields.update(self.reserved_attrs)
147
141
148 def _str_to_fn(self, fn_as_str):
142 def _str_to_fn(self, fn_as_str):
149 """
143 """
150 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.
151 Parses a string such as package.module.function, imports the module
145 Parses a string such as package.module.function, imports the module
152 and returns the function.
146 and returns the function.
153
147
154 :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.
155 """
149 """
156 if not isinstance(fn_as_str, str):
150 if not isinstance(fn_as_str, str):
157 return fn_as_str
151 return fn_as_str
158
152
159 path, _, function = fn_as_str.rpartition('.')
153 path, _, function = fn_as_str.rpartition('.')
160 module = importlib.import_module(path)
154 module = importlib.import_module(path)
161 return getattr(module, function)
155 return getattr(module, function)
162
156
163 def parse(self):
157 def parse(self):
164 """
158 """
165 Parses format string looking for substitutions
159 Parses format string looking for substitutions
166
160
167 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)
168 to include in all log messages.
162 to include in all log messages.
169 """
163 """
170 standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
164 standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
171 return standard_formatters.findall(self._fmt)
165 return standard_formatters.findall(self._fmt)
172
166
173 def add_fields(self, log_record, record, message_dict):
167 def add_fields(self, log_record, record, message_dict):
174 """
168 """
175 Override this method to implement custom logic for adding fields.
169 Override this method to implement custom logic for adding fields.
176 """
170 """
177 for field in self._required_fields:
171 for field in self._required_fields:
178 log_record[field] = record.__dict__.get(field)
172 log_record[field] = record.__dict__.get(field)
179 log_record.update(message_dict)
173 log_record.update(message_dict)
180 merge_record_extra(record, log_record, reserved=self._skip_fields)
174 merge_record_extra(record, log_record, reserved=self._skip_fields)
181
175
182 if self.timestamp:
176 if self.timestamp:
183 key = self.timestamp if type(self.timestamp) == str else 'timestamp'
177 key = self.timestamp if type(self.timestamp) == str else 'timestamp'
184 log_record[key] = datetime.fromtimestamp(record.created, tz=utc)
178 log_record[key] = datetime.fromtimestamp(record.created, tz=utc)
185
179
186 def process_log_record(self, log_record):
180 def process_log_record(self, log_record):
187 """
181 """
188 Override this method to implement custom logic
182 Override this method to implement custom logic
189 on the possibly ordered dictionary.
183 on the possibly ordered dictionary.
190 """
184 """
191 return log_record
185 return log_record
192
186
193 def jsonify_log_record(self, log_record):
187 def jsonify_log_record(self, log_record):
194 """Returns a json string of the log record."""
188 """Returns a json string of the log record."""
195 return self.json_serializer(log_record,
189 return self.json_serializer(log_record,
196 default=self.json_default,
190 default=self.json_default,
197 cls=self.json_encoder,
191 cls=self.json_encoder,
198 indent=self.json_indent,
192 indent=self.json_indent,
199 ensure_ascii=self.json_ensure_ascii)
193 ensure_ascii=self.json_ensure_ascii)
200
194
201 def serialize_log_record(self, log_record):
195 def serialize_log_record(self, log_record):
202 """Returns the final representation of the log record."""
196 """Returns the final representation of the log record."""
203 return "{}{}".format(self.prefix, self.jsonify_log_record(log_record))
197 return "{}{}".format(self.prefix, self.jsonify_log_record(log_record))
204
198
205 def format(self, record):
199 def format(self, record):
206 """Formats a log record and serializes to json"""
200 """Formats a log record and serializes to json"""
207 message_dict = {}
201 message_dict = {}
208 # FIXME: logging.LogRecord.msg and logging.LogRecord.message in typeshed
202 # FIXME: logging.LogRecord.msg and logging.LogRecord.message in typeshed
209 # 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.
210 if isinstance(record.msg, dict):
204 if isinstance(record.msg, dict):
211 message_dict = record.msg
205 message_dict = record.msg
212 record.message = None
206 record.message = None
213 else:
207 else:
214 record.message = record.getMessage()
208 record.message = record.getMessage()
215 # only format time if needed
209 # only format time if needed
216 if "asctime" in self._required_fields:
210 if "asctime" in self._required_fields:
217 record.asctime = self.formatTime(record, self.datefmt)
211 record.asctime = self.formatTime(record, self.datefmt)
218
212
219 # Display formatted exception, but allow overriding it in the
213 # Display formatted exception, but allow overriding it in the
220 # user-supplied dict.
214 # user-supplied dict.
221 if record.exc_info and not message_dict.get('exc_info'):
215 if record.exc_info and not message_dict.get('exc_info'):
222 message_dict['exc_info'] = self.formatException(record.exc_info)
216 message_dict['exc_info'] = self.formatException(record.exc_info)
223 if not message_dict.get('exc_info') and record.exc_text:
217 if not message_dict.get('exc_info') and record.exc_text:
224 message_dict['exc_info'] = record.exc_text
218 message_dict['exc_info'] = record.exc_text
225 # Display formatted record of stack frames
219 # Display formatted record of stack frames
226 # default format is a string returned from :func:`traceback.print_stack`
220 # default format is a string returned from :func:`traceback.print_stack`
227 try:
221 try:
228 if record.stack_info and not message_dict.get('stack_info'):
222 if record.stack_info and not message_dict.get('stack_info'):
229 message_dict['stack_info'] = self.formatStack(record.stack_info)
223 message_dict['stack_info'] = self.formatStack(record.stack_info)
230 except AttributeError:
224 except AttributeError:
231 # Python2.7 doesn't have stack_info.
225 # Python2.7 doesn't have stack_info.
232 pass
226 pass
233
227
234 try:
228 try:
235 log_record = OrderedDict()
229 log_record = OrderedDict()
236 except NameError:
230 except NameError:
237 log_record = {}
231 log_record = {}
238
232
239 _inject_req_id(record, with_prefix=False)
233 _inject_req_id(record, with_prefix=False)
240 self.add_fields(log_record, record, message_dict)
234 self.add_fields(log_record, record, message_dict)
241 log_record = self.process_log_record(log_record)
235 log_record = self.process_log_record(log_record)
242
236
243 return self.serialize_log_record(log_record)
237 return self.serialize_log_record(log_record)
@@ -1,394 +1,402 b''
1
1
2 import threading
2 import threading
3 import weakref
3 import weakref
4 from base64 import b64encode
4 from base64 import b64encode
5 from logging import getLogger
5 from logging import getLogger
6 from os import urandom
6 from os import urandom
7 from typing import Union
7 from typing import Union
8
8
9 from redis import StrictRedis
9 from redis import StrictRedis
10
10
11 __version__ = '4.0.0'
11 __version__ = '4.0.0'
12
12
13 loggers = {
13 loggers = {
14 k: getLogger("vcsserver." + ".".join((__name__, k)))
14 k: getLogger("vcsserver." + ".".join((__name__, k)))
15 for k in [
15 for k in [
16 "acquire",
16 "acquire",
17 "refresh.thread.start",
17 "refresh.thread.start",
18 "refresh.thread.stop",
18 "refresh.thread.stop",
19 "refresh.thread.exit",
19 "refresh.thread.exit",
20 "refresh.start",
20 "refresh.start",
21 "refresh.shutdown",
21 "refresh.shutdown",
22 "refresh.exit",
22 "refresh.exit",
23 "release",
23 "release",
24 ]
24 ]
25 }
25 }
26
26
27 text_type = str
27 text_type = str
28 binary_type = bytes
28 binary_type = bytes
29
29
30
30
31 # Check if the id match. If not, return an error code.
31 # Check if the id match. If not, return an error code.
32 UNLOCK_SCRIPT = b"""
32 UNLOCK_SCRIPT = b"""
33 if redis.call("get", KEYS[1]) ~= ARGV[1] then
33 if redis.call("get", KEYS[1]) ~= ARGV[1] then
34 return 1
34 return 1
35 else
35 else
36 redis.call("del", KEYS[2])
36 redis.call("del", KEYS[2])
37 redis.call("lpush", KEYS[2], 1)
37 redis.call("lpush", KEYS[2], 1)
38 redis.call("pexpire", KEYS[2], ARGV[2])
38 redis.call("pexpire", KEYS[2], ARGV[2])
39 redis.call("del", KEYS[1])
39 redis.call("del", KEYS[1])
40 return 0
40 return 0
41 end
41 end
42 """
42 """
43
43
44 # Covers both cases when key doesn't exist and doesn't equal to lock's id
44 # Covers both cases when key doesn't exist and doesn't equal to lock's id
45 EXTEND_SCRIPT = b"""
45 EXTEND_SCRIPT = b"""
46 if redis.call("get", KEYS[1]) ~= ARGV[1] then
46 if redis.call("get", KEYS[1]) ~= ARGV[1] then
47 return 1
47 return 1
48 elseif redis.call("ttl", KEYS[1]) < 0 then
48 elseif redis.call("ttl", KEYS[1]) < 0 then
49 return 2
49 return 2
50 else
50 else
51 redis.call("expire", KEYS[1], ARGV[2])
51 redis.call("expire", KEYS[1], ARGV[2])
52 return 0
52 return 0
53 end
53 end
54 """
54 """
55
55
56 RESET_SCRIPT = b"""
56 RESET_SCRIPT = b"""
57 redis.call('del', KEYS[2])
57 redis.call('del', KEYS[2])
58 redis.call('lpush', KEYS[2], 1)
58 redis.call('lpush', KEYS[2], 1)
59 redis.call('pexpire', KEYS[2], ARGV[2])
59 redis.call('pexpire', KEYS[2], ARGV[2])
60 return redis.call('del', KEYS[1])
60 return redis.call('del', KEYS[1])
61 """
61 """
62
62
63 RESET_ALL_SCRIPT = b"""
63 RESET_ALL_SCRIPT = b"""
64 local locks = redis.call('keys', 'lock:*')
64 local locks = redis.call('keys', 'lock:*')
65 local signal
65 local signal
66 for _, lock in pairs(locks) do
66 for _, lock in pairs(locks) do
67 signal = 'lock-signal:' .. string.sub(lock, 6)
67 signal = 'lock-signal:' .. string.sub(lock, 6)
68 redis.call('del', signal)
68 redis.call('del', signal)
69 redis.call('lpush', signal, 1)
69 redis.call('lpush', signal, 1)
70 redis.call('expire', signal, 1)
70 redis.call('expire', signal, 1)
71 redis.call('del', lock)
71 redis.call('del', lock)
72 end
72 end
73 return #locks
73 return #locks
74 """
74 """
75
75
76
76
77 class AlreadyAcquired(RuntimeError):
77 class AlreadyAcquired(RuntimeError):
78 pass
78 pass
79
79
80
80
81 class NotAcquired(RuntimeError):
81 class NotAcquired(RuntimeError):
82 pass
82 pass
83
83
84
84
85 class AlreadyStarted(RuntimeError):
85 class AlreadyStarted(RuntimeError):
86 pass
86 pass
87
87
88
88
89 class TimeoutNotUsable(RuntimeError):
89 class TimeoutNotUsable(RuntimeError):
90 pass
90 pass
91
91
92
92
93 class InvalidTimeout(RuntimeError):
93 class InvalidTimeout(RuntimeError):
94 pass
94 pass
95
95
96
96
97 class TimeoutTooLarge(RuntimeError):
97 class TimeoutTooLarge(RuntimeError):
98 pass
98 pass
99
99
100
100
101 class NotExpirable(RuntimeError):
101 class NotExpirable(RuntimeError):
102 pass
102 pass
103
103
104
104
105 class Lock:
105 class Lock:
106 """
106 """
107 A Lock context manager implemented via redis SETNX/BLPOP.
107 A Lock context manager implemented via redis SETNX/BLPOP.
108 """
108 """
109
109
110 unlock_script = None
110 unlock_script = None
111 extend_script = None
111 extend_script = None
112 reset_script = None
112 reset_script = None
113 reset_all_script = None
113 reset_all_script = None
114 blocking = None
114
115
115 _lock_renewal_interval: float
116 _lock_renewal_interval: float
116 _lock_renewal_thread: Union[threading.Thread, None]
117 _lock_renewal_thread: Union[threading.Thread, None]
117
118
118 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000, blocking=True):
119 """
120 """
120 :param redis_client:
121 :param redis_client:
121 An instance of :class:`~StrictRedis`.
122 An instance of :class:`~StrictRedis`.
122 :param name:
123 :param name:
123 The name (redis key) the lock should have.
124 The name (redis key) the lock should have.
124 :param expire:
125 :param expire:
125 The lock expiry time in seconds. If left at the default (None)
126 The lock expiry time in seconds. If left at the default (None)
126 the lock will not expire.
127 the lock will not expire.
127 :param id:
128 :param id:
128 The ID (redis value) the lock should have. A random value is
129 The ID (redis value) the lock should have. A random value is
129 generated when left at the default.
130 generated when left at the default.
130
131
131 Note that if you specify this then the lock is marked as "held". Acquires
132 Note that if you specify this then the lock is marked as "held". Acquires
132 won't be possible.
133 won't be possible.
133 :param auto_renewal:
134 :param auto_renewal:
134 If set to ``True``, Lock will automatically renew the lock so that it
135 If set to ``True``, Lock will automatically renew the lock so that it
135 doesn't expire for as long as the lock is held (acquire() called
136 doesn't expire for as long as the lock is held (acquire() called
136 or running in a context manager).
137 or running in a context manager).
137
138
138 Implementation note: Renewal will happen using a daemon thread with
139 Implementation note: Renewal will happen using a daemon thread with
139 an interval of ``expire*2/3``. If wishing to use a different renewal
140 an interval of ``expire*2/3``. If wishing to use a different renewal
140 time, subclass Lock, call ``super().__init__()`` then set
141 time, subclass Lock, call ``super().__init__()`` then set
141 ``self._lock_renewal_interval`` to your desired interval.
142 ``self._lock_renewal_interval`` to your desired interval.
142 :param strict:
143 :param strict:
143 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
144 :param signal_expire:
145 :param signal_expire:
145 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 :param blocking:
148 Boolean value specifying whether lock should be blocking or not.
149 Used in `__enter__` method.
146 """
150 """
147 if strict and not isinstance(redis_client, StrictRedis):
151 if strict and not isinstance(redis_client, StrictRedis):
148 raise ValueError("redis_client must be instance of StrictRedis. "
152 raise ValueError("redis_client must be instance of StrictRedis. "
149 "Use strict=False if you know what you're doing.")
153 "Use strict=False if you know what you're doing.")
150 if auto_renewal and expire is None:
154 if auto_renewal and expire is None:
151 raise ValueError("Expire may not be None when auto_renewal is set")
155 raise ValueError("Expire may not be None when auto_renewal is set")
152
156
153 self._client = redis_client
157 self._client = redis_client
154
158
155 if expire:
159 if expire:
156 expire = int(expire)
160 expire = int(expire)
157 if expire < 0:
161 if expire < 0:
158 raise ValueError("A negative expire is not acceptable.")
162 raise ValueError("A negative expire is not acceptable.")
159 else:
163 else:
160 expire = None
164 expire = None
161 self._expire = expire
165 self._expire = expire
162
166
163 self._signal_expire = signal_expire
167 self._signal_expire = signal_expire
164 if id is None:
168 if id is None:
165 self._id = b64encode(urandom(18)).decode('ascii')
169 self._id = b64encode(urandom(18)).decode('ascii')
166 elif isinstance(id, binary_type):
170 elif isinstance(id, binary_type):
167 try:
171 try:
168 self._id = id.decode('ascii')
172 self._id = id.decode('ascii')
169 except UnicodeDecodeError:
173 except UnicodeDecodeError:
170 self._id = b64encode(id).decode('ascii')
174 self._id = b64encode(id).decode('ascii')
171 elif isinstance(id, text_type):
175 elif isinstance(id, text_type):
172 self._id = id
176 self._id = id
173 else:
177 else:
174 raise TypeError(f"Incorrect type for `id`. Must be bytes/str not {type(id)}.")
178 raise TypeError(f"Incorrect type for `id`. Must be bytes/str not {type(id)}.")
175 self._name = 'lock:' + name
179 self._name = 'lock:' + name
176 self._signal = 'lock-signal:' + name
180 self._signal = 'lock-signal:' + name
177 self._lock_renewal_interval = (float(expire) * 2 / 3
181 self._lock_renewal_interval = (float(expire) * 2 / 3
178 if auto_renewal
182 if auto_renewal
179 else None)
183 else None)
180 self._lock_renewal_thread = None
184 self._lock_renewal_thread = None
181
185
186 self.blocking = blocking
187
182 self.register_scripts(redis_client)
188 self.register_scripts(redis_client)
183
189
184 @classmethod
190 @classmethod
185 def register_scripts(cls, redis_client):
191 def register_scripts(cls, redis_client):
186 global reset_all_script
192 global reset_all_script
187 if reset_all_script is None:
193 if reset_all_script is None:
188 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
194 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
189 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
195 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
190 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
196 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
191 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
197 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
192 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
198 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
193
199
194 @property
200 @property
195 def _held(self):
201 def _held(self):
196 return self.id == self.get_owner_id()
202 return self.id == self.get_owner_id()
197
203
198 def reset(self):
204 def reset(self):
199 """
205 """
200 Forcibly deletes the lock. Use this with care.
206 Forcibly deletes the lock. Use this with care.
201 """
207 """
202 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
208 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
203
209
204 @property
210 @property
205 def id(self):
211 def id(self):
206 return self._id
212 return self._id
207
213
208 def get_owner_id(self):
214 def get_owner_id(self):
209 owner_id = self._client.get(self._name)
215 owner_id = self._client.get(self._name)
210 if isinstance(owner_id, binary_type):
216 if isinstance(owner_id, binary_type):
211 owner_id = owner_id.decode('ascii', 'replace')
217 owner_id = owner_id.decode('ascii', 'replace')
212 return owner_id
218 return owner_id
213
219
214 def acquire(self, blocking=True, timeout=None):
220 def acquire(self, blocking=True, timeout=None):
215 """
221 """
216 :param blocking:
222 :param blocking:
217 Boolean value specifying whether lock should be blocking or not.
223 Boolean value specifying whether lock should be blocking or not.
218 :param timeout:
224 :param timeout:
219 An integer value specifying the maximum number of seconds to block.
225 An integer value specifying the maximum number of seconds to block.
220 """
226 """
221 logger = loggers["acquire"]
227 logger = loggers["acquire"]
222
228
223 logger.debug("Getting blocking: %s acquire on %r ...", blocking, self._name)
229 logger.debug("Getting blocking: %s acquire on %r ...", blocking, self._name)
224
230
225 if self._held:
231 if self._held:
226 owner_id = self.get_owner_id()
232 owner_id = self.get_owner_id()
227 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
233 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
228
234
229 if not blocking and timeout is not None:
235 if not blocking and timeout is not None:
230 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
236 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
231
237
232 if timeout:
238 if timeout:
233 timeout = int(timeout)
239 timeout = int(timeout)
234 if timeout < 0:
240 if timeout < 0:
235 raise InvalidTimeout(f"Timeout ({timeout}) cannot be less than or equal to 0")
241 raise InvalidTimeout(f"Timeout ({timeout}) cannot be less than or equal to 0")
236
242
237 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
243 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
238 raise TimeoutTooLarge(f"Timeout ({timeout}) cannot be greater than expire ({self._expire})")
244 raise TimeoutTooLarge(f"Timeout ({timeout}) cannot be greater than expire ({self._expire})")
239
245
240 busy = True
246 busy = True
241 blpop_timeout = timeout or self._expire or 0
247 blpop_timeout = timeout or self._expire or 0
242 timed_out = False
248 timed_out = False
243 while busy:
249 while busy:
244 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
250 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
245 if busy:
251 if busy:
246 if timed_out:
252 if timed_out:
247 return False
253 return False
248 elif blocking:
254 elif blocking:
249 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
255 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
250 else:
256 else:
251 logger.warning("Failed to acquire Lock(%r).", self._name)
257 logger.warning("Failed to acquire Lock(%r).", self._name)
252 return False
258 return False
253
259
254 logger.debug("Acquired Lock(%r).", self._name)
260 logger.debug("Acquired Lock(%r).", self._name)
255 if self._lock_renewal_interval is not None:
261 if self._lock_renewal_interval is not None:
256 self._start_lock_renewer()
262 self._start_lock_renewer()
257 return True
263 return True
258
264
259 def extend(self, expire=None):
265 def extend(self, expire=None):
260 """
266 """
261 Extends expiration time of the lock.
267 Extends expiration time of the lock.
262
268
263 :param expire:
269 :param expire:
264 New expiration time. If ``None`` - `expire` provided during
270 New expiration time. If ``None`` - `expire` provided during
265 lock initialization will be taken.
271 lock initialization will be taken.
266 """
272 """
267 if expire:
273 if expire:
268 expire = int(expire)
274 expire = int(expire)
269 if expire < 0:
275 if expire < 0:
270 raise ValueError("A negative expire is not acceptable.")
276 raise ValueError("A negative expire is not acceptable.")
271 elif self._expire is not None:
277 elif self._expire is not None:
272 expire = self._expire
278 expire = self._expire
273 else:
279 else:
274 raise TypeError(
280 raise TypeError(
275 "To extend a lock 'expire' must be provided as an "
281 "To extend a lock 'expire' must be provided as an "
276 "argument to extend() method or at initialization time."
282 "argument to extend() method or at initialization time."
277 )
283 )
278
284
279 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
285 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
280 if error == 1:
286 if error == 1:
281 raise NotAcquired(f"Lock {self._name} is not acquired or it already expired.")
287 raise NotAcquired(f"Lock {self._name} is not acquired or it already expired.")
282 elif error == 2:
288 elif error == 2:
283 raise NotExpirable(f"Lock {self._name} has no assigned expiration time")
289 raise NotExpirable(f"Lock {self._name} has no assigned expiration time")
284 elif error:
290 elif error:
285 raise RuntimeError(f"Unsupported error code {error} from EXTEND script")
291 raise RuntimeError(f"Unsupported error code {error} from EXTEND script")
286
292
287 @staticmethod
293 @staticmethod
288 def _lock_renewer(name, lockref, interval, stop):
294 def _lock_renewer(name, lockref, interval, stop):
289 """
295 """
290 Renew the lock key in redis every `interval` seconds for as long
296 Renew the lock key in redis every `interval` seconds for as long
291 as `self._lock_renewal_thread.should_exit` is False.
297 as `self._lock_renewal_thread.should_exit` is False.
292 """
298 """
293 while not stop.wait(timeout=interval):
299 while not stop.wait(timeout=interval):
294 loggers["refresh.thread.start"].debug("Refreshing Lock(%r).", name)
300 loggers["refresh.thread.start"].debug("Refreshing Lock(%r).", name)
295 lock: "Lock" = lockref()
301 lock: "Lock" = lockref()
296 if lock is None:
302 if lock is None:
297 loggers["refresh.thread.stop"].debug(
303 loggers["refresh.thread.stop"].debug(
298 "Stopping loop because Lock(%r) was garbage collected.", name
304 "Stopping loop because Lock(%r) was garbage collected.", name
299 )
305 )
300 break
306 break
301 lock.extend(expire=lock._expire)
307 lock.extend(expire=lock._expire)
302 del lock
308 del lock
303 loggers["refresh.thread.exit"].debug("Exiting renewal thread for Lock(%r).", name)
309 loggers["refresh.thread.exit"].debug("Exiting renewal thread for Lock(%r).", name)
304
310
305 def _start_lock_renewer(self):
311 def _start_lock_renewer(self):
306 """
312 """
307 Starts the lock refresher thread.
313 Starts the lock refresher thread.
308 """
314 """
309 if self._lock_renewal_thread is not None:
315 if self._lock_renewal_thread is not None:
310 raise AlreadyStarted("Lock refresh thread already started")
316 raise AlreadyStarted("Lock refresh thread already started")
311
317
312 loggers["refresh.start"].debug(
318 loggers["refresh.start"].debug(
313 "Starting renewal thread for Lock(%r). Refresh interval: %s seconds.",
319 "Starting renewal thread for Lock(%r). Refresh interval: %s seconds.",
314 self._name, self._lock_renewal_interval
320 self._name, self._lock_renewal_interval
315 )
321 )
316 self._lock_renewal_stop = threading.Event()
322 self._lock_renewal_stop = threading.Event()
317 self._lock_renewal_thread = threading.Thread(
323 self._lock_renewal_thread = threading.Thread(
318 group=None,
324 group=None,
319 target=self._lock_renewer,
325 target=self._lock_renewer,
320 kwargs={
326 kwargs={
321 'name': self._name,
327 'name': self._name,
322 'lockref': weakref.ref(self),
328 'lockref': weakref.ref(self),
323 'interval': self._lock_renewal_interval,
329 'interval': self._lock_renewal_interval,
324 'stop': self._lock_renewal_stop,
330 'stop': self._lock_renewal_stop,
325 },
331 },
326 )
332 )
327 self._lock_renewal_thread.daemon = True
333 self._lock_renewal_thread.daemon = True
328 self._lock_renewal_thread.start()
334 self._lock_renewal_thread.start()
329
335
330 def _stop_lock_renewer(self):
336 def _stop_lock_renewer(self):
331 """
337 """
332 Stop the lock renewer.
338 Stop the lock renewer.
333
339
334 This signals the renewal thread and waits for its exit.
340 This signals the renewal thread and waits for its exit.
335 """
341 """
336 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
342 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
337 return
343 return
338 loggers["refresh.shutdown"].debug("Signaling renewal thread for Lock(%r) to exit.", self._name)
344 loggers["refresh.shutdown"].debug("Signaling renewal thread for Lock(%r) to exit.", self._name)
339 self._lock_renewal_stop.set()
345 self._lock_renewal_stop.set()
340 self._lock_renewal_thread.join()
346 self._lock_renewal_thread.join()
341 self._lock_renewal_thread = None
347 self._lock_renewal_thread = None
342 loggers["refresh.exit"].debug("Renewal thread for Lock(%r) exited.", self._name)
348 loggers["refresh.exit"].debug("Renewal thread for Lock(%r) exited.", self._name)
343
349
344 def __enter__(self):
350 def __enter__(self):
345 acquired = self.acquire(blocking=True)
351 acquired = self.acquire(blocking=self.blocking)
346 if not acquired:
352 if not acquired:
347 raise AssertionError(f"Lock({self._name}) wasn't acquired, but blocking=True was used!")
353 if self.blocking:
354 raise AssertionError(f"Lock({self._name}) wasn't acquired, but blocking=True was used!")
355 raise NotAcquired(f"Lock({self._name}) is not acquired or it already expired.")
348 return self
356 return self
349
357
350 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
358 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
351 self.release()
359 self.release()
352
360
353 def release(self):
361 def release(self):
354 """Releases the lock, that was acquired with the same object.
362 """Releases the lock, that was acquired with the same object.
355
363
356 .. note::
364 .. note::
357
365
358 If you want to release a lock that you acquired in a different place you have two choices:
366 If you want to release a lock that you acquired in a different place you have two choices:
359
367
360 * Use ``Lock("name", id=id_from_other_place).release()``
368 * Use ``Lock("name", id=id_from_other_place).release()``
361 * Use ``Lock("name").reset()``
369 * Use ``Lock("name").reset()``
362 """
370 """
363 if self._lock_renewal_thread is not None:
371 if self._lock_renewal_thread is not None:
364 self._stop_lock_renewer()
372 self._stop_lock_renewer()
365 loggers["release"].debug("Releasing Lock(%r).", self._name)
373 loggers["release"].debug("Releasing Lock(%r).", self._name)
366 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
374 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
367 if error == 1:
375 if error == 1:
368 raise NotAcquired(f"Lock({self._name}) is not acquired or it already expired.")
376 raise NotAcquired(f"Lock({self._name}) is not acquired or it already expired.")
369 elif error:
377 elif error:
370 raise RuntimeError(f"Unsupported error code {error} from EXTEND script.")
378 raise RuntimeError(f"Unsupported error code {error} from EXTEND script.")
371
379
372 def locked(self):
380 def locked(self):
373 """
381 """
374 Return true if the lock is acquired.
382 Return true if the lock is acquired.
375
383
376 Checks that lock with same name already exists. This method returns true, even if
384 Checks that lock with same name already exists. This method returns true, even if
377 lock have another id.
385 lock have another id.
378 """
386 """
379 return self._client.exists(self._name) == 1
387 return self._client.exists(self._name) == 1
380
388
381
389
382 reset_all_script = None
390 reset_all_script = None
383
391
384
392
385 def reset_all(redis_client):
393 def reset_all(redis_client):
386 """
394 """
387 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
395 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
388
396
389 :param redis_client:
397 :param redis_client:
390 An instance of :class:`~StrictRedis`.
398 An instance of :class:`~StrictRedis`.
391 """
399 """
392 Lock.register_scripts(redis_client)
400 Lock.register_scripts(redis_client)
393
401
394 reset_all_script(client=redis_client) # noqa
402 reset_all_script(client=redis_client) # noqa
@@ -1,50 +1,51 b''
1 import logging
1 import logging
2
2
3 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
3 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
4 from .udp import StatsClient # noqa
4 from .udp import StatsClient # noqa
5
5
6 HOST = 'localhost'
6 HOST = 'localhost'
7 PORT = 8125
7 PORT = 8125
8 IPV6 = False
8 IPV6 = False
9 PREFIX = None
9 PREFIX = None
10 MAXUDPSIZE = 512
10 MAXUDPSIZE = 512
11
11
12 log = logging.getLogger('rhodecode.statsd')
12 log = logging.getLogger('rhodecode.statsd')
13
13
14
14
15 def statsd_config(config, prefix='statsd.'):
15 def statsd_config(config, prefix='statsd.'):
16 _config = {}
16 _config = {}
17 for key in config.keys():
17 for key in list(config.keys()):
18 if key.startswith(prefix):
18 if key.startswith(prefix):
19 _config[key[len(prefix):]] = config[key]
19 _config[key[len(prefix):]] = config[key]
20 return _config
20 return _config
21
21
22
22
23 def client_from_config(configuration, prefix='statsd.', **kwargs):
23 def client_from_config(configuration, prefix='statsd.', **kwargs):
24 from pyramid.settings import asbool
24 from pyramid.settings import asbool
25
25
26 _config = statsd_config(configuration, prefix)
26 _config = statsd_config(configuration, prefix)
27 statsd_flag = _config.get('enabled')
27 statsd_enabled = asbool(_config.pop('enabled', False))
28 statsd_enabled = asbool(_config.pop('enabled', False))
28 if not statsd_enabled:
29 if not statsd_enabled:
29 log.debug('statsd client not enabled by statsd.enabled = flag, skipping...')
30 log.debug('statsd client not enabled by statsd.enabled = %s flag, skipping...', statsd_flag)
30 return
31 return
31
32
32 host = _config.pop('statsd_host', HOST)
33 host = _config.pop('statsd_host', HOST)
33 port = _config.pop('statsd_port', PORT)
34 port = _config.pop('statsd_port', PORT)
34 prefix = _config.pop('statsd_prefix', PREFIX)
35 prefix = _config.pop('statsd_prefix', PREFIX)
35 maxudpsize = _config.pop('statsd_maxudpsize', MAXUDPSIZE)
36 maxudpsize = _config.pop('statsd_maxudpsize', MAXUDPSIZE)
36 ipv6 = asbool(_config.pop('statsd_ipv6', IPV6))
37 ipv6 = asbool(_config.pop('statsd_ipv6', IPV6))
37 log.debug('configured statsd client %s:%s', host, port)
38 log.debug('configured statsd client %s:%s', host, port)
38
39
39 try:
40 try:
40 client = StatsClient(
41 client = StatsClient(
41 host=host, port=port, prefix=prefix, maxudpsize=maxudpsize, ipv6=ipv6)
42 host=host, port=port, prefix=prefix, maxudpsize=maxudpsize, ipv6=ipv6)
42 except Exception:
43 except Exception:
43 log.exception('StatsD is enabled, but failed to connect to statsd server, fallback: disable statsd')
44 log.exception('StatsD is enabled, but failed to connect to statsd server, fallback: disable statsd')
44 client = None
45 client = None
45
46
46 return client
47 return client
47
48
48
49
49 def get_statsd_client(request):
50 def get_statsd_client(request):
50 return client_from_config(request.registry.settings)
51 return client_from_config(request.registry.settings)
@@ -1,2 +1,14 b''
1 # use orjson by default
1 import json as stdlib_json
2 import orjson as json
2
3 try:
4 # we keep simplejson for having dump functionality still
5 # orjson doesn't support it
6 import simplejson as sjson
7 except ImportError:
8 sjson = stdlib_json
9
10 try:
11 import orjson
12 import orjson as json
13 except ImportError:
14 json = stdlib_json
@@ -1,53 +1,63 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import sys
19 import sys
20 import logging
20 import logging
21
21
22
22
23 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(30, 38))
23 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(30, 38))
24
24
25 # Sequences
25 # Sequences
26 RESET_SEQ = "\033[0m"
26 RESET_SEQ = "\033[0m"
27 COLOR_SEQ = "\033[0;%dm"
27 COLOR_SEQ = "\033[0;%dm"
28 BOLD_SEQ = "\033[1m"
28 BOLD_SEQ = "\033[1m"
29
29
30 COLORS = {
30 COLORS = {
31 'CRITICAL': MAGENTA,
31 'CRITICAL': MAGENTA,
32 'ERROR': RED,
32 'ERROR': RED,
33 'WARNING': CYAN,
33 'WARNING': CYAN,
34 'INFO': GREEN,
34 'INFO': GREEN,
35 'DEBUG': BLUE,
35 'DEBUG': BLUE,
36 'SQL': YELLOW
36 'SQL': YELLOW
37 }
37 }
38
38
39
39
40 def _inject_req_id(record, *args, **kwargs):
41 return record
42
43
44 class ExceptionAwareFormatter(logging.Formatter):
45 pass
46
47
40 class ColorFormatter(logging.Formatter):
48 class ColorFormatter(logging.Formatter):
41
49
42 def format(self, record):
50 def format(self, record):
43 """
51 """
44 Change record's levelname to use with COLORS enum
52 Changes record's levelname to use with COLORS enum
45 """
53 """
46 def_record = super().format(record)
54 def_record = super().format(record)
47
55
48 levelname = record.levelname
56 levelname = record.levelname
49 start = COLOR_SEQ % (COLORS[levelname])
57 start = COLOR_SEQ % (COLORS[levelname])
50 end = RESET_SEQ
58 end = RESET_SEQ
51
59
52 colored_record = ''.join([start, def_record, end])
60 colored_record = ''.join([start, def_record, end])
53 return colored_record
61 return colored_record
62
63
General Comments 0
You need to be logged in to leave comments. Login now