Ketika satu request HTTP berjalan menembus middleware, handler, dan dua layanan hilir, sebuah insiden menghasilkan lusinan baris log dari berbagai tempat sekaligus. Tanpa penanda bersama, baris-baris tersebut bercampur dengan log request lain yang berlangsung pada saat bersamaan. Request ID adalah benang merahnya: satu identifier unik dibuat di titik masuk, disimpan agar semua fungsi dapat mengaksesnya, dan dicantumkan pada setiap baris log serta setiap panggilan antar layanan. Pada chapter ini kita membangun mekanisme tersebut dari nol dengan context, middleware, dan header HTTP.

Anatomi Mekanisme #

Alur lengkapnya berdiri di atas tiga komponen:

  1. Middleware di pintu masuk membaca header X-Request-ID; bila kosong, middleware membuat ID baru, lalu menyalin ID tersebut ke header respons agar klien juga mengetahuinya.
  2. Penyimpanan di context memakai context.WithValue dengan kunci bertipe struct yang tidak diekspor — pola dari chapter context yang mencegah kode lain menabrak nilai secara tidak sengaja.
  3. Propagasi lintas layanan: setiap panggilan HTTP keluar menempelkan ID yang sama pada header X-Request-ID, sehingga layanan hilir mencatat log dengan identifier yang identik.

Setelah mekanisme ini berjalan, satu perintah grep req-001 pada seluruh log gabungan memunculkan jalur lengkap request tersebut — inilah korelasi lintas komponen yang menjadi tujuan akhirnya.

Program Demo #

Berkas reqid-demo.go membangun dua layanan kecil lewat httptest: gateway yang mengenakan middleware request ID, dan layanan pesanan yang menerimanya. Klien melakukan dua request — pertama tanpa header (gateway membuat req-001), kedua dengan header X-Request-ID: ext-9f2c seolah berasal dari layanan upstream.

go
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"sync/atomic"
)

// ctxKunciRequestID adalah tipe kunci context yang tidak diekspor sehingga
// hanya helper di berkas ini yang dapat membaca request ID dari context.
type ctxKunciRequestID struct{}

// PembuatID adalah pabrik request ID; bertipe fungsi agar mudah diganti
// generator deterministik saat pengujian.
type PembuatID func() string

// buatIDAcak adalah versi produksi: 8 byte acak dari crypto/rand, dienkode
// hexadecimal. Demo memakai penghitung berurutan agar output deterministik.
func buatIDAcak() (string, error) {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

// penghitungID menghasilkan req-001, req-002, ... untuk keperluan demo.
func penghitungID() PembuatID {
	var n atomic.Int64
	return func() string {
		return fmt.Sprintf("req-%03d", n.Add(1))
	}
}

// requestIDDariContext mengambil request ID yang disimpan middleware.
func requestIDDariContext(ctx context.Context) string {
	id, _ := ctx.Value(ctxKunciRequestID{}).(string)
	return id
}

// middlewareRequestID membaca header X-Request-ID (atau membuat ID baru),
// menyimpannya di context, dan menyalinnya ke header respons.
func middlewareRequestID(pembuat PembuatID, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if id == "" {
			id = pembuat()
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), ctxKunciRequestID{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
			}
			return a
		},
	}))
	klien := &http.Client{}

	// Layanan pesanan: menerima header X-Request-ID hasil propagasi gateway.
	layananPesanan := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		logger.Info("pesanan diproses", "request_id", id, "layanan", "pesanan")
		fmt.Fprintln(w, "pesanan siap")
	}))
	defer layananPesanan.Close()

	// Gateway: middleware membuat/mewarisi request ID, lalu meneruskannya.
	pembuat := penghitungID()
	gateway := httptest.NewServer(middlewareRequestID(pembuat,
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			id := requestIDDariContext(r.Context())
			logger.Info("request diterima", "request_id", id, "layanan", "gateway")

			req, err := http.NewRequestWithContext(r.Context(), http.MethodGet,
				layananPesanan.URL+"/proses", nil)
			if err != nil {
				panic(err)
			}
			req.Header.Set("X-Request-ID", id) // propagasi lintas layanan

			resp, err := klien.Do(req)
			if err != nil {
				logger.Error("panggilan layanan gagal", "request_id", id, "err", err)
				http.Error(w, "layanan pesanan tak tersedia", http.StatusBadGateway)
				return
			}
			defer resp.Body.Close()
			isi, _ := io.ReadAll(resp.Body)

			logger.Info("respons layanan pesanan", "request_id", id,
				"status", resp.StatusCode, "isi", string(isi))
			fmt.Fprintln(w, "OK")
		})))
	defer gateway.Close()

	fmt.Println("--- request pertama: gateway membuat request ID ---")
	resp, err := klien.Get(gateway.URL)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))

	fmt.Println("--- request kedua: header X-Request-ID dari pemanggil ---")
	req, err := http.NewRequest(http.MethodGet, gateway.URL, nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-Request-ID", "ext-9f2c")
	resp, err = klien.Do(req)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))
}

