Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions tracker/huble/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
Tracker — Integração do X-HUBLE-TRACE-ID (FastAPI, Django, Chalice)

Este README mostra como ligar a lib do trace id e dos logs nos três frameworks que você usa: FastAPI, Django (DRF) e Chalice — de forma modular, sem dependências cruzadas.
A lib garante que:

toda requisição entra com um X-HUBLE-TRACE-ID (se não vier, a lib cria um UUID v4);

o X-HUBLE-TRACE-ID é propagado no contexto e adicionado automaticamente no response;

chamadas outbound via httpx/requests recebem o header automaticamente;

todos os logs ganham o campo huble_trace_id.

APIs públicas principais:

from tracker.huble import setup_core, enable_framework, enable_clients
from tracker.huble.core import CANON_TRACE_HEADER, get_current_trace_id
# CANON_TRACE_HEADER == "X-HUBLE-TRACE-ID"


setup_core() → instala o filtro de log para incluir huble_trace_id em todos os logs.

enable_framework(app, log_request_response=False) → pluga o middleware/hook do framework detectado.

enable_clients(wanted=None) → aplica patch em httpx e requests para enviar o header automaticamente
(wanted pode ser ["httpx"], ["requests"] ou None para ambos).

1) FastAPI

main.py

import logging
from fastapi import FastAPI
from tracker.huble import setup_core, enable_framework, enable_clients
from tracker.huble.core import CANON_TRACE_HEADER, get_current_trace_id

# 1) Inicializa logging + trace-id em logs
setup_core()

# 2) Aplica patch de clients (httpx/requests) para outbound
enable_clients() # ou enable_clients(["httpx"]) / enable_clients(["requests"])

# 3) Cria app e liga o middleware
app = FastAPI()
enable_framework(app, log_request_response=True) # opcional: log detalhado de request/response

@app.get("/health")
def health():
return {
"ok": True,
"trace_id_in_context": get_current_trace_id(),
}


Testes rápidos

# 1) Sem header → a lib cria um UUID e retorna no response:
curl -i http://localhost:8000/health

# 2) Com header fixo → a lib respeita e propaga:
curl -i -H "X-HUBLE-TRACE-ID: 4a750618-2ad1-494f-9142-bbe0a6e55874" \
http://localhost:8000/health

2) Django (com DRF ou não)
2.1. Habilitar o middleware

settings.py

MIDDLEWARE = [
# Coloque o Huble cedo na cadeia
"tracker.huble.integrations.django_fw.HubleMiddleware",
# ... seus outros middlewares ...
]


Em dev dentro de Docker Compose, lembre de ajustar ALLOWED_HOSTS para aceitar o host/container (ex.: ALLOWED_HOSTS = ["*", "django_api", "localhost"]).

2.2. Inicializar a lib no startup do Django

Crie um AppConfig para rodar código de inicialização:

core/apps.py

from django.apps import AppConfig
from tracker.huble import setup_core, enable_clients

class CoreConfig(AppConfig):
name = "core"

def ready(self):
# 1) logging + trace em logs
setup_core()
# 2) patch de clients p/ outbound
enable_clients() # httpx/requests, conforme usado no projeto


settings.py

INSTALLED_APPS = [
"core.apps.CoreConfig", # <-- registre o AppConfig
# ... suas outras apps ...
]

2.3. Exemplo de view
# views.py
from django.http import JsonResponse
from tracker.huble.core import get_current_trace_id

def health(request):
return JsonResponse({
"ok": True,
"trace_id_in_context": get_current_trace_id(),
})


Testes rápidos

# Sem header → a lib cria e envia de volta
curl -i http://localhost:8000/health

# Com header fixo
curl -i -H "X-HUBLE-TRACE-ID: 4a750618-2ad1-494f-9142-bbe0a6e55874" \
http://localhost:8000/health

3) Chalice

app.py

from chalice import Chalice
from tracker.huble import setup_core, enable_framework, enable_clients
from tracker.huble.core import get_current_trace_id

