(20241008) logging decorator and model improved.

This commit is contained in:
2024-10-08 15:36:20 +05:30
parent 1880840ae8
commit d9117f2a92
33 changed files with 1406 additions and 226 deletions
+31 -85
View File
@@ -46,13 +46,8 @@ from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.security import sanitizers
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.log import APILogModel
from utils_v2.api.response import ResponseModel
from utils_v2.api.metrics_prometheus import (
TOTAL_REQUEST_COUNT,
LIVE_REQUEST_COUNT,
REQUEST_LATENCY,
MetricsAPI
)
# To work with date and time:
import time
@@ -60,9 +55,6 @@ import datetime
# System-level activities:
import io
import distro
import socket
import platform
# For Pydantic data-models:
import pydantic
@@ -90,11 +82,6 @@ import asyncio
# *****************************************************************************************************************
# Info for logging that will stay constant during runtime:
SERVER_HOSTNAME = socket.gethostname()
PLATFORM_INFO = platform.uname()
HOST_OS = distro.name(True)
# Chars to choose from for random strings:
ALPHANUMERIC_CHARS = string.ascii_letters + string.digits
@@ -572,6 +559,8 @@ def validate_input(
def log_request_to_mongo(
attr_name,
collection: str = "logs",
api_version: str = None,
project: str = None,
log_type: str = None,
operation: str = None,
log_input: bool = True,
@@ -586,6 +575,8 @@ def log_request_to_mongo(
:param attr_name: The name of the variable that holds the instance of 'AsyncMongo'. It should be accessible in the
scope of 'current_app'.
:param collection: The name of the collection to write the log into.
:param api_version: The version code of the API endpoint that is being logged.
:param project: The name of the project that the endpoint was built for.
:param log_type: A hint to identify what the log was for.
:param operation: A hint to identify what was action was being performed.
:param log_input: Whether, or not, you would like to log the input that came in.
@@ -602,6 +593,9 @@ def log_request_to_mongo(
# Let the next in-line decorator know that it has been wrapped:
kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
# Set the api version as needed:
kwargs["api_version"] = kwargs.get("api_version", api_version)
# Make variables and extract available info.:
exception = None
response = None
@@ -635,7 +629,8 @@ def log_request_to_mongo(
elif isinstance(response, tuple): response_to_log, http_code_to_log = response
else: response_to_log, http_code_to_log = str(response), 200
# Try to get the information about the request:
# Try to get the information about the request.
# There will be no data in any of these if the decorator was used to catch start-up and shut-down events.
request_method = None
request_url = None
request_route = None
@@ -653,41 +648,37 @@ def log_request_to_mongo(
except: pass
# Construct the log:
# for k in sensitive_keys: kwargs.get("inbound_headers", {}).pop(k, None)
# for k in sensitive_keys: kwargs.get("inbound_data", {}).pop(k, None)
log_json = {
"hostname": SERVER_HOSTNAME,
"os": f"{HOST_OS}",
"cpu": f"{PLATFORM_INFO.processor} ({PLATFORM_INFO.machine})",
"logId": kwargs.get("log_id"),
"logChain": kwargs.get("inbound_headers", {}).get("X-Log-Chain"),
"log": log_type,
"operation": operation,
"apiVer": kwargs.get("api_version"),
"method": request_method,
"url": request_url,
"route": request_route,
"ts": request_ts,
"tat": time.perf_counter() - start_ts,
"cpuTime": time.process_time() - cpu_start_ts,
"headers": kwargs.get("inbound_headers"),
"data": kwargs.get("inbound_data") if log_input else "not logged",
"files": {
api_log = APILogModel(
project = project,
log = log_type,
operation = operation,
apiVer = kwargs.get("api_version"),
logId = kwargs.get("log_id"),
logChain = kwargs.get("inbound_headers", {}).get("X-Log-Chain"),
method = request_method,
url = request_url,
route = request_route,
ts = request_ts,
tat = time.perf_counter() - start_ts,
cpuTime = time.process_time() - cpu_start_ts,
headers = kwargs.get("inbound_headers"),
data = kwargs.get("inbound_data") if log_input else "not logged",
files = {
k: {
"name": v["name"],
"size": v["size"]
} for k, v in kwargs.get("inbound_files", {}).items()
},
"exception": None if exception is None else describe_exception(exception),
"response": response_to_log,
"httpCode": http_code_to_log
}
exception = None if exception is None else describe_exception(exception),
response = response_to_log,
httpCode = http_code_to_log
)
# Write the log:
app_attr = getattr(current_app, attr_name)
inserted_id = await app_attr.insert_one(
collection = collection,
document = log_json
document = api_log.model_dump()
)
# Return the response from the wrapped function.
@@ -950,51 +941,6 @@ def handle_cancelled_request(cleanup_func = None, cleanup_coro = None):
return decorator
# ---------------------------------------------------------------------------------------------------------------------
def measure_metrics_for_prometheus():
"""
Use this decorator to automatically measure metrics for using in Prometheus.
:return: The decorator factory.
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Let the next in-line decorator know that it has been wrapped:
kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
# We use the context of the measurement class:
async with MetricsAPI(
method = f"{request.method}",
endpoint = str(request.url_rule.rule)
) as metrics:
# Invoke the wrapped function:
response = await func(*args, **kwargs)
kwargs["decorator_count"] -= 1
# Interpret the HTTP code:
if isinstance(response, ResponseModel): _, metrics.http_code = response.for_quart()
elif isinstance(response, tuple): metrics.http_code = response[1]
else: metrics.http_code = 200
# Done here:
if (
kwargs["decorator_count"] == 1 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
return wrapper
return decorator
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***