رفتن به محتوا
LoopX

لاگ‌ها

توی این درس یاد می‌گیری از لاگ‌های Nginx جواب سؤال‌های واقعی را دربیاوری: «چه کسی بیشتر از همه درخواست می‌فرستد؟»، «کدام صفحه‌ها ۴۰۴ می‌دهند؟»، «کدام آدرس کند است؟». دو لاگ Nginx را می‌شناسی: access log (هر درخواست یک خط) و error log (مشکل‌ها، با سطح اهمیت). قالب پیش‌فرض combined را فیلد به فیلد می‌خوانی، با log_format قالب خودت را می‌سازی (با زمان پاسخ، و حتی JSON)، برای هر سایت لاگ جدا می‌گذاری، با tail -f لاگ زنده را تماشا می‌کنی و با ابزارهای لینوکس (awk، sort، uniq) تحلیلش می‌کنی. آخر سر می‌فهمی چرخش لاگ (logrotate) چطور کار می‌کند و چرا Nginx باید بعدش فایل‌ها را «دوباره باز کند».

مسئله: سایت کند شده، چرا؟

Section titled “مسئله: سایت کند شده، چرا؟”

ساعت ۱۰ صبح پیام می‌آید: «سایت کند شده». CPU بالاست. کسی حمله کرده؟ یک ربات دارد همه‌ی صفحه‌ها را می‌خزد؟ یک صفحه‌ی خاص کند شده؟ یا فقط ترافیک واقعی زیاد شده؟ بدون نگاه به لاگ، فقط حدس است. با لاگ، در یک دقیقه می‌بینی ۶۰٪ درخواست‌ها از یک IP می‌آیند و همه به /search می‌روند.

تشبیه: دفتر ورود و خروج و دفتر حوادث

Section titled “تشبیه: دفتر ورود و خروج و دفتر حوادث”

نگهبان ساختمان دو دفتر دارد. دفتر ورود و خروج (access log): هر کس آمد، ساعت، از کجا، به کدام طبقه رفت و چه شد؛ یک خط برای هر نفر، چه مشکلی بوده باشد چه نه. دفتر حوادث (error log): فقط وقتی چیزی غیرعادی رخ داد (در قفل بود، آسانسور خراب شد، کسی بی‌اجازه خواست وارد شود)، با درجه‌ی اهمیت. برای آمار و «چه کسی چه کرد» سراغ دفتر اول می‌روی و برای «چرا خراب شد» سراغ دومی.

access log error log
چه چیزی هر درخواست، یک خط خطاها و هشدارها، با سطح
مسیر پیش‌فرض Ubuntu /var/log/nginx/access.log /var/log/nginx/error.log
directive access_log مسیر [قالب]; error_log مسیر [سطح];
قالب قابل تنظیم با log_format ثابت
کجا تنظیم می‌شود http، server، location main، http، server، location
کاربرد آمار، امنیت، کندی، رفتار کاربران عیب‌یابی، مجوزها، خطای اپ پشتی
مسیر یک درخواست تا لاگ: Nginx درخواست را پردازش می‌کند؛ اگر در این میان مشکلی رخ دهد (فایل نیست، مجوز ندارد، اپ پشتی جواب نمی‌دهد)، یک خط در error log می‌نویسد. در پایان، با قالب log_format یک خط در access log می‌نویسد. logrotate هر روز فایل‌ها را جابه‌جا و فشرده می‌کند و به Nginx سیگنال می‌دهد که فایل‌های تازه را باز کند.

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده. برای اینکه لاگ واقعی و متنوع داشته باشیم، با curl از چند آدرس مبدأ مختلف (127.0.0.x؛ لینوکس اجازه می‌دهد از هر آدرس 127.* به‌عنوان مبدأ استفاده کنی) و با User-Agent های مختلف ترافیک می‌سازیم؛ پس همه‌ی خط‌های لاگ این درس واقعی‌اند ولی کاربرانشان شبیه‌سازی شده‌اند.

مثال ۱: اولین خط‌های access log

Section titled “مثال ۱: اولین خط‌های access log”

یک درخواست موفق، یک ۴۰۴ و یک درخواست از «مرورگر»:

Terminal window
curl -s -o /dev/null http://localhost/
curl -s -o /dev/null http://localhost/missing-page
curl -s -o /dev/null --interface 127.0.0.23 -A 'Mozilla/5.0 (Windows NT 10.0) Firefox/131.0' -e 'https://www.google.com/' 'http://localhost/?q=nginx'
tail -n 3 /var/log/nginx/access.log
خروجی
127.0.0.1 - - [04/Oct/2026:12:55:28 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
127.0.0.1 - - [04/Oct/2026:12:55:28 +0000] "GET /missing-page HTTP/1.1" 404 162 "-" "curl/8.5.0"
127.0.0.23 - - [04/Oct/2026:12:55:28 +0000] "GET /?q=nginx HTTP/1.1" 200 615 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0) Firefox/131.0"

