Metadata-Version: 2.4
Name: pxa-uv-exporter
Version: 0.1.0
Summary: Lightweight Prometheus exporter for FastAPI/Uvicorn multi-worker services (HTTP, event loop, worker, DB, external API)
License-Expression: Apache-2.0
Classifier: Programming Language :: Python :: 3.13
Classifier: Framework :: FastAPI
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: prometheus_client<1,>=0.21
Requires-Dist: psutil>=6.0
Requires-Dist: starlette>=0.40
Provides-Extra: fastapi
Requires-Dist: fastapi==0.128.0; extra == "fastapi"
Requires-Dist: uvicorn[standard]>=0.30; extra == "fastapi"
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == "httpx"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
Requires-Dist: httpx>=0.27; extra == "test"
Requires-Dist: fastapi==0.128.0; extra == "test"
Requires-Dist: uvicorn>=0.30; extra == "test"
Dynamic: license-file

# pxa-uv-exporter

FastAPI 0.128 / Uvicorn(4~32 worker) / Python 3.13 서비스용 경량 Prometheus exporter.
HTTP, event loop, worker, Tibero(pyodbc), 외부 REST API 구간만 계측한다.

## 1. Architecture

```
            ┌──────────────────────── uvicorn supervisor (pid P) ────────────────────────┐
            │  worker 1            worker 2              ...          worker N           │
 request ──►│  PxaHTTPMiddleware   PxaHTTPMiddleware                                      │
            │  LoopLagMonitor      LoopLagMonitor        (asyncio task, 0.1s)            │
            │  DB wrapper          DB wrapper            (pyodbc Connection/Cursor proxy)│
            │  httpx transport     httpx transport       (외부 API)                      │
            │      │ mmap write        │                                                │
            └──────┼───────────────────┼────────────────────────────────────────────────┘
                   ▼                   ▼
   $TMPDIR/pxa-uv-exporter-<user>/<P>-<P 시작시각>/
       counter_<pid>.db  histogram_<pid>.db  gauge_livesum_<pid>.db  gauge_livemax_<pid>.db
       pxa_worker_<pid>.json   ← worker registry (pid + create_time)

GET /metrics (아무 worker나 응답)
   → MultiProcessCollector 가 *.db 를 합산
   → _WorkerCollector 가 registry 의 살아있는 pid 에 대해 psutil(cpu, rss) 합산
```

* `/metrics` 는 sync handler → threadpool 에서 실행되어 event loop 를 막지 않는다.
  읽는 것은 수십 개의 작은 mmap 파일 + 최대 32개 pid 의 `/proc` 정보뿐이다.
* 모든 계측 코드는 `safe()` 로 감싸져 있어 계측 실패가 업무 요청을 실패시키지 않는다.

## 2. 프로젝트 구조

```
pxa-uv-exporter/
├── pyproject.toml
├── src/pxa_uv_exporter/
│   ├── __init__.py      # 공개 API (setup_metrics, wrap_connection, connect, external_call, httpx_transport)
│   ├── core.py          # setup_metrics, PxaExporter, /metrics, lifespan 연동
│   ├── _metrics.py      # metric 이름/label/bucket 정의 (유일한 정의 위치)
│   ├── _multiproc.py    # multiprocess dir, worker registry, stale 파일 정리
│   ├── http.py          # pure ASGI middleware, route template 해석
│   ├── loop_lag.py      # event loop lag (30s window max)
│   ├── db.py            # pyodbc Connection/Cursor wrapper
│   ├── external.py      # httpx transport + 범용 context manager
│   └── _safe.py
├── examples/app.py
└── tests/               # 단위 + 2-worker uvicorn 통합 테스트
```

## 3. 설치 (pyproject.toml 은 저장소 참조)

```bash
python3.13 -m venv ~/pxa-venv && . ~/pxa-venv/bin/activate
pip install ./pxa-uv-exporter            # 또는 사내 PyPI: pip install pxa-uv-exporter
```

의존성: `prometheus_client`, `psutil` (둘 다 root 불필요). httpx 는 선택.

## 4. 애플리케이션 적용 (최소 코드)

```python
from fastapi import FastAPI
from pxa_uv_exporter import setup_metrics

app = FastAPI()
setup_metrics(app)
```

`setup_metrics(app)` 가 등록하는 것: HTTP middleware, `GET /metrics`, event loop lag task
(기존 `lifespan` 을 감싸므로 사용자 lifespan 과 공존), multiprocess 디렉터리/worker registry.

옵션: `setup_metrics(app, path="/metrics", workers=8, dependencies=("agent-gateway","rag","llm-gateway"))`

### DB (Tibero / unixODBC / pyodbc)

```python
import pyodbc
import pxa_uv_exporter as pxa

# (1) 직접 connect: 획득 시간 + 이후 SQL 시간 모두 측정
conn = pxa.connect(pyodbc.connect, "DSN=TIBERO;UID=app;PWD=...", autocommit=False)

# (2) 이미 얻은 connection (자체 pool 등)
with pxa.acquire_timer():           # 획득 시간 측정 (선택)
    raw = my_pool.get()
conn = pxa.wrap_connection(raw)

cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE id = ?", user_id)   # → operation="select"
rows = cur.fetchall()
conn.close()                                               # → connections_in_use -1
```

