통합 호스트 에이전트 매뉴얼
감시할 리눅스 호스트마다 1개씩 실행하는 Go 단일 바이너리 에이전트의 설치·실행 방법과, PostgreSQL·MySQL·Redis·nginx·HTTP 앱을 감시 대상으로 등록하는 방법을 설명합니다.
1.개요
통합 호스트 에이전트(host-agent)는 하나의 프로세스가 두 가지를 함께 수집해
한 번의 POST로 수집 서버(monitor 백엔드)에 전송합니다.
- 호스트 시스템 자원 — CPU/메모리/디스크 사용률, 디스크 I/O 속도, Load Average, 네트워크 송수신 속도
- 서비스 인스턴스 지표 —
services.json에 등록한 PostgreSQL / MySQL / Redis / nginx / HTTP 앱(FastAPI, Django, Node.js, Next.js, Nuxt.js …)
push 단방향 구조입니다. 백엔드가 에이전트 쪽으로 접속하는 일이 없으므로 감시 대상 서버에 인바운드 포트를 열 필요가 없습니다. 수집 서버가 꺼져 있어도 에이전트는 종료되지 않고 페이로드를 메모리 버퍼에 보관했다가 복구되면 원래 수집 시각 그대로 재전송합니다 (6.2절).
주요 특징
| 항목 | 내용 |
|---|---|
| 단일 정적 바이너리 | Go로 빌드된 파일 1개. 대상 서버에 런타임 설치 불필요 |
| 자동 등록 | 호스트·서비스 모두 사전 등록 절차 없음 — 첫 전송 시 백엔드가 자동 등록 |
| 전송 버퍼링 | 수집 서버 장애 시 기본 10분 분량 보관 후 복구 시 재전송 |
| 비밀번호 보호 | services.json 값에 ${ENV_VAR} 치환 지원 — 평문 저장 불필요 |
| HTTPS 인증서 감시 | HTTPS 프로브 시 인증서 만료까지 남은 일수(cert_expiry_days) 자동 수집 |
2.빠른 시작
Go가 설치된 서버라면 5분 안에 시작할 수 있습니다.
# 1) 에이전트 폴더로 이동
cd host_agent
# 2) 환경 설정 작성 — 수집 서버 주소·API 키·호스트명 (4장 참고)
cp .env.example .env
vi .env
# 3) 감시할 서비스 목록 작성 — 없으면 시스템 메트릭만 수집 (5장 참고)
cp services.json.example services.json
vi services.json
# 4) 빌드 + 백그라운드 실행
./start.sh
# 5) 로그 확인 — 시작 배너와 "전송 성공"이 보이면 정상
tail -f host-agent.log
# 중지
./stop.sh
실행 직후 출력되는 시작 배너에서 설정 상태를 즉시 확인할 수 있습니다.
============================================================
Antigravity Unified Host Agent
------------------------------------------------------------
호스트명 : prod-01
IP / OS : 10.0.0.5 / ubuntu 22.04 (5.15.0-91-generic)
수집 서버 (Target) : http://10.0.0.100:8009/api/v1/host/metrics
수집 주기 : 5초
설정된 서비스 : 3개
- postgres payment-db [docker] dsn 설정됨
- redis cache [native] localhost:6379
- fastapi api [native] http://localhost:8000/health
수집 서버 연결 확인 : ✓ 연결 성공 (10.0.0.100:8009)
============================================================
2.1호스트는 어떻게 등록되나
사전 등록 절차가 없습니다. 에이전트가 첫 페이로드를 보내는 순간 백엔드가
hostname 기준으로 자동 등록(upsert)하고, 대시보드에 호스트 카드가 나타납니다.
- 대시보드 표시 이름은
.env의MONITOR_HOSTNAME으로 지정합니다. 미지정 시 OS 호스트명을 사용합니다. - 서비스도
(호스트, type, name)기준으로 자동 등록됩니다 — 5장 참고.
호스트명은 전체 서버에서 유일해야 합니다. 두 서버가 같은
MONITOR_HOSTNAME을 쓰면 대시보드에서 하나의 호스트로 합쳐져 메트릭이 섞입니다.
3.설치와 실행
상황에 따라 네 가지 방법으로 실행할 수 있습니다. 어느 방법이든 준비물은
.env와 services.json 두 파일입니다.
| 방법 | 대상 서버에 필요한 것 | 권장 상황 |
|---|---|---|
| 3.1 start.sh | Go 1.21+ | 개발·테스트, Go가 이미 있는 서버 |
| 3.2 크로스 컴파일 배포 | 없음 (바이너리만 복사) | 운영 서버 표준 배포 |
| 3.3 Docker Compose | Docker | 컨테이너 중심으로 운영하는 서버 |
| 3.4 systemd | 없음 (3.2와 병행) | 부팅 시 자동 시작이 필요한 운영 서버 |
3.1start.sh로 바로 실행
빌드부터 백그라운드 실행까지 한 번에 처리합니다.
cd host_agent
./start.sh
스크립트가 하는 일:
- 이미 실행 중인지 확인 (
host-agent.pid) services.json/.env존재 확인 — 없으면 안내만 출력하고 계속 진행go build -o host-agent ./cmd/host-agent빌드nohup으로 백그라운드 실행, PID 기록, 로그는host-agent.log
tail -f host-agent.log # 로그 확인
./stop.sh # 중지 (정상 종료 5초 대기 후 강제 종료)
3.2크로스 컴파일 — 바이너리만 배포
운영 서버에 Go를 설치할 필요 없이, 빌드 머신(개발 PC 등)에서 리눅스용 바이너리를 만들어 복사합니다.
# 1) 빌드 머신에서 (macOS/Windows/Linux 어디서든)
cd host_agent
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o host-agent ./cmd/host-agent
# ARM 서버(AWS Graviton 등)라면: GOARCH=arm64
# 2) 대상 서버로 배포
ssh user@target-server "mkdir -p /opt/host-agent"
scp host-agent .env services.json stop.sh user@target-server:/opt/host-agent/
# 3) 대상 서버에서 실행
ssh user@target-server
cd /opt/host-agent
chmod +x host-agent
nohup ./host-agent > host-agent.log 2>&1 &
# 4) 동작 확인
tail -f host-agent.log
.env는 에이전트 실행 디렉터리 기준으로 로드됩니다.
반드시 바이너리와 같은 폴더에 두고, 그 폴더에서 실행하거나 systemd의
WorkingDirectory를 지정하세요.
여러 서버에 일괄 배포하려면:
for h in web-01 web-02 db-01; do
ssh $h "mkdir -p /opt/host-agent"
scp host-agent stop.sh $h:/opt/host-agent/
# .env와 services.json은 서버마다 다르므로 서버별 파일을 준비해 복사
scp envs/$h.env $h:/opt/host-agent/.env
scp services/$h.json $h:/opt/host-agent/services.json
done
3.3Docker Compose
에이전트 자체를 컨테이너로 실행합니다. 호스트의 실제 지표를 읽기 위해
/proc·/sys 마운트와 host 네트워크 모드를 사용합니다(compose 파일에 이미 구성됨).
cd host_agent
# services.json 작성 (컨테이너에 read-only 마운트됨)
cp services.json.example services.json
vi services.json
# 환경변수는 셸 또는 같은 폴더의 .env로 전달
MONITOR_SERVER_URL=http://10.0.0.100:8009/api/v1/host/metrics \
MONITOR_API_KEY=secret_monitoring_key \
MONITOR_HOSTNAME=prod-01 \
docker compose up -d --build
docker logs -f host-agent # 로그 확인
docker compose down # 중지
docker-compose.yml이 마운트하는 것들:
| 마운트 | 용도 |
|---|---|
/proc, /sys, /etc (읽기 전용) | 호스트 시스템 메트릭 수집 (gopsutil이 HOST_PROC=/host/proc 경로로 읽음) |
/var/run/docker.sock (읽기 전용) | runtime: "docker" 서비스의 컨테이너 지표 수집 |
./services.json | 감시 서비스 정의 |
services.json에서 ${ENV_VAR} 치환을 쓴다면 해당 변수를 컨테이너
환경으로도 전달해야 합니다 — docker-compose.yml의 environment:에
- PAYMENT_DB_PASS=${PAYMENT_DB_PASS}처럼 추가하세요.
runtime: "native" 서비스의 프로세스 지표(PID/CPU/MEM)는 컨테이너 안에서
호스트 프로세스를 볼 수 없어 수집되지 않을 수 있습니다. native 서비스가 많다면
3.2(바이너리 직접 실행)를 권장합니다.
3.4systemd 서비스 등록
3.2로 배포한 바이너리를 부팅 시 자동 시작·비정상 종료 시 자동 재시작되도록 등록합니다.
# 1) 유닛 파일 작성
sudo tee /etc/systemd/system/host-agent.service > /dev/null <<'EOF'
[Unit]
Description=Antigravity Unified Host Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# .env와 services.json이 이 폴더에 있어야 합니다
WorkingDirectory=/opt/host-agent
ExecStart=/opt/host-agent/host-agent
Restart=always
RestartSec=5
# services.json의 ${ENV_VAR} 치환에 쓸 시크릿은 여기서 주입
# Environment=PAYMENT_DB_PASS=실제비밀번호
# 또는 별도 파일: EnvironmentFile=/opt/host-agent/secrets.env
[Install]
WantedBy=multi-user.target
EOF
# 2) 활성화 및 시작
sudo systemctl daemon-reload
sudo systemctl enable --now host-agent
# 3) 상태·로그 확인
systemctl status host-agent
journalctl -u host-agent -f
# 설정(.env / services.json) 변경 후 반영
sudo systemctl restart host-agent
docker 런타임 서비스를 감시한다면 에이전트 실행 계정이 /var/run/docker.sock을
읽을 수 있어야 합니다. root로 실행하거나, 전용 계정을 docker 그룹에 추가하세요
(User=monitor + usermod -aG docker monitor).
설치 확인 체크리스트
- 로그에 시작 배너가 출력되고
수집 서버 연결 확인 : ✓ 연결 성공인지 - 수집 주기마다
전송 성공 (host=..., services=N, ...)로그가 찍히는지 (MONITOR_SILENT=true면 주기 로그는 생략 — 배너만 확인) - 대시보드(
http://수집서버:8009/)에 호스트 카드가 나타나는지 - 등록한 서비스가 카드 안에 카테고리별(웹서버/캐시/DB)로 보이는지
4.환경변수 (.env)
에이전트는 시작 시 실행 디렉터리의 .env 파일을 읽습니다
(이미 설정된 OS 환경변수가 우선). 모든 항목에 기본값이 있으므로 .env 없이도 동작하지만,
운영에서는 최소한 MONITOR_SERVER_URL / MONITOR_API_KEY /
MONITOR_HOSTNAME 세 가지는 지정하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
MONITOR_SERVER_URL | http://localhost:8009/api/v1/host/metrics |
수집 서버 엔드포인트. 경로까지 포함한 전체 URL |
MONITOR_API_KEY | secret_monitoring_key |
인증 키. 요청 헤더 X-API-Key로 전송되며 백엔드의 MONITORING_API_KEY와 동일해야 함 |
MONITOR_HOSTNAME | (OS 호스트명) | 대시보드 표시 호스트명. 전체 서버에서 유일해야 함 |
SERVICES_CONFIG | ./services.json | 서비스 정의 파일 경로 |
COLLECT_INTERVAL_SEC | 5 |
수집·전송 주기(초). 백엔드 오프라인 판정 임계(기본 15초)보다 짧아야 함 |
MONITOR_SILENT | (없음) | true면 주기적 "전송 성공" 로그를 끔 (시작 배너는 항상 출력) |
SEND_BUFFER_MAX | 120 |
수집 서버 장애 시 메모리에 보관할 페이로드 개수 (기본값 = 5초 주기 기준 10분 분량) |
DOCKER_SOCK | /var/run/docker.sock | Docker 데몬 소켓 경로 (docker 런타임 지표 수집용) |
작성 예시
# /opt/host-agent/.env — 운영 서버 예시
# 수집 서버 (monitor 백엔드 서버 주소:8009 + 고정 경로)
MONITOR_SERVER_URL=http://10.0.0.100:8009/api/v1/host/metrics
# 백엔드와 동일한 키 (운영에서는 반드시 기본값에서 교체)
MONITOR_API_KEY=1c9a7e...강력한_임의_문자열
# 대시보드 표시명 — 서버마다 다르게!
MONITOR_HOSTNAME=prod-web-01
# 수집 주기 (기본 5초면 충분. 15초 이상이면 오프라인으로 오인됨)
COLLECT_INTERVAL_SEC=5
# 운영에서는 로그를 줄이는 것을 권장
MONITOR_SILENT=true
# services.json의 ${ENV_VAR} 치환에 쓰는 시크릿도 여기에 둘 수 있음
PAYMENT_DB_PASS=실제_DB_비밀번호
REDIS_PASS=실제_레디스_비밀번호
자주 하는 실수
| 실수 | 증상 / 해결 |
|---|---|
| URL에 경로 누락 | http://10.0.0.100:8009가 아니라
…:8009/api/v1/host/metrics까지 적어야 합니다 |
| API 키 불일치 | 에이전트 로그에 전송 실패 ... HTTP 401 — 백엔드의 MONITORING_API_KEY와 맞추세요 |
| 호스트명 중복 | 두 서버가 하나의 카드로 합쳐져 메트릭이 섞임 — 서버별로 다른 이름 지정 |
| 주기를 너무 길게 | COLLECT_INTERVAL_SEC=30처럼 15초를 넘기면 매 주기 사이에 오프라인으로 판정 |
5.서비스 등록 (services.json)
이 호스트 위에서 감시할 서비스 인스턴스들을 services.json 파일 하나로 정의합니다.
에이전트가 각 항목에 직접 접속해 지표를 수집하므로, 서비스 쪽에는 아무것도 설치하지 않습니다.
백엔드에는 (호스트, type, name) 기준으로 자동 등록되며 type에 따라
대시보드 카테고리가 정해집니다.
type 값 | 대시보드 분류 | 수집 방식 |
|---|---|---|
postgres, mysql | DB | DSN으로 접속해 통계 쿼리 |
redis | 캐시 | TCP로 INFO 명령 |
nginx | 웹서버 | stub_status HTTP 조회 |
fastapi, django, nodejs, nextjs, nuxtjs | 웹서버 | HTTP GET 프로브 (지연·상태코드) |
그 외 임의 문자열 (springboot 등) | 기타 |
5.1파일 구조와 공통 필드
파일 전체는 JSON 배열이며, 배열 원소 1개가 서비스 인스턴스 1개입니다.
같은 종류의 서비스를 여러 개 감시하려면(예: postgres 2개) name만 다르게 해서
항목을 추가하면 됩니다.
[
{ /* 서비스 1 */ },
{ /* 서비스 2 */ },
...
]
| 필드 | 필수 | 대상 type | 설명 |
|---|---|---|---|
type | 필수 | 전체 | 서비스 종류 (위 표 참고) |
name | 필수 | 전체 | 인스턴스 식별 라벨. 같은 호스트 안에서 (type, name) 조합이 유일해야 함 |
runtime | 선택 | 전체 | native(기본) 또는 docker — 5.2절 |
container | 선택 | docker 런타임 | Docker 컨테이너 이름 |
db | 필수 | postgres, mysql | 접속 정보 객체 — host/port/user/password/dbname(/sslmode) |
dsn | 선택 | postgres, mysql | 접속 문자열 (레거시 — db 미지정 시에만 사용) |
addr | 선택 | redis | host:port (기본 localhost:6379) |
password | 선택 | redis | Redis 비밀번호 (requirepass 설정 시) |
url | 필수 | HTTP 앱 | 프로브 대상 URL (헬스 체크 엔드포인트 권장) |
status_url | 필수 | nginx | stub_status URL |
port | 선택 | native 런타임 | 리슨 포트 — 프로세스 CPU/MEM 수집용 PID 탐색 |
process | 선택 | native 런타임 | 프로세스명 — port로 못 찾을 때 대안 |
5.2실행 방식: native / docker
runtime은 서비스 지표에 리소스 지표를 덧붙이는 방식을 결정합니다.
runtime: "native" (직접 설치형, 기본값)
port(리슨 포트) 또는 process(프로세스명)로 PID를 찾아
해당 프로세스의 CPU%·메모리를 수집합니다. 둘 다 없거나 못 찾으면 리소스 지표 없이 서비스 지표만 수집합니다.
{
"type": "redis", "name": "cache",
"runtime": "native",
"addr": "localhost:6379",
"port": 6379 ← 6379 포트를 리슨 중인 프로세스의 CPU/MEM 수집
}
runtime: "docker" (컨테이너형)
container에 지정한 컨테이너의 상태·CPU%·메모리·재시작 횟수·헬스체크 결과를
Docker 소켓으로 수집합니다. 이미지 이름도 자동 수집되어 대시보드에 표시됩니다.
# 컨테이너 이름 먼저 확인
docker ps --format '{{.Names}}'
{
"type": "postgres", "name": "payment-db",
"runtime": "docker",
"container": "payment-pg", ← docker ps에 나오는 이름 그대로
"db": { "host": "localhost", "port": 5432, "user": "monitor",
"password": "${PG_MONITOR_PASS}", "dbname": "payment" }
}
컨테이너가 running이 아니면(exited 등) 그 상태가 서비스 상태를
덮어써서 대시보드에 위험으로 표시됩니다. 헬스체크가 unhealthy여도 위험으로 판정됩니다.
5.3비밀번호는 환경변수로
services.json의 모든 문자열 값에서 ${VAR} 형식이
환경변수로 치환됩니다. DSN·비밀번호를 파일에 평문으로 두지 마세요.
# .env (또는 systemd Environment=)
PG_MONITOR_PASS=실제비밀번호
// services.json — 파일은 git에 넣어도 안전
{ "db": { "user": "monitor", "password": "${PG_MONITOR_PASS}", "dbname": "payment" } }
- 치환은
${VAR}형식만 지원합니다 ($VAR는 건드리지 않으므로 비밀번호에$가 있어도 안전) - 정의되지 않은 변수는 원문
${VAR}그대로 남습니다 — 접속 실패 로그에 원문이 보이면 변수 설정 누락을 의심하세요
5.4PostgreSQL 등록 DB
1단계 — 모니터링 전용 계정 생성 (권장)
슈퍼유저 계정을 쓰지 말고, 통계 열람 권한만 있는 전용 계정을 만드세요. 대상 DB에서 1회 실행:
-- psql로 접속해 실행
CREATE USER monitor WITH PASSWORD '강력한비밀번호';
GRANT pg_monitor TO monitor; -- 통계 뷰 열람 롤 (PostgreSQL 10+)
GRANT CONNECT ON DATABASE payment TO monitor; -- 감시할 DB 접속 허용
2단계 — services.json 항목 작성
접속 정보는 db 객체에 항목별로 적습니다.
DSN 문자열을 직접 조립하지 않으므로 형식 오류가 없고, 비밀번호에 특수문자가 있어도
에이전트가 알아서 이스케이프합니다.
{
"type": "postgres",
"name": "payment-db",
"runtime": "docker",
"container": "payment-pg",
"db": {
"host": "localhost", ← 생략 시 localhost
"port": 5432, ← 생략 시 5432
"user": "monitor",
"password": "${PG_MONITOR_PASS}",
"dbname": "payment", ← 생략 시 postgres
"sslmode": "disable" ← 생략 시 disable
}
}
DB가 컨테이너라도 포트가 호스트에 노출돼 있으면 host: "localhost"로 접속합니다.
지표(연결 수·캐시 히트율 등)는 dbname에 지정한 DB 기준으로 수집되므로
DB별로 감시하려면 항목을 나눠 등록하세요.
기존 dsn 문자열("dsn": "postgres://user:pass@host:5432/db?sslmode=disable")도
계속 지원합니다 — db가 없을 때만 사용됩니다.
3단계 — 실행/재시작 후 확인
./stop.sh && ./start.sh # 또는 sudo systemctl restart host-agent
tail -f host-agent.log # 시작 배너에 "postgres payment-db [docker] dsn 설정됨" 확인
수집 지표
| 지표 | 의미 |
|---|---|
total_connections / active_connections | 전체/활성 세션 수 (pg_stat_activity) |
db_size_bytes | DB 물리 크기 |
locks_count | 현재 락 수 (pg_locks) |
cache_hit_ratio / index_hit_ratio | 힙/인덱스 캐시 히트율(%) |
deadlocks / temp_bytes | 누적 데드락 횟수 / 임시 파일 사용량 |
dead_tuples / live_tuples | 죽은/살아있는 튜플 수 (VACUUM 필요성 판단) |
5.5MySQL 등록 DB
1단계 — 모니터링 전용 계정 생성 (권장)
CREATE USER 'monitor'@'%' IDENTIFIED BY '강력한비밀번호';
GRANT PROCESS ON *.* TO 'monitor'@'%'; -- 상태 조회용 최소 권한
FLUSH PRIVILEGES;
2단계 — services.json 항목 작성
PostgreSQL과 마찬가지로 db 객체에 항목별로 적습니다.
(host 생략 시 localhost, port 생략 시 3306, dbname은 생략 가능 —
서버 전역 상태만 수집하므로 특정 DB 지정이 필수는 아닙니다)
{
"type": "mysql",
"name": "orders-db",
"runtime": "native",
"port": 3306,
"db": {
"host": "localhost",
"port": 3306,
"user": "monitor",
"password": "${MYSQL_MONITOR_PASS}",
"dbname": "orders"
}
}
// docker로 도는 MySQL이라면
{
"type": "mysql",
"name": "shop-db",
"runtime": "docker",
"container": "shop-mysql",
"db": { "port": 3307, "user": "monitor", "password": "${MYSQL_MONITOR_PASS}" }
}
레거시 dsn 문자열도 지원합니다(Go 드라이버 형식:
사용자:비밀번호@tcp(호스트:포트)/DB명) — db가 없을 때만 사용됩니다.
수집 지표
| 지표 | 의미 |
|---|---|
threads_connected / threads_running | 연결/실행 중 스레드 수 |
max_connections | 최대 연결 설정값 (사용률 판정에 사용) |
questions / slow_queries | 누적 쿼리 수 / 슬로우 쿼리 수 |
aborted_connects / uptime | 실패한 접속 시도 / 가동 시간(초) |
5.6Redis 등록 캐시
별도 계정이 필요 없습니다. requirepass가 설정된 경우에만 password를 지정하세요.
// 비밀번호 없는 기본 구성
{
"type": "redis",
"name": "cache",
"runtime": "native",
"addr": "localhost:6379",
"port": 6379
}
// requirepass가 걸린 docker Redis
{
"type": "redis",
"name": "session-store",
"runtime": "docker",
"container": "session-redis",
"addr": "localhost:6380",
"password": "${REDIS_PASS}"
}
addr는 host:port 외에 host:port/0,
redis://host:port/0 형식도 허용합니다(접속에는 host:port만 사용).
수집 지표
| 지표 | 의미 |
|---|---|
used_memory / maxmemory | 사용/최대 메모리 |
connected_clients | 연결된 클라이언트 수 |
hit_rate (keyspace_hits/misses) | 캐시 히트율(%) |
evicted_keys | 메모리 부족으로 축출된 키 수 |
ops_per_sec / uptime / mem_frag_ratio | 초당 명령 수 / 가동 시간 / 메모리 단편화율 |
5.7nginx 등록 웹서버
1단계 — stub_status 활성화
nginx 설정에 상태 페이지를 추가합니다. 외부 노출을 막기 위해 로컬에서만 접근을 허용하세요.
# /etc/nginx/conf.d/status.conf
server {
listen 127.0.0.1:8080;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
sudo nginx -t && sudo systemctl reload nginx
# 동작 확인 — 아래처럼 나오면 성공
curl http://127.0.0.1:8080/nginx_status
# Active connections: 2
# server accepts handled requests
# 1024 1024 4096
# Reading: 0 Writing: 1 Waiting: 1
2단계 — services.json 항목 작성
{
"type": "nginx",
"name": "edge",
"runtime": "native",
"status_url": "http://127.0.0.1:8080/nginx_status",
"port": 80
}
수집 지표
| 지표 | 의미 |
|---|---|
active / reading / writing / waiting | 연결 상태별 수 |
accepts / handled / requests | 누적 수락/처리 연결 수, 누적 요청 수 |
latency_ms / status_code | 상태 페이지 응답 지연·코드 |
5.8HTTP 앱 등록 — FastAPI · Django · Node.js · Next.js 등 웹서버
위 네 타입에 해당하지 않는 모든 type은 HTTP 프로브로 처리됩니다.
에이전트가 url에 GET 요청을 보내 응답 지연·상태 코드를 수집합니다.
가능하면 무거운 페이지 대신 가벼운 헬스 체크 엔드포인트(/health,
/healthz 등)를 등록하세요. 5초마다 호출됩니다.
엔드포인트를 직접 만드는 방법은 5.9절 헬스체크 API 작성 가이드를 참고하세요.
// FastAPI (native, 8000 포트 프로세스 리소스도 수집)
{
"type": "fastapi",
"name": "api",
"runtime": "native",
"url": "http://localhost:8000/health",
"port": 8000
}
// Next.js (docker)
{
"type": "nextjs",
"name": "web-front",
"runtime": "docker",
"container": "web-front",
"url": "http://localhost:3000/"
}
// HTTPS 도메인 프로브 — 인증서 만료일(cert_expiry_days)도 자동 수집
{
"type": "nodejs",
"name": "public-api",
"runtime": "native",
"url": "https://api.example.com/health",
"process": "node"
}
// 목록에 없는 종류도 type 자유 지정 가능 (대시보드 분류만 '기타'가 됨)
{
"type": "springboot",
"name": "batch",
"runtime": "native",
"url": "http://localhost:8081/actuator/health",
"port": 8081
}
수집 지표
| 지표 | 의미 |
|---|---|
latency_ms | 응답 지연(ms) |
status_code / ok | HTTP 상태 코드 / 2xx·3xx 여부 |
cert_expiry_days | HTTPS일 때 인증서 만료까지 남은 일수 (만료 시 음수) |
5.9헬스체크 API 작성 가이드
5.8절의 url에 등록할 /health 엔드포인트를 각 애플리케이션에
직접 만들 때의 가이드입니다. 핵심 원칙은 하나 —
"프로세스가 살아 있는가"가 아니라 "실제로 일을 할 수 있는가"를 검사합니다.
프로세스 생존·CPU·메모리는 에이전트가 이미 수집하므로 중복 검사할 필요가 없고,
루트 페이지(/)는 내부(DB 연결 등)가 고장 나도 200을 줄 수 있어 헬스체크로 부적합합니다.
검사해야 할 것 — 직접 의존성만
| 검사 | 방법 | 실패의 의미 |
|---|---|---|
| DB 연결 | 커넥션 풀에서 SELECT 1 | 핵심 기능 전부 불가 |
| Redis/캐시 | PING | 세션·캐시 의존 기능 불가 |
| 필수 외부 API 선택 | 가벼운 엔드포인트 GET | 해당 연동 기능 불가 |
| 디스크 쓰기 선택 | 임시 파일 1바이트 쓰기 (파일 처리가 핵심일 때만) | 쓰기 작업 불가 |
설계 규칙 4가지
- 검사마다 짧은 타임아웃(1~2초) — 에이전트는 8초 타임아웃으로 5초마다 호출합니다. 타임아웃 없이 멈춘 DB를 기다리면 헬스체크 자체가 응답하지 못해 down(응답 없음)으로 보입니다. 타임아웃을 걸면 "500 + 어떤 검사가 실패했는지"를 답할 수 있습니다.
- 검사는 병렬로 실행 — DB 2초 + Redis 2초를 순차로 하면 4초, 병렬이면 2초.
- 부작용 없이, 가볍게 — 5초마다 호출되므로 무거운 쿼리 금지.
SELECT 1이면 충분하고SELECT count(*) FROM orders같은 건 금물입니다. - 다른 서비스의 /health를 연쇄 호출하지 말 것 — 장애가 연쇄 증폭됩니다. 내 앱이 직접 쥔 연결(내 커넥션 풀, 내 Redis 클라이언트)만 검사합니다.
FastAPI 예시
import asyncio
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
CHECK_TIMEOUT = 2.0 # 검사당 제한 시간(초) — 에이전트 타임아웃(8초)보다 충분히 짧게
async def check_db():
async with db_pool.acquire() as conn: # 앱이 실제 쓰는 커넥션 풀 재사용
await conn.execute("SELECT 1")
async def check_redis():
await redis_client.ping() # 앱이 실제 쓰는 클라이언트 재사용
async def _run(name, coro):
try:
await asyncio.wait_for(coro, timeout=CHECK_TIMEOUT)
return name, "ok"
except asyncio.TimeoutError:
return name, f"timeout ({CHECK_TIMEOUT}s)"
except Exception as e:
return name, f"error: {type(e).__name__}" # 상세 내용은 로그로, 응답엔 최소한만
@app.get("/health")
async def health():
results = dict(await asyncio.gather( # 병렬 실행
_run("db", check_db()),
_run("redis", check_redis()),
))
healthy = all(v == "ok" for v in results.values())
return JSONResponse(
status_code=200 if healthy else 500, # 모니터가 보는 것은 이 상태 코드
content={"status": "ok" if healthy else "fail", "checks": results},
)
문제가 있을 때의 응답 — 담당자가 URL을 직접 열면 왜 500인지 바로 보입니다:
# HTTP 500
{"status": "fail", "checks": {"db": "timeout (2.0s)", "redis": "ok"}}
모니터링과 이렇게 연결됩니다
마지막으로 services.json의 url을 이 엔드포인트로 바꾸면 됩니다 —
예: "url": "http://localhost:8000/health".
필수/선택 의존성 구분 — 외부 PG사 API처럼 일부 기능만 막는 의존성은 실패해도 200을 유지하고 응답 본문에만 표시하는 방식을 권장합니다. 500은 "이 서버로 트래픽을 보내면 안 된다" 수준일 때만 반환해야 알림 피로가 없습니다.
보안 — 인증 없이 노출되는 경로이므로 응답에 DSN·내부 호스트명·버전 정보를
넣지 마세요. 에이전트는 같은 호스트에서 호출하므로, 가능하면 리버스 프록시에서
/health를 localhost/내부망만 허용하는 것이 안전합니다.
5.10전체 예시와 설정 적용
웹 서버 한 대에서 nginx + Next.js + FastAPI + Redis + PostgreSQL을 모두 감시하는 예시입니다.
// /opt/host-agent/services.json
[
{
"type": "nginx",
"name": "edge",
"runtime": "native",
"status_url": "http://127.0.0.1:8080/nginx_status",
"port": 80
},
{
"type": "nextjs",
"name": "web",
"runtime": "native",
"url": "http://localhost:3000/",
"port": 3000
},
{
"type": "fastapi",
"name": "api",
"runtime": "docker",
"container": "api-server",
"url": "http://localhost:8000/health"
},
{
"type": "redis",
"name": "cache",
"runtime": "docker",
"container": "cache-redis",
"addr": "localhost:6379",
"password": "${REDIS_PASS}"
},
{
"type": "postgres",
"name": "main-db",
"runtime": "docker",
"container": "main-pg",
"db": {
"host": "localhost",
"port": 5432,
"user": "monitor",
"password": "${PG_MONITOR_PASS}",
"dbname": "app"
}
}
]
설정 적용 — services.json을 수정한 뒤에는 에이전트를 재시작해야 반영됩니다.
./stop.sh && ./start.sh # start.sh 방식
sudo systemctl restart host-agent # systemd 방식
docker compose restart # Docker Compose 방식
재시작 후 시작 배너의 설정된 서비스 목록과 대시보드 호스트 카드에서
서비스들이 카테고리별로 보이는지 확인하세요. JSON 문법 오류가 있으면 배너 위에
services.json 로드 실패가 출력되고 시스템 메트릭만 수집합니다.
6.운영
6.1로그와 상태 확인
# start.sh 방식
tail -f host-agent.log
cat host-agent.pid && ps -p $(cat host-agent.pid)
# systemd 방식
systemctl status host-agent
journalctl -u host-agent -f
# Docker Compose 방식
docker logs -f host-agent
정상 동작 시 주기마다 다음 로그가 출력됩니다
(MONITOR_SILENT=true면 생략):
2026/07/07 14:00:05 전송 성공 (host=prod-01, services=5, CPU 12.3%, MEM 56.8%)
6.2전송 버퍼링 — 수집 서버 장애 대비
수집 서버가 응답하지 않으면 페이로드를 메모리 FIFO 버퍼에 보관합니다
(기본 SEND_BUFFER_MAX=120개 = 5초 주기 기준 10분 분량, 초과 시 오래된 것부터 폐기).
서버가 복구되면 오래된 것부터 순서대로, 원래 수집 시각을 유지한 채 재전송하므로
대시보드 그래프에 공백이 생기지 않습니다.
# 장애 중
전송 실패 (37건 보관, 복구 시 재전송): Post "...": connection refused
# 복구 직후
밀린 페이로드 38건 재전송 완료
전송 성공 (host=prod-01, services=5, CPU 11.4%, MEM 56.8%)
버퍼는 메모리에만 존재합니다. 에이전트 자체가 재시작되면 보관분은 사라집니다.
더 긴 장애에 대비하려면 SEND_BUFFER_MAX를 늘리세요 (페이로드당 수 KB 수준).
6.3상태 레벨과 임계값
백엔드는 수집된 지표로 ok(정상) · warn(주의) · crit(위험) · offline(오프라인)을 판정하고, 상태가 바뀌는 순간에만 텔레그램 알림을 보냅니다.
| 대상 | 주의(warn) | 위험(crit) |
|---|---|---|
| 호스트 CPU/MEM/DISK | ≥ 75% | ≥ 90% |
| 서비스 status | degraded, restarting, unhealthy | down, exited, dead |
| 컨테이너 | 재시작 ≥ 5회, 메모리 ≥ 75% | 정지 상태, 메모리 ≥ 90%, 헬스체크 unhealthy |
| postgres 캐시 히트율 | < 95% | < 90% |
| postgres 데드락 | > 0 | ≥ 5 |
| postgres 죽은 튜플 비율 | ≥ 10% | ≥ 25% |
| postgres/mysql 연결 사용률 | ≥ 80% | ≥ 95% |
| redis 히트율 | < 95% | < 90% |
| HTTP 앱 상태 코드 | 4xx | 5xx |
| HTTP 앱 응답 지연 | ≥ 1,000ms | ≥ 3,000ms |
| 호스트 오프라인 | 마지막 수신 후 15초(OFFLINE_THRESHOLD_SEC) 초과 | |
6.4트러블슈팅
| 증상 | 확인 사항 |
|---|---|
| 대시보드에 호스트가 안 보임 | 에이전트 로그에 전송 성공이 찍히는지 → 안 찍히면 MONITOR_SERVER_URL(경로 포함)·방화벽(8009)·백엔드 기동 여부 확인 |
전송 실패 ... HTTP 401 |
에이전트 MONITOR_API_KEY와 백엔드 MONITORING_API_KEY 불일치 |
| 호스트가 자꾸 Offline으로 표시 | COLLECT_INTERVAL_SEC이 15초 이상인지, 에이전트 프로세스가 살아 있는지 확인 |
서비스가 down + db(권장) 또는 dsn 미설정/url 미설정 |
해당 type의 필수 필드 누락 — 5.1절 필드 표 확인 |
DB가 down + 인증 오류 |
비밀번호에 ${VAR} 원문이 남아 있으면 환경변수 미설정. 같은 접속 정보를 psql/mysql CLI로 직접 검증 |
redis가 down + INFO 응답 파싱 실패 |
requirepass가 걸려 있는데 password 미지정(또는 오타)일 가능성 |
nginx가 down |
curl [status_url]이 서버 안에서 되는지 → 안 되면 stub_status 설정(5.7절) 재확인 |
| 컨테이너 지표가 안 나옴 | container 이름이 docker ps와 일치하는지, 에이전트가 /var/run/docker.sock을 읽을 수 있는지 |
| native 프로세스 CPU/MEM이 안 나옴 | port가 실제 리슨 포트인지 (ss -lntp | grep 포트), 다른 계정 프로세스는 권한 부족일 수 있음 |
services.json 로드 실패 |
JSON 문법 오류 — python3 -m json.tool services.json으로 검증 |