Salome HOME
add AllTestLauncher and src/loggingSimple.py etc...
[tools/sat.git] / src / logger.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2012  CEA/DEN
4 #
5 #  This library is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU Lesser General Public
7 #  License as published by the Free Software Foundation; either
8 #  version 2.1 of the License.
9 #
10 #  This library is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #  Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public
16 #  License along with this library; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19 """\
20 Implements the classes and method relative to the logging
21 """
22
23 import sys
24 import os
25 import datetime
26 import re
27 import tempfile
28
29 import src
30 import printcolors
31 import xmlManager
32
33 import src.debug as DBG
34
35 log_macro_command_file_expression = "^[0-9]{8}_+[0-9]{6}_+.*\.xml$"
36 log_all_command_file_expression = "^.*[0-9]{8}_+[0-9]{6}_+.*\.xml$"
37
38 class Logger(object):
39     """\
40     Class to handle log mechanism.
41     """
42     def __init__(self,
43                  config= None,
44                  silent_sysstd=False,
45                  all_in_terminal=False,
46                  micro_command = False):
47         """Initialization
48         
49         :param config pyconf.Config: The global configuration.
50         :param silent_sysstd boolean: if True, do not write anything
51                                       in terminal.
52         """
53         DBG.write("src.logger.Logger", id(self))
54         self.config = config
55         self.default_level = 3
56         self.silentSysStd = silent_sysstd
57         
58         # Construct xml log file location for sat prints.
59         prefix = ""
60         if micro_command:
61             prefix = "micro_"
62         hour_command_host = (config.VARS.datehour + "_" + 
63                              config.VARS.command + "_" + 
64                              config.VARS.hostname)
65         logFileName = prefix + hour_command_host + ".xml"
66         log_dir = src.get_log_path(config)
67         logFilePath = os.path.join(log_dir, logFileName)
68         # Construct txt file location in order to log 
69         # the external commands calls (cmake, make, git clone, etc...)
70         txtFileName = prefix + hour_command_host + ".txt"
71         txtFilePath = os.path.join(log_dir, "OUT", txtFileName)
72         
73         src.ensure_path_exists(os.path.dirname(logFilePath))
74         src.ensure_path_exists(os.path.dirname(txtFilePath))
75         
76         # The path of the log files (one for sat traces, and the other for 
77         # the system commands traces)
78         self.logFileName = logFileName
79         self.logFilePath = logFilePath
80         self.txtFileName = txtFileName
81         self.txtFilePath = txtFilePath
82         
83         # The list of all log files corresponding to the current command and
84         # the commands called by the current command
85         self.l_logFiles = [logFilePath, txtFilePath]
86         
87         # Initialize xml instance and put first fields 
88         # like beginTime, user, command, etc... 
89         self.xmlFile = xmlManager.XmlLogFile(logFilePath, "SATcommand", 
90                             attrib = {"application" : config.VARS.application})
91         self.put_initial_xml_fields()
92         # Initialize the txt file for reading
93         try:
94             self.logTxtFile = open(str(self.txtFilePath), 'w')
95         except IOError:
96             #msg1 = _("WARNING! Trying to write to a file that"
97             #         " is not accessible:")
98             #msg2 = _("The logs won't be written.")
99             #print("%s\n%s\n%s\n" % (src.printcolors.printcWarning(msg1),
100             #                        src.printcolors.printcLabel(str(self.txtFilePath)),
101             #                        src.printcolors.printcWarning(msg2) ))
102             self.logTxtFile = tempfile.TemporaryFile()
103             
104         # If the option all_in_terminal was called, all the system commands
105         # are redirected to the terminal
106         if all_in_terminal:
107             self.logTxtFile = sys.__stdout__
108         
109     def put_initial_xml_fields(self):
110         """\
111         Called at class initialization: Put all fields 
112         corresponding to the command context (user, time, ...)
113         """
114         # command name
115         self.xmlFile.add_simple_node("Site", attrib={"command" : 
116                                                      self.config.VARS.command})
117         # version of salomeTools
118         self.xmlFile.append_node_attrib("Site", attrib={"satversion" : 
119                                             self.config.INTERNAL.sat_version})
120         # machine name on which the command has been launched
121         self.xmlFile.append_node_attrib("Site", attrib={"hostname" : 
122                                                     self.config.VARS.hostname})
123         # Distribution of the machine
124         self.xmlFile.append_node_attrib("Site", attrib={"OS" : 
125                                                         self.config.VARS.dist})
126         # The user that have launched the command
127         self.xmlFile.append_node_attrib("Site", attrib={"user" : 
128                                                         self.config.VARS.user})
129         # The time when command was launched
130         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
131         date_hour = "%2s/%2s/%4s %2sh%2sm%2ss" % (dd, m, Y, H, M, S)
132         self.xmlFile.append_node_attrib("Site", attrib={"beginTime" : 
133                                                         date_hour})
134         # The application if any
135         if "APPLICATION" in self.config:
136             self.xmlFile.append_node_attrib("Site", 
137                         attrib={"application" : self.config.VARS.application})
138         # The initialization of the trace node
139         self.xmlFile.add_simple_node("Log",text="")
140         # The system commands logs
141         self.xmlFile.add_simple_node("OutLog",
142                                     text=os.path.join("OUT", self.txtFileName))
143         # The initialization of the node where 
144         # to put the links to the other sat commands that can be called by any
145         # command 
146         self.xmlFile.add_simple_node("Links")
147
148     def add_link(self,
149                  log_file_name,
150                  command_name,
151                  command_res,
152                  full_launched_command):
153         """Add a link to another log file.
154         
155         :param log_file_name str: The file name of the link.
156         :param command_name str: The name of the command linked.
157         :param command_res str: The result of the command linked. "0" or "1"
158         :parma full_launched_command str: The full lanch command 
159                                           ("sat command ...")
160         """
161         xmlLinks = self.xmlFile.xmlroot.find("Links")
162         src.xmlManager.add_simple_node(xmlLinks,
163                                        "link", 
164                                        text = log_file_name,
165                                        attrib = {"command" : command_name,
166                                                  "passed" : command_res,
167                                            "launchedCommand" : full_launched_command})
168
169     def write(self, message, level=None, screenOnly=False):
170         """\
171         function used in the commands 
172         to print in the terminal and the log file.
173         
174         :param message str: The message to print.
175         :param level int: The output level corresponding 
176                           to the message 0 < level < 6.
177         :param screenOnly boolean: if True, do not write in log file.
178         """
179         # avoid traces if unittest
180         if isCurrentLoggerUnittest():
181             # print("doing unittest")
182             sendMessageToCurrentLogger(message, level)
183             return
184
185         # do not write message starting with \r to log file
186         if not message.startswith("\r") and not screenOnly:
187             self.xmlFile.append_node_text("Log", 
188                                           printcolors.cleancolor(message))
189
190         # get user or option output level
191         current_output_verbose_level = self.config.USER.output_verbose_level
192         if not ('isatty' in dir(sys.stdout) and sys.stdout.isatty()):
193             # clean the message color if the terminal is redirected by user
194             # ex: sat compile appli > log.txt
195             message = printcolors.cleancolor(message)
196         
197         # Print message regarding the output level value
198         if level:
199             if level <= current_output_verbose_level and not self.silentSysStd:
200                 sys.stdout.write(message)
201         else:
202             if self.default_level <= current_output_verbose_level and not self.silentSysStd:
203                 sys.stdout.write(message)
204         self.flush()
205
206     def error(self, message):
207         """Print an error.
208         
209         :param message str: The message to print.
210         """
211         # Print in the log file
212         self.xmlFile.append_node_text("traces", _('ERROR:') + message)
213
214         # Print in the terminal and clean colors if the terminal 
215         # is redirected by user
216         if not ('isatty' in dir(sys.stderr) and sys.stderr.isatty()):
217             sys.stderr.write(printcolors.printcError(_('ERROR:') + message))
218         else:
219             sys.stderr.write(_('ERROR:') + message)
220
221     def flush(self):
222         """Flush terminal"""
223         sys.stdout.flush()
224         self.logTxtFile.flush()
225         
226     def end_write(self, attribute):
227         """\
228         Called just after command end: Put all fields 
229         corresponding to the command end context (time).
230         Write the log xml file on the hard drive.
231         And display the command to launch to get the log
232         
233         :param attribute dict: the attribute to add to the node "Site".
234         """       
235         # Get current time (end of command) and format it
236         dt = datetime.datetime.now()
237         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
238         t0 = datetime.datetime(int(Y), int(m), int(dd), int(H), int(M), int(S))
239         tf = dt
240         delta = tf - t0
241         total_time = timedelta_total_seconds(delta)
242         hours = int(total_time / 3600)
243         minutes = int((total_time - hours*3600) / 60)
244         seconds = total_time - hours*3600 - minutes*60
245         # Add the fields corresponding to the end time
246         # and the total time of command
247         endtime = dt.strftime('%Y/%m/%d %Hh%Mm%Ss')
248         self.xmlFile.append_node_attrib("Site", attrib={"endTime" : endtime})
249         self.xmlFile.append_node_attrib("Site", 
250                 attrib={"TotalTime" : "%ih%im%is" % (hours, minutes, seconds)})
251         
252         # Add the attribute passed to the method
253         self.xmlFile.append_node_attrib("Site", attrib=attribute)
254         
255         # Call the method to write the xml file on the hard drive
256         self.xmlFile.write_tree(stylesheet = "command.xsl")
257         
258         # Dump the config in a pyconf file in the log directory
259         logDir = src.get_log_path(self.config)
260         dumpedPyconfFileName = (self.config.VARS.datehour 
261                                 + "_" 
262                                 + self.config.VARS.command 
263                                 + ".pyconf")
264         dumpedPyconfFilePath = os.path.join(logDir, 'OUT', dumpedPyconfFileName)
265         try:
266             f = open(dumpedPyconfFilePath, 'w')
267             self.config.__save__(f)
268             f.close()
269         except IOError:
270             pass
271
272 def date_to_datetime(date):
273     """\
274     From a string date in format YYYYMMDD_HHMMSS
275     returns list year, mon, day, hour, minutes, seconds 
276     
277     :param date str: The date in format YYYYMMDD_HHMMSS
278     :return: the same date and time in separate variables.
279     :rtype: (str,str,str,str,str,str)
280     """
281     Y = date[:4]
282     m = date[4:6]
283     dd = date[6:8]
284     H = date[9:11]
285     M = date[11:13]
286     S = date[13:15]
287     return Y, m, dd, H, M, S
288
289 def timedelta_total_seconds(timedelta):
290     """\
291     Replace total_seconds from datetime module 
292     in order to be compatible with old python versions
293     
294     :param timedelta datetime.timedelta: The delta between two dates
295     :return: The number of seconds corresponding to timedelta.
296     :rtype: float
297     """
298     return (
299         timedelta.microseconds + 0.0 +
300         (timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
301         
302 def show_command_log(logFilePath, cmd, application, notShownCommands):
303     """\
304     Used in updateHatXml. 
305     Determine if the log xml file logFilePath 
306     has to be shown or not in the hat log.
307     
308     :param logFilePath str: the path to the command xml log file
309     :param cmd str: the command of the log file
310     :param application str: the application passed as parameter 
311                             to the salomeTools command
312     :param notShownCommands list: the list of commands 
313                                   that are not shown by default
314     
315     :return: True if cmd is not in notShownCommands and the application 
316              in the log file corresponds to application
317     :rtype: boolean
318     """
319     # When the command is not in notShownCommands, no need to go further :
320     # Do not show
321     if cmd in notShownCommands:
322         return False, None, None
323  
324     # Get the application of the log file
325     try:
326         logFileXml = src.xmlManager.ReadXmlFile(logFilePath)
327     except Exception as e:
328         msg = _("WARNING: the log file %s cannot be read:" % logFilePath)
329         sys.stdout.write(printcolors.printcWarning("%s\n%s\n" % (msg, e)))
330         return False, None, None
331
332     if 'application' in logFileXml.xmlroot.keys():
333         appliLog = logFileXml.xmlroot.get('application')
334         launched_cmd = logFileXml.xmlroot.find('Site').attrib['launchedCommand']
335         # if it corresponds, then the log has to be shown
336         if appliLog == application:
337             return True, appliLog, launched_cmd
338         elif application != 'None':
339             return False, appliLog, launched_cmd
340         
341         return True, appliLog, launched_cmd
342     
343     if application == 'None':
344             return True, None, None
345         
346     return False, None, None
347
348 def list_log_file(dirPath, expression):
349     """Find all files corresponding to expression in dirPath
350     
351     :param dirPath str: the directory where to search the files
352     :param expression str: the regular expression of files to find
353     :return: the list of files path and informations about it
354     :rtype: list
355     """
356     lRes = []
357     for fileName in os.listdir(dirPath):
358         # YYYYMMDD_HHMMSS_namecmd.xml
359         sExpr = expression
360         oExpr = re.compile(sExpr)
361         if oExpr.search(fileName):
362             file_name = fileName
363             if fileName.startswith("micro_"):
364                 file_name = fileName[len("micro_"):]
365             # get date and hour and format it
366             date_hour_cmd_host = file_name.split('_')
367             date_not_formated = date_hour_cmd_host[0]
368             date = "%s/%s/%s" % (date_not_formated[6:8], 
369                                  date_not_formated[4:6], 
370                                  date_not_formated[0:4])
371             hour_not_formated = date_hour_cmd_host[1]
372             hour = "%s:%s:%s" % (hour_not_formated[0:2], 
373                                  hour_not_formated[2:4], 
374                                  hour_not_formated[4:6])
375             if len(date_hour_cmd_host) < 4:
376                 cmd = date_hour_cmd_host[2][:-len('.xml')]
377                 host = ""
378             else:
379                 cmd = date_hour_cmd_host[2]
380                 host = date_hour_cmd_host[3][:-len('.xml')]
381             lRes.append((os.path.join(dirPath, fileName), 
382                          date_not_formated,
383                          date,
384                          hour_not_formated,
385                          hour,
386                          cmd,
387                          host))
388     return lRes
389
390 def update_hat_xml(logDir, application=None, notShownCommands = []):
391     """\
392     Create the xml file in logDir that contain all the xml file 
393     and have a name like YYYYMMDD_HHMMSS_namecmd.xml
394     
395     :param logDir str: the directory to parse
396     :param application str: the name of the application if there is any
397     """
398     # Create an instance of XmlLogFile class to create hat.xml file
399     xmlHatFilePath = os.path.join(logDir, 'hat.xml')
400     xmlHat = src.xmlManager.XmlLogFile(xmlHatFilePath,
401                                     "LOGlist", {"application" : application})
402     # parse the log directory to find all the command logs, 
403     # then add it to the xml file
404     lLogFile = list_log_file(logDir, log_macro_command_file_expression)
405     for filePath, __, date, __, hour, cmd, __ in lLogFile:
406         showLog, cmdAppli, full_cmd = show_command_log(filePath, cmd,
407                                               application, notShownCommands)
408         #if cmd not in notShownCommands:
409         if showLog:
410             # add a node to the hat.xml file
411             xmlHat.add_simple_node("LogCommand", 
412                                    text=os.path.basename(filePath), 
413                                    attrib = {"date" : date, 
414                                              "hour" : hour, 
415                                              "cmd" : cmd, 
416                                              "application" : cmdAppli,
417                                              "full_command" : full_cmd})
418     
419     # Write the file on the hard drive
420     xmlHat.write_tree('hat.xsl')
421
422
423 # TODO for future
424 # prepare skip to logging logger sat5.1
425 # suppose only one logger in sat5.1
426 _currentLogger = []
427
428 def getCurrentLogger():
429   """temporary send all in stdout as simple logging logger"""
430   import src.loggingSimple as LOGSI
431   if len(_currentLogger) == 0:
432     logger = LOGSI.getDefaultLogger()
433     _currentLogger.append(logger)
434     logger.warning("set current logger as default %s" % logger.name)
435   return _currentLogger[0]
436
437 def getDefaultLogger():
438   """temporary send all in stdout as simple logging logger"""
439   import src.loggingSimple as LOGSI
440   if len(_currentLogger) == 0:
441     logger = LOGSI.getDefaultLogger()
442     _currentLogger.append(logger)
443     logger.warning("set current logger as default %s" % logger.name)
444   return _currentLogger[0]
445
446 def setCurrentLogger(logger):
447   """temporary send all in stdout as simple logging logger"""
448   import src.loggingSimple as LOGSI
449   if len(_currentLogger) == 0:
450     _currentLogger.append(logger)
451   else:
452     logger.warning("quit current logger as default %s" % _currentLogger[0].name)
453     _currentLogger[0] = logger
454     logger.warning("change current logger as default %s" % logger.name)
455   return _currentLogger[0]
456
457 def isCurrentLoggerUnittest():
458     logger = getCurrentLogger()
459     if "Unittest" in logger.name:
460       res = True
461     else:
462       res = False
463     DBG.write("isCurrentLoggerUnittest %s" % logger.name, res)
464     return res
465
466 def sendMessageToCurrentLogger(message, level):
467     logger = getCurrentLogger()
468     if level is None:
469       lev = 2
470     else:
471       lev = level
472     if lev <= 1:
473       logger.critical(message)
474       return
475     if lev == 2:
476       logger.warning(message)
477       return
478     if lev == 3:
479       logger.info(message)
480       return
481     if lev == 4:
482       logger.step(message)
483       return
484     if lev == 5:
485       logger.trace(message)
486       return
487     if lev >= 6:
488       logger.debug(message)
489       return
490     msg = "What is this level: '%s' for message:\n%s" % (level, message)
491     logger.warning(msg)
492     return