/blogs

Homelab

ลองใช้ Grafana Alloy เก็บ log เข้า Loki

แนะนำ Grafana Alloy ตัวเก็บ telemetry รุ่นใหม่ของ Grafana ตั้งแต่ติดตั้งด้วย docker เขียน config ส่ง log เข้า Loki ไปจนถึงคำสั่ง validate และ convert จาก Promtail

Introduction

สวัสดีครับ พอดีที่บ้านผมมี container รันอยู่หลายตัว แล้วเวลาจะดู log ทีก็ต้อง docker logs ไล่ทีละตัว ซึ่งพอมันเยอะขึ้นเรื่อย ๆ ก็เริ่มไม่ไหวแล้วครับ เลยคิดว่าถึงเวลารวม log ไว้ที่เดียวสักที

ตัวที่คนใช้กันมานานคือ Promtail แต่พอไปเปิดเอกสารดูก็เจอบรรทัดนี้ครับ "Promtail is end of life (EOL) as of March 2, 2026" คือหมดอายุไปตั้งแต่ต้นปีแล้ว ไม่มี update ไม่มี security fix อีกต่อไป (Grafana Agent ก็หมดอายุไปก่อนหน้านั้นตั้งแต่ 1 พฤศจิกายน 2025 เหมือนกัน) ตัวที่ Grafana ให้ย้ายไปคือ Alloy ผมเลยข้ามไปลองตัวนี้เลยดีกว่า

บทความนี้ผมรันจริงทั้งหมดบนเครื่องตัวเองด้วย docker นะครับ output ที่แปะไว้ก๊อปมาจาก terminal ตรง ๆ ไม่ได้พิมพ์ขึ้นมาเอง เวอร์ชันที่ใช้คือ Alloy v1.18.0 กับ Loki 3.7.4

ของที่ต้องมีก่อนคือ docker กับ jq (เอาไว้อ่าน JSON ที่ Loki ตอบกลับมา ถ้ายังไม่มีก็ brew install jq หรือ apt install jq) แล้วก็ต้องมี port 3100 กับ 12345 ว่างอยู่ด้วย

Alloy คืออะไร

Alloy คือ distribution ของ OpenTelemetry Collector ที่ Grafana ทำขึ้นมาเองครับ (OpenTelemetry Collector คือตัวกลางมาตรฐานที่คอยรับข้อมูลพวก log, metrics, traces จากที่นึงแล้วส่งต่อไปอีกที่นึง) เอาไว้เก็บ log, metrics, traces แล้วก็ profiles ส่งไปยังปลายทางที่เราต้องการ พูดง่าย ๆ คือของเดิมที่เคยแยกเป็น Promtail (log) กับ Prometheus agent (metrics) ตอนนี้รวมมาอยู่ในตัวเดียวแล้ว

ที่ผมชอบคือมันเขียน config เป็นก้อน ๆ เรียกว่า component แล้วเอา output ของก้อนนึงต่อเข้า input ของอีกก้อนได้เลย ไม่ได้เขียนเป็น YAML ก้อนใหญ่ ๆ ที่ต้องมานั่งไล่ว่า key ไหนอยู่ใต้ key ไหน เดี๋ยวจะได้เห็นกันครับ

เช็คเวอร์ชันได้แบบนี้

docker run --rm grafana/alloy:latest --version
alloy, version v1.18.0 (branch: HEAD, revision: a435563)
  build user:       root@buildkitsandbox
  build date:       2026-07-20T09:02:30Z
  go version:       go1.26.5
  platform:         linux/arm64
  tags:             gore2regex,netgo,embedalloyui,promtail_journal_enabled

วิธีการติดตั้ง

ผมยก Loki ขึ้นมาก่อนเพื่อให้มีปลายทางให้ส่ง แล้วค่อยตาม Alloy ทีหลัง สร้าง network ให้สองตัวคุยกันได้ด้วย

docker network create alloy-net

docker run -d --name loki-demo \
  --network alloy-net \
  -p 3100:3100 \
  grafana/loki:latest

Loki ใช้เวลาบูตพอสมควรครับ ของผมรอไป 17 วินาทีกว่าจะพร้อมรับ query เช็คได้แบบนี้

curl -s http://localhost:3100/ready

ถ้ายังไม่พร้อมจะได้ Ingester not ready: waiting for 15s after being ready กลับมา รอจนกว่าจะขึ้นคำว่า ready แล้วค่อยไปต่อ

