Chuyển đến nội dung
Phase 1: foundation và observability spine

Phase 1: foundation và observability spine

15 tháng 10, 2026

Đã xảy ra gì

Xây giai đoạn 1 của hango: module, cấu hình env, composition root, http.ServeMux của stdlib với một router nhỏ, các health endpoint, xả việc gọn gàng khi tắt, và package telemetry (một tracer, meter và logger provider, otelslog làm logger mặc định, metric của Go runtime). Thêm cấu hình collector, compose stack (hango, postgres, otelcol, lgtm), Dockerfile, Makefile và cấu hình golangci-lint v2.

Đã kiểm chứng trực tiếp: /readyz trả 200 kèm X-Request-Id và X-Trace-Id; trace có trong Tempo, dòng log của nó có trong Loki theo trace_id, go_* và http_server_request_duration_seconds_* có trong Prometheus; SIGTERM xả việc và thoát với mã 0 trong khoảng 1 giây.

Vấn đề và cách sửa

  • Span bị đặt tên GET và metric thời lượng thiếu http.route. Nguyên nhân: requestID truyền r.WithContext(ctx), một bản sao, nên mux đặt Pattern lên bản sao trong khi otelhttp đọc nó từ bản gốc. Cách sửa: chép inner.Pattern ngược lại sau next.ServeHTTP. Được server_test.go phát hiện.
  • Collector 0.161 cảnh báo "otlp" alias is deprecated; use "otlp_grpc" và tương tự cho otlphttp. Đã đổi tên các exporter.
  • Collector log lỗi từ chối kết nối tới lgtm:4317 lúc khởi động. Compose giờ chờ lgtm khỏe rồi mới chạy.
  • Một request thành công không log gì, nên Loki không có dòng nào để tìm theo trace id. Thêm middleware logRequests (một dòng cho mỗi request, bỏ qua /healthz) kèm test.
  • Xuất sang Langfuse trả 401 khi chưa đặt key. Đã đổi sau review, xem bên dưới.

Sửa theo review

Một lần code review không tìm thấy gì chặn được. Đã sửa: traceparent đến giờ chỉ là một link (WithPublicEndpointFn), nên client không thể chọn trace id hay ép lấy mẫu; /readyz giấu nội dung lỗi của check; một check bỏ qua context của nó không còn làm readiness bị treo; việc tắt telemetry chia ngân sách của mình để log vẫn được xả; compose stop_grace_period: 35s; Ctrl-C lần hai thoát cưỡng bức; *Error không có status trở thành 500; các cổng chỉ bind vào 127.0.0.1.

Chặn Langfuse theo điều kiện: khi không có key, collector vẫn gửi span tới Langfuse Cloud (bị từ chối với 401), và từ giai đoạn 3 các span đó sẽ mang nội dung prompt. Quyết định: nhánh Langfuse chỉ chạy khi LANGFUSE_PUBLIC_KEY được đặt. filter/langfuse_disabled bỏ mọi span trên nhánh đó khi key rỗng. Đã kiểm chứng: không có key thì không xuất; key giả thì 401.

Quyết định

  • lgtm chỉ publish 3000/3200/9090 ra host; 4317/4318 của nó sẽ đụng với otelcol và chỉ otelcol gửi tới nó.
  • Cờ -healthcheck trên binary vì image distroless không có shell hay curl.
  • G101 được tắt inline trên gen_ai.request.max_tokens (đó là tên attribute, không phải credential).

Bước tiếp theo

  • Thêm key Langfuse vào .env và xác nhận cùng trace đó xuất hiện trong Langfuse (tiêu chí mở cuối cùng).
  • Giai đoạn 2: persistence và tenancy core; đăng ký check readiness cho Postgres.

Bản ghi công việc lịch sử, không phải căn cứ lâu dài. Hãy ưu tiên docs/specs/ADR cho các quyết định hiện hành.