JobDri์ ๋น๋๊ธฐ AI ์์
์ ์ฒ๋ฆฌํ๋ Python ์์ปค ์๋ฒ์
๋๋ค.
์ด ๋ ํฌ๋ Spring Boot ๋ฉ์ธ ์๋ฒ์ RabbitMQ ์ฌ์ด์์ ๋์ํ๋ฉฐ, ์๋ ๋ ์ข ๋ฅ์ ์์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
- ์ฑ์ฉ ๊ณต๊ณ ์ ๋ฆฌ(
JOB_POSTING_INGEST) - ์์์ ๋ถ์(
ANALYSIS)
์์ ์ ํ์์ ๊ตฌ๋ ํ ๋ค OpenAI ํธ์ถ์ ์ํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ Spring ๋ด๋ถ API๋ก ์ ์ฅ/์๋ฃ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์ด ๋ ํฌ์ ํต์ฌ ์ญํ ์ ๋๋ค.
๋ฉ์ธ ๋ฐฑ์๋(Spring)๋ ์ฌ์ฉ์ ์์ฒญ์ ๋ฐ์ ๋ค ์ง์ ๊ธด AI ์์ ์ ์ํํ์ง ์์ต๋๋ค. ๋์ RabbitMQ์ ์์ ๋ฉ์์ง๋ฅผ ์ ์ฌํ๊ณ , ์ด ์์ปค๊ฐ ๋ฉ์์ง๋ฅผ ์๋นํฉ๋๋ค.
์์ปค๋ ๋ค์ ์์๋ก ๋์ํฉ๋๋ค.
- RabbitMQ ํ๋ฅผ ๊ตฌ๋ ํฉ๋๋ค.
- ๋ฉ์์ง๋ฅผ ์ญ์ง๋ ฌํํด ์์ ํ์ ์ ํ๋ณํฉ๋๋ค.
- Spring ๋ด๋ถ API์ ์์
์ํ๋ฅผ
RUNNING์ผ๋ก ๋ฐ์ํฉ๋๋ค. - OpenAI๋ฅผ ํธ์ถํด ์ฑ์ฉ ๊ณต๊ณ ์ถ์ถ/๋ถ๋ฅ/์์ฑ ๋๋ ์์์ ๋ถ์์ ์ํํฉ๋๋ค.
- ๊ฒฐ๊ณผ๋ฅผ Spring ๋ด๋ถ API์ ์ ์ฅํฉ๋๋ค.
- ์ต์ข ์๋ฃ ์ฝ๋ฐฑ์ ์ ๋ฌํฉ๋๋ค.
- ์คํจ ์ ์ฌ์๋, DLQ ์ ์ฌ, recovery spool ๋ณต๊ตฌ๋ฅผ ์ํํฉ๋๋ค.
flowchart LR
A["Client"] --> B["Spring Boot API"]
B --> C["RabbitMQ Exchange<br/>jobdri.worker.exchange"]
C --> D["jobdri.job-posting.ingest"]
C --> E["jobdri.analysis.execute"]
D --> F["Python Worker"]
E --> F
F --> G["OpenAI API"]
F --> H["Spring Internal Worker API"]
F --> I["Recovery Spool"]
F --> J["DLQ"]
H --> B
ํ ์ ์ฌ๋ ์ด ๋ ํฌ๊ฐ ์๋๋ผ ๋ฉ์ธ ๋ฐฑ์๋(Spring)์์ ์ํํฉ๋๋ค.
- ์ฑ์ฉ ๊ณต๊ณ ์์
APP_WORKER_JOB_POSTING_EXCHANGE+APP_WORKER_JOB_POSTING_ROUTING_KEY - ์์์ ๋ถ์ ์์
APP_WORKER_ANALYSIS_EXCHANGE+APP_WORKER_ANALYSIS_ROUTING_KEY
์์ปค๋ ์ ์ฌ๋ ๋ฉ์์ง๋ฅผ ์๋นํ๋ ์ชฝ์ ๋๋ค.
์์ปค ์์ ์ app/consumer.py ์ RabbitMqConsumer.start()๊ฐ ์คํ๋๊ณ , ๋ด๋ถ async runtime์ด app/async_runtime.py ๋ฅผ ํตํด ๋ค์ ๋ ํ๋ฅผ ๋น๋๊ธฐ๋ก ๊ตฌ๋
ํฉ๋๋ค.
APP_WORKER_JOB_POSTING_QUEUEAPP_WORKER_ANALYSIS_QUEUE
RabbitMQ ์ฐ๊ฒฐ์ aio-pika ๊ธฐ๋ฐ์ด๋ฉฐ, ์ฑ๋ QoS prefetch_count=WORKER_PREFETCH_COUNT ๋ฅผ ์ ์งํฉ๋๋ค. ์ฆ, MQ fetch ์์ฒด์ ๋ด๋ถ HTTP/OpenAI I/O๊ฐ ๊ฐ์ event loop์์ ๊ฒน์ณ ์ฒ๋ฆฌ๋ ์ ์์ต๋๋ค.
๋ํ task type๋ณ bounded concurrency๋ฅผ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.
WORKER_DEFAULT_CONCURRENCY_LIMITWORKER_ANALYSIS_CONCURRENCY_LIMITWORKER_JOB_POSTING_CONCURRENCY_LIMIT
๊ฐ ๋ฉ์์ง๋ ๊ณตํต consumer๊ฐ task type๋ณ processor๋ก dispatchํ๊ณ , ์ค์ ๋์ ์ฒ๋ฆฌ ์ฌ๋กฏ์ app/concurrency.py ์ limiter๊ฐ ์ ์ดํฉ๋๋ค. ์ด ๊ตฌ์กฐ ๋๋ถ์ analysis ์ ์ฒด๊ฐ jobposting ์ ์ฒด๋ฅผ ๋ง์ง ์๋๋ก ์ ํ๊ฐ์ ๋ถ๋ฆฌํด ์ด์ํ ์ ์์ต๋๋ค.
์์ต๋๋ค. ์ด ์์ปค๋ ์๋น์์ด๋ฉด์ ์ผ๋ถ ์ํฉ์์๋ ๋ค์ RabbitMQ์ ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค.
- ์ฌ์๋ ์
์๋ณธ ๋ฉ์์ง์
retryCount๋ฅผ ์ฆ๊ฐ์์ผ ๊ฐ์ exchange/routing key๋ก ์ฌ๋ฐํ - ์ต์ข ์คํจ ์ DLQ ํ๋ก ๋ฐํ
์ฆ, ์ ์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ๋ RabbitMQ๋ก ๋ค์ ๋ณด๋ด์ง ์๊ณ Spring ๋ด๋ถ API๋ก ์ ๋ฌํ๊ณ , ํ ์ฌ๋ฐํ์ ์ฌ์๋/์คํจ ์ฒ๋ฆฌ์๋ง ์ฌ์ฉํฉ๋๋ค.
taskType=JOB_POSTING_INGEST
- Spring์ด ์์ ๋ฉ์์ง๋ฅผ ํ์ ์ ์ฌํฉ๋๋ค.
- ์์ปค๊ฐ ๋ฉ์์ง๋ฅผ ์๋นํฉ๋๋ค.
/api/internal/worker/job-postings/tasks/{taskId}/running์ผ๋ก ์ํ๋ฅผRUNNING์ฒ๋ฆฌํฉ๋๋ค./api/internal/worker/job-postings/ingest/context๋ก ์ด๋ฏธ์ง URL ๋ฑ ์ปจํ ์คํธ๋ฅผ ์กฐํํฉ๋๋ค.- OpenAI๋ก ๊ณต๊ณ ์ถ์ถ์ ์ํํฉ๋๋ค.
/api/internal/worker/job-postings/classification/candidates๋ก ๋ถ๋ฅ ํ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.- OpenAI๋ก ํ๋ณด ์ค ์๋ถ๋ฅ๋ฅผ ์ ํํฉ๋๋ค.
- ์ ๋ขฐ๋๊ฐ ๋ฎ์ผ๋ฉด ์ ์ฅ ์์ด
/complete๋ก ์ข ๋ฃํฉ๋๋ค. - ์ ๋ขฐ๋๊ฐ ์ถฉ๋ถํ๋ฉด OpenAI๋ก ์ ์ฅ์ฉ ๊ณต๊ณ ๋ด์ฉ์ ์์ฑํฉ๋๋ค.
/result๋ก ์ค๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํฉ๋๋ค./ingest/finalize๋ก ์ต์ข ์ ์ฅ/์๋ฃ๋ฅผ ์์ฒญํฉ๋๋ค.
taskType=ANALYSIS
- Spring์ด ๋ถ์ ์์ ๋ฉ์์ง๋ฅผ ํ์ ์ ์ฌํฉ๋๋ค.
- ์์ปค๊ฐ ๋ฉ์์ง๋ฅผ ์๋นํฉ๋๋ค.
- ํ ๋๊ธฐ ์๊ฐ์ด
APP_WORKER_ANALYSIS_QUEUE_TIMEOUT_MILLIS๋ฅผ ์ด๊ณผํ๋์ง ๋จผ์ ๊ฒ์ฌํฉ๋๋ค. /api/internal/worker/analysis/tasks/{taskId}/running์ผ๋ก ์ํ๋ฅผRUNNING์ฒ๋ฆฌํฉ๋๋ค./api/internal/worker/analysis/context๋ก ๋ถ์์ ํ์ํ ๊ณต๊ณ /๋ฌธํญ/๋ต๋ณ ๋ฐ์ดํฐ๋ฅผ ์กฐํํฉ๋๋ค.- OpenAI๋ก ๋ถ์์ ์ํํฉ๋๋ค.
/result๋ก ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํฉ๋๋ค./complete๋ก ์ต์ข ์๋ฃ ์ฒ๋ฆฌํฉ๋๋ค.
{
"messageId": "msg-001",
"taskType": "JOB_POSTING_INGEST",
"taskId": "task-job-001",
"userId": 1,
"rawText": "์ฑ์ฉ ๊ณต๊ณ ์๋ฌธ",
"imageObjectKey": null,
"retryCount": 0,
"maxRetryCount": 3,
"submittedAt": "2026-07-20T09:00:00Z"
}{
"messageId": "msg-002",
"taskType": "ANALYSIS",
"taskId": "task-analysis-001",
"userId": 1,
"mockApplyId": 42,
"retryCount": 0,
"maxRetryCount": 3,
"submittedAt": "2026-07-20T09:10:00Z"
}์คํค๋ง ์๋ณธ์ app/schemas.py ์ ์ ์๋์ด ์์ต๋๋ค.
์ด ์์ปค๋ ๋จ์ consume ํ ์ข ๋ฃํ์ง ์๊ณ , ์คํจ ๋ด์ฑ์ ๊ฐ์ถ ๊ตฌ์กฐ๋ก ์์ฑ๋์ด ์์ต๋๋ค.
RetryableWorkerError๋ฐ์ ์ ์ฌ์๋ ๋์์ผ๋ก ๊ฐ์ฃผํฉ๋๋ค.retryCount + 1๊ฐ์ ๋ด์ ๊ฐ์ ํ ํ ํด๋ก์ง๋ก ์ฌ๋ฐํํฉ๋๋ค.- ์ต๋ ์ฌ์๋ ํ์๋ ์์
๋ณ๋ก ๋ค๋ฆ
๋๋ค.
- ์ฑ์ฉ ๊ณต๊ณ :
WORKER_MAX_RETRY_COUNT - ์์์ ๋ถ์:
APP_WORKER_ANALYSIS_MAX_RETRY_COUNT
- ์ฑ์ฉ ๊ณต๊ณ :
์๋ ๊ฒฝ์ฐ์๋ DLQ๋ก ๋ณด๋ ๋๋ค.
- ๋น์ฌ์๋ ์ค๋ฅ(
NonRetryableWorkerError) - ์ฌ์๋ ํ์ ์ด๊ณผ
์ค์ ํค๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
APP_WORKER_JOB_POSTING_DLQAPP_WORKER_ANALYSIS_DLQ
OpenAI ํธ์ถ์ ์ฑ๊ณตํ์ง๋ง Spring ๋ด๋ถ API๋ก ์ต์ข ์๋ฃ ์ฝ๋ฐฑ์ ๋ณด๋ด๋ ๋จ๊ณ์์ ์คํจํ ์ ์์ต๋๋ค. ์ด๋ ๊ฒฐ๊ณผ๋ฅผ ์์ง ์๋๋ก ํ์ผ ๊ธฐ๋ฐ spool์ pending delivery๋ฅผ ๋จ๊น๋๋ค.
- ๊ธฐ๋ณธ ๊ฒฝ๋ก
APP_WORKER_RECOVERY_SPOOL_DIR=.worker-spool - terminal message ledger ๊ฒฝ๋ก
APP_WORKER_TERMINAL_MESSAGE_DIR=.worker-spool/terminal-messages
์์ปค๋ ์์ ์์ ๊ณผ ์ฃผ๊ธฐ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฃจํ์์ spool ํ์ผ์ ๋ค์ ์ฝ์ด ๋ฏธ์ ๋ฌ ์๋ฃ ์์ฒญ์ ์ฌ์ ์กํฉ๋๋ค.
๊ด๋ จ ๊ตฌํ์ ๋ค์ ํ์ผ์ ์์ต๋๋ค.
์ด ์์ปค๋ Spring ๊ณต๊ฐ API๊ฐ ์๋๋ผ ๋ด๋ถ worker API๋ฅผ ํธ์ถํฉ๋๋ค. ๊ณตํต ํค๋๋ก X-Internal-Api-Key๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์ฃผ์ ์ฐ๋ ์๋ํฌ์ธํธ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
POST /api/internal/worker/job-postings/tasks/{taskId}/runningPOST /api/internal/worker/job-postings/ingest/contextPOST /api/internal/worker/job-postings/classification/candidatesPOST /api/internal/worker/job-postings/tasks/{taskId}/resultPOST /api/internal/worker/job-postings/ingest/finalizePOST /api/internal/worker/job-postings/tasks/{taskId}/retryPOST /api/internal/worker/job-postings/tasks/{taskId}/failedGET /api/internal/worker/job-postings/tasks/{taskId}
POST /api/internal/worker/analysis/tasks/{taskId}/runningPOST /api/internal/worker/analysis/contextPOST /api/internal/worker/analysis/tasks/{taskId}/resultPOST /api/internal/worker/analysis/tasks/{taskId}/completePOST /api/internal/worker/analysis/tasks/{taskId}/retryPOST /api/internal/worker/analysis/tasks/{taskId}/failedGET /api/internal/worker/analysis/tasks/{taskId}
๊ตฌํ์ app/api_client.py ์ ์์ผ๋ฉฐ, ํ์ฌ๋ sync ๋ฉ์๋์ httpx.AsyncClient ๊ธฐ๋ฐ async ๋ฉ์๋๋ฅผ ํจ๊ป ์ ๊ณตํฉ๋๋ค.
app/openai_client.py ์ JobPostingOpenAiWorker ๊ฐ ์๋ 3๋จ๊ณ๋ฅผ ๋ด๋นํฉ๋๋ค.
extract๊ณต๊ณ ํ ์คํธ ๋๋ ์ด๋ฏธ์ง์์ ๊ตฌ์กฐํ ์ ๋ณด ์ถ์ถclassifySpring์ด ์ค ๋ถ๋ฅ ํ๋ณด ์ค ๊ฐ์ฅ ์ ํฉํ ์๋ถ๋ฅ ์ ํgenerate์ ์ฅ ๊ฐ๋ฅํ ํํ์ ์ ์ ๋ ๊ณต๊ณ ๋ด์ฉ ์์ฑ
AnalysisOpenAiWorker ๊ฐ ๋ฌธํญ๋ณ ๋ต๋ณ๊ณผ ๊ณต๊ณ ๋งฅ๋ฝ์ ๋ฐํ์ผ๋ก ์ข
ํฉ ์ ์์ ํผ๋๋ฐฑ์ ์์ฑํฉ๋๋ค.
ํ์ฌ OpenAI ํธ์ถ๋ sync/async wrapper๋ฅผ ๋ชจ๋ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, ์ค์ consumer ๊ฒฝ๋ก๋ async client๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๊ธฐ๋ณธ ๋ชจ๋ธ์ ์๋ ํ๊ฒฝ๋ณ์๋ก ์ ์ด๋ฉ๋๋ค.
OPENAI_JOB_POSTING_MODEL=gpt-4o-miniOPENAI_ANALYSIS_MODEL=gpt-4.1-mini
app/
main.py # FastAPI ์ฑ๊ณผ health endpoint
worker.py # CLI worker ์ง์
์
consumer.py # worker ์กฐ๋ฆฝ๊ณผ ๊ณตํต consume helper
async_runtime.py # aio-pika ๊ธฐ๋ฐ async consumer runtime
concurrency.py # task type๋ณ bounded concurrency limiter
processors.py # task type๋ณ processor ๋ถ๋ฆฌ
delivery.py # result store / finalize / complete ์ ๋ฌ ๊ณ์ธต
api_client.py # Spring ๋ด๋ถ API ํด๋ผ์ด์ธํธ (sync + async)
openai_client.py # OpenAI ํธ์ถ ๋ก์ง (sync + async)
async_utils.py # sync/async bridge helper
recovery.py # recovery spool ์ ์ฅ/๋ณต๊ตฌ
schemas.py # ๋ฉ์์ง/์๋ต ์คํค๋ง
logging_utils.py # ๊ตฌ์กฐํ ๋ก๊ทธ ํํฐ
tests/
test_prometheus_metrics.py
test_recovery_flow.py
deploy/
docker-compose.worker.prod.yml
docs/
BACKEND_SERVER_DEPLOY.md
RENDER_DEPLOY_CHECKLIST.md
- Python 3.12 ์ด์ ๊ถ์ฅ
- RabbitMQ ์ ๊ทผ ๊ฐ๋ฅ
- Spring ๋ด๋ถ worker API ์ ๊ทผ ๊ฐ๋ฅ
- OpenAI API ํค
์ด ๋ ํฌ๋ Dockerfile ๊ธฐ์ค์ผ๋ก Python 3.12 ํ๊ฒฝ์์ ์คํ๋ฉ๋๋ค.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt์ต์ํ ์๋ ๊ฐ์ ํ์ํฉ๋๋ค.
APP_WORKER_INTERNAL_API_KEY=change-me
SPRING_API_BASE_URL=http://localhost:8080
OPENAI_API_KEY=sk-...
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_VHOST=/
APP_WORKER_JOB_POSTING_EXCHANGE=jobdri.worker.exchange
APP_WORKER_JOB_POSTING_QUEUE=jobdri.job-posting.ingest
APP_WORKER_JOB_POSTING_ROUTING_KEY=job-posting.ingest
APP_WORKER_JOB_POSTING_DLQ=jobdri.job-posting.ingest.dlq
APP_WORKER_ANALYSIS_EXCHANGE=jobdri.worker.exchange
APP_WORKER_ANALYSIS_QUEUE=jobdri.analysis.execute
APP_WORKER_ANALYSIS_ROUTING_KEY=analysis.execute
APP_WORKER_ANALYSIS_DLQ=jobdri.analysis.execute.dlq
WORKER_DEFAULT_CONCURRENCY_LIMIT=1
WORKER_ANALYSIS_CONCURRENCY_LIMIT=1
WORKER_JOB_POSTING_CONCURRENCY_LIMIT=1์ ์ฒด ๋ชฉ๋ก์ app/config.py ์ deploy/docker-compose.worker.prod.yml ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋ฉ๋๋ค.
uvicorn app.main:app --host 0.0.0.0 --port 8000- health endpoint
GET /health - metrics endpoint
GET /metrics
FastAPI ์ฑ startup์์ RabbitMQ consumer๋ ํจ๊ป ์์๋๋ฏ๋ก, ๋ณ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ปค ํ๋ก์ธ์ค๋ฅผ ์ถ๊ฐ๋ก ๋์ฐ์ง ์์ต๋๋ค.
์ด๋ฏธ์ง๋ Dockerfile ๊ธฐ์ค์ผ๋ก ๋น๋๋๋ฉฐ ๊ธฐ๋ณธ ์คํ ๋ช
๋ น์ ์๋์ ๊ฐ์ต๋๋ค.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]์ด์ ๋ฐฐํฌ ์์๋ deploy/docker-compose.worker.prod.yml ์ ์์ต๋๋ค.
์์ธ ์ด์ ๋ฌธ์๋ ์๋ ํ์ผ์ ์ฐธ๊ณ ํ์ธ์.
๋ก๊ทธ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ ์ปจํ ์คํธ๊ฐ ํจ๊ป ๋จ์ต๋๋ค.
taskIdmessageIdworkerIdretryCount
์ ์ ๊ธฐ๋ ์ ํ์ธํ ๋ํ ๋ก๊ทธ:
Worker process started.RabbitMQ consumer started.
๊ธฐ์กด Prometheus ๋ฉํธ๋ฆญ์ ์ ์งํ๋ฉด์, 2์ฐจ ๊ตฌํ์์๋ async ๊ฒฝ๋ก์ ์ด์ ๋ณ๋ชฉ์ ๋ ์ง์ ํด์ํ ์ ์๋๋ก ์๋๋ฅผ ์ ๋ฆฌํ์ต๋๋ค.
worker_task_queue_wait_duration_seconds{task_type}ํ ์ ์ฒด ์๊ฐ์ ๋ด ๋๋ค. ํ์ฌ ๋ณ๋ชฉ 1์์ ํ์ธ์ฉ์ ๋๋ค.worker_task_processing_duration_seconds{task_type,outcome}worker ๋ด๋ถ end-to-end ์ฒ๋ฆฌ ์๊ฐ์ ๋ด ๋๋ค.llm_request_duration_seconds{task_type,operation,outcome}OpenAI ํธ์ถ latency๋ฅผ ๋ด ๋๋ค.worker_internal_api_duration_seconds{task_type,endpoint,method,outcome}Spring internal API ์ ์ฒด ํธ์ถ ์๊ฐ์ ๋ด ๋๋ค.worker_context_fetch_duration_seconds{task_type,endpoint,outcome}context fetch ์ ์ฉ ๋ทฐ์ ๋๋ค.worker_callback_duration_seconds{task_type,endpoint,outcome}complete/finalize/retry/failed callback ์ ์ฉ ๋ทฐ์ ๋๋ค.worker_task_inflight{task_type}ํ์ฌ ๋์์ ์ฒ๋ฆฌ ์ค์ธ task ์์ ๋๋ค.worker_task_concurrency_limit{task_type}task type๋ณ ์ค์ ์ํ์ ๋๋ค.inflight์ ๊ฐ์ด ๋ณด๋ฉด ์ฌ๋กฏ ํฌํ ์ฌ๋ถ๋ฅผ ํด์ํ๊ธฐ ์ฝ์ต๋๋ค.worker_task_retry_count_total{task_type,reason}retry ์ ํ๋์ ๋๋ค.
- analysis ์๋ต schema ๊ฒ์ฆ ์คํจ๋
VALIDATION_ERROR+ non-retryable๋ก ๋ถ๋ฅํฉ๋๋ค. - job posting
classify/generate์ schema ๊ฒ์ฆ ์คํจ๋ fallback์ผ๋ก ์ฒ๋ฆฌํ๊ณ ,llm_request_duration_seconds{outcome="fallback"}๋ก ๊ธฐ๋กํฉ๋๋ค. - fallback๋ ์ฑ๊ณต์ผ๋ก ์์ง ์๊ณ ๋ณ๋ outcome์ผ๋ก ์ ์งํด, success latency์ fallback latency๋ฅผ ๋ถ๋ฆฌํด์ ๋ณผ ์ ์๊ฒ ํ์ต๋๋ค.
- retryable / non-retryable ์๋ฏธ๋ ๊ธฐ์กด ๋น์ฆ๋์ค ๊ณ์ฝ์ ์ ์งํฉ๋๋ค.
RATE_LIMIT,OPENAI_TIMEOUT, ์ผ๋ฐINTERNAL_ERROR, Spring API transport/server error๋ ์ฌ์๋ ๊ฐ๋ฅ- validation/parsing failure์ 4xx ์ฑ๊ฒฉ ์ค๋ฅ๋ ๋น์ฌ์๋
ํ ๋๊ธฐ p95:
histogram_quantile(
0.95,
sum by (le, task_type) (
rate(worker_task_queue_wait_duration_seconds_bucket[5m])
)
)
ํ ๋๊ธฐ p99:
histogram_quantile(
0.99,
sum by (le, task_type) (
rate(worker_task_queue_wait_duration_seconds_bucket[5m])
)
)
์ฒ๋ฆฌ ์๊ฐ p95:
histogram_quantile(
0.95,
sum by (le, task_type, outcome) (
rate(worker_task_processing_duration_seconds_bucket[5m])
)
)
LLM ์์ฒญ p95:
histogram_quantile(
0.95,
sum by (le, task_type, operation, outcome) (
rate(llm_request_duration_seconds_bucket[5m])
)
)
inflight ํ๊ท :
avg_over_time(worker_task_inflight[5m])
inflight ์ต๋:
max_over_time(worker_task_inflight[5m])
์ค์ ์ํ ๋๋น ์ฌ์ฉ๋:
worker_task_inflight / clamp_min(worker_task_concurrency_limit, 1)
retry rate:
sum by (task_type, reason) (
rate(worker_task_retry_count_total[5m])
)
context fetch p95:
histogram_quantile(
0.95,
sum by (le, task_type, endpoint, outcome) (
rate(worker_context_fetch_duration_seconds_bucket[5m])
)
)
callback p95:
histogram_quantile(
0.95,
sum by (le, task_type, endpoint, outcome) (
rate(worker_callback_duration_seconds_bucket[5m])
)
)
python3 -m unittest \
tests/test_prometheus_metrics.py \
tests/test_worker_concurrency.py \
tests/test_recovery_flow.py \
tests/test_observability_logging.pyํ์ฌ ํ ์คํธ๋ ์๋๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
- recovery spool ๊ธฐ๋ฐ ์ฌ์ ์ก๊ณผ ์๋ฃ ์ฝ๋ฐฑ ๋ณต๊ตฌ
- validation error / fallback / retry ๋ฉํธ๋ฆญ ๋ถ๋ฅ
- task type๋ณ inflight / concurrency metric
- observability ๋ก๊ทธ ํ๋ ์ ํฉ์ฑ