# Ping URL: báo động khi việc định kỳ im lặng

> Mỗi monitor có một URL bí mật; job gọi nó khi chạy xong, thêm /fail khi lỗi. Belvador báo khi hết tín hiệu — kể cả khi app, database hay node của bạn đã sập.

**Monitor ping URL** (heartbeat, hay "công tắc người chết") canh những việc phải xảy ra đều đặn — cron job, bộ hẹn giờ gọi vào app mỗi phút, script backup, lịch CI. Mỗi lần chạy, job gọi một URL HTTPS bí mật. Khi các lần gọi dừng lại, Belvador báo **"không nhận được tín hiệu"**.

Không cần SDK, không cần header: thứ gì gửi được request HTTPS đều dùng được — `curl`, `crontab`, Python, Node.js, PHP, Go, GitHub Actions.

## Vì sao vẫn chạy khi hệ thống của bạn sập

App không thể tự báo rằng nó đã ngừng. Vì vậy hạn chờ của từng monitor ping URL do Belvador giữ ở edge — không phải app của bạn, cũng không phải node của bạn:

- Nếu app, database hay bộ hẹn giờ chết, ping dừng và Belvador báo đúng hạn.
- Nếu node Belvador của bạn mất kết nối, monitor ping URL vẫn được canh và vẫn báo động. Chỉ danh sách chi tiết các lần ping gần đây phải chờ node quay lại.

