---
id: logs-files
title: "Đọc log trong /var/log"
shortTitle: "/var/log, tail, grep"
track: logs
trackTitle: "Xem log trên server"
order: 1
level: core
status: published
estimatedMinutes: 35
prerequisites:
  - linux-filesystem-permissions
next: logs-journald
reviewedAgainst: 2026-10-12
sources:
  - title: "Filesystem Hierarchy Standard — /var/log"
    url: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s08.html
  - title: "tail(1)"
    url: https://man7.org/linux/man-pages/man1/tail.1.html
  - title: "grep(1)"
    url: https://man7.org/linux/man-pages/man1/grep.1.html
  - title: "awk(1)"
    url: https://man7.org/linux/man-pages/man1/awk.1p.html
---

# Đọc log trong /var/log

Lệnh chạy trên Linux hoặc WSL2. Tên file trong `/var/log` khác nhau giữa Debian/Ubuntu và RHEL, nên chỗ nào phụ thuộc distro thì bài sẽ ghi rõ.

## Để làm gì

Khi app trả 500 hoặc service không lên, việc đầu tiên là mở đúng file log, xem phần mới nhất, rồi lọc theo thời gian hoặc từ khoá. Đọc log trước, rồi mới đoán xem config sai ở đâu.

## Gồm những gì

- `/var/log` theo FHS: log của host, file có thể xoay vòng.
- `tail -n` và `tail -f` / `tail -F`.
- `less` để đọc file dài.
- `grep` và `awk` để lọc dòng.

`journalctl` nằm ở bài sau. Log của container (`docker logs`) và Pod (`kubectl logs`) nằm ở bài Docker và Kubernetes.

## Dùng khi nào

Bài này áp dụng khi service ghi log ra file, hoặc khi rsyslog/syslog-ng đang chuyển journal sang file. Ubuntu desktop thường vẫn có `/var/log/syslog` và `/var/log/auth.log`, nhưng máy chỉ dùng journald có thể không có hai file đó. Không thấy file thì chuyển sang `journalctl`, đừng tạo file giả.

## Kiến thức cốt lõi

Theo FHS, log của host nằm trong `/var/log`. Các file này được xoay vòng (rotate) thành `syslog`, `syslog.1`, `syslog.2.gz`, và sau mỗi lần xoay, process đang ghi có thể chuyển sang file mới. `tail -f` bám theo file descriptor cũ, nên sau khi xoay vòng nó đứng im. `tail -F` tương đương `--follow=name --retry`: nó bám theo tên file, nên khi file bị đổi tên thì nó mở lại file mới.

| File hay gặp | Distro | Nội dung |
| --- | --- | --- |
| `/var/log/syslog` | Debian, Ubuntu | log hệ thống chung |
| `/var/log/messages` | RHEL, Fedora | cùng vai trò với syslog |
| `/var/log/auth.log` | Debian, Ubuntu | đăng nhập, sudo, sshd |
| `/var/log/secure` | RHEL | cùng vai trò với auth.log |
| `/var/log/nginx/access.log` | nơi đã cài Nginx | mỗi request một dòng |
| `/var/log/nginx/error.log` | nơi đã cài Nginx | lỗi Nginx và upstream |

Một dòng access log của Nginx thường bắt đầu bằng IP client, rồi tới thời điểm, method, path, status. `awk` tách cột theo khoảng trắng, nên status trong combined log nằm ở một cột khá xa phía sau. Đừng học thuộc số cột; hãy in `$0` ra trước và đếm trên một dòng thật.

`grep -F` tìm đúng chuỗi, không coi `.` hay `*` là ký tự regex. `grep -E` bật extended regex. `grep -i` bỏ qua phân biệt hoa thường.

## Lệnh và code mẫu

Lab không cần cài Nginx. Bạn tự tạo một file giống access log:

