[FIX] Removed ANSI output in logging 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 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_TEST = 'test'
131 LOG_INFO = 'info'
132 LOG_WARNING = 'warn'
133 LOG_ERROR = 'error'
134 LOG_CRITICAL = 'critical'
135
136 logging.DEBUG_RPC = logging.DEBUG - 2
137 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
138
139 logging.TEST = logging.INFO - 5
140 logging.addLevelName(logging.TEST, 'TEST')
141
142 def init_logger():
143     import os
144     from tools.translate import resetlocale
145     resetlocale()
146
147     logger = logging.getLogger()
148     # create a format for log messages and dates
149     formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
150
151     if tools.config['syslog']:
152         # SysLog Handler
153         if os.name == 'nt':
154             handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
155         else:
156             handler = logging.handlers.SysLogHandler('/dev/log')
157         formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
158
159     elif tools.config['logfile']:
160         # LogFile Handler
161         logf = tools.config['logfile']
162         try:
163             dirname = os.path.dirname(logf)
164             if dirname and not os.path.isdir(dirname):
165                 os.makedirs(dirname)
166             if tools.config['logrotate'] is not False:
167                 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
168             elif os.name == 'posix':
169                 handler = logging.handlers.WatchedFileHandler(logf)
170             else:
171                 handler = logging.handlers.FileHandler(logf)
172         except Exception, ex:
173             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
174             handler = logging.StreamHandler(sys.stdout)
175     else:
176         # Normal Handler on standard output
177         handler = logging.StreamHandler(sys.stdout)
178
179
180     # tell the handler to use this format
181     handler.setFormatter(formatter)
182
183     # add the handler to the root logger
184     logger.addHandler(handler)
185     logger.setLevel(int(tools.config['log_level'] or '0'))
186
187     if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
188         # change color of level names
189         # uses of ANSI color codes
190         # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
191         # maybe use http://code.activestate.com/recipes/574451/
192         colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
193         foreground = lambda f: 30 + colors.index(f)
194         background = lambda f: 40 + colors.index(f)
195
196         mapping = {
197             'DEBUG_RPC': ('blue', 'white'),
198             'DEBUG': ('blue', 'default'),
199             'INFO': ('green', 'default'),
200             'TEST': ('white', 'blue'),
201             'WARNING': ('yellow', 'default'),
202             'ERROR': ('red', 'default'),
203             'CRITICAL': ('white', 'red'),
204         }
205
206         for level, (fg, bg) in mapping.items():
207             msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
208 # jth: we should not override default level name
209 # jth: use this instead http://stackoverflow.com/questions/384076/how-can-i-make-the-python-logging-output-to-be-colored
210 # jth: also, do not output ANSI if terminal doesn't support it (test it in Eclipse console and on windows)
211             #logging.addLevelName(getattr(logging, level), msg)
212
213
214 class Logger(object):
215     def __init__(self):
216         warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
217                       "use the standard `logging.getLogger` API instead",
218                       PendingDeprecationWarning, stacklevel=2)
219         super(Logger, self).__init__()
220
221     def notifyChannel(self, name, level, msg):
222         warnings.warn("notifyChannel API shouldn't be used anymore, please use "
223                       "the standard `logging` module instead",
224                       PendingDeprecationWarning, stacklevel=2)
225         from service.web_services import common
226
227         log = logging.getLogger(tools.ustr(name))
228
229         if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
230             fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
231             setattr(log, level, fct)
232
233
234         level_method = getattr(log, level)
235
236         if isinstance(msg, Exception):
237             msg = tools.exception_to_unicode(msg)
238
239         try:
240             msg = tools.ustr(msg).strip()
241             if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
242                 msg = common().exp_get_server_environment() + "\n" + msg
243
244             result = msg.split('\n')
245         except UnicodeDecodeError:
246             result = msg.strip().split('\n')
247         try:
248             if len(result)>1:
249                 for idx, s in enumerate(result):
250                     level_method('[%02d]: %s' % (idx+1, s,))
251             elif result:
252                 level_method(result[0])
253         except IOError,e:
254             # TODO: perhaps reset the logger streams?
255             #if logrotate closes our files, we end up here..
256             pass
257         except:
258             # better ignore the exception and carry on..
259             pass
260
261     def set_loglevel(self, level):
262         log = logging.getLogger()
263         log.setLevel(logging.INFO) # make sure next msg is printed
264         log.info("Log level changed to %s" % logging.getLevelName(level))
265         log.setLevel(level)
266
267     def shutdown(self):
268         logging.shutdown()
269
270 import tools
271 init_logger()
272
273 class Agent(object):
274     _timers = {}
275     _logger = Logger()
276
277     __logger = logging.getLogger('timer')
278
279     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
280         wait = dt - time.time()
281         if wait > 0:
282             self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
283             timer = threading.Timer(wait, fn, args, kwargs)
284             timer.start()
285             self._timers.setdefault(db_name, []).append(timer)
286
287         for db in self._timers:
288             for timer in self._timers[db]:
289                 if not timer.isAlive():
290                     self._timers[db].remove(timer)
291
292     @classmethod
293     def cancel(cls, db_name):
294         """Cancel all timers for a given database. If None passed, all timers are cancelled"""
295         for db in cls._timers:
296             if db_name is None or db == db_name:
297                 for timer in cls._timers[db]:
298                     timer.cancel()
299
300     @classmethod
301     def quit(cls):
302         cls.cancel(None)
303
304 import traceback
305
306 class Server:
307     """ Generic interface for all servers with an event loop etc.
308         Override this to impement http, net-rpc etc. servers.
309
310         Servers here must have threaded behaviour. start() must not block,
311         there is no run().
312     """
313     __is_started = False
314     __servers = []
315
316
317     __logger = logging.getLogger('server')
318
319     def __init__(self):
320         if Server.__is_started:
321             raise Exception('All instances of servers must be inited before the startAll()')
322         Server.__servers.append(self)
323
324     def start(self):
325         self.__logger.debug("called stub Server.start")
326
327     def stop(self):
328         self.__logger.debug("called stub Server.stop")
329
330     def stats(self):
331         """ This function should return statistics about the server """
332         return "%s: No statistics" % str(self.__class__)
333
334     @classmethod
335     def startAll(cls):
336         if cls.__is_started:
337             return
338         cls.__logger.info("Starting %d services" % len(cls.__servers))
339         for srv in cls.__servers:
340             srv.start()
341         cls.__is_started = True
342
343     @classmethod
344     def quitAll(cls):
345         if not cls.__is_started:
346             return
347         cls.__logger.info("Stopping %d services" % len(cls.__servers))
348         for srv in cls.__servers:
349             srv.stop()
350         cls.__is_started = False
351
352     @classmethod
353     def allStats(cls):
354         res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
355         res.extend(srv.stats() for srv in cls.__servers)
356         return '\n'.join(res)
357
358 class OpenERPDispatcherException(Exception):
359     def __init__(self, exception, traceback):
360         self.exception = exception
361         self.traceback = traceback
362
363 class OpenERPDispatcher:
364     def log(self, title, msg):
365         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
366
367     def dispatch(self, service_name, method, params):
368         try:
369             self.log('service', service_name)
370             self.log('method', method)
371             self.log('params', params)
372             auth = getattr(self, 'auth_provider', None)
373             result = ExportService.getService(service_name).dispatch(method, auth, params)
374             self.log('result', result)
375             # We shouldn't marshall None,
376             if result == None:
377                 result = False
378             return result
379         except Exception, e:
380             self.log('exception', tools.exception_to_unicode(e))
381             tb = getattr(e, 'traceback', sys.exc_info())
382             tb_s = "".join(traceback.format_exception(*tb))
383             if tools.config['debug_mode']:
384                 import pdb
385                 pdb.post_mortem(tb[2])
386             raise OpenERPDispatcherException(e, tb_s)
387
388 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: