WIP: dev into master #184

Draft
edraft wants to merge 121 commits from dev into master
33 changed files with 171 additions and 81 deletions
Showing only changes of commit 6639946346 - Show all commits

3
.gitignore vendored
View File

@@ -139,3 +139,6 @@ PythonImportHelper-v2-Completion.json
# cpl unittest stuff # cpl unittest stuff
unittests/test_*_playground unittests/test_*_playground
# cpl logs
**/logs/*.jsonl

View File

@@ -203,7 +203,7 @@ class WebApp(ApplicationABC):
_policies.append(policy) _policies.append(policy)
self._policies.extend_policies(_policies) self._policies.extend(_policies)
self.with_middleware(AuthorizationMiddleware) self.with_middleware(AuthorizationMiddleware)
return self return self

View File

@@ -1,8 +1,7 @@
from cpl.core.log import LoggerABC
from cpl.core.log.wrapped_logger import WrappedLogger from cpl.core.log.wrapped_logger import WrappedLogger
class APILogger(WrappedLogger): class APILogger(WrappedLogger):
def __init__(self, logger: LoggerABC): def __init__(self):
WrappedLogger.__init__(self, logger) WrappedLogger.__init__(self, "api")

View File

@@ -1,5 +1,5 @@
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
from typing import Optional, Any, Coroutine, Awaitable from typing import Optional
from cpl.api.typing import PolicyResolver from cpl.api.typing import PolicyResolver
from cpl.core.ctx import get_user from cpl.core.ctx import get_user

View File

@@ -1,6 +1,5 @@
from typing import Optional from typing import Optional
from cpl.api.model.policy import Policy
from cpl.api.model.api_route import ApiRoute from cpl.api.model.api_route import ApiRoute
from cpl.core.abc.registry_abc import RegistryABC from cpl.core.abc.registry_abc import RegistryABC

View File

@@ -5,7 +5,6 @@ from cpl.application.host import Host
from cpl.core.log.log_level import LogLevel from cpl.core.log.log_level import LogLevel
from cpl.core.log.log_settings import LogSettings from cpl.core.log.log_settings import LogSettings
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.core.log.structured_logger import StructuredLogger
from cpl.dependency.service_provider_abc import ServiceProviderABC from cpl.dependency.service_provider_abc import ServiceProviderABC

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from cpl.dependency import ServiceProviderABC from cpl.dependency.service_provider_abc import ServiceProviderABC
class ApplicationExtensionABC(ABC): class ApplicationExtensionABC(ABC):

View File

@@ -6,7 +6,7 @@ from cpl.auth import permission as _permission
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin
from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient
from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection
from .auth_logger import AuthLogger from .logger import AuthLogger
from .keycloak_settings import KeycloakSettings from .keycloak_settings import KeycloakSettings
from .permission_seeder import PermissionSeeder from .permission_seeder import PermissionSeeder

View File

@@ -1,8 +0,0 @@
from cpl.core.log import LoggerABC
from cpl.core.log.wrapped_logger import WrappedLogger
class AuthLogger(WrappedLogger):
def __init__(self, logger: LoggerABC):
WrappedLogger.__init__(self, logger)

View File

@@ -1,13 +1,13 @@
from keycloak import KeycloakAdmin as _KeycloakAdmin, KeycloakOpenIDConnection from keycloak import KeycloakAdmin as _KeycloakAdmin, KeycloakOpenIDConnection
from cpl.auth.auth_logger import AuthLogger
from cpl.auth.keycloak_settings import KeycloakSettings from cpl.auth.keycloak_settings import KeycloakSettings
from cpl.auth.logger import AuthLogger
class KeycloakAdmin(_KeycloakAdmin): class KeycloakAdmin(_KeycloakAdmin):
def __init__(self, logger: AuthLogger, settings: KeycloakSettings): def __init__(self, logger: AuthLogger, settings: KeycloakSettings):
logger.info("Initializing Keycloak admin") # logger.info("Initializing Keycloak admin")
_connection = KeycloakOpenIDConnection( _connection = KeycloakOpenIDConnection(
server_url=settings.url, server_url=settings.url,
client_id=settings.client_id, client_id=settings.client_id,

View File

@@ -2,7 +2,7 @@ from typing import Optional
from keycloak import KeycloakOpenID from keycloak import KeycloakOpenID
from cpl.auth.auth_logger import AuthLogger from cpl.auth.logger import AuthLogger
from cpl.auth.keycloak_settings import KeycloakSettings from cpl.auth.keycloak_settings import KeycloakSettings

View File

@@ -0,0 +1,7 @@
from cpl.core.log.wrapped_logger import WrappedLogger
class AuthLogger(WrappedLogger):
def __init__(self):
WrappedLogger.__init__(self, "auth")

View File

@@ -14,7 +14,7 @@ from cpl.auth.schema import (
) )
from cpl.core.utils.get_value import get_value from cpl.core.utils.get_value import get_value
from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
class PermissionSeeder(DataSeederABC): class PermissionSeeder(DataSeederABC):

View File

@@ -9,7 +9,7 @@ from cpl.auth.keycloak import KeycloakAdmin
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC from cpl.database.abc import DbModelABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProviderABC
@@ -36,8 +36,8 @@ class AuthUser(DbModelABC):
return "ANONYMOUS" return "ANONYMOUS"
try: try:
keycloak_admin: KeycloakAdmin = ServiceProviderABC.get_global_service(KeycloakAdmin) keycloak = ServiceProviderABC.get_global_service(KeycloakAdmin)
return keycloak_admin.get_user(self._keycloak_id).get("username") return keycloak.get_user(self._keycloak_id).get("username")
except KeycloakGetError as e: except KeycloakGetError as e:
return "UNKNOWN" return "UNKNOWN"
except Exception as e: except Exception as e:
@@ -51,8 +51,8 @@ class AuthUser(DbModelABC):
return "ANONYMOUS" return "ANONYMOUS"
try: try:
keycloak_admin: KeycloakAdmin = ServiceProviderABC.get_global_service(KeycloakAdmin) keycloak = ServiceProviderABC.get_global_service(KeycloakAdmin)
return keycloak_admin.get_user(self._keycloak_id).get("email") return keycloak.get_user(self._keycloak_id).get("email")
except KeycloakGetError as e: except KeycloakGetError as e:
return "UNKNOWN" return "UNKNOWN"
except Exception as e: except Exception as e:

View File

@@ -6,13 +6,13 @@ from cpl.auth.schema._administration.auth_user import AuthUser
_user_context: ContextVar[Optional[AuthUser]] = ContextVar("user", default=None) _user_context: ContextVar[Optional[AuthUser]] = ContextVar("user", default=None)
def set_user(user_id: Optional[AuthUser]): def set_user(user: Optional[AuthUser]):
from cpl.dependency.service_provider_abc import ServiceProviderABC from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
logger = ServiceProviderABC.get_global_service(LoggerABC) logger = ServiceProviderABC.get_global_service(LoggerABC)
logger.trace("Setting user context", user_id) logger.trace("Setting user context", user.id)
_user_context.set(user_id) _user_context.set(user)
def get_user() -> Optional[AuthUser]: def get_user() -> Optional[AuthUser]:

View File

@@ -1,9 +1,13 @@
import asyncio import asyncio
import importlib.util import importlib.util
import json
import traceback import traceback
from datetime import datetime from datetime import datetime
from cpl.core.log import Logger, LogLevel from starlette.requests import Request
from cpl.core.log.log_level import LogLevel
from cpl.core.log.logger import Logger
from cpl.core.typing import Source, Messages from cpl.core.typing import Source, Messages
@@ -20,8 +24,9 @@ class StructuredLogger(Logger):
try: try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
formatted_message = self._format_message(level.value, timestamp, *messages) formatted_message = self._format_message(level.value, timestamp, *messages)
structured_message = self._get_structured_message(level.value, timestamp, formatted_message)
self._write_log_to_file(level, formatted_message) self._write_log_to_file(level, structured_message)
self._write_to_console(level, formatted_message) self._write_to_console(level, formatted_message)
except Exception as e: except Exception as e:
print(f"Error while logging: {e} -> {traceback.format_exc()}") print(f"Error while logging: {e} -> {traceback.format_exc()}")
@@ -37,10 +42,31 @@ class StructuredLogger(Logger):
self._enrich_message_with_request(structured_message) self._enrich_message_with_request(structured_message)
self._enrich_message_with_user(structured_message) self._enrich_message_with_user(structured_message)
return str(structured_message) return json.dumps(structured_message, ensure_ascii=False)
@staticmethod @staticmethod
def _enrich_message_with_request(message: dict): def _scope_to_json(request: Request, include_headers: bool = False) -> dict:
scope = dict(request.scope)
def convert(value):
if isinstance(value, bytes):
return value.decode("utf-8")
if isinstance(value, (list, tuple)):
return [convert(v) for v in value]
if isinstance(value, dict):
return {str(k): convert(v) for k, v in value.items()}
if not isinstance(value, (str, int, float, bool, type(None))):
return str(value)
return value
serializable_scope = {str(k): convert(v) for k, v in scope.items()}
if not include_headers and "headers" in serializable_scope:
serializable_scope["headers"] = "<omitted>"
return serializable_scope
def _enrich_message_with_request(self, message: dict):
if importlib.util.find_spec("cpl.api") is None: if importlib.util.find_spec("cpl.api") is None:
return return
@@ -55,7 +81,7 @@ class StructuredLogger(Logger):
message["request"] = { message["request"] = {
"url": str(request.url), "url": str(request.url),
"method": request.method, "method": request.method,
"scope": request.scope, "scope": self._scope_to_json(request),
} }
if isinstance(request, Request) and request.scope == "http": if isinstance(request, Request) and request.scope == "http":
request: Request = request # fix typing for IDEs request: Request = request # fix typing for IDEs
@@ -73,8 +99,13 @@ class StructuredLogger(Logger):
if user is None: if user is None:
return return
from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin
keycloak = ServiceProviderABC.get_global_service(KeycloakAdmin)
kc_user = keycloak.get_user(user.keycloak_id)
message["user"] = { message["user"] = {
"id": str(user.id), "id": str(user.id),
"username": user.username, "username": kc_user.get("username"),
"email": user.email, "email": kc_user.get("email"),
} }

View File

@@ -1,13 +1,28 @@
import inspect
from cpl.core.log import LoggerABC, LogLevel from cpl.core.log import LoggerABC, LogLevel
from cpl.core.typing import Messages from cpl.core.typing import Messages, Source
from cpl.dependency.service_provider_abc import ServiceProviderABC
class WrappedLogger(LoggerABC): class WrappedLogger(LoggerABC):
def __init__(self, logger: LoggerABC): def __init__(self, file_prefix: str):
LoggerABC.__init__(self) LoggerABC.__init__(self)
assert isinstance(logger, LoggerABC), "logger must be an instance of LoggerABC" assert file_prefix is not None and file_prefix != "", "file_prefix must be a non-empty string"
self._logger = logger
t_logger = ServiceProviderABC.get_global_service(LoggerABC)
self._t_logger = type(t_logger) if t_logger is not None else None
self._source = None
self._file_prefix = file_prefix
self._set_logger()
def _set_logger(self):
if self._t_logger is None:
raise Exception("No LoggerABC service registered in ServiceProviderABC")
self._logger = self._t_logger(self._source, self._file_prefix)
def set_level(self, level: LogLevel): def set_level(self, level: LogLevel):
self._logger.set_level(level) self._logger.set_level(level)
@@ -15,23 +30,68 @@ class WrappedLogger(LoggerABC):
def _format_message(self, level: str, timestamp, *messages: Messages) -> str: def _format_message(self, level: str, timestamp, *messages: Messages) -> str:
return self._logger._format_message(level, timestamp, *messages) return self._logger._format_message(level, timestamp, *messages)
@staticmethod
def _get_source() -> str | None:
stack = inspect.stack()
if len(stack) <= 1:
return None
from cpl.dependency import ServiceCollection
ignore_classes = [
ServiceProviderABC,
ServiceProviderABC.__subclasses__(),
ServiceCollection,
WrappedLogger,
WrappedLogger.__subclasses__(),
]
ignore_modules = [x.__module__ for x in ignore_classes if isinstance(x, type)]
for i, frame_info in enumerate(stack[1:]):
module = inspect.getmodule(frame_info.frame)
if module is None:
continue
if module.__name__ in ignore_classes or module in ignore_classes:
continue
if module in ignore_modules or module.__name__ in ignore_modules:
continue
if module.__name__ != __name__:
return module.__name__
return None
def _set_source(self):
self._source = self._get_source()
self._set_logger()
def header(self, string: str): def header(self, string: str):
self._set_source()
self._logger.header(string) self._logger.header(string)
def trace(self, *messages: Messages): def trace(self, *messages: Messages):
self._set_source()
self._logger.trace(*messages) self._logger.trace(*messages)
def debug(self, *messages: Messages): def debug(self, *messages: Messages):
self._set_source()
self._logger.debug(*messages) self._logger.debug(*messages)
def info(self, *messages: Messages): def info(self, *messages: Messages):
self._set_source()
self._logger.info(*messages) self._logger.info(*messages)
def warning(self, *messages: Messages): def warning(self, *messages: Messages):
self._set_source()
self._logger.warning(*messages) self._logger.warning(*messages)
def error(self, messages: str, e: Exception = None): def error(self, messages: str, e: Exception = None):
self._set_source()
self._logger.error(messages, e) self._logger.error(messages, e)
def fatal(self, messages: str, e: Exception = None): def fatal(self, messages: str, e: Exception = None):
self._set_source()
self._logger.fatal(messages, e) self._logger.fatal(messages, e)

View File

@@ -9,7 +9,7 @@ from cpl.core.utils.get_value import get_value
from cpl.core.utils.string import String from cpl.core.utils.string import String
from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.abc.db_context_abc import DBContextABC
from cpl.database.const import DATETIME_FORMAT from cpl.database.const import DATETIME_FORMAT
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder
from cpl.database.postgres.sql_select_builder import SQLSelectBuilder from cpl.database.postgres.sql_select_builder import SQLSelectBuilder
from cpl.database.typing import T_DBM, Attribute, AttributeFilters, AttributeSorts from cpl.database.typing import T_DBM, Attribute, AttributeFilters, AttributeSorts

View File

@@ -1,8 +0,0 @@
from cpl.core.log import LoggerABC
from cpl.core.log.wrapped_logger import WrappedLogger
class DBLogger(WrappedLogger):
def __init__(self, logger: LoggerABC):
WrappedLogger.__init__(self, logger)

View File

@@ -0,0 +1,7 @@
from cpl.core.log.wrapped_logger import WrappedLogger
class DBLogger(WrappedLogger):
def __init__(self):
WrappedLogger.__init__(self, "db")

View File

@@ -5,7 +5,7 @@ from mysql.connector import Error as MySQLError, PoolError
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.abc.db_context_abc import DBContextABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model.database_settings import DatabaseSettings from cpl.database.model.database_settings import DatabaseSettings
from cpl.database.mysql.mysql_pool import MySQLPool from cpl.database.mysql.mysql_pool import MySQLPool

View File

@@ -4,7 +4,7 @@ import sqlparse
from mysql.connector.aio import MySQLConnectionPool from mysql.connector.aio import MySQLConnectionPool
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model import DatabaseSettings from cpl.database.model import DatabaseSettings
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProviderABC

View File

@@ -7,7 +7,7 @@ from psycopg_pool import PoolTimeout
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.abc.db_context_abc import DBContextABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model import DatabaseSettings from cpl.database.model import DatabaseSettings
from cpl.database.postgres.postgres_pool import PostgresPool from cpl.database.postgres.postgres_pool import PostgresPool

View File

@@ -5,7 +5,7 @@ from psycopg import sql
from psycopg_pool import AsyncConnectionPool, PoolTimeout from psycopg_pool import AsyncConnectionPool, PoolTimeout
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model import DatabaseSettings from cpl.database.model import DatabaseSettings
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProviderABC

View File

@@ -2,7 +2,7 @@ import glob
import os import os
from cpl.database.abc import DBContextABC from cpl.database.abc import DBContextABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model import Migration from cpl.database.model import Migration
from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.model.server_type import ServerType, ServerTypes
from cpl.database.schema.executed_migration import ExecutedMigration from cpl.database.schema.executed_migration import ExecutedMigration

View File

@@ -1,5 +1,5 @@
from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.database.db_logger import DBLogger from cpl.database.logger import DBLogger
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProviderABC

View File

@@ -1,9 +1,6 @@
from typing import Union, Type, Callable, Self from typing import Union, Type, Callable, Self
from cpl.core.log.logger import Logger
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.core.log.structured_logger import StructuredLogger
from cpl.core.log.wrapped_logger import WrappedLogger
from cpl.core.typing import T, Service from cpl.core.typing import T, Service
from cpl.dependency.service_descriptor import ServiceDescriptor from cpl.dependency.service_descriptor import ServiceDescriptor
from cpl.dependency.service_lifetime_enum import ServiceLifetimeEnum from cpl.dependency.service_lifetime_enum import ServiceLifetimeEnum
@@ -82,14 +79,20 @@ class ServiceCollection:
return self return self
def add_logging(self) -> Self: def add_logging(self) -> Self:
from cpl.core.log.logger import Logger
from cpl.core.log.wrapped_logger import WrappedLogger
self.add_transient(LoggerABC, Logger) self.add_transient(LoggerABC, Logger)
return self for wrapper in WrappedLogger.__subclasses__():
self.add_transient(wrapper)
def add_structured_logging(self) -> Self: return self
self.add_transient(LoggerABC, StructuredLogger)
return self def add_structured_logging(self) -> Self:
from cpl.core.log.structured_logger import StructuredLogger
def add_wrapped_logging(self) -> Self: from cpl.core.log.wrapped_logger import WrappedLogger
self.add_transient(LoggerABC, StructuredLogger)
for wrapper in WrappedLogger.__subclasses__(): for wrapper in WrappedLogger.__subclasses__():
self.add_transient(wrapper) self.add_transient(wrapper)
return self return self

View File

@@ -3,7 +3,7 @@ from .abc.email_client_abc import EMailClientABC
from .email_client import EMailClient from .email_client import EMailClient
from .email_client_settings import EMailClientSettings from .email_client_settings import EMailClientSettings
from .email_model import EMail from .email_model import EMail
from .mail_logger import MailLogger from .logger import MailLogger
def add_mail(collection: _ServiceCollection): def add_mail(collection: _ServiceCollection):

View File

@@ -5,7 +5,7 @@ from typing import Optional
from cpl.mail.abc.email_client_abc import EMailClientABC from cpl.mail.abc.email_client_abc import EMailClientABC
from cpl.mail.email_client_settings import EMailClientSettings from cpl.mail.email_client_settings import EMailClientSettings
from cpl.mail.email_model import EMail from cpl.mail.email_model import EMail
from cpl.mail.mail_logger import MailLogger from cpl.mail.logger import MailLogger
class EMailClient(EMailClientABC): class EMailClient(EMailClientABC):

View File

@@ -0,0 +1,7 @@
from cpl.core.log.wrapped_logger import WrappedLogger
class MailLogger(WrappedLogger):
def __init__(self):
WrappedLogger.__init__(self, "mail")

View File

@@ -1,8 +0,0 @@
from cpl.core.log import LoggerABC
from cpl.core.log.wrapped_logger import WrappedLogger
class MailLogger(WrappedLogger):
def __init__(self, logger: LoggerABC):
WrappedLogger.__init__(self, logger)

View File

@@ -16,8 +16,8 @@ def main():
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json") Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True) Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
# builder.services.add_logging()
builder.services.add_structured_logging() builder.services.add_structured_logging()
builder.services.add_wrapped_logging()
builder.services.add_transient(PingService) builder.services.add_transient(PingService)
builder.services.add_module(api) builder.services.add_module(api)

View File

@@ -1,17 +1,16 @@
from urllib.request import Request from urllib.request import Request
from service import PingService
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from cpl.api import APILogger
from cpl.api.router import Router from cpl.api.router import Router
from cpl.auth.permission.permissions import Permissions
from cpl.core.log import Logger
from service import PingService
@Router.authenticate() @Router.authenticate()
@Router.authorize(permissions=[Permissions.administrator]) # @Router.authorize(permissions=[Permissions.administrator])
# @Router.authorize(policies=["test"]) # @Router.authorize(policies=["test"])
@Router.get(f"/ping") @Router.get(f"/ping")
async def ping(r: Request, ping: PingService, logger: Logger): async def ping(r: Request, ping: PingService, logger: APILogger):
logger.info(f"Ping: {ping}") logger.info(f"Ping: {ping}")
return JSONResponse(ping.ping(r)) return JSONResponse(ping.ping(r))