• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""
16log module
17"""
18import warnings
19import sys
20import os
21import re
22import stat
23import time
24import logging
25from logging.handlers import RotatingFileHandler
26import traceback
27import threading
28import platform
29if platform.system() != "Windows":
30    import fcntl
31
32__all__ = ['get_level', 'get_log_config']
33
34# The lock for setting up the logger
35_setup_logger_lock = threading.Lock()
36
37# When getting the logger, Used to check whether
38# the logger already exists
39_global_logger = None
40
41# The flag for enable console output
42_std_on = '1'
43# The flag for disable console output
44_std_off = '0'
45# Rotating max bytes, default is 50M
46_logger_def_max_bytes = '52428800'
47# Rotating backup count, default is 30
48_logger_def_backup_count = '30'
49# The default log level
50_logger_def_level = '2'
51
52# Log level name and level mapping
53_name_to_level = {
54    'DEBUG': 10,
55    'INFO': 20,
56    'WARNING': 30,
57    'ERROR': 40,
58}
59
60# GLog level and level name
61_gloglevel_to_name = {
62    '0': 'DEBUG',
63    '1': 'INFO',
64    '2': 'WARNING',
65    '3': 'ERROR',
66    '4': 'ERROR',
67}
68
69# The mapping of logger configurations to glog configurations
70_confmap_dict = {'level': 'GLOG_v', 'console': 'GLOG_logtostderr', 'filepath': 'GLOG_log_dir',
71                 'maxBytes': 'logger_maxBytes', 'backupCount': 'logger_backupCount',
72                 'stderr_level': 'GLOG_stderrthreshold'}
73
74
75class _MultiCompatibleRotatingFileHandler(RotatingFileHandler):
76    """Inherit RotatingFileHandler for multiprocess compatibility."""
77
78    def doRollover(self):
79        """Override doRollover for multiprocess compatibility
80        and setting permission of Log file"""
81
82        # Attain an exclusive lock with blocking mode by `fcntl` module.
83        with open(self.baseFilename, 'a') as file_pointer:
84            if platform.system() != "Windows":
85                fcntl.lockf(file_pointer.fileno(), fcntl.LOCK_EX)
86            os.chmod(self.baseFilename, stat.S_IREAD)
87            super().doRollover()
88            # Modify the permission of Log file
89            os.chmod(self.baseFilename, stat.S_IREAD | stat.S_IWRITE)
90
91
92class _DataFormatter(logging.Formatter):
93    """Log formatter"""
94
95    def __init__(self, sub_module, fmt=None, **kwargs):
96        """
97        Initialization of logFormatter.
98
99        Args:
100            sub_module (str): The submodule name.
101            fmt (str): Specified format pattern. Default: None.
102        """
103        super(_DataFormatter, self).__init__(fmt=fmt, **kwargs)
104        self.sub_module = sub_module.upper()
105
106    def formatTime(self, record, datefmt=None):
107        """
108        Override formatTime for uniform format %Y-%m-%d-%H:%M:%S.SSS.SSS
109
110        Args:
111            record (str): Log record.
112            datefmt (str): Date format.
113
114        Returns:
115            str, formatted timestamp.
116        """
117        created_time = self.converter(record.created)
118        if datefmt:
119            return time.strftime(datefmt, created_time)
120
121        timestamp = time.strftime('%Y-%m-%d-%H:%M:%S', created_time)
122        msecs = str(round(record.msecs * 1000))
123        # Format the time stamp
124        return f'{timestamp}.{msecs[:3]}.{msecs[3:]}'
125
126    def format(self, record):
127        """
128        Apply log format with specified pattern.
129
130        Args:
131            record (str): Format pattern.
132
133        Returns:
134            str, formatted log content according to format pattern.
135        """
136        # NOTICE: when the Installation directory of mindspore changed,
137        # ms_home_path must be changed
138        ms_install_home_path = 'mindspore'
139        idx = record.pathname.rfind(ms_install_home_path)
140        if idx >= 0:
141            # Get the relative path of the file
142            record.filepath = record.pathname[idx:]
143        else:
144            record.filepath = record.pathname
145        record.sub_module = self.sub_module
146        return super().format(record)
147
148
149def _get_logger():
150    """
151    Get logger instance.
152
153    Returns:
154        Logger, a logger.
155    """
156    if _global_logger:
157        return _global_logger
158
159    kwargs = _get_env_config()
160    _verify_config(kwargs)
161    logger = _setup_logger(_adapt_cfg(kwargs))
162    return logger
163
164
165def _adapt_cfg(kwargs):
166    """
167    Glog configurations converted to logger configurations.
168
169    Args:
170        kwargs (dict): The dictionary of log configurations.
171
172            - console (str): Whether to output log to stdout.
173            - level (str): Log level.
174            - filepath (str): The path for saving logs, if console is false, a file path must be assigned.
175            - maxBytes (str): The Maximum value of a log file for rotating, only valid if console is false.
176            - backupCount (str): The count of rotating backup log files, only valid if console is false.
177
178    Returns:
179        Dict, the input parameter dictionary.
180    """
181    kwargs['level'] = _gloglevel_to_name.get(kwargs.get('level', _logger_def_level))
182    kwargs['stderr_level'] = _gloglevel_to_name.get(kwargs.get('stderr_level', _logger_def_level))
183    kwargs['console'] = not kwargs.get('console') == _std_off
184    kwargs['maxBytes'] = int(kwargs.get('maxBytes', _logger_def_max_bytes))
185    kwargs['backupCount'] = int(kwargs.get('backupCount', _logger_def_backup_count))
186    return kwargs
187
188
189def info(msg, *args, **kwargs):
190    """
191    Log a message with severity 'INFO' on the MindSpore logger.
192
193    Examples:
194        >>> from mindspore import log as logger
195        >>> logger.info("The arg(%s) is: %r", name, arg)
196    """
197    _get_logger().info(msg, *args, **kwargs)
198
199
200def debug(msg, *args, **kwargs):
201    """
202    Log a message with severity 'DEBUG' on the MindSpore logger.
203
204    Examples:
205        >>> from mindspore import log as logger
206        >>> logger.debug("The arg(%s) is: %r", name, arg)
207    """
208    _get_logger().debug(msg, *args, **kwargs)
209
210
211def error(msg, *args, **kwargs):
212    """Log a message with severity 'ERROR' on the MindSpore logger."""
213    _get_logger().error(msg, *args, **kwargs)
214
215
216def exception(msg, *args, **kwargs):
217    """Log a message with severity 'EXCEPTION' on the MindSpore logger."""
218    _get_logger().exception(msg, *args, **kwargs)
219
220
221def warning(msg, *args, **kwargs):
222    """Log a message with severity 'WARNING' on the MindSpore logger."""
223    _get_logger().warning(msg, *args, **kwargs)
224
225
226def get_level():
227    """
228    Get the logger level.
229
230    Returns:
231        str, the Log level includes 4(EXCEPTION), 3(ERROR), 2(WARNING), 1(INFO), 0(DEBUG).
232
233    Examples:
234        >>> import os
235        >>> os.environ['GLOG_v'] = '0'
236        >>> from mindspore import log as logger
237        >>> level = logger.get_level()
238        >>> print(level)
239        '0'
240    """
241    # level and glog level mapping dictionary
242    level_to_glog_level = dict(zip(_name_to_level.values(), _gloglevel_to_name.keys()))
243
244    return level_to_glog_level.get(_get_logger().getEffectiveLevel())
245
246
247def _get_formatter():
248    """
249    Get the string of log formatter.
250
251    Returns:
252        str, the string of log formatter.
253    """
254    formatter = '[%(levelname)s] %(sub_module)s(%(process)d:' \
255                '%(thread)d,%(processName)s):%(asctime)s ' \
256                '[%(filepath)s:%(lineno)d] %(message)s'
257    return formatter
258
259
260def _get_env_config():
261    """
262    Get configurations from environment variables.
263
264    Returns:
265        Dict, the dictionary of configurations.
266    """
267    config_dict = {}
268    for key, env_value in _confmap_dict.items():
269        value = os.environ.get(env_value)
270        if value:
271            config_dict[key] = value.strip()
272    return config_dict
273
274
275def _check_directory_by_regular(target, reg=None, flag=re.ASCII, prim_name=None):
276    """Check whether directory is legitimate."""
277    if not isinstance(target, str):
278        raise ValueError("Args directory {} must be string, please check it".format(target))
279    if reg is None:
280        reg = r"^[\/0-9a-zA-Z\_\-\.\:\\]+$"
281    if re.match(reg, target, flag) is None:
282        prim_name = f'in `{prim_name}`' if prim_name else ""
283        raise ValueError("'{}' {} is illegal, it should be match regular'{}' by flags'{}'".format(
284            target, prim_name, reg, flag))
285
286
287def _make_directory(path: str):
288
289    """Make directory."""
290    if path is None or not isinstance(path, str) or path.strip() == "":
291        raise TypeError("Input path '{}' is invalid type".format(path))
292
293    path = os.path.realpath(path)
294    _check_directory_by_regular(path)
295    if os.path.exists(path):
296        real_path = path
297    else:
298        try:
299            permissions = os.R_OK | os.W_OK | os.X_OK
300            os.umask(permissions << 3 | permissions)
301            mode = permissions << 6
302            os.makedirs(path, mode=mode, exist_ok=True)
303            real_path = path
304        except PermissionError:
305            raise TypeError("No write permission on the directory `{path}`.")
306    return real_path
307
308
309def _verify_config(kwargs):
310
311    """
312    Verify log configurations.
313
314    Args:
315        kwargs (dict): The dictionary of log configurations.
316
317            - console (str): Whether to output log to stdout.
318            - level (str): Log level.
319            - filepath (str): The path for saving logs, if console is false, a file path must be assigned.
320            - maxBytes (str): The Maximum value of a log file for rotating, only valid if console is false.
321            - backupCount (str): The count of rotating backup log files, only valid if console is false.
322    """
323    # Check the input value of level
324    level = kwargs.get('level', None)
325    if level is not None:
326        _verify_level(level)
327
328    # Check the input value of stderr_level
329    level = kwargs.get('stderr_level', None)
330    if level is not None:
331        _verify_level(level)
332
333    # Check the input value of console
334    console = kwargs.get('console', None)
335    file_path = kwargs.get('filepath', None)
336
337    if console is not None:
338        if not console.isdigit() or console not in (_std_off, _std_on):
339            raise ValueError(f'Incorrect value, The value of {_confmap_dict["console"]} must be 0 or 1,'
340                             f' Output log to console, configure to 1.')
341
342        if console == _std_off and not file_path:
343            raise ValueError(f'When {_confmap_dict["console"]} is set to 0, The directory of '
344                             f'saving log must be set, {_confmap_dict["filepath"]} cannot be empty.')
345
346        # Check the input value of filepath
347        if console == _std_off and file_path is not None:
348            file_real_path = os.path.realpath(file_path)
349            if not os.path.exists(file_real_path):
350                _make_directory(file_real_path)
351        # Check the input value of maxBytes
352        max_bytes = kwargs.get('maxBytes', None)
353        if console == _std_off and max_bytes is not None:
354            if not max_bytes.isdigit():
355                raise ValueError(f'Incorrect value, The value of {_confmap_dict["maxBytes"]} must be positive integer. '
356                                 f'{_confmap_dict["maxBytes"]}:{max_bytes}')
357
358        # Check the input value of backupCount
359        backup_count = kwargs.get('backupCount', None)
360        if console == _std_off and backup_count is not None:
361            if not backup_count.isdigit():
362                raise ValueError(f'Incorrect value, The value of {_confmap_dict["backupCount"]} must be positive '
363                                 f'integer. {_confmap_dict["backupCount"]}:{backup_count}')
364
365
366def _verify_level(level):
367    """
368    Verify log level.
369
370    Args:
371        level (str): The log level.
372    """
373    level_name = _gloglevel_to_name.get(level, None)
374
375    # Check the value of input level
376    if level_name not in _name_to_level:
377        raise ValueError(f'Incorrect log level:{level}, Please check the configuration of GLOG_v or '
378                         'GLOG_stderrthreshold, desired log level: 4-EXCEPTION, 3-ERROR, 2-WARNING, '
379                         '1-INFO, 0-DEBUG')
380
381
382def get_log_config():
383    """
384    Get logger configurations.
385
386    Returns:
387        Dict, the dictionary of logger configurations.
388
389    Examples:
390        >>> import os
391        >>> os.environ['GLOG_v'] = '1'
392        >>> os.environ['GLOG_logtostderr'] = '0'
393        >>> os.environ['GLOG_log_dir'] = '/var/log'
394        >>> os.environ['logger_maxBytes'] = '5242880'
395        >>> os.environ['logger_backupCount'] = '10'
396        >>> os.environ['GLOG_stderrthreshold'] = '2'
397        >>> from mindspore import log as logger
398        >>> config= logger.get_log_config()
399        >>> print(config)
400        {'GLOG_v': '1', 'GLOG_logtostderr': '0', 'GLOG_log_dir': '/var/log',
401         'logger_maxBytes': '5242880', 'logger_backupCount': '10', 'GLOG_stderrthreshold': '2'}
402    """
403    logger = _get_logger()
404    handler = logger.handlers[0]
405    config_dict = {}
406    config_dict['GLOG_v'] = get_level()
407    config_dict['GLOG_logtostderr'] = _std_on
408
409    if handler.name == 'FileHandler':
410        config_dict['GLOG_logtostderr'] = _std_off
411        # Separating file path and name
412        file_path_and_name = os.path.split(handler.baseFilename)
413        config_dict['GLOG_log_dir'] = file_path_and_name[0]
414        config_dict['logger_maxBytes'] = handler.maxBytes
415        config_dict['logger_backupCount'] = handler.backupCount
416        handler_stderr = logger.handlers[1]
417        # level and glog level mapping dictionary
418        level_to_glog_level = dict(zip(_name_to_level.values(), _gloglevel_to_name.keys()))
419        config_dict['GLOG_stderrthreshold'] = level_to_glog_level.get(handler_stderr.level)
420    return config_dict
421
422
423def _clear_handler(logger):
424    """Clear the handlers that has been set, avoid repeated loading"""
425    for handler in logger.handlers:
426        logger.removeHandler(handler)
427
428
429def _find_caller(stack_info=False, stacklevel=1):
430    """
431    Find the stack frame of the caller.
432
433    Override findCaller on the logger, Support for getting log record.
434    Find the stack frame of the caller so that we can note the source
435    file name, function name and line number.
436
437    Args:
438        stack_info (bool): If the value is true, print stack information to the log. Default: False.
439
440    Returns:
441        tuple, the tuple of the frame data.
442    """
443    f = sys._getframe(3)
444    sinfo = None
445    # log_file is used to check caller stack frame
446    log_file = os.path.normcase(f.f_code.co_filename)
447    f = f.f_back
448    rv = "(unknown file)", 0, "(unknown function)", None
449    while f:
450        co = f.f_code
451        filename = os.path.normcase(co.co_filename)
452        if log_file == filename:
453            f = f.f_back
454            continue
455        if stack_info:
456            sinfo = _get_stack_info(f)
457        rv = (co.co_filename, f.f_lineno, co.co_name, sinfo)
458        break
459    return rv
460
461
462def _get_stack_info(frame):
463    """
464    Get the stack information.
465
466    Args:
467        frame(frame): the frame requiring information.
468
469    Returns:
470        str, the string of the stack information.
471    """
472    stack_prefix = 'Stack (most recent call last):\n'
473    sinfo = stack_prefix + "".join(traceback.format_stack(frame))
474    return sinfo
475
476
477def _get_rank_id():
478    """
479    get rank id
480    ascend, CPU: RANK_ID
481    GPU: OMPI_COMM_WORLD_RANK
482    Both RANK_ID and OMPI_COMM_WORLD_RANK exist, Report to the police, default: RANK_ID
483    Returns:
484        rank.
485    """
486    rank_id = os.getenv('RANK_ID')
487    gpu_rank_id = os.getenv('OMPI_COMM_WORLD_RANK')
488    rank = '0'
489    if rank_id and gpu_rank_id:
490        warnings.warn("Environment variables RANK_ID and OMPI_COMM_WORLD_RANK both exist,"
491                      "we will use RANK_ID to get rank id by default.")
492    if rank_id:
493        rank = rank_id
494    elif gpu_rank_id:
495        rank = gpu_rank_id
496    return rank
497
498
499def _create_logfile_dir(kwargs):
500    """
501    create logs dir
502    Args:
503        kwargs (dict): The dictionary of log configurations.
504    Returns:
505        Log_dir: Create subdirectory.
506        Examples:
507        >>> /rank_0/logs
508    """
509    log_dir = os.path.realpath(kwargs.get('filepath'))
510    rank_id = _get_rank_id()
511    log_dir += '/rank_' + rank_id + '/logs'
512    if not os.path.exists(log_dir):
513        os.makedirs(log_dir, exist_ok=True)
514    return log_dir
515
516
517def _setup_logger(kwargs):
518    """
519    Set up the logger.
520
521    Args:
522        kwargs (dict): The dictionary of log configurations.
523
524            - console (bool): Whether to output log to stdout. Default: True.
525            - level (str): Log level. Default: WARNING.
526            - filepath (str): The path for saving logs, if console is false, a file path must be assigned.
527            - maxBytes (int): The Maximum value of a log file for rotating, only valid if console is false.
528              Default: 52428800.
529            - backupCount (int): The count of rotating backup log files, only valid if console is false. Default: 30.
530
531    Returns:
532        Logger, well-configured logger.
533    """
534
535    # The name of Submodule
536    sub_module = 'ME'
537    # The name of Base log file
538    pid = str(os.getpid())
539    log_name = 'mindspore.log.' + pid
540
541    global _global_logger
542
543    _setup_logger_lock.acquire()
544    try:
545        if _global_logger:
546            return _global_logger
547
548        logger = logging.getLogger(name=f'{sub_module}.{log_name}')
549        # Override findCaller on the logger, Support for getting log record
550        logger.findCaller = _find_caller
551        console = kwargs.get('console', True)
552        # Set log level
553        logger.setLevel(kwargs.get('level', logging.WARNING))
554        # Set "propagate" attribute to False, stop searching up the hierarchy,
555        # avoid to load the handler of the root logger
556        logger.propagate = False
557        # Get the formatter for handler
558        formatter = _get_formatter()
559
560        # Clean up handle to avoid repeated loading
561        _clear_handler(logger)
562
563        # Set streamhandler for the console appender
564        if console:
565            console_handler = logging.StreamHandler(sys.stderr)
566            console_handler.name = 'StreamHandler'
567            console_handler.formatter = _DataFormatter(sub_module, formatter)
568            logger.addHandler(console_handler)
569
570        # Set rotatingFileHandler for the file appender
571        else:
572            # filepath cannot be null, checked in function _verify_config ()
573            logfile_dir = _create_logfile_dir(kwargs)
574            file_name = f'{logfile_dir}/{log_name}'
575            logfile_handler = _MultiCompatibleRotatingFileHandler(
576                filename=file_name,
577                # Rotating max bytes, default is 50M
578                maxBytes=kwargs.get('maxBytes', _logger_def_max_bytes),
579                # Rotating backup count, default is 30
580                backupCount=kwargs.get('backupCount', _logger_def_backup_count),
581                encoding='utf8'
582            )
583            logfile_handler.name = 'FileHandler'
584            logfile_handler.formatter = _DataFormatter(sub_module, formatter)
585            logger.addHandler(logfile_handler)
586
587            with open(file_name, 'a'):
588                # Modify the permission of Log file
589                os.chmod(file_name, stat.S_IREAD | stat.S_IWRITE)
590
591            # Write the file and output warning and error logs to stderr
592            console_handler = logging.StreamHandler(sys.stderr)
593            console_handler.name = 'StreamHandler'
594            console_handler.formatter = _DataFormatter(sub_module, formatter)
595            console_handler.setLevel(kwargs.get('stderr_level', logging.WARNING))
596            logger.addHandler(console_handler)
597
598        _global_logger = logger
599
600    finally:
601        _setup_logger_lock.release()
602    return _global_logger
603