통합 호스트 에이전트Antigravity Server Monitor · 설치와 서버 등록 매뉴얼
Monitor 백엔드 매뉴얼
Antigravity Server Monitor › 통합 호스트 에이전트

통합 호스트 에이전트 매뉴얼

감시할 리눅스 호스트마다 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 …)
┌─ 감시 대상 호스트 ──────────────────────────────┐ │ host-agent (기본 5초 주기) │ │ ├─ 시스템 자원 수집 (gopsutil) │ │ ├─ services.json의 각 서비스에 직접 접속 │ │ │ ├─ postgres → SQL 통계 쿼리 │ │ │ ├─ mysql → SHOW GLOBAL STATUS │ │ │ ├─ redis → INFO 명령 │ │ │ ├─ nginx → stub_status HTTP │ │ │ └─ HTTP 앱 → GET 프로브(지연·상태코드) │ │ └─ 실행 방식 오버레이 │ │ ├─ docker → 컨테이너 CPU/MEM/상태/재시작 │ │ └─ native → 프로세스 CPU/MEM │ └──────────────┬──────────────────────────────────┘ │ POST /api/v1/host/metrics (X-API-Key) ▼ monitor 백엔드 → 대시보드 · 텔레그램 알림

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)하고, 대시보드에 호스트 카드가 나타납니다.

  • 대시보드 표시 이름은 .envMONITOR_HOSTNAME으로 지정합니다. 미지정 시 OS 호스트명을 사용합니다.
  • 서비스도 (호스트, type, name) 기준으로 자동 등록됩니다 — 5장 참고.
!

호스트명은 전체 서버에서 유일해야 합니다. 두 서버가 같은 MONITOR_HOSTNAME을 쓰면 대시보드에서 하나의 호스트로 합쳐져 메트릭이 섞입니다.

3.설치와 실행

상황에 따라 네 가지 방법으로 실행할 수 있습니다. 어느 방법이든 준비물은 .envservices.json 두 파일입니다.

방법대상 서버에 필요한 것권장 상황
3.1 start.shGo 1.21+개발·테스트, Go가 이미 있는 서버
3.2 크로스 컴파일 배포없음 (바이너리만 복사)운영 서버 표준 배포
3.3 Docker ComposeDocker컨테이너 중심으로 운영하는 서버
3.4 systemd없음 (3.2와 병행)부팅 시 자동 시작이 필요한 운영 서버

3.1start.sh로 바로 실행

빌드부터 백그라운드 실행까지 한 번에 처리합니다.

cd host_agent
./start.sh

스크립트가 하는 일:

  1. 이미 실행 중인지 확인 (host-agent.pid)
  2. services.json / .env 존재 확인 — 없으면 안내만 출력하고 계속 진행
  3. go build -o host-agent ./cmd/host-agent 빌드
  4. 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
i

.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.ymlenvironment:- 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
i

docker 런타임 서비스를 감시한다면 에이전트 실행 계정이 /var/run/docker.sock을 읽을 수 있어야 합니다. root로 실행하거나, 전용 계정을 docker 그룹에 추가하세요 (User=monitor + usermod -aG docker monitor).

설치 확인 체크리스트

  1. 로그에 시작 배너가 출력되고 수집 서버 연결 확인 : ✓ 연결 성공인지
  2. 수집 주기마다 전송 성공 (host=..., services=N, ...) 로그가 찍히는지 (MONITOR_SILENT=true면 주기 로그는 생략 — 배너만 확인)
  3. 대시보드(http://수집서버:8009/)에 호스트 카드가 나타나는지
  4. 등록한 서비스가 카드 안에 카테고리별(웹서버/캐시/DB)로 보이는지

4.환경변수 (.env)

에이전트는 시작 시 실행 디렉터리의 .env 파일을 읽습니다 (이미 설정된 OS 환경변수가 우선). 모든 항목에 기본값이 있으므로 .env 없이도 동작하지만, 운영에서는 최소한 MONITOR_SERVER_URL / MONITOR_API_KEY / MONITOR_HOSTNAME 세 가지는 지정하세요.

변수기본값설명
MONITOR_SERVER_URLhttp://localhost:8009/api/v1/host/metrics 수집 서버 엔드포인트. 경로까지 포함한 전체 URL
MONITOR_API_KEYsecret_monitoring_key 인증 키. 요청 헤더 X-API-Key로 전송되며 백엔드의 MONITORING_API_KEY와 동일해야 함
MONITOR_HOSTNAME(OS 호스트명) 대시보드 표시 호스트명. 전체 서버에서 유일해야 함
SERVICES_CONFIG./services.json서비스 정의 파일 경로
COLLECT_INTERVAL_SEC5 수집·전송 주기(초). 백엔드 오프라인 판정 임계(기본 15초)보다 짧아야 함
MONITOR_SILENT(없음)true면 주기적 "전송 성공" 로그를 끔 (시작 배너는 항상 출력)
SEND_BUFFER_MAX120 수집 서버 장애 시 메모리에 보관할 페이로드 개수 (기본값 = 5초 주기 기준 10분 분량)
DOCKER_SOCK/var/run/docker.sockDocker 데몬 소켓 경로 (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, mysqlDBDSN으로 접속해 통계 쿼리
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(기본) 또는 docker5.2절
container선택docker 런타임Docker 컨테이너 이름
db필수postgres, mysql접속 정보 객체 — host/port/user/password/dbname(/sslmode)
dsn선택postgres, mysql접속 문자열 (레거시 — db 미지정 시에만 사용)
addr선택redishost:port (기본 localhost:6379)
password선택redisRedis 비밀번호 (requirepass 설정 시)
url필수HTTP 앱프로브 대상 URL (헬스 체크 엔드포인트 권장)
status_url필수nginxstub_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" }
}
i

컨테이너가 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
  }
}
i

