1 # ================================================================================
2 # Copyright (c) 2019-2020 AT&T Intellectual Property. All rights reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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 # ============LICENSE_END=========================================================
17 """generic class to keep track of request handling
18 from receiving it through reponse and log all the activities
20 call Audit.init("component-name", "path/to/config_file") to init the loggers before any requests
22 start each outside request with creation of the Audit object
23 audit = Audit(request_id=None, headers=None, msg=None)
32 from datetime import datetime
33 from threading import Lock
36 from .CommonLogger import CommonLogger
37 from .health import Health
39 REQUEST_X_ECOMP_REQUESTID = "X-ECOMP-RequestID"
40 REQUEST_REMOTE_ADDR = "Remote-Addr"
44 AUDIT_REQUESTID = 'requestID'
45 AUDIT_IPADDRESS = 'IPAddress'
46 AUDIT_SERVER = 'server'
47 AUDIT_TARGET_ENTITY = 'targetEntity'
49 HEADER_CLIENTAUTH = "clientauth"
50 HEADER_AUTHORIZATION = "authorization"
52 class AuditHttpCode(Enum):
53 """audit http codes"""
55 PERMISSION_UNAUTHORIZED_ERROR = 401
56 PERMISSION_FORBIDDEN_ERROR = 403
58 DATA_NOT_FOUND_ERROR = 404
59 SERVER_INTERNAL_ERROR = 500
60 SERVICE_UNAVAILABLE_ERROR = 503
64 class AuditResponseCode(Enum):
65 """audit response codes"""
67 PERMISSION_ERROR = 100
68 AVAILABILITY_ERROR = 200
71 BUSINESS_PROCESS_ERROR = 500
75 def get_response_code(http_status_code):
76 """calculates the response_code from max_http_status_code"""
77 response_code = AuditResponseCode.UNKNOWN_ERROR
78 if http_status_code <= AuditHttpCode.HTTP_OK.value:
79 response_code = AuditResponseCode.SUCCESS
81 elif http_status_code in [AuditHttpCode.PERMISSION_UNAUTHORIZED_ERROR.value,
82 AuditHttpCode.PERMISSION_FORBIDDEN_ERROR.value]:
83 response_code = AuditResponseCode.PERMISSION_ERROR
84 elif http_status_code == AuditHttpCode.SERVICE_UNAVAILABLE_ERROR.value:
85 response_code = AuditResponseCode.AVAILABILITY_ERROR
86 elif http_status_code == AuditHttpCode.SERVER_INTERNAL_ERROR.value:
87 response_code = AuditResponseCode.BUSINESS_PROCESS_ERROR
88 elif http_status_code in [AuditHttpCode.DATA_ERROR.value,
89 AuditHttpCode.RESPONSE_ERROR.value,
90 AuditHttpCode.DATA_NOT_FOUND_ERROR.value]:
91 response_code = AuditResponseCode.DATA_ERROR
92 elif http_status_code == AuditHttpCode.SCHEMA_ERROR.value:
93 response_code = AuditResponseCode.SCHEMA_ERROR
98 def get_human_text(response_code):
99 """convert enum name into human readable text"""
100 if not response_code:
102 return response_code.name.lower().replace("_", " ")
105 """put the audit object on stack per each initiating request in the system
107 :request_id: is the X-ECOMP-RequestID for tracing
109 :req_message: is the request message string for logging
111 :aud_parent: is the parent request - used for sub-query metrics to other systems
113 :kwargs: - put any request related params into kwargs
116 _service_version = ""
117 _service_instance_uuid = str(uuid.uuid4())
118 _started = datetime.now()
121 _logger_metrics = None
124 _py_ver = sys.version.replace("\n", "")
127 def init(service_name, service_version, config_file_path):
128 """init static invariants and loggers"""
129 Audit._service_name = service_name
130 Audit._service_version = service_version
131 Audit._logger_debug = CommonLogger(config_file_path, "debug", \
132 instanceUUID=Audit._service_instance_uuid, serviceName=Audit._service_name)
133 Audit._logger_error = CommonLogger(config_file_path, "error", \
134 instanceUUID=Audit._service_instance_uuid, serviceName=Audit._service_name)
135 Audit._logger_metrics = CommonLogger(config_file_path, "metrics", \
136 instanceUUID=Audit._service_instance_uuid, serviceName=Audit._service_name)
137 Audit._logger_audit = CommonLogger(config_file_path, "audit", \
138 instanceUUID=Audit._service_instance_uuid, serviceName=Audit._service_name)
142 """returns json for health check"""
145 "service_name" : Audit._service_name,
146 "service_version" : Audit._service_version,
147 "service_instance_UUID" : Audit._service_instance_uuid,
148 "python" : Audit._py_ver,
149 "started" : str(Audit._started),
151 "uptime" : str(now - Audit._started),
152 "stats" : Audit._health.dump(),
153 "packages" : "N/A" # Audit._packages
156 def __init__(self, request_id=None, req_message=None, aud_parent=None, **kwargs):
157 """create audit object per each request in the system
159 :request_id: is the X-ECOMP-RequestID for tracing
160 :req_message: is the request message string for logging
161 :aud_parent: is the parent Audit - used for sub-query metrics to other systems
162 :kwargs: - put any request related params into kwargs
164 self.request_id = request_id
165 self.req_message = req_message or ""
166 self.aud_parent = aud_parent
167 self.kwargs = kwargs or {}
169 self.retry_get_config = False
170 self.max_http_status_code = 0
174 if not self.request_id:
175 self.request_id = self.aud_parent.request_id
176 if not self.req_message:
177 self.req_message = self.aud_parent.req_message
178 self.kwargs = self.aud_parent.merge_all_kwargs(**self.kwargs)
180 headers = self.kwargs.get("headers", {})
182 if not self.request_id:
183 self.request_id = headers.get(REQUEST_X_ECOMP_REQUESTID)
184 if AUDIT_IPADDRESS not in self.kwargs:
185 self.kwargs[AUDIT_IPADDRESS] = headers.get(REQUEST_REMOTE_ADDR)
186 if AUDIT_SERVER not in self.kwargs:
187 self.kwargs[AUDIT_SERVER] = headers.get(REQUEST_HOST)
189 if AUDIT_SERVER not in self.kwargs:
190 self.kwargs[AUDIT_SERVER] = os.environ.get(HOSTNAME)
193 if not self.request_id:
194 created_req = " with new"
195 self.request_id = str(uuid.uuid4())
197 self.kwargs[AUDIT_REQUESTID] = self.request_id
199 self._started = time.time()
200 self._start_event = Audit._logger_audit.getStartRecordEvent()
203 if not self.aud_parent:
204 self.info("new audit{0} request_id {1}, msg({2}), kwargs({3})"\
205 .format(created_req, self.request_id, self.req_message, json.dumps(self.kwargs)))
207 def merge_all_kwargs(self, **kwargs):
208 """returns the merge of copy of self.kwargs with the param kwargs"""
209 all_kwargs = self.kwargs.copy()
211 all_kwargs.update(kwargs)
214 def set_http_status_code(self, http_status_code):
215 """accumulate the highest(worst) http status code"""
217 if self.max_http_status_code < AuditHttpCode.SERVER_INTERNAL_ERROR.value:
218 self.max_http_status_code = max(http_status_code, self.max_http_status_code)
221 def get_max_http_status_code(self):
222 """returns the highest(worst) http status code"""
224 max_http_status_code = self.max_http_status_code
226 return max_http_status_code
229 def get_status_code(success):
230 """COMPLETE versus ERROR"""
236 def hide_secrets(obj):
237 """hides the known secret field values of the dictionary"""
238 if not isinstance(obj, dict):
242 if key.lower() in [HEADER_CLIENTAUTH, HEADER_AUTHORIZATION]:
244 elif isinstance(obj[key], dict):
245 obj[key] = Audit.hide_secrets(obj[key])
250 def log_json_dumps(obj, **kwargs):
251 """hide the known secret field values of the dictionary and return json.dumps"""
252 if not isinstance(obj, dict):
253 return json.dumps(obj, **kwargs)
255 return json.dumps(Audit.hide_secrets(copy.deepcopy(obj)), **kwargs)
257 def is_serious_error(self, status_code):
258 """returns whether the response_code is success and a human text for response code"""
259 return AuditResponseCode.PERMISSION_ERROR.value \
260 == AuditResponseCode.get_response_code(status_code).value \
261 or self.get_max_http_status_code() >= AuditHttpCode.SERVER_INTERNAL_ERROR.value
263 def _get_response_status(self):
264 """calculates the response status fields from max_http_status_code"""
265 max_http_status_code = self.get_max_http_status_code()
266 response_code = AuditResponseCode.get_response_code(max_http_status_code)
267 success = (response_code.value == AuditResponseCode.SUCCESS.value)
268 response_description = AuditResponseCode.get_human_text(response_code)
269 return success, max_http_status_code, response_code, response_description
271 def is_success(self):
272 """returns whether the response_code is success and a human text for response code"""
273 success, _, _, _ = self._get_response_status()
276 def debug(self, log_line, **kwargs):
277 """debug - the debug=lowest level of logging"""
278 Audit._logger_debug.debug(log_line, **self.merge_all_kwargs(**kwargs))
280 def info(self, log_line, **kwargs):
281 """debug - the info level of logging"""
282 Audit._logger_debug.info(log_line, **self.merge_all_kwargs(**kwargs))
284 def info_requested(self, result=None, **kwargs):
285 """info "requested ..." - the info level of logging"""
286 self.info("requested {0} {1}".format(self.req_message, result or ""), \
287 **self.merge_all_kwargs(**kwargs))
289 def warn(self, log_line, **kwargs):
290 """debug+error - the warn level of logging"""
291 all_kwargs = self.merge_all_kwargs(**kwargs)
292 Audit._logger_debug.warn(log_line, **all_kwargs)
293 Audit._logger_error.warn(log_line, **all_kwargs)
295 def error(self, log_line, **kwargs):
296 """debug+error - the error level of logging"""
297 all_kwargs = self.merge_all_kwargs(**kwargs)
298 Audit._logger_debug.error(log_line, **all_kwargs)
299 Audit._logger_error.error(log_line, **all_kwargs)
301 def fatal(self, log_line, **kwargs):
302 """debug+error - the fatal level of logging"""
303 all_kwargs = self.merge_all_kwargs(**kwargs)
304 Audit._logger_debug.fatal(log_line, **all_kwargs)
305 Audit._logger_error.fatal(log_line, **all_kwargs)
308 def get_elapsed_time(started):
309 """returns the elapsed time since started in milliseconds"""
310 return int(round(1000 * (time.time() - started)))
312 def metrics_start(self, log_line=None, **kwargs):
313 """reset metrics timing"""
314 self._metrics_started = time.time()
315 self._metrics_start_event = Audit._logger_metrics.getStartRecordEvent()
317 self.info(log_line, **self.merge_all_kwargs(**kwargs))
319 def metrics(self, log_line, **kwargs):
320 """debug+metrics - the metrics=sub-audit level of logging"""
321 all_kwargs = self.merge_all_kwargs(**kwargs)
322 success, max_http_status_code, response_code, response_description = \
323 self._get_response_status()
325 timer = Audit.get_elapsed_time(self._metrics_started)
327 log_line = "done: {0}".format(log_line)
328 self.info(log_line, **all_kwargs)
329 metrics_func = Audit._logger_metrics.info
330 Audit._health.success(all_kwargs.get(AUDIT_TARGET_ENTITY, Audit._service_name), timer)
332 log_line = "failed: {0}".format(log_line)
333 self.error(log_line, errorCode=response_code.value, \
334 errorDescription=response_description, **all_kwargs)
335 metrics_func = Audit._logger_metrics.error
336 Audit._health.error(all_kwargs.get(AUDIT_TARGET_ENTITY, Audit._service_name), timer)
338 metrics_func(log_line, begTime=self._metrics_start_event, timer=timer,
339 statusCode=Audit.get_status_code(success), responseCode=response_code.value,
340 responseDescription=response_description,
345 return (success, max_http_status_code, response_description)
347 def audit_done(self, result=None, **kwargs):
348 """debug+audit - the audit=top level of logging"""
349 all_kwargs = self.merge_all_kwargs(**kwargs)
350 success, max_http_status_code, response_code, response_description = \
351 self._get_response_status()
352 log_line = "{0} {1}".format(self.req_message, result or "").strip()
354 timer = Audit.get_elapsed_time(self._started)
356 log_line = "done: {0}".format(log_line)
357 self.info(log_line, **all_kwargs)
358 audit_func = Audit._logger_audit.info
359 Audit._health.success(all_kwargs.get(AUDIT_TARGET_ENTITY, Audit._service_name), timer)
361 log_line = "failed: {0}".format(log_line)
362 self.error(log_line, errorCode=response_code.value,
363 errorDescription=response_description, **all_kwargs)
364 audit_func = Audit._logger_audit.error
365 Audit._health.error(all_kwargs.get(AUDIT_TARGET_ENTITY, Audit._service_name), timer)
367 audit_func(log_line, begTime=self._start_event, timer=timer,
368 statusCode=Audit.get_status_code(success),
369 responseCode=response_code.value,
370 responseDescription=response_description,
374 return (success, max_http_status_code, response_description)
375 # this line added to test