ข้ามไปยังเนื้อหา

Logging และ Metrics

Kubernetes ให้ logging, live debugging, และ metrics มาพอสำหรับสืบหาปัญหาของ Pod เดียวตอนนี้เลย แต่ไม่มีอะไร aggregate หรือเก็บข้อมูลไว้ทั่ว cluster — นั่นเป็น stack แยกที่คุณต้องเพิ่มเอง

kubectl logs: หน้าต่างสด ไม่ใช่หนังสือประวัติศาสตร์

หัวข้อที่มีชื่อว่า “kubectl logs: หน้าต่างสด ไม่ใช่หนังสือประวัติศาสตร์”

kubectl logs อ่าน stdout/stderr ของ container ตรงจาก node ที่มันรันอยู่ เป็นวิธีเร็วที่สุดในการดูว่า Pod กำลังทำอะไรอยู่ตอนนี้เลย

Terminal window
# Stream logs live as they are written
kubectl logs -f deploy/checkout
# A Pod with more than one container: pick which one
kubectl logs my-pod -c sidecar
# The container just crashed and restarted: see its last words before it died
kubectl logs my-pod --previous

--previous เป็นตัวที่คนลืมบ่อยที่สุดแล้วก็ต้องใช้บ่อยที่สุด: ตอน container crash-loop log ของ instance ปัจจุบัน อาจผ่านช่วงที่น่าสนใจไปแล้ว แต่ --previous โชว์ log ของ instance ที่เพิ่ง crash พิสูจน์จริง container ที่พิมพ์บรรทัดเดียวแล้ว exit ด้วย code ไม่ใช่ศูนย์

k8s/logging-and-metrics/crasher.yaml
apiVersion: v1
kind: Pod
metadata:
name: crasher
namespace: log-demo
spec:
restartPolicy: Always
containers:
- name: c
image: busybox
command: ["sh", "-c", "echo 'boot message, crashing now'; sleep 2; exit 1"]
$ kubectl get pod crasher
NAME READY STATUS RESTARTS AGE
crasher 0/1 Error 1 (2s ago) 12s
$ kubectl logs crasher --previous
boot message, crashing now

kubelet เก็บ container ที่ terminate ไว้ให้แค่จำนวนจำกัดก่อนจะ garbage-collect log ของมันทิ้ง ดังนั้น --previous ต้องรันภายในเวลาไม่นานหลัง crash — รอนานเกินไปผ่านหลาย restart cycle แม้แต่ --previous ก็จะรายงาน unable to retrieve container logs เพราะ container instance ที่ terminate ตัวนั้นหายไปแล้วจริง ๆ

เลือก log จากหลาย Pod พร้อมกัน และตัดให้เหลือแค่ที่ต้องการ

$ kubectl logs -l app=web -n log-demo --tail=2 --prefix
[pod/web-57787d5f95-dwbnf/c] tick 2 from web-57787d5f95-dwbnf
[pod/web-57787d5f95-dwbnf/c] tick 3 from web-57787d5f95-dwbnf
[pod/web-57787d5f95-g6qrh/c] tick 2 from web-57787d5f95-g6qrh
[pod/web-57787d5f95-g6qrh/c] tick 3 from web-57787d5f95-g6qrh
[pod/web-57787d5f95-q9hkw/c] tick 1 from web-57787d5f95-q9hkw
[pod/web-57787d5f95-q9hkw/c] tick 2 from web-57787d5f95-q9hkw
$ kubectl logs -l app=web -n log-demo --since=5s --prefix
[pod/web-57787d5f95-g6qrh/c] tick 2 from web-57787d5f95-g6qrh
[pod/web-57787d5f95-g6qrh/c] tick 3 from web-57787d5f95-g6qrh
[pod/web-57787d5f95-q9hkw/c] tick 2 from web-57787d5f95-q9hkw
[pod/web-57787d5f95-dwbnf/c] tick 2 from web-57787d5f95-dwbnf
[pod/web-57787d5f95-dwbnf/c] tick 3 from web-57787d5f95-dwbnf

