Log berformat teks seperti 2025-06-15 09:30:45 WARNING pesanan gagal nyaman dibaca manusia, tetapi menyiksa mesin. Tidak ada pemisah yang konsisten antara stempel waktu, level, dan isi pesan, sehingga tidak ada cara andal mengekstrak “berapa pesanan yang gagal hari ini” dari ribuan baris seperti itu. Structured logging membalik pendekatannya: setiap baris log menjadi satu object berfield — biasanya JSON — dengan nilai yang bisa dicari, disaring, dan dihitung secara otomatis. Pada Chapter ini kita akan melihat mengapa teks polos gagal di skala besar, menulis formatter JSON sendiri dengan standard library, lalu menyematkan correlation ID agar baris-baris log dari satu request bisa dikumpulkan kembali.
Masalah Log Teks Polos #
Bayangkan tiga baris log dari tiga style penulisan yang berbeda:
2025-06-15 09:30:45 [WARN] pesanan 4128 gagal diproses
09:30:46 WARNING: gagal memproses pesanan id=4129, alasan: stok habis
2025-06-15 09:30:47 pesanan 4130 GAGAL (stok)Ketiganya menyampaikan kejadian yang sama, tetapi formatnya berbeda-beda. Mencari seluruh kegagalan pesanan menuntut pola grep yang berbeda untuk setiap style, dan itupun belum menghitung pesan yang salah ketik. Masalahnya memburuk ketika satu baris ingin memuat angka (id pesanan, total harga, durasi): di teks polos angka-angka itu terkubur dalam kalimat.
Solusinya adalah menjadikan setiap baris log satu object dengan field bernama:
{"timestamp": "2025-06-15T09:30:45", "level": "WARNING", "logger": "toko", "message": "pesanan gagal diproses", "pesanan_id": 4128}Field pesanan_id kini adalah data bertipe angka yang bisa disaring dengan pesanan_id = 4128, bukan teks yang harus digali dengan regular expression.
Formatter JSON dengan Standard Library #
Kita bisa menulis formatter JSON sendiri dengan turunan logging.Formatter dan module json — tanpa satu pun paket pihak ketiga. Kuncinya ada pada dua hal: object LogRecord menyimpan field tambahan yang dikirim lewat parameter extra, dan method format() kita ganti agar mengembalikan string JSON:
import json
import logging
class JsonFormatter(logging.Formatter):
BAWAAN = set(logging.makeLogRecord({}).__dict__) | {
"message",
"asctime",
"taskName",
}
def format(self, record):
payload = {
"timestamp": self.formatTime(record, datefmt="%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
for kunci, nilai in record.__dict__.items():
if kunci not in self.BAWAAN:
payload[kunci] = nilai
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("toko")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info("Pesanan diproses", extra={"pesanan_id": 4128, "total": 125000})
logger.error("Pembayaran gagal", extra={"pesanan_id": 4129})Output yang Dihasilkan (dicetak module logging ke stderr; nilai timestamp berubah setiap eksekusi):
{"timestamp": "2025-06-15T09:30:45", "level": "INFO", "logger": "toko", "message": "Pesanan diproses", "pesanan_id": 4128, "total": 125000}
{"timestamp": "2025-06-15T09:30:46", "level": "ERROR", "logger": "toko", "message": "Pembayaran gagal", "pesanan_id": 4129}Mari kita bedah bagian-bagian pentingnya:
- Baris 6-10 (
BAWAAN): kumpulan nama atribut internal milikLogRecord. Atribut ini dibangun dengan membuatLogRecordkosong lalu mengambil seluruh key-nya — dipakai untuk membedakan field bawaan dari field buatan kita. - Baris 12-17 (
payload): empat field inti setiap baris log — stempel waktu, level, nama logger, dan isi pesan. - Baris 18-20 (loop
record.__dict__): field apa pun yang tidak termasuk atribut bawaan adalah kiriman parameterextra, sepertipesanan_iddantotal, dan dimasukkan ke payload. - Baris 21-22 (
exc_info): bila pesan dicatat lewatlogger.exception(), traceback lengkap ikut disimpan sebagai fieldexception. - Baris 23 (
json.dumps(...)): payload diubah menjadi satu baris JSON;ensure_ascii=Falseagar teks Bahasa Indonesia tetap tersimpan apa adanya.
Satu baris output = satu object lengkap. Mencari kejadian kini cukup memfilter field, misalnya semua log dengan "level": "ERROR" atau "pesanan_id": 4128.
Correlation ID dengan contextvars #
Pada aplikasi web, satu request bisa menyentuh banyak function sebelum selesai. Agar baris-baris log dari request yang sama bisa dikumpulkan kembali, setiap request diberi tanda unik bernama correlation ID yang disematkan pada setiap baris lognya. Standard library menyediakan module contextvars untuk menyimpan nilai konteks seperti ini secara aman — satu tempat penyimpanan per konteks eksekusi:
import contextvars
import logging
request_id = contextvars.ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get()
return True
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("[%(request_id)s] %(levelname)s %(message)s"))
logger = logging.getLogger("toko")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.addFilter(RequestIdFilter())
def tangani_pesanan(pesanan_id):
request_id.set(f"req-{pesanan_id:04d}")
logger.info("Pesanan diterima")
logger.warning("Stok hampir habis")
tangani_pesanan(4128)
tangani_pesanan(4129)Output yang Dihasilkan (dicetak module logging ke stderr):
[req-4128] INFO Pesanan diterima
[req-4128] WARNING Stok hampir habis
[req-4129] INFO Pesanan diterima
[req-4129] WARNING Stok hampir habisMari kita telaah mekanismenya:
- Baris 4 (
ContextVar): satu slot penyimpanan bernamarequest_iddengan nilai default"-"untuk kode yang berjalan di luar request. - Baris 7-10 (
RequestIdFilter):logging.Filtermenyuntikkan nilai konteks ke setiapLogRecordsebagai atributrequest_idsebelum diformat — itulah mengapa placeholder%(request_id)sbisa dipakai. - Baris 23 (
request_id.set(...)): begitu pesanan ditangani, ID-nya dipasang di konteks; seluruhlogger.info()danlogger.warning()setelah otomatis membawa tanda yang sama.
Di aplikasi web sungguhan, nilai request_id biasanya diisi uuid4() oleh middleware di awal setiap request, sehingga filter tidak perlu mengubah kode function mana pun.
Log Aggregation #
Log terstruktur baru benar-benar berdaya guna setelah terkumpul di satu tempat. Layanan log aggregation seperti ELK (Elasticsearch, Logstash, Kibana), Grafana Loki, atau produk berbayar seperti Datadog menampung log dari seluruh server, mengindeks field-field JSON, lalu menyediakan pencarian serupa query: “tampilkan semua "level": "ERROR" dengan "pesanan_id": 4128 dalam satu jam terakhir”. Format JSON satu-baris-per-event adalah bahasa pengantar yang dimengerti seluruh layanan tersebut; inilah alasan praktis mengapa structured logging menjadi standar industri.
Kita sudah membuat log yang mudah dicari. Chapter penutup section ini menyusun gambar besar lengkapnya: bagaimana log, metrics, dan trace bergabung menjadi observability.