هر خط با قالب پیش‌فرض combined است. سومی را فیلد به فیلد بخوانیم:

بخش مقدار متغیر Nginx
IP کاربر 127.0.0.23 $remote_addr
- (قدیمی، همیشه خط تیره)
کاربر (اگر احراز هویت HTTP بود) - $remote_user
زمان [04/Oct/2026:...+0000] $time_local
خط درخواست "GET /?q=nginx HTTP/1.1" $request
کد وضعیت 200 $status
بایت‌های بدنه‌ی پاسخ 615 $body_bytes_sent
صفحه‌ی ارجاع‌دهنده "https://www.google.com/" $http_referer
مرورگر "Mozilla/5.0 ..." $http_user_agent

دقت کن: درخواست ۴۰۴ هم در access log آمد. access log همه‌ی درخواست‌ها را دارد، موفق یا ناموفق.

حالا ببینیم آن ۴۰۴ و چند مشکل دیگر در error log چطور ثبت می‌شوند:

Terminal window
mkdir -p /var/www/html/private && echo secret > /var/www/html/private/index.html && chmod 700 /var/www/html/private
curl -s -o /dev/null http://localhost/private/
cat > /etc/nginx/conf.d/lx-proxy.conf <<'EOF'
server {
listen 8081;
location / {
proxy_pass http://127.0.0.1:9999;
}
}
server {
listen 8084;
root /var/www/html;
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -o /dev/null -w "proxy to a dead app: HTTP %{http_code}\n" http://localhost:8081/
curl -s -o /dev/null -w "no try_files, missing file: HTTP %{http_code}\n" http://localhost:8084/missing-page
sed -E 's/^[0-9/]+ [0-9:]+ //' /var/log/nginx/error.log
rm -rf /var/www/html/private
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
proxy to a dead app: HTTP 502
no try_files, missing file: HTTP 404
[error] 20845#20845: *4 "/var/www/html/private/index.html" is forbidden (13: Permission denied), client: 127.0.0.1, server: _, request: "GET /private/ HTTP/1.1", host: "localhost"
[error] 20861#20861: *5 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:9999/", host: "localhost:8081"
[error] 20861#20861: *7 open() "/var/www/html/missing-page" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /missing-page HTTP/1.1", host: "localhost:8084"

هر خط error log: زمان (که اینجا برای خوانایی حذفش کرده‌ایم)، سطح در کروشه، شماره‌ی پروسه و اتصال، و توضیح:

  • ".../private/index.html" is forbidden (13: Permission denied): مجوز (۴۰۳).
  • connect() failed (111: Connection refused) while connecting to upstream: اپ پشتی خاموش است؛ کاربر 502 Bad Gateway گرفت. این پرکاربردترین خط error log در reverse proxy است (درس بعدی).
  • open() ".../missing-page" failed (2: No such file or directory): یک ۴۰۴ (سطح error).

ولی دقت کن: ۴۰۴ مثال ۱ (/missing-page روی سایت پیش‌فرض) در error log نیامد؛ فقط ۴۰۴ پورت ۸۰۸۴. فرقشان try_files است: سایت پیش‌فرض Ubuntu try_files $uri $uri/ =404; دارد که وجود فایل را بی‌صدا بررسی می‌کند و فقط ۴۰۴ برمی‌گرداند، ولی سرور ۸۰۸۴ بدون try_files سعی کرد فایل را باز کند و شکستش را ثبت کرد. پس ۴۰۴ همیشه در access log هست، ولی در error log بستگی به تنظیمات دارد.

سطح‌ها از کم به زیاد: debug، info، notice، warn، error، crit، alert، emerg. با error_log /var/log/nginx/error.log warn; فقط warn و بالاتر ثبت می‌شوند. (اگر ۴۰۴ ها error log را پر می‌کنند و برایت مهم نیستند، log_not_found off; همان خط‌ها را خاموش می‌کند.)

tail -f خط‌های تازه را همان لحظه که نوشته می‌شوند نشان می‌دهد؛ اولین ابزار وقتی می‌خواهی ببینی «همین الان چه می‌گذرد». در یک ترمینال tail -f اجرا می‌کنیم و هم‌زمان چند درخواست می‌فرستیم:

Terminal window
tmux kill-server 2>/dev/null
tmux new-session -d -s live -x 120 -y 10 "tail -n 0 -f /var/log/nginx/access.log"
sleep 1
for p in /pricing /cart /checkout; do
curl -s -o /dev/null --interface 127.0.0.31 "http://localhost$p"
sleep 0.3
done
sleep 1
tmux capture-pane -t live -p | grep -v '^$'
tmux kill-session -t live
خروجی
127.0.0.31 - - [04/Oct/2026:12:55:30 +0000] "GET /pricing HTTP/1.1" 404 162 "-" "curl/8.5.0"
127.0.0.31 - - [04/Oct/2026:12:55:30 +0000] "GET /cart HTTP/1.1" 404 162 "-" "curl/8.5.0"
127.0.0.31 - - [04/Oct/2026:12:55:31 +0000] "GET /checkout HTTP/1.1" 404 162 "-" "curl/8.5.0"

سه درخواست، به محض ثبت، در ترمینال دوم ظاهر شدند (با Ctrl+C از tail -f خارج می‌شوی). ترکیب‌های پرکاربرد:

  • tail -f access.log | grep ' 500 ': فقط خطاهای سرور، زنده.
  • tail -f access.log error.log: هر دو با هم (با سرتیتر اسم فایل).
  • tail -F: مثل -f، ولی اگر فایل چرخیده شد (logrotate)، فایل تازه را دنبال می‌کند (اشتباهات رایج).

مثال ۴: ترافیک واقعی و تحلیل با ابزارهای لینوکس

Section titled “مثال ۴: ترافیک واقعی و تحلیل با ابزارهای لینوکس”

حالا یک «روز کاری» کوچک می‌سازیم: کاربران عادی، یک ربات که صفحه‌های زیادی را می‌خزد، و یک اسکنر که دنبال فایل‌های حساس می‌گردد. ابتدا فایل‌های سایت:

Terminal window
for p in index pricing about blog/nginx-intro blog/docker-tips; do
mkdir -p "/var/www/html/$(dirname "$p")"
echo "<h1>$p</h1>" > "/var/www/html/$p.html"
done
gen() {
local ip=$1 ua=$2; shift 2
for p in "$@"; do curl -s -o /dev/null --interface "$ip" -A "$ua" "http://localhost$p"; done
}
ff='Mozilla/5.0 (X11; Linux x86_64) Firefox/131.0'
ch='Mozilla/5.0 (Windows NT 10.0) Chrome/129.0'
gen 127.0.0.11 "$ff" /index.html /pricing.html /about.html /pricing.html
gen 127.0.0.12 "$ch" /index.html /blog/nginx-intro.html /blog/docker-tips.html /index.html
gen 127.0.0.13 "$ch" /index.html /pricing.html
for i in $(seq 1 12); do gen 127.0.0.66 'Mozilla/5.0 (compatible; Googlebot/2.1)' /index.html /blog/nginx-intro.html /about.html; done
gen 127.0.0.99 'python-requests/2.31' /.env /wp-login.php /.git/config /admin.php /phpmyadmin/ /backup.zip
wc -l /var/log/nginx/access.log
خروجی
61 /var/log/nginx/access.log

حالا تمرین اصلی درس، یعنی سؤال‌های واقعی با یک خط فرمان. پرتکرارترین IPها (فیلد اول):

Terminal window
cd /var/log/nginx
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
خروجی
36 127.0.0.66
6 127.0.0.99
5 127.0.0.1
4 127.0.0.12
4 127.0.0.11
3 127.0.0.31
2 127.0.0.13
1 127.0.0.23

خط لوله (pipe) را یادت هست (درس pipe‌ها در دوره‌ی لینوکس): awk '{print $1}' فیلد اول (IP) هر خط را می‌دهد؛ sort آن‌ها را کنار هم می‌چیند؛ uniq -c تکرارهای پشت سر هم را می‌شمارد (برای همین sort قبلش لازم است)؛ sort -rn بر اساس عدد، از زیاد به کم؛ head ده تای اول. ربات (127.0.0.66) بالاترین است.

پرتکرارترین صفحه‌ها (فیلد هفتم، مسیر درخواست):

Terminal window
cd /var/log/nginx
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -n 5
خروجی
16 /index.html
13 /blog/nginx-intro.html
13 /about.html
3 /pricing.html
2 /missing-page

توزیع کدهای وضعیت (فیلد نهم) و همه‌ی ۴۰۴ ها با IP:

Terminal window
cd /var/log/nginx
echo "=== کدهای وضعیت:"
awk '{print $9}' access.log | sort | uniq -c | sort -rn
echo "=== آدرس‌های ۴۰۴ و چه کسی خواسته:"
awk '$9 == 404 {print $1, $7}' access.log | sort | uniq -c | sort -rn
خروجی
=== کدهای وضعیت:
48 200
11 404
1 502
1 403
=== آدرس‌های ۴۰۴ و چه کسی خواسته:
2 127.0.0.1 /missing-page
1 127.0.0.99 /wp-login.php
1 127.0.0.99 /phpmyadmin/
1 127.0.0.99 /backup.zip
1 127.0.0.99 /admin.php
1 127.0.0.99 /.git/config
1 127.0.0.99 /.env
1 127.0.0.31 /pricing
1 127.0.0.31 /checkout
1 127.0.0.31 /cart

awk '$9 == 404 {...}' فقط خط‌هایی را که فیلد نهمشان ۴۰۴ است پردازش می‌کند. نتیجه داستان را تعریف می‌کند: 127.0.0.99 (با python-requests) دنبال .env، .git/config، wp-login.php و phpmyadmin گشته؛ یک اسکنر خودکار که در لاگ هر سایت واقعی روی اینترنت هر روز می‌بینی.

مرورگرها و ربات‌ها (User-Agent، که داخل کوتیشن است؛ پس با -F'"' جدا می‌کنیم):

Terminal window
cd /var/log/nginx
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn
خروجی
36 Mozilla/5.0 (compatible; Googlebot/2.1)
8 curl/8.5.0
6 python-requests/2.31
6 Mozilla/5.0 (Windows NT 10.0) Chrome/129.0
4 Mozilla/5.0 (X11; Linux x86_64) Firefox/131.0
1 Mozilla/5.0 (Windows NT 10.0) Firefox/131.0

با -F'"' جداکننده کوتیشن است: فیلد ۲ خط درخواست، ۴ ارجاع‌دهنده و ۶ User-Agent.

مثال ۵: log_format سفارشی با زمان پاسخ

Section titled “مثال ۵: log_format سفارشی با زمان پاسخ”

قالب combined نمی‌گوید هر درخواست چقدر طول کشید. با log_format قالب تازه‌ای می‌سازیم (باید در context http تعریف شود، پس در conf.d):

/etc/nginx/conf.d/log-formats.conf
log_format timed '$remote_addr - [$time_local] "$request" $status $body_bytes_sent '
'host=$host rt=$request_time urt=$upstream_response_time';

یک سایت shop.test با لاگ جدا و این قالب، که یک بخش از آن (API) به یک اپ پایتون کند می‌رود. (اپ آزمایشی یک سرور کوچک پایتون است که برای /api/report عمداً یک ثانیه صبر می‌کند.)

Terminal window
mkdir -p /srv/lx-app
cat > /srv/lx-app/app.py <<'EOF'
# tiny test API: /api/report is slow on purpose
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/api/report"):
time.sleep(1)
body = b'{"ok": true}\n'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
HTTPServer(("127.0.0.1", 9301), Handler).serve_forever()
EOF
(exec python3 /srv/lx-app/app.py) > /dev/null 2>&1 &
mkdir -p /var/www/shop.test && echo '<h1>shop</h1>' > /var/www/shop.test/index.html
cat > /etc/nginx/sites-available/shop.test <<'EOF'
server {
listen 80;
server_name shop.test;
root /var/www/shop.test;
access_log /var/log/nginx/shop.access.log timed;
error_log /var/log/nginx/shop.error.log warn;
location /api/ {
proxy_pass http://127.0.0.1:9301;
}
}
EOF
ln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/
sleep 1
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for p in / /api/products /api/report /api/products /api/report; do
curl -s -o /dev/null "http://shop.test$p"
done
cat /var/log/nginx/shop.access.log
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
127.0.0.1 - [04/Oct/2026:12:55:34 +0000] "GET / HTTP/1.1" 200 14 host=shop.test rt=0.000 urt=-
127.0.0.1 - [04/Oct/2026:12:55:34 +0000] "GET /api/products HTTP/1.1" 200 13 host=shop.test rt=0.001 urt=0.001
127.0.0.1 - [04/Oct/2026:12:55:35 +0000] "GET /api/report HTTP/1.1" 200 13 host=shop.test rt=1.001 urt=1.001
127.0.0.1 - [04/Oct/2026:12:55:35 +0000] "GET /api/products HTTP/1.1" 200 13 host=shop.test rt=0.001 urt=0.001
127.0.0.1 - [04/Oct/2026:12:55:36 +0000] "GET /api/report HTTP/1.1" 200 13 host=shop.test rt=1.001 urt=1.001
  • rt ($request_time) زمان کل از اولین بایت درخواست تا آخرین بایت پاسخ، به ثانیه با دقت میلی‌ثانیه.
  • urt ($upstream_response_time) زمانی که اپ پشتی صرف کرد؛ برای فایل استاتیک - است (اپی در کار نبود).
  • این لاگ فقط مال shop.test است؛ لاگ سایت‌های دیگر قاطی نمی‌شود.

کندترین درخواست‌ها، با جدا کردن فیلد rt=:

Terminal window
awk '{for (i = 1; i <= NF; i++) if ($i ~ /^rt=/) {sub("rt=", "", $i); print $i, $6}}' /var/log/nginx/shop.access.log | sort -rn | head -n 3
خروجی
1.001 /api/report
1.001 /api/report
0.001 /api/products

دو درخواست /api/report با بیش از یک ثانیه، بالای فهرست؛ همان «تیم برنامه‌نویسی، اینجا را ببینید» سناریوی اول درس.

مثال ۶: لاگ JSON برای ابزارهای تحلیل

Section titled “مثال ۶: لاگ JSON برای ابزارهای تحلیل”

ابزارهای جمع‌آوری لاگ (مثل ELK، Loki یا Graylog) JSON را خیلی راحت‌تر از متن آزاد می‌خوانند. escape=json مقدارها را برای JSON امن می‌کند (کوتیشن و کاراکترهای خاص):

Terminal window
cat >> /etc/nginx/conf.d/log-formats.conf <<'EOF'
log_format json escape=json '{"time":"$time_iso8601","ip":"$remote_addr","method":"$request_method",'
'"uri":"$request_uri","status":$status,"bytes":$body_bytes_sent,'
'"rt":$request_time,"ua":"$http_user_agent"}';
EOF
sed -i 's| error_log /var/log/nginx/shop.error.log warn;| access_log /var/log/nginx/shop.access.json json;\n error_log /var/log/nginx/shop.error.log warn;|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -o /dev/null -A 'Tool "with quotes"' 'http://shop.test/api/products?id=7'
tail -n 1 /var/log/nginx/shop.access.json
echo "--- با jq:"
jq -c '{uri, status, rt}' /var/log/nginx/shop.access.json
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
{"time":"2026-10-04T12:55:37+00:00","ip":"127.0.0.1","method":"GET","uri":"/api/products?id=7","status":200,"bytes":13,"rt":0.001,"ua":"Tool \"with quotes\""}
--- با jq:
{"uri":"/api/products?id=7","status":200,"rt":0.001}
  • یک location می‌تواند چند access_log داشته باشد؛ حالا هر درخواست هم در لاگ متنی و هم در JSON ثبت می‌شود.
  • کوتیشن داخل User-Agent به \" تبدیل شد و JSON معتبر ماند. jq (ابزار پردازش JSON) مستقیم آن را می‌خواند.

مثال ۷: لاگ شرطی و خاموش کردن لاگ

Section titled “مثال ۷: لاگ شرطی و خاموش کردن لاگ”

درخواست‌های پرتکرار و بی‌اهمیت (مثل health check load balancer که هر ۵ ثانیه می‌آید) لاگ را پر می‌کنند. دو راه:

Terminal window
cat > /etc/nginx/conf.d/lx-health.conf <<'EOF'
map $request_uri $loggable {
/health 0;
default 1;
}
server {
listen 8082;
access_log /var/log/nginx/lx-health.log combined if=$loggable;
location = /health { return 200 "ok\n"; }
location = /favicon.ico { access_log off; return 204; }
location / { return 200 "page\n"; }
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for p in /health /health /favicon.ico /page /health; do curl -s -o /dev/null "http://localhost:8082$p"; done
awk '{print $7, $9}' /var/log/nginx/lx-health.log
rm /etc/nginx/conf.d/lx-health.conf /var/log/nginx/lx-health.log
systemctl reload nginx
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
/page 200

از پنج درخواست، فقط /page ثبت شد. map یک متغیر تازه ($loggable) از روی یک متغیر دیگر می‌سازد (اینجا ۰ برای /health، ۱ برای بقیه) و if=$loggable یعنی «فقط وقتی مقدار ۰ یا خالی نیست، ثبت کن». برای یک location مشخص هم access_log off; ساده‌ترین راه است (favicon).

لاگ‌ها بی‌نهایت رشد می‌کنند، پس logrotate (که هر روز با cron یا systemd timer اجرا می‌شود) آن‌ها را جابه‌جا، فشرده و قدیمی‌ها را پاک می‌کند. تنظیم Nginx در Ubuntu:

Terminal window
cat /etc/logrotate.d/nginx
خروجی
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi \
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}

روزانه (daily)، ۱۴ نسخه نگه دار (rotate 14)، فشرده کن ولی نه آخرین نسخه را (compress و delaycompress)، و بعد از چرخش invoke-rc.d nginx rotate را اجرا کن. چرا آن خط آخر لازم است؟ چون Nginx فایل لاگ را یک بار باز می‌کند و از آن به بعد با inode (شناسه‌ی واقعی فایل روی دیسک) در آن می‌نویسد، نه با اسمش. این را ببین:

Terminal window
cd /var/log/nginx
ls -i shop.access.log
mv shop.access.log shop.access.log.1
curl -s -o /dev/null http://shop.test/after-rename
echo "--- بعد از mv و یک درخواست تازه:"
ls -i shop.access.log* 2>&1
tail -n 1 shop.access.log.1 | awk '{print $5, $6}'
echo "--- سیگنال reopen (USR1) به master:"
nginx -s reopen
sleep 1
curl -s -o /dev/null http://shop.test/after-reopen
ls -i shop.access.log*
tail -n 1 shop.access.log | awk '{print $5, $6}'
خروجی
704594 shop.access.log
--- بعد از mv و یک درخواست تازه:
704594 shop.access.log.1
"GET /after-rename
--- سیگنال reopen (USR1) به master:
2026/10/04 12:55:39 [notice] 21056#21056: signal process started
704597 shop.access.log
704594 shop.access.log.1
"GET /after-reopen

بعد از mv، اسم فایل عوض شد ولی inode همان ماند و Nginx درخواست تازه (/after-rename) را در shop.access.log.1 نوشت؛ فایل shop.access.log اصلاً وجود نداشت. بعد از nginx -s reopen (سیگنال USR1 به master؛ همان کاری که invoke-rc.d nginx rotate می‌کند)، Nginx فایل‌ها را با اسمشان دوباره باز کرد، فایل تازه ساخت و درخواست بعدی آنجا رفت. برای همین چرخش دستی لاگ بدون reopen یعنی لاگی که به فایل اشتباه می‌رود.

متغیرهای پرکاربرد برای log_format:

متغیر چیست
$remote_addr IP کاربر (پشت proxy: IP proxy؛ درس reverse proxy)
$time_local / $time_iso8601 زمان (قالب لاگ / ISO)
$request خط کامل درخواست (GET /path HTTP/1.1)
$request_method، $request_uri، $uri متد، آدرس کامل با query، آدرس نرمال‌شده
$status کد وضعیت
$body_bytes_sent / $bytes_sent بایت‌های بدنه / کل پاسخ
$request_time زمان کل (ثانیه، با میلی‌ثانیه)
$upstream_response_time، $upstream_addr زمان و آدرس اپ پشتی
$http_referer، $http_user_agent ارجاع‌دهنده و مرورگر
$host، $server_name دامنه‌ی درخواست / اسم server block

تحلیل با خط فرمان (قالب combined):

سؤال دستور
پرتکرارترین IPها awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
پرتکرارترین صفحه‌ها awk '{print $7}' access.log | sort | uniq -c | sort -rn | head
کدهای وضعیت awk '{print $9}' access.log | sort | uniq -c | sort -rn
همه‌ی ۴۰۴ ها awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn
مرورگرها awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn
خطاهای سرور زنده tail -f access.log | grep '" 5[0-9][0-9] '
حجم داده به ازای IP awk '{s[$1] += $10} END {for (i in s) print s[i], i}' access.log | sort -rn

directive ها:

directive نمونه
access_log access_log /var/log/nginx/shop.log timed; / access_log off; / ... if=$loggable;
error_log error_log /var/log/nginx/shop.error.log warn;
log_format (فقط در http) log_format timed '... rt=$request_time';
log_not_found log_not_found off; (۴۰۴ در error log ثبت نشود)
map ساختن متغیر شرطی برای if=

tail -f هم مثل Nginx فایل را با inode دنبال می‌کند؛ بعد از چرخش، به فایل قدیمی (.1) چسبیده و چیز تازه‌ای نشان نمی‌دهد. راه‌حل: tail -F (F بزرگ) که اسم فایل را دنبال می‌کند و بعد از چرخش، فایل تازه را باز می‌کند.

پشت پرده: بعد از mv، Nginx در فایل قدیمی می‌نویسد. راه‌حل: sudo nginx -s reopen (یا بگذار logrotate کارش را بکند).

Terminal window
cat > /etc/nginx/conf.d/lx-bad-format.conf <<'EOF'
server {
listen 8083;
log_format mine '$remote_addr $status';
}
EOF
nginx -t 2>&1 | grep emerg | sed -E 's/^[0-9/]+ [0-9:]+ //'
rm /etc/nginx/conf.d/lx-bad-format.conf
خروجی
[emerg] 21064#21064: "log_format" directive is not allowed here in /etc/nginx/conf.d/lx-bad-format.conf:3

log_format فقط در context http مجاز است. راه‌حل: تعریف در conf.d/ (که داخل http include می‌شود) و استفاده با اسمش در server یا location.

Terminal window
cd /var/log/nginx
echo "بدون sort (غلط): $(awk '{print $1}' access.log | uniq -c | wc -l) گروه"
echo "با sort (درست): $(awk '{print $1}' access.log | sort | uniq -c | wc -l) گروه"
خروجی
بدون sort (غلط): 9 گروه
با sort (درست): 8 گروه

uniq فقط تکرارهای پشت سر هم را یکی می‌کند؛ بدون sort همان IP در چند گروه جدا شمرده می‌شود.

بدون چرخش (یا وقتی logrotate خراب است) یا با لاگ debug روشن، لاگ‌ها می‌توانند دیسک را پر کنند و آن‌وقت Nginx و بقیه‌ی سرویس‌ها هم مشکل پیدا می‌کنند. راه‌حل: du -sh /var/log/nginx، sudo logrotate -d /etc/logrotate.d/nginx (اجرای آزمایشی، بدون تغییر) و برای health check ها access_log off یا if=.

✎ تمرینآسان

تمرین اصلی درس (بخش اول): از access.log این ماشین، پنج IP پرتکرار و پنج صفحه‌ی پرتکرار را دربیاور، و بگو کدام IP بیشترین ۴۰۴ را داشته است.

دیدن جواب
Terminal window
cd /var/log/nginx
echo "=== پنج IP پرتکرار:"
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5
echo "=== پنج صفحه‌ی پرتکرار:"
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -n 5
echo "=== بیشترین ۴۰۴:"
awk '$9 == 404 {print $1}' access.log | sort | uniq -c | sort -rn | head -n 1
خروجی
=== پنج IP پرتکرار:
36 127.0.0.66
6 127.0.0.99
5 127.0.0.1
4 127.0.0.12
4 127.0.0.11
=== پنج صفحه‌ی پرتکرار:
16 /index.html
13 /blog/nginx-intro.html
13 /about.html
3 /pricing.html
2 /missing-page
=== بیشترین ۴۰۴:
6 127.0.0.99

سه بار همان الگو: «فیلد مورد نظر را بیرون بکش، مرتب کن، بشمار، بر اساس عدد مرتب کن». با تغییر شماره‌ی فیلد و شرط awk، جواب تقریباً هر سؤالی را از لاگ درمی‌آوری.

✎ تمرینمتوسط

تمرین اصلی درس (بخش دوم): یک اسکریپت log-report.sh بنویس که مسیر یک فایل لاگ (قالب combined) را بگیرد و گزارشی با این بخش‌ها چاپ کند: تعداد کل درخواست‌ها، تعداد IP یکتا، درصد درخواست‌های موفق (۲xx)، سه IP و سه صفحه‌ی پرتکرار، و فهرست IP هایی که بیش از ۵ درخواست ۴۰۴ داشته‌اند (مشکوک به اسکن).

دیدن جواب
cat > /usr/local/bin/log-report.sh <<'EOF'
#!/usr/bin/env bash
# log-report.sh: summary of an nginx access log in the combined format
set -euo pipefail
log=${1:?usage: log-report.sh <access.log>}
[[ -r $log ]] || { echo "cannot read $log" >&2; exit 1; }
total=$(wc -l < "$log")
uniq_ips=$(awk '{print $1}' "$log" | sort -u | wc -l)
ok=$(awk '$9 ~ /^2/' "$log" | wc -l)
echo "Report for $log"
echo " requests: $total"
echo " unique IPs: $uniq_ips"
awk -v ok="$ok" -v t="$total" 'BEGIN { printf " 2xx: %.1f%%\n", (t ? ok * 100 / t : 0) }'
echo " top IPs:"
awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -n 3 | sed 's/^/ /'
echo " top pages:"
awk '{print $7}' "$log" | sort | uniq -c | sort -rn | head -n 3 | sed 's/^/ /'
echo " suspicious (more than 5 x 404):"
awk '$9 == 404 {c[$1]++} END {for (ip in c) if (c[ip] > 5) print " " ip, c[ip] " not found"}' "$log"
EOF
chmod +x /usr/local/bin/log-report.sh
log-report.sh /var/log/nginx/access.log
خروجی
Report for /var/log/nginx/access.log
requests: 61
unique IPs: 8
2xx: 78.7%
top IPs:
36 127.0.0.66
6 127.0.0.99
5 127.0.0.1
top pages:
16 /index.html
13 /blog/nginx-intro.html
13 /about.html
suspicious (more than 5 x 404):
127.0.0.99 6 not found

آرایه‌ی c[$1]++ در awk برای هر IP شمارنده‌ی جدا نگه می‌دارد (مثل آرایه‌ی انجمنی Bash) و در بلوک END فقط آن‌هایی که از ۵ بیشترند چاپ می‌شوند. printf "%.1f" درصد را با یک رقم اعشار نشان داد. این اسکریپت را می‌شود هر شب با cron اجرا کرد و نتیجه را ایمیل کرد (دوره‌ی Bash).

✎ تمرینسخت

برای shop.test یک لاگ جدا فقط برای درخواست‌های کند بساز: هر درخواستی که بیش از نیم ثانیه طول کشیده در /var/log/nginx/shop.slow.log ثبت شود (بقیه نه). راهنمایی: با map روی $request_time و یک regex، متغیر شرطی بساز و با access_log ... if= استفاده کن.

دیدن جواب
Terminal window
cat > /etc/nginx/conf.d/slow-log.conf <<'EOF'
map $request_time $is_slow {
~^0\.[0-4] 0;
~^0\. 1;
default 1;
}
EOF
sed -i 's| error_log /var/log/nginx/shop.error.log warn;| access_log /var/log/nginx/shop.slow.log timed if=$is_slow;\n error_log /var/log/nginx/shop.error.log warn;|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for p in / /api/products /api/report /api/products; do curl -s -o /dev/null "http://shop.test$p"; done
echo "=== slow log:"
cat /var/log/nginx/shop.slow.log | sed -E 's/\[[^]]+\] //'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
=== slow log:
127.0.0.1 - "GET /api/report HTTP/1.1" 200 13 host=shop.test rt=1.001 urt=1.001

$request_time یک رشته مثل 0.003 یا 1.004 است. map با regex: اگر با 0.0 تا 0.4 شروع شود، سریع (0)؛ هر 0. دیگر (۰.۵ تا ۰.۹) یا عدد ۱ به بالا، کند (1). فقط /api/report (حدود یک ثانیه) در لاگ کند آمد. (دقت کن مقدار $request_time در لحظه‌ی نوشتن لاگ حساب می‌شود، یعنی پایان درخواست؛ پس این کار درست است.)

⚡ بررسی سریع

کاربری صفحه‌ی ۴۰۴ دیده است. این درخواست حتماً در کدام لاگ ثبت شده است؟

؟ آزمونک
  1. در قالب combined، فیلد نهم (با جداکننده‌ی فاصله) چیست؟

  2. چرا awk '{print $1}' access.log | uniq -c نتیجه‌ی غلط می‌دهد؟

  3. لاگ را دستی با mv چرخاندی و Nginx هنوز در فایل قدیمی می‌نویسد. چرا و راه‌حل؟

  4. می‌خواهی ببینی هر درخواست چقدر طول کشیده و چقدرش مال اپ پشتی بوده. کدام متغیرها؟

  5. log_format را داخل یک server block نوشته‌ای و nginx -t خطا می‌دهد. چرا؟

  6. tail -f access.log بعد از نیمه‌شب دیگر چیزی نشان نمی‌دهد ولی سایت کار می‌کند. چرا؟

  • access log هر درخواست یک خط (/var/log/nginx/access.log)؛ error log مشکل‌ها با سطح (debug … emerg). ۴۰۴ همیشه در access log است و در error log فقط وقتی open() شکست بخورد (نه با try_files)؛ ۵۰۲ با connect() failed ... upstream در error log.
  • قالب combined: IP، زمان، "درخواست"، کد، بایت، "ارجاع"، "مرورگر"؛ با awk فیلدهای ۱ (IP)، ۷ (مسیر)، ۹ (کد) و با -F'"' فیلد ۶ (User-Agent).
  • الگوی تحلیل: awk '{print $N}' | sort | uniq -c | sort -rn | head؛ شرط با awk '$9 == 404 {...}'.
  • log_format (فقط در http) برای قالب خودت: $request_time، $upstream_response_time، $host؛ escape=json برای لاگ JSON. لاگ جدا برای هر سایت با access_log و error_log در server.
  • access_log off و if=$var (با map) برای حذف لاگ‌های بی‌اهمیت.
  • tail -f برای زنده و tail -F برای بعد از چرخش.
  • logrotate روزانه می‌چرخاند و با nginx -s reopen (USR1) فایل‌ها را دوباره باز می‌کند؛ Nginx با inode می‌نویسد.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
sudo tail -f /var/log/nginx/access.logلاگ زنده
sudo tail -F /var/log/nginx/access.logزنده، حتی بعد از چرخش
awk '{print $1}' access.log | sort | uniq -c | sort -rn | headپرتکرارترین IPها
awk '{print $7}' access.log | sort | uniq -c | sort -rn | headپرتکرارترین صفحه‌ها
awk '{print $9}' access.log | sort | uniq -c | sort -rnتوزیع کدهای وضعیت
awk '$9 == 404 {print $1, $7}' access.log | sort | uniq -c | sort -rn۴۰۴ ها با IP
log_format timed '... rt=$request_time urt=$upstream_response_time';قالب با زمان پاسخ (در http)
access_log /var/log/nginx/shop.access.log timed;لاگ جدا برای یک سایت
access_log off;خاموش کردن لاگ (مثلاً favicon)
access_log ... if=$loggable;لاگ شرطی (با map)
error_log /var/log/nginx/error.log warn;فقط warn و بالاتر
sudo nginx -s reopenباز کردن دوباره‌ی فایل‌های لاگ
sudo logrotate -d /etc/logrotate.d/nginxاجرای آزمایشی logrotate