-l (label selector) บวก -f จะ stream ทุก Pod ที่ match พร้อมกัน แต่ละบรรทัดติด tag ด้วย --prefix ให้รู้ว่ามาจาก Pod ไหน — เป็น built-in เทียบเท่ากับที่ tool ของ third-party อย่าง stern ให้ พร้อมสีสวยกว่าและ selector ข้าม namespace ได้ สำหรับ case ทั่วไปคือ “tail ทุกอย่างที่ match label นี้ตอนนี้เลย”

ข้อจำกัดที่สำคัญคือ Kubernetes เองไม่ aggregate หรือเก็บ log พวกนี้ ถ้า Pod ถูกลบ log ของมันก็มักจะหายไปด้วย ไม่มี “search log ทั่ว cluster” built-in นั่นเป็นหน้าที่ของ logging stack แยก — ปกติเป็น node-level agent อย่าง Fluent Bit (หรือ Fluentd/Vector) ที่ tail ไฟล์ log ของทุก container แล้วส่งไปที่ backend อย่าง Loki หรือ Elasticsearch ที่ search และเก็บระยะยาวได้

# A minimal illustration: Fluent Bit runs as a DaemonSet so one agent per node
# tails every container's logs and forwards them to a backend
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:5.1
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
hostPath:
path: /var/log

image แบบ minimal หรือ distroless (build จาก scratch หรือ gcr.io/distroless/static หรือ utility image อย่าง registry.k8s.io/pause) ไม่มี shell หรือ coreutils มาให้เลย — มีแค่ static binary ตัวเดียวที่ container รัน kubectl exec ต้องการ อะไรสักอย่าง ให้รันข้างใน container ดังนั้นมันจะพังด้วย error ที่เจาะจงจริง ไม่ใช่ปัญหา permission ระดับ Kubernetes

$ kubectl exec distroless-app -n log-demo -- sh
error: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "28673a297...": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH

Ephemeral container (GA/stable ตั้งแต่ Kubernetes 1.25) แก้ปัญหานี้ได้: kubectl debug แนบ container ใหม่ที่ใช้แล้วทิ้งเข้ากับ Pod ที่กำลังรันอยู่แล้ว แชร์ process namespace กับ target โดยไม่ต้อง restart หรือแทนที่อะไรใน Pod spec เดิมเลย

$ kubectl debug -it distroless-app -n log-demo --image=busybox --target=app -- ps aux
Targeting container "app". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-988qv.
PID USER TIME COMMAND
1 65535 0:00 /pause
17 root 0:00 ps aux

--target=app คือสิ่งที่ทำให้มันมีประโยชน์กว่าแค่ “Pod busybox ข้าง ๆ ของฉัน”: ephemeral container แชร์ process namespace กับ target container ที่ระบุไว้ ดังนั้น ps aux ที่รัน ข้างใน debug container เห็น PID 1 เป็น /pause — process ของ target แบบ distroless เอง — ไม่ใช่แค่ shell ของตัวมันเอง ไม่มีอะไรใน Pod spec เดิมเปลี่ยนเลย kubectl get pod distroless-app -o yaml ยังโชว์แค่ container เดิมตัวเดียวบวก entry ephemeralContainers ใหม่

kubectl debug ยังไปถึงทั้ง Node ได้ด้วย เวลาปัญหาอยู่ที่ node เองไม่ใช่ Pod ตัวใดตัวหนึ่งบนนั้น — มัน schedule debug Pod แบบ privileged ที่ mount root filesystem ของ node ไว้ที่ /host

$ kubectl debug node/observability-worker -it --image=busybox -- chroot /host cat /etc/os-release
VERSION_ID="13"
VERSION="13 (trixie)"
ID=debian

Warning event ทุกตัว (FailedScheduling, BackOff, Unhealthy, และอื่น ๆ) query ได้โดยตรง กรองฝั่ง server ได้เลย

