A gateway sends fleet events to Slack
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 50/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- backend
Hướng nghiên cứu
Start with src/alerts/alerts.test.ts and the createAlerts extension point from #719, then read src/alerts/AGENTS.md, src/alerts/, src/core/config.ts, and src/daemon/main.ts. Implement the Slack format and worker label lookup described in the issue, updating the protocol and docs as listed. Done means the specified message cases, escaping, retry behavior, config validation, and protocol tests pass.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Part of #707.
Scope
A destination can be format: "slack". The gateway posts one written message per event, naming the worker by its label, through the same queue, tries and limits as generic. After this PR, an operator gets readable messages in Slack. Other services that take Slack's incoming-webhook body may work; only Slack is promised.
In short
A slack destination gets {"text": "<message>"} for each event it lists. Each of the eleven events has one message, written below word for word. A slack destination takes no secret.
flowchart LR
E[Fact on the gateway's bus] --> Q[destination queue]
Q --> F{format}
F -->|generic| G[signed envelope]
F -->|"slack (new)"| S["{text: message}, worker named by label"]
Messages. <worker> is the worker's label from the gateway's view of it, or its id when it has no label or the gateway has no view of it. Variable parts are written as <name>.
| Event | Message |
|---|---|
worker.connected with version |
🟢 Worker <worker> connected (simlock <version>). |
worker.connected without version |
🟢 Worker <worker> connected. |
worker.disconnected, leaseCount 0 |
🔴 Worker <worker> disconnected. |
worker.disconnected, leaseCount 1 |
🔴 Worker <worker> disconnected while holding 1 lease. |
worker.disconnected, leaseCount n > 1 |
🔴 Worker <worker> disconnected while holding <n> leases. |
worker.rejected |
⛔ A worker was refused (<reason>): <claimed>. then, when count > 1, <count> more refusals like it came before this one. <claimed> is the payload's label, else its workerId, else an unknown worker; never the registry. |
worker.drain-started |
⏸ Worker <worker> is drained: it takes no new leases. |
worker.drain-ended |
▶️ Worker <worker> takes new leases again. |
device.quarantine-abandoned |
⚠️ Device <deviceId> on <worker> was retired after <attempts> failed clean-ups. |
device.quarantine-stranded |
🛑 Device <deviceId> on <worker> could not be cleaned up or deleted after <attempts> tries: <error>. It needs a person. |
device.recovery-failed |
⚠️ Device <deviceId> on <worker> crashed during lease <leaseId> and could not be brought back: <reason>. |
device.stalled-transition-detected |
⏳ Device <deviceId> on <worker> has been <state> for <age>. |
disk.pressure-detected |
💾 Worker <worker> is low on disk: <freeBytes> free (threshold <threshold>). |
component.install-failed |
❌ Installing <platform> <componentId> on <worker> failed: <error>. |
<age>fromageMs: under 60 000 ms → whole seconds,45 s; under 3 600 000 → whole minutes,14 min; else1 h 5 min(minutes left out when 0:2 h). Rounded down.<freeBytes>,<threshold>: GiB (1024³ bytes), asdiskPressure.freeBytesThresholdis documented, one decimal rounded half up,.0dropped: 8 804 682 956 →8.2 GiB, 10 737 418 240 (the default threshold) →10 GiB. Under 1 GiB (counted on the raw bytes) → MiB the same way: 536 870 912 →512 MiB.- Every variable part, the worker's name included, is first cut to 300 characters (counted as Unicode code points; a multi-code-point emoji such as
⚠️can still be cut in two, which only changes how that last character looks) with…added when longer, then has&,<,>written as&,<,>(Slack's escaping). A label like<!channel>therefore never pings a channel. - The message is built when it leaves the queue for its first try (#719's rule, with its test), naming the worker as the gateway knows it then. Later tries resend the same body.
- Known gap (decided in the spec session): a worker's relayed payload is not checked. The gateway replaces one that is not an object with
{workerId}(F11), so a message is always built. A missing field prints asundefined;<age>,<freeBytes>and<threshold>print?when not a finite number. The module manual lists this under Known gaps.
Inputs:
| Destination | Today (after #719) | After |
|---|---|---|
format: "slack", url, events |
expected one of "generic" |
accepted |
format: "slack" with any secret key ("s", "", null) |
expected one of "generic" |
expected no secret: a slack destination is not signed (path …secret) |
format: "generic" without secret |
expected a non-empty secret for a generic destination |
the same |
format: "teams" |
expected one of "generic" |
expected one of "generic", "slack" |
format: "slack", url with a user name |
URL error | the same URL error (#719) |
If this goes wrong, a message names the wrong worker, misses an event, pings a whole channel, or breaks Slack's formatting.
Technical spec
Facts
Commit ccb50353.
| # | Fact | Source |
|---|---|---|
| F1 | Payloads: worker.connected {workerId, label?, version?}, worker.disconnected {workerId, label?, leaseCount}, worker.rejected {reason: "forbidden" | "unauthenticated", workerId?, label?, count?}; workerId and label on worker.rejected are what the connection claimed. |
src/bus/index.ts:296-338 |
| F2 | Payloads: device.recovery-failed {deviceId, leaseId, attempts, reason, error}, device.quarantine-abandoned {deviceId, attempts}, device.quarantine-stranded {deviceId, attempts, error}. |
src/bus/index.ts:147-176 |
| F3 | Payloads: component.install-failed {platform, componentId, durationMs, error, requesterId?}, disk.pressure-detected {freeBytes, threshold}, device.stalled-transition-detected {deviceId, platform, state, ageMs, thresholdMs}. |
src/bus/index.ts:223-263 |
| F4 | worker.drain-started / worker.drain-ended carry workerId and label?. |
src/gateway/worker-registry.ts:474; docs/EVENTS.md:111-112 |
| F5 | A relayed worker event has workerId in its payload. |
src/gateway/worker-link.ts:533 |
| F6 | WorkerRegistry.view(workerId) returns the view or undefined; the view holds label when the worker set one. GatewayService.workers is the registry. |
src/gateway/worker-registry.ts:151, :244; src/gateway/service.ts:118 |
| F7 | startGatewayDaemon holds gatewayService before the dispatcher is built. |
src/daemon/main.ts:740-850 |
| F8 | Slack incoming webhooks take {"text": ...} and need &, <, > escaped in text; <!channel> in text pings the channel. |
Slack API docs, "Sending messages using incoming webhooks" and "Formatting text for app surfaces: escaping text"; Slack's service has no public source, so its docs are the source |
| F9 | No exported byte or duration formatter outside src/cli; src/core/cleanup/idle-destroy.ts has a private GiB one. diskPressure.freeBytesThreshold defaults to 10 * 1024 ** 3 and is documented as 10 GiB. |
src/core/cleanup/idle-destroy.ts:66-69; src/core/config.ts:753; docs/CONFIGURATION.md:64 |
| F11 | A worker's claimed workerId and label come from headers it sends before authentication; a relayed payload is z.unknown() and is not checked by the gateway. |
src/ports/uplink-websocket.ts:220-239; src/contract/schemas.ts:784; src/gateway/worker-link.ts:532-534 (isRecord(payload) ? {...payload, workerId} : {workerId}), :622-624 |
| F10 | #719's createAlerts, queue, and config validator, and the Shared values rows of #707. |
#719 |
Modules touched
Searches: ALERT_EVENTS, createAlerts, configSchema gateway.alerts, workers.view.
src/core/config.ts:formatadds"slack";secretrefused onslack, required ongeneric.src/contract/schemas.ts:format: z.enum(["generic", "slack"]);secretis already optional (#719).PROTOCOL_VERSION_RANGEand every test that pins the current number (search the number itself and "protocol " insrc/, e.g.src/contract/protocol.test.ts,src/daemon/server.test.ts,src/daemon-protocol/index.test.ts,src/gateway/service.test.ts): bump by one,minwith it.src/alerts/formats/slack/(new), one line inALERT_FORMAT_REGISTRY,"slack"in core'sALERT_FORMATSandALERT_FORMAT_SECRET("refused"); insrc/alerts/: a message tableRecord<AlertEventName, …>;createAlertsgainsworkerLabel(workerId: string): string | undefined, and alerts falls back to the id when it answersundefined; the manual.src/daemon/main.ts: passesworkerLabel: (id) => gatewayService.workers.view(id)?.label.src/alerts/AGENTS.md: the message table rule and the Known gap above.- Docs:
docs/CONFIGURATION.md(slackformat, the messages table, Slack as the promised service).
flowchart LR
main["daemon/main.ts *"] --> AL["alerts/ *"]
main -. "workerLabel" .-> REG[gateway WorkerRegistry]
AL --> CFG["core/config.ts *"]
CS["contract/schemas.ts *"]
Surface and extension points
createAlerts options gain workerLabel(workerId: string): string | undefined; the fallback to the id is inside alerts, so its tests see it. A new directory and one registry line, as #719's extension point asks; the queue does not change. The conformance suite runs on slack too.
Contract and event changes
config.getoutput:formatmay be"slack", with nosecret. This PR bumps the protocol by one,minwith it; the PR that merges second among the open tasks bumping this line (today #713, #714, #629-#632, #635-#637, #719, #721) bumps again.- No event change.
Other code on the same state
Searches: view( on WorkerRegistry (6 hits), ALERT_EVENTS (#719).
- The registry writes a view's
labelat connect;workerLabelonly reads. A worker removed before its message is formatted falls back to the id. - #719's queue and tries carry
slackmessages unchanged.
Failure modes
Same as #719 for the POST. A missing optional field takes the shorter message above. A malformed relayed payload is the Known gap above: a message with undefined or ? in it.
Rules in play
- safety, "wire input is a claim":
worker.rejectednames what the connection claimed, never a registry worker; every variable part is escaped and cut. - architecture, a module hands out a capability, not its parts: alerts gets
workerLabel, not the registry. - ADR 0035 §5.
Tests
Seam: createAlerts with a real EventBus, FakeClock, FakeWebhookPoster, in src/alerts/alerts.test.ts (#719's file); config through loadConfig in src/core/config.test.ts.
a slack destination posts {"text": message} with only the Content-Type and User-Agent headerseach of the eleven events posts its message word for word(one case per row of the In short table,worker.connectedandworker.disconnectedwith each variant)names the worker by the label workerLabel answers, and by the id when it answers undefined(cases: a label;undefined)builds a message when it leaves the queue and resends the same body on later tries(the label changes while the message waits: the first try carries the new label; it changes again between try 1 and try 2: try 2's body equals try 1's)worker.rejected names the claimed label, else the claimed id, else an unknown worker, and adds the count only above one(cases: label; id only; neither;count1;count4)formats an age in seconds, minutes, and hours with minutes(45 000 →45 s; 840 000 →14 min; 3 900 000 →1 h 5 min; 7 200 000 →2 h; 59 999 →59 s)formats bytes in GiB and MiB with one decimal rounded half up, dropping .0(8 804 682 956 →8.2 GiB; 10 737 418 240 →10 GiB; 536 870 912 →512 MiB; 1 020 054 733 →972.8 MiB)cuts every variable part to 300 code points, then escapes &, < and >(cases: an error text<b>&; a registry label<!channel>; a claimedworker.rejectedlabel<!here>; a 301-character error; a label whose 300th code point is an emoji)a relayed payload with missing fields posts the text the Known gap names(cases:device.quarantine-abandonedwith noattempts→…retired after undefined failed clean-ups.;disk.pressure-detectedwith nofreeBytes→…low on disk: ? free (threshold 10 GiB).; a payload replaced by{workerId}fordevice.stalled-transition-detected→…has been undefined for ?.)redactConfig masks a slack destination's url and adds no secret key(extends #719's case)config.get's schema keeps a slack destination without a secret(extends #719's deep-equal case)a slack destination is tried again on 5xx like a generic one(one case: #719 proves the rest of delivery)loadConfig accepts a slack destination, refuses a secret on it, and lists both formats in the format message(the In short inputs table)a worker joining a gateway with a slack destination makes a loopback receiver get its connected message naming the worker's label(extends #719's case ingateway-fleet.e2e.test.ts; provesmain.tspasses the registry's label)
Done when
- CI is green with the lines above.
- In
gateway-fleet.e2e.test.ts, a loopback receiver for aslackdestination gets🟢 Worker <label> connected (simlock 1.0.0-e2e).with the worker's label. docs/CONFIGURATION.mdlists theslackformat, promises Slack only ("other services that take Slack's incoming-webhook body may work"), and gives the message for each event.
Out of scope
- Slack Block Kit, threads, mentions, links to the console.
- Teams, PagerDuty, email.
Depends on
- #719
Written by an agent.
- Ngôn ngữ chính
- TypeScript
- Star
- 20
- Fork
- 2
- Merge trung bình
- 8 giờ 17 phút
- Pull request đã merge (30 ngày)
- 273
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của callstackincubator/simlock
-
bug:needs-info
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
callstackincubator/simlock#657 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
callstackincubator/simlock#639 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
flaky-test
Độ khó 3/5 Nửa ngày Mức phù hợp với người mới 58/100
callstackincubator/simlock#730 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
task:ready
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
callstackincubator/simlock#721 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
task:ready
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
callstackincubator/simlock#719 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của callstackincubator/simlock
Issue tương tự
-
area: aria/combobox gemini-triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
angular/components#33973 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug priority: P2
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[bug] status, list and push crash with an uncaught NotInitializedError stack trace when teamai is not initializedCó thể đã có người làm @jay23606 đã nhận hôm nay. Đang mởbug good first issue help wanted
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Tencent/teamai-cli#1227 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Tool]: YapĐang mở
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 62/100
withkarann/aifoxx#417 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
sindresorhus/eslint-plugin-unicorn#3921 ·
Maintainer thường phản hồi trong vòng 1 ngày