app = Chalice(app_name="my-chalice-app")

# 1) logging + trace nos logs
setup_core()

# 2) patch de clients p/ outbound
enable_clients()

# 3) integra com o Chalice (hooks para request/response)
enable_framework(app, log_request_response=True)

@app.route("/health")
def health():
return {"ok": True, "trace_id_in_context": get_current_trace_id()}


Testes rápidos

# Chalice local (por exemplo via docker/compose)
curl -i http://localhost:8000/health

# Usando um trace fixo
curl -i -H "X-HUBLE-TRACE-ID: 4a750618-2ad1-494f-9142-bbe0a6e55874" \
http://localhost:8000/health

4) Logs com huble_trace_id

Ao chamar setup_core(), a lib instala um filtro no root logger que injeta huble_trace_id em todos os registros.
Você pode referenciar %(huble_trace_id)s no seu formatter:

Exemplo (Django settings.py)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"format": "%(asctime)s %(levelname)s trace=%(huble_trace_id)s %(name)s: %(message)s",
},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "console"},
},
"root": {"level": "INFO", "handlers": ["console"]},
}


Não precisa declarar o filtro aqui; o setup_core() já o adiciona programaticamente no root logger.

5) Outbound HTTP (httpx / requests)

Chamou enable_clients()? Então toda chamada cliente sai com o header X-HUBLE-TRACE-ID do contexto atual automaticamente.

import httpx
from tracker.huble.core import get_current_trace_id

async def call_upstream():
async with httpx.AsyncClient() as c:
r = await c.get("http://service/internal")
# O header X-HUBLE-TRACE-ID foi enviado automaticamente


Mesma ideia para requests:

import requests

def call_upstream_sync():
s = requests.Session()
r = s.get("http://service/internal")

6) Dicas e troubleshooting

Django 400 / DisallowedHost: adicione o hostname do container em ALLOWED_HOSTS (ou * em dev).

Quer ver o header no response? Ele sempre é adicionado; rode curl -i ... e procure X-HUBLE-TRACE-ID.

Header fixo para teste: gere um UUID v4 e envie:

curl -i -H "X-HUBLE-TRACE-ID: 4a750618-2ad1-494f-9142-bbe0a6e55874" http://...
15 changes: 15 additions & 0 deletions tracker/huble/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import logging
from .logging.filter import HubleTraceFilter

def setup_core() -> None:
root = logging.getLogger()
if not any(isinstance(f, HubleTraceFilter) for f in getattr(root, "filters", [])):
root.addFilter(HubleTraceFilter())

def enable_framework(app, **kwargs):
from .integrations import attach_framework
return attach_framework(app, **kwargs)

def enable_clients(clients=None) -> None:
from .integrations import attach_clients
attach_clients(clients)
50 changes: 50 additions & 0 deletions tracker/huble/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import uuid
from contextvars import ContextVar
from typing import Optional, Mapping, Any

CANON_TRACE_HEADER = "X-HUBLE-TRACE-ID"

_current_trace_id: ContextVar[Optional[str]] = ContextVar("x_huble_trace_id", default=None)

def get_current_trace_id() -> Optional[str]:
return _current_trace_id.get()

def set_current_trace_id(trace_id: str):
return _current_trace_id.set(trace_id)

def reset_current_trace_id(token) -> None:
try:
_current_trace_id.reset(token)
except Exception:
pass

def _ci_get(headers: Mapping[str, Any], name: str) -> Optional[str]:
nl = name.lower()
for k, v in headers.items():
if str(k).lower() == nl:
return v
return None

def _normalize_uuid(raw: str) -> Optional[str]:
if not raw:
return None
s = raw.strip()
try:
return str(uuid.UUID(s))
except Exception:
try:
return str(uuid.UUID(hex=s))
except Exception:
return None

def get_trace_id_if_any(headers: Mapping[str, Any]) -> Optional[str]:
raw = _ci_get(headers, CANON_TRACE_HEADER)
return _normalize_uuid(raw) if raw else None

