Akar Masalah: Schema Drift antara Nginx dan GoAccess

Perubahan direktif log_format pada konfigurasi Nginx sering dilakukan saat tim backend atau SRE membutuhkan metrik baru, seperti $request_time, upstream response header, atau tracing ID. Namun, perubahan ini jarang diuji bersama konfigurasi parser telemetri seperti GoAccess.

GoAccess mengandalkan direktif log-format statis yang memetakan token log baris demi baris menggunakan penanda posisi (seperti %h, %r, %s, atau %^). Jika posisi token bergeser atau format timestamp berubah tanpa sinkronisasi konfigurasi parser, GoAccess akan gagal memproses baris log baru. Hasilnya terjadi schema drift: parser secara diam-diam membuang rekaman data log (data loss), metrik trafik rontok, dan visualisasi analisis menjadi korup di lingkungan produksi.

Sinkronisasi Konfigurasi: Nginx vs GoAccess

Agar parsing akurat, setiap token pada Nginx wajib dipetakan secara simetris ke dalam format GoAccess. Perhatikan contoh format log custom berikut yang menambahkan response time server:

Konfigurasi Nginx (nginx.conf)

log_format custom_telemetry '$remote_addr - $remote_user [$time_local] '
                            '"$request" $status $body_bytes_sent '
                            '"$http_referer" "$http_user_agent" '
                            '$request_time';

access_log /var/log/nginx/access.log custom_telemetry;

Konfigurasi GoAccess (goaccess.conf)

time-format %T
date-format %d/%b/%Y
log-format %h %^ %^ [%d:%t %^] "%r" %s %b "%R" "%u" %T

Penjelasan pemetaan token:

  • %h: IP klien ($remote_addr).
  • %^: Mengabaikan field yang tidak diperlukan (- dan $remote_user).
  • %d:%t: Tanggal dan jam yang diekstrak dari format $time_local Nginx.
  • "%r": Request line (method, URI, protokol).
  • %s: Status kode HTTP ($status).
  • %b: Ukuran byte respon ($body_bytes_sent).
  • "%R": Referrer header ($http_referer).
  • "%u": User agent header ($http_user_agent).
  • %T: Response time dalam detik dengan resolusi milidetik ($request_time).

Test Harness: Menghasilkan Log Uji Sintetis di CI

Pola lazim pengujian format log adalah menggunakan unit test dummy, tetapi cara tersebut rentan melewatkan kasus riil (misal: encoding URL oleh Nginx atau perlakuan string kosong). Solusi paling deterministik: jalankan container Nginx resmi di runner CI, terapkan konfigurasi repositori, lalu kirim request sintetis menggunakan curl.

Berikut skrip harness sederhana untuk memicu berbagai skenario HTTP request:

#!/usr/bin/env bash
set -euo pipefail

NGINX_HOST="http://127.0.0.1:8080"

# 1. Request standar GET 200
curl -s -o /dev/null -A "Mozilla/5.0 SyntheticTest" "${NGINX_HOST}/healthz"

# 2. Request POST dengan payload
curl -s -o /dev/null -X POST -d '{"event":"ping"}' -H "Content-Type: application/json" "${NGINX_HOST}/api/v1/event"

# 3. Request 404 Not Found dengan referer
curl -s -o /dev/null -e "https://ref.example.com" "${NGINX_HOST}/non-existent-route"

# 4. Request dengan query string kompleks dan URL encoding
curl -s -o /dev/null "${NGINX_HOST}/search?q=nginx+ci&page=1"

# Pastikan buffer log Nginx telah ditulis ke disk
sync

Automated Validation dan Assertion Gate

GoAccess dapat dijalankan secara headless (tanpa antarmuka TUI) untuk memvalidasi log. Parameter penting yang harus digunakan adalah --invalid-requests-log dan opsi output JSON.

Gunakan script assert berikut sebagai failure-gate di pipeline CI:

#!/usr/bin/env bash
set -euo pipefail

LOG_PATH="/var/log/nginx/access.log"
CONF_PATH="./goaccess.conf"
INVALID_LOG="/tmp/goaccess_invalid.log"
REPORT_JSON="/tmp/goaccess_report.json"

echo "Menjalankan GoAccess validation..."

# Jalankan GoAccess dalam mode parsing JSON
goaccess "${LOG_PATH}" \
    --config-file="${CONF_PATH}" \
    --invalid-requests-log="${INVALID_LOG}" \
    --no-progress \
    -o "${REPORT_JSON}"

# Ekstrak data kegagalan parsing
FAILED_REQUESTS=$(jq '.general.failed' "${REPORT_JSON}")
VALID_REQUESTS=$(jq '.general.valid' "${REPORT_JSON}")

echo "Valid records parsed: ${VALID_REQUESTS}"
echo "Failed records: ${FAILED_REQUESTS}"

if [ "${FAILED_REQUESTS}" -gt 0 ]; then
    echo "[ERROR] Regresi parser terdeteksi! Terdapat baris log yang tidak cocok dengan skema GoAccess."
    echo "Daftar log yang gagal diparsing:"
    cat "${INVALID_LOG}"
    exit 1
fi

if [ "${VALID_REQUESTS}" -eq 0 ]; then
    echo "[ERROR] Tidak ada record log yang berhasil diparsing. Periksa apakah test harness berjalan."
    exit 1
fi

echo "[SUCCESS] Seluruh format log Nginx kompatibel dengan konfigurasi GoAccess."

Integrasi pada GitHub Actions Pipeline

Satukan test harness dan script assertion ke dalam satu workflow GitHub Actions:

name: Verify Nginx Log Schema

on:
  pull_request:
    paths:
      - 'nginx/**'
      - 'telemetry/goaccess.conf'

jobs:
  validate-log-format:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Install Tools
        run: |
          sudo apt-get update
          sudo apt-get install -y goaccess jq curl

      - name: Start Nginx Container
        run: |
          docker run -d --name test-nginx \
            -p 8080:80 \
            -v $(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
            -v $(pwd)/logs:/var/log/nginx \
            nginx:alpine
          sleep 2

      - name: Generate Synthetic Traffic
        run: |
          chmod +x ./scripts/generate_traffic.sh
          ./scripts/generate_traffic.sh

      - name: Assert Log Compatibility
        run: |
          # Beri hak akses baca ke direktori log yang dimount
          sudo chmod -R 755 ./logs
          chmod +x ./scripts/validate_goaccess.sh
          LOG_PATH="./logs/access.log" CONF_PATH="./telemetry/goaccess.conf" ./scripts/validate_goaccess.sh

Strategi Menjaga Backward-Compatibility

Saat memperbarui log Nginx di sistem skala besar, pertahankan kompatibilitas parser menggunakan prinsip berikut:

  • Append-Only Fields: Bila menggunakan log berbasis whitespace/delimeter teks, tambahkan token baru di ujung paling kanan baris log. Jangan pernah menyisipkan field di tengah format yang telah berjalan.
  • Gunakan Format JSON untuk Log Kompleks: Jika log memuat lebih dari 10 parameter atau field opsional (seperti auth token status, upstream response time berulang), migrasikan log_format Nginx ke format JSON native via escape=json. JSON parser lebih toleran terhadap urutan key baru.
  • Disiplin Penggunaan Flag %^: Gunakan token pengabaian %^ di GoAccess hanya untuk field metadata yang tidak akan pernah dianalisis, hindari mengabaikan identifier krusial yang dapat memecah integritas metrik.