$ kubectl get events -n log-demo --field-selector type=Warning --sort-by=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
37s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(607c53af-...)
3s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(91d78655-...)
$ kubectl get events -n log-demo --field-selector involvedObject.name=crasher
LAST SEEN TYPE REASON OBJECT MESSAGE
31s Normal Scheduled pod/crasher Successfully assigned log-demo/crasher to observability-worker
29s Normal Pulled pod/crasher Successfully pulled image "busybox" in 1.59s ...
5s Normal Started pod/crasher Container started
3s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(...)

--field-selector กรองให้เหลือ object เดียวหรือ event type เดียวฝั่ง server (ถูกกว่า pipe ผ่าน grep) และ kubectl get events --watch stream event ใหม่สด ๆ ตามที่ API server ปล่อยออกมา ใช้กลไก watch เดียวกับ flag -w/--watch ของ kubectl ที่เหลือ event ก็มีอายุสั้นด้วย (default retention ประมาณหนึ่งชั่วโมง ควบคุมด้วย --event-ttl บน API server) ดังนั้นมันตอบ “เพิ่งเกิดอะไรขึ้น” ได้ดีกว่า “เมื่อวานเกิดอะไรขึ้น” มาก — อีกเหตุผลที่ต้องมี logging/metrics stack จริง

metrics-server เป็น cluster add-on เบา ๆ ที่เก็บ CPU/memory usage จากทุก kubelet และเก็บไว้ ในหน่วยความจำเท่านั้น — ไม่มี history ไม่มี persistence ไม่มี alerting มันไม่ได้เป็นส่วนหนึ่งของ kind โดย default และต้อง patch จริงหนึ่งจุดเพื่อทน self-signed certificate ของ kubelet บน kind

scripts/install-metrics-server.sh
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl -n kube-system patch deployment metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deploy/metrics-server

มันมีไว้ทำสองอย่างเท่านั้น พิสูจน์ด้วยตัวเลขจริงจาก kind cluster 1.37 ที่รันอยู่จริง

$ kubectl top nodes
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
observability-control-plane 166m 1% 806Mi 9%
observability-worker 41m 0% 222Mi 2%
observability-worker2 43m 0% 244Mi 2%
$ kubectl top pods -n log-demo
NAME CPU(cores) MEMORY(bytes)
distroless-app 0m 0Mi
web-57787d5f95-dwbnf 1m 0Mi
web-57787d5f95-g6qrh 1m 0Mi
web-57787d5f95-q9hkw 1m 0Mi

…และ HorizontalPodAutoscaler ที่ poll metrics-server เพื่อตัดสินใจว่าจะ scale Deployment ขึ้นหรือลง (พูดถึงลึกกว่านี้ในโมดูล Scheduling and Scaling)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

metrics-server ไม่ใช่ monitoring solution ชัด ๆ — ถ้าถามว่า “CPU usage เมื่อสามชั่วโมงก่อนเป็นยังไง” หรือ “แจ้งเตือนตอน memory เกิน 90%” metrics-server ตอบไม่ได้เลย มันรู้แค่ snapshot ปัจจุบัน

สำหรับ observability จริง ๆ — historical data, dashboard, alerting — combination มาตรฐานคือ Prometheus กับ Grafana

Prometheus ทำงานด้วย scraping: มันดึง metrics จาก HTTP endpoint /metrics ที่ workload เปิดไว้ในรูปแบบ text ของ Prometheus เอง เป็นระยะ ๆ application และ sidecar หลายตัวเปิด endpoint นี้เอง สำหรับ state ของ object ระดับ cluster (replica count ของ Deployment, phase ของ Pod, condition ของ node และอื่น ๆ) Prometheus scrape kube-state-metrics ซึ่งเป็น service แยกที่แปลง object ของ Kubernetes API เป็น metrics ของ Prometheus

apiVersion: v1
kind: Pod
metadata:
name: checkout
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
spec:
containers:
- name: checkout
image: checkout:2.3.0
ports:
- containerPort: 9090
Terminal window
# A workload's own /metrics endpoint, in Prometheus text format
curl http://checkout-pod:9090/metrics

Prometheus เก็บข้อมูลที่ scrape มาเป็น time series แล้วประเมิน alerting rule กับมัน Grafana อยู่ชั้นบนเป็น visualization layer query Prometheus เพื่อ render dashboard — trend ของ CPU/memory, request latency, error rate — และขับเคลื่อน alert notification

OpenTelemetry Collector: pipeline ที่เป็นกลางต่อ vendor ไม่ใช่ตัวแทน Prometheus

หัวข้อที่มีชื่อว่า “OpenTelemetry Collector: pipeline ที่เป็นกลางต่อ vendor ไม่ใช่ตัวแทน Prometheus”

OpenTelemetry Collector เป็น pipeline ที่เป็นกลางต่อ vendor รับ, process, และ export log, metric, และ trace พร้อมกันได้ แทนที่จะต้องมี agent แยกต่อ signal ต่อ vendor ใน cluster Kubernetes มันมักรันเป็น DaemonSet (เก็บระดับ node คล้าย Fluent Bit ด้านบน) หรือ sidecar รับข้อมูลจาก application แล้ว route ไปยัง backend หนึ่งตัวหรือมากกว่า พร้อม batch, retry, และ filter ระหว่างทาง

ที่สำคัญคือ Collector เสริม Prometheus แทนที่จะแทนที่มัน: มัน รับ metrics ในรูปแบบ scrape ของ Prometheus เองได้แล้ว export ต่อไป ทำให้ cluster หนึ่งใช้ collection pipeline เดียวได้ ในขณะที่ยังเลือก (หรือเปลี่ยน) backend ได้ — Prometheus, vendor APM เชิงพาณิชย์, หรือทั้งสอง — โดยไม่ต้อง instrument application ใหม่ทั้งหมด บทนี้พูดถึงมันแค่ภาพรวม ไม่ได้ติดตั้งจริง

flowchart LR
  subgraph Debug
    exec["kubectl exec"] -.->|"fails, no shell"| distroless["Distroless container"]
    debug["kubectl debug --target"] --> distroless
  end
  subgraph Logs
    stdout["Pod stdout/stderr"] --> agent["Node-level log agent\n(Fluent Bit)"]
    agent --> backend["Log backend\n(Loki / Elasticsearch)"]
  end
  subgraph Metrics
    cadvisor["kubelet cAdvisor"] --> ms["metrics-server\n(in-memory)"]
    ms --> top["kubectl top / HPA"]
    workload["Workload /metrics\n+ kube-state-metrics"] --> prom["Prometheus"]
    prom --> grafana["Grafana dashboards"]
  end
สาม pipeline ขนานกัน: เครื่องมือ debug สด, log ไปที่ log backend, และ metrics ไปที่ kubectl top/HPA และ Prometheus/Grafana

ทุกอย่างในบทนี้คือ Kubernetes ให้ หน้าต่างสด ไม่ใช่ ประวัติศาสตร์ kubectl logs/exec/debug/top/get events ทั้งหมดตอบว่า “อะไรเป็นจริงกับ Pod หรือ Node นี้ตอนนี้ (หรือเมื่อกี้นี้)” — และทุกตัวสูญเสียข้อมูลทันทีที่ Pod ถูกลบ container ถูก garbage-collect หรือ event TTL หมดอายุ observability จริง (Fluent Bit → Loki/Elasticsearch, Prometheus → Grafana, หรือ OpenTelemetry Collector pipeline ที่ป้อนตัวใดตัวหนึ่ง) มีไว้เพื่ออยู่รอดจากตรงนั้นโดยเฉพาะ — มัน copy สัญญาณสดออกไปที่มี retention ก่อน ที่ Kubernetes จะทิ้งมัน ephemeral container ก็อยู่ในเรื่องเดียวกันแต่มองจากมุมอื่น: image แบบ distroless จงใจไม่มี tooling สำหรับ live-window ติดมาเลย ดังนั้น kubectl debug เอาหน้าต่างชั่วคราวของตัวเองมาแทนที่จะสมมติว่ามีอยู่แล้วข้างใน container

