Dùng Golang Duy Trì Sự Sống: Hệ Thống Incident Management Tự Động Cho Bệnh Tiểu Đường Type 1

Dùng Golang Duy Trì Sự Sống: Hệ Thống Incident Management Tự Động Cho Bệnh Tiểu Đường Type 1

Mục lục

Bia hơi hoặc một đĩa salad ngỡ như lành mạnh chứa bao nhiêu gram carbohydrate? Với đại đa số mọi người, câu trả lời thường là “ai mà quan tâm”, ngoại trừ những người đang theo chế độ ăn kiêng khắt khe. Nhưng đối với khoảng 8 triệu người mắc bệnh Tiểu đường Type 1 (T1D) trên thế giới, đây là câu hỏi sống còn diễn ra mỗi ngày.

Tiểu đường Type 1 là một căn bệnh tự miễn khiến tuyến tụy ngừng sản xuất Insulin - hormone bắt buộc để chuyển hóa carbohydrate thành năng lượng. Nếu chỉ số đường huyết (Blood Glucose) tăng quá cao trong thời gian dài, cơ thể sẽ chịu tổn thương nội tạng vĩnh viễn. Ngược lại, nếu đường huyết tụt quá thấp (Hypoglycemia, hạ đường huyết) dù chỉ trong vài phút, người bệnh có thể bị ngất, liệt tạm thời và tử vong nếu không được hỗ trợ kịp thời.

Bài viết này phân tích giải pháp kiến trúc của Matt Boyle (Staff Engineer) - người đã vận dụng tư duy thiết kế hệ thống phân tán, Go microservices, Prometheus telemetry và incident escalation workflow để tự động hóa việc theo dõi chỉ số sinh tốn của chính mình.

Bài toán Incident Management của cơ thể người

Trong kỹ thuật hệ thống, chúng ta thường nói về SLA 99.99%, MTTD (Mean Time to Detect) và MTTR (Mean Time to Resolve). Đối với một bệnh nhân T1D, việc quản lý cơ thể về bản chất chính là một quy trình Incident Management 24/7:

  1. Detection (Phát hiện): Liên tục đo nồng độ đường huyết trong máu theo thời gian thực.
  2. Analysis (Phân tích): Đánh giá xu hướng tăng/giảm dựa trên lượng Carb đã nạp, liều Insulin đã tiêm và mức độ vận động thể chất.
  3. Mitigation (Xử lý sự cố): Nạp thêm Carb cấp tốc khi đường huyết đi vào vùng nguy hiểm (< 4.0 mmol/L).

Warning

Sự cố từ phần mềm thương mại: Các thiết bị cảm biến y tế đeo trên tay (như Libre) đi kèm app ứng dụng trên smartphone. Tuy nhiên, khi chỉ số đường huyết tụt dốc đột ngột (điểm rơi nguy hiểm nhất), phần mềm thương mại thường gặp lỗi xử lý dữ liệu bất thường (anomaly threshold) và ngừng hiển thị kết quả, khiến người dùng hoàn toàn mất dấu sinh hiệu đúng vào thời khắc sinh tử.

Để giải quyết triệt để sự thiếu tin cậy này, giải pháp đặt ra là: Reverse-engineer luồng dữ liệu cảm biến và tự xây dựng pipeline giám sát riêng bằng Golang.

Kiến trúc tổng thể hệ thống Telemetry & Escalation

Luồng dữ liệu được thiết kế tách biệt ranh giới giữa thu thập dữ liệu (Data Ingestion), giám sát (Observability), cảnh báo (Alerting Engine) và phân cấp cứu hộ (Incident Escalation).

graph TD
    subgraph "Physical Boundary (IoT Edge)"
        Sensor[Libre Sensor trên cánh tay] -->|NFC/Bluetooth| Bridge[MiaoMiao / Tomato App]
    end

    subgraph "Logical Boundary (Go Backend Core)"
        Bridge -->|HTTP POST 2-min| EchoServer[Go Echo Server / Encore.dev]
        EchoServer -->|Update Gauge| Prom[Prometheus Metrics]
        EchoServer -->|Query| Grafana[Grafana Dashboard]
        
        Cron[Go Alert Cron - 5min] -->|Check Threshold < 4.0| EchoServer
        TelegramBot[Telegram Bot - Manual Panic] -->|User Command| IncidentSvc[Incident Microservice]
        Cron -->|Trigger Incident| IncidentSvc
    end

    subgraph "External Alerting & Escalation"
        IncidentSvc -->|HTTP POST + Idempotency Key| IncIO[incident.io API]
        IncIO -->|SMS Cảnh báo| Self[Điện thoại cá nhân]
        IncIO -->|Escalate sau 20 phút không ACK| Family[Điện thoại người thân / Cấp cứu]
    end

