Format Log Kustom #
Format log default combined yang digunakan oleh Nginx sangat baik untuk kebutuhan dasar, namun seiring bertambahnya kompleksitas infrastruktur aplikasi kita, kebutuhan akan informasi yang lebih mendalam menjadi mutlak. Kita sering kali membutuhkan data tentang seberapa cepat backend merespons permintaan, apakah konten disajikan dari cache lokal atau harus diambil ulang dari hulu (upstream), serta bagaimana melacak sebuah transaksi dari browser pengguna hingga ke mikrolayanan (microservices) terdalam.
Untungnya, Nginx dirancang dengan tingkat fleksibilitas konfigurasi yang sangat tinggi. Menggunakan direktif log_format, kita dapat merancang pola pencatatan informasi apa pun yang didukung oleh variabel internal Nginx. Di artikel ini, kita akan membahas cara mendefinisikan format log kustom, merekam metrik performa secara presisi, menyusun format JSON yang valid untuk log aggregator modern menggunakan parameter escape=json, serta mengimplementasikan distributed tracing menggunakan $request_id.
Sintaksis Dasar Direktif log_format
#
Direktif log_format hanya dapat dideklarasikan di dalam konteks http global. Kita tidak bisa menuliskan direktif ini secara langsung di dalam blok server atau location. Namun, setelah format dideklarasikan di level http, kita dapat menggunakannya di server block mana pun dengan memanggil namanya pada direktif access_log.
Sintaksis Penulisan #
http {
log_format nama_format [escape=default|json|none] string_format;
}
nama_format: Nama pengenal unik kustom yang akan kita panggil pada direktifaccess_log. Kita tidak boleh menggunakan nama format bawaan sistem seperticombined.escape: Menentukan metode penyandian (escaping) untuk karakter-karakter khusus (seperti karakter kutip, tab, atau baris baru) yang ditemukan di dalam nilai variabel. Pilihan terbaik untuk format teks biasa adalahdefault, sedangkan untuk format log JSON kita wajib menggunakanjson.string_format: Kumpulan teks dan variabel Nginx yang menyusun pola baris log yang akan dihasilkan.
Berikut adalah contoh deklarasi sederhana dan pemanggilannya:
http {
# Deklarasi format kustom bernama 'sederhana'
log_format sederhana '$remote_addr - $status - $request';
server {
listen 80;
server_name example.com;
# Pemanggilan format kustom di server block
access_log /var/log/nginx/example_sederhana.log sederhana;
}
}
Menambahkan Timing Variabel untuk Monitoring Performa #
Salah satu skenario kustomisasi paling penting di lingkungan produksi adalah mencatat metrik waktu (timing metrics). Dengan data waktu ini, kita dapat memecahkan masalah degradasi performa aplikasi web secara objektif.
Nginx menyediakan beberapa variabel waktu krusial yang dapat kita masukkan ke dalam format log:
$request_time: Total waktu pemrosesan request dari bita pertama yang diterima dari klien hingga bita respons terakhir dikirimkan ke klien. Waktu ini diukur dalam hitungan detik dengan presisi milidetik. Waktu ini mencakup waktu transmisi data di jaringan, sehingga jika koneksi internet klien lambat, nilai variabel ini akan tinggi.$upstream_response_time: Waktu yang dibutuhkan oleh server backend (upstream) untuk memproses request dan mengirimkan respons kembali ke Nginx. Variabel ini murni mencerminkan performa aplikasi kita (seperti PHP, Node.js, Go) tanpa dipengaruhi oleh kecepatan internet klien.$upstream_connect_time: Waktu yang dibutuhkan Nginx untuk melakukan koneksi jaringan (TCP handshake) dengan server backend. Jika nilainya tinggi, kemungkinan terjadi kemacetan jaringan internal atau beban backend terlalu tinggi untuk menerima koneksi baru.$upstream_header_time: Waktu antara dimulainya koneksi ke backend hingga Nginx menerima bita pertama header respons dari backend.
Contoh Format Log Performa #
Kita dapat menyusun format log khusus untuk menguji performa server seperti ini:
http {
log_format performance '$remote_addr [$time_local] "$request" '
'status=$status bytes=$body_bytes_sent '
'req_time=$request_time up_resp_time=$upstream_response_time '
'up_conn_time=$upstream_connect_time cache=$upstream_cache_status';
server {
listen 80;
server_name api.unisbadri.com;
# Gunakan format performance untuk melacak latensi
access_log /var/log/nginx/api-performance.log performance;
}
}
Membaca Log Performa untuk Analisis Latensi #
Misalkan kita melihat entri log berikut pada berkas api-performance.log:
203.0.113.12 - [16/Jun/2026:13:30:00 +0700] "GET /api/data HTTP/1.1" status=200 bytes=1048576 req_time=4.120 up_resp_time=0.105 up_conn_time=0.002 cache=MISS
Dari baris di atas, kita dapat menganalisis kondisi performa berikut:
up_resp_time=0.105: Backend memproses data dengan sangat cepat, hanya membutuhkan waktu 105 milidetik.req_time=4.120: Namun, total waktu request selesai adalah 4,120 detik.- Analisis: Ada selisih waktu sekitar 4 detik. Karena ukuran data yang dikirim cukup besar (
bytes=1048576atau 1 MB), kita dapat menyimpulkan bahwa backend kita sehat, tetapi koneksi internet klien lambat dalam mengunduh data respons 1 MB tersebut.
Format JSON untuk Log Aggregator Modern #
Pada infrastruktur komputasi awan modern, kita jarang membaca file log secara manual langsung di server individual. Kita biasanya mengirimkan data log ke sistem pengumpul log terpusat (log aggregator) seperti ELK Stack (Elasticsearch, Logstash, Kibana), Grafana Loki, Datadog, Splunk, atau AWS CloudWatch.
Alat-alat log aggregator tersebut sangat menyukai format JSON karena strukturnya yang terorganisir dalam pasangan kunci dan nilai (key-value pairs). Ini mempermudah pengindeksan, pencarian, dan pembuatan dasbor visualisasi tanpa memerlukan penulisan aturan parsing regex (regex parser rules) yang rumit.
Urgensi Parameter escape=json
#
Saat membuat format log JSON, kita wajib menambahkan parameter escape=json pada deklarasi log_format.
Jika kita tidak mengaktifkannya, karakter-karakter khusus seperti tanda kutip ganda (") pada user agent atau path request yang dikirimkan klien akan ditulis apa adanya ke file log. Hal ini akan merusak struktur sintaksis JSON (menyebabkan invalid JSON format) dan membuat log aggregator gagal membaca data tersebut.
Dengan escape=json, Nginx akan otomatis mengubah karakter khusus menjadi string aman (misalnya mengubah " menjadi \" atau \ menjadi \\) sehingga struktur JSON dijamin selalu valid.
Konfigurasi Format JSON Produksi #
Berikut adalah konfigurasi log format JSON standar produksi yang sangat direkomendasikan:
http {
log_format json_combined escape=json
'{'
'"timestamp":"$time_iso8601",'
'"client_ip":"$remote_addr",'
'"request_id":"$request_id",'
'"method":"$request_method",'
'"scheme":"$scheme",'
'"host":"$host",'
'"uri":"$uri",'
'"query_string":"$args",'
'"status":$status,'
'"bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_cache_status":"$upstream_cache_status",'
'"referrer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"x_forwarded_for":"$http_x_forwarded_for"'
'}';
server {
listen 443 ssl;
server_name app.unisbadri.com;
# Simpan log dalam format JSON
access_log /var/log/nginx/app-access.json json_combined;
}
}
Tracing Terdistribusi menggunakan $request_id
#
Dalam arsitektur mikrolayanan (microservices), satu request dari pengguna di browser dapat memicu rantai pemanggilan API ke belasan layanan backend internal yang berbeda. Jika terjadi kegagalan di salah satu layanan di tengah rantai tersebut, melacak penyebabnya akan sangat sulit karena setiap layanan mencatat lognya sendiri-sendiri.
Untuk mengatasi tantangan ini, kita menggunakan konsep Distributed Tracing. Nginx, sebagai gerbang masuk utama (reverse proxy / API gateway), bertindak sebagai pihak yang membuat tanda pengenal unik untuk setiap request yang masuk, lalu meneruskan pengenal tersebut ke seluruh sistem backend.
Variabel $request_id
#
Nginx menyediakan variabel bawaan bernama $request_id (tersedia sejak Nginx versi 1.11.0). Variabel ini menghasilkan nilai string acak heksadesimal unik sepanjang 32 karakter untuk setiap request yang diproses.
Berikut adalah ilustrasi alur distributed tracing menggunakan request ID:
flowchart TD
Client["Browser Klien"] -->|"1. Kirim Request"| Nginx["Nginx Reverse Proxy<br/>(Generate $request_id)"]
Nginx -->|"2. Catat Log dengan request_id"| LogNginx["("Nginx Access Log")"]
Nginx -->|"3. Tambah header X-Request-ID"| Backend["Backend API Service<br/>(Node.js / Go)"]
Backend -->|"4. Minta Data"| DB["("Database Server")"]
Backend -->|"5. Catat Log dengan request_id"| LogBackend["("Backend App Log")"]
classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
classDef highlight fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a;
class Nginx,Backend highlight;Konfigurasi Distributed Tracing di Nginx #
Kita harus mengonfigurasi Nginx agar:
- Mencatat
$request_iddi access log kita. - Meneruskan
$request_idke server backend via header HTTPX-Request-ID. - Mengembalikan
$request_idke browser klien di header respons agar klien dapat menggunakannya sebagai referensi saat melaporkan kendala ke tim support.
http {
log_format trace_format '$remote_addr [$time_local] '
'req_id=$request_id status=$status '
'rt=$request_time urt=$upstream_response_time';
server {
listen 80;
server_name shop.unisbadri.com;
access_log /var/log/nginx/shop-access.log trace_format;
# 1. Sertakan Request ID ke respons header agar dapat dibaca oleh browser klien
add_header X-Request-ID $request_id always;
location / {
# 2. Teruskan Request ID ke aplikasi backend internal
proxy_set_header X-Request-ID $request_id;
proxy_pass http://shop_backend;
}
}
}
Di sisi backend aplikasi kita (misalnya menggunakan PHP, Node.js, atau Go), tim developer harus mengonfigurasi library logging aplikasi mereka untuk membaca header X-Request-ID dan menyertakannya di setiap baris log aplikasi maupun kueri database.
Dengan demikian, jika terjadi kegagalan transaksi, kita cukup menyalin Request ID yang dilaporkan oleh browser pengguna, lalu mencarinya di log aggregator. Seluruh aktivitas transaksi dari level Nginx, backend API, hingga kueri database akan terkorelasi secara instan menggunakan ID unik yang sama.
Mencatat HTTP Header Kustom (Request & Upstream) #
Nginx juga memungkinkan kita mencatat data dari header HTTP kustom yang dikirimkan oleh klien maupun yang dikembalikan oleh backend.
- Untuk mencatat header yang dikirimkan klien, gunakan awalan variabel
$http_diikuti nama header dalam huruf kecil, dengan tanda hubung (-) diganti menjadi garis bawah (_). - Untuk mencatat header yang dikembalikan oleh backend, gunakan awalan variabel
$upstream_http_dengan aturan penulisan yang sama.
Contoh Skenario Pencatatan Header #
Misalkan aplikasi kita menggunakan header kustom berikut:
- Klien mengirim header
X-Client-Versionuntuk mengidentifikasi versi aplikasi mobile. - Backend mengembalikan header
X-Process-Memoryuntuk melaporkan konsumsi memori aplikasi.
Kita dapat mencatat kedua informasi tersebut ke log Nginx kita seperti ini:
http {
log_format app_header_log '$remote_addr - [$time_local] '
'status=$status '
'client_ver=$http_x_client_version ' # Merujuk ke X-Client-Version dari klien
'backend_mem=$upstream_http_x_process_memory'; # Merujuk ke X-Process-Memory dari backend
server {
listen 80;
server_name test.example.com;
access_log /var/log/nginx/app-headers.log app_header_log;
location / {
proxy_pass http://app_backend;
}
}
}
Pencatatan header kustom ini sangat membantu tim operasional kita dalam melacak bug spesifik pada versi aplikasi mobile tertentu tanpa perlu mengubah kode backend aplikasi.
Penyaringan Data Sensitif dalam Log (Data Masking & Anonymization) #
Di era regulasi perlindungan data yang ketat seperti Undang-Undang Pelindungan Data Pribadi (UU PDP) di Indonesia atau GDPR di Uni Eropa, mencatat data sensitif pengguna ke dalam berkas log adalah pelanggaran kepatuhan yang serius. Data seperti alamat IP lengkap, token otorisasi, kata sandi di dalam query string, atau nomor kartu kredit tidak boleh disimpan dalam bentuk teks biasa di dalam berkas log yang dapat diakses oleh banyak pihak.
Kita dapat memanfaatkan kekuatan direktif map di Nginx untuk menyaring (masking) atau menyamarkan (anonymize) data tersebut sebelum ditulis ke file log.
1. Anonimisasi Alamat IP Klien #
Untuk melindungi privasi pengguna, kita bisa menyamarkan oktet terakhir dari alamat IPv4 atau segmen akhir dari alamat IPv6 menggunakan regex di dalam blok map:
http {
# Samarkan oktet terakhir dari IPv4 dan setengah segmen IPv6
map $remote_addr $ip_anonymized {
# Contoh IPv4: 203.0.113.88 menjadi 203.0.113.0
~^(?P<first>\d+\.\d+\.\d+)\.\d+$ $first.0;
# Contoh IPv6: 2001:db8:85a3::8a2e:370:7334 menjadi 2001:db8:85a3::
~^(?P<first>[0-9a-fA-F:]+:[0-9a-fA-F:]+:[0-9a-fA-F:]+):[0-9a-fA-F:]+$ $first::;
default 0.0.0.0;
}
log_format anonymous_log '$ip_anonymized - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent';
}
Dengan menggunakan $ip_anonymized menggantikan $remote_addr pada deklarasi log_format, kita tetap dapat menganalisis wilayah geografis makro dari pengunjung situs kita tanpa menyimpan informasi identifikasi pribadi (Personally Identifiable Information - PII) mereka secara spesifik.
2. Sensor Token Otorisasi (Bearer Token)
#
Jika aplikasi kita menggunakan otentikasi berbasis token (JWT) yang dikirimkan via header Authorization, mencatat header tersebut secara mentah akan membocorkan kredensial akses pengguna. Kita bisa menyensor token tersebut dan hanya menyisakan beberapa karakter pertamanya:
http {
# Sensor token otorisasi bearer
map $http_authorization $masked_authorization {
# Jika header berisi "Bearer abcdef12345...", ubah menjadi "Bearer abcdef***"
~^Bearer\s+(?P<prefix>.{6}).*$ "Bearer $prefix***";
# Jika kosong, tetap kosong
"" "";
# Selain itu, sensor total
default "***";
}
log_format security_audit_log '$remote_addr [$time_local] '
'status=$status auth="$masked_authorization"';
}
3. Menyensor Parameter Sensitif pada URL (Query String) #
Sering kali aplikasi backend yang kurang aman mengirimkan token akses atau data sensitif melalui parameter URL (misalnya /reset-password?token=secret123). Untuk mencegah token ini tercatat di dalam variabel $request atau $args, kita dapat mendefinisikan penyaringan URL kustom:
http {
# Bersihkan token rahasia dari query string
map $request_uri $filtered_request_uri {
# Cari parameter 'token=...' atau 'password=...' dan ganti nilainya dengan [REDATED]
~^(?P<path>[^?]+)\?(?P<query1>.*)(?:token|password)=[^&]+(?P<query2>.*)$ $path?$query1token=[REDACTED]$query2;
# Jika tidak mengandung parameter sensitif, gunakan URL asli
default $request_uri;
}
log_format safe_url_log '$remote_addr - [$time_local] "$request_method $filtered_request_uri" $status';
}
Integrasi Log Aggregator (Grafana Loki & Filebeat) #
Mengumpulkan log di disk lokal server hanyalah langkah awal. Untuk infrastruktur produksi berskala besar, kita harus mengirimkan log tersebut ke log aggregator. Ada dua pendekatan arsitektur utama untuk mengirimkan log dari Nginx:
Pendekatan 1: Log Shipper Agent (Rekomendasi Industri) #
Pada pendekatan ini, Nginx tetap menulis log dalam format JSON ke berkas lokal di disk. Kemudian, aplikasi agen pengirim log (log shipper agent) seperti Promtail (untuk Grafana Loki), Filebeat (untuk Elasticsearch), atau Vector berjalan di latar belakang server untuk membaca (tail) perubahan berkas log tersebut dan mengirimkannya ke server pusat.
flowchart LR
A["Nginx JSON<br/>Log Engine"] -->|"Tulis ke"| B["Log File<br/>on Disk"]
C["Log Shipper Agent<br/>(Promtail/Filebeat)"] -->|"Membaca"| B
C -->|"Mengirim"| D["Central Aggregator<br/>(Loki/Elasticsearch)"]Kelebihan sistem ini adalah keandalan yang tinggi. Jika server pusat log sedang mengalami gangguan (downtime), agen pengirim log akan mencatat posisi terakhir pembacaan berkas dan akan mengirimkan kembali sisa antrean log setelah server pusat aktif kembali (backpressure handling).
Pendekatan 2: Pengiriman Langsung via Syslog Protokol #
Jika kita ingin menghindari penggunaan I/O disk lokal sama sekali demi alasan performa atau keamanan disk, Nginx mendukung pengiriman entri log secara langsung menggunakan protokol Syslog melalui jaringan UDP atau TCP:
http {
# Kirim log langsung ke server log terpusat via Syslog UDP port 514
access_log syslog:server=10.0.0.50:514,facility=local7,tag=nginx,severity=info json_combined;
}
server=10.0.0.50:514: Menentukan alamat IP dan port server penerima Syslog.facility=local7: Kategori fasilitas Syslog yang digunakan (umumnyalocal0hinggalocal7untuk aplikasi kustom).tag=nginx: Tag identifikasi untuk mempermudah penyaringan log di server penerima.severity=info: Menentukan level keparahan pengiriman pesan log.
Kekurangan dari pendekatan Syslog UDP adalah tidak adanya jaminan pengiriman (fire-and-forget). Jika jaringan internal kita mengalami kongesti, ada kemungkinan beberapa entri log akan hilang di tengah jalan tanpa terdeteksi oleh Nginx.
Ringkasan dan Praktik Terbaik #
- Gunakan escape=json untuk Log JSON: Pastikan parameter
escape=jsonselalu aktif saat mendefinisikan format log JSON agar log tidak rusak oleh karakter khusus.- Pantau Selisih Waktu: Selalu sertakan variabel
$request_timedan$upstream_response_timeuntuk mengukur performa jaringan vs performa backend aplikasi kita.- Implementasikan Request ID: Gunakan variabel
$request_iduntuk distributed tracing guna menyatukan korelasi log Nginx dengan log aplikasi backend secara instan.- Anonimkan PII (Personally Identifiable Information): Manfaatkan direktif
mapuntuk menyamarkan alamat IP klien dan menyensor token otentikasi sensitif sebelum ditulis ke disk.- Grupkan Deklarasi di http: Seluruh direktif
log_formatwajib ditulis di dalam blokhttpglobal, bukan di dalam blokserverataulocation.