Sentry (Error Tracking)
core.ISentry — ระบบ track error ที่ผูกกับ context เหมือนทุก capability อื่น (ctx.Sentry()) ตั้ง DSN ตัวเดียว แล้ว error / panic / job ที่ fail / breadcrumb ถูกส่งครบโดยไม่ต้องเขียนโค้ดเพิ่ม
APP_SENTRY_DSN=https://[email protected]/123เท่านี้ — ไม่ต้องแก้ handler, ไม่ต้อง init เอง, ไม่ต้อง defer sentry.Recover()
ไม่มี DSN = no-op ทุก method ของ
ctx.Sentry()เรียกได้ปลอดภัยเสมอ (คืน"") จึงเขียนโค้ดแบบเดียวกันได้ทั้ง dev / test / prod โดยไม่ต้องif
สิ่งที่ถูกส่งอัตโนมัติ
| เหตุการณ์ | ทำอะไรให้ | จุดที่ทำงาน |
|---|---|---|
ctx.NewError(...) ที่ status ≥ 500 | capture ทันทีตรงจุดที่สร้าง error (ยังมี context ครบที่สุด) พร้อมผูก event id กลับเข้า error | context.go |
error ที่ handler return ออกมา | capture ถ้ายังไม่เคยถูก capture + ใส่ tag http.method / http.route | HTTP middleware |
ctx.Log() ที่พก error status ≥ 500 | capture — error ที่ "จัดการเองแล้ว log ทิ้งไว้" จึงไม่หายไปจาก Sentry | logger bridge |
| panic ใน handler | capture ที่ level fatal + tag panic=true แล้วตอบ 500 (ไม่ crash) | recover middleware |
| job run fail | capture พร้อม tag job / run_id / attempt / queue / trigger + log tail | job runner |
| scheduler enqueue fail | capture (tick ที่ไม่ได้กลายเป็น run จะไม่มีที่อื่นบันทึกเลย) | scheduler |
ทุกบรรทัด ctx.Log() | กลายเป็น breadcrumb ของ request/run นั้น | logger bridge |
HTTP call ขาออก (ctx.Requester()) | breadcrumb http.client (method, url, status, duration) | resty hook |
mq.Publish | breadcrumb mq.publish | mq |
| query ผ่าน GORM | breadcrumb db.* (ต้อง opt-in — ดูหัวข้อ Database) | InstrumentGorm |
ทุก event ไม่ว่ามาทางไหน มาพร้อมชุดเดียวกัน: user (ctx.GetUser()), scoped data (ctx.GetAllData()), config ที่ scrub แล้ว, request_id, mode (http/cron/mq), request + body ที่ scrub แล้ว, breadcrumb และ stack trace ของ จุดที่ error เกิด (ไม่ใช่จุดที่ capture)
กติกาข้อเดียว: status ≥ 500 = incident
ตัวตัดสินว่าจะยิงเข้า Sentry ไหมคือ status ของ error ไม่ใช่ log level และ ไม่ใช่ว่าคุณเรียก method ไหน
| เขียนแบบนี้ | ได้อะไร |
|---|---|
ctx.Log().Info("...") | breadcrumb |
ctx.Log().Error("...") (ไม่มี error) | breadcrumb — ข้อความเฉยๆ ไม่ใช่ incident |
ctx.Log().Error("...", "err", err400) | breadcrumb — client ผิด ไม่ใช่ incident |
ctx.Log().Error("...", "err", err500) | event + breadcrumb |
return ctx.NewError(err, errmsgs.X) (500) | event |
return errmsgs.BadRequest | ไม่มีอะไร |
ปรับเส้นแบ่งได้ที่ APP_SENTRY_MIN_STATUS — เส้นเดียวกันทั้งระบบ
เขียนโค้ดยังไง (สั้นๆ: เขียนเหมือนเดิม)
ไม่ต้องมีคำว่า Sentry ในโค้ดแอป — log กับ error ที่เขียนอยู่แล้วคือ input ของ Sentry
func Charge(c core.IHTTPContext) error {
log := c.Log().With("component", "billing") // component → breadcrumb category
log.Info("เริ่มตัดบัตร", "order_id", orderID) // → breadcrumb พร้อม data
if err := refreshRate(); err != nil {
// จัดการเองแล้ว ไม่ได้ fail request — แต่ยังอยากรู้
log.Error("ดึงเรตไม่ได้ ใช้เรตเก่า", "err", err) // → event ถ้า err เป็น 5xx
}
if err := gateway.Charge(); err != nil {
// fail + report ในบรรทัดเดียว: args ท้ายสุดกลายเป็น extras ใน Sentry
return c.NewError(err, errmsgs.PaymentFailed, "order_id", orderID)
}
return c.JSON(200, result)
}- error ตัวเดียว = issue เดียว ไม่ว่าจะถูก log ก่อนแล้ว return ทีหลัง หรือกลับกัน (event id ถูกผูกไว้กับตัว error เอง แล้ว layer ที่เหลือเห็นว่ามีแล้วก็ข้าม)
err/errorคือ key ที่ bridge มองหา — ซึ่งเป็นสิ่งที่โค้ดเขียนกันอยู่แล้ว
ctx.Sentry() ไว้ใช้เมื่อไร
เหลือไว้เป็น escape hatch จริงๆ ไม่ใช่ API ที่ต้องเรียนรู้:
c.Sentry().SetTag("tenant", tenantID) // tag ที่ค้นหาได้ (ต้องเป็น low-cardinality)
c.Sentry().StartTransaction("import", "task")
c.Sentry().Hub() // ลง SDK ตรงๆCaptureError / CaptureMessage / Breadcrumb / SetContextData มีให้ครบ แต่ ไม่ต้องใช้ในเส้นทางปกติ เพราะซ้ำกับ ctx.NewError / ctx.Log() / ctx.SetData() ที่ทำให้อัตโนมัติอยู่แล้ว
CaptureOption
c.Sentry().CaptureError(err,
core.CaptureLevel(core.LevelFatal),
core.CaptureTag("tenant", tenantID),
core.CaptureTags(map[string]string{"region": "th"}),
core.CaptureExtra("payload", payload),
core.CaptureContext("order", map[string]any{"id": id, "total": total}),
core.CaptureFingerprint("{{ default }}", "PAYMENT_FAILED"),
)การจัดกลุ่ม issue (grouping)
- ชื่อ issue = error code ไม่ใช่
*core.Error— issue list อ่านเป็นภาษาของ API เอง (PAYMENT_FAILED: gateway said no) - fingerprint default =
["", code]→ ยังจัดกลุ่มตาม stack แต่แยกตาม code - job ที่พัง =
["job", <job name>, <code>]→ job เดียวพัง = issue เดียว ไม่ว่าจะ fail กี่ run
ข้อมูลลับถูก mask ให้เสมอ
v1 ส่ง config ทั้งก้อนเข้า Sentry (รวม DB password, JWT secret, S3 key) v2 scrub ก่อนส่งเสมอ ที่ BeforeSend — จุดสุดท้ายก่อนออกจาก process จึงไม่มีทางรั่วเพราะลืม scrub ที่ capture path ใหม่
mask ทั้ง key ที่ชื่อเข้าข่าย (password, secret, token, authorization, api_key, credential, cookie, session, jwt, otp, cvv, …) ทั้งใน config, scoped data, header, query string, JSON body, breadcrumb และ URL ขาออก
core.NewSentry(env, core.SentryOptions{
ScrubKeys: []string{"citizen_id", "account_no"}, // เพิ่ม key ที่ต้อง mask
ScrubValues: []string{internalAPIKey}, // mask ค่านี้ทุกที่ที่โผล่
})ค่าที่เป็นความลับใน config (DB_PASSWORD, JWT_SECRET, S3_SECRET_KEY, …) ถูกใส่ใน ScrubValues ให้อัตโนมัติ — ต่อให้มันไปโผล่ใน error message ของ driver ก็ยังถูก mask
Request body
body ถูกอ่าน แบบ tee ตามที่ handler อ่านจริง (ไม่ได้อ่านซ้ำ, ไม่ได้อ่านก่อน) เก็บไม่เกิน 16 KiB, ข้าม multipart/binary, และ scrub ก่อนส่ง
APP_SENTRY_CAPTURE_BODY=false # ปิด
APP_SENTRY_MAX_BODY_BYTES=65536 # ขยายเพดานJobs และ Cron monitors
job run = unit of work เหมือน request จึงได้ hub, breadcrumb และ transaction ของตัวเอง ไม่ปนกับ run อื่นที่รันพร้อมกัน
// retry ที่ fail ระหว่างทางไม่ถือเป็น incident — รายงานเมื่อ attempt หมดแล้ว
// เปิดให้รายงานทุก attempt ได้ด้วย:
APP_SENTRY_CAPTURE_RETRIES=trueSentry Crons — เปิดแล้ว job ที่มี Schedule จะส่ง check-in (in_progress → ok/error) ทุกครั้งที่ตารางเวลาสั่งให้รัน ทำให้ Sentry เตือนได้เมื่อ run ที่ควรเกิดแต่ไม่เกิด (process ตาย, scheduler ไม่ทำงาน)
APP_SENTRY_ENABLE_CRONS=true- monitor slug มาจากชื่อ job (
Nightly Report→nightly-report) - ตารางเวลาถูกส่งไปด้วย (cron expression หรือ interval) Sentry จึงรู้ว่า "สาย" คือเมื่อไร
- นับเฉพาะ run ที่ trigger จาก schedule เท่านั้น — trigger เองหรือ replay ไม่ถูกนับ
- ⚠️ เปิดแล้ว Sentry จะสร้าง monitor ในองค์กรอัตโนมัติ (มีโควตาแยก) จึง default = ปิด
Tracing (performance)
APP_SENTRY_ENABLE_TRACING=true
APP_SENTRY_TRACES_SAMPLE_RATE=0.1 # 10% ของ requestเปิดแล้วจะได้:
- transaction ต่อ request (
GET /users/:id— ใช้ route pattern ไม่ใช่ path จริง จึงไม่ระเบิดเป็นล้าน transaction) - transaction ต่อ job run (
job nightly) - span ลูกของทุก HTTP call ขาออก พร้อมส่ง
sentry-trace/baggageheader ต่อ → service ปลายทางที่ใช้ Sentry เดียวกันจะอยู่ใน trace เดียวกัน trace_idถูกแปะในทุกบรรทัด log ของ request นั้น → กระโดดจาก log ไป trace ไป issue ได้
สร้าง span เองได้:
span := c.Sentry().StartTransaction("import ledger", "task")
defer span.Finish(err)
child := span.Child("db.query", "SELECT ledger")
child.Finish(nil)Database (opt-in)
db, _ := core.NewDatabase(env)
_ = core.InstrumentGorm(db, core.SentryGormOptions{
SlowQuery: 200 * time.Millisecond, // ช้ากว่านี้ = breadcrumb ระดับ warning
AllQueries: false, // true = ทุก query (ระวัง breadcrumb เต็ม)
ExcludeSQL: false, // true = ไม่ส่งตัว statement
})
app, _ := core.NewApp(env, core.WithSQL("default", db))default บันทึกเฉพาะ query ที่ ช้า หรือ fail เพราะ breadcrumb มีโควตา 50 อัน ต่อ event — ถ้าใส่ทุก SELECT บรรทัดที่อธิบายสาเหตุจริงจะถูกดันหายไป (ErrRecordNotFound ไม่นับเป็น fail)
Event id ในหน้าเว็บ / support
- response ที่ error จะมี header
X-Sentry-Id - ฝั่ง Go อ่านได้จาก error โดยตรง:
var e *core.Error
if errors.As(err, &e) {
fmt.Println(e.EventID()) // "" ถ้าไม่ได้ส่ง
}เขียน test ว่า error ถูกรายงานจริง
tracker, rec, _ := core.NewRecordingSentry(env)
app, _ := core.NewApp(env, core.WithSentry(tracker))
// ... ยิง request / รัน job ...
require.Equal(t, 1, rec.Len())
require.Equal(t, "PAYMENT_FAILED", rec.Codes()[0])
require.Equal(t, "u-1", rec.Last().User.ID)
require.True(t, rec.HasBreadcrumb("เริ่มตัดบัตร"))NewRecordingSentry รัน path จริงทั้งหมด (scope, scrub, fingerprint, breadcrumb) เปลี่ยนแค่ transport — assertion จึงเชื่อถือได้
Configuration
คำอธิบายละเอียดของทุก key (พร้อมกติกา prefix
APP_และไฟล์.env) อยู่ที่ Configuration (ENV) — ตารางข้างล่างเป็นสรุปย่อ
| Env | Default | ความหมาย |
|---|---|---|
SENTRY_DSN | — | ว่าง = ปิดทั้งระบบ (no-op) |
SENTRY_ENVIRONMENT | ค่า ENV | environment ใน Sentry |
SENTRY_RELEASE | — | version/commit สำหรับ regression tracking |
SENTRY_SERVER_NAME | hostname | ชื่อเครื่อง |
SENTRY_DEBUG | false | log การทำงานของ SDK เอง |
SENTRY_SAMPLE_RATE | 1.0 | สัดส่วน error ที่ส่ง |
SENTRY_TRACES_SAMPLE_RATE | 0 | สัดส่วน transaction ที่ส่ง |
SENTRY_ENABLE_TRACING | false | เปิด transaction (sample rate = 1.0 ถ้าไม่ได้ตั้ง) |
SENTRY_ENABLE_CRONS | false | ส่ง check-in ของ job ที่มี schedule |
SENTRY_MIN_STATUS | 500 | status ต่ำสุดที่ถือว่าเป็น incident |
SENTRY_CAPTURE_RETRIES | false | รายงานทุก attempt ของ job ไม่ใช่เฉพาะครั้งสุดท้าย |
SENTRY_BREADCRUMB_LEVEL | info | ระดับ log ต่ำสุดที่กลายเป็น breadcrumb (off = ปิด) |
SENTRY_MAX_BREADCRUMBS | 50 | จำนวน breadcrumb ต่อ event |
SENTRY_ATTACH_STACKTRACE | true | แนบ stack แม้กับ message |
SENTRY_SEND_DEFAULT_PII | false | ส่ง IP / cookie ตามค่า default ของ SDK |
SENTRY_CAPTURE_BODY | true | แนบ request body (scrub แล้ว) |
SENTRY_MAX_BODY_BYTES | 16384 | เพดาน body |
SENTRY_SEND_ENV | true | แนบ config (scrub แล้ว) |
SENTRY_IGNORE_ERRORS | — | รายการ regex คั่นด้วย , |
SENTRY_IGNORE_TRANSACTIONS | — | รายการ regex คั่นด้วย , |
SENTRY_FLUSH_TIMEOUT | 5 | วินาทีที่รอตอน shutdown |
ทุกค่าตั้งผ่าน core.SentryOptions ได้ด้วย (มีลำดับสูงกว่า env):
tracker, err := core.NewSentry(env, core.SentryOptions{
Release: buildVersion,
ScrubKeys: []string{"citizen_id"},
BeforeSend: func(e *sentry.Event, hint *sentry.EventHint) *sentry.Event {
if e.Tags["tenant"] == "loadtest" {
return nil // ทิ้ง event นี้
}
return e
},
})
app, err := core.NewApp(env, core.WithSentry(tracker))Shutdown
app.Shutdown(ctx) flush event ที่ค้างอยู่ให้เป็นขั้นตอนสุดท้าย — error ที่เกิด ระหว่างปิดระบบจึงยังส่งทัน ไม่ต้องเรียก sentry.Flush เอง
เทียบกับ v1
| v1 | v2 |
|---|---|
sentry.ConfigureScope (global scope) — request ที่รันพร้อมกันเขียนทับ scope กัน | hub ต่อ request/run แยกกันจริง |
ส่ง ENV().All() ดิบๆ รวม password/secret | scrub ที่ BeforeSend เสมอ |
ต้องเรียก CaptureError(...) เอง | error ≥ 500, panic, job fail, log ที่พก 5xx ส่งอัตโนมัติ |
| log กับ Sentry เป็นคนละระบบ ต้องเขียนสองรอบ | ctx.Log() = breadcrumb + event (ถ้าเป็น 5xx) |
| error เดียวถูกรายงานซ้ำหลายชั้น | event id ผูกกับตัว error → issue เดียวเสมอ |
| ไม่มี tracing / cron monitor | transaction + check-in ในตัว |
issue ชื่อ *core.Error | issue ชื่อ error code |
| ทดสอบไม่ได้ | NewRecordingSentry |
ดูเพิ่ม
- Error handling —
IError,Wrap, sentinel - Logger — บรรทัด log ที่กลายเป็น breadcrumb
- Jobs — job runner, retry, replay