Skip to content
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include python_common_logger *
recursive-include src *
File renamed without changes.
29 changes: 29 additions & 0 deletions python_common_logger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Python Common Logger to log in JSON format with context data.

Check: https://simpplr.atlassian.net/wiki/spaces/SA/pages/2467365251/Logging+Strategy

Classes:
Logger.ContextFilter
DjangoMiddleware.ContextMiddleware
ContextConstants.ExecutionContextType
LoggingConstants.LoggerContextConfigKeys
LoggingConstants.LoggerKeys
RequestConstants.RequestHeaderKeys

Functions:
Logger.initialise_console_logger(logger_name, service_name, level=logging.WARNING, context_config=None)
ContextHandler.get_thread_execution_context(key='execution_context')
ContextHandler.update_execution_context(execution_context, key='execution_context', reset=False)
"""

from .src import logger as Logger
from .src.context import context_handler as ContextHandler
from .src.django import middleware as DjangoMiddleware

from .src.constants import context as ContextConstants
from .src.constants import logger as LoggingConstants
from .src.constants import request as RequestConstants

from .src.context.execution_context import *

18 changes: 18 additions & 0 deletions python_common_logger/src/constants/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum

class ExecutionContextType(Enum):
"""
ExecutionContextType

Attributes
----------
CORRELATION_ID : str
Correlation Id
TENANT_ID : str
Tenant Id
USER_ID : str
User Id
"""
CORRELATION_ID = 'correlation_id'
TENANT_ID = 'tenant_id'
USER_ID = 'user_id'
35 changes: 35 additions & 0 deletions python_common_logger/src/constants/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from enum import Enum

class LoggerContextConfigKeys(Enum):
"""
Context Config Keys

Attributes
----------
DISABLE_TID : str
Disable Tenant Id
DISABLE_UID : str
Disable User Id
DISABLE_CID : str
Disable Correlation Id
"""
DISABLE_TID = 'disable_tid'
DISABLE_UID = 'disable_uid'
DISABLE_CID = 'disable_cid'

class LoggerKeys(Enum):
"""
Logging Key

Attributes
----------
CORRELATION_ID : str
Correlation Id
TENANT_ID : str
Tenant Id
USER_ID : str
User Id
"""
CORRELATION_ID = 'cid'
TENANT_ID = 'tid'
USER_ID = 'uid'
18 changes: 18 additions & 0 deletions python_common_logger/src/constants/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum

class RequestHeaderKeys(Enum):
"""
Header keys

Attributes
----------
ACCOUNT_ID : str
Account Id / Tenant ID
USER_ID : str
User Id
CORRELATION_ID : str
Correlation Id
"""
ACCOUNT_ID = 'x-smtip-tid',
USER_ID = 'x-smtip-uid',
CORRELATION_ID = 'x-smtip-cid',
37 changes: 37 additions & 0 deletions python_common_logger/src/context/context_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from .execution_context import ExecutionContext

from threading import local

_locals = local()

def get_thread_execution_context(key='execution_context') -> ExecutionContext:
"""
Fetches the execution context from the thread local. If absent, initialises and returns an empty one.

Args:
key (str, optional): key. Defaults to 'execution_context'.

Returns:
ExecutionContext: Thread local execution context.
"""
return getattr(_locals, key, ExecutionContext({}))

def update_execution_context(execution_context: ExecutionContext, key='execution_context', reset=False) -> ExecutionContext:
"""
Updates the execution context.

Args:
execution_context (ExecutionContext): Execution context to be updated.
key (str, optional): key. Defaults to 'execution_context'.
reset (bool, optional): Reset the entire context. Defaults to False.

Returns:
ExecutionContext: Updated execution context
"""
current_execution_context: ExecutionContext = get_thread_execution_context()

current_execution_context.update(execution_context.get_context(), reset)

setattr(_locals, key, current_execution_context)

return current_execution_context
80 changes: 80 additions & 0 deletions python_common_logger/src/context/execution_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import copy

from ..exceptions.validation_error import ValidationException
from ..constants.context import ExecutionContextType

class ExecutionContext:
"""
Stores the Execution context and provides helper methods to manage it.

Methods
-------
update(execution_context:dict, reset:bool=False):
Updates the Execution Context.

get_context_by_key(key:str):
Returns the execution context stored for the provided key.

get_context() -> dict:
Returns the entire execution context.

reset(key:str):
Resets the context.
"""
ALLOWED_KEYS = [e.value for e in ExecutionContextType]

def __init__(self, execution_context: dict):
self._context = {}
for key in execution_context.keys():
if key not in self.ALLOWED_KEYS:
# TODO: Create Validation error
raise ValidationException(f'Invalid execution context type: {key}')
else:
self._context[key] = execution_context[key]

def update(self, execution_context: dict, reset=False):
"""
Updates the execution context.

Args:
execution_context (dict): Execution context to be updated.
reset (bool, optional): If True, replaces the entire context. If False, updates only the provided values. Defaults to False.

Raises:
ValidationException: If an invalid key is provided.
"""
if reset:
self.reset()

for key in execution_context.keys():
if key not in self.ALLOWED_KEYS:
raise ValidationException(f'Invalid execution context type: {key}')
else:
self._context[key] = execution_context[key]

def get_context_by_key(self, key: str) -> str:
"""
Returns the execution context for the specified key

