- 2020년 1형 당뇨병 진단을 받은 엔지니어가 혈당 관리를 장애 대응처럼 다루기 위해 Go 기반 개인 모니터링 시스템을 만듦
- Libre에 공식 API나 SDK가 없어 직접 연동하기 어려웠지만, Miao Miao와 Tomato 앱을 거쳐 2분마다 혈당 데이터를 외부 엔드포인트로 보내는 경로를 찾음
- Go echo server와 Encore 배포로 Tomato 요청 형식을 확인하고, SGV 값을 변환해 Grafana 대시보드에 실시간 혈당 게이지로 표시함
- Telegram 봇은 식사·운동·인슐린 주입 같은 맥락 주석을 남기고, 5분마다 실행되는 cron은 혈당이 기준치 아래로 떨어지면 incident.io 인시던트를 생성함
- 알림, 에스컬레이션, 리포트를 붙인 이 워크플로는 낮은 혈당이나 측정 공백을 혼자 감당하지 않게 해주는 개인 건강 관측성 시스템으로 동작함
1형 당뇨병을 장애 대응 문제로 다루기
- 1형 당뇨병은 췌장이 탄수화물을 에너지로 바꾸는 데 필요한 인슐린을 거의 또는 전혀 만들지 못하는 자가면역 질환임
- 인슐린은 주사로 보충해야 하며, 주입량은 먹는 음식과 여러 변수에 따라 달라짐
- 1형은 생활습관 때문에 생기는 병이 아니며, 현재 치료법은 없음
- 혈당은 계속 모니터링해야 함
- 너무 높은 상태가 오래 지속되면 장기 손상과 수명 단축으로 이어질 수 있음
- 너무 낮은 상태는 짧은 시간에도 치명적일 수 있음
- 혈당이 너무 낮아지면 안전하게 먹거나 마실 수 없어 다른 사람의 도움이 필요할 수 있음
- 음식이나 음료를 볼 때마다 탄수화물 양과 필요한 인슐린 양을 계산해야 하고, 짧은 산책 같은 운동도 체내 활성 인슐린 양을 고려해야 함
- 혈당 가시성이 사라지는 상황은 업무 시스템에서 관측성이 0이 되는 상태와 비슷하게 다룰 수 있음
- 업무라면 인시던트를 선언하고, 가시성이 복구될 때까지 닫지 않음
- 낮은 혈당은 무언가 잘못되고 있다는 첫 신호이며 즉시 조치가 필요한 상태임
Libre, Miao Miao, Tomato로 데이터 경로 찾기
- 팔에 부착하는 Libre 기기는 휴대폰을 가까이 대면 혈당을 읽을 수 있음
- 기기는 2주마다 교체해야 함
- 혈당이 특정 빨간 선 아래로 내려가면 탄수화물을 섭취해야 함
- 초록 범위를 벗어나면 인슐린 투여 등 조치를 고려할 수 있음
- 운동, 체중 증가, 질병은 인슐린 민감도를 바꾸기 때문에 혈당 관리는 계속 움직이는 목표가 됨
- Libre가 오류를 보이면 기기에서 혈당을 읽을 수 없고 위험 수준 알림도 받을 수 없음
- Libre는 폐쇄형 기기라 API나 SDK 없이 직접 데이터를 가져오기 어려움
- Miao Miao는 Libre 위에 장착해 약 2분마다 스캔하고 Tomato 앱으로 결과를 보냄
- 기본 Libre 앱이 오류를 보여도 Miao Miao 경로는 데이터를 계속 발행함
- 기본 앱은 혈당의 급격한 변화를 이상치로 보고 데이터를 보내지 않는 것으로 보였고, 사용자는 그런 데이터도 직접 판단하고 싶어 함
- Tomato 앱에는 혈당을 Google Calendar 이벤트로 5분마다 발행하는 기능이 있어 Apple Watch 컴플리케이션으로 혈당을 볼 수 있었음
Go echo server로 Tomato 요청 확인하기
- Tomato 앱 설정에는 Nightscout용 데이터 동기화 URL 입력란이 있었음
- Nightscout URL 대신 직접 운영하는 웹서버 주소를 넣자, 게이트웨이 로그에 다음 경로가 나타남
/id/e1d67817-4591-4e8e-9bca-58a07a1087d8/api/v1/devicestatus
- 요청 본문을 확인하기 위해 Go로 간단한 echo server를 작성함
- 배포와 실행에는 encore.dev를 사용했으며, 무료로 모니터링 시스템을 운영할 수 있었음
// encore:api public raw method=POST path=/id/:id/api/v1/devicestatus
func Echo(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", req.Header.Get("Content-Type"))
if _, err := io.Copy(w, req.Body); err != nil {
http.Error(w, "Failed to echo request", http.StatusInternalServerError)
return
}
}
- echo server는 클라이언트가 보낸 데이터를 그대로 돌려주는 서버로, 네트워크 문제 해결이나 애플리케이션 동작 테스트에 유용함
- 같은 방식으로 다른 요청도 확인해 혈당 데이터가 담긴 응답을 얻음
- date: 1696171541297
sgV: 73
delta: 0
sysTime: 1696171541381
dateString: "2023-10-01T14:45:41.297Z"
_id: "dOUXaI8HcaulCGrQfxe23UE0"
type: "sgv"
device: "Tomato"
direction: "Flat"
sgV는 혈당 값이며, 영국에서 사용하는 단위로 맞추기 위해 18로 나눔
- 응답에는 날짜·시간과 추세 방향도 포함됨
- 이 호출은 2분마다 발생함
Grafana 대시보드와 혈당 게이지
- 수신한 혈당 값을 Gauge metric으로 설정함
- 게이지는 오르내릴 수 있는 단일 수치를 나타내는 메트릭이며, 메모리 사용량, 동시 요청 수, 온도 같은 변동값 측정에 쓰임
- 혈당 값은
newValue / 18로 변환해 기록함
var BloodSugar = metrics.NewGauge[float64]{
name: "blood_sugar",
metrics.GaugeConfig{},
}
BloodSugar.Set(float64(newValue) / 18)
if err := insertReading(ctx, newValue); err != nil {
rlog.Error(msg: "failed to insert blood sugar, proceeding", keysAndValues: "db_err", err)
}
- 혈당 데이터는 최선 노력 방식으로 Postgres에도 저장함
- DB 저장 실패가 나머지 로직을 실패시키지 않도록 로그만 남기고 계속 진행함
- Grafana 대시보드는 2분마다 갱신되는 혈당 게이지를 실시간으로 보여줌
- 목표 혈당 범위는 이상적으로 4에서 9 사이임
- 대시보드를 공유하거나 집에 모니터를 두면 혈당 상태를 한눈에 확인할 수 있음
Telegram 봇으로 혈당 맥락 추가하기
- 혈당 수치만으로는 충분하지 않으며, 복잡한 시스템 모니터링처럼 맥락이 중요함
- 식사 직후 혈당이 올라가더라도 이미 인슐린을 맞았다면 즉시 걱정할 상황이 아닐 수 있음
- Grafana는 주석을 지원하지만, 로그인해서 직접 주석을 남기는 방식은 부담이 큼
- Telegram 봇을 만들어 메시지를 받을 때마다 웹훅으로 주석을 추가하도록 구성함
// encore:api public raw method=POST path=/webhook
func Webhook(w http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "could not read request body", http.StatusBadRequest)
return
}
defer req.Body.Close()
var update Update
err = json.Unmarshal(body, &update)
if err != nil {
http.Error(w, "could not unmarshal JSON", http.StatusBadRequest)
return
}
}
- 웹훅은 요청을 받고, 메시지를 검증하고, JSON을 언마셜한 뒤 처리 결과를 Telegram으로 돌려줌
- 메시지 검증 후 정수를 파싱하고 주석 서비스를 호출함
- 주석 서비스는 활동 내용에 따라 태그를 붙임
func getTags(ctx context.Context, activity string) []string {
a := strings.ToLower(activity)
switch a {
case "walk", "run", "ran", "gym":
return []string{"exercise"}
case "eat", "ate":
return []string{"food eaten"}
case "inject":
return []string{"inject"}
default:
return []string{"other"}
}
}
- Grafana에서는 태그별 색상을 지정하고 주석 쿼리를 설정할 수 있음
- 식사, 운동, 인슐린 주입 같은 맥락이 그래프 위에 표시되면 본인과 대시보드를 보는 사람이 혈당 상황을 더 쉽게 해석할 수 있음
낮은 혈당을 인시던트로 전환하기
- 혈당 가시성은 확보됐지만, 알림이 없으면 실시간 대응이 어려움
- 간호사에게 받은 지침에 따라 혈당이 4 아래로 내려가면 처치를 시작해야 함
- 5분마다 측정값을 확인하는 작은 cron을 작성하고, 사전 정의한 하한선보다 낮으면 인시던트를 트리거함
var _ = cron.NewJob(
id: "monitor-blood",
cron.JobConfig{
Title: "monitor blood to check if there is reason to open an incident",
Every: 5 * cron.Minute,
Endpoint: BloodIncidentCron,
},
)
if r < BloodLowerLimit {
if err := triggerIncident(ctx); err != nil {
return fmt.Errorf("failed to trigger an incident: %w", err)
}
}
- Telegram 봇에는
"i need help" 메시지로 수동 인시던트를 여는 코드도 추가함
- 인시던트가 트리거되면 별도 마이크로서비스가 incident.io에 인시던트를 생성함
- incident.io를 선택한 이유는 Cloudflare의 사내 인시던트 도구와 가장 비슷하고, 완전히 Go로 작성되어 있기 때문임
payload := Payload{
IdempotencyKey: idemKey.String(),
Mode: incidentModeStandard,
Name: fmt.Sprintf("Matt's blood sugar is currently #%s", blood),
SeverityID: incidentSeverityCritical,
Summary: "Matt's blood sugar is low!",
Visibility: "public",
}
- 실제 인시던트 도구를 쓰면 에스컬레이션 정책을 설정하고, 인시던트 상태에서 보낸 시간 같은 리포트를 볼 수 있음
- incident.io 워크플로는 Zapier와 비슷한 규칙을 구성하고 구독자에게 문자 알림을 보낼 수 있음
- 예시 워크플로에서는 인시던트가 열릴 때 본인에게 문자를 보내고, 20분 동안 닫히지 않으면 파트너나 형제자매에게 자동 에스컬레이션함
리포트와 향후 개선
- 인시던트 도구를 사용하면 혈당 저하 인시던트가 시간에 따라 얼마나 발생했는지 리포트로 볼 수 있음
- 예시 그래프는 블로그용 데모 데이터이며, 실제 데이터는 다행히 더 지루한 수준임
- 낮은 혈당 이벤트가 증가 추세라면 현재 치료 방식이 맞지 않는 신호일 수 있고, 의사와 상담할 근거가 됨
- 현재 또는 계획 중인 개선 항목은 다음과 같음
- 자동 종료: 지금은 데이터를 검토하기 위해 수동으로 인시던트를 닫지만, 몇 번 늦게 닫아 에스컬레이션된 적이 있어 15분 정도 안정적인 혈당이 유지되면 자동 종료할 수 있음
- 데이터 대화: 수년치 혈당 데이터를 저장하고 있으며, LLM을 학습시켜 “왜 항상 오후 3시에 낮아지는가?” 같은 질문을 하고 Google Calendar 같은 다른 데이터도 함께 활용하고 싶어 함
- 그래프 확장: 현재 그래프는 단순하지만, 당뇨 관리 성공을 판단하는 hba1c 같은 지표를 추가할 수 있음
- 실패 방지 강화: 시스템이 중요하기 때문에 실패를 더 많이 예방할수록 좋음
코딩으로 만든 개인 건강 운영 시스템
- 1형 진단 초기에는 질환이 삶을 어렵게 만들 것이라는 두려움이 있었음
- 이 프로젝트는 1형 상태를 이해하고 관리·모니터링하는 데 도움이 됨
- 혈당 관리를 분산 시스템처럼 다루며, 관측성, 알림, 인시던트, 에스컬레이션, 리포트를 붙인 개인 운영 시스템으로 만들었음
- 코딩 능력은 1920년대까지만 해도 사망 선고였던 질환의 관리를 어느 정도 자동화할 수 있게 해줌