Output yang Dihasilkan:

text
--- request pertama: gateway membuat request ID ---
time=2026-01-15T08:00:00Z level=INFO msg="request diterima" request_id=req-001 layanan=gateway
time=2026-01-15T08:00:00Z level=INFO msg="pesanan diproses" request_id=req-001 layanan=pesanan
time=2026-01-15T08:00:00Z level=INFO msg="respons layanan pesanan" request_id=req-001 status=200 isi="pesanan siap\n"
respons klien: status=200 X-Request-ID=req-001

--- request kedua: header X-Request-ID dari pemanggil ---
time=2026-01-15T08:00:00Z level=INFO msg="request diterima" request_id=ext-9f2c layanan=gateway
time=2026-01-15T08:00:00Z level=INFO msg="pesanan diproses" request_id=ext-9f2c layanan=pesanan
time=2026-01-15T08:00:00Z level=INFO msg="respons layanan pesanan" request_id=ext-9f2c status=200 isi="pesanan siap\n"
respons klien: status=200 X-Request-ID=ext-9f2c

Mari kita telaah baris demi baris program di atas:

  1. Baris 16-22 (ctxKunciRequestID dan PembuatID): kunci context bertipe struct kosong tak diekspor; PembuatID bertipe fungsi sehingga generator bisa disuntikkan — teknik dependency injection kecil dari chapter middleware .
  2. Baris 26-32 (buatIDAcak): versi produksi memakai crypto/rand — 8 byte acak dienkode hex.EncodeToString menjadi 16 karakter; demo tidak memanggilnya agar output tetap deterministik.
  3. Baris 34-40 (penghitungID): generator tiruan menghasilkan req-001, req-002 lewat atomic.Int64 — aman untuk concurrency dan dapat diprediksi saat belajar.
  4. Baris 42-46 (requestIDDariContext): type assertion .(string) pada nilai context; kunci privat menjamin hanya helper ini yang membaca nilainya.
  5. Baris 50-60 (middlewareRequestID): header dibaca lebih dulu, id == "" berarti klien tidak mengirim, header respons diisi sebelum handler berjalan, dan r.WithContext(ctx) membawa context baru ke handler berikutnya.
  6. Baris 74-79: layanan pesanan membaca ID langsung dari header — sisi hilir cukup mempercayai propagasi gateway, tidak perlu membuat ID sendiri.
  7. Baris 86-87: handler gateway mengambil ID dari context, bukan dari header mentah — satu sumber kebenaran di dalam proses.
  8. Baris 89-94: http.NewRequestWithContext membawa context request asli (pembatalan ikut menular) dan req.Header.Set menuliskan propagasi; tiga baris log pada output membuktikan request_id yang sama muncul di dua layanan.
  9. Baris 111-132: dua request klien memperlihatkan dua sisi aturan: ID dibuat bila tidak ada, ID diwarisi bila pemanggil mengirimkannya.