Args:
key (str): key

Returns:
any: Execution Context
"""
return copy.deepcopy(self._context[key])

def get_context(self) -> dict:
"""
Returns the execution context.

Returns:
dict: Entire execution context
"""
return copy.deepcopy(self._context)

def reset(self):
"""
Resets the execution context.
"""
self._context = {}
34 changes: 34 additions & 0 deletions python_common_logger/src/django/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from uuid import uuid4
from ..context.context_handler import update_execution_context
from ..context.execution_context import ExecutionContext

from ..constants.request import RequestHeaderKeys
from ..constants.context import ExecutionContextType

class ContextMiddleware():
"""
Django Context Middle. Extracts the headers from the request and populates the execution context
in thread local data.
"""
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
execution_context = {}

if RequestHeaderKeys.CORRELATION_ID.value in request.META:
execution_context[ExecutionContextType.CORRELATION_ID.value] = request.META[RequestHeaderKeys.CORRELATION_ID.value]
else:
execution_context[ExecutionContextType.CORRELATION_ID.value] = uuid4()

if RequestHeaderKeys.ACCOUNT_ID.value in request.META:
execution_context[ExecutionContextType.TENANT_ID.value] = request.META[RequestHeaderKeys.ACCOUNT_ID.value]

if RequestHeaderKeys.USER_ID.value in request.META:
execution_context[ExecutionContextType.USER_ID.value] = request.META[RequestHeaderKeys.USER_ID.value]

update_execution_context(ExecutionContext(execution_context))

response = self.get_response(request)

return response
14 changes: 14 additions & 0 deletions python_common_logger/src/exceptions/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class BaseException(Exception):
"""
A class used to represent an Exception with code

Attributes
----------
message : str
Message to describe the exception
code : int
Represents the integer code. Usually the HTTP error code.
"""
def __init__(self, message, code=500):
super().__init__(message)
self.code = code
8 changes: 8 additions & 0 deletions python_common_logger/src/exceptions/validation_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .base import BaseException

class ValidationException(BaseException):
"""
A class used to represent a Validation Error.
"""
def __init__(self, message, code=400):
super().__init__(message, code)
79 changes: 79 additions & 0 deletions python_common_logger/src/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import logging
import json
import sys

from .context.context_handler import get_thread_execution_context
from .context.execution_context import ExecutionContext, ExecutionContextType

from .constants.logger import LoggerKeys, LoggerContextConfigKeys
from .constants.context import ExecutionContextType

class ContextFilter(logging.Filter):
"""
Log filter to extract the execution context from thread locals and populate it in the log record
"""

def filter(self, record):
execution_context: ExecutionContext = get_thread_execution_context()

context_values: dict = execution_context.get_context()

for key in ExecutionContext.ALLOWED_KEYS:
if key in context_values:
setattr(record, key, context_values[key])
else:
setattr(record, key, '')
return True

def initialise_console_logger(logger_name, service_name, level=logging.WARNING, context_config=None):
"""
Initialises the logger with the handler, formatter and filter to log context data along with message
in JSON format on the console.

Args:
logger_name (string): Name of the logger to be initialised
service_name (string): Service name that appears as the source in the logs
level (int, optional): Log level. Defaults to logging.WARNING.
context_config (dict, optional): Context config to configure logging parameters See LoggerContextConfigKeys for list of allowed params. Defaults to None.

Returns:
Logger: Initialised logger
"""
logger = logging.getLogger(logger_name)

# Create handlers
log_handler = logging.StreamHandler(sys.stdout)

log_format = {
"source": f"{service_name}",
"time": "%(asctime)s",
"log": {
"message": "%(message)s"
},
"logLevel": "%(levelname)s"
}

if not context_config:
context_config = {}

if not context_config.get(LoggerContextConfigKeys.DISABLE_CID.value):
log_format[LoggerKeys.CORRELATION_ID.value] = f"%({ExecutionContextType.CORRELATION_ID.value})s"

if not context_config.get(LoggerContextConfigKeys.DISABLE_TID.value):
log_format[LoggerKeys.TENANT_ID.value] = f"%({ExecutionContextType.TENANT_ID.value})s"

if not context_config.get(LoggerContextConfigKeys.DISABLE_UID.value):
log_format[LoggerKeys.USER_ID.value] = f"%({ExecutionContextType.USER_ID.value})s"

# Create formatters and add it to handlers
log_formatter = logging.Formatter(json.dumps(log_format), datefmt='%Y-%m-%dT%H:%M:%S%z')
log_handler.setFormatter(log_formatter)

# Populate Context Filter in Record
log_handler.addFilter(ContextFilter())

logger.addHandler(log_handler)

logger.setLevel(level)

return logger
20 changes: 20 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from setuptools import setup, find_packages

setup(
name = 'python_common_logger',
packages = find_packages(),
include_package_data = True,
version = '1.0',
license='None',
description = 'Common Python Logger for Simpplr packages',
author = 'Team Delta',
author_email = '[email protected]',
url = 'https://github.com/Simpplr/python-common-logger',
keywords = ['PYTHON', 'SIMPPLR', 'COMMON', 'LOGGER'],
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
'Programming Language :: Python :: 3.8',
],
)