จากนั้นเตรียมโฟลเดอร์กับไฟล์ log ตัวอย่างที่จะให้ Alloy อ่าน

mkdir -p ~/alloy/logs
echo '{"level":"info","msg":"service started"}' > ~/alloy/logs/app.log

เขียน config

สร้างไฟล์ ~/alloy/config.alloy ขึ้นมาครับ จะใช้ editor เปิดเขียนเองก็ได้ หรือก๊อปทั้งก้อนนี้ไปวางใน terminal เลยก็ได้

cat > ~/alloy/config.alloy <<'EOF'
local.file_match "applog" {
  path_targets = [{
    __path__ = "/var/log/demo/*.log",
    job      = "demo-app",
    host     = "homelab",
  }]
}

loki.source.file "applog" {
  targets    = local.file_match.applog.targets
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://loki-demo:3100/loki/api/v1/push"
  }
}
EOF

ขั้นตอนนี้ห้ามข้ามนะครับ ถ้ายังไม่มีไฟล์นี้แล้วไปสั่ง docker run เลย docker จะสร้างโฟลเดอร์ชื่อ config.alloy ให้แทน แล้ว container จะพังตั้งแต่ยังไม่ทันขึ้น พร้อม error ที่งงมากว่า not a directory แถม docker logs ก็ว่างเปล่าไม่มีอะไรให้ดูด้วย

อ่าน config จากบนลงล่างได้เลยครับ

  • local.file_match คือตัวบอกว่าจะอ่านไฟล์ไหนบ้าง ตรง job กับ host คือ label ที่จะติดไปกับ log ทุกบรรทัด เอาไว้ค้นทีหลัง
  • loki.source.file คือตัวอ่านไฟล์ สังเกตว่า targets ชี้ไปที่ผลลัพธ์ของบล็อกแรกตรง ๆ (local.file_match.applog.targets) แล้ว forward_to ก็ชี้ต่อไปยังบล็อกที่สาม
  • loki.write คือตัวส่งออก

การต่อกันแบบนี้แหละครับที่เป็นจุดขายของ Alloy คือ output ของ component นึงเสียบเข้า input ของอีกตัวได้เลย

อีกเรื่องที่ควรรู้ไว้คือ loki.source.file มีบล็อก file_match ในตัวอยู่แล้ว ใส่ glob ลงไปใน __path__ ตรง ๆ ได้โดยไม่ต้องมี local.file_match แยกออกมา แต่ผมเลือกเขียนแยกเพราะอยากให้เห็นภาพว่า component ต่อกันยังไง แล้วถ้าวันหลังมี component อื่นอยากใช้ targets ชุดเดียวกันก็ดึงไปใช้ต่อได้

ทีนี้รันได้แล้ว

docker run -d --name alloy-demo \
  --network alloy-net \
  -p 12345:12345 \
  -v ~/alloy/config.alloy:/etc/alloy/config.alloy \
  -v ~/alloy/logs:/var/log/demo \
  grafana/alloy:latest \
    run --server.http.listen-addr=0.0.0.0:12345 \
    --storage.path=/var/lib/alloy/data \
    /etc/alloy/config.alloy

ตรง --server.http.listen-addr=0.0.0.0:12345 อย่าลืมใส่นะครับ ถ้าไม่ใส่มันจะไป bind ที่ 127.0.0.1 ข้างใน container แล้วเราจะเข้าหน้า UI จากข้างนอกไม่ได้เลย

หน้า UI ของ Alloy

เปิด http://localhost:12345 แล้วไปที่เมนู Graph ได้เลยครับ (หน้าแรกที่เด้งขึ้นมาจะเป็น Components) มันจะวาด pipeline ให้ดูว่า component ไหนต่อกับอันไหน

นี่แหละครับคือสิ่งที่อธิบายไปเมื่อกี้ local.file_match ยิงไปหา loki.source.file แล้วต่อไป loki.write ตรงตาม config ที่เขียนไว้เป๊ะ ๆ เส้นสีเขียวคือ log ถ้ามี metrics หรือ traces ด้วยก็จะเป็นคนละสี อันนี้มีประโยชน์มากตอน config เริ่มใหญ่ เพราะดูรูปทีเดียวรู้เลยว่าอะไรต่อกับอะไร

เช็คว่า log เข้าจริง

ลองเขียน log เพิ่มเข้าไปในไฟล์

for i in 1 2 3; do
  echo "{\"level\":\"info\",\"msg\":\"handled request $i\"}" >> ~/alloy/logs/app.log
