| # Copyright (c) 2019 AT&T Intellectual Property. |
| # Copyright (c) 2018-2019 Nokia. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| # This source code is part of the near-RT RIC (RAN Intelligent Controller) |
| # platform project (RICP). |
| # |
| |
| """Structured logging library with Mapped Diagnostic Context |
| |
| Outputs the log entries to standard out in structured format, json currently. |
| Severity based filtering. |
| Supports Mapped Diagnostic Context (MDC). |
| |
| Set MDC pairs are automatically added to log entries by the library. |
| """ |
| from typing import TypeVar |
| from enum import IntEnum |
| import sys |
| import json |
| import time |
| import os |
| import inotify.adapters |
| import threading |
| |
| |
| class Level(IntEnum): |
| """Severity levels of the log messages.""" |
| DEBUG = 10 |
| INFO = 20 |
| WARNING = 30 |
| ERROR = 40 |
| |
| |
| LEVEL_STRINGS = {Level.DEBUG: "DEBUG", |
| Level.INFO: "INFO", |
| Level.WARNING: "WARNING", |
| Level.ERROR: "ERROR"} |
| |
| |
| Value = TypeVar('Value', str, int) |
| |
| |
| class MDCLogger(): |
| """Initialize the mdclogging module. |
| Calling of the function is optional. If not called, the process name |
| (sys.argv[0]) is used by default. |
| |
| Keyword arguments: |
| name -- name of the component. The name will appear as part of the log |
| entries. |
| """ |
| def __init__(self, name: str = sys.argv[0], level: Level = Level.ERROR): |
| """Initialize a Logger instance. |
| |
| Keyword arguments: |
| name -- name of the component. The name will appear as part of the |
| log entries. |
| """ |
| self.procname = name |
| self.current_level = level |
| self.mdc = {} |
| |
| # Pass configmap_monitor = True to monitor configmap to change logs dynamically using configmap |
| |
| def mdclog_format_init(self, configmap_monitor=False): |
| |
| self.mdc = {"PID": "", "SYSTEM_NAME": "", "HOST_NAME": "", "SERVICE_NAME": "", "CONTAINER_NAME": "", "POD_NAME": ""} |
| self.get_env_params_values() |
| try: |
| self.filename = os.environ['CONFIG_MAP_NAME'] |
| self.dirname = str(self.filename[:self.filename.rindex('/')]) |
| self.parse_file() |
| |
| if configmap_monitor: |
| self.register_log_change_notify() |
| |
| except Exception as e: |
| print("Unable to Add Watch on ConfigMap File", e) |
| |
| def _output_log(self, log: str): |
| |
| """Output the log, currently to stdout.""" |
| print(log) |
| |
| def log(self, level: Level, message: str): |
| """Log a message. |
| |
| Logs the message with the given severity if it is equal or higher than |
| the current logging level. |
| |
| Keyword arguments: |
| level -- severity of the log message |
| message -- log message |
| """ |
| if level >= self.current_level: |
| log_entry = {} |
| log_entry["ts"] = int(round(time.time() * 1000)) |
| log_entry["crit"] = LEVEL_STRINGS[level] |
| log_entry["id"] = self.procname |
| log_entry["mdc"] = self.mdc |
| log_entry["msg"] = message |
| self._output_log(json.dumps(log_entry)) |
| |
| def error(self, message: str): |
| """Log an error message. Equals to log(ERROR, msg).""" |
| self.log(Level.ERROR, message) |
| |
| def warning(self, message: str): |
| """Log a warning message. Equals to log(WARNING, msg).""" |
| self.log(Level.WARNING, message) |
| |
| def info(self, message: str): |
| """Log an info message. Equals to log(INFO, msg).""" |
| self.log(Level.INFO, message) |
| |
| def debug(self, message: str): |
| """Log a debug message. Equals to log(DEBUG, msg).""" |
| self.log(Level.DEBUG, message) |
| |
| def set_level(self, level: Level): |
| """Set current logging level. |
| |
| Keyword arguments: |
| level -- logging level. Log messages with lower severity will be |
| filtered. |
| """ |
| try: |
| self.current_level = Level(level) |
| except ValueError: |
| pass |
| |
| def get_level(self) -> Level: |
| """Return the current logging level.""" |
| return self.current_level |
| |
| def add_mdc(self, key: str, value: Value): |
| """Add a logger specific MDC. |
| |
| If an MDC with the given key exists, it is replaced with the new one. |
| An MDC can be removed with remove_mdc() or clean_mdc(). |
| |
| Keyword arguments: |
| key -- MDC key |
| value -- MDC value |
| """ |
| self.mdc[key] = value |
| |
| def get_env_params_values(self): |
| |
| try: |
| self.mdc['SYSTEM_NAME'] = os.environ['SYSTEM_NAME'] |
| except Exception: |
| self.mdc['SYSTEM_NAME'] = "" |
| |
| try: |
| self.mdc['HOST_NAME'] = os.environ['HOST_NAME'] |
| except Exception: |
| self.mdc['HOST_NAME'] = "" |
| |
| try: |
| self.mdc['SERVICE_NAME'] = os.environ['SERVICE_NAME'] |
| except Exception: |
| self.mdc['SERVICE_NAME'] = "" |
| |
| try: |
| self.mdc['CONTAINER_NAME'] = os.environ['CONTAINER_NAME'] |
| except Exception: |
| self.mdc['CONTAINER_NAME'] = "" |
| |
| try: |
| self.mdc['POD_NAME'] = os.environ['POD_NAME'] |
| except Exception: |
| self.mdc['POD_NAME'] = "" |
| try: |
| self.mdc['PID'] = os.getpid() |
| except Exception: |
| self.mdc['PID'] = "" |
| |
| def update_mdc_log_level_severity(self, level): |
| |
| severity_level = Level.ERROR |
| |
| if (level == ""): |
| print("Invalid Log Level defined in ConfigMap") |
| elif ((level.upper() == "ERROR") or (level.upper() == "ERR")): |
| severity_level = Level.ERROR |
| elif ((level.upper() == "WARNING") or (level.upper() == "WARN")): |
| severity_level = Level.WARNING |
| elif (level.upper() == "INFO"): |
| severity_level = Level.INFO |
| elif (level.upper() == "DEBUG"): |
| severity_level = Level.DEBUG |
| |
| self.set_level(severity_level) |
| |
| def parse_file(self): |
| src = open(self.filename, 'r') |
| level = "" |
| for line in src: |
| if 'log-level:' in line: |
| level_tmp = str(line.split(':')[-1]).strip() |
| level = level_tmp |
| break |
| src.close() |
| self.update_mdc_log_level_severity(level) |
| |
| def monitor_loglevel_change_handler(self): |
| i = inotify.adapters.Inotify() |
| i.add_watch(self.dirname) |
| for event in i.event_gen(): |
| if (event is not None) and ('IN_MODIFY' in str(event[1]) or 'IN_DELETE' in str(event[1])): |
| self.parse_file() |
| |
| def register_log_change_notify(self): |
| t1 = threading.Thread(target=self.monitor_loglevel_change_handler) |
| t1.daemon = True |
| try: |
| t1.start() |
| except (KeyboardInterrupt, SystemExit): |
| # TODO: add cleanup handler |
| # cleanup_stop_thread() |
| sys.exit() |
| |
| def get_mdc(self, key: str) -> Value: |
| """Return logger's MDC value with the given key or None.""" |
| try: |
| return self.mdc[key] |
| except KeyError: |
| return None |
| |
| def remove_mdc(self, key: str): |
| """Remove logger's MDC with the given key.""" |
| try: |
| del self.mdc[key] |
| except KeyError: |
| pass |
| |
| def clean_mdc(self): |
| """Remove all MDCs of the logger instance.""" |
| self.mdc = {} |