Access & Error Logs
nginx log format কাস্টমাইজ করুন, প্রতিটি field-এর মানে বুঝুন, status বা duration দিয়ে request খুঁজুন, আর সঠিকভাবে access log আর error log-এর পার্থক্য করুন।
গল্পে বুঝি
খোয়ারিজমি বাগদাদের একটা বড় দালানের দারোয়ান। তার হাতে সবসময় দুটো খাতা থাকে। প্রথমটা ভিজিটর খাতা — দালানে যে-ই ঢোকে, প্রতিটা মানুষের নাম, সে কী চেয়েছে (কার সাথে দেখা করতে এসেছে বা কোন অফিসে গেছে), তাকে ঢুকতে দেওয়া হয়েছে নাকি ফিরিয়ে দেওয়া হয়েছে, আর ঠিক কখন — সব লাইন ধরে লেখা থাকে। দিনে হাজারখানেক লোক এলে হাজারটা লাইনই ওঠে, ভালো-মন্দ নির্বিশেষে। এই খাতা দেখে যে কেউ বলে দিতে পারে গতকাল দুপুরে কারা কারা এসেছিল আর কে কোথায় গিয়েছিল।
দ্বিতীয় খাতাটা আলাদা — ঘটনার খাতা। এখানে সব লেখা হয় না, শুধু গোলমাল লেখা হয়। লিফট আটকে গেল, কাউকে অনুমতি না থাকায় ঢুকতে দেওয়া গেল না, তিনতলায় আগুন-আগুন বলে হুড়োহুড়ি পড়ল — এসবই ঘটনার খাতায় ওঠে, সময় আর বিবরণসহ। কোনো দিন কিছু গোলমাল না হলে এই খাতা প্রায় ফাঁকাই থাকে। কিন্তু কোনো দিন গণ্ডগোল হলে সিনা এসে দুই খাতা পাশাপাশি রেখে মেলান — ভিজিটর খাতায় দেখেন ঠিক ওই সময়ে কারা এসেছিল, আর ঘটনার খাতায় দেখেন তখন কী বিপত্তি ঘটেছিল — দুটো মিলিয়ে গোটা দিনটা হুবহু পুনর্গঠন করে ফেলেন।
ঠিক এই দুটো খাতাই হলো nginx-এর দুই log। ভিজিটর খাতা হলো access log — প্রতিটা request-এর জন্য একটা লাইন, কে (IP), কী চেয়েছে (request line), সাড়া কী পেয়েছে (status code), আর কখন (time)। ঘটনার খাতা হলো error log — শুধু ব্যর্থতা আর সতর্কতা, যেমন backend সাড়া দিল না বা কাউকে ঢুকতে দেওয়া গেল না। “সাইট কি চলছে, কারা এল” — এর উত্তর access log-এ; “ভাঙল কেন” — এর উত্তর error log-এ। আর দুটো একসাথে মেলানোটাই বাস্তবে হলো debugging আর traffic analysis: একটা slow বা ব্যর্থ request-এর সময় ধরে access log-এ কোন request-টা ছিল খুঁজে বের করে error log-এ তার আসল কারণ পাওয়া — ঠিক যেমন বড় সাইটগুলো একটা shared request ID দিয়ে দুই log জুড়ে ঘটনা পুরোপুরি সাজিয়ে ফেলে।
দুটি log, দুটি উদ্দেশ্য
nginx দুটি stream লেখে:
- access log — প্রতি HTTP request-এ একটি লাইন। কে, কী, কখন, status, size, duration। ডিফল্ট:
/var/log/nginx/access.log। - error log — অস্বাভাবিকতা। ব্যর্থ config reload, upstream connection failure, timeout, malformed request, denied request। ডিফল্ট:
/var/log/nginx/error.log।
“সাইট কি কাজ করছে?” — এর বেশিরভাগ উত্তর আসে access log থেকে। “এটা ভাঙল কেন?” — এর বেশিরভাগ উত্তর আসে error log থেকে। কোথায় দেখতে হবে তা জানলে সময় বাঁচে।
বাস্তব জীবনের উপমা
Access আর error log অনেকটা প্রতিটি ভিজিটরকে রেকর্ড করা security camera-র মতো — আপনি হয়তো লাইভ দেখেন না, কিন্তু যখন কিছু ভুল হয় আর ঘটনাগুলো পুনর্গঠন করতে হয় তখন এটা অমূল্য হয়ে ওঠে।
ডিফল্ট access log format
192.0.2.4 - - [04/May/2026:10:42:11 +0000] "GET /api/users HTTP/1.1" 200 1234 "https://example.com/" "Mozilla/5.0 ..." Field-গুলো, space দিয়ে আলাদা (quoting সহ):
| Field | Variable | মানে |
|---|---|---|
192.0.2.4 | $remote_addr | Client IP (বা সামনে proxy থাকলে proxy IP) |
- | $remote_user | Auth user (বিরল) |
[04/May/2026:10:42:11 +0000] | $time_local | Local timestamp |
"GET /api/users HTTP/1.1" | $request | Request line |
200 | $status | Response status |
1234 | $body_bytes_sent | Response body-র size bytes-এ |
"https://example.com/" | $http_referer | Referer header |
"Mozilla/5.0 ..." | $http_user_agent | User-Agent header |
এটি combined format — Apache-র সময় থেকে কার্যত স্ট্যান্ডার্ড। প্রতিটি log analyzer, GoAccess, AWStats, ELK, ইন্টারনেটের প্রতিটি grep pattern এর কাছাকাছি কিছু আশা করে।
ডিফল্ট কেন যথেষ্ট নয়
combined format-এ আপনার যা লাগবে এমন কিছু জিনিস নেই:
- Request duration (
$request_time) — এই request-টা মোট কতক্ষণ নিল? - Upstream response time (
$upstream_response_time) — backend কতক্ষণ নিল? - Backend pool member (
$upstream_addr) — কোন backend এটা সামলাল? - Upstream status (
$upstream_status) — backend কী ফেরত দিল (nginx যা ফেরত দিল তার বিপরীতে)? - Request ID (
$request_id) — nginx আর আপনার app-এর মধ্যে log correlate করার জন্য। - proxy-র মধ্য দিয়ে Real IP —
$http_x_forwarded_for।
একটি সমৃদ্ধ format ডিফাইন করুন:
http {
log_format main_ext
'$remote_addr - $remote_user [$time_iso8601] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" '
'rt=$request_time urt=$upstream_response_time '
'us=$upstream_status ua=$upstream_addr '
'rid=$request_id';
access_log /var/log/nginx/access.log main_ext;
} এখন প্রতিটি লাইনে duration, backend status, আর request ID থাকে:
192.0.2.4 - - [2026-05-04T10:42:11+00:00] "GET /api/users HTTP/1.1" 200 1234 "https://example.com/" "Mozilla/5.0" "10.0.0.5" rt=0.123 urt=0.115 us=200 ua=127.0.0.1:8080 rid=4f5d6e7a8b9c $time_local-এর বদলে $time_iso8601 parser আর grep window-র জন্য অনেক বেশি সুবিধাজনক।
$request_time বনাম $upstream_response_time বোঝা
দুটি timing field, প্রায়ই গুলিয়ে ফেলা হয়:
$request_time— এই request-এ nginx মোট যে সময় ব্যয় করেছে: request-এর প্রথম byte পাওয়া থেকে response-এর শেষ byte লেখা পর্যন্ত।$upstream_response_time— backend-এর সাথে কথা বলতে nginx যে সময় ব্যয় করেছে: backend-এ connect করা থেকে তার পূর্ণ response পাওয়া পর্যন্ত।
যদি $request_time, $upstream_response_time-এর চেয়ে অনেক বড় হয়, তাহলে সময়টা ব্যয় হয়েছে একটি slow client থেকে request পড়তে অথবা একটিকে response লিখতে। যদি এরা কাছাকাছি হয়, তাহলে backend ধীর ছিল।
JSON log format — আধুনিক পছন্দ
log aggregator-এ পাঠানোর জন্য JSON parse করা সহজ:
log_format json_main escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"http_user_agent":"$http_user_agent",'
'"http_referer":"$http_referer",'
'"request_id":"$request_id"'
'}';
access_log /var/log/nginx/access.log json_main; escape=json অত্যন্ত জরুরি — এটি field value-র মধ্যে থাকা quote আর special character সঠিকভাবে escape করে যাতে ফলাফল JSON valid হয়।
Output:
{"time":"2026-05-04T10:42:11+00:00","remote_addr":"192.0.2.4",...,"status":200,"request_time":0.123,...} jq query তুচ্ছ হয়ে যায়:
# All slow requests
sudo tail -f /var/log/nginx/access.log | jq 'select(.request_time > 1)'
# Status 5xx in the last hour
sudo cat /var/log/nginx/access.log | jq 'select(.status >= 500)'
# Per-endpoint p99 (rough)
jq -s 'group_by(.request_uri) | map({uri: .[0].request_uri, p99: (sort_by(.request_time)[(length*0.99|floor)].request_time)})' /var/log/nginx/access.log Conditional logging — noise বাদ দেওয়া
Health check, asset request, আর authenticated keep-alive ping signal-কে ডুবিয়ে দিতে পারে:
map $request_uri $loggable {
~^/health$ 0;
~^/metrics$ 0;
~^/favicon.ico$ 0;
default 1;
}
server {
access_log /var/log/nginx/access.log main_ext if=$loggable;
} if= হলো access_log-এর একটি feature। যখন variable-টা 0 বা খালি, লাইনটা বাদ পড়ে।
অথবা status অনুযায়ী বাদ দিন:
map $status $log_4xx {
~^[45] 1; # log 4xx and 5xx
default 0;
}
access_log /var/log/nginx/errors.log main_ext if=$log_4xx;
access_log /var/log/nginx/access.log main_ext; দুটি log file: একটায় সবকিছু, একটায় শুধু error। error file-টা ছোট আর grep-বান্ধব।
logrotate দিয়ে rotate করা
বাক্সের বাইরেই, /etc/logrotate.d/nginx:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 nginx 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
} প্রতিদিন rotation, 14 দিন রাখা, gzipped। postrotate hook nginx-কে বলে তার log file আবার খুলতে (যাতে সেটা renamed file-এ লিখতে না থাকে) nginx rotate init script-এর মাধ্যমে (যা master-কে USR1 পাঠায়)।
অপেক্ষা না করেই একটা rotation টেস্ট করুন:
sudo logrotate -fv /etc/logrotate.d/nginx
ls -la /var/log/nginx/ error log
ভিন্ন file, ভিন্ন format, কিছু ভাঙলে অনেক বেশি জরুরি:
2026/05/04 10:42:11 [error] 1234#1234: *5678 connect() failed (111: Connection refused) while connecting to upstream, client: 192.0.2.4, server: example.com, request: "GET /api/users HTTP/1.1", upstream: "http://127.0.0.1:8080/api/users", host: "example.com" এভাবে পড়ুন:
- Timestamp.
- Severity —
[debug],[info],[notice],[warn],[error],[crit],[alert],[emerg]। - PID আর TID.
- Internal request ID —
*5678। - error message.
- Context — client, server, request, upstream, host।
nginx.conf-এ severity কনফিগার করুন:
error_log /var/log/nginx/error.log warn; প্রতি server-এর জন্য আপনি একটি আলাদা error log রাখতে পারেন:
server {
server_name api.example.com;
error_log /var/log/nginx/api.error.log warn;
# ...
} সাধারণ error log entry — এদের মানে কী
upstream timed out (110: Connection timed out)— backend সময়মতো সাড়া দেয়নি।proxy_read_timeoutচেক করুন।connect() failed (111: Connection refused)— backend listen করছে না। আপনার app চালু আছে কিনা চেক করুন।upstream prematurely closed connection— backend ক্র্যাশ করেছে বা partial response দিয়েছে। backend-এর log চেক করুন।client intended to send too large body— request bodyclient_max_body_sizeছাড়িয়ে গেছে। ডিফল্ট 1MB।request rate exceeded— আপনারlimit_reqzone fire করেছে।SSL_do_handshake() failed— TLS negotiation ব্যর্থ হয়েছে। পুরনো client বা ভুল cert chain।worker_connections are not enough— আপনি per-worker connection limit-এ পৌঁছে গেছেন।worker_connectionsআরworker_rlimit_nofileবাড়ান।
কোনো tool ছাড়াই access log query করা
শুধু grep, awk, আর cut:
# Top 10 IPs by request count today
sudo grep "$(date +%d/%b/%Y)" /var/log/nginx/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head
# Top 10 requested paths
sudo awk '{print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head
# All 5xx responses today
sudo awk '$9 ~ /^5/' /var/log/nginx/access.log
# Slow requests (rt > 1 second) — assuming the rt= format above
sudo grep -oE 'rt=[0-9]+\.[0-9]+' /var/log/nginx/access.log \
| awk -F= '$2 > 1 {print}'
# Average response time per endpoint
sudo awk '{print $7, $11}' /var/log/nginx/access.log | sort \
| awk '{a[$1] += $2; c[$1]++} END { for (u in a) printf "%s %.3f\n", u, a[u]/c[u] }' \
| sort -k2 -rn | head log aggregator-এর কাছে যাওয়ার আগেই এই one-liner-গুলো “কী ঘটছে” ধরনের 80% প্রশ্নের উত্তর দেয়।
GoAccess — একটি real-time terminal dashboard
sudo apt install -y goaccess
sudo goaccess /var/log/nginx/access.log -c log format বেছে নিন (CCBS = Combined)। এটি একটি live dashboard তৈরি করে: top URL, top IP, status code, response size, OS/browser breakdown। কোনো setup নেই, কোনো JS নেই, কোনো infrastructure নেই — আপনার SSH session-এই চলে।
একটি public HTML report-এর জন্য:
sudo goaccess /var/log/nginx/access.log \
--log-format=COMBINED \
-o /var/www/example.com/stats.html host-এর বাইরে log পাঠানো
একটি single VPS তার নিজের log ধরে রাখে। box-এর সাথেই সেগুলো মরে যায়। real system-এর জন্য:
- Vector / Fluent Bit / Promtail — ছোট forwarder যা log file (বা journald) পড়ে আর একটি central destination-এ পাঠায় (Loki, Elasticsearch, S3, ClickHouse)।
- rsyslog TCP/TLS forwarding সহ — পুরনো কিন্তু rock-solid।
journalctl -o json -fএকটি forwarder-এর মধ্য দিয়ে pipe করা — যখন nginx file-এর বদলে journald-এ (stdout-এর মাধ্যমে) log করতে কনফিগার করা থাকে।
এই অধ্যায়ের জন্য নিয়ম হলো: local log-কে ঠান্ডা মাথায় query করতে পারা। একবার পারলে, সেগুলো export করা পাঁচ-লাইনের config।
Application log — আপনার app থেকে কী log করবেন
nginx request-এর envelope log করে। আপনার application content log করে — কোন business সিদ্ধান্ত নেওয়া হলো, কোন user কী করল, একটা 500 কেন হলো। এদের জোড়া লাগান:
- nginx আর app দুটোই একই
$request_idlog করে। - App JSON (বা অন্য structured format) log করে।
- App log stdout-এ যায়; journald সেগুলো সংগ্রহ করে; আপনি
journalctl -u myappদিয়ে query করেন।
একটি সাধারণ correlated debug session:
# Find a slow request in nginx
sudo cat /var/log/nginx/access.log | jq 'select(.request_time > 5)'
# Note the request_id
# Pull app logs for that request
journalctl -u myapp --since "10 min ago" | grep "<request_id>" রিক্যাপ
- Access log প্রতিটি request রেকর্ড করে; error log অস্বাভাবিকতা রেকর্ড করে। দুটোরই ডিফল্ট
/var/log/nginx/। - ডিফল্ট format হলো “combined”।
$request_time,$upstream_response_time,$upstream_addr,$request_idযোগ করুন। escape=jsonসহ JSON format হলো log aggregation-এর সবচেয়ে পরিষ্কার পথ।access_log-এif=দিয়ে noisy endpoint বাদ দিন। সহজ grep-এর জন্য error আলাদা file-এ split করুন।- logrotate দিয়ে rotate করুন; rotation-এর পর nginx-কে file descriptor আবার খুলতে বলুন।
- error log entry পড়ুন severity, message, আর context দিয়ে। বেশিরভাগ “সাইট ভেঙে গেছে” সমস্যা প্রথমে এখানেই দেখা যায়।
- cross-system debugging-এর জন্য shared
$request_idদিয়ে nginx log আর app log জোড়া লাগান।
পরের অধ্যায়: edge-এ caching — কীভাবে nginx backend-কে ছুঁয়েও না দেখে RAM থেকে আপনার 80% request-এর উত্তর দিতে পারে।