From 538fc3648e63222434dc8189569c92ee24835c57 Mon Sep 17 00:00:00 2001 From: Michele Mondelli Date: Mon, 13 Oct 2025 16:24:20 +0200 Subject: [PATCH] feat: add request state --- .github/workflows/publish-tag.yaml | 5 ++ app/pyproject.toml | 2 +- app/src/zcs/core/exception/exception.py | 19 ++++--- app/src/zcs/core/logger/logger.py | 19 ++++++- app/src/zcs/core/session/__init__.py | 2 + app/src/zcs/core/session/request_context.py | 3 + app/src/zcs/core/session/request_state.py | 63 +++++++++++++++++++++ 7 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 app/src/zcs/core/session/__init__.py create mode 100644 app/src/zcs/core/session/request_context.py create mode 100644 app/src/zcs/core/session/request_state.py diff --git a/.github/workflows/publish-tag.yaml b/.github/workflows/publish-tag.yaml index e2360e7..c7cfa45 100644 --- a/.github/workflows/publish-tag.yaml +++ b/.github/workflows/publish-tag.yaml @@ -28,6 +28,11 @@ jobs: service_account: github-actions-sa@ai-accounting-405809.iam.gserviceaccount.com access_token_lifetime: 600s + - name: Set application version + run: | + sed -i 's|version = "\(.*\)"|version = "${{ github.ref_name }}"|g' app/pyproject.toml && + cat app/pyproject.toml + - name: Setup Python uses: actions/setup-python@v3 diff --git a/app/pyproject.toml b/app/pyproject.toml index 621ca6f..0f4b02e 100644 --- a/app/pyproject.toml +++ b/app/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "zcs-python-core" -version = "0.3.0" +version = "0.dev0" description = "A Python library that provides core functionalities for ZCS projects" [tool.setuptools.packages.find] diff --git a/app/src/zcs/core/exception/exception.py b/app/src/zcs/core/exception/exception.py index 02615a3..536b933 100644 --- a/app/src/zcs/core/exception/exception.py +++ b/app/src/zcs/core/exception/exception.py @@ -1,12 +1,16 @@ import random import string import traceback +from typing import Optional + class ZcsException(Exception): - def __init__(self, user_message = "Unknown error.", error_source = "Unspecified", status_code = 500, internal_message = None): + def __init__( + self, user_message: str = "Unknown error.", error_source: str = "Unspecified", + status_code: int = 500, internal_message: Optional[str] = None): super().__init__(user_message) - + self.__error_source = error_source self.__status_code = status_code self.__user_message = user_message @@ -15,16 +19,16 @@ def __init__(self, user_message = "Unknown error.", error_source = "Unspecified" def get_status_code(self): return self.__status_code - + def get_error_source(self): return self.__error_source - + def get_user_message(self): return self.__user_message - + def get_internal_message(self): return self.__internal_message - + def get_error_code(self): return self.__error_code @@ -37,7 +41,6 @@ def get_as_dict(self): 'status_code': self.get_status_code(), 'traceback': "".join(traceback.format_exception(self)) } - + def __str__(self): return f"{self.get_error_code()} - {self.get_error_source()} - {self.get_internal_message()} - {self.get_user_message()}" - diff --git a/app/src/zcs/core/logger/logger.py b/app/src/zcs/core/logger/logger.py index 0274a12..f7d7396 100644 --- a/app/src/zcs/core/logger/logger.py +++ b/app/src/zcs/core/logger/logger.py @@ -1,7 +1,10 @@ import json import logging +import time +from logging import LogRecord from zcs.core.exception import ZcsException +from zcs.core.session import request_context, RequestState old_factory = logging.getLogRecordFactory() @@ -31,6 +34,9 @@ def record_factory(*args, **kwargs): class CloudJsonFormatter(logging.Formatter): def format(self, record): + # FIXME + # log op_code, request_id and time deltas + message = record.getMessage() if record.exc_info and record.original_exception: message = str(record.original_exception) @@ -79,8 +85,19 @@ def __init__(self, verbose=False): logging.CRITICAL: logging.Formatter(set_bold_red + base_format + reset) } - def format(self, record): + def format(self, record: LogRecord) -> str: formatter = self.FORMATTERS.get(record.levelno) + + # Retrieve request state from request context + request_state: RequestState = request_context.get() + if request_state: + time_ns = time.perf_counter_ns() + delta_ns = time_ns - request_state.getCheckpointNs() + total_ns = time_ns - request_state.getRequestStartNs() + request_state.setCheckpointNs(time_ns) + record.msg = "[OP_CODE:{}][REQ_ID:{}] {} ({:.2f}s delta - {:.2f}s total)".format( + request_state.getOpCode(), request_state.getRequestId(), record.msg, delta_ns / 1e9, total_ns / 1e9) + return formatter.format(record) diff --git a/app/src/zcs/core/session/__init__.py b/app/src/zcs/core/session/__init__.py new file mode 100644 index 0000000..51c5508 --- /dev/null +++ b/app/src/zcs/core/session/__init__.py @@ -0,0 +1,2 @@ +from .request_context import request_context +from .request_state import RequestState diff --git a/app/src/zcs/core/session/request_context.py b/app/src/zcs/core/session/request_context.py new file mode 100644 index 0000000..54ef9e6 --- /dev/null +++ b/app/src/zcs/core/session/request_context.py @@ -0,0 +1,3 @@ +import contextvars + +request_context = contextvars.ContextVar("request_state", default=None) diff --git a/app/src/zcs/core/session/request_state.py b/app/src/zcs/core/session/request_state.py new file mode 100644 index 0000000..cd56ce6 --- /dev/null +++ b/app/src/zcs/core/session/request_state.py @@ -0,0 +1,63 @@ +import time +import uuid +from typing import Optional + + +class RequestState(): + + def __init__( + self, + request_id: Optional[str] = None, + prefix: Optional[str] = None, + op_code: Optional[str] = None): + + self.__request_id = request_id if request_id else RequestState.generate_op_code(prefix=prefix) + self.__op_code = op_code if op_code else self.__request_id + self.__request_start_ns = time.perf_counter_ns() + self.__checkpoint_ns = self.__request_start_ns + + def getOpCode(self) -> str: + """ + Get operation code value. + """ + + return self.__op_code + + def getRequestId(self) -> str: + """ + Get request id value. + """ + + return self.__request_id + + def getRequestStartNs(self) -> int: + """ + Get request start time in nanoseconds. + """ + + return self.__request_start_ns + + def getCheckpointNs(self) -> int: + """ + Get checkpoint time in nanoseconds. + """ + + return self.__checkpoint_ns + + def setCheckpointNs(self, time_ns: int): + """ + Set checkpoint time in nanoseconds. + """ + + self.__checkpoint_ns = time_ns + + @staticmethod + def generate_op_code(prefix: Optional[str] = None) -> str: + """ + Generate a new unique operation code. + If prefix is provided, prepend it to the generated code. + """ + op_code = str(uuid.uuid4()) + if prefix: + return f"{prefix}_{op_code}" + return op_code