Step 1: Intercept luồng telemetry và Go Echo Server

Khi kiểm tra phần cài đặt của ứng dụng trung gian Tomato, tác giả phát hiện tùy chọn cho phép cấu hình URL Data Sync hướng tới nền tảng mã nguồn mở Nightscout. Thay vì trỏ tới Nightscout, tác giả trỏ URL về một Go Echo Server tự dựng để bắt toàn bộ request HTTP body.

Tác giả sử dụng framework Encore.dev để khởi chạy server Go siêu nhẹ. Hàm Endpoint nhận dữ liệu có dạng:

  • Go Echo Handler
  • Payload JSON từ Sensor
// encore:api public raw method=POST path=/id/:id/api/v1/devicestatus
func Echo(w http.ResponseWriter, req *http.Request) {
    // Thiết lập Content-Type tương ứng với request
    w.Header().Set("Content-Type", req.Header.Get("Content-Type"))
    
    // Copy trực tiếp body request sang ResponseWriter để đọc log
    if _, err := io.Copy(w, req.Body); err != nil {
        http.Error(w, "Failed to echo request", http.StatusInternalServerError)
        return
    }
}
{
  "date": 1696171541297,
  "sgv": 73,
  "delta": 0,
  "sysTime": 1696171541381,
  "dateString": "2023-10-01T14:45:41.297Z",
  "_id": "dOUXaI8HcaulCGrQfxe23UE0",
  "type": "sgv",
  "device": "Tomato",
  "direction": "Flat"
}

Trong đó:

  • sgv (Sensor Glucose Value): Chỉ số đường huyết thô. Tại Anh, giá trị này được chia cho 18 để quy đổi sang đơn vị mmol/L chuẩn.
  • direction: Vectơ xu hướng (Flat: ổn định, SingleDown: đang giảm, FortyFiveDown: giảm từ từ).
  • Chu kỳ gửi data: 2 phút / lần.

Step 2: Telemetry & Observability với Prometheus & Grafana

Ngay khi nhận được payload hợp lệ từ sensor, Go server sẽ cập nhật chỉ số vào một Prometheus Gauge metric:

var BloodSugar = metrics.NewGauge[float64](
    name: "blood_sugar",
    metrics.GaugeConfig{},
)

func ProcessReading(sgv float64) {
    // Quy đổi từ mg/dL sang mmol/L
    mmolValue := sgv / 18.0
    BloodSugar.Set(mmolValue)
}

Trực quan hóa dữ liệu trên Grafana cho phép gắn các Annotation Query: khi nào ăn nạp Carb, khi nào tiêm bao nhiêu đơn vị Insulin, khi nào tập thể dục. Việc này giúp biến dữ liệu sinh học hỗn loạn thành biểu đồ hệ thống có ngữ cảnh (Contextual Observability).

Step 3: Engine Cảnh báo & Chống trùng lặp (Idempotency)

Visibility tốt mới chỉ là một nửa chặng đường. Nếu người bệnh đang ngủ mà đường huyết hạ xuống dưới 4.0 mmol/L, hệ thống bắt buộc phải chủ động báo động.

1. Alert Cron tự động trong Go

Sử dụng Cron Engine tích hợp sẵn trong Encore để kiểm tra chỉ số sinh hiệu mỗi 5 phút:

var _ = cron.NewJob("monitor-blood", cron.JobConfig{
    Title:    "Monitor blood to check if there is reason to open an incident",
    Every:    5 * cron.Minute,
    Endpoint: BloodIncidentCron,
})

func BloodIncidentCron(ctx context.Context) error {
    currentReading := getLatestReading()
    const BloodLowerLimit = 4.0

    if currentReading < BloodLowerLimit {
        if err := triggerIncident(ctx, currentReading); err != nil {
            return fmt.Errorf("failed to trigger incident: %w", err)
        }
    }
    return nil
}

2. Manual Panic via Telegram Bot

Bổ sung nút bấm cứu hộ thủ công qua Telegram Bot khi cảm thấy hoa mắt chóng mặt:

if strings.ToLower(update.Message.Text) == "i need help" {
    _, err := incident.Create(req.Context(), "low")
    if err != nil {
        rlog.Error("error creating incident", "err", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    writeBackToTelegram(req.Context(), "Ack, incident opened. Godspeed.")
}

3. Tích hợp Incident.io & Idempotency Key

Một trong những thiết kế quan trọng nhất là việc sử dụng Idempotency Key (UUIDv4) khi gọi API tới incident.io. Trong trường hợp mạng chập chờn hoặc Cron retry liên tục, hệ thống không được phép spam hàng chục Incident trùng lặp.

// encore:api public path=/incident/:blood
func Create(ctx context.Context, blood string) (*Response, error) {
    idemKey, _ := uuid.NewV4()
    
    payload := Payload{
        IdempotencyKey: idemKey.String(),
        Mode:           "standard",
        Name:           fmt.Sprintf("Matt's blood sugar is currently critical: %s", blood),
        SeverityID:     "severity_critical",
        Summary:        "Matt's blood sugar dropped below 4.0 mmol/L threshold!",
        Visibility:     "public",
    }
    
    data, err := json.Marshal(payload)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal incident request: %w", err)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.incident.io/v2/incidents", bytes.NewBuffer(data))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", secrets.IncidentAPIKey))

    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("failed incident.io req: %w", err)
    }
    defer resp.Body.Close()

    return &Response{Message: blood}, nil
}

Dynamic Escalation Policy: Bảo hiểm tính mạng 2 lớp

Tại sao lại dùng công cụ quản lý sự cố doanh nghiệp như incident.io cho sức khỏe cá nhân? Câu trả lời nằm ở Escalation Workflows:

Tip

Quy trình leo thang sự cố (Escalation Chain):

  1. Trigger Incident: Ngay khi Incident mở ra, hệ thống tự động gửi tin nhắn SMS khẩn cấp tới điện thoại cá nhân của Matt.
  2. ACK Timeout (20 phút): Nếu sau 20 phút Matt không bấm nút Acknowledge/Resolve (dấu hiệu cho thấy đã ngất hoặc không thể tự nạp đường), hệ thống tự động leo thang (Escalate).
  3. Broadcast Cấp cứu: Workflow chuyển cảnh báo SMS/Cuộc gọi tới bạn đời và người thân trong gia đình kèm vị trí để đến ứng cứu kịp thời.

Bài học System Design áp dụng cho đời thực

Nhờ áp dụng tư duy lập trình Backend và hệ thống phân tán, tác giả không chỉ tự bảo vệ tính mạng mà còn thu thập dữ liệu báo cáo sự cố (Incident rate over time). Việc phân tích xu hướng tần suất bị hạ đường huyết giúp làm việc với bác sĩ chuyên khoa để điều chỉnh lại liều lượng tiêm Insulin phù hợp với nhịp sống thực tế.

Khả năng lập trình không chỉ dừng lại ở việc xây dựng sản phẩm cho doanh nghiệp hay tối ưu hóa truy vấn Database. Khi được vận dụng đúng chỗ, nó thực sự là một “siêu năng lực” giúp kiểm soát rủi ro và duy trì sự sống.

Chia sẻ :

Bài viết liên quan

Khi nào nên dùng cache (và khi nào không)

Khi nào nên dùng cache (và khi nào không)

Có một bug production mà đến giờ mình vẫn nhớ như in: user A hủy đơn hàng, nhưng app vẫn hiển thị “đang giao” trong 10 phút tiếp theo. Support nhận 30 ticket trong 1 buổi sáng. Nguyên nhân: cache TTL 10 phút, nhưng không ai invalidate khi order status thay đổi.

Đọc thêm
API Filtering: gọi dữ liệu như dân sành cà phê

API Filtering: gọi dữ liệu như dân sành cà phê

Năm đầu đi làm, mình từng viết một endpoint GET /api/menus trả về… toàn bộ menu. 200 items mỗi lần gọi. JSON nặng 1.2MB. Frontend chỉ cần tên + giá của 10 món đang active. Mình nhớ câu đầu tiên anh lead nói: “Mày đang gửi cả cái kho hàng cho người ta chỉ cần xem menu đấy à?”

Đọc thêm
Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng

Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng

Hồi mới code Go, mình có một bug production kỳ lạ: cứ sau 3-4 ngày chạy, service tự dưng ngốn 8GB RAM rồi OOM. Mò suốt chiều thứ Sáu mới phát hiện: một cái goroutine không bao giờ kết thúc vì mình quên truyền context xuống gRPC call. Mỗi request leak một goroutine, sau 100K request thì server “đội mồ” luôn.

Đọc thêm