295 lines
12 KiB
Python
295 lines
12 KiB
Python
import functools
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import time
|
|
from enum import Enum, auto
|
|
from logging import Logger, LoggerAdapter
|
|
from types import (BuiltinFunctionType, BuiltinMethodType,
|
|
ClassMethodDescriptorType, FunctionType,
|
|
MethodDescriptorType, MethodType)
|
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
|
|
from django.db import connections
|
|
|
|
from .logging import TAG_DATA_FEED, TAG_PERF, get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
# attribut de classe : logger
|
|
CLASS_ATTR_LOGGER = 'logger'
|
|
|
|
# clé du dictionnaire contexte : désactivé
|
|
CTX_KEY_DISABLED = 'disabled'
|
|
|
|
# clé du dictionnaire contexte : logger
|
|
CTX_KEY_LOGGER = 'logger'
|
|
|
|
# attribut de décorateur : contexte
|
|
DECORATOR_ATTR_CTX = 'decorator_ctx'
|
|
|
|
# attribut de décorateur : clé
|
|
DECORATOR_ATTR_KEY = 'decorator_key'
|
|
|
|
# attribut de décorateur : cible (classe, fonction)
|
|
DECORATOR_ATTR_TARGET = 'decorator_target'
|
|
|
|
# types de fonctions
|
|
FUNCTION_TYPES = (
|
|
BuiltinFunctionType,
|
|
BuiltinMethodType,
|
|
ClassMethodDescriptorType,
|
|
FunctionType,
|
|
MethodDescriptorType,
|
|
MethodType,
|
|
)
|
|
|
|
|
|
def _get_query_count() -> int:
|
|
""" renvoie le nombre total de requêtes (il peut y avoir plusieurs connexions) """
|
|
|
|
return sum(len(c.queries) for c in connections.all())
|
|
|
|
|
|
class OnConflict(Enum):
|
|
""" choix de gestion de conflit quand le même décorateur est déjà utilisé """
|
|
|
|
# laisse l'ancien décorateur
|
|
SKIP = auto()
|
|
|
|
# remplace l'ancien décorateur
|
|
REPLACE = auto()
|
|
|
|
# ajoute à l'ancien décorateur
|
|
STACK = auto()
|
|
|
|
|
|
# amélioration possible : et s'il y a plusieurs décorateurs de même clé ?
|
|
def _find_former_decorator(key: str, target: Callable) -> Tuple[Optional[Callable], Optional[Callable]]:
|
|
"""
|
|
Trouve le décorateur de même clé si la cible est déjà décorée. Un décorateur désactivé est ignoré.
|
|
note : ne fonctionne pas correctement s'il existe un décorateur intermédiaire sans attribut DECORATOR_ATTR_TARGET
|
|
|
|
:param key: clé de décorateur
|
|
:type key: str
|
|
|
|
:param target: fonction potentiellement décorée
|
|
:type target: Callable
|
|
|
|
:return: ancien décorateur + ancienne cible
|
|
:rtype: Tuple[Callable, Callable]
|
|
"""
|
|
current = target
|
|
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
|
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
|
|
|
while decorator_key and decorator_key != key and decorator_target:
|
|
current = decorator_target
|
|
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
|
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
|
|
|
# les décorateurs désactivés ne comptent pas
|
|
if decorator_key == key and not getattr(current, DECORATOR_ATTR_CTX, {}).get(CTX_KEY_DISABLED, False):
|
|
return current, decorator_target
|
|
return None, None
|
|
|
|
|
|
def _create_decorator_from_action(
|
|
action: Callable,
|
|
decorator_key: str,
|
|
on_conflict: OnConflict = OnConflict.SKIP):
|
|
"""
|
|
Fonction d'ordre supérieur qui construit un décorateur de fonction/méthode à partir des paramètres. Ce décorateur exécute l'action fournie.
|
|
|
|
Un contexte (dictionnaire) est passé à l'appel. Il contient :
|
|
- l'état désactivé : clé CTX_KEY_DISABLED optionnelle
|
|
|
|
:param action: action à exécuter
|
|
:type action: Callable[[dict, Callable[P, R], P.args, P.kwargs], R]
|
|
|
|
:param decorator_key: clé correspondant au décorateur, permet de gérer les conflits
|
|
:type decorator_key: str, optional
|
|
|
|
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
|
:type on_conflict: class:`OnConflict`
|
|
"""
|
|
def inner(func):
|
|
if on_conflict is not OnConflict.STACK:
|
|
former_decorator, former_target = _find_former_decorator(decorator_key, func)
|
|
if former_decorator:
|
|
if on_conflict is OnConflict.SKIP:
|
|
# pas de nouveau décorateur
|
|
return func
|
|
|
|
if on_conflict is OnConflict.REPLACE:
|
|
if former_decorator is func and former_target:
|
|
# ancien décorateur déjà fourni : on le remplace vraiment
|
|
func = former_target
|
|
else:
|
|
# ancien décorateur déjà décoré : on le désactive
|
|
former_ctx = getattr(former_decorator, DECORATOR_ATTR_CTX, None)
|
|
if former_ctx is None:
|
|
former_ctx = {}
|
|
getattr(former_decorator, DECORATOR_ATTR_CTX, former_ctx)
|
|
former_ctx.update({CTX_KEY_DISABLED: True})
|
|
ctx = {}
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
if ctx.get(CTX_KEY_DISABLED, False):
|
|
# désactivé : simple appel de la fonction d'origine
|
|
return func(*args, **kwargs)
|
|
return action(ctx, func, *args, **kwargs)
|
|
|
|
setattr(wrapper, DECORATOR_ATTR_CTX, ctx)
|
|
setattr(wrapper, DECORATOR_ATTR_KEY, decorator_key)
|
|
setattr(wrapper, DECORATOR_ATTR_TARGET, func)
|
|
return wrapper
|
|
return inner
|
|
|
|
|
|
def execution_time(
|
|
level: int = logging.DEBUG,
|
|
logger_name: Any = None,
|
|
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
|
on_conflict: OnConflict = OnConflict.SKIP,
|
|
warn_after: int = None):
|
|
"""
|
|
Décorateur de fonction pour tracer le temps d'exécution (au niveau <level> avec un logger de performances).
|
|
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
|
|
|
:param level: niveau de base du logger, defaults to logging.DEBUG
|
|
:type level: int
|
|
|
|
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
|
:type logger_name: str, optional
|
|
|
|
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
|
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
|
|
|
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
|
:type on_conflict: class:`OnConflict`
|
|
|
|
:param warn_after: durée (en ms) au-delà de laquelle on souhaite un log d'avertissement, defaults to None
|
|
:type warn_after: int, optional
|
|
|
|
:return: résultat de la fonction d'origine
|
|
"""
|
|
|
|
def action(ctx: Dict, func, *args, **kwargs):
|
|
if CTX_KEY_LOGGER not in ctx:
|
|
# initialisation du logger, pas toujours possible avant le premier appel
|
|
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
|
|
|
temps_debut = time.time()
|
|
try:
|
|
return func(*args, **kwargs)
|
|
finally:
|
|
try:
|
|
temps_ecoule = round((time.time() - temps_debut) * 1000)
|
|
log_level = logging.WARNING if isinstance(warn_after, int) and temps_ecoule > warn_after else level
|
|
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' exécuté en %d ms", func.__name__, temps_ecoule)
|
|
except Exception:
|
|
logger.exception('impossible de tracer la durée')
|
|
|
|
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
|
|
|
|
|
def query_count(
|
|
level: int = logging.DEBUG,
|
|
logger_name: Any = None,
|
|
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
|
on_conflict: OnConflict = OnConflict.SKIP,
|
|
warn_after: int = None):
|
|
"""
|
|
Décorateur de fonction pour tracer le nombre de requêtes (au niveau <level> avec un logger de performances).
|
|
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
|
|
|
:param level: niveau de base du logger, defaults to logging.DEBUG
|
|
:type level: int
|
|
|
|
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
|
:type logger_name: str, optional
|
|
|
|
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
|
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
|
|
|
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
|
:type on_conflict: class:`OnConflict`
|
|
|
|
:param warn_after: nombre de requêtes au-delà duquel on souhaite un log d'avertissement, defaults to None
|
|
:type warn_after: int, optional
|
|
|
|
:return: résultat de la fonction d'origine
|
|
"""
|
|
|
|
def action(ctx: Dict, func, *args, **kwargs):
|
|
if CTX_KEY_LOGGER not in ctx:
|
|
# initialisation du logger, pas toujours possible avant le premier appel
|
|
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
|
|
|
nb_debut = _get_query_count()
|
|
try:
|
|
return func(*args, **kwargs)
|
|
finally:
|
|
try:
|
|
nb_requetes = _get_query_count() - nb_debut
|
|
log_level = logging.WARNING if isinstance(warn_after, int) and nb_requetes > warn_after else level
|
|
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' a exécuté %d requête(s)", func.__name__, nb_requetes)
|
|
except Exception:
|
|
logger.exception('impossible de tracer le nombre de requêtes')
|
|
|
|
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
|
|
|
|
|
def class_logger(cls):
|
|
"""
|
|
Décorateur de classe pour stocker un logger standard dans l'attribut 'logger' (aucune gestion de conflit)
|
|
"""
|
|
if not inspect.isclass(cls):
|
|
return cls
|
|
|
|
@functools.wraps(cls, updated=())
|
|
class Wrapper(cls):
|
|
decorated = 1
|
|
|
|
setattr(Wrapper, CLASS_ATTR_LOGGER, get_logger(Wrapper))
|
|
return Wrapper
|
|
|
|
|
|
def decorate_functions(
|
|
decorator: Callable,
|
|
func_filter: Callable[[Callable], bool],
|
|
factory: bool = False):
|
|
"""
|
|
Décorateur de classe qui applique le décorateur donné aux fonctions/méthodes (attributs d'un type de FUNCTION_TYPES) qui passent le filtre.
|
|
|
|
:param decorator: décorateur à appliquer
|
|
:type decorator: Callable
|
|
|
|
:param func_filter: filtre permettant de sélectionner les fonctions/méthodes à décorer
|
|
:type func_filter: Callable[[Callable], bool]
|
|
|
|
:param factory: True indique que le décorateur est une méthode "factory" à exécuter avec la classe, defaults to False
|
|
:type factory: bool
|
|
"""
|
|
|
|
def decorate(cls):
|
|
if not inspect.isclass(cls):
|
|
return cls
|
|
|
|
@functools.wraps(cls, updated=())
|
|
class Wrapper(cls):
|
|
decorated = 1
|
|
|
|
_decorator = decorator(Wrapper) if factory else decorator
|
|
for attr_name in dir(Wrapper):
|
|
# __new__ et __init__ sont complexes à décorer, à gérer au cas par cas
|
|
if attr_name != '__new__' and attr_name != '__init__':
|
|
value = getattr(Wrapper, attr_name)
|
|
if isinstance(value, FUNCTION_TYPES) and func_filter(value):
|
|
setattr(Wrapper, attr_name, _decorator(value))
|
|
return Wrapper
|
|
return decorate
|