توی این درس یاد میگیری از لاگهای 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): فقط وقتی چیزی غیرعادی رخ داد (در قفل بود، آسانسور خراب شد، کسی بیاجازه خواست وارد شود)، با درجهی اهمیت. برای آمار و «چه کسی چه کرد» سراغ دفتر اول میروی و برای «چرا خراب شد» سراغ دومی.
دو لاگ Nginx
Section titled “دو لاگ Nginx”| 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 |
| کاربرد | آمار، امنیت، کندی، رفتار کاربران | عیبیابی، مجوزها، خطای اپ پشتی |
مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده. برای اینکه لاگ واقعی و متنوع داشته باشیم، با curl از چند آدرس مبدأ مختلف (127.0.0.x؛ لینوکس اجازه میدهد از هر آدرس 127.* بهعنوان مبدأ استفاده کنی) و با User-Agent های مختلف ترافیک میسازیم؛ پس همهی خطهای لاگ این درس واقعیاند ولی کاربرانشان شبیهسازی شدهاند.
مثال ۱: اولین خطهای access log
Section titled “مثال ۱: اولین خطهای access log”یک درخواست موفق، یک ۴۰۴ و یک درخواست از «مرورگر»:
curl -s -o /dev/null http://localhost/curl -s -o /dev/null http://localhost/missing-pagecurl -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.log127.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 و سطحها
Section titled “مثال ۲: error log و سطحها”حالا ببینیم آن ۴۰۴ و چند مشکل دیگر در error log چطور ثبت میشوند:
mkdir -p /var/www/html/private && echo secret > /var/www/html/private/index.html && chmod 700 /var/www/html/privatecurl -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;}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -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-pagesed -E 's/^[0-9/]+ [0-9:]+ //' /var/log/nginx/error.logrm -rf /var/www/html/privatenginx: configuration file /etc/nginx/nginx.conf test is successfulproxy to a dead app: HTTP 502no 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، لاگ زنده
Section titled “مثال ۳: tail -f، لاگ زنده”tail -f خطهای تازه را همان لحظه که نوشته میشوند نشان میدهد؛ اولین ابزار وقتی میخواهی ببینی «همین الان چه میگذرد». در یک ترمینال tail -f اجرا میکنیم و همزمان چند درخواست میفرستیم:
tmux kill-server 2>/dev/nulltmux new-session -d -s live -x 120 -y 10 "tail -n 0 -f /var/log/nginx/access.log"sleep 1for p in /pricing /cart /checkout; do curl -s -o /dev/null --interface 127.0.0.31 "http://localhost$p" sleep 0.3donesleep 1tmux capture-pane -t live -p | grep -v '^$'tmux kill-session -t live127.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 “مثال ۴: ترافیک واقعی و تحلیل با ابزارهای لینوکس”حالا یک «روز کاری» کوچک میسازیم: کاربران عادی، یک ربات که صفحههای زیادی را میخزد، و یک اسکنر که دنبال فایلهای حساس میگردد. ابتدا فایلهای سایت:
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"donegen() { 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.htmlgen 127.0.0.12 "$ch" /index.html /blog/nginx-intro.html /blog/docker-tips.html /index.htmlgen 127.0.0.13 "$ch" /index.html /pricing.htmlfor 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; donegen 127.0.0.99 'python-requests/2.31' /.env /wp-login.php /.git/config /admin.php /phpmyadmin/ /backup.zipwc -l /var/log/nginx/access.log61 /var/log/nginx/access.logحالا تمرین اصلی درس، یعنی سؤالهای واقعی با یک خط فرمان. پرتکرارترین IPها (فیلد اول):
cd /var/log/nginxawk '{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) بالاترین است.
پرتکرارترین صفحهها (فیلد هفتم، مسیر درخواست):
cd /var/log/nginxawk '{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:
cd /var/log/nginxecho "=== کدهای وضعیت:"awk '{print $9}' access.log | sort | uniq -c | sort -rnecho "=== آدرسهای ۴۰۴ و چه کسی خواسته:"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 /cartawk '$9 == 404 {...}' فقط خطهایی را که فیلد نهمشان ۴۰۴ است پردازش میکند. نتیجه داستان را تعریف میکند: 127.0.0.99 (با python-requests) دنبال .env، .git/config، wp-login.php و phpmyadmin گشته؛ یک اسکنر خودکار که در لاگ هر سایت واقعی روی اینترنت هر روز میبینی.
مرورگرها و رباتها (User-Agent، که داخل کوتیشن است؛ پس با -F'"' جدا میکنیم):
cd /var/log/nginxawk -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):
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 عمداً یک ثانیه صبر میکند.)
mkdir -p /srv/lx-appcat > /srv/lx-app/app.py <<'EOF'# tiny test API: /api/report is slow on purposeimport timefrom 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.htmlcat > /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; }}EOFln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/sleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /api/products /api/report /api/products /api/report; do curl -s -o /dev/null "http://shop.test$p"donecat /var/log/nginx/shop.access.lognginx: configuration file /etc/nginx/nginx.conf test is successful127.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.001127.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.001127.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.001127.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.001rt($request_time) زمان کل از اولین بایت درخواست تا آخرین بایت پاسخ، به ثانیه با دقت میلیثانیه.urt($upstream_response_time) زمانی که اپ پشتی صرف کرد؛ برای فایل استاتیک-است (اپی در کار نبود).- این لاگ فقط مال
shop.testاست؛ لاگ سایتهای دیگر قاطی نمیشود.
کندترین درخواستها، با جدا کردن فیلد rt=:
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 31.001 /api/report1.001 /api/report0.001 /api/productsدو درخواست /api/report با بیش از یک ثانیه، بالای فهرست؛ همان «تیم برنامهنویسی، اینجا را ببینید» سناریوی اول درس.
مثال ۶: لاگ JSON برای ابزارهای تحلیل
Section titled “مثال ۶: لاگ JSON برای ابزارهای تحلیل”ابزارهای جمعآوری لاگ (مثل ELK، Loki یا Graylog) JSON را خیلی راحتتر از متن آزاد میخوانند. escape=json مقدارها را برای JSON امن میکند (کوتیشن و کاراکترهای خاص):
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"}';EOFsed -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.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -o /dev/null -A 'Tool "with quotes"' 'http://shop.test/api/products?id=7'tail -n 1 /var/log/nginx/shop.access.jsonecho "--- با jq:"jq -c '{uri, status, rt}' /var/log/nginx/shop.access.jsonnginx: 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 که هر ۵ ثانیه میآید) لاگ را پر میکنند. دو راه:
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"; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in /health /health /favicon.ico /page /health; do curl -s -o /dev/null "http://localhost:8082$p"; doneawk '{print $7, $9}' /var/log/nginx/lx-health.logrm /etc/nginx/conf.d/lx-health.conf /var/log/nginx/lx-health.logsystemctl reload nginxnginx: configuration file /etc/nginx/nginx.conf test is successful/page 200از پنج درخواست، فقط /page ثبت شد. map یک متغیر تازه ($loggable) از روی یک متغیر دیگر میسازد (اینجا ۰ برای /health، ۱ برای بقیه) و if=$loggable یعنی «فقط وقتی مقدار ۰ یا خالی نیست، ثبت کن». برای یک location مشخص هم access_log off; سادهترین راه است (favicon).
پشت پرده: چرخش لاگ و inode
Section titled “پشت پرده: چرخش لاگ و inode”لاگها بینهایت رشد میکنند، پس logrotate (که هر روز با cron یا systemd timer اجرا میشود) آنها را جابهجا، فشرده و قدیمیها را پاک میکند. تنظیم Nginx در Ubuntu:
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 (شناسهی واقعی فایل روی دیسک) در آن مینویسد، نه با اسمش. این را ببین:
cd /var/log/nginxls -i shop.access.logmv shop.access.log shop.access.log.1curl -s -o /dev/null http://shop.test/after-renameecho "--- بعد از mv و یک درخواست تازه:"ls -i shop.access.log* 2>&1tail -n 1 shop.access.log.1 | awk '{print $5, $6}'echo "--- سیگنال reopen (USR1) به master:"nginx -s reopensleep 1curl -s -o /dev/null http://shop.test/after-reopenls -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 started704597 shop.access.log704594 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 یعنی لاگی که به فایل اشتباه میرود.
جدولهای مرجع
Section titled “جدولهای مرجع”متغیرهای پرکاربرد برای 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= |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) tail -f بعد از چرخش لاگ
Section titled “۱) tail -f بعد از چرخش لاگ”tail -f هم مثل Nginx فایل را با inode دنبال میکند؛ بعد از چرخش، به فایل قدیمی (.1) چسبیده و چیز تازهای نشان نمیدهد. راهحل: tail -F (F بزرگ) که اسم فایل را دنبال میکند و بعد از چرخش، فایل تازه را باز میکند.
۲) چرخش دستی بدون reopen
Section titled “۲) چرخش دستی بدون reopen”پشت پرده: بعد از mv، Nginx در فایل قدیمی مینویسد. راهحل: sudo nginx -s reopen (یا بگذار logrotate کارش را بکند).
۳) log_format در جای اشتباه
Section titled “۳) log_format در جای اشتباه”cat > /etc/nginx/conf.d/lx-bad-format.conf <<'EOF'server { listen 8083; log_format mine '$remote_addr $status';}EOFnginx -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:3log_format فقط در context http مجاز است. راهحل: تعریف در conf.d/ (که داخل http include میشود) و استفاده با اسمش در server یا location.
۴) uniq -c بدون sort
Section titled “۴) uniq -c بدون sort”cd /var/log/nginxecho "بدون 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 در چند گروه جدا شمرده میشود.
۵) پر شدن دیسک با لاگ
Section titled “۵) پر شدن دیسک با لاگ”بدون چرخش (یا وقتی 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 بیشترین ۴۰۴ را داشته است.
دیدن جواب
cd /var/log/nginxecho "=== پنج IP پرتکرار:"awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5echo "=== پنج صفحهی پرتکرار:"awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -n 5echo "=== بیشترین ۴۰۴:"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 formatset -euo pipefaillog=${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"EOFchmod +x /usr/local/bin/log-report.shlog-report.sh /var/log/nginx/access.logReport 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= استفاده کن.
دیدن جواب
cat > /etc/nginx/conf.d/slow-log.conf <<'EOF'map $request_time $is_slow { ~^0\.[0-4] 0; ~^0\. 1; default 1;}EOFsed -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.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /api/products /api/report /api/products; do curl -s -o /dev/null "http://shop.test$p"; doneecho "=== 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 در لحظهی نوشتن لاگ حساب میشود، یعنی پایان درخواست؛ پس این کار درست است.)
آزمونک
Section titled “آزمونک”کاربری صفحهی ۴۰۴ دیده است. این درخواست حتماً در کدام لاگ ثبت شده است؟
access log همهی درخواستها را دارد. در error log فقط وقتی میآید که Nginx واقعاً سعی کرده فایل را باز کند (بدون try_files) و log_not_found خاموش نباشد.
در قالب combined، فیلد نهم (با جداکنندهی فاصله) چیست؟
فیلد ۱ IP، ۷ مسیر، ۹ کد وضعیت، ۱۰ بایتها.
چرا awk '{print $1}' access.log | uniq -c نتیجهی غلط میدهد؟
الگوی کامل: sort | uniq -c | sort -rn.
لاگ را دستی با mv چرخاندی و Nginx هنوز در فایل قدیمی مینویسد. چرا و راهحل؟
logrotate بعد از چرخش همین را با invoke-rc.d nginx rotate انجام میدهد.
میخواهی ببینی هر درخواست چقدر طول کشیده و چقدرش مال اپ پشتی بوده. کدام متغیرها؟
با log_format سفارشی ثبتشان کن.
log_format را داخل یک server block نوشتهای و nginx -t خطا میدهد. چرا؟
در conf.d تعریفش کن و در server با اسم استفاده کن.
tail -f access.log بعد از نیمهشب دیگر چیزی نشان نمیدهد ولی سایت کار میکند. چرا؟
tail -F اسم فایل را دنبال میکند.
جمعبندی
Section titled “جمعبندی”- 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 |