done
echo '{"level":"error","msg":"database connection refused"}' >> ~/alloy/logs/app.log

รอสัก 2-3 วินาทีให้ Alloy อ่านไฟล์แล้วส่งเข้า Loki ก่อนนะครับ ถ้ายิง query ทันทีจะเห็นแค่บรรทัดแรกบรรทัดเดียวแล้วนึกว่าพัง

ถามก่อนว่า Loki รู้จัก label อะไรบ้าง

curl -s http://localhost:3100/loki/api/v1/labels
{ "status": "success", "data": ["filename", "host", "job", "service_name"] }

job กับ host คือที่เราใส่ไว้ใน config เอง ส่วนอีกสองตัวมาคนละที่กันครับ filename เป็นของ Alloy ที่เอา __path__ มาแปะให้ทุกบรรทัด ส่วน service_name เป็นของ Loki ที่ map มาจาก label job ให้อัตโนมัติตอน ingest

ทีนี้ลองดึง log ออกมาดูจริง ๆ

curl -sG http://localhost:3100/loki/api/v1/query_range \
  --data-urlencode 'query={job="demo-app"}' \
  --data-urlencode 'limit=10'

ที่ได้กลับมาคือ JSON ก้อนเดียวยาว ๆ หน้าตาประมาณนี้

{"status":"success","data":{"resultType":"streams","result":[{"stream":{"detected_level":"error","filename":"/var/log/demo/app.log","host":"homelab","job":"demo-app","service_name":"demo-app"},"values":[["1785760821561036812","{\"level\":\"error\",\"msg\":\"database connection refused\"}"]]},...

อ่านไม่ไหวครับ ต่อ jq เอาเฉพาะบรรทัด log ดีกว่า

curl -sG http://localhost:3100/loki/api/v1/query_range \
  --data-urlencode 'query={job="demo-app"}' \
  --data-urlencode 'limit=10' | jq -r '.data.result[].values[][1]'
{"level":"error","msg":"database connection refused"}
{"level":"info","msg":"handled request 3"}
{"level":"info","msg":"handled request 2"}
{"level":"info","msg":"handled request 1"}
{"level":"info","msg":"service started"}

ครบทั้ง 5 บรรทัดที่เขียนลงไฟล์ไป

กลับไปดูใน JSON ก้อนบนอีกทีจะเห็น detected_level โผล่มาด้วย อันนี้ผมไม่ได้ใส่ไว้ใน config เลยครับ เป็น Loki ที่อ่านเนื้อ log แล้วแยกให้เองว่าบรรทัดไหน error บรรทัดไหน info ทั้งที่ผมไม่ได้เขียน parser อะไรไว้สักตัว

ถ้าอยากกรองเฉพาะ error ก็ใส่ filter ต่อท้าย query ได้

curl -sG http://localhost:3100/loki/api/v1/query_range \
  --data-urlencode 'query={job="demo-app"} |= "error"' \
  --data-urlencode 'limit=10' | jq -r '.data.result[].values[][1]'
{"level":"error","msg":"database connection refused"}

ตรวจ config ก่อนรัน

Alloy มีคำสั่ง validate ให้ใช้ครับ ไม่ต้องรันจริงแล้วมานั่งดู log ว่าพังตรงไหน

docker run --rm -v ~/alloy/config.alloy:/c.alloy \
  grafana/alloy:latest validate /c.alloy

ถ้าผ่านจะไม่มีอะไรขึ้นมาเลย ทีนี้ลองทำให้มันพังดูบ้าง ผมสร้างไฟล์ bad.alloy ที่ใส่ attribute มั่ว ๆ เข้าไปหนึ่งตัว

cat > ~/alloy/bad.alloy <<'EOF'
loki.write "default" {
  endpoint {
    url = "http://loki-demo:3100/loki/api/v1/push"
  }
  bogus_arg = 1
}
EOF

docker run --rm -v ~/alloy/bad.alloy:/bad.alloy \
  grafana/alloy:latest validate /bad.alloy
Error: /bad.alloy:5:3: unrecognized attribute name "bogus_arg"

4 |   }
5 |   bogus_arg = 1
  |   ^^^^^^^^^^^^^
6 | }
Error: validation failed
2026/08/03 13:32:02 collector server run finished with error: validation failed

ชี้ให้เลยว่าบรรทัด 5 ตัวอักษรที่ 3 พร้อมขีดเส้นใต้ให้ด้วย

ระวังอย่าไปสับสนกับคำสั่ง fmt นะครับ สองตัวนี้ตรวจคนละชั้นกัน fmt ดูแค่ syntax คือวงเล็บปิดครบไหม โครงสร้างถูกไหม (ผมลองเอา config ที่ลืมปิดวงเล็บไปให้ มันฟ้อง expected }, got EOF แล้ว exit 1) แต่มันไม่เช็คว่าชื่อ attribute หรือ component นั้นมีอยู่จริงหรือเปล่า ตัว bogus_arg ข้างบนผ่าน fmt ฉลุยด้วย exit 0 แถมยังพ่น config ที่จัดหน้าเรียบร้อยแล้วออกมาให้อีก ต้องใช้ validate ถึงจะจับได้