def get_or_create_trace_id(headers: Mapping[str, Any]) -> tuple[str, str]:
raw = _ci_get(headers, CANON_TRACE_HEADER)
if raw:
norm = _normalize_uuid(raw)
if norm:
return norm, CANON_TRACE_HEADER.lower()
return str(uuid.uuid4()), "generated"
33 changes: 33 additions & 0 deletions tracker/huble/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Any, Iterable, Optional

def attach_framework(app: Any, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eu penso que vale o desenvolvedor da aplicação definir do que tentarmos adivinhar.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mas é ele que define. Da uma olhada no Readme de implementação para cada framework

mod = app.__class__.__module__
if "fastapi.applications" in mod or "starlette.applications" in mod:
from .fastapi_fw import FastAPIFrameworkAdapter
return FastAPIFrameworkAdapter().attach(app, **kwargs)
if "chalice.app" in mod:
from .chalice_fw import ChaliceFrameworkAdapter
return ChaliceFrameworkAdapter().attach(app, **kwargs)
if "django.core.handlers" in mod or "django.core.asgi" in mod:
from .django_fw import DjangoFrameworkAdapter
return DjangoFrameworkAdapter().attach(app, **kwargs)
raise RuntimeError(f"Framework não suportado: {mod}")

def attach_clients(clients: Optional[Iterable[str]] = None) -> None:
# auto por default
wanted = set((clients or {"httpx","requests"}))
if "httpx" in wanted:
try:
import httpx # noqa
from .httpx_client import HttpxClientAdapter
HttpxClientAdapter().patch()
except Exception:
pass
if "requests" in wanted:
try:
import requests # noqa
from .requests_client import RequestsClientAdapter
RequestsClientAdapter().patch()
except Exception:
pass
55 changes: 55 additions & 0 deletions tracker/huble/integrations/chalice_fw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations
from typing import Any, Optional, Iterable, Callable
from ..interfaces import IFrameworkAdapter
from ..core import (
CANON_TRACE_HEADER, get_or_create_trace_id, set_current_trace_id, reset_current_trace_id
)
import time, json

def _with_trace(log_request_response: bool=False, logger=None, redact_headers=None):
redact = {*(redact_headers or {"authorization","cookie","x-api-key"})}
def deco(fn: Callable):
def wrapper(*args, **kwargs):
from chalice import current_app, Response
req = current_app.current_request
tid, _ = get_or_create_trace_id(req.headers or {})
tok = set_current_trace_id(tid)
try:
start = time.perf_counter() if log_request_response else None
rv = fn(*args, **kwargs)
if log_request_response and logger:
try:
elapsed = round((time.perf_counter()-start)*1000, 2) if start else None
safe_headers = {
str(k).lower(): ("***REDACTED***" if str(k).lower() in redact else str(v))
for k, v in (req.headers or {}).items()
}
logger.info(json.dumps({
"event":"http_cycle","framework":"chalice",
"trace_id": tid, "method": req.method,
"path": req.context.get("path") or req.context.get("resourcePath"),
"status_code": 200, "elapsed_ms": elapsed, "headers": safe_headers
}))
except Exception:
pass
if isinstance(rv, Response):
rv.headers[CANON_TRACE_HEADER] = tid
return rv
# tuple/dict simples
return rv, 200, {CANON_TRACE_HEADER: tid}
finally:
reset_current_trace_id(tok)
return wrapper
return deco

def _autowrap(app, **opts):
for route in list(app.routes.values()):
for method, view in list(route.view_functions.items()):
route.view_functions[method] = _with_trace(**opts)(view)

class ChaliceFrameworkAdapter(IFrameworkAdapter):
def attach(
self, app: Any, *, log_request_response: bool=False, logger=None, redact_headers=None
) -> Any:
_autowrap(app, log_request_response=log_request_response, logger=logger, redact_headers=redact_headers)
return app
Loading
Loading