จัดการ Log Go ให้เป็นระบบ ด้วย Zap และ CloudWatch

สวัสดีครับ โปรแกรมเมอร์ทุกคน นะครับ เคยไหมครับที่ต้องมานั่งงมหา Log เวลาที่ระบบมันมีปัญหานะครับ แบบว่าหาเท่าไหร่ก็ไม่เจอ หรือเจอแล้วก็อ่านยากมากๆ บทความนี้นะครับ ผมจะมาแนะนำวิธีจัดการ Log ใน Go ให้มันดูเป็นระบบระเบียบขึ้นมา ด้วยไลบรารี่ตัวเจ๋งอย่าง uber-go/zap แล้วก็จากนั้น เราจะมาลองดูกันว่าถ้าเราจะส่ง Log พวกนี้ไปให้ CloudWatch ของ AWS เค้าจัดการดูมันจะเป็นยังไงบ้าง นะครับ มันจะช่วยให้เรา มอนิเตอร์ ระบบได้ง่ายขึ้นมากๆ เลยล่ะครับ

ขั้นตอนที่ 1: ติดตั้ง uber-go/zap

ง่ายๆ ครับ แค่รันคำสั่งนี้ นะครับ

go get go.uber.org/zap

ขั้นตอนที่ 2: ใช้ zap สร้าง Log ทั้ง Console และ ไฟล์

มาดูตัวอย่างโค้ดนะครับ ที่จะทำให้เราได้ Log แบบเป็นโครงสร้าง (Structured Log) ทั้งไปที่ Console แล้วก็ บันทึกลงไฟล์ด้วยนะครับ ผมจะตั้งค่าให้เวลาใน Log เป็นแบบ ISO8601 ด้วย เพื่อให้มันเข้ากับ CloudWatch ได้ง่ายๆ นะครับ

package main

import (
\t"fmt"
\t"time"

\t"go.uber.org/zap"
\t"go.uber.org/zap/zapcore"
)

func main() {
\t// ตั้งค่า Config สำหรับ Logger ครับ
\t// ให้ Output เป็น JSON และมีเวลาแบบ ISO8601
\tconfig := zap.NewProductionConfig()
\tconfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder

\t// กำหนดเส้นทาง Output ครับ ให้ไปทั้ง stdout (Console) และไฟล์ "app.log"
\t// ถ้าจะรันในเครื่องตัวเอง ให้สร้างไฟล์ app.log ในโฟลเดอร์เดียวกันได้เลยนะครับ
\tconfig.OutputPaths = []string{"stdout", "app.log"}
\tconfig.ErrorOutputPaths = []string{"stderr"} // อันนี้สำหรับ Log ระดับ Error ครับ

\t// สร้าง Logger จาก Config ที่เราตั้งไว้
\tlogger, err := config.Build()
\tif err != nil {
\t\tfmt.Printf("มีปัญหาตอนสร้าง Logger ครับ: %v\
", err)
\t\treturn
\t}
\tdefer logger.Sync() // สำคัญมากๆ ครับ เพื่อให้มั่นใจว่า Log ถูกเขียนหมดก่อนโปรแกรมจะปิด

\t// ตัวอย่างการเขียน Log แบบ Info
\tlogger.Info("แอพของเราเริ่มทำงานแล้วครับ",
\t\tzap.String("service", "billing-api"),
\t\tzap.String("version", "1.0.0"),
\t)

\t// ตัวอย่างการเขียน Log พร้อม Field ที่เป็นข้อมูลเฉพาะ
\tlogger.Warn("มีบางอย่างดูแปลกๆ นะครับ",
\t\tzap.String("module", "payment-gateway"),
\t\tzap.Int("transaction_id", 98765),
\t\tzap.Duration("response_time", time.Millisecond*500),
\t)

\t// ตัวอย่างการเขียน Log ระดับ Error
\tlogger.Error("เจอข้อผิดพลาดร้ายแรง ครับ",
\t\tzap.String("component", "database"),
\t\tzap.Error(fmt.Errorf("เชื่อมต่อฐานข้อมูลไม่ได้")),
\t\tzap.Stack("stacktrace"), // ลองเพิ่ม stacktrace เข้าไปดูครับ
\t)

\tfmt.Println("\
--- ลองตรวจสอบไฟล์ 'app.log' ในโฟลเดอร์นี้ดูนะครับ ---\
")
}

Output ที่เราจะได้ (ประมาณนี้นะครับ)

พอเรารันโค้ดข้างบน นะครับ เราก็จะได้ Log ออกมา 2 ที่ครับ คือที่ Console และในไฟล์ app.log นะครับ ซึ่งจะมีหน้าตาประมาณนี้เลยนะครับ

{\"level\":\"info\",\"ts\":\"2024-03-15T10:30:00.123+0700\",\"caller\":\"main/main.go:30\",\"msg\":\"แอพของเราเริ่มทำงานแล้วครับ\",\"service\":\"billing-api\",\"version\":\"1.0.0\"}
{\"level\":\"warn\",\"ts\":\"2024-03-15T10:30:00.456+0700\",\"caller\":\"main/main.go:37\",\"msg\":\"มีบางอย่างดูแปลกๆ นะครับ\",\"module\":\"payment-gateway\",\"transaction_id\":98765,\"response_time\":\"500ms\"}
{\"level\":\"error\",\"ts\":\"2024-03-15T10:30:00.789+0700\",\"caller\":\"main/main.go:45\",\"msg\":\"เจอข้อผิดพลาดร้ายแรง ครับ\",\"component\":\"database\",\"error\":\"เชื่อมต่อฐานข้อมูลไม่ได้\",\"stacktrace\":\"goroutine 1 [running]:\
main.main()\
\\t/path/to/main.go:46 +0x...\"}

เป็นไงครับ อ่านง่าย จัดเป็นระบบมากๆ เลยใช่ไหมครับ ทำให้เราหาสิ่งที่ต้องการได้ง่ายขึ้นเยอะเลย

ขั้นตอนที่ 3: เชื่อม Log กับ CloudWatch

ทีนี้มาถึงการเชื่อมกับ CloudWatch นะครับ Log ที่เป็น JSON แบบที่เราได้จาก zap เนี่ย มันเหมาะมากๆ เลยนะครับ ที่จะเอาไปใช้กับ CloudWatch Logs ของ AWS

สิ่งที่เราต้องทำนะครับ คือการติดตั้ง CloudWatch Agent บน Server หรือ Container ที่รันแอพ Go ของเราครับ จากนั้นเราก็แค่กำหนดค่าในไฟล์ Config ของ Agent (ปกติจะเป็น amazon-cloudwatch-agent.json) ให้มันไปอ่านไฟล์ Log ของเรา เช่น app.log ที่เราสร้างขึ้นมานะครับ แบบนี้นะครับ

ตัวอย่าง Config ของ CloudWatch Agent (บางส่วนนะครับ)

{
\t\"logs\": {
\t\t\"logs_collected\": {
\t\t\t\"files\": {
\t\t\t\t\"collect_list\": [
\t\t\t\t\t{
\t\t\t\t\t\t\"file_path\": \"/path/to/your/app.log\", // แก้ไขเป็น Path จริงของไฟล์ Log ของคุณนะครับ
\t\t\t\t\t\t\"log_group_name\": \"MyGoZapLogs\", // ชื่อ Log Group ใน CloudWatch
\t\t\t\t\t\t\"log_stream_name\": \"{instance_id}\", // ใช้ instance_id หรือชื่อที่เหมาะกับคุณนะครับ
\t\t\t\t\t\t\"timestamp_format\": \"%Y-%m-%dT%H:%M:%S%Z\", // ต้องตรงกับเวลาที่เรา encode ด้วย zap นะครับ
\t\t\t\t\t\t\"multi_line_start_pattern\": \"^{\", // บอก Agent ว่า Log JSON แต่ละบรรทัดเริ่มที่ '{'
\t\t\t\t\t\t\"encoding\": \"utf-8\"
\t\t\t\t\t}
\t\t\t\t]
\t\t\t}
\t\t}
\t}
}