```bash
mkdir -p "$HOME/devops-lab/logs"
cd "$HOME/devops-lab/logs"
cat > access.log <<'EOF'
10.0.0.8 - - [12/Oct/2026:09:01:02 +0700] "GET /health HTTP/1.1" 200 12
10.0.0.8 - - [12/Oct/2026:09:01:03 +0700] "GET /api/orders HTTP/1.1" 500 40
10.0.0.9 - - [12/Oct/2026:09:01:04 +0700] "POST /api/orders HTTP/1.1" 201 18
10.0.0.8 - - [12/Oct/2026:09:02:10 +0700] "GET /api/orders HTTP/1.1" 500 40
EOF
```

```bash
tail -n 2 access.log
grep -F '" 500 ' access.log
awk '$9 == 500 { print $1, $7 }' access.log
```

Với các dòng ở trên, `$9` là status và `$7` là path. Nếu format log đổi thì số cột cũng đổi theo.

Theo dõi file đang được ghi:

```bash
tail -f access.log
```

Ở một terminal khác, chạy `echo '10.0.0.8 - - [12/Oct/2026:09:03:00 +0700] "GET /health HTTP/1.1" 200 2' >> access.log`. Dòng mới sẽ hiện ra ở cửa sổ `tail`. Bấm `Ctrl-C` để thoát.

Với `less access.log`, gõ `F` để follow; muốn thoát thì bấm `Ctrl-C` rồi `q`.

## Thực hành

1. Tạo `access.log` như trên.
2. Đếm số dòng 500: `grep -c -F '" 500 ' access.log`.
3. Liệt kê IP nào gặp 500, mỗi IP một lần: `awk '$9 == 500 { print $1 }' access.log | sort | uniq`.
4. Chạy `tail -f` ở một terminal, rồi append một dòng từ terminal kia.
5. Đổi tên file trong lúc `tail -f` đang chạy: `mv access.log access.log.1 && cp access.log.1 access.log`, rồi append thêm một dòng vào `access.log`. Bạn sẽ thấy `tail -f` đứng im. Chạy lại với `tail -F` và lặp lại bước đổi tên.

## Kết quả mong đợi

- `grep -c` in `2`.
- `uniq` chỉ còn `10.0.0.8`.
- `tail -F` in dòng mới sau khi file bị đổi tên. `tail -f` thì không.

## Checklist

- [ ] Biết file auth của Debian là `auth.log`, của RHEL là `secure`.
- [ ] Dùng `tail -F` khi log bị xoay vòng.
- [ ] Dùng `grep -F` khi từ khoá có dấu `.` hoặc `"`.
- [ ] Đếm cột trên một dòng thật trước khi viết `awk`.
- [ ] Không kết luận "không có log" chỉ vì thiếu `/var/log/syslog`.

## Thẻ ôn

### logs-files-auth
kind: distinguish
cue: Log đăng nhập trên Debian và Ubuntu thường là `/var/log/auth.log`. RHEL dùng `/var/log/secure`. Không thấy `/var/log/syslog` chưa có nghĩa là máy không có log.

Q: Cần xem vì sao đăng nhập SSH thất bại. Máy này là Debian, máy kia là RHEL. Mỗi bên thường đọc file nào? Nếu không thấy `/var/log/syslog` thì có kết luận máy không ghi log không?

A: Debian và Ubuntu thường để log xác thực ở `/var/log/auth.log`. RHEL để ở `/var/log/secure`. Cùng một việc, hai đường dẫn, đừng tìm một tên cho mọi distro. Máy cấu hình chỉ dùng journald có thể không có hai file đó, log nằm trong journal. Thiếu `/var/log/syslog` chỉ nói file đó không được ghi ra đĩa, chưa nói là không có log. Hỏi đúng nơi distro đó đang ghi trước khi kết luận logging tắt.

### logs-files-tail
kind: distinguish
cue: `tail -f` bám file descriptor cũ. Logrotate đổi tên file thì cửa sổ đứng im. `tail -F` bám tên và mở lại file mới.

