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