[REF] Use the logging module instead of the Logger().notifyChannel() function
[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 logging
28 import logging.handlers
29 import sys
30 import threading
31 import time
32 import release
33 from pprint import pformat
34 import warnings
35
36 class Service(object):
37     """ Base class for *Local* services
38
39         Functionality here is trusted, no authentication.
40     """
41     _services = {}
42     def __init__(self, name, audience=''):
43         Service._services[name] = self
44         self.__name = name
45         self._methods = {}
46
47     def joinGroup(self, name):
48         raise Exception("No group for local services")
49         #GROUPS.setdefault(name, {})[self.__name] = self
50
51     @classmethod
52     def exists(cls, name):
53         return name in cls._services
54
55     @classmethod
56     def remove(cls, name):
57         if cls.exists(name):
58             cls._services.pop(name)
59
60     def exportMethod(self, method):
61         if callable(method):
62             self._methods[method.__name__] = method
63
64     def abortResponse(self, error, description, origin, details):
65         if not tools.config['debug_mode']:
66             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
67         else:
68             raise
69
70 class LocalService(object):
71     """ Proxy for local services. 
72     
73         Any instance of this class will behave like the single instance
74         of Service(name)
75     """
76     __logger = logging.getLogger('service')
77     def __init__(self, name):
78         self.__name = name
79         try:
80             self._service = Service._services[name]
81             for method_name, method_definition in self._service._methods.items():
82                 setattr(self, method_name, method_definition)
83         except KeyError, keyError:
84             self.__logger.error('This service does not exist: %s' % (str(keyError),) )
85             raise
86
87     def __call__(self, method, *params):
88         return getattr(self, method)(*params)
89
90 class ExportService(object):
91     """ Proxy for exported services. 
92
93     All methods here should take an AuthProxy as their first parameter. It
94     will be appended by the calling framework.
95
96     Note that this class has no direct proxy, capable of calling 
97     eservice.method(). Rather, the proxy should call 
98     dispatch(method,auth,params)
99     """
100     
101     _services = {}
102     _groups = {}
103     
104     def __init__(self, name, audience=''):
105         ExportService._services[name] = self
106         self.__name = name
107
108     def joinGroup(self, name):
109         ExportService._groups.setdefault(name, {})[self.__name] = self
110
111     @classmethod
112     def getService(cls,name):
113         return cls._services[name]
114
115     def dispatch(self, method, auth, params):
116         raise Exception("stub dispatch at %s" % self.__name)
117         
118     def new_dispatch(self,method,auth,params):
119         raise Exception("stub dispatch at %s" % self.__name)
120
121     def abortResponse(self, error, description, origin, details):
122         if not tools.config['debug_mode']:
123             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
124         else:
125             raise
126
127 LOG_NOTSET = 'notset'
128 LOG_DEBUG_RPC = 'debug_rpc'
129 LOG_DEBUG = 'debug'
130 LOG_INFO = 'info'
131 LOG_WARNING = 'warn'
132 LOG_ERROR = 'error'
133 LOG_CRITICAL = 'critical'
134
135 logging.DEBUG_RPC = logging.DEBUG - 2
136 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
137
138 def init_logger():
139     import os
140     from tools.translate import resetlocale
141     resetlocale()
142
143     logger = logging.getLogger()
144     # create a format for log messages and dates
145     formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
146
147     if tools.config['syslog']:
148         # SysLog Handler
149         if os.name == 'nt':
150             handler = logging.handlers.NTEventLogHandler("%s %s" %
151                                                          (release.description,
152                                                           release.version))
153         else:
154             handler = logging.handlers.SysLogHandler('/dev/log')
155         formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
156
157     elif tools.config['logfile']:
158         # LogFile Handler
159         logf = tools.config['logfile']
160         try:
161             dirname = os.path.dirname(logf)
162             if dirname and not os.path.isdir(dirname):
163                 os.makedirs(dirname)
164             if tools.config['logrotate'] is not False:
165                 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
166             elif os.name == 'posix':
167                 handler = logging.handlers.WatchedFileHandler(logf)
168             else:
169                 handler = logging.handlers.FileHandler(logf)
170         except Exception, ex:
171             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
172             handler = logging.StreamHandler(sys.stdout)
173     else:
174         # Normal Handler on standard output
175         handler = logging.StreamHandler(sys.stdout)
176
177
178     # tell the handler to use this format
179     handler.setFormatter(formatter)
180
181     # add the handler to the root logger
182     logger.addHandler(handler)
183     logger.setLevel(int(tools.config['log_level'] or '0'))
184
185     if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
186         # change color of level names
187         # uses of ANSI color codes
188         # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
189         # maybe use http://code.activestate.com/recipes/574451/
190         colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
191         foreground = lambda f: 30 + colors.index(f)
192         background = lambda f: 40 + colors.index(f)
193
194         mapping = {
195             'DEBUG_RPC': ('blue', 'white'),
196             'DEBUG': ('blue', 'default'),
197             'INFO': ('green', 'default'),
198             'WARNING': ('yellow', 'default'),
199             'ERROR': ('red', 'default'),
200             'CRITICAL': ('white', 'red'),
201         }
202
203         for level, (fg, bg) in mapping.items():
204             msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
205             logging.addLevelName(getattr(logging, level), msg)
206
207
208 class Logger(object):
209     def __init__(self):
210         warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
211                       "use the standard `logging.getLogger` API instead",
212                       PendingDeprecationWarning, stacklevel=2)
213         super(Logger, self).__init__()
214
215     def notifyChannel(self, name, level, msg):
216         warnings.warn("notifyChannel API shouldn't be used anymore, please use "
217                       "the standard `logging` module instead",
218                       PendingDeprecationWarning, stacklevel=2)
219         from service.web_services import common
220
221         log = logging.getLogger(tools.ustr(name))
222
223         if level == LOG_DEBUG_RPC and not hasattr(log, level):
224             fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
225             setattr(log, LOG_DEBUG_RPC, fct)
226
227         level_method = getattr(log, level)
228
229         if isinstance(msg, Exception):
230             msg = tools.exception_to_unicode(msg)
231
232         try:
233             msg = tools.ustr(msg).strip()
234             if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
235                 msg = common().exp_get_server_environment() + "\n" + msg
236
237             result = msg.split('\n')
238         except UnicodeDecodeError:
239             result = msg.strip().split('\n')
240         try:
241             if len(result)>1:
242                 for idx, s in enumerate(result):
243                     level_method('[%02d]: %s' % (idx+1, s,))
244             elif result:
245                 level_method(result[0])
246         except IOError,e:
247             # TODO: perhaps reset the logger streams?
248             #if logrotate closes our files, we end up here..
249             pass
250         except:
251             # better ignore the exception and carry on..
252             pass
253
254     def set_loglevel(self, level):
255         log = logging.getLogger()
256         log.setLevel(logging.INFO) # make sure next msg is printed
257         log.info("Log level changed to %s" % logging.getLevelName(level))
258         log.setLevel(level)
259
260     def shutdown(self):
261         logging.shutdown()
262
263 import tools
264 init_logger()
265
266 class Agent(object):
267     _timers = {}
268     _logger = Logger()
269
270     __logger = logging.getLogger('timer')
271
272     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
273         wait = dt - time.time()
274         if wait > 0:
275             self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
276             timer = threading.Timer(wait, fn, args, kwargs)
277             timer.start()
278             self._timers.setdefault(db_name, []).append(timer)
279
280         for db in self._timers:
281             for timer in self._timers[db]:
282                 if not timer.isAlive():
283                     self._timers[db].remove(timer)
284
285     @classmethod
286     def cancel(cls, db_name):
287         """Cancel all timers for a given database. If None passed, all timers are cancelled"""
288         for db in cls._timers:
289             if db_name is None or db == db_name:
290                 for timer in cls._timers[db]:
291                     timer.cancel()
292
293     @classmethod
294     def quit(cls):
295         cls.cancel(None)
296
297 import traceback
298
299 class Server:
300     """ Generic interface for all servers with an event loop etc.
301         Override this to impement http, net-rpc etc. servers.
302
303         Servers here must have threaded behaviour. start() must not block,
304         there is no run().
305     """
306     __is_started = False
307     __servers = []
308
309
310     __logger = logging.getLogger('server')
311
312     def __init__(self):
313         if Server.__is_started:
314             raise Exception('All instances of servers must be inited before the startAll()')
315         Server.__servers.append(self)
316
317     def start(self):
318         self.__logger.debug("called stub Server.start")
319
320     def stop(self):
321         self.__logger.debug("called stub Server.stop")
322
323     def stats(self):
324         """ This function should return statistics about the server """
325         return "%s: No statistics" % str(self.__class__)
326
327     @classmethod
328     def startAll(cls):
329         if cls.__is_started:
330             return
331         cls.__logger.info("Starting %d services" % len(cls.__servers))
332         for srv in cls.__servers:
333             srv.start()
334         cls.__is_started = True
335
336     @classmethod
337     def quitAll(cls):
338         if not cls.__is_started:
339             return
340         cls.__logger.info("Stopping %d services" % len(cls.__servers))
341         for srv in cls.__servers:
342             srv.stop()
343         cls.__is_started = False
344
345     @classmethod
346     def allStats(cls):
347         res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
348         res.extend(srv.stats() for srv in cls.__servers)
349         return '\n'.join(res)
350
351 class OpenERPDispatcherException(Exception):
352     def __init__(self, exception, traceback):
353         self.exception = exception
354         self.traceback = traceback
355
356 class OpenERPDispatcher:
357     def log(self, title, msg):
358         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
359
360     def dispatch(self, service_name, method, params):
361         try:
362             self.log('service', service_name)
363             self.log('method', method)
364             self.log('params', params)
365             auth = getattr(self, 'auth_provider', None)
366             result = ExportService.getService(service_name).dispatch(method, auth, params)
367             self.log('result', result)
368             # We shouldn't marshall None,
369             if result == None:
370                 result = False
371             return result
372         except Exception, e:
373             self.log('exception', tools.exception_to_unicode(e))
374             tb = getattr(e, 'traceback', sys.exc_info())
375             tb_s = "".join(traceback.format_exception(*tb))
376             if tools.config['debug_mode']:
377                 import pdb
378                 pdb.post_mortem(tb[2])
379             raise OpenERPDispatcherException(e, tb_s)
380
381 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: