1"""
2    salt._logging.handlers
3    ~~~~~~~~~~~~~~~~~~~~~~
4
5    Salt's logging handlers
6"""
7
8
9import copy
10import logging
11import logging.handlers
12import queue
13import sys
14from collections import deque
15
16from salt._logging.mixins import ExcInfoOnLogLevelFormatMixin, NewStyleClassMixin
17
18# from salt.utils.versions import warn_until_date
19
20log = logging.getLogger(__name__)
21
22
23class TemporaryLoggingHandler(logging.NullHandler):
24    """
25    This logging handler will store all the log records up to its maximum
26    queue size at which stage the first messages stored will be dropped.
27
28    Should only be used as a temporary logging handler, while the logging
29    system is not fully configured.
30
31    Once configured, pass any logging handlers that should have received the
32    initial log messages to the function
33    :func:`TemporaryLoggingHandler.sync_with_handlers` and all stored log
34    records will be dispatched to the provided handlers.
35
36    .. versionadded:: 0.17.0
37    """
38
39    def __init__(self, level=logging.NOTSET, max_queue_size=10000):
40        # warn_until_date(
41        #    '20220101',
42        #    'Please stop using \'{name}.TemporaryLoggingHandler\'. '
43        #    '\'{name}.TemporaryLoggingHandler\' will go away after '
44        #    '{{date}}.'.format(name=__name__)
45        # )
46        self.__max_queue_size = max_queue_size
47        super().__init__(level=level)
48        self.__messages = deque(maxlen=max_queue_size)
49
50    def handle(self, record):
51        self.acquire()
52        self.__messages.append(record)
53        self.release()
54
55    def sync_with_handlers(self, handlers=()):
56        """
57        Sync the stored log records to the provided log handlers.
58        """
59        if not handlers:
60            return
61
62        while self.__messages:
63            record = self.__messages.popleft()
64            for handler in handlers:
65                if handler.level > record.levelno:
66                    # If the handler's level is higher than the log record one,
67                    # it should not handle the log record
68                    continue
69                handler.handle(record)
70
71
72class StreamHandler(
73    ExcInfoOnLogLevelFormatMixin, logging.StreamHandler, NewStyleClassMixin
74):
75    """
76    Stream handler which properly handles exc_info on a per handler basis
77    """
78
79
80class FileHandler(
81    ExcInfoOnLogLevelFormatMixin, logging.FileHandler, NewStyleClassMixin
82):
83    """
84    File handler which properly handles exc_info on a per handler basis
85    """
86
87
88class SysLogHandler(
89    ExcInfoOnLogLevelFormatMixin, logging.handlers.SysLogHandler, NewStyleClassMixin
90):
91    """
92    Syslog handler which properly handles exc_info on a per handler basis
93    """
94
95    def handleError(self, record):
96        """
97        Override the default error handling mechanism for py3
98        Deal with syslog os errors when the log file does not exist
99        """
100        handled = False
101        if sys.stderr and sys.version_info >= (3, 5, 4):
102            exc_type, exc, exc_traceback = sys.exc_info()
103            try:
104                if exc_type.__name__ in "FileNotFoundError":
105                    sys.stderr.write(
106                        "[WARNING ] The log_file does not exist. Logging not "
107                        "setup correctly or syslog service not started.\n"
108                    )
109                    handled = True
110            finally:
111                # 'del' recommended. See documentation of
112                # 'sys.exc_info()' for details.
113                del exc_type, exc, exc_traceback
114
115        if not handled:
116            super().handleError(record)
117
118
119class RotatingFileHandler(
120    ExcInfoOnLogLevelFormatMixin,
121    logging.handlers.RotatingFileHandler,
122    NewStyleClassMixin,
123):
124    """
125    Rotating file handler which properly handles exc_info on a per handler basis
126    """
127
128    def handleError(self, record):
129        """
130        Override the default error handling mechanism
131
132        Deal with log file rotation errors due to log file in use
133        more softly.
134        """
135        handled = False
136
137        # Can't use "salt.utils.platform.is_windows()" in this file
138        if (
139            sys.platform.startswith("win") and logging.raiseExceptions and sys.stderr
140        ):  # see Python issue 13807
141            exc_type, exc, exc_traceback = sys.exc_info()
142            try:
143                # PermissionError is used since Python 3.3.
144                # OSError is used for previous versions of Python.
145                if (
146                    exc_type.__name__ in ("PermissionError", "OSError")
147                    and exc.winerror == 32
148                ):
149                    if self.level <= logging.WARNING:
150                        sys.stderr.write(
151                            '[WARNING ] Unable to rotate the log file "{}" '
152                            "because it is in use\n".format(self.baseFilename)
153                        )
154                    handled = True
155            finally:
156                # 'del' recommended. See documentation of
157                # 'sys.exc_info()' for details.
158                del exc_type, exc, exc_traceback
159
160        if not handled:
161            super().handleError(record)
162
163
164class WatchedFileHandler(
165    ExcInfoOnLogLevelFormatMixin,
166    logging.handlers.WatchedFileHandler,
167    NewStyleClassMixin,
168):
169    """
170    Watched file handler which properly handles exc_info on a per handler basis
171    """
172
173
174if sys.version_info < (3, 2):
175
176    class QueueHandler(
177        ExcInfoOnLogLevelFormatMixin, logging.Handler, NewStyleClassMixin
178    ):
179        """
180        This handler sends events to a queue. Typically, it would be used together
181        with a multiprocessing Queue to centralise logging to file in one process
182        (in a multi-process application), so as to avoid file write contention
183        between processes.
184
185        This code is new in Python 3.2, but this class can be copy pasted into
186        user code for use with earlier Python versions.
187        """
188
189        def __init__(self, queue):
190            """
191            Initialise an instance, using the passed queue.
192            """
193            # warn_until_date(
194            #    '20220101',
195            #    'Please stop using \'{name}.QueueHandler\' and instead '
196            #    'use \'logging.handlers.QueueHandler\'. '
197            #    '\'{name}.QueueHandler\' will go away after '
198            #    '{{date}}.'.format(name=__name__)
199            # )
200            logging.Handler.__init__(self)
201            self.queue = queue
202
203        def enqueue(self, record):
204            """
205            Enqueue a record.
206
207            The base implementation uses put_nowait. You may want to override
208            this method if you want to use blocking, timeouts or custom queue
209            implementations.
210            """
211            try:
212                self.queue.put_nowait(record)
213            except queue.Full:
214                sys.stderr.write(
215                    "[WARNING ] Message queue is full, "
216                    'unable to write "{}" to log'.format(record)
217                )
218
219        def prepare(self, record):
220            """
221            Prepares a record for queuing. The object returned by this method is
222            enqueued.
223            The base implementation formats the record to merge the message
224            and arguments, and removes unpickleable items from the record
225            in-place.
226            You might want to override this method if you want to convert
227            the record to a dict or JSON string, or send a modified copy
228            of the record while leaving the original intact.
229            """
230            # The format operation gets traceback text into record.exc_text
231            # (if there's exception data), and also returns the formatted
232            # message. We can then use this to replace the original
233            # msg + args, as these might be unpickleable. We also zap the
234            # exc_info and exc_text attributes, as they are no longer
235            # needed and, if not None, will typically not be pickleable.
236            msg = self.format(record)
237            # bpo-35726: make copy of record to avoid affecting other handlers in the chain.
238            record = copy.copy(record)
239            record.message = msg
240            record.msg = msg
241            record.args = None
242            record.exc_info = None
243            record.exc_text = None
244            return record
245
246        def emit(self, record):
247            """
248            Emit a record.
249
250            Writes the LogRecord to the queue, preparing it for pickling first.
251            """
252            try:
253                self.enqueue(self.prepare(record))
254            except Exception:  # pylint: disable=broad-except
255                self.handleError(record)
256
257
258elif sys.version_info < (3, 7):
259    # On python versions lower than 3.7, we sill subclass and overwrite prepare to include the fix for:
260    #  https://bugs.python.org/issue35726
261    class QueueHandler(
262        ExcInfoOnLogLevelFormatMixin, logging.handlers.QueueHandler
263    ):  # pylint: disable=no-member,inconsistent-mro
264        def __init__(self, queue):  # pylint: disable=useless-super-delegation
265            super().__init__(queue)
266            # warn_until_date(
267            #    '20220101',
268            #    'Please stop using \'{name}.QueueHandler\' and instead '
269            #    'use \'logging.handlers.QueueHandler\'. '
270            #    '\'{name}.QueueHandler\' will go away after '
271            #    '{{date}}.'.format(name=__name__)
272            # )
273
274        def enqueue(self, record):
275            """
276            Enqueue a record.
277
278            The base implementation uses put_nowait. You may want to override
279            this method if you want to use blocking, timeouts or custom queue
280            implementations.
281            """
282            try:
283                self.queue.put_nowait(record)
284            except queue.Full:
285                sys.stderr.write(
286                    "[WARNING ] Message queue is full, "
287                    'unable to write "{}" to log.\n'.format(record)
288                )
289
290        def prepare(self, record):
291            """
292            Prepares a record for queuing. The object returned by this method is
293            enqueued.
294            The base implementation formats the record to merge the message
295            and arguments, and removes unpickleable items from the record
296            in-place.
297            You might want to override this method if you want to convert
298            the record to a dict or JSON string, or send a modified copy
299            of the record while leaving the original intact.
300            """
301            # The format operation gets traceback text into record.exc_text
302            # (if there's exception data), and also returns the formatted
303            # message. We can then use this to replace the original
304            # msg + args, as these might be unpickleable. We also zap the
305            # exc_info and exc_text attributes, as they are no longer
306            # needed and, if not None, will typically not be pickleable.
307            msg = self.format(record)
308            # bpo-35726: make copy of record to avoid affecting other handlers in the chain.
309            record = copy.copy(record)
310            record.message = msg
311            record.msg = msg
312            record.args = None
313            record.exc_info = None
314            record.exc_text = None
315            return record
316
317
318else:
319
320    class QueueHandler(
321        ExcInfoOnLogLevelFormatMixin, logging.handlers.QueueHandler
322    ):  # pylint: disable=no-member,inconsistent-mro
323        def __init__(self, queue):  # pylint: disable=useless-super-delegation
324            super().__init__(queue)
325            # warn_until_date(
326            #    '20220101',
327            #    'Please stop using \'{name}.QueueHandler\' and instead '
328            #    'use \'logging.handlers.QueueHandler\'. '
329            #    '\'{name}.QueueHandler\' will go away after '
330            #    '{{date}}.'.format(name=__name__)
331            # )
332
333        def enqueue(self, record):
334            """
335            Enqueue a record.
336
337            The base implementation uses put_nowait. You may want to override
338            this method if you want to use blocking, timeouts or custom queue
339            implementations.
340            """
341            try:
342                self.queue.put_nowait(record)
343            except queue.Full:
344                sys.stderr.write(
345                    "[WARNING ] Message queue is full, "
346                    'unable to write "{}" to log.\n'.format(record)
347                )
348