---
title: "ลองใช้ Grafana Alloy เก็บ log เข้า Loki"
description: "แนะนำ Grafana Alloy ตัวเก็บ telemetry รุ่นใหม่ของ Grafana ตั้งแต่ติดตั้งด้วย docker เขียน config ส่ง log เข้า Loki ไปจนถึงคำสั่ง validate และ convert จาก Promtail"
canonical_url: "https://iamthana.com/blogs/2026-08-03-grafana-alloy-loki"
---

> Index of every page on this site: [llms.txt](https://iamthana.com/llms.txt)

## 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 ไหน เดี๋ยวจะได้เห็นกันครับ

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

```sh
docker run --rm grafana/alloy:latest --version
```

```text
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 ให้สองตัวคุยกันได้ด้วย

```sh
docker network create alloy-net

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

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

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

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

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

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

## เขียน config

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

```sh
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 ชุดเดียวกันก็ดึงไปใช้ต่อได้

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

```sh
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 ไหนต่อกับอันไหน

![](https://iamthana.com/images/2026-08-03-grafana-alloy-loki/alloy-ui.png)

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

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

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

```sh
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 อะไรบ้าง

```sh
curl -s http://localhost:3100/loki/api/v1/labels
```

```json
{ "status": "success", "data": ["filename", "host", "job", "service_name"] }
```

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

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

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

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

```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 ดีกว่า

```sh
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]'
```

```text
{"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 ได้

```sh
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]'
```

```text
{"level":"error","msg":"database connection refused"}
```

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

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

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

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

```sh
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
```

```text
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 สั้น ๆ แบบนี้ไปแปลงดู

```sh
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
```

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

```hcl
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` ออกมาเหมือนที่ผมเขียนไว้ข้างบน ซึ่งก็ถูกทั้งคู่ครับ

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

## ลบของทิ้ง

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

```sh
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

- [Grafana Alloy documentation](https://grafana.com/docs/alloy/latest/)
- [Install Alloy on Docker](https://grafana.com/docs/alloy/latest/set-up/install/docker/)
- [loki.source.file component](https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.file/)
- [Promtail EOL notice](https://grafana.com/docs/loki/latest/send-data/promtail/)
- [Grafana Agent EOL announcement](https://grafana.com/docs/agent/latest/)

## Sitemap

See the full [sitemap](https://iamthana.com/sitemap.md) for all pages.
