mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 13:54:20 +00:00
138 lines
4.9 KiB
Markdown
138 lines
4.9 KiB
Markdown
# Padrão de implementação para clients de API externa
|
|
|
|
Este documento define o padrão obrigatório para qualquer client novo
|
|
que faça chamadas a APIs externas (HTTP ou outras). Ele garante que
|
|
todas as requisições apareçam no Langfuse com endpoint, status code
|
|
e response body, **sem que o dev precise escrever código de tracing
|
|
manual**.
|
|
|
|
## 1. Use sempre `traced_async_client` / `traced_sync_client`
|
|
|
|
Nunca instancie `httpx.AsyncClient` (ou `httpx.Client`) diretamente
|
|
em um client de API. Use os factories de [`src/utils/http.py`](../src/utils/http.py):
|
|
|
|
```python
|
|
from src.utils.http import traced_async_client
|
|
|
|
async with traced_async_client(timeout=self.timeout) as client:
|
|
response = await client.get(url, headers=headers)
|
|
# ...
|
|
```
|
|
|
|
O factory devolve um `httpx.AsyncClient` que automaticamente:
|
|
|
|
- registra o **endpoint real** (URL completa, com path e query string)
|
|
da request realizada — não depende de `self.base_url` ou `self.url`;
|
|
- registra o **status code** retornado;
|
|
- registra o **response body** (JSON ou texto truncado em 2000 chars);
|
|
- registra metadata também em **erros de rede** (timeout, connect error)
|
|
com `status_code=None` e o erro como body.
|
|
|
|
Argumentos comuns aceitos: `timeout`, `verify_ssl` (default
|
|
`settings.VERIFY_SSL`), `headers`, `base_url`, `response_sanitizer`,
|
|
e qualquer outro kwarg de `httpx.AsyncClient`.
|
|
|
|
## 2. Decore os métodos públicos com `@trace_tool`
|
|
|
|
Aplique `@trace_tool` em todo método que faz chamada externa, incluindo
|
|
helpers privados de autenticação (`_get_token`, `_refresh_xxx`). Cada
|
|
método decorado vira um span filho próprio no Langfuse, com nome
|
|
`NomeDaClasse.nome_do_metodo` — então uma classe com múltiplas APIs
|
|
(ex: `SpeechAnalyticsClient._get_token` vs `.get_prediction` vs
|
|
`.get_history`) não perde a granularidade.
|
|
|
|
```python
|
|
from src.utils.observer import trace_tool
|
|
|
|
class MyClient:
|
|
@trace_tool
|
|
async def get_something(self, ...) -> ...:
|
|
...
|
|
```
|
|
|
|
## 3. Convenções de nomenclatura
|
|
|
|
- **Métodos** com verbo + recurso: `get_history`, `post_prediction`,
|
|
`open_service_request`. O nome do método aparece no nome do span.
|
|
- **`__init__`** não precisa expor `base_url` para fins de tracing —
|
|
o endpoint real vem da request. Mantenha `self.base_url` apenas se
|
|
o client realmente precisa dela para construir URLs.
|
|
|
|
## 4. Sanitização de payloads sensíveis
|
|
|
|
Quando a resposta contém credenciais, tokens ou dados sensíveis que
|
|
não devem ir para o Langfuse, passe um `response_sanitizer` ao factory.
|
|
A função recebe o body já parseado e devolve a versão filtrada:
|
|
|
|
```python
|
|
def _sanitize_token_response(body):
|
|
if not isinstance(body, dict):
|
|
return body
|
|
return {
|
|
"expires_in": body.get("expires_in"),
|
|
"status": body.get("status"),
|
|
}
|
|
|
|
async with traced_async_client(
|
|
timeout=settings.SPEECH_TIMEOUT,
|
|
response_sanitizer=_sanitize_token_response,
|
|
) as client:
|
|
response = await client.post(url, data=...)
|
|
```
|
|
|
|
Exemplo real: [`SpeechAnalyticsClient._get_token`](../src/components/clients/speech_analytics_client.py).
|
|
|
|
## 5. Clients que não usam HTTP
|
|
|
|
Para clients que não falam HTTP (ex: [`TaisKbClient`](../src/components/clients/tais_kb_client.py),
|
|
que usa OCI SDK + oracledb), basta decorar o método público com
|
|
`@trace_tool`. O decorator cai no fallback de logar o retorno da função
|
|
como output do span. Não tente sintetizar um endpoint via
|
|
`set_tool_call_metadata` — apenas adiciona ruído.
|
|
|
|
## 6. Exemplo mínimo
|
|
|
|
```python
|
|
import httpx
|
|
import logging
|
|
from src.utils.observer import trace_tool
|
|
from src.utils.http import traced_async_client
|
|
from src.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FooClient:
|
|
def __init__(self) -> None:
|
|
self.base_url = settings.FOO_API_BASE_URL
|
|
self.token = settings.FOO_API_TOKEN
|
|
self.timeout = settings.FOO_API_TIMEOUT
|
|
|
|
@trace_tool
|
|
async def get_foo(self, foo_id: str) -> dict:
|
|
url = f"{self.base_url}/foo/{foo_id}"
|
|
headers = {"Authorization": f"Bearer {self.token}"}
|
|
try:
|
|
async with traced_async_client(timeout=self.timeout) as client:
|
|
response = await client.get(url, headers=headers)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
logger.error(f"Foo API HTTP error {exc.response.status_code}")
|
|
raise
|
|
```
|
|
|
|
Nada de `set_tool_call_metadata` espalhado pelo método. O span no
|
|
Langfuse já vai conter `endpoint`, `status_code` e `response`
|
|
automaticamente.
|
|
|
|
## Checklist de revisão
|
|
|
|
Ao revisar um PR de novo client, valide:
|
|
|
|
- [ ] Usa `traced_async_client` / `traced_sync_client` (não `httpx.AsyncClient` direto).
|
|
- [ ] Métodos públicos têm `@trace_tool`.
|
|
- [ ] Não há chamadas manuais a `set_tool_call_metadata`.
|
|
- [ ] Respostas com dados sensíveis passam por `response_sanitizer`.
|
|
- [ ] Nomes de método são `verbo_recurso` (legíveis como nome de span).
|