logger.py 10.5 KB
Newer Older
louiz’'s avatar
louiz’ committed
1
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
2
3
4
5
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7

8
9
10
11
12
"""
The logger module that handles logging of the poezio
conversations and roster changes
"""

13
import mmap
14
import os
15
import re
mathieui's avatar
mathieui committed
16
from os import makedirs
17
from datetime import datetime
18

mathieui's avatar
mathieui committed
19
20
21
22
from poezio import common
from poezio.config import config
from poezio.xhtml import clean_text
from poezio.theming import dump_tuple, get_theme
23

24
25
26
27
import logging

log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
28
from poezio.config import LOG_DIR as log_dir
29

mathieui's avatar
mathieui committed
30
MESSAGE_LOG_RE = re.compile(r'MR (\d{4})(\d{2})(\d{2})T'
31
32
                            r'(\d{2}):(\d{2}):(\d{2})Z '
                            r'(\d+) <([^ ]+)>  (.*)')
mathieui's avatar
mathieui committed
33
INFO_LOG_RE = re.compile(r'MI (\d{4})(\d{2})(\d{2})T'
34
35
                         r'(\d{2}):(\d{2}):(\d{2})Z '
                         r'(\d+) (.*)')
36

mathieui's avatar
mathieui committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class LogItem:
    def __init__(self, year, month, day, hour, minute, second, nb_lines, message):
        self.time = datetime(int(year), int(month), int(day), int(hour),
                             int(minute), int(second))
        self.nb_lines = int(nb_lines)
        self.text = message

class LogInfo(LogItem):
    def __init__(self, *args):
        LogItem.__init__(self, *args)

class LogMessage(LogItem):
    def __init__(self, year, month, day, hour, minute, seconds, nb_lines, nick, message):
        LogItem.__init__(self, year, month, day, hour, minute, seconds,
                         nb_lines, message)
        self.nick = nick

def parse_message_line(msg):
    if re.match(MESSAGE_LOG_RE, msg):
        return LogMessage(*[i for i in re.split(MESSAGE_LOG_RE, msg) if i])
    if re.match(INFO_LOG_RE, msg):
        return LogInfo(*[i for i in re.split(INFO_LOG_RE, msg) if i])
59
    return None
60

61
62
63
64
65
class Logger(object):
    """
    Appends things to files. Error/information/warning logs
    and also log the conversations to logfiles
    """
66
    def __init__(self):
67
        self._roster_logfile = None
68
        # a dict of 'groupchatname': file-object (opened)
69
        self._fds = {}
70

71
    def __del__(self):
72
        for opened_file in self._fds.values():
73
            if opened_file:
mathieui's avatar
mathieui committed
74
75
76
77
                try:
                    opened_file.close()
                except: # Can't close? too bad
                    pass
78

79
    def close(self, jid):
80
        jid = str(jid).replace('/', '\\')
81
82
83
84
85
        if jid in self._fds:
            self._fds[jid].close()
            log.debug('Log file for %s closed.', jid)
            del self._fds[jid]

86
87
    def reload_all(self):
        """Close and reload all the file handles (on SIGHUP)"""
88
        for opened_file in self._fds.values():
89
90
91
            if opened_file:
                opened_file.close()
        log.debug('All log file handles closed')
92
93
        for room in self._fds:
            self._fds[room] = self._check_and_create_log_dir(room)
94
            log.debug('Log handle for %s re-created', room)
95

96
    def _check_and_create_log_dir(self, room, open_fd=True):
97
        """
98
99
        Check that the directory where we want to log the messages
        exists. if not, create it
100
        """
101
        if not config.get_by_tabname('use_log', room):
mathieui's avatar
mathieui committed
102
            return
103
        try:
104
            makedirs(log_dir)
mathieui's avatar
mathieui committed
105
106
107
        except OSError as e:
            if e.errno != 17: # file exists
                log.error('Unable to create the log dir', exc_info=True)
108
109
        except:
            log.error('Unable to create the log dir', exc_info=True)
110
111
112
            return
        if not open_fd:
            return
113
        try:
114
            fd = open(os.path.join(log_dir, room), 'a')
115
            self._fds[room] = fd
116
            return fd
117
        except IOError:
118
119
120
            log.error('Unable to open the log file (%s)',
                    os.path.join(log_dir, room),
                    exc_info=True)
121

122
    def get_logs(self, jid, nb=10):
123
        """
124
125
126
127
        Get the nb last messages from the log history for the given jid.
        Note that a message may be more than one line in these files, so
        this function is a little bit more complicated than “read the last
        nb lines”.
128
        """
129
        if config.get_by_tabname('load_log', jid) <= 0:
130
131
            return

132
        if not config.get_by_tabname('use_log', jid):
133
134
            return

135
        if nb <= 0:
136
            return
137

138
        self._check_and_create_log_dir(jid, open_fd=False)
139

140
        try:
141
            fd = open(os.path.join(log_dir, jid), 'rb')
142
143
144
145
146
147
        except FileNotFoundError:
            log.info('Non-existing log file (%s)',
                     os.path.join(log_dir, jid),
                     exc_info=True)
            return
        except OSError:
148
            log.error('Unable to open the log file (%s)',
149
150
                      os.path.join(log_dir, jid),
                      exc_info=True)
151
152
153
            return
        if not fd:
            return
154

155
156
157
158
        # read the needed data from the file, we just search nb messages by
        # searching "\nM" nb times from the end of the file.  We use mmap to
        # do that efficiently, instead of seek()s and read()s which are costly.
        with fd:
159
160
161
162
163
164
165
            try:
                m = mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ)
            except Exception: # file probably empty
                log.error('Unable to mmap the log file for (%s)',
                        os.path.join(log_dir, jid),
                        exc_info=True)
                return
