fk_util_tools 0.1.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fk_util_tools-0.1.14/LICENSE +21 -0
- fk_util_tools-0.1.14/PKG-INFO +31 -0
- fk_util_tools-0.1.14/fk_utils/__init__.py +3 -0
- fk_util_tools-0.1.14/fk_utils/cache/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/cache/redis.py +85 -0
- fk_util_tools-0.1.14/fk_utils/config.py +24 -0
- fk_util_tools-0.1.14/fk_utils/envs/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/envs/aws/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/envs/aws/parameters.py +41 -0
- fk_util_tools-0.1.14/fk_utils/envs/aws/secrets.py +31 -0
- fk_util_tools-0.1.14/fk_utils/exceptions/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/exceptions/custom_http_exception.py +12 -0
- fk_util_tools-0.1.14/fk_utils/exceptions/custom_http_exception.pyi +8 -0
- fk_util_tools-0.1.14/fk_utils/logging/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/logging/fastapi/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/logging/fastapi/logging.py +56 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/django/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/django/handler.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/django/sql_middleware.py +60 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/error_middleware.py +128 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/i18n.py +66 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/language_middleware.py +21 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/sql_middleware.py +70 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/flask/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/flask/handler.py +0 -0
- fk_util_tools-0.1.14/fk_utils/middlewares/flask/sql_middleware.py +66 -0
- fk_util_tools-0.1.14/fk_utils/py.typed +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/django/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/django/trace.py +68 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/fastapi/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/fastapi/trace.py +56 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/flask/__init__.py +0 -0
- fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/flask/trace.py +59 -0
- fk_util_tools-0.1.14/pyproject.toml +28 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Finkargo Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fk_util_tools
|
|
3
|
+
Version: 0.1.14
|
|
4
|
+
Summary: Herramientas de utilidad o funciones comunes para los proyectos de FK.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Steven Santacruz Garcia
|
|
7
|
+
Author-email: stevengarcia1118@gmail.com
|
|
8
|
+
Requires-Python: >=3.7,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Dist: boto3
|
|
19
|
+
Requires-Dist: opentelemetry-api
|
|
20
|
+
Requires-Dist: opentelemetry-exporter-jaeger
|
|
21
|
+
Requires-Dist: opentelemetry-exporter-otlp
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-django
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-flask
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg2
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-requests
|
|
27
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy
|
|
28
|
+
Requires-Dist: opentelemetry-sdk
|
|
29
|
+
Requires-Dist: psycopg2-binary
|
|
30
|
+
Requires-Dist: starlette
|
|
31
|
+
Project-URL: Homepage, https://gitlab.com/f3315/team-back-end/cross/packages/package_utils
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import redis
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheService:
|
|
9
|
+
_instance = None
|
|
10
|
+
_lock = threading.Lock()
|
|
11
|
+
|
|
12
|
+
def __new__(cls, redis_url: str):
|
|
13
|
+
if not cls._instance:
|
|
14
|
+
with cls._lock:
|
|
15
|
+
if not cls._instance:
|
|
16
|
+
cls._instance = super(CacheService, cls).__new__(cls)
|
|
17
|
+
cls._instance.__init_instance__(redis_url)
|
|
18
|
+
return cls._instance
|
|
19
|
+
|
|
20
|
+
def __init_instance__(self, redis_url: str):
|
|
21
|
+
""" Actual initialization of the singleton instance """
|
|
22
|
+
self.redis_url = redis_url
|
|
23
|
+
self.__connect__()
|
|
24
|
+
|
|
25
|
+
def __connect__(self):
|
|
26
|
+
""" Establishes the connection with Redis and handles connection errors """
|
|
27
|
+
try:
|
|
28
|
+
print(f"Connecting to Redis at {self.redis_url}...")
|
|
29
|
+
self._cache = redis.Redis.from_url(self.redis_url, decode_responses=True)
|
|
30
|
+
self._cache.ping()
|
|
31
|
+
except redis.ConnectionError as e:
|
|
32
|
+
print(f"Error de conexión a Redis: {e}")
|
|
33
|
+
self._cache = None
|
|
34
|
+
|
|
35
|
+
def _ensure_connection(self):
|
|
36
|
+
""" Reconnects if necessary """
|
|
37
|
+
if not self._cache or not self._cache.ping():
|
|
38
|
+
print("Reconnecting to Redis...")
|
|
39
|
+
self.__connect__()
|
|
40
|
+
|
|
41
|
+
def add(
|
|
42
|
+
self, key: str, row: Union[List[Any], Dict[str, Any]], expiration_time: Optional[int] = None
|
|
43
|
+
):
|
|
44
|
+
assert isinstance(key, str), "Only str keys are supported!"
|
|
45
|
+
assert isinstance(row, (list, dict)), "Only dict or list objects are supported!"
|
|
46
|
+
|
|
47
|
+
self._ensure_connection()
|
|
48
|
+
if not self._cache:
|
|
49
|
+
print("Could not establish connection with Redis.")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
raw = {
|
|
53
|
+
"content": base64.b64encode(json.dumps(row).encode("ascii")).decode("ascii")
|
|
54
|
+
}
|
|
55
|
+
self._cache.hset(key, mapping=raw)
|
|
56
|
+
if expiration_time:
|
|
57
|
+
self._cache.expire(key, expiration_time)
|
|
58
|
+
|
|
59
|
+
def get(self, key: str) -> Optional[Union[List[Any], Dict[str, Any]]]:
|
|
60
|
+
assert isinstance(key, str), "Only str keys are supported!"
|
|
61
|
+
|
|
62
|
+
self._ensure_connection()
|
|
63
|
+
if not self._cache:
|
|
64
|
+
print("Could not establish connection with Redis.")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
row = self._cache.hgetall(key)
|
|
68
|
+
if not row:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
content = row.get("content")
|
|
72
|
+
if content is None:
|
|
73
|
+
return None
|
|
74
|
+
return json.loads(base64.b64decode(content).decode("ascii"))
|
|
75
|
+
|
|
76
|
+
def delete(self, key: str):
|
|
77
|
+
assert isinstance(key, str), "Only str keys are supported!"
|
|
78
|
+
|
|
79
|
+
self._ensure_connection()
|
|
80
|
+
if not self._cache:
|
|
81
|
+
print("Could not establish connection with Redis.")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if self._cache.exists(key):
|
|
85
|
+
self._cache.delete(key)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
# Environment
|
|
7
|
+
self.DD_ENV = os.environ.get('DD_ENV', 'localhost')
|
|
8
|
+
self.DEBUG_UTILS = os.environ.get('DEBUG_UTILS', False)
|
|
9
|
+
self.REPOSITORY = os.environ.get('REPOSITORY', 'msa_repository')
|
|
10
|
+
self.TYPE_DEPLOY = os.environ.get('TYPE_DEPLOY', 'api')
|
|
11
|
+
|
|
12
|
+
# AWS
|
|
13
|
+
self.AWS_PROFILE_NAME = os.environ.get('AWS_PROFILE_NAME', 'default')
|
|
14
|
+
self.AWS_REGION_NAME = os.environ.get('AWS_REGION_NAME', 'us-east-1')
|
|
15
|
+
self.SECRET_NAME = os.environ.get('SECRET_NAME', 'api/testing/ms-communicator')
|
|
16
|
+
|
|
17
|
+
# Opentelemtry
|
|
18
|
+
self.OTLP_HOST = os.environ.get('OTLP_HOST', 'localhost')
|
|
19
|
+
self.OTLP_PORT = os.environ.get('OTLP_PORT', '0000')
|
|
20
|
+
self.OTLP_NAME = os.environ.get('OTLP_NAME', 'ms-localhost')
|
|
21
|
+
|
|
22
|
+
# DataDog
|
|
23
|
+
self.DD_HOST = os.environ.get('DD_HOST', 'localhost')
|
|
24
|
+
self.DD_PORT = os.environ.get('DD_PORT', '8126')
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from fk_utils import SETTINGS
|
|
6
|
+
|
|
7
|
+
# Configuración del logger
|
|
8
|
+
logging.basicConfig(level=logging.ERROR)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Crear clientes de Boto3 fuera de las funciones
|
|
12
|
+
ssm_client = boto3.client('ssm', region_name=SETTINGS.AWS_REGION_NAME)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_parameter(param: str, param_type: str) -> Optional[str]:
|
|
16
|
+
"""
|
|
17
|
+
Obtiene un parámetro de AWS Systems Manager Parameter Store.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
param (str): Nombre del parámetro.
|
|
21
|
+
param_type (str): Tipo de parámetro (static, global, static_env, default).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str: Valor del parámetro o 'None' si hay un error.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
param_mappings = {
|
|
28
|
+
'static': f"/ecs/{SETTINGS.REPOSITORY}_{SETTINGS.TYPE_DEPLOY}/{param}",
|
|
29
|
+
'global': f"/ecs/global/{param}",
|
|
30
|
+
'static_env': f"/ecs/{SETTINGS.DD_ENV}/{param}",
|
|
31
|
+
'default': f"/ecs/{SETTINGS.DD_ENV}/{SETTINGS.REPOSITORY}_{SETTINGS.TYPE_DEPLOY}/{param}"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
param_name = param_mappings.get(param_type, param_mappings['default'])
|
|
35
|
+
|
|
36
|
+
response = ssm_client.get_parameter(Name=param_name, WithDecryption=True)
|
|
37
|
+
return response['Parameter']['Value']
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f'Error al obtener el parámetro en AWS: {str(e)}')
|
|
40
|
+
|
|
41
|
+
return os.environ.get(param, 'None')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from fk_utils import SETTINGS
|
|
6
|
+
|
|
7
|
+
# Configuración del logger
|
|
8
|
+
logging.basicConfig(level=logging.ERROR)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Crear clientes de Boto3 fuera de las funciones
|
|
12
|
+
secrets_manager_client = boto3.client('secretsmanager', region_name=SETTINGS.AWS_REGION_NAME)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_secret_key() -> Optional[dict]:
|
|
16
|
+
"""
|
|
17
|
+
Obtiene la clave secreta almacenada en AWS Secrets Manager.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict or None: Diccionario con la clave secreta o None si hay un error.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
response = secrets_manager_client.get_secret_value(SecretId=SETTINGS.SECRET_NAME)
|
|
24
|
+
secret_value = response.get('SecretString')
|
|
25
|
+
|
|
26
|
+
if secret_value:
|
|
27
|
+
return json.loads(secret_value)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logger.error(f'Error al obtener el secreto: {str(e)}')
|
|
30
|
+
|
|
31
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import TypeVar, Optional
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T", bound=Enum)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomHTTPException(HTTPException):
|
|
9
|
+
def __init__(self, error_code: T, detail: Optional[str] = None):
|
|
10
|
+
super().__init__(status_code=getattr(error_code, "status_code", 500),
|
|
11
|
+
detail=detail or getattr(error_code, "detail", "Unknown error"))
|
|
12
|
+
self.error_code = error_code
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from opentelemetry import trace
|
|
3
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
4
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
5
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
6
|
+
from opentelemetry.sdk.resources import Resource
|
|
7
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
10
|
+
|
|
11
|
+
from fk_utils import SETTINGS
|
|
12
|
+
|
|
13
|
+
# Configurar logging
|
|
14
|
+
logging.basicConfig(level=logging.INFO)
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def configure_tracer():
|
|
19
|
+
try:
|
|
20
|
+
# Configurar el proveedor de trazas
|
|
21
|
+
trace.set_tracer_provider(TracerProvider(
|
|
22
|
+
resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
# Configurar el exportador OTLP
|
|
26
|
+
otlp_exporter = OTLPSpanExporter(
|
|
27
|
+
endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
|
|
28
|
+
insecure=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Añadir el procesador de spans
|
|
32
|
+
trace.get_tracer_provider().add_span_processor(
|
|
33
|
+
BatchSpanProcessor(otlp_exporter)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.info("OpenTelemetry tracer configurado correctamente.")
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def instrument_app(app):
|
|
43
|
+
try:
|
|
44
|
+
# Instrumentar FastAPI
|
|
45
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
46
|
+
|
|
47
|
+
# Instrumentar SQLAlchemy
|
|
48
|
+
SQLAlchemyInstrumentor().instrument()
|
|
49
|
+
|
|
50
|
+
# Instrumentar Requests
|
|
51
|
+
RequestsInstrumentor().instrument()
|
|
52
|
+
|
|
53
|
+
logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Error al instrumentar la aplicación: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
import fcntl
|
|
4
|
+
import termios
|
|
5
|
+
from django.db import connection
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def terminal_width():
|
|
10
|
+
"""
|
|
11
|
+
Function to compute the terminal width.
|
|
12
|
+
"""
|
|
13
|
+
width = 0
|
|
14
|
+
try:
|
|
15
|
+
s = struct.pack('HHHH', 0, 0, 0, 0)
|
|
16
|
+
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
|
17
|
+
width = struct.unpack('HHHH', x)[1]
|
|
18
|
+
except (struct.error, fcntl.error, termios.error) as e:
|
|
19
|
+
print(f"Error computing terminal width: {e}")
|
|
20
|
+
|
|
21
|
+
if width <= 0:
|
|
22
|
+
try:
|
|
23
|
+
width = int(os.environ['COLUMNS'])
|
|
24
|
+
except (KeyError, ValueError) as e:
|
|
25
|
+
print(f"Error getting terminal width from environment: {e}")
|
|
26
|
+
|
|
27
|
+
return width if width > 0 else 80
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def SqlPrintingMiddleware(get_response):
|
|
31
|
+
"""
|
|
32
|
+
Middleware to print SQL queries in the terminal during a request in debug mode.
|
|
33
|
+
"""
|
|
34
|
+
def middleware(request):
|
|
35
|
+
response = get_response(request)
|
|
36
|
+
|
|
37
|
+
if not settings.DEBUG or not connection.queries:
|
|
38
|
+
return response
|
|
39
|
+
|
|
40
|
+
indentation = 2
|
|
41
|
+
print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.path_info}\033[0m\n")
|
|
42
|
+
width = terminal_width()
|
|
43
|
+
total_time = 0.0
|
|
44
|
+
|
|
45
|
+
for query in connection.queries:
|
|
46
|
+
nice_sql = query['sql'].replace('"', '').replace(',', ', ')
|
|
47
|
+
sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
|
|
48
|
+
total_time += float(query['time'])
|
|
49
|
+
|
|
50
|
+
while len(sql) > width - indentation:
|
|
51
|
+
print(f"{' ' * indentation}{sql[:width - indentation]}")
|
|
52
|
+
sql = sql[width - indentation:]
|
|
53
|
+
print(f"{' ' * indentation}{sql}\n")
|
|
54
|
+
|
|
55
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
|
|
56
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(connection.queries)}]\033[0m")
|
|
57
|
+
|
|
58
|
+
return response
|
|
59
|
+
|
|
60
|
+
return middleware
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Callable, Awaitable, Type, Any
|
|
3
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
4
|
+
from fastapi.exceptions import RequestValidationError
|
|
5
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from fk_utils.exceptions.custom_http_exception import CustomHTTPException
|
|
8
|
+
from fastapi import Request, Response
|
|
9
|
+
from fk_utils.middlewares.fastapi.i18n import _
|
|
10
|
+
import uuid
|
|
11
|
+
import logging
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ErrorCode(Enum):
|
|
18
|
+
VALIDATION_ERROR = (
|
|
19
|
+
"MAIN-VAL-001",
|
|
20
|
+
"Validation Error",
|
|
21
|
+
422,
|
|
22
|
+
"The input data is invalid.",
|
|
23
|
+
)
|
|
24
|
+
RESOURCE_NOT_FOUND = (
|
|
25
|
+
"MAIN-VAL-002",
|
|
26
|
+
"Resource Not Found",
|
|
27
|
+
404,
|
|
28
|
+
"The requested resource does not exist.",
|
|
29
|
+
)
|
|
30
|
+
SYSTEM_ERROR = (
|
|
31
|
+
"MAIN-SYS-001",
|
|
32
|
+
"System Error",
|
|
33
|
+
500,
|
|
34
|
+
"An unexpected error occurred.",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def __init__(self, error_code: str, title: str, status_code: int, detail: str):
|
|
38
|
+
self.error_code = error_code
|
|
39
|
+
self.title = title
|
|
40
|
+
self.status_code = status_code
|
|
41
|
+
self.detail = detail
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ErrorMiddleware(BaseHTTPMiddleware):
|
|
45
|
+
def __init__(self, app: Callable, error_enum: Type[ErrorCode], **kwargs: Any) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Middleware to handle global errors in the application.
|
|
48
|
+
|
|
49
|
+
:param app: FastAPI application.
|
|
50
|
+
:param error_enum: Enumeration of error codes.
|
|
51
|
+
"""
|
|
52
|
+
super().__init__(app, **kwargs)
|
|
53
|
+
self.error_enum = error_enum
|
|
54
|
+
|
|
55
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
56
|
+
try:
|
|
57
|
+
response = await call_next(request)
|
|
58
|
+
return response
|
|
59
|
+
except CustomHTTPException as custom_exc:
|
|
60
|
+
return self.build_error_response(
|
|
61
|
+
error_code=custom_exc.error_code,
|
|
62
|
+
detail=custom_exc.detail,
|
|
63
|
+
request=request,
|
|
64
|
+
)
|
|
65
|
+
except RequestValidationError as validation_exc:
|
|
66
|
+
user_friendly_detail = self.format_validation_errors(validation_exc, request)
|
|
67
|
+
return self.build_error_response(
|
|
68
|
+
error_code=self.error_enum.VALIDATION_ERROR,
|
|
69
|
+
detail=user_friendly_detail,
|
|
70
|
+
request=request,
|
|
71
|
+
)
|
|
72
|
+
except StarletteHTTPException as http_exc:
|
|
73
|
+
error_code = (
|
|
74
|
+
self.error_enum.RESOURCE_NOT_FOUND
|
|
75
|
+
if http_exc.status_code == 404
|
|
76
|
+
else self.error_enum.VALIDATION_ERROR
|
|
77
|
+
)
|
|
78
|
+
return self.build_error_response(
|
|
79
|
+
error_code=error_code,
|
|
80
|
+
detail=str(http_exc.detail),
|
|
81
|
+
request=request,
|
|
82
|
+
)
|
|
83
|
+
except Exception:
|
|
84
|
+
return self.build_error_response(
|
|
85
|
+
error_code=self.error_enum.SYSTEM_ERROR,
|
|
86
|
+
detail="An unexpected error occurred.",
|
|
87
|
+
request=request,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def build_error_response(self, error_code: ErrorCode, detail: str, request: Request) -> JSONResponse:
|
|
91
|
+
"""
|
|
92
|
+
Builds a JSON response for errors.
|
|
93
|
+
|
|
94
|
+
:param error_code: Error code.
|
|
95
|
+
:param detail: Error detail.
|
|
96
|
+
:param request: HTTP request.
|
|
97
|
+
:return: JSON response with the formatted error.
|
|
98
|
+
"""
|
|
99
|
+
instance_id = str(uuid.uuid4())
|
|
100
|
+
translated_detail = _(detail, request) if isinstance(detail, str) else detail
|
|
101
|
+
logger.error(
|
|
102
|
+
f"Error: {error_code.error_code} - {error_code.title} - {translated_detail} - Instance: {instance_id} - Traceback: {traceback.format_exc()}"
|
|
103
|
+
)
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
status_code=error_code.status_code,
|
|
106
|
+
content={
|
|
107
|
+
"error_code": error_code.error_code,
|
|
108
|
+
"title": _(error_code.title, request),
|
|
109
|
+
"status": error_code.status_code,
|
|
110
|
+
"detail": translated_detail,
|
|
111
|
+
"instance": instance_id,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def format_validation_errors(self, exc: RequestValidationError, request: Request) -> str:
|
|
116
|
+
error_messages = []
|
|
117
|
+
for error in exc.errors():
|
|
118
|
+
loc = "".join(map(str, error.get("loc", []))).replace("body", "")
|
|
119
|
+
msg = error.get("msg", "Invalid value")
|
|
120
|
+
|
|
121
|
+
ctx = error.get("ctx")
|
|
122
|
+
if ctx:
|
|
123
|
+
ctx_details = ", ".join(f"{value}" for key, value in ctx.items())
|
|
124
|
+
msg += f" {ctx_details}"
|
|
125
|
+
|
|
126
|
+
error_messages.append(f"{_(msg, request)}: {loc}")
|
|
127
|
+
|
|
128
|
+
return " | ".join(error_messages)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import gettext
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import boto3
|
|
5
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
from mypy_boto3_translate.client import TranslateClient
|
|
8
|
+
from fk_utils.cache.redis import CacheService
|
|
9
|
+
|
|
10
|
+
AWS_REGION_NAME = os.environ.get("AWS_REGION_NAME", "us-east-2")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TranslationWrapper:
|
|
14
|
+
def __init__(self, lang: str, translation_dir: Path, redis_url: str):
|
|
15
|
+
"""Initializes the translation object for a specific language."""
|
|
16
|
+
self.lang = lang
|
|
17
|
+
self.translation_dir = translation_dir
|
|
18
|
+
self.cache_service = CacheService(redis_url)
|
|
19
|
+
|
|
20
|
+
self.translations = gettext.translation(
|
|
21
|
+
"messages",
|
|
22
|
+
localedir=self.translation_dir,
|
|
23
|
+
languages=[self.lang],
|
|
24
|
+
fallback=True,
|
|
25
|
+
)
|
|
26
|
+
self.translations.install()
|
|
27
|
+
|
|
28
|
+
self.translate_client: TranslateClient = boto3.client(
|
|
29
|
+
"translate", region_name=AWS_REGION_NAME
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def gettext(self, message: str) -> str:
|
|
33
|
+
"""Returns the translation from the file or AWS Translate."""
|
|
34
|
+
cache_key = f"translation:{self.lang}:{message}"
|
|
35
|
+
cached_translation = self.cache_service.get(cache_key)
|
|
36
|
+
if isinstance(cached_translation, dict) and self.lang in cached_translation:
|
|
37
|
+
return cached_translation[self.lang]
|
|
38
|
+
|
|
39
|
+
translated_message = self.translations.gettext(message)
|
|
40
|
+
if translated_message == message:
|
|
41
|
+
try:
|
|
42
|
+
response = self.translate_client.translate_text(
|
|
43
|
+
Text=message,
|
|
44
|
+
SourceLanguageCode="en",
|
|
45
|
+
TargetLanguageCode=self.lang,
|
|
46
|
+
)
|
|
47
|
+
translated_message = response["TranslatedText"]
|
|
48
|
+
self.cache_service.add(
|
|
49
|
+
cache_key, {self.lang: translated_message}, expiration_time=86400
|
|
50
|
+
)
|
|
51
|
+
except (BotoCoreError, ClientError) as e:
|
|
52
|
+
print(f"Error en AWS Translate: {e}")
|
|
53
|
+
translated_message = message
|
|
54
|
+
|
|
55
|
+
return translated_message
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def set_locale(request: Request, translation_dir: Path, redis_url: str):
|
|
59
|
+
"""Sets the language according to the 'Accept-Language' header."""
|
|
60
|
+
lang = request.headers.get("Accept-Language", "en")
|
|
61
|
+
request.state.translation = TranslationWrapper(lang, translation_dir, redis_url)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _(message: str, request: Request) -> str:
|
|
65
|
+
"""Gets the translation of the message using the language configured in the request."""
|
|
66
|
+
return request.state.translation.gettext(message)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Awaitable, Callable
|
|
2
|
+
from fastapi import Request, Response
|
|
3
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from fk_utils.middlewares.fastapi.i18n import set_locale
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LanguageMiddleware(BaseHTTPMiddleware):
|
|
9
|
+
def __init__(self, app, *, translation_dir: Path, redis_url: str, **kwargs):
|
|
10
|
+
super().__init__(app, **kwargs)
|
|
11
|
+
self.translation_dir = translation_dir
|
|
12
|
+
self.redis_url = redis_url
|
|
13
|
+
|
|
14
|
+
async def dispatch(
|
|
15
|
+
self,
|
|
16
|
+
request: Request,
|
|
17
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
18
|
+
) -> Response:
|
|
19
|
+
await set_locale(request, self.translation_dir, self.redis_url)
|
|
20
|
+
response = await call_next(request)
|
|
21
|
+
return response
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
import fcntl
|
|
4
|
+
import termios
|
|
5
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
6
|
+
from sqlalchemy import event
|
|
7
|
+
from sqlalchemy.engine import Engine
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def terminal_width():
|
|
11
|
+
"""
|
|
12
|
+
Function to compute the terminal width.
|
|
13
|
+
"""
|
|
14
|
+
width = 0
|
|
15
|
+
try:
|
|
16
|
+
s = struct.pack('HHHH', 0, 0, 0, 0)
|
|
17
|
+
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
|
18
|
+
width = struct.unpack('HHHH', x)[1]
|
|
19
|
+
except (struct.error, fcntl.error, termios.error) as e:
|
|
20
|
+
print(f"Error computing terminal width: {e}")
|
|
21
|
+
|
|
22
|
+
if width <= 0:
|
|
23
|
+
try:
|
|
24
|
+
width = int(os.environ['COLUMNS'])
|
|
25
|
+
except (KeyError, ValueError) as e:
|
|
26
|
+
print(f"Error getting terminal width from environment: {e}")
|
|
27
|
+
|
|
28
|
+
return width if width > 0 else 80
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqlPrintingMiddleware(BaseHTTPMiddleware):
|
|
32
|
+
def __init__(self, app, debug: bool = True):
|
|
33
|
+
super().__init__(app)
|
|
34
|
+
self.debug = debug
|
|
35
|
+
|
|
36
|
+
async def dispatch(self, request, call_next):
|
|
37
|
+
if self.debug:
|
|
38
|
+
request.state.queries = []
|
|
39
|
+
|
|
40
|
+
@event.listens_for(Engine, "after_cursor_execute")
|
|
41
|
+
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
|
42
|
+
if not hasattr(request.state, 'queries'):
|
|
43
|
+
request.state.queries = []
|
|
44
|
+
request.state.queries.append({
|
|
45
|
+
'sql': statement,
|
|
46
|
+
'time': context.execution_time
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
response = await call_next(request)
|
|
50
|
+
|
|
51
|
+
if self.debug and hasattr(request.state, 'queries'):
|
|
52
|
+
indentation = 2
|
|
53
|
+
print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.url.path}\033[0m\n")
|
|
54
|
+
width = terminal_width()
|
|
55
|
+
total_time = 0.0
|
|
56
|
+
|
|
57
|
+
for query in request.state.queries:
|
|
58
|
+
nice_sql = query['sql'].replace('"', '').replace(',', ', ')
|
|
59
|
+
sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
|
|
60
|
+
total_time += float(query['time'])
|
|
61
|
+
|
|
62
|
+
while len(sql) > width - indentation:
|
|
63
|
+
print(f"{' ' * indentation}{sql[:width - indentation]}")
|
|
64
|
+
sql = sql[width - indentation:]
|
|
65
|
+
print(f"{' ' * indentation}{sql}\n")
|
|
66
|
+
|
|
67
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
|
|
68
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(request.state.queries)}]\033[0m")
|
|
69
|
+
|
|
70
|
+
return response
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
import fcntl
|
|
4
|
+
import termios
|
|
5
|
+
from flask import request, g
|
|
6
|
+
from sqlalchemy import event
|
|
7
|
+
from sqlalchemy.engine import Engine
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def terminal_width():
|
|
11
|
+
"""
|
|
12
|
+
Function to compute the terminal width.
|
|
13
|
+
"""
|
|
14
|
+
width = 0
|
|
15
|
+
try:
|
|
16
|
+
s = struct.pack('HHHH', 0, 0, 0, 0)
|
|
17
|
+
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
|
18
|
+
width = struct.unpack('HHHH', x)[1]
|
|
19
|
+
except (struct.error, fcntl.error, termios.error) as e:
|
|
20
|
+
print(f"Error computing terminal width: {e}")
|
|
21
|
+
|
|
22
|
+
if width <= 0:
|
|
23
|
+
try:
|
|
24
|
+
width = int(os.environ['COLUMNS'])
|
|
25
|
+
except (KeyError, ValueError) as e:
|
|
26
|
+
print(f"Error getting terminal width from environment: {e}")
|
|
27
|
+
|
|
28
|
+
return width if width > 0 else 80
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def SqlPrintingMiddleware(app):
|
|
32
|
+
@app.before_request
|
|
33
|
+
def before_request():
|
|
34
|
+
g.queries = []
|
|
35
|
+
|
|
36
|
+
@event.listens_for(Engine, "after_cursor_execute")
|
|
37
|
+
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
|
38
|
+
if 'queries' not in g:
|
|
39
|
+
g.queries = []
|
|
40
|
+
g.queries.append({
|
|
41
|
+
'sql': statement,
|
|
42
|
+
'time': context.execution_time
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
@app.after_request
|
|
46
|
+
def after_request(response):
|
|
47
|
+
if app.config['DEBUG'] and hasattr(g, 'queries'):
|
|
48
|
+
indentation = 2
|
|
49
|
+
print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.path}\033[0m\n")
|
|
50
|
+
width = terminal_width()
|
|
51
|
+
total_time = 0.0
|
|
52
|
+
|
|
53
|
+
for query in g.queries:
|
|
54
|
+
nice_sql = query['sql'].replace('"', '').replace(',', ', ')
|
|
55
|
+
sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
|
|
56
|
+
total_time += float(query['time'])
|
|
57
|
+
|
|
58
|
+
while len(sql) > width - indentation:
|
|
59
|
+
print(f"{' ' * indentation}{sql[:width - indentation]}")
|
|
60
|
+
sql = sql[width - indentation:]
|
|
61
|
+
print(f"{' ' * indentation}{sql}\n")
|
|
62
|
+
|
|
63
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
|
|
64
|
+
print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(g.queries)}]\033[0m")
|
|
65
|
+
|
|
66
|
+
return response
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# opentelemetry_config.py
|
|
2
|
+
import logging
|
|
3
|
+
from opentelemetry import trace
|
|
4
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
5
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
7
|
+
from opentelemetry.sdk.resources import Resource
|
|
8
|
+
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
10
|
+
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
|
|
11
|
+
|
|
12
|
+
from django.apps import AppConfig
|
|
13
|
+
|
|
14
|
+
from fk_utils import SETTINGS
|
|
15
|
+
|
|
16
|
+
# Configurar logging
|
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def configure_tracer():
|
|
22
|
+
try:
|
|
23
|
+
# Configurar el proveedor de trazas
|
|
24
|
+
trace.set_tracer_provider(TracerProvider(
|
|
25
|
+
resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
# Configurar el exportador OTLP
|
|
29
|
+
otlp_exporter = OTLPSpanExporter(
|
|
30
|
+
endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
|
|
31
|
+
insecure=True
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Añadir el procesador de spans
|
|
35
|
+
trace.get_tracer_provider().add_span_processor(
|
|
36
|
+
BatchSpanProcessor(otlp_exporter)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger.info("OpenTelemetry tracer configurado correctamente.")
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def instrument_app():
|
|
46
|
+
try:
|
|
47
|
+
# Instrumentar Django
|
|
48
|
+
DjangoInstrumentor().instrument()
|
|
49
|
+
|
|
50
|
+
# Instrumentar Requests
|
|
51
|
+
RequestsInstrumentor().instrument()
|
|
52
|
+
|
|
53
|
+
# Instrumentar PostgreSQL
|
|
54
|
+
Psycopg2Instrumentor().instrument()
|
|
55
|
+
|
|
56
|
+
logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error al instrumentar la aplicación: {e}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OpenTelemetryConfig(AppConfig):
|
|
63
|
+
name = 'fk_utils.traces.opentelemetry.django.trace'
|
|
64
|
+
verbose_name = "OpenTelemetry Integration"
|
|
65
|
+
|
|
66
|
+
def ready(self):
|
|
67
|
+
configure_tracer()
|
|
68
|
+
instrument_app()
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from opentelemetry import trace
|
|
3
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
4
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
5
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
6
|
+
from opentelemetry.sdk.resources import Resource
|
|
7
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
10
|
+
|
|
11
|
+
from fk_utils import SETTINGS
|
|
12
|
+
|
|
13
|
+
# Configurar logging
|
|
14
|
+
logging.basicConfig(level=logging.INFO)
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def configure_tracer():
|
|
19
|
+
try:
|
|
20
|
+
# Configurar el proveedor de trazas
|
|
21
|
+
trace.set_tracer_provider(TracerProvider(
|
|
22
|
+
resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
# Configurar el exportador OTLP
|
|
26
|
+
otlp_exporter = OTLPSpanExporter(
|
|
27
|
+
endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
|
|
28
|
+
insecure=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Añadir el procesador de spans
|
|
32
|
+
trace.get_tracer_provider().add_span_processor(
|
|
33
|
+
BatchSpanProcessor(otlp_exporter)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.info("OpenTelemetry tracer configurado correctamente.")
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def instrument_app(app):
|
|
43
|
+
try:
|
|
44
|
+
# Instrumentar FastAPI
|
|
45
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
46
|
+
|
|
47
|
+
# Instrumentar SQLAlchemy
|
|
48
|
+
SQLAlchemyInstrumentor().instrument()
|
|
49
|
+
|
|
50
|
+
# Instrumentar Requests
|
|
51
|
+
RequestsInstrumentor().instrument()
|
|
52
|
+
|
|
53
|
+
logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Error al instrumentar la aplicación: {e}")
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from opentelemetry import trace
|
|
3
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
4
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
5
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
6
|
+
from opentelemetry.sdk.resources import Resource
|
|
7
|
+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
10
|
+
|
|
11
|
+
from fk_utils import SETTINGS
|
|
12
|
+
|
|
13
|
+
# Configurar logging
|
|
14
|
+
logging.basicConfig(level=logging.INFO)
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def configure_tracer():
|
|
19
|
+
try:
|
|
20
|
+
# Configurar el proveedor de trazas
|
|
21
|
+
trace.set_tracer_provider(TracerProvider(
|
|
22
|
+
resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
# Configurar el exportador OTLP
|
|
26
|
+
otlp_exporter = OTLPSpanExporter(
|
|
27
|
+
endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
|
|
28
|
+
insecure=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Añadir el procesador de spans
|
|
32
|
+
trace.get_tracer_provider().add_span_processor(
|
|
33
|
+
BatchSpanProcessor(otlp_exporter)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.info("OpenTelemetry tracer configurado correctamente.")
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def instrument_app(app):
|
|
43
|
+
try:
|
|
44
|
+
# Configurar el tracer
|
|
45
|
+
configure_tracer()
|
|
46
|
+
|
|
47
|
+
# Instrumentar Flask
|
|
48
|
+
FlaskInstrumentor().instrument_app(app)
|
|
49
|
+
|
|
50
|
+
# Instrumentar SQLAlchemy
|
|
51
|
+
SQLAlchemyInstrumentor().instrument()
|
|
52
|
+
|
|
53
|
+
# Instrumentar Requests
|
|
54
|
+
RequestsInstrumentor().instrument()
|
|
55
|
+
|
|
56
|
+
logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error al instrumentar la aplicación: {e}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "fk_util_tools"
|
|
3
|
+
version = "0.1.14"
|
|
4
|
+
description = "Herramientas de utilidad o funciones comunes para los proyectos de FK."
|
|
5
|
+
authors = ["Steven Santacruz Garcia <stevengarcia1118@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
homepage = "https://gitlab.com/f3315/team-back-end/cross/packages/package_utils"
|
|
8
|
+
packages = [{include = "fk_utils"}]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = ">=3.7,<4.0"
|
|
12
|
+
boto3 = "*"
|
|
13
|
+
opentelemetry-api = "*"
|
|
14
|
+
opentelemetry-sdk = "*"
|
|
15
|
+
opentelemetry-instrumentation-fastapi = "*"
|
|
16
|
+
opentelemetry-instrumentation-flask = "*"
|
|
17
|
+
opentelemetry-instrumentation-django = "*"
|
|
18
|
+
opentelemetry-exporter-otlp = "*"
|
|
19
|
+
opentelemetry-instrumentation-sqlalchemy = "*"
|
|
20
|
+
opentelemetry-instrumentation-requests = "*"
|
|
21
|
+
opentelemetry-exporter-jaeger = "*"
|
|
22
|
+
opentelemetry-instrumentation-psycopg2 = "*"
|
|
23
|
+
psycopg2-binary = "*"
|
|
24
|
+
starlette = "*"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["poetry-core>=1.0.0"]
|
|
28
|
+
build-backend = "poetry.core.masonry.api"
|