added dynamic log level, reflecting from config map
[com/pylog.git] / mdclogpy / mdclogpy / Logger.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 Logger():
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
76     # Pass configmap_monitor = True to monitor configmap to change logs dynamically using configmap
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 == True:
87                 self.register_log_change_notify()
88             
89         except Exception as e:
90             print("Unable to Add Watch on ConfigMap File",e)
91
92
93     def _output_log(self, log: str):
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
161     def get_env_params_values(self):
162
163         try:
164             self.mdc['SYSTEM_NAME'] = os.environ['SYSTEM_NAME']
165         except:
166             self.mdc['SYSTEM_NAME'] = ""
167
168         try:
169             self.mdc['HOST_NAME'] = os.environ['HOST_NAME']
170         except:
171             self.mdc['HOST_NAME'] = ""
172
173         try:
174             self.mdc['SERVICE_NAME'] = os.environ['SERVICE_NAME']
175         except:
176             self.mdc['SERVICE_NAME'] = ""
177     
178         try:
179             self.mdc['CONTAINER_NAME'] = os.environ['CONTAINER_NAME']
180         except:
181             self.mdc['CONTAINER_NAME'] = ""
182
183         try:
184             self.mdc['POD_NAME'] = os.environ['POD_NAME']
185         except:
186             self.mdc['POD_NAME'] = ""
187         try:
188             self.mdc['PID'] = os.getpid()
189         except:
190             self.mdc['PID'] = ""
191
192
193     
194     def update_mdc_log_level_severity(self,level):
195         severity_level = Level.ERROR
196
197         if(level == ""):
198             print("Invalid Log Level defined in ConfigMap")
199         elif((level.upper() == "ERROR") or (level.upper() == "ERR" )):
200             severity_level = Level.ERROR
201         elif((level.upper() == "WARNING") or (level.upper() == "WARN")):
202             severity_level = Level.WARNING
203         elif(level.upper() == "INFO"):
204             severity_level = Level.INFO
205         elif(level.upper() == "DEBUG"):
206             severity_level = Level.DEBUG
207         
208         self.set_level(severity_level)
209       
210         
211         
212
213
214     def parse_file(self):
215         src = open(self.filename,'r')
216         level = ""
217         for line in src:
218             if 'log-level:' in line:
219                 level_tmp = str(line.split(':')[-1]).strip()
220                 level = level_tmp
221                 break
222         src.close()
223         self.update_mdc_log_level_severity(level)
224         
225
226
227
228     def monitor_loglevel_change_handler(self):
229         i = inotify.adapters.Inotify()
230         i.add_watch(self.dirname)
231         for event in i.event_gen():
232             if (event is not None) and ('IN_MODIFY' in str(event[1]) or 'IN_DELETE' in str(event[1])):
233                 self.parse_file()
234
235
236     def register_log_change_notify(self):
237         t1 = threading.Thread(target=self.monitor_loglevel_change_handler)
238         t1.daemon = True
239         try:
240             t1.start() 
241         except (KeyboardInterrupt, SystemExit):
242             cleanup_stop_thread()
243             sys.exit()
244         
245
246
247
248     def get_mdc(self, key: str) -> Value:
249         """Return logger's MDC value with the given key or None."""
250         try:
251             return self.mdc[key]
252         except KeyError:
253             return None
254
255     def remove_mdc(self, key: str):
256         """Remove logger's MDC with the given key."""
257         try:
258             del self.mdc[key]
259         except KeyError:
260             pass
261
262     def clean_mdc(self):
263         """Remove all MDCs of the logger instance."""
264         self.mdc = {}