Q: Bạn đang `tail -f` một file log thì logrotate chạy, đổi tên file cũ và tạo file mới cho process ghi tiếp. Cửa sổ tail đứng im, không còn dòng mới. Vì sao, và cờ nào theo được file mới?

A: `tail -f` mở file và giữ file descriptor đó. Logrotate đổi tên inode cũ thành file đã xoay, process ứng dụng mở một file mới cùng tên cũ. Cửa sổ của bạn vẫn đọc inode cũ, nên hết dòng mới là đứng. `tail -F` theo tên file: khi tên bị thay, nó mở lại file mang tên đó. Xem log của service đang bị xoay thì dùng `-F`.

### logs-files-grep
kind: command
cue: `grep 500` trên access log khớp cả byte, timestamp hoặc IP có chứa `500`. Đếm status thì lọc đúng cột, hoặc `grep -F` khi từ khóa có `.` và `"`.

Q: Access log dạng combined, bạn chạy `grep 500 access.log` để đếm request trả status 500. Con số đó có đáng tin không?

A: Không. `grep` tìm chuỗi `500` ở bất kỳ chỗ nào trên dòng. Byte count, timestamp, hoặc một IP có chứa `500` đều khớp, dù status là 200. Bạn đếm nhầm request. Khi từ khóa có `.` hoặc `"`, `grep -F` tìm đúng chữ, không coi dấu chấm là "một ký tự bất kỳ". Muốn đúng status, đếm cột trên một dòng thật rồi lọc đúng cột đó bằng `awk`, đừng đếm mọi dòng có chứa ba chữ số.

### logs-files-rm
kind: trap
cue: `rm` một file log đang mở không trả đĩa ngay, vì process còn giữ file descriptor cũ. Xoay bằng logrotate hoặc copy-truncate.

Q: Đĩa đầy. Bạn `rm` file log mà ứng dụng đang ghi, rồi tạo lại một file cùng tên, và chờ chỗ trống trở về. Chỗ trống có về ngay không, log mới có vào file vừa tạo không?

A: Không ngay. Ứng dụng vẫn giữ file descriptor của inode cũ. `rm` chỉ bỏ tên, inode còn sống chừng nào process chưa đóng, nên khối đĩa chưa được trả. Process cũng không tự chuyển sang file mới bạn vừa tạo cùng tên. Nó tiếp tục ghi vào inode đã mất tên, và bạn không thấy dòng mới trong file mới. Xoay log bằng logrotate, hoặc cơ chế copy-truncate mà ứng dụng còn ghi đúng chỗ. Đừng `rm` file đang mở để "dọn đĩa".

## Lỗi thường gặp

**`grep 500 access.log` trên combined log.** Lệnh này khớp luôn cả byte count, timestamp hay IP có chứa `500`. Hãy neo pattern bằng dấu cách và dấu nháy, hoặc lọc đúng cột status bằng `awk`.

**`tail -f` đúng lúc logrotate chạy.** File cũ bị đổi tên, process chuyển sang ghi file mới, còn cửa sổ tail thì đứng im. Dùng `tail -F`.

**Xoá file log đang mở để "cho sạch".** Process vẫn giữ file descriptor cũ, nên disk không được trả lại chỗ trống, và log mới cũng không ghi vào file bạn vừa tạo lại. Hãy xoay log bằng logrotate hoặc copy-truncate, đừng `rm` file đang mở.

## Nguồn

1. [FHS /var/log](https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s08.html) — Linux Foundation, vai trò thư mục log.
2. [tail(1)](https://man7.org/linux/man-pages/man1/tail.1.html) — `-f` theo descriptor, `-F` theo tên file.
3. [grep(1)](https://man7.org/linux/man-pages/man1/grep.1.html) — `-F`, `-E`, `-c`.
4. [awk(1p)](https://man7.org/linux/man-pages/man1/awk.1p.html) — field `$1`, `$9`.

## Tiếp theo

[journalctl](./02-journald.md)
