Logging #
Server yang tidak bisa diinvestigasi saat terjadi masalah adalah server yang tidak bisa dikelola. Log adalah sumber informasi paling mendasar untuk memahami apa yang terjadi di sistem — namun log yang tersebar di puluhan server tanpa agregasi terpusat hampir sama tidak bergunanya dengan tidak adanya log sama sekali. Mengotomasi setup infrastruktur logging dengan Ansible memastikan setiap server mengirimkan log ke tempat yang tepat dengan format yang konsisten, sejak pertama kali server dibuat — bukan setelah insiden pertama terjadi dan kita menyadari tidak ada jejak apa pun untuk dianalisis.
Artikel ini akan membawa kita dari konsep fundamental tentang tiga pilar log — log aplikasi, log sistem, dan log akses — sampai ke setup konkret log shipper, backend penyimpanan, log rotation, dan integrasi dengan stack /observability/monitoring/ yang sudah kita bangun. Setelah selesai membaca, kita akan memiliki playbook dan role Ansible yang bisa langsung digunakan untuk men-deploy pipeline logging di seluruh fleet server kita.
Anatomi Pipeline Logging #
Sebelum menulis role Ansible, kita perlu memahami alur lengkap sebuah log dari baris teks yang ditulis aplikasi sampai bisa kita query di dashboard. Setiap komponen memiliki peran spesifik, dan kegagalan di satu titik akan memutuskan seluruh rantai pengiriman log:
flowchart LR
A["Aplikasi<br/>stdout / file"] --> B["Log Shipper<br/>Filebeat / Promtail"]
C["OS & Services<br/>syslog / journald"] --> B
B --> D["Buffer / Queue<br/>in-memory"]
D --> E["Backend Storage<br/>Elasticsearch / Loki"]
E --> F["Query Layer<br/>Kibana / Grafana"]
F --> G["SRE / Developer<br/>Investigasi insiden"]
style A stroke:#b45309,stroke-width:2px
style C stroke:#b45309,stroke-width:2px
style B stroke:#1d4ed8,stroke-width:2px
style E stroke:#15803d,stroke-width:2px
style F stroke:#be185d,stroke-width:2px
style G stroke:#7e22ce,stroke-width:2px
Log shipper berjalan di setiap host. Tugasnya sangat spesifik: membaca file log, mem-parsing setiap baris, dan mengirimkannya ke backend. Log shipper bukan tempat menyimpan log jangka panjang — itu tugas backend. Memahami pemisahan tanggung jawab ini penting supaya kita tidak salah memilih tool: jika kita membutuhkan query full-text dengan index cepat, Elasticsearch adalah pilihannya. Namun, jika kita membutuhkan solusi ringan yang hemat disk dan integrasi mulus dengan Prometheus, Loki adalah pilihan terbaik.
Log yang sampai di backend hanya seberguna formatnya. Plain text tanpa struktur memaksa kita menulis regex yang berbeda untuk setiap microservice. JSON terstruktur dengan field konsisten (level, timestamp, service, trace_id) membuat query di backend menjadi satu baris PromQL atau KQL. Investasi dua jam di awal untuk setup structured logging menghemat ratusan jam debugging dalam setahun.
Tiga Format Log yang Wajib Kita Kenal #
Pemilihan format log menentukan bagaimana backend bisa melakukan query, agregasi, dan alerting. Ketiganya memiliki karakteristik tersendiri di infrastruktur:
| Format | Contoh Baris | Kelebihan | Kekurangan | Backend Ideal |
|---|---|---|---|---|
| Plain text | 2026-06-07 08:01:23 INFO User 42 logged in |
Kompatibel di mana-mana, mudah dibaca manusia | Tidak ada field konsisten, regex brittle | Tidak disarankan untuk produksi |
| logfmt | level=info ts=2026-06-07T08:01:23Z user=42 msg="logged in" |
Ringkas, mudah di-parse, format standar Heroku/Cloudflare | Kurang ekspresif untuk nested data | Loki, ELK |
| JSON | {"level":"info","ts":"2026-06-07T08:01:23Z","user":42,"msg":"logged in"} |
Mendukung nested, library client di semua bahasa, schema validation | Verbose, butuh pretty-print untuk debugging lokal | ELK, Loki, Datadog, semua vendor |
Saat ini, JSON adalah default yang seharusnya kita pilih untuk aplikasi baru. Hampir semua bahasa memiliki library structured logger (seperti Python structlog, Go zap, Java Logback + logstash-encoder, dan Node pino) yang menghasilkan JSON tanpa overhead signifikan. Untuk aplikasi warisan (legacy) yang masih menghasilkan plain text, kita bisa melakukan parsing di log shipper dengan pipeline processor — tetapi itu merupakan workaround, bukan solusi jangka panjang.
Setup Filebeat sebagai Log Shipper #
Filebeat adalah agen ringan dari Elastic yang membaca file log dan mengirimkannya ke Elasticsearch atau Logstash. Pilihan ini masuk akal jika stack monitoring kita sudah berbasis Elastic — jika belum, silakan lompat ke bagian Setup Loki sebagai Backend di bawah.
# roles/filebeat/tasks/main.yml
---
- name: Tambahkan Elastic GPG key
apt_key:
url: https://artifacts.elastic.co/GPG-KEY-elasticsearch
state: present
- name: Tambahkan repository Elastic
apt_repository:
repo: "deb https://artifacts.elastic.co/packages/8.x/apt stable main"
state: present
- name: Install Filebeat
apt:
name: "filebeat={{ filebeat_version }}"
state: present
update_cache: true
- name: Deploy konfigurasi Filebeat
template:
src: filebeat.yml.j2
dest: /etc/filebeat/filebeat.yml
owner: root
group: root
mode: '0644'
notify: Restart Filebeat
- name: Aktifkan dan jalankan Filebeat
systemd:
name: filebeat
state: started
enabled: true
Perhatikan checksum — di task install di bawah ini kita akan melihat bagaimana menambahkannya untuk keamanan. Konfigurasi Filebeat di bawah ini menambahkan field wajib ke setiap event log: hostname, environment, dan service name. Field-field ini adalah satu-satunya cara kita bisa memfilter log dari 200 server di fleet kita — jika tidak ada sejak awal, query service:api-gateway di Kibana tidak akan menemukan apa-apa:
{# roles/filebeat/templates/filebeat.yml.j2 #}
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/*.log
- /var/log/syslog
fields:
host: {{ inventory_hostname }}
environment: {{ env }}
fields_under_root: true
- type: log
enabled: true
paths:
- /var/log/app/*.log
fields:
service: {{ app_name }}
host: {{ inventory_hostname }}
environment: {{ env }}
fields_under_root: true
json.keys_under_root: true # Parse JSON log secara otomatis
json.overwrite_keys: true # Field JSON naik ke root, bukan nested di 'message'
json.add_error_key: true # Track error parsing supaya gagal parse tidak silent
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
- drop_fields:
fields: ["agent.ephemeral_id", "agent.id"]
ignore_missing: true
output.elasticsearch:
hosts: {{ elasticsearch_hosts | to_json }}
index: "logs-{{ '{{' }} fields.service {{ '}}' }}-%{+yyyy.MM.dd}"
username: "{{ elasticsearch_username }}"
password: "{{ vault_elasticsearch_password }}"
ssl.certificate_authorities: ["/etc/filebeat/ca.crt"]
ssl.verification_mode: full
setup.template.name: "logs"
setup.template.pattern: "logs-*"
setup.ilm.enabled: true # Index Lifecycle Management — auto hapus log lama
Jangan lupajson.overwrite_keys: truesaat input log berupa JSON. Tanpa flag tersebut, semua field JSON kita akan tersimpan di bawah keymessagesebagai satu objek, dan querylevel:errordi Kibana tidak akan menemukan apa-apa. Ini adalah bug paling umum dalam konfigurasi Filebeat dengan proses debugging yang bisa memakan waktu berjam-jam.
Pair Anti-Pattern: Field Kosong vs Field Wajib #
Kasus paling umum yang sering muncul saat review konfigurasi logging di tim yang baru setup logging:
# ANTI-PATTERN: hanya membaca /var/log/*.log tanpa menambahkan field metadata
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/*.log
# Masalah: saat ada 50 server mengirim log, kita tidak bisa memfilter
# "tampilkan error dari service X di production" karena tidak ada
# field service/host/environment. Setiap query harus melakukan scan terhadap seluruh dataset.
# BENAR: tambahkan fields di setiap input
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.log
fields:
service: {{ app_name }}
host: {{ inventory_hostname }}
environment: {{ env }}
fields_under_root: true
# Hasil: query di Kibana cukup `service:api-gateway AND level:error AND
# environment:production` — selesai dalam 2 detik di 50 server.
Memvalidasi Download Binary dengan Checksum #
Filebeat binary di-download langsung dari internet. Tanpa verifikasi checksum, satu compromised release server bisa menyisipkan backdoor ke seluruh fleet kita. Tambahkan verifikasi ini ke role:
# roles/filebeat/tasks/main.yml — tambahkan setelah apt install
- name: Verifikasi checksum binary Filebeat
stat:
path: /usr/share/filebeat/bin/filebeat
checksum_algorithm: sha256
register: filebeat_binary_stat
- name: Fail jika checksum tidak sesuai
fail:
msg: "Filebeat binary checksum mismatch — kemungkinan file korup atau compromised"
when: filebeat_binary_stat.stat.checksum != filebeat_expected_sha256
# Ambil nilai dari https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.x.x-linux-x86_64.tar.gz.sha512
# Verifikasi checksum bukan paranoia — supply chain attack terhadap tool monitoring
# adalah skenario nyata. Tambahkan checksum di defaults/main.yml dan validasi
# sebelum binary dieksekusi. Lima menit setup menghemat dari kompromi seluruh fleet.
Manajemen Log Rotation #
Log yang tidak di-rotate akan memenuhi disk. Setup logrotate untuk semua log penting — dan penting artinya: log aplikasi, log nginx, log PostgreSQL, bukan cuma /var/log/syslog:
# roles/logrotate/tasks/main.yml
---
- name: Deploy konfigurasi logrotate untuk aplikasi
template:
src: app-logrotate.j2
dest: "/etc/logrotate.d/{{ app_name }}"
owner: root
group: root
mode: '0644'
- name: Pastikan cron harian logrotate aktif
cron:
name: "logrotate daily"
minute: "0"
hour: "1"
user: root
job: "/usr/sbin/logrotate /etc/logrotate.conf"
cron_file: ansible_logrotate
when: logrotate_manage_cron | default(false)
{# roles/logrotate/templates/app-logrotate.j2 #}
{{ app_log_dir }}/*.log {
daily
rotate {{ log_rotate_days | default(14) }}
dateext # suffix tanggal, bukan .1.gz
dateformat -%Y%m%d # format YYYYMMDD
compress
delaycompress
missingok
notifempty
sharedscripts
copytruncate # untuk aplikasi yang tidak bisa di-signal reopen
postrotate
systemctl reload {{ app_name }} > /dev/null 2>&1 || true
endscript
}
copytruncate adalah opsi penting untuk aplikasi yang tidak menangani SIGHUP dengan benar (seperti beberapa aplikasi Java): daripada me-rename file log (yang membuat aplikasi tetap menulis ke file dengan inode lama), copytruncate menyalin isi file lalu memotong (truncate) file asli di tempat. Aplikasi tidak perlu tahu apa-apa.
Pair Anti-Pattern: Tanpa dateext vs Dengan dateext #
# ANTI-PATTERN: logrotate default rotation
/var/log/myapp/*.log {
daily
rotate 7
compress
}
# Masalah: hasil file adalah myapp.log.1.gz, myapp.log.2.gz, ...
# Kita tidak bisa mengetahui kapan log tersebut di-rotate tanpa membuka isinya.
# Saat restore dari backup, urutan file .1 .2 .3 tidak informatif.
# BENAR: gunakan dateext + dateformat
/var/log/myapp/*.log {
daily
rotate 14
dateext
dateformat -%Y%m%d
compress
}
# Hasil: myapp.log-20260607.gz, myapp.log-20260606.gz, ...
# Jelas terlihat: file ini di-rotate tanggal berapa. Pencarian di backup
# tool (atau `ls`) langsung jelas. Plus, urutan rotasi jadi tidak ambigu.
Setup Loki sebagai Backend Logging Ringan #
Untuk tim yang tidak ingin mengelola Elasticsearch, Grafana Loki adalah alternatif yang jauh lebih ringan. Loki menyimpan log dalam format yang mirip Prometheus (label-based, bukan full-text index), sehingga sangat cocok digunakan bersama /observability/monitoring/ yang sudah kita deploy:
# roles/loki/tasks/main.yml
---
- name: Buat user loki
user:
name: loki
system: true
shell: /usr/sbin/nologin
home: /opt/loki
create_home: true
- name: Download Loki binary
get_url:
url: "https://github.com/grafana/loki/releases/download/v{{ loki_version }}/loki-linux-amd64.zip"
dest: /tmp/loki.zip
mode: '0644'
- name: Extract Loki
unarchive:
src: /tmp/loki.zip
dest: /usr/local/bin/
remote_src: true
creates: /usr/local/bin/loki-linux-amd64
- name: Rename binary Loki
command: mv /usr/local/bin/loki-linux-amd64 /usr/local/bin/loki
args:
creates: /usr/local/bin/loki
- name: Deploy konfigurasi Loki
template:
src: loki-config.yml.j2
dest: /etc/loki/config.yml
owner: loki
mode: '0640'
notify: Restart Loki
- name: Deploy systemd unit file Loki
template:
src: loki.service.j2
dest: /etc/systemd/system/loki.service
notify:
- Reload systemd
- Restart Loki
{# roles/loki/templates/loki-config.yml.j2 #}
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
path_prefix: /var/lib/loki
storage:
filesystem:
chunks_directory: /var/lib/loki/chunks
rules_directory: /var/lib/loki/rules
replication_factor: 1
ring:
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: {{ loki_retention_days | default(30) }}d
ingestion_rate_mb: 10
ingestion_burst_size_mb: 20
max_entries_limit_per_query: 5000
compactor:
working_directory: /var/lib/loki/compactor
retention_enabled: true
delete_request_store: filesystem
ruler:
alertmanager_url: http://alertmanager:9093
storage:
type: local
local:
directory: /var/lib/loki/rules
Perhatikan retention_period: 30d di limits_config — ini adalah kebijakan jangka waktu penyimpanan log. Atur sesuai dengan kebutuhan kepatuhan (compliance) bisnis kita. Industri fintech dan layanan kesehatan biasanya membutuhkan 1–7 tahun, sedangkan startup standar cukup 30–90 hari. Menghapus retention policy berarti menyimpan selamanya, yang berujung pada membengkaknya biaya penyimpanan.
Promtail: Log Shipper untuk Loki #
Promtail adalah agen Loki — lebih ringan dari Filebeat untuk use case yang tidak membutuhkan fitur Elastic. Yang paling penting: Promtail menghasilkan label Prometheus-style yang bisa kita query menggunakan LogQL dengan sintaksis yang mirip dengan PromQL:
# roles/promtail/tasks/main.yml
---
- name: Download dan install Promtail
get_url:
url: "https://github.com/grafana/loki/releases/download/v{{ promtail_version }}/promtail-linux-amd64.zip"
dest: /tmp/promtail.zip
- name: Extract Promtail
unarchive:
src: /tmp/promtail.zip
dest: /usr/local/bin/
remote_src: true
creates: /usr/local/bin/promtail-linux-amd64
- name: Rename Promtail binary
command: mv /usr/local/bin/promtail-linux-amd64 /usr/local/bin/promtail
args:
creates: /usr/local/bin/promtail
- name: Deploy konfigurasi Promtail
template:
src: promtail-config.yml.j2
dest: /etc/promtail/config.yml
notify: Restart Promtail
- name: Deploy systemd unit Promtail
template:
src: promtail.service.j2
dest: /etc/systemd/system/promtail.service
notify:
- Reload systemd
- Restart Promtail
{# templates/promtail-config.yml.j2 #}
server:
http_listen_port: 9080
grpc_listen_port: 0
log_level: info
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://{{ loki_host }}:3100/loki/api/v1/push
batchwait: 1s
batchsize: 1048576
backoff_config:
min_period: 500ms
max_period: 5m
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
host: {{ inventory_hostname }}
env: {{ env }}
__path__: /var/log/*log
- job_name: {{ app_name }}
static_configs:
- targets:
- localhost
labels:
job: {{ app_name }}
host: {{ inventory_hostname }}
env: {{ env }}
__path__: {{ app_log_dir }}/*.log
pipeline_stages:
- json:
expressions:
level: level
message: message
request_id: request_id
- labels:
level:
- metrics:
log_lines_total:
type: Counter
description: "Total log lines for {{ app_name }}"
config:
match_all: true
action: inc
# Promtail menghasilkan metric `log_lines_total` dari jumlah baris log
# yang di-scrape. Metric ini bisa di-scrape Prometheus lewat
# `http://promtail-host:9080/metrics` — artinya kita bisa melakukan alerting saat
# log rate turun drastis (indikasi aplikasi crash tanpa menulis log).
# Bridge ini sangat powerful: metrik aplikasi hilang bisa berarti aplikasi hilang,
# atau bisa juga logging pipeline yang rusak.
Structured Logging dari Aplikasi #
Log shipper hanya bisa mengelompokkan log jika field-nya konsisten. Mendorong structured logging dari aplikasi jauh lebih efektif daripada melakukan parsing plain text di shipper — kita memiliki konteks penuh (trace ID, user ID, durasi) yang tidak akan pernah bisa diturunkan (derive) secara andal dari regex. Contoh untuk aplikasi Python:
# app/utils/logger.py
import logging
import sys
import json
from pythonjsonlogger import jsonlogger
class CustomJsonFormatter(jsonlogger.JsonFormatter):
def add_fields(self, log_record, record, message_dict):
super().add_fields(log_record, record, message_dict)
log_record['timestamp'] = self.formatTime(record, self.datefmt)
log_record['level'] = record.levelname
log_record['logger'] = record.name
log_record['service'] = '{{ app_name }}' # di-inject oleh Ansible
log_record['environment'] = '{{ env }}'
logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
formatter = CustomJsonFormatter('%(timestamp)s %(level)s %(name)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Penggunaan di aplikasi:
# logger.info("user_login", extra={"user_id": 42, "ip": "10.0.0.1"})
# Output: {"timestamp":"2026-06-07T08:01:23Z","level":"INFO","service":"api",
# "environment":"production","message":"user_login",
# "user_id":42,"ip":"10.0.0.1"}
Ansible bisa diintegrasikan untuk menyuntikkan nilai app_name dan env saat deploy — gunakan modul template untuk membuat file konfigurasi logger dengan nilai yang relevan untuk environment tersebut.
Decision Tree: Filebeat atau Promtail? #
Pemilihan log shipper menentukan backend dan biaya operasional. Gunakan decision tree ini sebagai panduan awal:
flowchart TD
A["Stack monitoring<br/>sudah ada?"] -->|"Elasticsearch"| B["Gunakan Filebeat"]
A -->|"Grafana + Prometheus"| C["Gunakan Promtail"]
A -->|"Belum ada"| D{"Volume log<br/>per hari?"}
D -->|"< 10 GB"| E["Promtail + Loki<br/>lebih ringan"]
D -->|"> 50 GB"| F["Filebeat + Elasticsearch<br/>index lebih kuat"]
B --> G{"Butuh parsing<br/>kompleks?"}
C --> H{"Butuh integrasi<br/>dengan alertmanager?"}
G -->|"Ya"| I["Filebeat + Logstash<br/>pipeline lebih kaya"]
G -->|"Tidak"| J["Filebeat langsung<br/>ke Elasticsearch"]
H -->|"Ya"| K["Promtail + Loki Ruler<br/>alerting built-in"]
H -->|"Tidak"| L["Promtail standar"]
style A stroke:#b45309,stroke-width:2px
style E stroke:#15803d,stroke-width:2px
style F stroke:#15803d,stroke-width:2px
style I stroke:#1d4ed8,stroke-width:2px
style J stroke:#1d4ed8,stroke-width:2px
| Kriteria | Filebeat + Elasticsearch | Promtail + Loki |
|---|---|---|
| Resource per host | ~50-100 MB RAM | ~30-50 MB RAM |
| Storage cost | Tinggi (full-text index) | Rendah (label-based) |
| Query performance | Sangat cepat untuk full-text search | Optimal untuk query by label, lambat untuk full-text |
| Index Lifecycle | ILM built-in | Compactor + retention policy |
| Alerting dari log | Logstash + ElastAlert | Loki Ruler + Alertmanager |
| Learning curve | Moderate (Elastic stack) | Rendah (jika sudah pakai Grafana) |
| Best for | Search-driven investigation, audit log | Operational log, integration dengan metric |
Pair Anti-Pattern: Password Plaintext vs Ansible Vault #
Credential backend seharusnya tidak pernah ada di file konfigurasi dalam bentuk plaintext. Pasangan ini menunjukkan perbedaan yang harus kita hindari dalam review:
{# ANTI-PATTERN: password hardcoded di template Jinja2 #}
{# roles/filebeat/templates/filebeat.yml.j2 #}
output.elasticsearch:
hosts: ["{{ elasticsearch_host }}:9200"]
username: "elastic"
password: "MyS3cretP@ssw0rd" # ✗ BAHAYA — ada di Git, terlihat di `ps`, bocor saat debug
# Masalah: password tersimpan di Git history selamanya. Saat developer
# menjalankan playbook di laptop, password muncul di log output. Saat
# file di-share untuk debugging, credential ikut tersebar.
{# BENAR: credential dari Ansible Vault, di-inject saat render template #}
{# roles/filebeat/templates/filebeat.yml.j2 #}
output.elasticsearch:
hosts: {{ elasticsearch_hosts | to_json }}
username: "{{ elasticsearch_username }}"
password: "{{ vault_elasticsearch_password }}" # ✓ dari vault
ssl.certificate_authorities: ["/etc/filebeat/ca.crt"]
Cara menyuntikkan vault ke template:
# Buat vault (dilakukan sekali, simpan vault password di password manager)
ansible-vault create group_vars/all/vault.yml
# Isi:
# vault_elasticsearch_password: "MyS3cretP@ssw0rd"
# Jalankan playbook dengan vault password
ansible-playbook site.yml --ask-vault-pass
# Atau otomatis dari CI:
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Jangan pernah membiarkan log output menampilkan rendered template. Tambahkan
no_log: truepada task yang merender template berisi credential:- name: Deploy konfigurasi Filebeat template: src: filebeat.yml.j2 dest: /etc/filebeat/filebeat.yml no_log: true # ✓ menyembunyikan output yang bisa membocorkan credentialTanpa
no_log: true, password bisa muncul di log Ansible jika terjadi error. Untuk pipeline CI/CD yang menyimpan log artifact, ini setara dengan menuliskan password ke penyimpanan publik.
Mengintegrasikan Log dengan Metric Collection #
Log dan metrik saling melengkapi. Saat Prometheus alerting memberitahukan bahwa CPU usage naik di host-42, kita ingin langsung melompat ke log host tersebut untuk melihat apa yang sedang terjadi. Integrasi ini membutuhkan label yang konsisten di kedua belah pihak:
# roles/loki/tasks/integration.yml — label harus sama dengan
# yang dipakai Prometheus (lihat artikel metric-collection)
---
- name: Pastikan label Loki konsisten dengan Prometheus
lineinfile:
path: /etc/loki/config.yml
regexp: '^ external_labels:'
line: |
external_labels:
cluster: {{ cluster_name }}
environment: {{ env }}
insertafter: 'common:'
notify: Restart Loki
# Di Promtail scrape config, host label juga harus konsisten
# dengan `host` label di Prometheus node_exporter scrape
Query gabungan di Grafana — mengambil metrik dari Prometheus, melihat log yang relevan dari Loki di panel yang sama:
# LogQL query di Grafana Explore, filter menggunakan label yang sama dengan PromQL
{service="api-gateway", env="production", host="api-42"}
| json
| level="error"
| line_format "{{.message}}"
Ringkasan #
- Filebeat untuk ekosistem Elastic (Elasticsearch/Logstash); Promtail untuk ekosistem Grafana (Loki) — pilih berdasarkan stack monitoring yang sudah ada, jangan paksakan yang baru.
- Selalu tambahkan
fieldsdi konfigurasi log shipper:host,environment,service— tanpa ini, query di backend pada 50+ server hampir mustahil dilakukan.logrotatewajib di-setup untuk semua log aplikasi — log tanpa rotation akan menghabiskan disk dalam hitungan minggu di server produksi dan menyebabkan cascading failure.- Untuk log dalam format JSON, aktifkan
json.keys_under_root: true(Filebeat) ataupipeline_stages: json(Promtail) agar field log bisa dicari dan difilter secara individual.- Gunakan Ansible Vault untuk menyimpan credential Elasticsearch atau Loki — password tidak boleh plaintext di file konfigurasi atau di Git history.
- Pin versi Filebeat/Promtail/Loki di
defaults/main.yml— update minor yang tidak terkontrol bisa mengubah format log, label, atau struktur index.- Aktifkan
json.overwrite_keys: truesaat input Filebeat adalah JSON — tanpa flag ini, field JSON akan nested di dalammessagedan tidak bisa di-query per-field.- Tambahkan
no_log: truedi task yang render template berisi credential — mencegah kebocoran password di Ansible log atau CI artifact.