ทางเลือกข้อดีต้นทุน
kubectl logs/exec/debug/topไม่ต้องมี component เพิ่ม ใช้งานได้ทันทีที่ cluster มีอยู่ (พิสูจน์แล้วด้านบน)ไม่มี retention, ไม่มี history, ไม่ aggregate ข้าม Pod หรือเวลา
kubectl debug --target (ephemeral container)debug container แบบ distroless/minimal ได้โดยไม่ต้อง rebuild image หรือ restart Pod (พิสูจน์แล้วด้านบน)ต้องการ container runtime ที่รองรับ ephemeral container/shared process namespace; เพิ่ม container พิเศษที่มองเห็นได้ใน Pod ระหว่างที่มันรัน
Fluent Bit + Loki/Elasticsearchlog ที่ search ได้และเก็บไว้ทั่วทุก Pod และผ่านการลบinfrastructure จริงที่ต้องรัน กำหนดขนาด และดูแล availability
metrics-serverfootprint น้อยที่สุด พอสำหรับ kubectl top/HPA เป๊ะ (พิสูจน์แล้วด้านบน)ไม่มี history เลย — ตอบ “เมื่อชั่วโมงก่อนเป็นยังไง” ไม่ได้
Prometheus + Grafanahistorical, query ได้, alert ได้ — มาตรฐานของวงการservice ที่ต้อง stateful เพิ่มอีกตัวที่ต้อง operate (storage, retention, scrape config)
OpenTelemetry Collectorpipeline เดียวสำหรับ log+metric+trace เป็นกลางต่อ vendor ป้อน Prometheus ได้อีกชิ้นส่วนที่เคลื่อนไหวได้; ไม่ตัดความจำเป็นของ backend (Prometheus, Loki, หรือ vendor) ที่ต้องเก็บข้อมูลจริง ๆ ออกไป
  • “kubectl exec ควรทำงานได้เสมอเพื่อเปิด shell ใน container ไหนก็ได้” พิสูจน์แล้วด้านบนด้วย error ที่เจาะจงจริง: image แบบ distroless/minimal ไม่มี shell binary เลย ดังนั้น exec พังด้วย executable file not found in $PATH — วิธีแก้คือ kubectl debug --target ไม่ใช่การสมมติว่า container พัง
  • “Kubernetes เก็บ log ไว้ที่ไหนสักแห่งอย่างถาวรโดย default” พิสูจน์แล้วด้านบน: log ถูกอ่านสดจาก node และหลังผ่าน restart cycle มากพอ แม้แต่ kubectl logs --previous ก็ดึง log ของ container ที่ terminate แล้วไม่ได้อีก — ไม่มีอะไรถาวรถ้าไม่มี logging stack แยก
  • “metrics-server เป็น Prometheus แบบเบา ๆ” พิสูจน์แล้วด้านบน: มันเก็บแค่ snapshot ปัจจุบันในหน่วยความจำ ไม่มี history เลย มีไว้แค่ตอบ kubectl top และป้อน HPA ไม่ใช่ให้ query หรือ alert
  • “kubectl debug --target restart หรือแทนที่ container ของ Pod” พิสูจน์แล้วด้านบน: container เดิม (/pause, PID 1) รันต่อเนื่องตลอด ephemeral container เป็น process แยกเพิ่มเข้ามาที่เห็นได้ใน ps aux output เดียวกัน ไม่ใช่ตัวแทนที่
  • “OpenTelemetry Collector แทนที่ Prometheus” ตามเอกสารของ OpenTelemetry Collector เป็น pipeline ที่เป็นกลางต่อ vendor ที่ รับจากและ export ไปยัง Prometheus ได้ในบรรดา backend อื่น ๆ — มันเป็น routing/processing layer ไม่ใช่ storage backend ที่แข่งกับ Prometheus

