Dashboard & Cloud

Grafana Loki: Log Aggregation Modern untuk IoT & Cloud

Tutorial komprehensif Grafana Loki — arsitektur LogQL, label indexing, chunks storage, compactor, dan alerting rules untuk observability modern

1. Apa Itu Grafana Loki?

Grafana Loki adalah sistem log aggregation open-source yang dikembangkan oleh Grafana Labs. Berbeda dengan solusi logging tradisional seperti ELK Stack (Elasticsearch, Logstash, Kibana), Loki dirancang dengan filosofi "like Prometheus, but for logs" — yaitu hanya meng-index label (metadata) dari log, bukan isi keseluruhan teks log.

Pendekatan ini membuat Loki sangat hemat sumber daya dan storage. Loki tidak memerlukan full-text index yang besar seperti Elasticsearch, sehingga biaya operasional jauh lebih rendah. Loki sangat cocok untuk lingkungan cloud-native, Kubernetes, dan IoT di mana volume log bisa sangat besar namun pencarian hanya perlu dilakukan berdasarkan label tertentu.

💡 Tips

Loki menggunakan label yang sama dengan Prometheus, sehingga sangat mudah diintegrasikan. Kamu bisa mengkorelasikan metrics dari Prometheus dengan logs dari Loki dalam satu Grafana dashboard.

Loki vs ELK Stack

Aspek Grafana Loki ELK Stack
Indexing Hanya label (metadata) Full-text index
Penggunaan Memori Rendah Tinggi (Elasticsearch berat)
Storage Cost Sangat rendah Tinggi karena index size
Query Language LogQL KQL / Lucene
Full-text Search Tidak (grep-like saat query) Ya (di-index)
Ekosistem Grafana, Promtail, Alloy Kibana, Logstash, Beats
Kompleksitas Rendah Tinggi

Kapan Menggunakan Loki?

2. Arsitektur & Komponen Loki

Loki memiliki dua mode deployment: monolithic (semua komponen dalam satu proses) dan microservices (setiap komponen terpisah). Untuk development dan skala kecil, monolithic sudah cukup. Untuk produksi skala besar, mode microservices memberikan fleksibilitas dan skalabilitas lebih baik.

Komponen Utama Loki

Komponen Fungsi
Distributor Menerima log dari client, melakukan validasi, dan mendistribusikan ke ingester
Ingester Menerima log streams, mengompresinya ke chunks, dan menyimpan ke storage
Querier Menangani query LogQL dari Grafana, membaca dari ingester dan storage
Query Frontend Splitting dan caching query untuk optimasi performa
Compactor Mengompresi dan merge chunks, menghapus data expired
Ruler Menjalankan alerting rules dan recording rules

Data Flow

📦 Alur Data di Loki

Promtail/Alloy → mengumpulkan log dari file/container → Distributor → memvalidasi dan meneruskan → Ingester → mengompres ke chunks → Object Storage (S3, GCS, local filesystem). Saat query: GrafanaQuery FrontendQuerier → baca dari ingester + storage.

Log Streams dan Label

Setiap entry log di Loki termasuk dalam sebuah log stream. Stream diidentifikasi oleh kombinasi label unik. Misalnya:

# Label set untuk stream
{job="iot-gateway", instance="sensor-01", level="error"}

# Label set berbeda = stream berbeda
{job="iot-gateway", instance="sensor-01", level="info"}

Label yang umum digunakan di konteks IoT:

