526b06547a28f7c34af645a5a999e70f0d204d33
[ric-plt/xapp-frame-py.git] / ricxappframe / logger / mdclogger.py
1 # Copyright (c) 2019 AT&T Intellectual Property.
2 # Copyright (c) 2018-2019 Nokia.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 # This source code is part of the near-RT RIC (RAN Intelligent Controller)
17 # platform project (RICP).
18 #
19
20 """Structured logging library with Mapped Diagnostic Context
21
22 Outputs the log entries to standard out in structured format, json currently.
23 Severity based filtering.
24 Supports Mapped Diagnostic Context (MDC).
25
26 Set MDC pairs are automatically added to log entries by the library.
27 """
28 from typing import TypeVar
29 from enum import IntEnum
30 import sys
31 import json
32 import time
33 import os
34 import inotify.adapters
35 import threading
36
37
38 class Level(IntEnum):
39     """Severity levels of the log messages."""
40     DEBUG = 10
41     INFO = 20
42     WARNING = 30
43     ERROR = 40
44
45
46 LEVEL_STRINGS = {Level.DEBUG: "DEBUG",
47                  Level.INFO: "INFO",
48                  Level.WARNING: "WARNING",
49                  Level.ERROR: "ERROR"}
50
51
52 Value = TypeVar('Value', str, int)
53
54
55 class MDCLogger():
56     """Initialize the mdclogging module.
57     Calling of the function is optional. If not called, the process name
58     (sys.argv[0]) is used by default.
59
60     Keyword arguments:
61     name -- name of the component. The name will appear as part of the log
62             entries.
63     """
64     def __init__(self, name: str = sys.argv[0], level: Level = Level.ERROR):
65         """Initialize a Logger instance.
66
67             Keyword arguments:
68             name -- name of the component. The name will appear as part of the
69                     log entries.
70         """
71         self.procname = name
72         self.current_level = level
73         self.mdc = {}
74
75     # Pass configmap_monitor = True to monitor configmap to change logs dynamically using configmap
76
77     def mdclog_format_init(self, configmap_monitor=False):
78
79         self.mdc = {"PID": "", "SYSTEM_NAME": "", "HOST_NAME": "", "SERVICE_NAME": "", "CONTAINER_NAME": "", "POD_NAME": ""}
80         self.get_env_params_values()
81         try:
82             self.filename = os.environ['CONFIG_MAP_NAME']
83             self.dirname = str(self.filename[:self.filename.rindex('/')])
84             self.parse_file()
85
86             if configmap_monitor:
87                 self.register_log_change_notify()
88
89         except Exception as e:
90             print("Unable to Add Watch on ConfigMap File", e)
91
92     def _output_log(self, log: str):
93
94         """Output the log, currently to stdout."""
95         print(log)
96
97     def log(self, level: Level, message: str):
98         """Log a message.
99
100         Logs the message with the given severity if it is equal or higher than
101         the current logging level.
102
103         Keyword arguments:
104         level -- severity of the log message
105         message -- log message
106         """
107         if level >= self.current_level:
108             log_entry = {}
109             log_entry["ts"] = int(round(time.time() * 1000))
110             log_entry["crit"] = LEVEL_STRINGS[level]
111             log_entry["id"] = self.procname
112             log_entry["mdc"] = self.mdc
113             log_entry["msg"] = message
114             self._output_log(json.dumps(log_entry))
115
116     def error(self, message: str):
117         """Log an error message. Equals to log(ERROR, msg)."""
118         self.log(Level.ERROR, message)
119
120     def warning(self, message: str):
121         """Log a warning message. Equals to log(WARNING, msg)."""
122         self.log(Level.WARNING, message)
123
124     def info(self, message: str):
125         """Log an info message. Equals to log(INFO, msg)."""
126         self.log(Level.INFO, message)
127
128     def debug(self, message: str):
129         """Log a debug message. Equals to log(DEBUG, msg)."""
130         self.log(Level.DEBUG, message)
131
132     def set_level(self, level: Level):
133         """Set current logging level.
134
135         Keyword arguments:
136         level -- logging level. Log messages with lower severity will be
137                  filtered.
138         """
139         try:
140             self.current_level = Level(level)
141         except ValueError:
142             pass
143
144     def get_level(self) -> Level:
145         """Return the current logging level."""
146         return self.current_level
147
148     def add_mdc(self, key: str, value: Value):
149         """Add a logger specific MDC.
150
151         If an MDC with the given key exists, it is replaced with the new one.
152         An MDC can be removed with remove_mdc() or clean_mdc().
153
154         Keyword arguments:
155         key -- MDC key
156         value -- MDC value
157         """
158         self.mdc[key] = value
159
160     def get_env_params_values(self):
161
162         try:
163             self.mdc['SYSTEM_NAME'] = os.environ['SYSTEM_NAME']
164         except Exception:
165             self.mdc['SYSTEM_NAME'] = ""
166
167         try:
168             self.mdc['HOST_NAME'] = os.environ['HOST_NAME']
169         except Exception:
170             self.mdc['HOST_NAME'] = ""
171
172         try:
173             self.mdc['SERVICE_NAME'] = os.environ['SERVICE_NAME']
174         except Exception:
175             self.mdc['SERVICE_NAME'] = ""
176
177         try:
178             self.mdc['CONTAINER_NAME'] = os.environ['CONTAINER_NAME']
179         except Exception:
180             self.mdc['CONTAINER_NAME'] = ""
181
182         try:
183             self.mdc['POD_NAME'] = os.environ['POD_NAME']
184         except Exception:
185             self.mdc['POD_NAME'] = ""
186         try:
187             self.mdc['PID'] = os.getpid()
188         except Exception:
189             self.mdc['PID'] = ""
190
191     def update_mdc_log_level_severity(self, level):
192
193         severity_level = Level.ERROR
194
195         if(level == ""):
196             print("Invalid Log Level defined in ConfigMap")
197         elif((level.upper() == "ERROR") or (level.upper() == "ERR")):
198             severity_level = Level.ERROR
199         elif((level.upper() == "WARNING") or (level.upper() == "WARN")):
200             severity_level = Level.WARNING
201         elif(level.upper() == "INFO"):
202             severity_level = Level.INFO
203         elif(level.upper() == "DEBUG"):
204             severity_level = Level.DEBUG
205
206         self.set_level(severity_level)
207
208     def parse_file(self):
209         src = open(self.filename, 'r')
210         level = ""
211         for line in src:
212             if 'log-level:' in line:
213                 level_tmp = str(line.split(':')[-1]).strip()
214                 level = level_tmp
215                 break
216         src.close()
217         self.update_mdc_log_level_severity(level)
218
219     def monitor_loglevel_change_handler(self):
220         i = inotify.adapters.Inotify()
221         i.add_watch(self.dirname)
222         for event in i.event_gen():
223             if (event is not None) and ('IN_MODIFY' in str(event[1]) or 'IN_DELETE' in str(event[1])):
224                 self.parse_file()
225
226     def register_log_change_notify(self):
227         t1 = threading.Thread(target=self.monitor_loglevel_change_handler)
228         t1.daemon = True
229         try:
230             t1.start()
231         except (KeyboardInterrupt, SystemExit):
232             # TODO: add cleanup handler
233             # cleanup_stop_thread()
234             sys.exit()
235
236     def get_mdc(self, key: str) -> Value:
237         """Return logger's MDC value with the given key or None."""
238         try:
239             return self.mdc[key]
240         except KeyError:
241             return None
242
243     def remove_mdc(self, key: str):
244         """Remove logger's MDC with the given key."""
245         try:
246             del self.mdc[key]
247         except KeyError:
248             pass
249
250     def clean_mdc(self):
251         """Remove all MDCs of the logger instance."""
252         self.mdc = {}