166
167
168
169
170
171
172
173
174
175
            pos = m.rfind(b"\nM") # start of messages begin with MI or MR,
                                  # after a \n
            # number of message found so far
            count = 0
            while pos != -1 and count < nb-1:
                count += 1
                pos = m.rfind(b"\nM", 0, pos)
            if pos == -1:       # If we don't have enough lines in the file
                pos = 1         # 1, because we do -1 just on the next line
                                # to get 0 (start of the file)
louiz’'s avatar
louiz’ committed
176
            lines = m[pos-1:].decode(errors='replace').splitlines()
177
178

        messages = []
179
        color = '\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG)
180

181
        # now convert that data into actual Message objects
182
183
184
185
186
187
        idx = 0
        while idx < len(lines):
            if lines[idx].startswith(' '): # should not happen ; skip
                idx += 1
                log.debug('fail?')
                continue
mathieui's avatar
mathieui committed
188
            log_item = parse_message_line(lines[idx])
189
            idx += 1
mathieui's avatar
mathieui committed
190
191
            if not isinstance(log_item, LogItem):
                log.debug('wrong log format? %s', tup)
192
                continue
193
194
            message = {'lines': [],
                       'history': True,
mathieui's avatar
mathieui committed
195
196
197
198
199
200
201
                       'time': common.get_local_time(log_item.time)}
            size = log_item.nb_lines
            if isinstance(log_item, LogInfo):
                message['lines'].append(color + log_item.text)
            elif isinstance(log_item, LogMessage):
                message['nickname'] = log_item.nick
                message['lines'].append(color + log_item.text)
202
203
204
205
206
207
208
209
210
            while size != 0 and idx < len(lines):
                message['lines'].append(lines[idx][1:])
                size -= 1
                idx += 1
            message['txt'] = '\n'.join(message['lines'])
            del message['lines']
            messages.append(message)

        return messages
211

212
    def log_message(self, jid, nick, msg, date=None, typ=1):
213
        """
214
        log the message in the appropriate jid's file
215
216
217
        type:
              0 = Don’t log
              1 = Message
218
              2 = Status/whatever
219
        """
220
221
222
        if not typ:
            return True

223
        jid = str(jid).replace('/', '\\')
224
        if not config.get_by_tabname('use_log', jid):
225
            return True
226
227
        if jid in self._fds.keys():
            fd = self._fds[jid]
228
        else:
229
            fd = self._check_and_create_log_dir(jid)
230
        if not fd:
mathieui's avatar
mathieui committed
231
            return True
232
        try:
mathieui's avatar
mathieui committed
233
            msg = clean_text(msg)
mathieui's avatar
mathieui committed
234
            if date is None:
235
                str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ')
mathieui's avatar
mathieui committed
236
            else:
237
                str_time = common.get_utc_time(date).strftime('%Y%m%dT%H:%M:%SZ')
238
239
240
241
            if typ == 1:
                prefix = 'MR'
            else:
                prefix = 'MI'
242
243
244
245
            lines = msg.split('\n')
            first_line = lines.pop(0)
            nb_lines = str(len(lines)).zfill(3)

246
            if nick:
247
                nick = '<' + nick + '>'
248
                fd.write(' '.join((prefix, str_time, nb_lines, nick, ' '+first_line, '\n')))
249
            else:
250
251
252
                fd.write(' '.join((prefix, str_time, nb_lines, first_line, '\n')))
            for line in lines:
                fd.write(' %s\n' % line)
mathieui's avatar
mathieui committed
253
        except:
254
255
256
            log.error('Unable to write in the log file (%s)',
                    os.path.join(log_dir, jid),
                    exc_info=True)
mathieui's avatar
mathieui committed
257
            return False
258
        else:
mathieui's avatar
mathieui committed
259
260
261
            try:
                fd.flush()          # TODO do something better here?
            except:
262
263
264
                log.error('Unable to flush the log file (%s)',
                        os.path.join(log_dir, jid),
                        exc_info=True)
mathieui's avatar
mathieui committed
265
266
                return False
        return True
267

mathieui's avatar
mathieui committed
268
    def log_roster_change(self, jid, message):
269
270
271
        """
        Log a roster change
        """
272
        if not config.get_by_tabname('use_log', jid):
273
            return True
274
275
        self._check_and_create_log_dir('', open_fd=False)
        if not self._roster_logfile:
mathieui's avatar
mathieui committed
276
            try:
277
                self._roster_logfile = open(os.path.join(log_dir, 'roster.log'), 'a')
mathieui's avatar
mathieui committed
278
            except IOError:
279
280
281
                log.error('Unable to create the log file (%s)',
                        os.path.join(log_dir, 'roster.log'),
                        exc_info=True)
mathieui's avatar
mathieui committed
282
283
                return False
        try:
284
            str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ')
285
            message = clean_text(message)
286
287
288
            lines = message.split('\n')
            first_line = lines.pop(0)
            nb_lines = str(len(lines)).zfill(3)
289
            self._roster_logfile.write('MI %s %s %s %s\n' % (str_time, nb_lines, jid, first_line))
290
            for line in lines:
291
292
                self._roster_logfile.write(' %s\n' % line)
            self._roster_logfile.flush()
mathieui's avatar
mathieui committed
293
        except:
294
295
296
            log.error('Unable to write in the log file (%s)',
                    os.path.join(log_dir, 'roster.log'),
                    exc_info=True)
mathieui's avatar
mathieui committed
297
298
            return False
        return True
mathieui's avatar
mathieui committed
299

300
301
302
303
304
305
def create_logger():
    "Create the global logger object"
    global logger
    logger = Logger()

logger = None