[IMP] logging: fixed DEBUG_RPC logging to be included at DEBUG_SQL level
[odoo/odoo.git] / bin / netsvc.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Management Solution
6 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
7 #    The refactoring about the OpenSSL support come from Tryton
8 #    Copyright (C) 2007-2009 Cédric Krier.
9 #    Copyright (C) 2007-2009 Bertrand Chenal.
10 #    Copyright (C) 2008 B2CK SPRL.
11 #
12 #    This program is free software: you can redistribute it and/or modify
13 #    it under the terms of the GNU General Public License as published by
14 #    the Free Software Foundation, either version 3 of the License, or
15 #    (at your option) any later version.
16 #
17 #    This program is distributed in the hope that it will be useful,
18 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
19 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 #    GNU General Public License for more details.
21 #
22 #    You should have received a copy of the GNU General Public License
23 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 #
25 ##############################################################################
26
27 import errno
28 import logging
29 import logging.handlers
30 import os
31 import socket
32 import sys
33 import threading
34 import time
35 import release
36 from pprint import pformat
37 import warnings
38
39 class Service(object):
40     """ Base class for *Local* services
41
42         Functionality here is trusted, no authentication.
43     """
44     _services = {}
45     def __init__(self, name, audience=''):
46         Service._services[name] = self
47         self.__name = name
48         self._methods = {}
49
50     def joinGroup(self, name):
51         raise Exception("No group for local services")
52         #GROUPS.setdefault(name, {})[self.__name] = self
53
54     @classmethod
55     def exists(cls, name):
56         return name in cls._services
57
58     @classmethod
59     def remove(cls, name):
60         if cls.exists(name):
61             cls._services.pop(name)
62
63     def exportMethod(self, method):
64         if callable(method):
65             self._methods[method.__name__] = method
66
67     def abortResponse(self, error, description, origin, details):
68         if not tools.config['debug_mode']:
69             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
70         else:
71             raise
72
73 class LocalService(object):
74     """ Proxy for local services. 
75     
76         Any instance of this class will behave like the single instance
77         of Service(name)
78     """
79     __logger = logging.getLogger('service')
80     def __init__(self, name):
81         self.__name = name
82         try:
83             self._service = Service._services[name]
84             for method_name, method_definition in self._service._methods.items():
85                 setattr(self, method_name, method_definition)
86         except KeyError, keyError:
87             self.__logger.error('This service does not exist: %s' % (str(keyError),) )
88             raise
89
90     def __call__(self, method, *params):
91         return getattr(self, method)(*params)
92
93 class ExportService(object):
94     """ Proxy for exported services. 
95
96     All methods here should take an AuthProxy as their first parameter. It
97     will be appended by the calling framework.
98
99     Note that this class has no direct proxy, capable of calling 
100     eservice.method(). Rather, the proxy should call 
101     dispatch(method,auth,params)
102     """
103     
104     _services = {}
105     _groups = {}
106     
107     def __init__(self, name, audience=''):
108         ExportService._services[name] = self
109         self.__name = name
110
111     def joinGroup(self, name):
112         ExportService._groups.setdefault(name, {})[self.__name] = self
113
114     @classmethod
115     def getService(cls,name):
116         return cls._services[name]
117
118     def dispatch(self, method, auth, params):
119         raise Exception("stub dispatch at %s" % self.__name)
120         
121     def new_dispatch(self,method,auth,params):
122         raise Exception("stub dispatch at %s" % self.__name)
123
124     def abortResponse(self, error, description, origin, details):
125         if not tools.config['debug_mode']:
126             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
127         else:
128             raise
129
130 LOG_NOTSET = 'notset'
131 LOG_DEBUG_SQL = 'debug_sql'
132 LOG_DEBUG_RPC = 'debug_rpc'
133 LOG_DEBUG = 'debug'
134 LOG_TEST = 'test'
135 LOG_INFO = 'info'
136 LOG_WARNING = 'warn'
137 LOG_ERROR = 'error'
138 LOG_CRITICAL = 'critical'
139
140 logging.DEBUG_RPC = logging.DEBUG - 2
141 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
142 logging.DEBUG_SQL = logging.DEBUG_RPC - 2
143 logging.addLevelName(logging.DEBUG_SQL, 'DEBUG_SQL')
144
145 logging.TEST = logging.INFO - 5
146 logging.addLevelName(logging.TEST, 'TEST')
147
148 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
149 #The background is set with 40 plus the number of the color, and the foreground with 30
150 #These are the sequences need to get colored ouput
151 RESET_SEQ = "\033[0m"
152 COLOR_SEQ = "\033[1;%dm"
153 BOLD_SEQ = "\033[1m"
154 COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
155 LEVEL_COLOR_MAPPING = {
156     logging.DEBUG_SQL: (WHITE, MAGENTA),
157     logging.DEBUG_RPC: (BLUE, WHITE),
158     logging.DEBUG: (BLUE, DEFAULT),
159     logging.INFO: (GREEN, DEFAULT),
160     logging.TEST: (WHITE, BLUE),
161     logging.WARNING: (YELLOW, DEFAULT),
162     logging.ERROR: (RED, DEFAULT),
163     logging.CRITICAL: (WHITE, RED),
164 }
165
166 class ColoredFormatter(logging.Formatter):
167     def format(self, record):
168         fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno]
169         record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
170         return logging.Formatter.format(self, record)
171
172
173 def init_logger():
174     import os
175     from tools.translate import resetlocale
176     resetlocale()
177
178     logger = logging.getLogger()
179     # create a format for log messages and dates
180     format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
181
182     if tools.config['syslog']:
183         # SysLog Handler
184         if os.name == 'nt':
185             handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
186         else:
187             handler = logging.handlers.SysLogHandler('/dev/log')
188         format = '%s %s' % (release.description, release.version) \
189                + ':%(levelname)s:%(name)s:%(message)s'
190
191     elif tools.config['logfile']:
192         # LogFile Handler
193         logf = tools.config['logfile']
194         try:
195             dirname = os.path.dirname(logf)
196             if dirname and not os.path.isdir(dirname):
197                 os.makedirs(dirname)
198             if tools.config['logrotate'] is not False:
199                 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
200             elif os.name == 'posix':
201                 handler = logging.handlers.WatchedFileHandler(logf)
202             else:
203                 handler = logging.handlers.FileHandler(logf)
204         except Exception, ex:
205             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
206             handler = logging.StreamHandler(sys.stdout)
207     else:
208         # Normal Handler on standard output
209         handler = logging.StreamHandler(sys.stdout)
210
211     if isinstance(handler, logging.StreamHandler) and os.isatty(handler.stream.fileno()):
212         formatter = ColoredFormatter(format)
213     else:
214         formatter = logging.Formatter(format)
215     handler.setFormatter(formatter)
216
217     # add the handler to the root logger
218     logger.addHandler(handler)
219     logger.setLevel(int(tools.config['log_level'] or '0'))
220
221
222 class Logger(object):
223     def __init__(self):
224         warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
225                       "use the standard `logging.getLogger` API instead",
226                       PendingDeprecationWarning, stacklevel=2)
227         super(Logger, self).__init__()
228
229     def notifyChannel(self, name, level, msg):
230         warnings.warn("notifyChannel API shouldn't be used anymore, please use "
231                       "the standard `logging` module instead",
232                       PendingDeprecationWarning, stacklevel=2)
233         from service.web_services import common
234
235         log = logging.getLogger(tools.ustr(name))
236
237         if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
238             fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
239             setattr(log, level, fct)
240
241
242         level_method = getattr(log, level)
243
244         if isinstance(msg, Exception):
245             msg = tools.exception_to_unicode(msg)
246
247         try:
248             msg = tools.ustr(msg).strip()
249             if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
250                 msg = common().exp_get_server_environment() + "\n" + msg
251
252             result = msg.split('\n')
253         except UnicodeDecodeError:
254             result = msg.strip().split('\n')
255         try:
256             if len(result)>1:
257                 for idx, s in enumerate(result):
258                     level_method('[%02d]: %s' % (idx+1, s,))
259             elif result:
260                 level_method(result[0])
261         except IOError,e:
262             # TODO: perhaps reset the logger streams?
263             #if logrotate closes our files, we end up here..
264             pass
265         except:
266             # better ignore the exception and carry on..
267             pass
268
269     def set_loglevel(self, level, logger=None):
270         if logger is not None:
271             log = logging.getLogger(str(logger))
272         else:
273             log = logging.getLogger()
274         log.setLevel(logging.INFO) # make sure next msg is printed
275         log.info("Log level changed to %s" % logging.getLevelName(level))
276         log.setLevel(level)
277
278     def shutdown(self):
279         logging.shutdown()
280
281 import tools
282 init_logger()
283
284 class Agent(object):
285     _timers = {}
286     _logger = Logger()
287
288     __logger = logging.getLogger('timer')
289
290     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
291         wait = dt - time.time()
292         if wait > 0:
293             self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
294             timer = threading.Timer(wait, fn, args, kwargs)
295             timer.start()
296             self._timers.setdefault(db_name, []).append(timer)
297
298         for db in self._timers:
299             for timer in self._timers[db]:
300                 if not timer.isAlive():
301                     self._timers[db].remove(timer)
302
303     @classmethod
304     def cancel(cls, db_name):
305         """Cancel all timers for a given database. If None passed, all timers are cancelled"""
306         for db in cls._timers:
307             if db_name is None or db == db_name:
308                 for timer in cls._timers[db]:
309                     timer.cancel()
310
311     @classmethod
312     def quit(cls):
313         cls.cancel(None)
314
315 import traceback
316
317 class Server:
318     """ Generic interface for all servers with an event loop etc.
319         Override this to impement http, net-rpc etc. servers.
320
321         Servers here must have threaded behaviour. start() must not block,
322         there is no run().
323     """
324     __is_started = False
325     __servers = []
326
327     # we don't want blocking server calls (think select()) to
328     # wait forever and possibly prevent exiting the process,
329     # but instead we want a form of polling/busy_wait pattern, where
330     # _server_timeout should be used as the default timeout for
331     # all I/O blocking operations
332     _busywait_timeout = 0.5
333
334
335     __logger = logging.getLogger('server')
336
337     def __init__(self):
338         if Server.__is_started:
339             raise Exception('All instances of servers must be inited before the startAll()')
340         Server.__servers.append(self)
341
342     def start(self):
343         self.__logger.debug("called stub Server.start")
344
345     def stop(self):
346         self.__logger.debug("called stub Server.stop")
347
348     def stats(self):
349         """ This function should return statistics about the server """
350         return "%s: No statistics" % str(self.__class__)
351
352     @classmethod
353     def startAll(cls):
354         if cls.__is_started:
355             return
356         cls.__logger.info("Starting %d services" % len(cls.__servers))
357         for srv in cls.__servers:
358             srv.start()
359         cls.__is_started = True
360
361     @classmethod
362     def quitAll(cls):
363         if not cls.__is_started:
364             return
365         cls.__logger.info("Stopping %d services" % len(cls.__servers))
366         for srv in cls.__servers:
367             srv.stop()
368         cls.__is_started = False
369
370     @classmethod
371     def allStats(cls):
372         res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
373         res.extend(srv.stats() for srv in cls.__servers)
374         return '\n'.join(res)
375
376     def _close_socket(self):
377         if os.name != 'nt':
378             try:
379                 self.socket.shutdown(getattr(socket, 'SHUT_RDWR', 2))
380             except socket.error, e:
381                 if e.errno != errno.ENOTCONN: raise
382                 # OSX, socket shutdowns both sides if any side closes it
383                 # causing an error 57 'Socket is not connected' on shutdown
384                 # of the other side (or something), see
385                 # http://bugs.python.org/issue4397
386                 self.__logger.debug(
387                     '"%s" when shutting down server socket, '
388                     'this is normal under OS X', e)
389         self.socket.close()
390
391 class OpenERPDispatcherException(Exception):
392     def __init__(self, exception, traceback):
393         self.exception = exception
394         self.traceback = traceback
395
396 class OpenERPDispatcher:
397     def log(self, title, msg):
398         logger = logging.getLogger(title)
399         if logger.isEnabledFor(logging.DEBUG_RPC):
400             for line in pformat(msg).split('\n'):
401                 logger.log(logging.DEBUG_RPC, line)
402
403     def dispatch(self, service_name, method, params):
404         try:
405             self.log('service', service_name)
406             self.log('method', method)
407             self.log('params', params)
408             auth = getattr(self, 'auth_provider', None)
409             result = ExportService.getService(service_name).dispatch(method, auth, params)
410             self.log('result', result)
411             # We shouldn't marshall None,
412             if result == None:
413                 result = False
414             return result
415         except Exception, e:
416             self.log('exception', tools.exception_to_unicode(e))
417             tb = getattr(e, 'traceback', sys.exc_info())
418             tb_s = "".join(traceback.format_exception(*tb))
419             if tools.config['debug_mode']:
420                 import pdb
421                 pdb.post_mortem(tb[2])
422             raise OpenERPDispatcherException(e, tb_s)
423
424 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: