Skip to content

Sentry (Error Tracking)

core.ISentry — ระบบ track error ที่ผูกกับ context เหมือนทุก capability อื่น (ctx.Sentry()) ตั้ง DSN ตัวเดียว แล้ว error / panic / job ที่ fail / breadcrumb ถูกส่งครบโดยไม่ต้องเขียนโค้ดเพิ่ม

sh
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 ≥ 500capture ทันทีตรงจุดที่สร้าง error (ยังมี context ครบที่สุด) พร้อมผูก event id กลับเข้า errorcontext.go
error ที่ handler return ออกมาcapture ถ้ายังไม่เคยถูก capture + ใส่ tag http.method / http.routeHTTP middleware
ctx.Log() ที่พก error status ≥ 500capture — error ที่ "จัดการเองแล้ว log ทิ้งไว้" จึงไม่หายไปจาก Sentrylogger bridge
panic ใน handlercapture ที่ level fatal + tag panic=true แล้วตอบ 500 (ไม่ crash)recover middleware
job run failcapture พร้อม tag job / run_id / attempt / queue / trigger + log tailjob runner
scheduler enqueue failcapture (tick ที่ไม่ได้กลายเป็น run จะไม่มีที่อื่นบันทึกเลย)scheduler
ทุกบรรทัด ctx.Log()กลายเป็น breadcrumb ของ request/run นั้นlogger bridge
HTTP call ขาออก (ctx.Requester())breadcrumb http.client (method, url, status, duration)resty hook
mq.Publishbreadcrumb mq.publishmq
query ผ่าน GORMbreadcrumb 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

go
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 ที่ต้องเรียนรู้:

go
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

go
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 ขาออก

go
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 ก่อนส่ง

sh
APP_SENTRY_CAPTURE_BODY=false     # ปิด
APP_SENTRY_MAX_BODY_BYTES=65536   # ขยายเพดาน

Jobs และ Cron monitors

job run = unit of work เหมือน request จึงได้ hub, breadcrumb และ transaction ของตัวเอง ไม่ปนกับ run อื่นที่รันพร้อมกัน

go
// retry ที่ fail ระหว่างทางไม่ถือเป็น incident — รายงานเมื่อ attempt หมดแล้ว
// เปิดให้รายงานทุก attempt ได้ด้วย:
APP_SENTRY_CAPTURE_RETRIES=true

Sentry Crons — เปิดแล้ว job ที่มี Schedule จะส่ง check-in (in_progressok/error) ทุกครั้งที่ตารางเวลาสั่งให้รัน ทำให้ Sentry เตือนได้เมื่อ run ที่ควรเกิดแต่ไม่เกิด (process ตาย, scheduler ไม่ทำงาน)

sh
APP_SENTRY_ENABLE_CRONS=true
  • monitor slug มาจากชื่อ job (Nightly Reportnightly-report)
  • ตารางเวลาถูกส่งไปด้วย (cron expression หรือ interval) Sentry จึงรู้ว่า "สาย" คือเมื่อไร
  • นับเฉพาะ run ที่ trigger จาก schedule เท่านั้น — trigger เองหรือ replay ไม่ถูกนับ
  • ⚠️ เปิดแล้ว Sentry จะสร้าง monitor ในองค์กรอัตโนมัติ (มีโควตาแยก) จึง default = ปิด

Tracing (performance)

sh
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 / baggage header ต่อ → service ปลายทางที่ใช้ Sentry เดียวกันจะอยู่ใน trace เดียวกัน
  • trace_id ถูกแปะในทุกบรรทัด log ของ request นั้น → กระโดดจาก log ไป trace ไป issue ได้

สร้าง span เองได้:

go
span := c.Sentry().StartTransaction("import ledger", "task")
defer span.Finish(err)

child := span.Child("db.query", "SELECT ledger")
child.Finish(nil)

Database (opt-in)

go
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 โดยตรง:
go
var e *core.Error
if errors.As(err, &e) {
    fmt.Println(e.EventID())   // "" ถ้าไม่ได้ส่ง
}

เขียน test ว่า error ถูกรายงานจริง

go
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) — ตารางข้างล่างเป็นสรุปย่อ

EnvDefaultความหมาย
SENTRY_DSNว่าง = ปิดทั้งระบบ (no-op)
SENTRY_ENVIRONMENTค่า ENVenvironment ใน Sentry
SENTRY_RELEASEversion/commit สำหรับ regression tracking
SENTRY_SERVER_NAMEhostnameชื่อเครื่อง
SENTRY_DEBUGfalselog การทำงานของ SDK เอง
SENTRY_SAMPLE_RATE1.0สัดส่วน error ที่ส่ง
SENTRY_TRACES_SAMPLE_RATE0สัดส่วน transaction ที่ส่ง
SENTRY_ENABLE_TRACINGfalseเปิด transaction (sample rate = 1.0 ถ้าไม่ได้ตั้ง)
SENTRY_ENABLE_CRONSfalseส่ง check-in ของ job ที่มี schedule
SENTRY_MIN_STATUS500status ต่ำสุดที่ถือว่าเป็น incident
SENTRY_CAPTURE_RETRIESfalseรายงานทุก attempt ของ job ไม่ใช่เฉพาะครั้งสุดท้าย
SENTRY_BREADCRUMB_LEVELinfoระดับ log ต่ำสุดที่กลายเป็น breadcrumb (off = ปิด)
SENTRY_MAX_BREADCRUMBS50จำนวน breadcrumb ต่อ event
SENTRY_ATTACH_STACKTRACEtrueแนบ stack แม้กับ message
SENTRY_SEND_DEFAULT_PIIfalseส่ง IP / cookie ตามค่า default ของ SDK
SENTRY_CAPTURE_BODYtrueแนบ request body (scrub แล้ว)
SENTRY_MAX_BODY_BYTES16384เพดาน body
SENTRY_SEND_ENVtrueแนบ config (scrub แล้ว)
SENTRY_IGNORE_ERRORSรายการ regex คั่นด้วย ,
SENTRY_IGNORE_TRANSACTIONSรายการ regex คั่นด้วย ,
SENTRY_FLUSH_TIMEOUT5วินาทีที่รอตอน shutdown

ทุกค่าตั้งผ่าน core.SentryOptions ได้ด้วย (มีลำดับสูงกว่า env):

go
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

v1v2
sentry.ConfigureScope (global scope) — request ที่รันพร้อมกันเขียนทับ scope กันhub ต่อ request/run แยกกันจริง
ส่ง ENV().All() ดิบๆ รวม password/secretscrub ที่ BeforeSend เสมอ
ต้องเรียก CaptureError(...) เองerror ≥ 500, panic, job fail, log ที่พก 5xx ส่งอัตโนมัติ
log กับ Sentry เป็นคนละระบบ ต้องเขียนสองรอบctx.Log() = breadcrumb + event (ถ้าเป็น 5xx)
error เดียวถูกรายงานซ้ำหลายชั้นevent id ผูกกับตัว error → issue เดียวเสมอ
ไม่มี tracing / cron monitortransaction + check-in ในตัว
issue ชื่อ *core.Errorissue ชื่อ error code
ทดสอบไม่ได้NewRecordingSentry

ดูเพิ่ม

  • Error handlingIError, Wrap, sentinel
  • Logger — บรรทัด log ที่กลายเป็น breadcrumb
  • Jobs — job runner, retry, replay

Maintained by Passakon Puttasuwan & Dev Core Team.