ID yang Dipercaya dan yang Tidak #

Klien mana pun dapat mengirim header X-Request-ID palsu. Untuk layanan publik, terima header hanya dari proxy atau gateway internal yang kita kendalikan, atau validasi formatnya (panjang tetap dan karakter heksadesimal) sebelum dipakai. Serangan log injection memanfaatkan nilai header yang tidak divalidasi untuk menaburi log dengan baris palsu — kaitannya dengan penyamaran dibahas di chapter secure logging .

Request ID menautkan baris log, tetapi pertanyaan “seberapa sering” dan “berapa lama” tetap harus dijawab dengan angka yang dapat dijumlahkan. Chapter Metrics berpindah dari peristiwa diskrit ke pengukuran agregat.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

reqid-demo.go

Unduh berkas
go
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"sync/atomic"
)

// ctxKunciRequestID adalah tipe kunci context yang tidak diekspor sehingga
// hanya helper di berkas ini yang dapat membaca request ID dari context.
type ctxKunciRequestID struct{}

// PembuatID adalah pabrik request ID; bertipe fungsi agar mudah diganti
// generator deterministik saat pengujian.
type PembuatID func() string

// buatIDAcak adalah versi produksi: 8 byte acak dari crypto/rand, dienkode
// hexadecimal. Demo memakai penghitung berurutan agar output deterministik.
func buatIDAcak() (string, error) {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

// penghitungID menghasilkan req-001, req-002, ... untuk keperluan demo.
func penghitungID() PembuatID {
	var n atomic.Int64
	return func() string {
		return fmt.Sprintf("req-%03d", n.Add(1))
	}
}

// requestIDDariContext mengambil request ID yang disimpan middleware.
func requestIDDariContext(ctx context.Context) string {
	id, _ := ctx.Value(ctxKunciRequestID{}).(string)
	return id
}

// middlewareRequestID membaca header X-Request-ID (atau membuat ID baru),
// menyimpannya di context, dan menyalinnya ke header respons.
func middlewareRequestID(pembuat PembuatID, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if id == "" {
			id = pembuat()
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), ctxKunciRequestID{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
			}
			return a
		},
	}))
	klien := &http.Client{}

	// Layanan pesanan: menerima header X-Request-ID hasil propagasi gateway.
	layananPesanan := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		logger.Info("pesanan diproses", "request_id", id, "layanan", "pesanan")
		fmt.Fprintln(w, "pesanan siap")
	}))
	defer layananPesanan.Close()

	// Gateway: middleware membuat/mewarisi request ID, lalu meneruskannya.
	pembuat := penghitungID()
	gateway := httptest.NewServer(middlewareRequestID(pembuat,
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			id := requestIDDariContext(r.Context())
			logger.Info("request diterima", "request_id", id, "layanan", "gateway")

			req, err := http.NewRequestWithContext(r.Context(), http.MethodGet,
				layananPesanan.URL+"/proses", nil)
			if err != nil {
				panic(err)
			}
			req.Header.Set("X-Request-ID", id) // propagasi lintas layanan

			resp, err := klien.Do(req)
			if err != nil {
				logger.Error("panggilan layanan gagal", "request_id", id, "err", err)
				http.Error(w, "layanan pesanan tak tersedia", http.StatusBadGateway)
				return
			}
			defer resp.Body.Close()
			isi, _ := io.ReadAll(resp.Body)

			logger.Info("respons layanan pesanan", "request_id", id,
				"status", resp.StatusCode, "isi", string(isi))
			fmt.Fprintln(w, "OK")
		})))
	defer gateway.Close()

	fmt.Println("--- request pertama: gateway membuat request ID ---")
	resp, err := klien.Get(gateway.URL)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))

	fmt.Println("--- request kedua: header X-Request-ID dari pemanggil ---")
	req, err := http.NewRequest(http.MethodGet, gateway.URL, nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-Request-ID", "ext-9f2c")
	resp, err = klien.Do(req)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))
}