L3 - Nền tảng giám sát máy chủ - Collector - Alerting, Events, Outbox
Quy ước tên: L3 - <P&L> - <Hệ thống L2> - <Thành phần>. P&L: chưa chỉ định. Hệ thống L2: Collector. Thành phần: Detector (phát hiện agent im lặng), Events (ID và seq), Outbox (hàng đợi sự kiện bền) và Sender (gửi tới Access Hub). Mã: internal/alerting, internal/events, internal/outbox, nối dây trong internal/app/wiring.go.
Quy ước đánh dấu: "Đã làm" là có mã và kiểm thử. "Chỉ thiết kế" là chỉ có trong docs/. "Đề xuất" là ý của tác giả bản nháp. Mã thắng khi lệch tài liệu. Khóa Redis mang tiền tố redis.prefix (mặc định ah:).
0. Front matter
Thông tin tài liệu đầy đủ
| Trạng thái | BẢN NHÁP, phiên bản 0.1, 2026-09-30. Người phê duyệt: chưa chỉ định |
| Thành phần (Component) | Detector, Reporter (last_seen, heartbeat), Events (IDGen, SeqAllocator, Builder), Outbox (bộ nhớ, tệp, Redis, discard), Sender và Lease |
| Truy vết L2 | L2 SAD Collector: thành phần "Detector", "Outbox, Sender" (mục 2.3, 6.1), FR phát hiện agent down và gửi sự kiện (mục 3), NFR độ tin cậy sự kiện (mục 4), mục 7.1 (Event), rủi ro AR-001, AR-005, AR-006. Mục tiêu L1: G2 (qua L2) |
| Tài liệu cha | L2 SAD Collector |
| Tài liệu anh em | Ingest API, Registry, Enroll, Presence, Bus, Worker, TSDB, Admin API |
| Tier | Đề xuất Cấp 3 (kế thừa L2, chưa xác nhận) |
| Phân loại dữ liệu | Trung bình: định danh máy chủ và thời điểm mất tín hiệu. Không chứa bí mật |
| Blast radius | Bỏ sót hoặc lặp sự kiện down làm Access Hub báo sai hoặc không báo. Mất sự kiện khi tiến trình chết đúng lúc (AR-001) |
| Sign-off gate | Chưa chỉ định. Không có ai đã sign-off |
Truy vết mục L3 đến thành phần L2:
| Mục L3 | Thành phần hoặc mục L2 |
|---|---|
| 1 Phạm vi | 2.3 Các thành phần chính |
| 2 Yêu cầu | 3 Functional, 4 NFR |
| 3 Kiến trúc | 6.1 Component table |
| 4, 6 Domain, dữ liệu | 7.1 Data Model |
| 5 Hợp đồng | 6.2 Integration (Sender đến Access Hub) |
| 7 Thuật toán | 8.3 (luồng agent down) |
| 8, 9 Lỗi, suy thoái | 6.3 Resilience, 12.2 Reliability |
| 10 Đồng thời | 6.3 (lease người gửi) |
| 11 Bảo mật | 9 Security |
| 13 Telemetry | 13 Observability |
1. Scope and Non-Goals
1.1 Vai trò
Collector là nơi duy nhất biết "agent đã im lặng bao lâu" (nhìn từ phía dữ liệu đẩy vào). Detector quét presence, quyết định agent down hoặc up, tạo sự kiện có ID duy nhất và số thứ tự seq, ghi vào Outbox bền, rồi Sender gửi tới Access Hub với ngữ nghĩa at-least-once (Access Hub khử trùng theo event_id). Access Hub là nơi tạo cảnh báo và thông báo; Collector không gửi thông báo.
1.2 Context diagram
flowchart LR
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff;
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff;
classDef entity fill:#3a3320,stroke:#d9b84a,color:#fff;
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff;
PS["Presence"]:::bc
DET["Detector"]:::owned
EV["Events builder"]:::owned
OB[("Outbox")]:::datastore
SD["Sender"]:::owned
HUB(["Access Hub (ext)"]):::entity
DET -->|"quét agent quá hạn"| PS
DET -->|"tạo sự kiện"| EV
DET -->|"ghi bền"| OB
SD -->|"đọc hàng đợi"| OB
SD -->|"POST events"| HUB
DET -->|"seen, heartbeat"| HUB| Chiều | Bên | Nội dung |
|---|---|---|
| Vào | Presence | Expired, DrainSeen, Counts, CompanyCounts |
| Vào | Ingest API | Gọi OnAgentUp khi Touch khôi phục agent đang down |
| Ra | Outbox | Append (bền trước khi trả về) |
| Ra | Access Hub | POST /events, POST /agents/seen, POST /heartbeat |
| Ra | Redis | Khóa ob:* và lease người gửi outbox:sender |
1.3 Trong phạm vi
- Phát hiện agent down và up theo ngưỡng.
- Sinh
event_id(UUIDv7) vàseqđơn điệu theo (máy chủ, luật). - Outbox nhiều backend và quy tắc dead-letter.
- Sender: lô, lùi mũ, tách lô bị từ chối, lease một người gửi.
- Báo
last_seentheo lô và heartbeat collector.
1.4 Ngoài phạm vi
| Nội dung | Thuộc về |
|---|---|
group_down, chống dao động (flapping), silence, bảo trì, cài đặt theo công ty | COL-9 và sau, chưa làm (chỉ thiết kế) |
Chống nhiễu: silence, bảo trì, flapping, group_down (COL-9) | Gói internal/suppress (silence, Suppressor), internal/rules/flap.go, internal/alerting/group*.go, xem docs/05-alerting.md mục 3 và 4 |
| Luật ngưỡng: kéo luật, biên dịch, đánh giá, trạng thái, snapshot | Gói internal/rules (COL-8, xong), xem docs/05-alerting.md mục 2.1. Phát sự kiện qua alerting.Emitter.Emit vào Outbox của tài liệu này |
| Tạo cảnh báo, thông báo, tắt cảnh báo | Access Hub |
Bản ghi presence (Touch, Ensure) | L3 Registry, Enroll, Presence |
| Hợp đồng HTTP sự kiện phía Access Hub | HUB-* (chưa xây; dev dùng cmd/mockhub) |
2. Requirements
2.1 Functional Requirements
| # | Trách nhiệm | Giải thích | Hiện thực ở |
|---|---|---|---|
| FR-ALR-01 | Ngưỡng down | max(3 x agent_interval, down_min) | TestThresholdAndGrace |
| FR-ALR-02 | Ân hạn khởi động | Không khai báo down trong 2 x agent_interval đầu | TestNothingIsDeclaredDownDuringStartupGrace |
| FR-ALR-03 | Down đúng một lần | Agent quá ngưỡng chỉ phát một sự kiện down | TestAgentDeclaredDownOnceAfterThreshold |
| FR-ALR-04 | Agent chưa từng báo cáo | Vẫn bị khai báo down sau ngưỡng | TestAgentThatNeverReportedIsDeclaredDown |
| FR-ALR-05 | Up sau phục hồi | Có dữ liệu hợp lệ thì phát agent.up | TestAgentUpEventAfterRecovery |
| FR-ALR-06 | Sự kiện tới Access Hub đúng một lần | Đầu cuối Detector đến Sender đến hub | TestEventsReachTheHubExactlyOnce |
| FR-ALR-07 | Thử lại khi Outbox lỗi | Sự kiện down lỗi ghi được giữ và ghi lại | TestDownEventsAreRetriedWhenTheOutboxFails |
| FR-ALR-08 | Báo last_seen | Chỉ lần thấy thật, mỗi lần một lượt | TestReportSeenSendsOnlyRealContactsOnce |
| FR-ALR-09 | Heartbeat | Mang số đếm và tốc độ | TestHeartbeatCarriesCountsAndRate |
| FR-ALR-10 | Kiểm tra tham số | Tùy chọn không hợp lệ bị từ chối | TestNewValidatesOptions |
| FR-EVT-01 | UUIDv7 | Định dạng đúng, dấu thời gian đúng | TestUUIDv7FormatAndTimestamp |
| FR-EVT-02 | ID tăng chặt và duy nhất | Kể cả khi đồng hồ lùi | TestUUIDv7IsStrictlyIncreasingAndUnique, TestUUIDv7SurvivesClockGoingBackwards |
| FR-EVT-03 | seq tăng chặt theo (máy chủ, luật) | max(now_ms, last+1) | TestSeqIsStrictlyIncreasingPerServerAndRule |
| FR-EVT-04 | Quên seq một máy chủ | Không ảnh hưởng máy chủ khác | TestSeqForgetOnlyDropsThatServer |
| FR-EVT-05 | Nội dung sự kiện | Loại agent.down và agent.up, cùng luật agent_down | TestAgentDownAndUpEvents |
| FR-OUT-01 | Hợp đồng Outbox chung | Mọi backend cùng hành vi | TestOutboxContract, TestRedisOutboxContract |
| FR-OUT-02 | Bỏ ID trùng | Append lặp không nhân đôi | TestFileOutboxIgnoresDuplicateIDs, TestRedisOutboxIgnoresDuplicateIDs |
| FR-OUT-03 | Bền qua khởi động lại (tệp) | Mở lại đọc được hàng đợi | TestFileOutboxSurvivesReopen |
| FR-OUT-04 | Bỏ qua dòng hỏng (tệp) | Không chặn cả hàng đợi | TestFileOutboxSkipsCorruptLines |
| FR-OUT-05 | Nén tệp | Thu gọn nhật ký | TestFileOutboxCompacts |
| FR-OUT-06 | Dùng chung giữa hai tiến trình (Redis) | Cùng một hàng đợi | TestRedisOutboxSharedByTwoProcesses |
| FR-OUT-07 | Lỗi Redis trả lỗi | Không nuốt lỗi | TestRedisOutboxErrorsWhenRedisIsDown |
| FR-OUT-08 | ID không có thân | Bị loại khỏi hàng đợi | TestRedisOutboxDropsQueuedIDWithoutBody |
| FR-SND-01 | Gửi và xác nhận | Gửi lô, Ack phần được nhận | TestSenderDeliversAndAcks |
| FR-SND-02 | Giữ khi hub lỗi | Gửi lại đúng một lần khi hub hồi phục | TestSenderKeepsEventsWhileHubIsDownAndRedeliversOnce |
| FR-SND-03 | Dead-letter sự kiện bị từ chối | Hub trả rejected | TestSenderDeadLettersRejectedEvents |
| FR-SND-04 | Tách lô bị từ chối vĩnh viễn | Gửi từng sự kiện một | TestSenderSplitsPermanentlyRefusedBatch |
| FR-SND-05 | Hết hạn lưu giữ | Sự kiện quá retention vào dead-letter | TestSenderDeadLettersExpiredEntries |
| FR-SND-06 | Trần lô 200 | TestSenderBatchesAtMost200 | |
| FR-SND-07 | Hồi phục sau sự cố | Vòng Run tự khôi phục | TestSenderRunRecoversAfterOutage |
| FR-SND-08 | Một người gửi tại một thời điểm | Bỏ qua khi lease ở nơi khác | TestSenderSkipsWhenLeaseIsHeldElsewhere, TestLeaseHasOneHolderAndFailsOver |
| FR-SND-09 | Kiểm tra tham số | TestNewSenderValidatesOptions |
2.2 Non-Functional Requirements
Allocated:
| NFR L2 | Target | Cách đáp ứng ở L3 |
|---|---|---|
| NFR-09 (không mất sự kiện khi hub lỗi) | Hàng đợi bền | Outbox Redis hoặc tệp, giữ tối đa retention (24 giờ) |
| NFR-10 (không lặp gây hại) | Khử trùng | event_id UUIDv7, hub khử trùng |
| NFR-13 (chịu lỗi node) | Mất node không mất trạng thái | Presence và Outbox Redis, lease người gửi |
Inherited: client Redis internal/redisx; hub client internal/hubclient.
Owned:
| ID | Target | Parent L2-NFR | Satisfied-by |
|---|---|---|---|
| NFR-ALR-01 | Phát hiện down không sớm hơn ngưỡng | NFR-09 | Ngưỡng max(3 x interval, down_min) |
| NFR-ALR-02 | Quét mỗi 10 giây | NFR-09 | alerting.scan_interval |
| NFR-OUT-01 | Sự kiện chưa gửi lưu tối đa 24 giờ rồi dead-letter | NFR-09 | outbox.retention |
| NFR-OUT-02 | Lô gửi tối đa 200 | NFR-09 | Giới hạn hub và outbox.batch_max (1 đến 200) |
| NFR-OUT-03 | Lùi 1 giây đến 5 phút | NFR-09 | outbox.backoff_min, backoff_max |
2.3 Acceptance Criteria
| AC | Given / When / Then | Test ID |
|---|---|---|
| AC-ALR-01 | Given agent ngừng báo, When quá ngưỡng, Then đúng một agent.down | TestAgentDeclaredDownOnceAfterThreshold |
| AC-ALR-02 | Given collector vừa khởi động, When trong ân hạn, Then không có down | TestNothingIsDeclaredDownDuringStartupGrace |
| AC-ALR-03 | Given agent down rồi báo lại, When có lô hợp lệ, Then agent.up | TestAgentUpEventAfterRecovery |
| AC-ALR-04 | Given hub lỗi, When sự kiện phát sinh, Then vào hàng đợi và giao đúng một lần khi hub hồi phục | TestSenderKeepsEventsWhileHubIsDownAndRedeliversOnce |
| AC-ALR-05 | Given hai collector, When cùng gửi, Then một bên giữ lease | TestSenderSkipsWhenLeaseIsHeldElsewhere |
| AC-ALR-06 | Given agent im lặng thật, When chạy đầu cuối với Access Hub giả, Then hub nhận down rồi up | TestSilentAgentGoesDownThenUpAtAccessHub |
| AC-ALR-07 | Given Outbox lỗi lúc ghi down, When thử lại, Then vẫn gửi được | TestDownEventsAreRetriedWhenTheOutboxFails |
2.4 Quality Attribute Scenarios
| Nguồn | Kích thích | Môi trường | Phản hồi | Thước đo |
|---|---|---|---|---|
| Agent | Mất điện | Bình thường | Collector khai báo down sau ngưỡng (90 giây với mặc định) | Sự kiện tới hub trong chu kỳ quét kế tiếp cộng thời gian gửi |
| Access Hub | Ngừng hoạt động nhiều giờ | Bình thường | Sự kiện xếp hàng | Không mất sớm hơn 24 giờ |
| Vận hành | Khởi động lại collector | Nhiều agent | Ân hạn tránh báo down hàng loạt | Không có down giả trong ân hạn |
| Vận hành | Chết tiến trình ngay sau khi khai báo down | Bình thường | Có thể mất sự kiện (AR-001) | Chưa có cơ chế bù, đề xuất dựng lại từ presence |
3. Application Architecture
3.1 Kiến trúc thời chạy (C&C)
flowchart LR
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff;
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff;
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff;
SC["Scan loop"]:::owned
RT["Retry list (bộ nhớ)"]:::owned
EB["Events builder"]:::owned
RP["Reporter"]:::owned
OB[("Outbox")]:::datastore
SND["Sender loop"]:::owned
LS["Lease"]:::owned
SC --> EB
SC --> OB
SC --> RT
RP --> SC
SND --> OB
SND --> LS| Thành phần | Trách nhiệm | Vòng đời |
|---|---|---|
| Scan loop | Mỗi scan_interval hỏi Presence agent quá hạn, tạo agent.down, ghi Outbox | Theo tiến trình |
| Retry list | Danh sách bộ nhớ các sự kiện down ghi lỗi, thử lại ở vòng sau (mất khi chết tiến trình) | Theo tiến trình |
| Events builder | IDGen (UUIDv7) và SeqAllocator | Theo tiến trình |
| Reporter | DrainSeen theo lô 500 mỗi seen_interval, Heartbeat mỗi heartbeat_interval | Theo tiến trình |
| Sender loop | Peek, gửi, Ack, lùi mũ, hết hạn | Theo tiến trình |
| Lease | redisx lease <prefix>outbox:sender, TTL 15 giây (chỉ với Outbox Redis) | Theo tiến trình |
| Kết nối | Kiểu | Chi tiết |
|---|---|---|
| Detector đến Presence | Đồng bộ | Timeout Redis 500 ms |
| Detector đến Outbox | Đồng bộ | Append chỉ trả về khi bền |
| Sender đến Access Hub | Đồng bộ | Lô 1 đến 200, lùi mũ khi lỗi |
| Sender đến Lease | Đồng bộ | Gia hạn mỗi vòng; chỉ gửi khi giữ lease |
3.2 Module view
flowchart TB
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff;
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff;
APP["internal/app"]:::bc
ALP["internal/alerting"]:::owned
EVP["internal/events"]:::owned
OBP["internal/outbox"]:::owned
PRE["internal/presence"]:::bc
HCP["internal/hubclient"]:::bc
RX["internal/redisx"]:::bc
APP --> ALP
APP --> OBP
ALP --> EVP
ALP --> OBP
ALP --> PRE
ALP --> HCP
EVP --> HCP
OBP --> HCP
OBP --> RXTệp: internal/alerting/alerting.go, internal/events/events.go, internal/outbox/outbox.go (interface, bộ nhớ), file.go, redis.go, sender.go.
4. Domain Model
classDiagram
class Event {
<<hubclient.Event>>
event_id
type
seq
occurred_at
company_id
server_id
agent_id
rule_id
severity
series_key
suppressed
labels
}
class Outbox {
<<interface>>
Append
Peek
Ack
Dead
Expire
Stats
}
class Entry {
event
enqueued_at
}
class DeadEntry {
reason
dead_at
}
class Sender {
<<service>>
Run
FlushOnce
}
Outbox "1" --> "*" Entry
Outbox "1" --> "*" DeadEntry
Entry --> Event
Sender --> OutboxBất biến:
event_idlà UUIDv7 duy nhất, tăng chặt kể cả khi đồng hồ lùi.seqtăng chặt theo (máy chủ, luật):max(now_ms, last+1).- Luật
agent_downphátagent.down,agent.up: mứccriticalcho down,infocho up,series_keylàagent. - Luật ngưỡng (COL-8) phát
alert.opened,alert.resolved,alert.reminderquaEmitter.Emit, mangrule_id,rule_version,series_key,value,threshold,labels.seqtăng chặt theo (máy chủ, luật,series_key).alert.resolvedcóduration_seconds,peak_value,reason. - Sự kiện luật chưa vào được Outbox (đầy hoặc lỗi) được giữ lại và thử lại ở lần quét kế tiếp (
ahc_rules_events_retry). suppressedluôn false ở Collector hiện tại (silence, bảo trì, flapping thuộc COL-9, chưa làm).- Outbox chỉ xóa sự kiện khi Access Hub đã nhận (accepted) hoặc đã dead-letter.
5. API Contract
Không có API HTTP mở ra ngoài. Hợp đồng gồm lời gọi ra Access Hub và interface Go nội bộ.
5.1 Operations
| Interface | Phương thức | Hành vi |
|---|---|---|
Outbox | Append(events, now) | Chỉ trả về sau khi bền; ID trùng bị bỏ |
Outbox | Peek(max) | Trả các mục cũ nhất, không xóa |
Outbox | Ack(ids) | Xóa sự kiện đã giao |
Outbox | Dead(ids, reason, now) | Chuyển vào dead-letter |
Outbox | Expire(cutoff, now) | Dead-letter mọi mục xếp hàng trước cutoff |
Outbox | Stats(now) | Độ sâu, tuổi mục cũ nhất, số dead-letter |
Locker | Acquire(ctx) | True khi tiến trình giữ lease |
Hub API (gọi ra, xem L3 Registry mục 5.1): POST /events, POST /agents/seen, POST /heartbeat.
5.2 Request và Response schema
POST /events
events![] (1..200): Event
Event:
event_id!: UUIDv7
type!: "agent.down" | "agent.up"
seq!: int64
occurred_at!: RFC3339
company_id!, server_id!: string
agent_id?, rule_id?, rule_version?, series_key?, severity?: string
value?, threshold?: number
suppressed!: bool
suppress_reason?, group_key?: string
labels?: map
agent.down: last_seen_at
agent.up: down_since, down_duration_s
Phản hồi: { accepted![]: event_id, rejected![]: { event_id, reason } }POST /agents/seen và /heartbeat: nội dung chi tiết xem internal/hubclient/hubclient.go (heartbeat mang số đếm agent và tốc độ).
5.3 Error codes
| Phản hồi của hub | Hành động của Sender | Metric |
|---|---|---|
200 với accepted | Ack | ahc_outbox_events_sent_total |
200 với rejected | Dead("rejected: <lý do>") | ahc_outbox_dead_letter_total{reason="rejected"} |
| Lỗi mạng | Giữ, lùi mũ | ahc_outbox_send_failures_total{kind="network"} |
| 429, 5xx, 401, 403, 408 | Giữ, lùi mũ | kind="temporary" |
| 4xx vĩnh viễn (lô nhiều sự kiện) | Gửi từng sự kiện; bị từ chối thì Dead("refused: ...") | kind="permanent", dead-letter reason="refused" |
Sự kiện quá retention | Expire rồi dead-letter | reason="expired" |
5.4 Versioning
Hợp đồng /api/v1/monitoring/collector do Access Hub định nghĩa (HUB-*, chưa xây). Event có các trường cho luật ngưỡng (rule_id, rule_version, value, threshold, duration_seconds, peak_value, reason) do COL-8 điền. group_key vẫn chưa điền (COL-9).
5.5 Authz
Sender dùng Bearer hub.token. Không có phân quyền người dùng. Sự kiện luôn mang company_id từ bản ghi registry của agent, không từ dữ liệu agent gửi.
6. Physical Data Schema
6.1 Mapping
Redis Outbox (tiền tố ah:):
| Khóa | Kiểu | Nội dung |
|---|---|---|
ob:q | zset | event_id đang chờ gửi, điểm là thời điểm xếp hàng (ms) |
ob:e | hash | event_id đến Entry JSON (thân sự kiện và thời điểm xếp hàng) |
ob:dead | hash | event_id đến DeadEntry JSON (lý do và dead_at) |
outbox:sender | khóa lease | Chủ người gửi, TTL 15 giây, không có fencing token. Chỉ tạo khi Outbox là Redis (wiring.go) |
Độ bền bằng độ bền của Redis: cần AOF (ít nhất appendfsync everysec) và replica (chú thích trong internal/outbox/redis.go).
Tệp Outbox (outbox.dir):
| Tệp | Nội dung |
|---|---|
outbox.log | Nhật ký JSONL (thêm, ack, dead), nén định kỳ |
deadletter.jsonl | Sự kiện dead-letter |
Bộ nhớ: hàng đợi trong RAM, mất khi khởi động lại (chỉ dev và kiểm thử). Discard: dùng khi không có hub, bỏ mọi sự kiện.
Chọn backend auto: Redis nếu có redis.addr; nếu không thì tệp nếu có outbox.dir; nếu không thì bộ nhớ.
flowchart LR
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff;
Q[("ob:q")]:::datastore
E[("ob:e")]:::datastore
D[("ob:dead")]:::datastore
Q -->|"id tra thân"| E
E -.->|"Dead"| D6.2 Phân loại và lưu giữ
| Dữ liệu | Phân loại | Lưu giữ |
|---|---|---|
| Sự kiện xếp hàng | Trung bình | Đến khi hub nhận, tối đa retention (24 giờ) |
| Dead-letter | Trung bình | Không có TTL trong mã; chưa có quy trình xem hoặc phát lại (đề xuất, OQ-ALR-3) |
| Presence | Xem L3 Registry |
7. Algorithms
7.1 Sequences (đường thành công)
sequenceDiagram
participant DT as Detector
participant PS as Presence
participant EB as Events builder
participant OB as Outbox
participant SD as Sender
participant HB as Access Hub (ext)
DT->>PS: Expired(ngưỡng, lô 500)
PS-->>DT: agent quá hạn (SADD pres:down thắng)
DT->>EB: AgentDown(agent, last_seen)
EB-->>DT: event (UUIDv7, seq)
DT->>OB: Append (bền)
SD->>OB: Peek(tối đa 200)
OB-->>SD: sự kiện
SD->>HB: POST /events
HB-->>SD: accepted
SD->>OB: Ack7.2 Sequences (đường lỗi)
sequenceDiagram
participant DT as Detector
participant OB as Outbox
participant SD as Sender
participant HB as Access Hub (ext)
DT->>OB: Append
alt Append lỗi
OB--)DT: lỗi
DT->>DT: đưa vào retry list (bộ nhớ)
DT->>OB: Append lại ở vòng quét sau
end
SD->>HB: POST /events
alt lỗi tạm thời
HB--)SD: lỗi
SD->>SD: lùi 1 s nhân đôi đến 5 phút
else 4xx vĩnh viễn
HB-->>SD: 4xx
SD->>HB: gửi lại từng sự kiện một
SD->>OB: Dead cho sự kiện bị từ chối
end7.3 State machines
stateDiagram-v2
direction LR
state "Hoạt động (up)" as UP
state "Mất tín hiệu (down)" as DOWN
[*] --> UP: Ensure (ân hạn)
UP --> DOWN: quá ngưỡng, phát agent.down
DOWN --> UP: có dữ liệu hợp lệ, phát agent.upstateDiagram-v2
direction LR
state "Xếp hàng (queued)" as Q
state "Đã nhận (acked)" as A
state "Dead-letter" as D
[*] --> Q: Append
Q --> A: hub accepted
Q --> D: hub rejected hoặc refused
Q --> D: quá retention
A --> [*]| Chuyển trạng thái | Sequence |
|---|---|
| Up đến Down | 7.1 |
| Down đến Up | Touch ở Ingest gọi OnAgentUp, Detector phát agent.up |
| Queued đến Acked | 7.1 |
| Queued đến Dead | 7.2 |
7.4 Core algorithms
| Vấn đề | Giải pháp | Trade-off |
|---|---|---|
| Ngưỡng down | max(3 x agent_interval, down_min) (90 giây với mặc định) | Phát hiện chậm hơn vài chục giây để chống báo nhầm |
| Báo nhầm lúc khởi động | Ân hạn 2 x agent_interval | Sự cố thật trong ân hạn bị báo muộn |
| Hai tiến trình cùng thấy quá hạn | SADD pres:down thắng thì sở hữu chuyển trạng thái | Nếu chết sau SADD, sự kiện mất (AR-001) |
| Append lỗi | Retry list bộ nhớ | Mất khi chết tiến trình; đề xuất dựng lại từ trạng thái pres:down chưa có sự kiện |
| Sự kiện trùng | event_id UUIDv7 | Hub cần khử trùng theo event_id |
| Thứ tự sự kiện | seq = max(now_ms, last+1) theo (máy chủ, luật) | Trạng thái seq nằm trong bộ nhớ: sau khởi động lại vẫn bắt đầu từ now_ms nên đơn điệu nhờ đồng hồ (giả định đồng hồ không lùi quá xa) |
| Một người gửi | Lease outbox:sender TTL 15 giây, không fencing | Hai bên có thể cùng gửi trong khoảng chuyển lease; hub khử trùng nên không hại |
| Lô bị từ chối | Tách từng sự kiện | Nhiều request hơn khi có sự kiện xấu |
| Hết hạn | Expire mỗi vòng gửi | Mất sự kiện cũ hơn 24 giờ (vào dead-letter) |
Lô last_seen | DrainSeen ZPOPMIN theo chunk 500 | Mất mục nếu gửi hub lỗi sau khi rút |
8. Error Handling
8.1 Bảng lỗi theo bước
| Bước lỗi | Nguyên nhân | Cơ chế xử lý | Trạng thái cuối |
|---|---|---|---|
| Quét Presence | Redis lỗi | Ghi log, thử lại vòng sau | Down chậm |
Append sự kiện down | Redis hoặc đĩa lỗi | Retry list bộ nhớ, thử lại vòng quét sau | Có thể mất nếu chết tiến trình |
Append sự kiện up | Redis hoặc đĩa lỗi | Không thử lại: tăng ahc_events_lost_total{type="agent.up"} và ghi log mức error | Mất sự kiện up |
| Gửi | Mạng, 5xx, 429, 401, 403, 408 | Giữ, lùi mũ | Sự kiện còn trong Outbox |
| Gửi | 4xx vĩnh viễn | Tách từng sự kiện, dead-letter | reason="refused" |
| Gửi | rejected trong phản hồi | Dead-letter | reason="rejected" |
| Lease | Redis lỗi | Không giữ lease, bỏ qua vòng | Không gửi |
Báo last_seen | Hub lỗi | Ghi log | Mục đã rút có thể mất |
| Tắt | Còn sự kiện | Gửi lần cuối tối đa 3 giây | Phần còn lại ở Outbox bền |
ahc_events_lost_total chỉ tăng ở đường Emitter.AgentUp (sự kiện up không có retry list). Nhánh down dùng retry list nên chưa tính là mất cho đến khi tiến trình chết.
8.2 Fail-fast
outbox.batch_maxngoài 1 đến 200 bị từ chối (NewSender).- Tùy chọn Detector không hợp lệ bị từ chối (
NewDetector,TestNewValidatesOptions). - Khi không có hub: Outbox
Discard(sự kiện bị bỏ, vẫn chạy được ở dev).
8.3 Race conditions
| Tình huống | Xử lý |
|---|---|
| Hai collector quét cùng lúc | SADD pres:down chọn một |
| Hai collector cùng gửi | Lease; hub khử trùng theo event_id |
| Chuyển lease | Không fencing, có thể gửi lặp, an toàn nhờ khử trùng |
| Gửi lặp sau mất câu trả lời | Hub khử trùng |
9. Degradation
Nguyên tắc: sự kiện quan trọng hơn tốc độ. Giữ trong Outbox bền và thử lại, chấp nhận trễ.
9.1 Dependency matrix
| Dependency lỗi | Hành vi | Kiểu suy thoái | Hệ quả |
|---|---|---|---|
| Access Hub | Sự kiện xếp hàng đến 24 giờ | Trễ | Cảnh báo trễ |
| Redis | Không quét, không gửi (lease), Outbox Redis không dùng được | Mù | Không phát hiện down (AR-005) |
| Đĩa (Outbox tệp) | Append lỗi | Mất | Retry list bộ nhớ |
| Cả Access Hub và Redis | Không có sự kiện | Mù |
9.2 Backup và recovery
Outbox Redis: AOF everysec ở dev (mất tối đa khoảng 1 giây). Outbox tệp bền qua khởi động lại, không chia sẻ giữa node. Sự kiện dead-letter giữ để điều tra, chưa có công cụ phát lại. RTO và RPO chính thức: chưa định nghĩa (đề xuất, OQ-ALR-1).
10. Concurrency
10.1 Ranh giới giao dịch
Mỗi Append là bền độc lập. Quyết định down (SADD) và ghi sự kiện (Append) không cùng giao dịch: khoảng hở này là nguồn AR-001. Không có outbox giao dịch theo nghĩa kép với trạng thái presence.
10.2 Cơ chế
| Cơ chế | Mô tả | Mối nguy |
|---|---|---|
SADD pres:down | Một chủ chuyển trạng thái | Mất sự kiện nếu chết ngay sau |
Lease outbox:sender | Một người gửi, TTL 15 giây | Không fencing, có thể lặp ngắn hạn |
event_id UUIDv7 | Khử trùng | Phụ thuộc hub |
Khóa trong tiến trình (SeqAllocator, IDGen) | An toàn luồng | Trạng thái mất khi khởi động lại |
11. Security
11.1 Ba lớp
| Lớp | Biện pháp |
|---|---|
| Truyền thông | Bearer hub.token tới Access Hub, TLS ở triển khai |
| Mã | company_id lấy từ registry; sự kiện không chứa dữ liệu nhạy cảm |
| Dữ liệu | Outbox không chứa bí mật; tệp Outbox nên có quyền 0600 (đề xuất rà soát, OQ-ALR-4) |
11.2 Pipeline phân quyền năm bước
Thành phần nội bộ, không phân quyền người dùng. Điều kiện tiên quyết: company_id và server_id của sự kiện đến từ bản ghi registry đã xác thực.
11.3 Chỉ mục neo
| Nguyên tắc L2 | Biện pháp nội bộ |
|---|---|
| NFR-12 cô lập tenant | Sự kiện mang company_id từ registry; hub kiểm lại |
| 9.3 secrets | hub.token từ tệp hoặc biến môi trường, không vào log |
12. Configuration
12.1 Tunables
Nguồn mặc định: Default() trong internal/config/config.go. Các khóa này chỉ YAML (trừ AHC_HUB_*, AHC_REDIS_*).
| Tham số | Default | Ý nghĩa và tác động | Mục liên quan |
|---|---|---|---|
alerting.agent_interval | 30 giây | Chu kỳ đẩy kỳ vọng của agent | 7.4 |
alerting.down_min | 90 giây | Sàn ngưỡng down | 7.4 |
alerting.scan_interval | 10 giây | Chu kỳ quét | 7.1 |
alerting.seen_interval | 1 phút | Chu kỳ báo last_seen. Phải nhỏ hơn một phần ba online_threshold_seconds của Access Hub (mặc định 180 giây), nếu không agent sẽ chập chờn online và offline | 7.4 |
alerting.heartbeat_interval | 1 phút | Chu kỳ heartbeat | 3.1 |
alerting.rules_sync_interval | 1 phút | Chu kỳ kéo luật từ Access Hub (COL-8). Ngoài ra kéo ngay khi POST /reload/rules | docs/05 2.1 |
alerting.rules_state_interval | 30 giây | Chu kỳ snapshot trạng thái đánh giá. Cũng ghi khi tắt êm | docs/05 2.1 |
alerting.no_data_after | 5 phút | Series im lặng quá mức này (máy vẫn gửi lô khác) thì áp no_data. Cũng là khoảng trống tối đa PENDING chịu được | docs/05 2.1 |
alerting.silences_sync_interval | 1 phút | Chu kỳ kéo GET /silences (COL-9). Ngoài ra kéo ngay khi POST /reload/silences | docs/05 4.1 |
alerting.flap_transitions | 4 | Số lần chuyển FIRING <-> OK trong flap_window để vào flapping, 0 tắt (COL-9) | docs/05 3.1 |
alerting.flap_window | 30 phút | Cửa sổ đếm chuyển trạng thái (COL-9) | docs/05 3.1 |
alerting.flap_stable | 30 phút | Yên lặng bao lâu thì ra flapping (COL-9) | docs/05 3.1 |
alerting.group_down.enabled | true | Bật gom sự cố diện rộng. Nhiều worker thì chỉ một worker bật (COL-9) | docs/05 3.2 |
alerting.group_down.min, .ratio, .window | 5, 0.30, 60 giây | Mặc định của group_down. Access Hub GET /settings/{company_id} ghi đè theo công ty (COL-9) | docs/05 3.2 |
alerting.group_down.companies.<company_id> | rỗng | Ghi đè min, ratio, window theo công ty, thấp hơn cài đặt của Access Hub, cao hơn mặc định (COL-9) | docs/05 3.2 |
alerting.rules_dir | rỗng | Thư mục snapshot tệp khi không có Redis. Rỗng và không Redis thì snapshot chỉ ở bộ nhớ (có log cảnh báo, mất khi khởi động lại) | docs/05 2.1 |
outbox.backend | auto | auto, memory, file, redis | 6.1 |
outbox.dir | rỗng | Thư mục cho backend tệp | 6.1 |
outbox.batch_max | 200 | Sự kiện mỗi request (1 đến 200) | 7.4 |
outbox.flush_interval | 1 giây | Chu kỳ thăm khi rảnh | 7.1 |
outbox.backoff_min | 1 giây | Lùi tối thiểu | 7.2 |
outbox.backoff_max | 5 phút | Lùi tối đa | 7.2 |
outbox.retention | 24 giờ | Quá hạn thì dead-letter | 7.4 |
Hằng số cố định: TTL lease 15 giây, lô quét presence và last_seen 500, gửi lần cuối khi tắt 3 giây.
12.2 Feature flags
Không có công tắc cho luật: bộ luật rỗng thì không có gì để đánh giá. Khi không có hub.base_url, Outbox Discard.
13. Telemetry
13.1 Metrics
| Tên | Kiểu | Nhãn | Ngữ nghĩa |
|---|---|---|---|
ahc_events_emitted_total | counter | type | Sự kiện phát ra |
ahc_events_lost_total | counter | type | Sự kiện không ghi được vào Outbox (hiện chỉ đường agent.up) |
ahc_agents_down | gauge | Agent đang down | |
ahc_outbox_events_sent_total | counter | Sự kiện giao thành công | |
ahc_outbox_send_failures_total | counter | kind | Lỗi gửi (network, temporary, permanent) |
ahc_outbox_dead_letter_total | counter | reason | Dead-letter (rejected, refused, expired) |
ahc_outbox_depth | gauge | Độ sâu hàng đợi | |
ahc_outbox_oldest_age_seconds | gauge | Tuổi mục cũ nhất |
Lệch tài liệu: docs/10 dùng tên khác (xem L2 TD-004). Mã thắng.
13.2 Log schema
JSON một dòng: time, level, msg, collector_id. Sự kiện đáng ghi: events dead-lettered after retention (mức error, kèm count), hub rejected event (kèm event_id, reason), khai báo down và up, lỗi gửi.
13.3 Cảnh báo đến runbook
Chưa có luật cảnh báo trong mã. Đề xuất: ahc_outbox_oldest_age_seconds cao (hub kẹt), ahc_outbox_dead_letter_total tăng bất kỳ, ahc_events_lost_total tăng bất kỳ, ahc_outbox_depth tăng liên tục. Runbook: docs/09 mục 6.
13.4 Probes
Readiness /readyz ("redis", "tsdb"); chưa có probe riêng cho Sender hoặc Detector (đề xuất, ví dụ thời điểm quét thành công cuối).
13.5 Trace propagation
event_id là khóa tương quan giữa log Collector và hub. Chưa có trace phân tán.
14. Test Plan
| Loại | Kiểm thử | Vị trí |
|---|---|---|
| Detector | TestThresholdAndGrace, TestAgentDeclaredDownOnceAfterThreshold, TestNothingIsDeclaredDownDuringStartupGrace, TestAgentThatNeverReportedIsDeclaredDown, TestAgentUpEventAfterRecovery, TestEventsReachTheHubExactlyOnce, TestDownEventsAreRetriedWhenTheOutboxFails, TestReportSeenSendsOnlyRealContactsOnce, TestHeartbeatCarriesCountsAndRate, TestNewValidatesOptions | internal/alerting/alerting_test.go |
| Events | TestUUIDv7FormatAndTimestamp, TestUUIDv7IsStrictlyIncreasingAndUnique, TestUUIDv7SurvivesClockGoingBackwards, TestSeqIsStrictlyIncreasingPerServerAndRule, TestSeqForgetOnlyDropsThatServer, TestAgentDownAndUpEvents | internal/events/events_test.go |
| Outbox | TestOutboxContract, TestFileOutboxIgnoresDuplicateIDs, TestFileOutboxSurvivesReopen, TestFileOutboxSkipsCorruptLines, TestFileOutboxCompacts, TestRedisOutboxContract, TestRedisOutboxIgnoresDuplicateIDs, TestRedisOutboxSharedByTwoProcesses, TestRedisOutboxDropsQueuedIDWithoutBody, TestRedisOutboxErrorsWhenRedisIsDown | internal/outbox |
| Sender | TestSenderDeliversAndAcks, TestSenderKeepsEventsWhileHubIsDownAndRedeliversOnce, TestSenderDeadLettersRejectedEvents, TestSenderSplitsPermanentlyRefusedBatch, TestSenderDeadLettersExpiredEntries, TestSenderBatchesAtMost200, TestSenderRunRecoversAfterOutage, TestSenderSkipsWhenLeaseIsHeldElsewhere, TestNewSenderValidatesOptions | internal/outbox/sender_test.go |
| Lease | TestLeaseHasOneHolderAndFailsOver | internal/redisx |
| Tích hợp | TestSilentAgentGoesDownThenUpAtAccessHub, TestSplitProcessesShareStateThroughRedis | internal/app |
| Chưa có | Chết tiến trình giữa SADD và Append (AR-001), kiểm thử dead-letter phát lại, kiểm thử với Access Hub thật |
Bản nháp này không chạy kiểm thử (tránh Redis 6380 và dev stack).
15. Implementation Sequence
15.1 Milestone matrix
| Milestone | Nội dung | Phụ thuộc | Đóng góp nghiệm thu |
|---|---|---|---|
| COL-6 | Detector agent down, events, outbox, sender | COL-3 | AC-ALR-01..07 |
| COL-R | Outbox Redis, presence Redis, lease người gửi | COL-6 | AC-ALR-05 |
| COL-8 | Luật ngưỡng, trạng thái, snapshot, sự kiện alert.* | COL-6 | Kiểm thử gói internal/rules và internal/app |
| COL-9 | group_down, chống dao động, silence, bảo trì | COL-8 | Chưa có AC |
| COL-13 (chưa làm) | Lease theo shard (liên quan bus) | COL-R | |
| Fencing lease (đề xuất, chưa có milestone) | Fencing token cho người gửi | COL-R | Giảm gửi lặp |
Tên theo docs/11-roadmap.md; nếu khác, roadmap thắng.
15.2 Dependency flowchart
flowchart LR
C3["COL-3 registry"] --> C6["COL-6 detector, outbox"]
C6 --> CR["COL-R Redis dùng chung"]
C6 --> C8["COL-8 luật (xong)"]
CR --> FN["fencing lease (đề xuất)"]Appendix A. Open Questions
| # | Câu hỏi | Hành vi tạm thời | Owner | Mã theo dõi |
|---|---|---|---|---|
| 1 | RTO và RPO cho đường sự kiện | Chưa định nghĩa | Chưa chỉ định | OQ-ALR-1 |
| 2 | Bù sự kiện down bị mất khi chết tiến trình (AR-001): dựng lại từ pres:down | Không có | Chưa chỉ định | OQ-ALR-2 |
| 3 | Quy trình xem, phát lại và dọn dead-letter | Chưa có | Chưa chỉ định | OQ-ALR-3 |
| 4 | Quyền tệp Outbox và nơi đặt outbox.dir ở sản xuất | Chưa xác minh | Chưa chỉ định | OQ-ALR-4 |
| 5 | Sự kiện agent.up ghi Outbox lỗi bị mất, không có retry như agent.down: có cần đối xứng | Chỉ log và đếm | Chưa chỉ định | OQ-ALR-5 |
| 6 | Có cần metric đếm mục trong retry list của Detector | Không có | Chưa chỉ định | OQ-ALR-6 |
| 7 | Nguồn đồng bộ ConfigFor và cài đặt theo công ty | Luật đã xong (COL-8), phần này chưa làm | Chưa chỉ định | OQ-ALR-7 |
| 8 | Có cần fencing cho lease người gửi | Dựa vào khử trùng của hub | Chưa chỉ định | OQ-ALR-8 |
| 9 | Hub thật có khử trùng theo event_id và theo seq không (HUB-*) | Giả định có | Chưa chỉ định | OQ-ALR-9 |
Appendix B. ADR nội bộ
| Mã ADR | Quyết định | Trạng thái | Driver |
|---|---|---|---|
| ADR 0008 | Sự kiện at-least-once với event_id | Chấp nhận | Hub khử trùng |
| ADR 0012 (đề xuất) | Redis dùng chung cho presence, outbox, lease | Đề xuất | COL-R |
| ADR nội bộ (đề xuất) | seq = max(now_ms, last+1) theo (máy chủ, luật) | Đề xuất, là hành vi mã | Đơn điệu không cần lưu |
| ADR nội bộ (đề xuất) | Retry list bộ nhớ cho down ghi lỗi | Đề xuất, là hành vi mã, có rủi ro AR-001 | Đơn giản |
| ADR nội bộ (đề xuất) | Lease người gửi không fencing | Đề xuất, là hành vi mã | Khử trùng ở hub đủ an toàn |
Appendix C. Section Profile
| Phân mục | Hồ sơ | Trạng thái điền | Giải trình |
|---|---|---|---|
| 0 đến 4 | Bắt buộc | Đã điền | |
| 5 API contract | Bắt buộc | Điền theo interface nội bộ và hub API | Hợp đồng hub là HUB-* chưa xây |
| 6 Physical data | Bắt buộc | Đã điền | Kiểu khóa lấy từ chú thích redis.go |
| 7.1 đến 7.4 | Bắt buộc | Đã điền | |
| 8 | Bắt buộc | Đã điền | |
| 9, 10, 11, 12, 13, 14, 15 | Bắt buộc | Đã điền | Kiểm thử chưa chạy trong bản nháp |