DB가 컨테이너라도 포트가 호스트에 노출돼 있으면 host: "localhost"로 접속합니다. 지표(연결 수·캐시 히트율 등)는 dbname에 지정한 DB 기준으로 수집되므로 DB별로 감시하려면 항목을 나눠 등록하세요.

i

기존 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_bytesDB 물리 크기
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}" }
}
i

레거시 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}"
}

addrhost: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 요청을 보내 응답 지연·상태 코드를 수집합니다.

i

가능하면 무거운 페이지 대신 가벼운 헬스 체크 엔드포인트(/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 / okHTTP 상태 코드 / 2xx·3xx 여부
cert_expiry_daysHTTPS일 때 인증서 만료까지 남은 일수 (만료 시 음수)

5.9헬스체크 API 작성 가이드

5.8절의 url에 등록할 /health 엔드포인트를 각 애플리케이션에 직접 만들 때의 가이드입니다. 핵심 원칙은 하나 — "프로세스가 살아 있는가"가 아니라 "실제로 일을 할 수 있는가"를 검사합니다. 프로세스 생존·CPU·메모리는 에이전트가 이미 수집하므로 중복 검사할 필요가 없고, 루트 페이지(/)는 내부(DB 연결 등)가 고장 나도 200을 줄 수 있어 헬스체크로 부적합합니다.

검사해야 할 것 — 직접 의존성만

검사방법실패의 의미
DB 연결커넥션 풀에서 SELECT 1핵심 기능 전부 불가
Redis/캐시PING세션·캐시 의존 기능 불가
필수 외부 API 선택가벼운 엔드포인트 GET해당 연동 기능 불가
디스크 쓰기 선택임시 파일 1바이트 쓰기 (파일 처리가 핵심일 때만)쓰기 작업 불가

설계 규칙 4가지

  1. 검사마다 짧은 타임아웃(1~2초) — 에이전트는 8초 타임아웃으로 5초마다 호출합니다. 타임아웃 없이 멈춘 DB를 기다리면 헬스체크 자체가 응답하지 못해 down(응답 없음)으로 보입니다. 타임아웃을 걸면 "500 + 어떤 검사가 실패했는지"를 답할 수 있습니다.
  2. 검사는 병렬로 실행 — DB 2초 + Redis 2초를 순차로 하면 4초, 병렬이면 2초.
  3. 부작용 없이, 가볍게 — 5초마다 호출되므로 무거운 쿼리 금지. SELECT 1이면 충분하고 SELECT count(*) FROM orders 같은 건 금물입니다.
  4. 다른 서비스의 /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"}}

모니터링과 이렇게 연결됩니다

/health가 500 응답 → 에이전트: status "degraded" + status_code 500 전송 → Monitor: crit(위험) 판정 → 호스트 카드 배지 "🔴 api · HTTP 500 · N분째" + 텔레그램 알림 + 최근 알림 타임라인

마지막으로 services.jsonurl을 이 엔드포인트로 바꾸면 됩니다 — 예: "url": "http://localhost:8000/health".

i

필수/선택 의존성 구분 — 외부 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%)
i

버퍼는 메모리에만 존재합니다. 에이전트 자체가 재시작되면 보관분은 사라집니다. 더 긴 장애에 대비하려면 SEND_BUFFER_MAX를 늘리세요 (페이로드당 수 KB 수준).

6.3상태 레벨과 임계값

백엔드는 수집된 지표로 ok(정상) · warn(주의) · crit(위험) · offline(오프라인)을 판정하고, 상태가 바뀌는 순간에만 텔레그램 알림을 보냅니다.

대상주의(warn)위험(crit)
호스트 CPU/MEM/DISK≥ 75%≥ 90%
서비스 statusdegraded, restarting, unhealthydown, exited, dead
컨테이너재시작 ≥ 5회, 메모리 ≥ 75%정지 상태, 메모리 ≥ 90%, 헬스체크 unhealthy
postgres 캐시 히트율< 95%< 90%
postgres 데드락> 0≥ 5
postgres 죽은 튜플 비율≥ 10%≥ 25%
postgres/mysql 연결 사용률≥ 80%≥ 95%
redis 히트율< 95%< 90%
HTTP 앱 상태 코드4xx5xx
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으로 검증