ย้ายจาก Promtail

ใครที่มี config Promtail อยู่แล้วไม่ต้องนั่งแปลมือครับ Alloy มีคำสั่ง convert ให้ ผมลองเอา config สั้น ๆ แบบนี้ไปแปลงดู

cat > ~/alloy/promtail.yaml <<'EOF'
clients:
  - url: http://localhost:3100/loki/api/v1/push
scrape_configs:
  - job_name: system
    static_configs:
      - targets: [localhost]
        labels:
          job: varlogs
          __path__: /var/log/*.log
EOF

docker run --rm -v ~/alloy/promtail.yaml:/p.yaml \
  grafana/alloy:latest convert --source-format=promtail /p.yaml

ได้ผลออกมาแบบนี้

loki.source.file "system" {
    targets = [{
        __address__ = "localhost",
        __path__    = "/var/log/*.log",
        job         = "varlogs",
    }]
    forward_to = [loki.write.default.receiver]

    file_match {
        enabled = true
    }
    legacy_positions_file = "/var/log/positions.yaml"
}

loki.write "default" {
    endpoint {
        url = "http://localhost:3100/loki/api/v1/push"
    }
    external_labels = {}
}

สังเกตว่ามันเลือกใช้ file_match block ในตัวของ loki.source.file แทนที่จะแยก local.file_match ออกมาเหมือนที่ผมเขียนไว้ข้างบน ซึ่งก็ถูกทั้งคู่ครับ

ใช้ได้เลยแต่แนะนำให้อ่านทวนก่อนสักรอบ อย่าเอาไปใช้ทั้งดุ้นเลยทีเดียว

ลบของทิ้ง

ลองเสร็จแล้วเก็บกวาดด้วยนะครับ

docker rm -f alloy-demo loki-demo
docker network rm alloy-net
rm -rf ~/alloy

สรุป

ที่ลองมาผมว่า Alloy เข้าใจง่ายกว่า Promtail อยู่พอสมควรครับ ตรงที่ config เป็น component ต่อกันเป็นท่อ แล้วมีหน้า UI ให้ดูว่าท่อมันต่อถูกไหม เวลาพังจะหาต้นตอง่ายกว่าเยอะ

ที่เขียนในบทความนี้เป็นแค่ log อย่างเดียวนะครับ จริง ๆ Alloy ทำ metrics กับ traces ได้ด้วยในตัวเดียวกัน ถ้ามีโอกาสจะมาเขียนต่อ

ส่วนใครที่ยังใช้ Promtail อยู่ ถึงตอนนี้มันยังทำงานได้ปกติก็จริง แต่ EOL ไปแล้วตั้งแต่เดือนมีนาคมครับ แปลว่าถ้ามีช่องโหว่โผล่มาก็จะไม่มีใครแก้ให้แล้ว ควรวางแผนย้ายไว้แต่เนิ่น ๆ ดีกว่า และมีคำสั่ง convert ช่วยอยู่แล้วด้วย

References

Related Articles

เปลี่ยน Notebook (เก่า) ที่ไม่ได้ใช้งานเป็น Home Lab Server
Homelab

เปลี่ยน Notebook (เก่า) ที่ไม่ได้ใช้งานเป็น Home Lab Server

อยากลองเล่น Home Lab แต่ไม่อยากซื้ออุปกรณ์ใหม่ ลองเอา notebook เครื่องเก่ามาลองใช้ก่อนได้นะ
ตั้งค่า Ubuntu Server
Homelab

ตั้งค่า Ubuntu Server

โน๊ตนี้คือ commands ที่มักจะใช้บ่อย ๆ เมื่อต้อง setup ubuntu server (20.04, 22.04)
Setup Uptime Kuma
Homelab

Setup Uptime Kuma

โน๊ตนี้คือ วิธีการ setup uptime kuma แบบไม่ใช้ docker

Copyright © 2026