ตอน review คำสั่ง debug, คำสั่งดู log, หรือ manifest ด้าน observability ที่ AI สร้าง ให้เช็ค

  • kubectl exec -it <pod> -- /bin/sh ที่แนะนำเป็นขั้นแรกในการ debug image แบบ distroless หรือ FROM scratch — พิสูจน์แล้วด้านบน มันพังทันทีด้วย error “executable file not found” จริง tool ที่ถูกต้องคือ kubectl debug --target
  • kubectl logs <pod> (ไม่มี --previous) ที่แนะนำเพื่อวินิจฉัยว่า ทำไม container ถึง crash เมื่อกี้ — ถ้า container restart ไปแล้ว log ปัจจุบันอาจเริ่มหลัง crash --previous ต่างหากที่โชว์คำพูดสุดท้ายของ instance ที่กำลังจะตาย พิสูจน์แล้วด้านบน
  • doc troubleshooting ที่สมมติว่า log search ได้ทั่ว cluster หรือทั่วเวลาโดยไม่ต้องทำอะไรเพิ่ม — พิสูจน์แล้วด้านบน Kubernetes เองไม่เก็บ history เลย ความสามารถนั้นมีก็ต่อเมื่อ deploy logging stack (Fluent Bit/Loki ฯลฯ) แล้วจริง ๆ
  • metrics-server ที่เสนอเป็นตัวแทน “เราต้องการ monitoring/alerting” — พิสูจน์แล้วด้านบน มันไม่มี history และ alert ไม่ได้ ต้องใช้ Prometheus (หรือเทียบเท่า) บวก Grafana หรือ alerting layer อื่น
  • hostPath: /var/log mount ที่แนะนำเป็นวิธีเร็ว ๆ “เก็บ log” จากข้างใน application Pod เอง แทนที่จะเป็น DaemonSet agent ระดับ node เฉพาะ — วิธีนี้ผูก application เข้ากับ path เฉพาะของ node และพลาด log ของ Pod อื่นทุกตัวบน node นั้น pattern มาตรฐานคือ DaemonSet อย่าง Fluent Bit ไม่ใช่โค้ดเก็บ log ต่อ application

🐛 หา bug ในโค้ดนี้

k8s/logging-and-metrics/debug-attempt.yaml
apiVersion: v1
kind: Pod
metadata:
name: distroless-app
namespace: log-demo
spec:
containers:
- name: app
image: registry.k8s.io/pause:3.10
เฉลย

manifest นี้ไม่มีอะไรผิดในตัวมันเอง — bug อยู่ที่วิธีที่ AI assistant มักแนะนำในการ debug มัน: kubectl exec distroless-app -- sh พิสูจน์แล้วด้านบน มันพังทันทีด้วย OCI runtime exec failed: ... exec: "sh": executable file not found in $PATH เพราะ registry.k8s.io/pause (เหมือน image แบบ distroless ทุกตัว) ไม่มี shell มาให้เลย วิธีที่ถูกต้อง ก็พิสูจน์แล้วด้านบนเช่นกัน คือ kubectl debug -it distroless-app --image=busybox --target=app -- ps aux ซึ่งแนบ container ที่ใช้แล้วทิ้งเข้าไปแชร์ process namespace กับ target แทนที่จะพยายามรัน shell ที่ไม่เคยมีอยู่

Kubernetes aggregate และเก็บ log ของ Pod ทั่ว cluster ไว้โดย default ไหม
ขั้นต่อไปที่ถูกต้องในการ debug container นี้คืออะไร
$ kubectl exec distroless-app -- sh
error: ... OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
metrics-server เหมาะกับอะไรที่สุด
flag ไหนโชว์ log ของ container ที่เพิ่ง crash แล้ว restart และทำไมมันถึงหยุดทำงานได้ในที่สุด
การเห็น PID 1 เป็น /pause จากข้างใน debug container busybox พิสูจน์อะไร
$ kubectl debug -it distroless-app --image=busybox --target=app -- ps aux
PID   USER     TIME  COMMAND
    1 65535     0:00 /pause
   17 root      0:00 ps aux
Prometheus กับ Grafana เกี่ยวข้องกับ metrics-server ยังไง
OpenTelemetry Collector เกี่ยวข้องกับ Prometheus ยังไง