93e4cf226550cd9fcb4ddf2ff2587f66e4c168a4
[com/pylog.git] / 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 """Structured logging library with Mapped Diagnostic Context
17
18 Outputs the log entries to standard out in structured format, json currently.
19 Severity based filtering.
20 Supports Mapped Diagnostic Context (MDC).
21
22 Set MDC pairs are automatically added to log entries by the library.
23 """
24 from typing import TypeVar
25 from enum import IntEnum
26 import sys
27 import json
28 import time
29
30
31 class Level(IntEnum):
32     """Severity levels of the log messages."""
33     DEBUG = 10
34     INFO = 20
35     WARNING = 30
36     ERROR = 40
37
38
39 LEVEL_STRINGS = {Level.DEBUG: "DEBUG",
40                  Level.INFO: "INFO",
41                  Level.WARNING: "WARNING",
42                  Level.ERROR: "ERROR"}
43
44
45 Value = TypeVar('Value', str, int)
46
47
48 class Logger():
49     """Initialize the mdclogging module.
50     Calling of the function is optional. If not called, the process name
51     (sys.argv[0]) is used by default.
52
53     Keyword arguments:
54     name -- name of the component. The name will appear as part of the log
55             entries.
56     """
57     def __init__(self, name: str = sys.argv[0], level: Level = Level.DEBUG):
58         """Initialize a Logger instance.
59
60             Keyword arguments:
61             name -- name of the component. The name will appear as part of the
62                     log entries.
63         """
64         self.procname = name
65         self.current_level = level
66         self.mdc = {}
67
68     def _output_log(self, log: str):
69         """Output the log, currently to stdout."""
70         print(log)
71
72     def log(self, level: Level, message: str):
73         """Log a message.
74
75         Logs the message with the given severity if it is equal or higher than
76         the current logging level.
77
78         Keyword arguments:
79         level -- severity of the log message
80         message -- log message
81         """
82         if level >= self.current_level:
83             log_entry = {}
84             log_entry["ts"] = int(round(time.time() * 1000))
85             log_entry["crit"] = LEVEL_STRINGS[level]
86             log_entry["id"] = self.procname
87             log_entry["mdc"] = self.mdc
88             log_entry["msg"] = message
89             self._output_log(json.dumps(log_entry))
90
91     def error(self, message: str):
92         """Log an error message. Equals to log(ERROR, msg)."""
93         self.log(Level.ERROR, message)
94
95     def warning(self, message: str):
96         """Log a warning message. Equals to log(WARNING, msg)."""
97         self.log(Level.WARNING, message)
98
99     def info(self, message: str):
100         """Log an info message. Equals to log(INFO, msg)."""
101         self.log(Level.INFO, message)
102
103     def debug(self, message: str):
104         """Log a debug message. Equals to log(DEBUG, msg)."""
105         self.log(Level.DEBUG, message)
106
107     def set_level(self, level: Level):
108         """Set current logging level.
109
110         Keyword arguments:
111         level -- logging level. Log messages with lower severity will be
112                  filtered.
113         """
114         if level in Level:
115             self.current_level = level
116
117     def get_level(self) -> Level:
118         """Return the current logging level."""
119         return self.current_level
120
121     def add_mdc(self, key: str, value: Value):
122         """Add a logger specific MDC.
123
124         If an MDC with the given key exists, it is replaced with the new one.
125         An MDC can be removed with remove_mdc() or clean_mdc().
126
127         Keyword arguments:
128         key -- MDC key
129         value -- MDC value
130         """
131         self.mdc[key] = value
132
133     def get_mdc(self, key: str) -> Value:
134         """Return logger's MDC value with the given key or None."""
135         try:
136             return self.mdc[key]
137         except KeyError:
138             return None
139
140     def remove_mdc(self, key: str):
141         """Remove logger's MDC with the given key."""
142         try:
143             del self.mdc[key]
144         except KeyError:
145             pass
146
147     def clean_mdc(self):
148         """Remove all MDCs of the logger instance."""
149         self.mdc = {}