Log adalah arsip kejadian sistem: setiap login, pembayaran gagal, dan stok menipis tercatat baris demi baris. Masalahnya, log ditulis untuk dibaca mesin penghasilnya, bukan untuk dianalisis — jutaan baris teks tidak bisa dimanfaatkan sebelum diubah menjadi data terstruktur. Pada chapter ini kita membangun parser log dengan dua pendekatan (string split dan regex), lalu menggunakannya untuk memfilter level, menghitung kejadian per jam, dan mengorelasikan peringatan dengan kegagalan. Sintaks regex sendiri pernah dibahas pada chapter Pengenalan Regular Expressions dan Groups dan Captures; di sini kita fokus pada penerapannya untuk data.
Berkas Log Latihan #
Simpan cuplikan log aplikasi toko berikut sebagai app.log. Empat belas baris pertama mengikuti format tanggal waktu LEVEL modul pesan; dua baris terakhir adalah potongan stack trace yang tidak mengikuti format:
2025-06-15 08:03:12 INFO auth Login berhasil: user=budi
2025-06-15 08:03:45 WARNING auth Login gagal: user=siti alasan=password salah
2025-06-15 08:04:01 ERROR payment Timeout gateway: order=ORD-1042 durasi=30.0s
2025-06-15 08:04:02 WARNING payment Retry ke-1 untuk order=ORD-1042
2025-06-15 08:04:33 ERROR payment Timeout gateway: order=ORD-1042 durasi=31.2s
2025-06-15 08:05:10 INFO payment Pembayaran berhasil: order=ORD-1055
2025-06-15 08:06:02 WARNING stok Stok menipis: sku=P004 sisa=3
2025-06-15 08:07:44 INFO katalog Produk baru ditambahkan: sku=P012
2025-06-15 09:01:19 ERROR payment Kartu ditolak: order=ORD-1061 kode=51
2025-06-15 09:02:03 INFO auth Login berhasil: user=siti
2025-06-15 09:15:47 WARNING stok Stok menipis: sku=P008 sisa=8
2025-06-15 09:16:02 ERROR stok Gagal memesan ulang: sku=P004 pemasok=tidak merespons
2025-06-15 10:04:11 INFO laporan Laporan harian dibuat
2025-06-15 10:05:02 ERROR payment Timeout gateway: order=ORD-1077 durasi=28.4s
File "gateway.py", line 42, in proses_pembayaran
TimeoutError: koneksi ke penyedia pembayaran terputusParsing dengan split #
Ketika formatnya konsisten dan dipisah spasi, str.split() dari chapter split() dan join() sudah cukup:
catatan = []
with open("app.log", encoding="utf-8") as berkas:
for baris in berkas:
try:
tanggal, waktu, level, modul, pesan = baris.split(maxsplit=4)
except ValueError:
continue # baris terlalu pendek
catatan.append({
"waktu": f"{tanggal} {waktu}",
"level": level,
"modul": modul,
"pesan": pesan.rstrip("\n"),
})
print(f"{len(catatan)} baris terurai")
print(catatan[0])
print(catatan[-1])Output yang Dihasilkan:
16 baris terurai
{'waktu': '2025-06-15 08:03:12', 'level': 'INFO', 'modul': 'auth', 'pesan': 'Login berhasil: user=budi'}
{'waktu': 'TimeoutError: koneksi', 'level': 'ke', 'modul': 'penyedia', 'pesan': 'pembayaran terputus'}Enam belas baris “berhasil” diurai — termasuk dua baris stack trace yang seharusnya ditolak. Parameter maxsplit=4 membatasi pemecahan menjadi maksimal lima bagian, tetapi tidak memvalidasi isinya; baris TimeoutError: koneksi... lolos sebagai record dengan level "ke". Data seperti ini merusak agregasi secara diam-diam.
Parsing dengan Regex #
Regex memperbaiki dua hal sekaligus: pola yang ketat menvalidasi struktur baris, dan named group memberi nama pada setiap field. Ini persis kasus penggunaan chapter Kapan Tidak Menggunakan Regex — split kalah bukan karena lambat, tetapi karena tidak bisa memvalidasi:
import re
POLA_LOG = re.compile(
r"^(?P<tanggal>\d{4}-\d{2}-\d{2}) (?P<waktu>\d{2}:\d{2}:\d{2}) "
r"(?P<level>[A-Z]+) (?P<modul>\w+) (?P<pesan>.+)$"
)
def parse_baris(baris):
hasil = POLA_LOG.match(baris.rstrip("\n"))
return hasil.groupdict() if hasil else None
catatan = []
with open("app.log", encoding="utf-8") as berkas:
for baris in berkas:
hasil = parse_baris(baris)
if hasil is not None:
hasil["waktu"] = f"{hasil['tanggal']} {hasil['waktu']}"
catatan.append(hasil)
print(f"{len(catatan)} baris terurai")
print(catatan[2])Output yang Dihasilkan:
14 baris terurai
{'tanggal': '2025-06-15', 'waktu': '2025-06-15 08:04:01', 'level': 'ERROR', 'modul': 'payment', 'pesan': 'Timeout gateway: order=ORD-1042 durasi=30.0s'}Mari kita bedah polanya:
- Baris 4 (
^dan$): jangkar memaksa seluruh baris cocok dengan pola — awal dan akhir baris harus persis di posisi itu. Baris stack trace yang dimulai spasi langsung gagal di awal. - Baris 4 (
(?P<tanggal>\d{4}-\d{2}-\d{2})): group bernama menangkap tanggal dengan format ketat;groupdict()mengubahnya menjadi dictionary tanpa pemotongan manual. - Baris 5 (
(?P<level>[A-Z]+)): level harus huruf kapital semua —INFO,ERROR— sehingga kata sepertipenyediaditolak. - Baris 10-11 (
return hasil.groupdict() if hasil else None): baris yang tidak cocok menghasilkanNone, bukan galat; pemanggil memutuskan untuk melewatkannya.
Dibandingkan split, regex membayar biaya keterbacaan yang lebih tinggi, tetapi menukarnya dengan validasi gratis. Untuk format sesederhana ini split masih bisa dipakai dengan validasi tambahan; begitu format menjadi lebih longgar (opsional kolom, berbagai level), regex jelas lebih kuat.
Filter Level dan Hitung per Jam #
Record terstruktur membuka seluruh kotak analisis. Counter dari chapter collections menjawab pertanyaan “seberapa sering”:
from collections import Counter
print(Counter(b["level"] for b in catatan))
per_jam = Counter(b["waktu"][:13] for b in catatan)
for jam, jumlah in sorted(per_jam.items()):
print(f"{jam} {jumlah} baris")Output yang Dihasilkan:
Counter({'INFO': 5, 'ERROR': 5, 'WARNING': 4})
2025-06-15 08 8 baris
2025-06-15 09 4 baris
2025-06-15 10 2 barisIrisan b["waktu"][:13] mengambil tahun-bulan-tanggal jam dari timestamp yang sudah digabung. Pola yang sama menghitung modul mana yang paling bermasalah, dan regex kedua yang kecil mengekstrak nomor pesanan dari pesan error:
import re
from collections import Counter
error_per_modul = Counter(b["modul"] for b in catatan if b["level"] == "ERROR")
print(error_per_modul)
pesanan_bermasalah = []
for b in catatan:
if b["level"] != "ERROR":
continue
cocok = re.search(r"order=(\S+)", b["pesan"])
if cocok:
pesanan_bermasalah.append(cocok.group(1))
print(pesanan_bermasalah)
print(Counter(pesanan_bermasalah))Output yang Dihasilkan:
Counter({'payment': 4, 'stok': 1})
['ORD-1042', 'ORD-1042', 'ORD-1061', 'ORD-1077']
Counter({'ORD-1042': 2, 'ORD-1061': 1, 'ORD-1077': 1})Modul payment jelas tersangka utama, dan pesanan ORD-1042 muncul dua kali — sinyal kuat bahwa timeout berulang pada satu transaksi.
Korelasi Waktu: Peringatan Sebelum Kegagalan #
Analisis log tingkat lanjut membutuhkan waktu sebagai tipe data, bukan string. Setelah dikonversi dengan datetime.strptime() (chapter Parsing dan Formatting datetime), kita bisa mencari WARNING yang mendahului ERROR di modul yang sama dalam jendela dua menit:
from datetime import datetime, timedelta
for b in catatan:
b["waktu_dt"] = datetime.strptime(b["waktu"], "%Y-%m-%d %H:%M:%S")
JENDELA = timedelta(minutes=2)
peringatan = [b for b in catatan if b["level"] == "WARNING"]
for galat in (b for b in catatan if b["level"] == "ERROR"):
kandidat = [
w for w in peringatan
if w["modul"] == galat["modul"]
and timedelta(0) <= galat["waktu_dt"] - w["waktu_dt"] <= JENDELA
]
if not kandidat:
continue
terdekat = max(kandidat, key=lambda w: w["waktu_dt"])
print(f"{galat['modul']}: WARNING {terdekat['waktu'][11:]} -> "
f"ERROR {galat['waktu'][11:]} ({galat['pesan'][:20]}...)")Output yang Dihasilkan:
payment: WARNING 08:04:02 -> ERROR 08:04:33 (Timeout gateway: ord...)
stok: WARNING 09:15:47 -> ERROR 09:16:02 (Gagal memesan ulang:...)Dua korelasi ditemukan: retry payment yang berujung timeout kedua, dan peringatan stok menipis yang berujung kegagalan pemesanan ulang. Error 08:04:01 tidak dilaporkan karena warning Retry ke-1 muncul setelahnya — arah waktu diperiksa dengan syarat timedelta(0) <= ....
Log Terstruktur: JSON per Baris #
Seluruh kerja keras di atas sebenarnya mengobati gejala: log teks memang tidak dirancang untuk dianalisis. Sistem modern semakin banyak memilih log terstruktur — satu objek JSON per baris (materi chapter Structured Logging):
{"ts": "2025-06-15T08:04:01", "level": "ERROR", "modul": "payment", "pesan": "Timeout gateway", "order": "ORD-1042"}
{"ts": "2025-06-15T08:04:02", "level": "WARNING", "modul": "payment", "pesan": "Retry ke-1", "order": "ORD-1042"}
{"ts": "2025-06-15T08:05:10", "level": "INFO", "modul": "payment", "pesan": "Pembayaran berhasil", "order": "ORD-1055"}
{"ts": "2025-06-15T08:06:02", "level": "WARNING", "modul": "stok", "pesan": "Stok menipis", "sku": "P004", "sisa": 3}
{"ts": "2025-06-15T09:01:19", "level": "ERROR", "modul": "payment", "pesan": "Kartu ditolak", "order": "ORD-1061", "kode": 51}
{"ts": "2025-06-15T09:02:03", "level": "INFO", "modul": "auth", "pesan": "Login berhasil", "user": "siti"}
{"ts": "2025-06-15T10:05:02", "level": "ERROR", "modul": "payment", "pesan": "Timeout gateway", "order": "ORD-1077", "durasi": 28.4}Parsingnya satu baris json.loads() tanpa regex sama sekali:
import json
from collections import Counter
with open("server_terstruktur.log", encoding="utf-8") as berkas:
terstruktur = [json.loads(baris) for baris in berkas]
print(Counter(b["level"] for b in terstruktur))
print([b["order"] for b in terstruktur if b["level"] == "ERROR" and "order" in b])Output yang Dihasilkan:
Counter({'ERROR': 3, 'WARNING': 2, 'INFO': 2})
['ORD-1042', 'ORD-1061', 'ORD-1077']Log adalah sumber data paling kotor yang sudah kita tangani; sisanya tentang menjadikan data yang sudah terbaca menjadi layak analisis. Kita mulai dari pembersihan.