# Contoh label di Promtail config
scrape_configs:
  - job_name: iot-devices
    static_configs:
      - targets: [localhost]
        labels:
          job: iot-gateway
          environment: production
          region: jakarta
          __path__: /var/log/iot/*.log

3. Instalasi & Konfigurasi Loki

Ada beberapa cara menginstal Loki: Docker, Helm chart untuk Kubernetes, atau binary langsung. Untuk development, Docker Compose adalah cara tercepat.

Instalasi dengan Docker Compose

# docker-compose.yml
version: "3.8"
services:
  loki:
    image: grafana/loki:3.0.0
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/loki
      - ./loki-config.yml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

  promtail:
    image: grafana/promtail:3.0.0
    volumes:
      - /var/log:/var/log
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml
    depends_on:
      - loki

  grafana:
    image: grafana/grafana:11.0.0
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
    depends_on:
      - loki

volumes:
  loki-data:
  grafana-data:

Konfigurasi Loki

# loki-config.yml
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info

common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: "2024-01-01"
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 744h          # 31 hari
  max_query_length: 721h
  max_query_parallelism: 4
  ingestion_rate_mb: 10
  ingestion_burst_size_mb: 20

compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150

Konfigurasi Promtail

# promtail-config.yml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: iot-logs
    static_configs:
      - targets: [localhost]
        labels:
          job: iot-gateway
          environment: production
          __path__: /var/log/iot/*.log
    pipeline_stages:
      - regex:
          expression: '^(?P\S+)\s+(?P\w+)\s+(?P\w+)\s+(?P.*)$'
      - labels:
          level:
          device:
      - timestamp:
          source: timestamp
          format: RFC3339Nano

Verifikasi Instalasi

# Cek Loki health
curl http://localhost:3100/ready

# Push log secara manual
curl -X POST http://localhost:3100/loki/api/v1/push \
  -H "Content-Type: application/json" \
  -d '{
    "streams": [{
      "stream": {
        "job": "iot-gateway",
        "device": "sensor-01",
        "level": "info"
      },
      "values": [
        ["1719600000000000000", "Temperature reading: 25.6°C"]
      ]
    }]
  }'

# Query log via API
curl http://localhost:3100/loki/api/v1/query_range \
  --data-urlencode 'query={job="iot-gateway"}' \
  --data-urlencode 'start=1719600000000000000' \
  --data-urlencode 'end=1719603600000000000'

4. LogQL: Query Language Loki

LogQL adalah bahasa query Grafana Loki yang terinspirasi dari PromQL (Prometheus Query Language). LogQL memiliki dua jenis query utama: log query untuk memfilter log streams dan metric query untuk menghitung nilai dari log.

Log Query (Filter)

# Filter berdasarkan label — semua log dari job iot-gateway
{job="iot-gateway"}

# Filter multiple label
{job="iot-gateway", level="error"}

# Filter menggunakan regex
{job="iot-gateway", device=~"sensor-.*"}

# Filter negasi
{job="iot-gateway", level!="debug"}

# Line filter — contains
{job="iot-gateway"} |= "temperature"

# Line filter — NOT contains
{job="iot-gateway"} != "heartbeat"

# Line filter — regex match
{job="iot-gateway"} |~ "error|warning|critical"

# Line filter — regex negation
{job="iot-gateway"} !~ "DEBUG|TRACE"

# Kombinasi filter
{job="iot-gateway", level="error"} |= "timeout" !~ "retry"

Pipeline & Parser

# JSON parser — ekstrak field dari log JSON
{job="iot-gateway"} | json

# Filter setelah parse
{job="iot-gateway"} | json | temperature > 30

# Logfmt parser — untuk log format key=value
{job="iot-gateway"} | logfmt | level="error"

# Pattern parser — custom pattern
{job="iot-gateway"} | pattern `   `

# Regexp parser
{job="iot-gateway"} | regexp `(?P\w+): temp=(?P[\d.]+)`

# Label formatting — rename extracted field
{job="iot-gateway"} | json | label_format device_name="device"

# Template formatting
{job="iot-gateway"} | json | line_format "{{.device}}: {{.temperature}}°C"

# Unwrap — untuk metric query dari nilai numerik
{job="iot-gateway"} | json | unwrap temperature

Metric Query

# Count log per stream
count_over_time({job="iot-gateway"}[5m])

# Rate log per detik
rate({job="iot-gateway"} |= "error" [1m])

# Jumlah error per device
sum by (device) (count_over_time({job="iot-gateway"} | json | level="error" [1h]))

# Quantile dari nilai numerik
quantile_over_time(0.95, {job="iot-gateway"} | json | unwrap temperature [1h])

# Top 5 device dengan error terbanyak
topk(5, sum by (device) (count_over_time({job="iot-gateway"} | json | level="error" [24h])))

# Bytes rate — volume log
bytes_rate({job="iot-gateway"}[1m])

# Variance dari temperature
variance_over_time({job="iot-gateway"} | json | unwrap temperature [1h])
💡 Tips LogQL Performance

Selalu filter berdasarkan label terlebih dahulu sebelum line filter. Label di-index sehingga pencarian sangat cepat. Line filter dilakukan dengan mem-browse chunk secara linier, sehingga semakin spesifik label filter, semakin cepat query dieksekusi.

5. Label Indexing & Struktur Data

Pemahaman tentang bagaimana Loki meng-index data sangat penting untuk optimasi performa dan penggunaan storage. Loki menggunakan index store untuk memetakan label set ke chunk IDs, dan chunk store untuk menyimpan actual log data.

Struktur Index

# Index entries di Loki:
# 1. Label name → label value (inverse index)
# "job" → ["iot-gateway", "iot-api", "iot-worker"]

# 2. Label set → chunk references
# {job="iot-gateway", level="error"} → [chunk1, chunk2, chunk3]

# 3. Chunk → (fingerprint, labels, time range, data)
# chunk1 → (fp=12345, {job="iot-gateway",...}, [t0-t1], compressed_data)

Best Practices untuk Label

Praktik Benar ✅ Salah ❌
Cardinality {job="iot", level="error"} {user_id="12345", request_id="abc"} (terlalu banyak kombinasi)
Jumlah Label 3-5 label per stream 20+ label per stream (high cardinality)
Dynamic vs Static Static: job, env, region Dynamic: timestamp, random_id
Ukuran Value Singkat: "prod", "error" Panjang: full error message

High Cardinality Problem

High cardinality terjadi ketika terlalu banyak kombinasi label unik, menyebabkan banyak stream kecil. Ini memperburuk performa dan meningkatkan penggunaan memori.

# ❌ BURUK: High cardinality — setiap sensor jadi stream terpisah
{device_id="sensor-00001", device_id="sensor-00002", ..., device_id="sensor-99999"}

# ✅ BAIK: Gunakan satu label untuk kelompok, extract device_id dari log
{job="iot-sensors", location="warehouse-a"} | json | device_id="sensor-00001"

Tenant Isolation

# Di production, gunakan multi-tenancy
# Header X-Scope-OrgID menentukan tenant
# Setiap tenant memiliki namespace label terpisah

# Distributor config
distributor:
  shard_by_all_labels: true

# Tenant limit override
overrides:
  "tenant-iot":
    ingestion_rate_mb: 50
    max_streams_per_user: 10000
    retention_period: 90d
  "tenant-test":
    ingestion_rate_mb: 5
    max_streams_per_user: 1000
    retention_period: 7d

6. Chunks Storage & Retensi

Loki menyimpan log data dalam bentuk chunks yang terkompresi. Setiap chunk berisi baris-baris log dari satu stream dalam rentang waktu tertentu. Pemahaman tentang chunks sangat penting untuk mengelola storage dan performa query.

Struktur Chunk

# Setiap chunk memiliki:
# - Fingerprint: hash dari label set
# - Labels: label set yang mengidentifikasi stream
# - Time range: rentang waktu (dari timestamp pertama ke terakhir)
# - Encoding: compression codec (snappy, lz4, gzip, zstd)
# - Data: blok terkompresi berisi baris log

# Chunk size target:
# - Default: 1.5MB compressed (target)
# - Max chunk age: 2h (sebelum dipaksa flushed)
# - Chunk idle period: 30m (flush jika tidak ada data baru)

Storage Backend

Backend Keterangan Cocok Untuk
Filesystem Menyimpan di disk lokal Development, single node
S3 / MinIO Object storage AWS atau self-hosted Production, scalable
GCS Google Cloud Storage GCP environment
Azure Blob Azure Blob Storage Azure environment
Swift OpenStack Object Storage OpenStack environment

Retention Policies

# Global retention — hapus data lebih dari 30 hari
limits_config:
  retention_period: 744h    # 31 hari

# Per-stream retention override
# (gunakan compactor untuk apply)
compactor:
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150

# Per-tenant retention (overrides API)
# POST /loki/api/v1/limits/retention
# Body: {"retention_period": "2160h"}  # 90 hari
⚠️ Perhatian Retensi

Retensi hanya bekerja jika compactor aktif. Pastikan compactor berjalan dan retention_enabled: true. Tanpa compactor, data lama tidak akan dihapus meskipun retention_period sudah diatur.

Mengukur Penggunaan Storage

# Cek chunk metrics via API
curl http://localhost:3100/metrics | grep loki_chunk

# Metrik penting:
# loki_ingester_chunk_entries — jumlah entries per chunk
# loki_ingester_chunk_size_bytes — ukuran chunk saat ini
# loki_bloomstore_chunks_downloaded_total — chunks downloaded saat query

# Monitoring storage di Grafana — tambahkan dashboard ID 13639
# (Loki Stack Monitoring dashboard)

7. Compactor & Optimasi Storage

Compactor adalah komponen yang melakukan compaction (penggabungan) dan retention (penghapusan data lama). Compactor membaca chunks dari storage, menggabungkan chunks kecil menjadi lebih besar, dan menghapus chunks yang sudah melewati batas retensi.

Mengapa Compaction Penting?

Konfigurasi Compactor

# loki-config.yml — compactor section
compactor:
  working_directory: /loki/compactor
  shared_store: filesystem           # harus sama dengan chunk store
  compaction_interval: 10m           # seberapa sering compaction berjalan
  compaction_window: 4h              # rentang waktu data yang di-compact
  retention_enabled: true
  retention_mode: filter-and-delete  # atau "disabled"
  retention_delete_delay: 2h         # delay sebelum benar-benar dihapus
  retention_delete_worker_count: 150 # parallelism delete
  delete_request_store: filesystem

# Untuk skala besar, gunakan mode microservices:
# Jalankan compactor sebagai service terpisah
# Target: 1 compactor instance per 100GB data per hari

Monitoring Compactor

# Metrik penting compactor:
# loki_compactor_runs_total — total runs
# loki_compaction_work_duration_seconds — durasi compaction
# loki_compactor_deleted_lines_total — baris yang dihapus retention
# loki_compactor_load_pending_requests_total — pending delete requests

# Alert compactor stalled:
# loki_compactor_runs_total tidak bertambah selama 30 menit

Tips Optimasi Storage

💡 Tips Optimasi
  • Gunakan zstd compression — rasio kompresi terbaik untuk log
  • Atur chunk_target_size ke 1.5MB untuk keseimbangan antara ukuran dan query speed
  • Gunakan per-tenant retention untuk data dengan prioritas berbeda
  • Monitor ingested bytes per label untuk mendeteksi high-cardinality
  • Pertimbangkan structured metadata (Loki 3.0+) untuk mengurangi label cardinality

8. Alerting Rules dengan Grafana

Loki mendukung alerting rules melalui komponen Ruler atau langsung dari Grafana. Alerting rules memungkinkan kamu mendeteksi pola dalam log dan mengirim notifikasi ketika kondisi tertentu terpenuhi.

Loki Ruler Configuration

# Tambahkan ruler ke loki-config.yml
ruler:
  enable_api: true
  storage:
    type: local
    local:
      directory: /loki/rules
  rule_path: /loki/rules-temp
  alertmanager_url: http://alertmanager:9093
  ring:
    kvstore:
      store: inmemory
  enable_alertmanager_v2: true

Contoh Alerting Rules

# /loki/rules/fake/iot-alerts.yaml
groups:
  - name: iot-device-alerts
    interval: 1m
    rules:
      # Alert: Device offline lebih dari 5 menit
      - alert: DeviceOffline
        expr: |
          absent_over_time(
            {job="iot-gateway"} |= "heartbeat" [5m]
          )
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "IoT device heartbeat hilang"
          description: "Tidak ada heartbeat dari device selama lebih dari 5 menit"

      # Alert: Temperature terlalu tinggi
      - alert: HighTemperature
        expr: |
          sum by (device) (
            count_over_time(
              {job="iot-gateway"} | json | temperature > 40 [5m]
            )
          ) > 0
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "Suhu device {{ $labels.device }} di atas 40°C"
          description: "Device {{ $labels.device }} melaporkan suhu tinggi"

      # Alert: Error rate spike
      - alert: ErrorRateSpike
        expr: |
          sum(rate({job="iot-gateway"} |= "error" [5m]))
          /
          sum(rate({job="iot-gateway"}[5m]))
          > 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Error rate IoT gateway di atas 10%"
          description: "Error rate saat ini: {{ $value | humanizePercentage }}"

      # Alert: Disk usage tinggi di gateway
      - alert: GatewayDiskHigh
        expr: |
          count_over_time(
            {job="iot-gateway"} |~ "disk_usage.*critical" [10m]
          ) > 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Disk usage IoT gateway kritis"

Alertmanager Integration

# alertmanager.yml
global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alerts@company.com'

route:
  group_by: ['alertname', 'severity']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'iot-team'

receivers:
  - name: 'iot-team'
    email_configs:
      - to: 'iot-team@company.com'
        send_resolved: true
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'
        channel: '#iot-alerts'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

  - name: 'pagerduty-critical'
    pagerduty_configs:
      - service_key: 'xxx-xxx-xxx'

Grafana Alert Rules (UI-based)

Selain Ruler, kamu juga bisa membuat alert langsung dari Grafana UI:

  1. Buka Grafana → AlertingAlert rules
  2. Klik New alert rule
  3. Pilih data source: Loki
  4. Masukkan LogQL query, contoh: count_over_time({job="iot-gateway"} |= "error" [5m])
  5. Atur threshold: IS ABOVE 10
  6. Konfigurasi labels dan annotations
  7. Pilih notification channel (email, Slack, webhook)
  8. Simpan dan aktifkan

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Grafana Loki:

Pertanyaan 1: Apa filosofi desain utama Grafana Loki yang membedakannya dari ELK Stack?

a) Full-text indexing untuk semua log
b) Hanya meng-index label (metadata), bukan isi log
c) Menggunakan NoSQL database untuk penyimpanan
d) Menggunakan machine learning untuk pencarian

Pertanyaan 2: Dalam LogQL, apa fungsi dari operator |= ?

a) Mengelompokkan log berdasarkan label
b) Memfilter log yang mengandung string tertentu
c) Menghitung jumlah log
d) Menghapus log yang duplikat

Pertanyaan 3: Apa akibat dari high cardinality labels di Loki?

a) Query menjadi lebih cepat
b) Banyak stream kecil, memperburuk performa dan meningkatkan memori
c) Storage menjadi lebih hemat
d) Tidak ada dampak signifikan

Pertanyaan 4: Apa fungsi utama dari Compactor di Loki?

a) Menerima log dari client dan mendistribusikan ke ingester
b) Menjalankan query LogQL
c) Menggabungkan chunks kecil, kompresi, dan menjalankan retensi data
d) Mengenkripsi data sebelum disimpan

Pertanyaan 5: Metric query rate({job="iot"} |= "error" [1m]) menghitung apa?

a) Jumlah total error sejak pertama
b) Rata-rata error per menit
c) Rate (kecepatan kemunculan) log error per detik dalam 1 menit terakhir
d) Persentase error dari total log
← Sebelumnya Dashboard Monitoring IoT Selanjutnya → InfluxDB dan Telegraf untuk IoT
🔍 Zoom
100%
🎨 Tema