Để làm được vậy, Belvador giữ một ít **metadata về tín hiệu sống** bên ngoài node: id ẩn danh của monitor, tín hiệu nào tới (`ok`, `start`, `fail`, `log`, mã thoát) và lúc nào, các hạn chờ và trạng thái sự cố. Đây là ngoại lệ duy nhất của nguyên tắc "dữ liệu event nằm trên node của bạn" và được ghi ở [trang quyền riêng tư](https://belvador.dev/vi/docs/privacy). Nó không bao giờ chứa body request, header, địa chỉ IP máy chủ của bạn hay bất cứ gì job in ra — output bạn chủ động gửi kèm chỉ đi về node của bạn ([xem bên dưới](#gui-kem-output-cua-job)).

## Tạo monitor

Trong một project, mở **Monitor → Tạo monitor** và chọn **Ping URL (canh định kỳ)**.

| Trường | Ý nghĩa |
|---|---|
| **Tên** và **slug** | Cách monitor hiện trong Belvador và trong cảnh báo |
| **Lịch** | **Mỗi N phút/giờ** (ví dụ mỗi 1 phút), hoặc **lịch cron** kèm múi giờ (`0 2 * * *` = 02:00 mỗi ngày) |
| **Cho phép trễ** | Sau thời điểm đáng lẽ có ping, Belvador chờ bao lâu rồi mới báo — mặc định 5 phút, tối đa 1440 |
| **Thời gian chạy tối đa** | Chỉ dùng khi job gọi thêm `/start`: một lượt mở quá lâu tính là quá giờ |
| **Môi trường** | Một ping URL báo cho một môi trường (mặc định `production`). Tạo monitor thứ hai cho staging |

Sau khi lưu, Belvador hiện **ping URL một lần duy nhất**. Hãy chép vào secret của máy chủ hoặc CI. Belvador chỉ giữ dấu vân tay của URL nên không hiện lại được — nếu làm mất, hãy xoay URL (xem bên dưới).

Monitor hiện **Đang chờ ping đầu tiên** cho tới khi có lần gọi đầu; trước đó không đánh giá gì.

## Gọi URL

```text
https://ping.belvador.dev/p/bvp_<your-ping-token>             chạy xong, ổn
https://ping.belvador.dev/p/bvp_<your-ping-token>/start       bắt đầu (tuỳ chọn; bật kiểm tra thời gian chạy tối đa)
https://ping.belvador.dev/p/bvp_<your-ping-token>/fail        lỗi — báo ngay
https://ping.belvador.dev/p/bvp_<your-ping-token>/<0-255>     mã thoát: 0 = ổn, 1–255 = lỗi
https://ping.belvador.dev/p/bvp_<your-ping-token>/log         gửi kèm output hoặc ghi chú; không đổi trạng thái
```

Gọi `GET` hay `POST` đều được. Có `/` ở cuối cũng được.

### curl

```bash
# Alive / finished OK
curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token> || true

# Started (optional: arms the max-runtime check)
curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/start || true

# Failed — alerts at once
curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/fail || true

# Or send the job's exit status right after it: 0 = success, 1–255 = failure
./nightly-job.sh; curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/$? || true
```

### crontab

```bash
# Runs at 02:00; Belvador gets "started", then the job's exit status.
0 2 * * * curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/start || true ; /usr/local/bin/nightly-job.sh ; curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/$? || true
```

### Node.js, Bun hoặc Deno

```ts
const PING_URL = process.env.BELVADOR_PING_URL!; // https://ping.belvador.dev/p/bvp_<your-ping-token>

/** Never lets monitoring break the job: 10 s timeout, errors ignored. */
async function ping(suffix = "") {
  await fetch(PING_URL + suffix, { method: "POST", signal: AbortSignal.timeout(10_000) }).catch(() => {});
}

await ping("/start");
try {
  await runJob();
  await ping();
} catch (error) {
  await ping("/fail");
  throw error;
}
```

### Python

```python
import os, urllib.request

PING_URL = os.environ["BELVADOR_PING_URL"]  # https://ping.belvador.dev/p/bvp_<your-ping-token>

def ping(suffix=""):
    try:
        urllib.request.urlopen(PING_URL + suffix, data=b"", timeout=10)
    except OSError:
        pass  # monitoring never breaks the job

ping("/start")
try:
    run_job()
    ping()
except Exception:
    ping("/fail")
    raise
```

### GitHub Actions

```text
# .github/workflows/nightly.yml — store the URL as the secret BELVADOR_PING_URL
jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}/start" || true
      - run: ./scripts/nightly.sh
      - if: success()
        run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}" || true
      - if: failure()
        run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}/fail" || true
```

## Gửi kèm output của job

Tín hiệu nào cũng có thể mang output của job trong body `POST` — vài dòng cuối của log, thông báo lỗi, số dòng đã xử lý. Nội dung hiện ở mục **Nội dung gửi kèm** trên trang monitor và trong lượt chạy tương ứng.

```bash
# Run the job, then send its exit status with the end of its output (60 KB; the cap is 64 KB).
out=$(./nightly-job.sh 2>&1); code=$?
printf '%s\n' "$out" | tail -c 60000 | curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @- https://ping.belvador.dev/p/bvp_<your-ping-token>/$code || true

# A progress note at any time — shown with the run, never changes the status
echo "exported 1204 rows" | curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @- https://ping.belvador.dev/p/bvp_<your-ping-token>/log || true
```

- Tối đa 64 KB văn bản mỗi lần ping. Body lớn hơn bị từ chối với `413` và **lần ping đó không được ghi nhận**, nên hãy cắt bớt (`tail -c 60000`).
- Output **chỉ được lưu trên node của bạn**, giữ 100 bản mới nhất mỗi môi trường. Trên đường đi nó được xử lý như event lỗi của bạn: chuyển thẳng về node và niêm phong bằng khoá của node mỗi khi phải chờ ở Cloudflare. Cơ sở dữ liệu và tin cảnh báo của Belvador không bao giờ chứa nó.
- [Quy tắc lọc dữ liệu](https://belvador.dev/vi/docs/privacy) của node chạy trên nội dung này, và mọi ping URL trong văn bản được thay thế trước khi rời dịch vụ ping.
- Output không bao giờ quyết định trạng thái: chỉ URL mới quyết định. Body ghi "error" mà gọi URL gốc thì vẫn là thành công.
- Việc chuyển về node là cố gắng tối đa và không bao giờ làm chậm mã `200`. Khi node không liên lạc được, ping vẫn được xét và cảnh báo vẫn gửi; output chờ node trong cùng giới hạn như event của bạn.

Mọi công thức đều đặt giới hạn 10 giây và bỏ qua lỗi (`|| true` sau mỗi lệnh `curl`), nên Belvador có gián đoạn cũng không bao giờ làm job của bạn thất bại — kể cả trong script `set -e` hay một step GitHub Actions, vốn dừng ngay ở lệnh lỗi đầu tiên. Nếu ping URL không bao giờ trả lời, mỗi lần ping bỏ cuộc sau khoảng 90 giây; thêm `--retry-max-time 30` nếu job của bạn không chờ được lâu như vậy.

## Các trạng thái

| Trạng thái | Ý nghĩa |
|---|---|
| **Đang chờ ping đầu tiên** | Mới tạo, chưa có lần gọi nào |
| **Khoẻ** | Lần gọi gần nhất là thành công và chưa tới hạn lần sau |
| **Trễ** | Đã quá giờ ping kế tiếp nhưng còn trong khoảng cho phép trễ — chưa báo |
| **Thất bại** | Không có ping trong khoảng cho phép trễ (**Bị lỡ**), job gọi `/fail` hoặc mã thoát khác 0, hoặc một lượt đã gọi `/start` chạy quá thời gian tối đa |
| **Tạm dừng** | Không đánh giá cho tới khi bạn bật lại hoặc hết thời gian tạm dừng |

Lần ping thành công kế tiếp đóng sự cố và gửi tin **đã hồi phục**.

Trang monitor hiện lúc nào đang chờ ping kế tiếp, lúc nào Belvador sẽ báo, và 7 ngày gần nhất theo từng giờ (ổn, lỗi, bị lỡ).

## Cảnh báo

Monitor ping URL dùng chung quy tắc cảnh báo với các monitor khác: một lượt bị lỡ hoặc lỗi mở sự cố và các nơi nhận của quy tắc nhận tin; khi hồi phục có tin đóng. **Không lặp lại trong** (mặc định 30 phút) giữ cho cùng một monitor không báo hai lần liền. Chọn nơi nhận ở [Cảnh báo](https://belvador.dev/vi/docs/alerts), kể cả [webhook của chính bạn](https://belvador.dev/vi/docs/alerts/webhooks).

Tin báo ghi **"không nhận được tín hiệu"**, không bao giờ ghi "máy chủ đã chết": Belvador chỉ biết là ping đã dừng.

## Tạm dừng để bảo trì

Trên trang monitor chọn **Tạm dừng** — 30 phút, 1, 2, 8 hoặc 24 giờ, tới một thời điểm bạn chọn, hoặc cho tới khi bật lại. Trong lúc tạm dừng, thiếu ping không báo động. Ping vẫn được đếm nên lịch sử vẫn đầy đủ.

## Xoay hoặc đặt lại URL

Hãy giữ ping URL như mật khẩu: ai có nó đều báo được cho monitor này (nhưng không đọc được gì). Owner và admin quản lý ở mục **Ping URL** trên trang monitor; mỗi thao tác hỏi mã xác thực của bạn.

- **Xoay URL** cấp URL mới và giữ URL cũ hoạt động thêm một khoảng bạn chọn — 1 giờ, 24 giờ (mặc định), 3 hoặc 7 ngày — để bạn cập nhật job mà không lỡ ping nào. Cùng lúc chỉ tối đa 2 URL hoạt động.
- **Ngừng URL cũ ngay** kết thúc khoảng chồng lấn sớm.
- **Đặt lại khi bị lộ** dừng mọi URL ngay lập tức và cấp URL mới.

Thay đổi có hiệu lực ở địa chỉ nhận ping trong vòng 60 giây.

## Mã trả về

| Mã | Ý nghĩa |
|---|---|
| `200 OK` | Tín hiệu đã được lưu — Belvador chỉ trả lời sau khi đã ghi an toàn |
| `404` | URL không tồn tại, đã thu hồi hoặc hết hạn (cùng một câu trả lời để không đoán được URL) |
| `400` | Hậu tố khác `start`, `fail`, `log` hoặc 0–255 |
| `403` | Request đến từ trang web (có header `Origin`), hoặc workspace đang không nhận dữ liệu |
| `405` | Phương thức khác `GET` hoặc `POST` |
| `413` | Body lớn hơn 64 KB; không ghi nhận gì |
| `429` | Quá 60 lần gọi mỗi phút cho monitor này (dồn tối đa 20), hoặc vượt giới hạn tháng của gói. Hãy tôn trọng `Retry-After` |
| `503` | Thử lại sau vài giây (`Retry-After`) |

Gọi lặp lại một lần gọi là an toàn: gửi header `Idempotency-Key` (hoặc `?rid=<id>`) thì các lần lặp chỉ tính một lần.

## Cần biết

- Đừng gọi URL từ trang web. Trình duyệt bị từ chối, và URL sẽ lộ cho mọi người xem trang.
- Ping tới sớm tối đa một phút trước giờ cron vẫn tính cho giờ đó, để bù lệch đồng hồ của máy chạy job.
- Nếu chính Belvador tạm thời không đánh giá được, nó chờ các ping còn đang đệm rồi mới quyết định, và đánh dấu quyết định muộn là **phát hiện muộn** thay vì báo hai lần.
