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