พอ CloudWatch Agent มันอ่านไฟล์ app.log ของเราได้แล้วนะครับ Log ทุกบรรทัดก็จะถูกส่งไปที่ CloudWatch Logs ใน Log Group ที่ชื่อ MyGoZapLogs โดยอัตโนมัติครับ เราก็จะสามารถเข้าไปดู Log, ค้นหา Log, หรือแม้แต่สร้าง Dashboard และ Alert จาก Log ใน CloudWatch ได้แบบสบายๆ เลยล่ะครับ

แค่นี้เองครับ ผม เราก็ได้ระบบจัดการ Log ที่เป็นระเบียบ อ่านง่าย แล้วก็ส่งไป มอนิเตอร์ ที่ CloudWatch ได้แล้วครับ หวังว่าบทความนี้จะเป็นประโยชน์กับเพื่อนๆ โปรแกรมเมอร์ทุกคน นะครับ ลองเอาไปปรับใช้กับโปรเจกต์ของตัวเองดูนะครับผม

แหล่งที่มา:\ - Uber-Go/Zap GitHub Repository\ - AWS CloudWatch Logs Documentation

Read more

ดราม่า "เบิร์ด วันว่างๆ" กับยาแนว: บทเรียนสำคัญของอินฟลูเอนเซอร์และความปลอดภัยบนโซเชียล

ดราม่า "เบิร์ด วันว่างๆ" กับยาแนว: บทเรียนสำคัญของอินฟลูเอนเซอร์และความปลอดภัยบนโซเชียล

เจาะลึกดราม่า "เบิร์ด วันว่างๆ" ใช้ยาแนวเล่นสงกรานต์ คำชี้แจง และผลกระทบต่อสังคม บทเรียนสำคัญสำหรับความรับผิดชอบของอินฟลูเอนเซอร์

By ทีมงาน devdog
Xiaomi 17T หลุดสเปคเด็ดบน Geekbench! ยืนยัน Dimensity 8500 พร้อมแบต 7,000mAh จ่อเปิดตัว

Xiaomi 17T หลุดสเปคเด็ดบน Geekbench! ยืนยัน Dimensity 8500 พร้อมแบต 7,000mAh จ่อเปิดตัว

Xiaomi 17T เตรียมเปิดตัว! พบข้อมูลบน Geekbench ยืนยันใช้ชิป Dimensity 8500 พร้อมแบตเตอรี่จุใจ 7,000mAh คาดบุกตลาดรองเรือธงเร็วๆ นี้

By ทีมงาน devdog
กยศ. เปิดทางรอด! ปรับโครงสร้างหนี้ออนไลน์ หยุดถูกฟ้อง ก่อน 5 ก.ค. 69

กยศ. เปิดทางรอด! ปรับโครงสร้างหนี้ออนไลน์ หยุดถูกฟ้อง ก่อน 5 ก.ค. 69

ผู้กู้ กยศ. กว่า 1 แสนราย เสี่ยงถูกฟ้อง! รีบปรับโครงสร้างหนี้ออนไลน์ผ่านเป๋าตัง/ThaID ก่อน 5 ก.ค. 69 รับสิทธิประโยชน์ ลดดอกเบี้ย หลีกเลี่ยงคดีความ

By ทีมงาน devdog
Baseus MC2: หูฟังคลิปหนีบหูสุดล้ำ แบตอึด 60 ชม. เสียง LDAC กันน้ำ IP67 เพื่ออิสระทางเสียงของคุณ

Baseus MC2: หูฟังคลิปหนีบหูสุดล้ำ แบตอึด 60 ชม. เสียง LDAC กันน้ำ IP67 เพื่ออิสระทางเสียงของคุณ

พบ Baseus MC2 หูฟังคลิปหนีบหูดีไซน์ล้ำ สวมใส่สบายตลอดวัน ด้วยแบตเตอรี่ 60 ชม., กันน้ำ IP67, เสียง LDAC ระดับ Hi-Res และราคาเข้าถึงง่าย

By ทีมงาน devdog