mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 22:04:20 +00:00
first commit
This commit is contained in:
137
docs_original_develop/api-clients.md
Normal file
137
docs_original_develop/api-clients.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user