---
title: "Logging terstruktur dengan format log dan level log yang bisa dicari per field"
description: "Logging terstruktur menaruh tiap nilai di field-nya sendiri, bukan di dalam kalimat. Kami mengirim 1 pesanan dengan 3 cara ke LogBrew dan menunjukkan mana yang bisa kamu temukan lagi lewat order_id."
canonical: "https://logbrew.co/id/guides/structured-logging"
last_updated: "2026-11-10"
---

# Logging terstruktur dengan format log dan level log yang bisa dicari per field

Logging terstruktur menaruh tiap nilai di field-nya sendiri, bukan di dalam kalimat. Kami mengirim 1 pesanan dengan 3 cara ke LogBrew dan menunjukkan mana yang bisa kamu temukan lagi lewat `order_id`.

Diuji pada 10 November 2026 dengan Python 3.13.5 dan logbrew-sdk 0.1.29

Oleh [Furkan Erday](https://github.com/furkanerday). Furkan Erday adalah pendiri LogBrew. Setiap hari ia membangun LogBrew dengan Claude Code dan Codex.

Canonical: https://logbrew.co/id/guides/structured-logging

## Baris log mana yang bisa kamu temukan lagi lewat `order_id`

| Ke mana id-nya pergi | Pencarian teks | Filter field |
| --- | --- | --- |
| Di dalam teks pesan | Ditemukan | Tidak ditemukan |
| Di field `extra={}` | Tidak ditemukan | Ditemukan |
| Di dict di dalam `extra={}` | Tidak ditemukan | Tidak ditemukan |

Kami mengirim 1 pesanan dengan 3 cara lewat handler Python, lalu mencarinya. Cangkir penuh berarti pencarian menemukan barisnya.

## Pilih format log yang bisa kamu filter

### Biarkan pesan tetap, taruh nilai di field

Field menyimpan nama dan tipenya. Nilai di dalam teks pesan hanya substring. Di Python, berikan field lewat `extra={}`, dan pesannya tetap sama untuk setiap pesanan.

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### Tulis baris JSON saat log dibaca program

Subclass `Formatter` mengubah tiap record menjadi 1 objek JSON per baris, dengan field standar dan setiap nilai `extra`. Log collector mengurai barisnya tanpa regex.

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

Output

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### Kirim field ke LogBrew

`LogBrewLoggingHandler` mengirim tiap record dengan nilai `extra`-nya sebagai field. Kami mengirim pesanan yang sama dengan 3 cara, di dalam teks pesan, sebagai field `extra`, dan sebagai dict di dalam `extra`.

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

Output

```text
HTTP 200 accepted 3 logs
```

### Temukan lagi barisnya lewat field

`GET /api/logs` menerima `attribute_filters` dengan path field, tipe, operator, dan nilai. Filter pada `order_id` hanya menemukan baris yang dikirim dengan `extra`. Dict di dalam `extra` dibuang, karena handler hanya menyimpan string, angka, boolean, dan `None`.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Output

```text
{"message":"order paid","order_id":"ord_42"}
```

## Pakai level log dengan cara yang sama di setiap layanan

### Pakai 4 level di produksi

`info` untuk apa yang terjadi, `warning` untuk apa yang salah tapi sudah ditangani, `error` untuk permintaan atau job yang gagal, dan `critical` untuk apa yang menghentikan layanan. Simpan `debug` untuk run lokal. Handler LogBrew mengirim record DEBUG sebagai `info`.

### Atur level sekali, saat startup

Baca level dari variabel lingkungan, jadi 1 host bisa berjalan di DEBUG sementara yang lain tetap di INFO. Di Python, `logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))` yang mengerjakannya.

### Catat angka sebagai angka

`amount_cents: 1999` menyimpan tipenya, jadi filter `amount_cents >= 1000` menemukannya. Kami juga mengirim `amount_cents: "1999"` sebagai teks, dan filter angka melewati baris itu.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

Output

```text
{"message":"order paid","amount_cents":1999}
```

## Hasil pengujian kami

Kami mengirim 1 pesanan dengan 3 cara lewat handler Python, lalu mencarinya dengan API LogBrew. Pencarian teks `search=ord_42` menemukan 1 dari 3 baris, yaitu yang punya id di dalam pesan.

Filter `order_id = ord_42` menemukan 1 baris, yang dikirim dengan `extra={"order_id": "ord_42"}`. Filter `amount_cents >= 1000` menemukan baris yang sama, karena handler menyimpan 1999 sebagai angka.

Baris yang dikirim dengan `extra={"order": {...}}` tidak cocok dengan apa pun. Handler mengirim string, angka, boolean, dan `None` dari `extra` dan melewati dict atau list.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Output

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## Format log teks biasa vs JSON

Format `%(message)s` biasa membuang setiap nilai `extra`. Baris di bawah berakhir dengan `INFO order paid` tanpa id pesanan, jadi tidak ada yang bisa menemukan pesanan itu nanti.

Baris JSON menyimpan field beserta tipenya, dan log collector menguraikannya tanpa regex. Orang lebih sulit membacanya, jadi buat pesan tetap pendek dan nama field tetap stabil.

Python tidak punya formatter JSON bawaan. python/cpython#77691 memintanya pada 2018 dan ditutup, karena dokumentasinya sudah menunjukkan cara menulisnya. `Formatter` 15 baris di atas adalah resep itu.

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

Output

```text
2026-11-10 08:34:04,955 INFO order paid
```

## Apa yang direkam LogBrew

Tiap log menyimpan level, nama logger, dan setiap field primitif dari `extra`, plus field di `metadata={}` pada handler. API memfilter berdasarkan path field, tipe, dan operator. Teks menerima `eq`, `ne`, `contains`, dan `starts_with`, dan angka menerima `gt`, `gte`, `lt`, dan `lte`.

LogBrew tidak mengindeks field yang namanya mengandung `password`, `secret`, `token`, `cookie`, atau `authorization`, jadi filter tidak pernah menemukannya. Tetap jauhkan rahasia dari baris log.

Log yang dikirim di dalam trace membawa id trace dan span, jadi filter lewat `order_id` mengarah ke permintaan yang menghasilkan baris itu.

## Pertanyaan yang sering diajukan

### Apa itu logging terstruktur?

Logging yang setiap nilainya adalah field bernama dengan tipe, sebagai JSON atau sebagai field di record log, bukan kata-kata di dalam kalimat. Alat pencarian lalu memfilter berdasarkan field, bukan substring.

### Apa urutan level log?

DEBUG, INFO, WARNING, ERROR, CRITICAL, dengan angka 10 sampai 50 di Python. Logger yang diatur ke INFO meloloskan INFO ke atas dan membuang DEBUG.

### Sebaiknya log dalam JSON atau teks biasa?

JSON saat barisnya dibaca program, seperti collector, file, atau alat pencarian. Teks biasa saat terminalnya hanya dibaca orang. Pertahankan field yang sama di keduanya.

### Bagaimana cara menambahkan field ke baris log Python?

Berikan `extra={"order_id": "ord_42"}` ke panggilan log. Formatter standar mengabaikan field extra. Formatter JSON atau handler LogBrew menyimpannya.

### Bisakah saya mencari log per field di LogBrew?

Bisa. `GET /api/logs` menerima sampai 4 `attribute_filters`, masing-masing dengan path field, tipe, dan operator seperti `eq` atau `gte`.

## Filter log kamu berdasarkan field apa pun

LogBrew menyimpan setiap field yang kamu catat dan menemukan barisnya lagi lewat field, di samping trace dan masalah tempat baris itu berada. Mulai dengan paket gratis yang lega, tanpa perlu kartu.

[Mulai gratis](https://logbrew.co/id)

## Sumber

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## Panduan lain untuk agen dan Python

- [Tambahkan server MCP ke Claude Code dan Codex](https://logbrew.co/id/guides/mcp-server-claude-code-codex)
- [Pakai AGENTS.md di Claude Code dan Codex](https://logbrew.co/id/guides/agents-md)
- [Atasi error Claude overloaded 529 dan rate limit 429](https://logbrew.co/id/guides/claude-overloaded-rate-limit-error)
- [Siapkan logging Python dengan level log yang tepat dan penangkapan exception](https://logbrew.co/id/guides/python-logging)
- [Cara mengatasi error di Claude Code, Codex, JavaScript, dan Python](https://logbrew.co/id/guides)