* `connection_acquire_seconds`: `connect()`/`acquire_timer()` 구간 (unixODBC pooling 대기 포함)
* `query_duration_seconds{operation}`: `execute/executemany` 가 반환될 때까지 (fetch 제외)
* SQL 은 첫 키워드만 보고 `select|insert|update|delete|other` 로 분류, 전문/parameter 는 저장하지 않음
* `conn.autocommit = ...`, `cursor.fast_executemany = True` 등 속성 설정은 pyodbc 객체로 그대로 전달
* pyodbc 는 blocking 이므로 반드시 `def` endpoint(threadpool) 나 `run_in_threadpool` 에서 사용할 것.
  `async def` 안에서 직접 호출하면 `pxa_uv_event_loop_lag_seconds` 가 올라간다 (이것이 바로 탐지 대상).

### 외부 API (Agent Gateway / RAG / LLM Gateway)

```python
import httpx, pxa_uv_exporter as pxa

agent_gw = httpx.AsyncClient(
    base_url="https://agent-gw.internal",
    timeout=httpx.Timeout(30, connect=3),
    transport=pxa.httpx_transport("agent-gateway",
                                  inner=httpx.AsyncHTTPTransport(limits=httpx.Limits(max_connections=200))),
)
r = await agent_gw.post("/v1/run", json=payload)   # 호출부 변경 없음
```

httpx 외 client (aiohttp, requests, vendor SDK):

```python
async with pxa.external_call("llm-gateway", "POST") as call:
    resp = await session.post(url, json=body)
    call.status = resp.status            # 설정 안 하면 result="error"
```

* `dependency` 는 `setup_metrics(dependencies=...)` 에 등록된 논리 이름만 허용, 그 외는 `other`
* `result`: `2xx|4xx|5xx|timeout|error` (예외 클래스명에 Timeout 포함 시 timeout)
* duration 은 **응답 헤더 수신까지** (streaming LLM 의 경우 time-to-first-byte)

## 5. Uvicorn multiprocess 처리

| 항목 | 처리 |
|---|---|
| 디렉터리 | `PROMETHEUS_MULTIPROC_DIR` 가 있으면 그대로, 없으면 `$TMPDIR/pxa-uv-exporter-<user>/<supervisor pid>-<start>` 자동 생성 (0700). 재기동 시 새 디렉터리라 이전 run 과 섞이지 않음 |
| 이미 import 된 prometheus_client | `values.ValueClass` 를 mmap 방식으로 명시 교체 후 metric 생성 |
| counter / histogram | worker 별 파일 → scrape 시 합산. worker 가 죽어도 파일 유지 → 합계 단조 증가 |
| in_flight / in_use | `livesum` gauge. 죽은 worker 의 live 파일은 scrape 시 `mark_process_dead` 로 제거 |
| event loop lag | `livemax` → 가장 나쁜 worker 값 |
| workers_alive / cpu / rss | worker registry(pid + create_time, pid 재사용 판별) 기준 psutil 합산 |
| workers_configured | `setup_metrics(workers=)` > `PXA_UV_WORKERS` > `WEB_CONCURRENCY` > `--workers` 인자 |
| 같은 디렉터리 재사용 | 시작 시 살아있는 다른 worker 가 없으면 죽은 pid 의 `*.db` 삭제 |

`pxa_uv_worker_cpu_seconds_total` 은 살아있는 worker 의 합이라 worker 재시작 시 감소할 수 있다.
Prometheus `rate()` 는 이를 counter reset 으로 처리한다.

## 6. 실행 (일반 계정, systemd 없이)

```bash
cd ~/app
export PXA_UV_WORKERS=8                       # 선택 (자동 감지됨)
nohup ~/pxa-venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 8 \
      > ~/logs/uvicorn.out 2>&1 &
echo $! > ~/run/uvicorn.pid
# 중지: kill -TERM $(cat ~/run/uvicorn.pid)
```

gunicorn 사용 시에도 동일: `gunicorn main:app -k uvicorn.workers.UvicornWorker -w 8`.

## 7. `/metrics` 출력 예 (2 worker 실측, bucket 행 일부 생략)

```
pxa_uv_http_requests_total{method="GET",route="/api/users/{user_id}",status_class="2xx"} 10.0
pxa_uv_http_requests_total{method="GET",route="other",status_class="4xx"} 1.0
pxa_uv_http_request_duration_seconds_bucket{method="GET",route="/api/users/{user_id}",status_class="2xx",le="0.01"} 9.0
pxa_uv_http_request_duration_seconds_count{method="GET",route="/api/users/{user_id}",status_class="2xx"} 10.0
pxa_uv_http_requests_in_flight 0.0
pxa_uv_event_loop_lag_seconds 0.29
pxa_uv_workers_configured 2.0
pxa_uv_workers_alive 2.0
pxa_uv_worker_cpu_seconds_total 1.375
pxa_uv_worker_rss_bytes 1.29134592e+08
pxa_uv_db_connection_acquire_seconds_count 10.0
pxa_uv_db_connections_in_use 0.0
pxa_uv_db_query_duration_seconds_count{operation="select"} 10.0
pxa_uv_external_requests_total{dependency="agent-gateway",method="GET",result="timeout"} 1.0
pxa_uv_external_request_duration_seconds_count{dependency="agent-gateway",method="GET",result="timeout"} 1.0
pxa_uv_external_requests_in_flight{dependency="agent-gateway",method="GET"} 0.0
```

## 8. 테스트

```bash
pip install -e ".[test]"
pytest -q          # 23 tests: route template, 5xx, 계측 실패 격리, DB, httpx, loop lag, 2-worker 통합
```
