Skip to main content

krabka_docgen/
broker.rs

1//! Generate the three broker reference pages.
2//!
3//! The pages are the server config, from the `FileConfig` JSON schema, the
4//! topic configs whitelist, and the protocol API catalog.
5
6use {
7    crate::schema_md::render_sectioned_field_table,
8    krabka_broker::{
9        api_catalog,
10        api_catalog::{ClientMetricsReceiver, ListenerKind, VersionGates},
11        file_config,
12    },
13    std::fmt::Write,
14};
15
16/// Server-config reference, rendered from the `FileConfig` JSON schema.
17///
18/// The schema comes from [`krabka_broker::file_config::config_schema`], which
19/// is the same value `krabka-broker --print-config-schema` prints and the same
20/// value `docs/config-schema.json` holds.
21///
22/// This function uses [`render_sectioned_field_table`], so each top-level TOML
23/// key or table becomes its own captioned section with a horizontal-rule
24/// separator. The result is not one dense flat table.
25#[must_use]
26pub fn server_config_md() -> String {
27    let value = file_config::config_schema();
28    let mut out = String::from(
29        "Krabka brokers are configured from a TOML file. Each section below maps \
30         to a top-level key or `[table]` of that file.\n\n",
31    );
32    out.push_str(&render_sectioned_field_table(&value));
33    out
34}
35
36/// Topic-config reference: the `AlterConfigs` whitelist.
37#[must_use]
38pub fn topic_configs_md() -> String {
39    let mut out = String::from(
40        "These dynamic per-topic configs are accepted by `AlterConfigs` / \
41         `IncrementalAlterConfigs`. Unknown keys are rejected with `INVALID_CONFIG`.\n\n\
42         | Key | Type | Default | KIP | Description |\n\
43         |-----|------|---------|-----|-------------|\n",
44    );
45    for d in krabka_broker::topic_config_docs() {
46        let default = d.default.map_or_default(|x| format!("`{x}`"));
47        let kip = d.kip.unwrap_or("—");
48        let _ = writeln!(
49            out,
50            "| `{}` | {} | {} | {} | {} |",
51            d.key, d.value_type, default, kip, d.description
52        );
53    }
54    out
55}
56
57/// The KIP, or the protocol-guide section, that defines an API key. It renders
58/// into a markdown link.
59struct ApiSpec {
60    label: &'static str,
61    url: &'static str,
62}
63
64/// Canonical specification for an API key.
65///
66/// This is the KIP, or the protocol-guide section, that defines the API key.
67///
68/// The Kafka wire protocol is not an IETF RFC standard. The
69/// [Apache Kafka protocol guide](https://kafka.apache.org/protocol) specifies
70/// it, together with the per-feature KIP that introduced each RPC. This
71/// function maps each API key to the KIP that defines it where we know that KIP
72/// confidently. For the other API keys, it falls back to the API-keys section
73/// of the protocol guide.
74fn api_spec(api_key: i16) -> ApiSpec {
75    // The general protocol-guide fallback for APIs without a single defining
76    // KIP (the original Kafka 0.x core RPCs and a handful of admin RPCs).
77    const PROTOCOL_GUIDE: ApiSpec = ApiSpec {
78        label: "Protocol guide",
79        url: "https://kafka.apache.org/43/design/protocol/",
80    };
81    const KIP_848: ApiSpec = ApiSpec {
82        label: "KIP-848",
83        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-848%3A+The+Next+Generation+of+the+Consumer+Rebalance+Protocol",
84    };
85    const KIP_932: ApiSpec = ApiSpec {
86        label: "KIP-932",
87        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka",
88    };
89    const KIP_1071: ApiSpec = ApiSpec {
90        label: "KIP-1071",
91        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-1071%3A+Streams+Rebalance+Protocol",
92    };
93    const KIP_595: ApiSpec = ApiSpec {
94        label: "KIP-595",
95        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-595%3A+A+Raft+Protocol+for+the+Metadata+Quorum",
96    };
97    const KIP_630: ApiSpec = ApiSpec {
98        label: "KIP-630",
99        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-630%3A+Kafka+Raft+Snapshot",
100    };
101    const KIP_853: ApiSpec = ApiSpec {
102        label: "KIP-853",
103        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-853%3A+KRaft+Controller+Membership+Changes",
104    };
105    const KIP_98: ApiSpec = ApiSpec {
106        label: "KIP-98",
107        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging",
108    };
109    const KIP_500: ApiSpec = ApiSpec {
110        label: "KIP-500",
111        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum",
112    };
113    const KIP_631: ApiSpec = ApiSpec {
114        label: "KIP-631",
115        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-631%3A+The+Quorum-based+Kafka+Controller",
116    };
117    const KIP_714: ApiSpec = ApiSpec {
118        label: "KIP-714",
119        url: "https://cwiki.apache.org/confluence/display/KAFKA/KIP-714%3A+Client+metrics+and+observability",
120    };
121
122    // Only the KIP-specific arms are enumerated; every API key without a single
123    // confidently-known defining KIP (the core 0.x RPCs, delegation tokens,
124    // SCRAM/quota admin, UpdateFeatures, Envelope, transactions admin, etc.)
125    // falls through to the protocol-guide permalink.
126    match api_key {
127        // KIP-98: idempotent producer + transactions
128        // (InitProducerId, Add*ToTxn, EndTxn, WriteTxnMarkers, TxnOffsetCommit).
129        22 | 24..=28 => KIP_98,
130        // KIP-595: the KRaft Raft protocol (Vote/Begin/EndQuorumEpoch,
131        // DescribeQuorum) and AlterPartition.
132        52..=56 => KIP_595,
133        // KIP-630: Raft snapshot fetch.
134        59 => KIP_630,
135        // KIP-500/631: KRaft broker & controller registration / heartbeat.
136        62..=64 | 70 => KIP_631,
137        // KIP-848: next-gen consumer group protocol.
138        68 | 69 => KIP_848,
139        // KIP-714: client metrics / observability telemetry.
140        71 | 72 => KIP_714,
141        // KIP-932: share groups (queues) and their state RPCs.
142        76..=79 | 83..=87 | 90..=92 => KIP_932,
143        // KIP-853: KRaft voter membership changes.
144        80..=82 => KIP_853,
145        // KIP-1071: Kafka Streams rebalance protocol.
146        88 | 89 => KIP_1071,
147        // GetReplicaLogInfo — krabka-private (1020) KRaft tooling RPC; KIP-500
148        // family.
149        1020 => KIP_500,
150        _ => PROTOCOL_GUIDE,
151    }
152}
153
154/// Protocol API catalog: every Kafka API key the broker advertises to a
155/// client.
156///
157/// The set is the one a client listener advertises with a client-metrics
158/// receiver configured. That is the widest client-facing surface: it leaves
159/// out the inter-broker-only keys, which no Kafka client sends, and it keeps
160/// the two KIP-714 telemetry keys, which a broker withholds only when an
161/// operator configures no receiver.
162#[must_use]
163pub fn protocol_apis_md() -> String {
164    use krabka_protocol::api_key::ApiKey;
165    let mut out = String::from(
166        "The Kafka protocol APIs this broker advertises to a client in its \
167         `ApiVersions` (key 18) response, with the supported version range for \
168         each. The wire protocol is specified by the [Apache Kafka protocol \
169         guide](https://kafka.apache.org/protocol); the **Spec** column links \
170         each API to the KIP (or protocol-guide section) that defines it.\n\n\
171         | API Key | Name | Min Version | Max Version | Spec |\n\
172         |---------|------|-------------|-------------|------|\n",
173    );
174    let mut apis = api_catalog::supported_apis(
175        ListenerKind::Client,
176        ClientMetricsReceiver::Configured,
177        VersionGates::default(),
178    );
179    apis.sort_by_key(|a| a.api_key);
180    for a in apis {
181        let name: &'static str = ApiKey::from_i16(a.api_key).map_or("?", <&'static str>::from);
182        let ApiSpec { label, url } = api_spec(a.api_key);
183        let _ = writeln!(
184            out,
185            "| {} | {} | {} | {} | [{}]({}) |",
186            a.api_key, name, a.min_version, a.max_version, label, url
187        );
188    }
189    out
190}
191
192#[cfg(test)]
193mod tests {
194    use {super::*, assert2::check};
195    #[test]
196    fn server_config_page_has_sectioned_field_tables() {
197        let md = server_config_md();
198        // Sectioned layout: per-key `## ` headings + horizontal-rule separators,
199        // and a known top-level table (`tls_config`) renders as its own section.
200        for needle in [
201            "| Field | Type | Required | Default | Description |",
202            "## ",
203            "\n---\n",
204            "## tls_config",
205        ] {
206            assert2::assert!(md.contains(needle));
207        }
208    }
209    #[test]
210    fn topic_configs_page_lists_retention_ms() {
211        let md = topic_configs_md();
212        assert2::assert!(md.contains("`retention.ms`"));
213        assert2::assert!(md.contains("| Key | Type | Default | KIP | Description |"));
214    }
215    #[test]
216    fn protocol_apis_page_lists_named_apis() {
217        let md = protocol_apis_md();
218        check!(md.contains("| API Key | Name | Min Version | Max Version | Spec |"));
219        check!(md.contains("| 18 | ApiVersions |"));
220        check!(!md.contains("| ? |"));
221    }
222    #[test]
223    fn protocol_apis_page_links_canonical_spec_and_kips() {
224        let md = protocol_apis_md();
225        for needle in [
226            // Canonical protocol-guide permalink in the header.
227            "https://kafka.apache.org/protocol",
228            // At least one real KIP link in the Spec column.
229            "[KIP-",
230            "cwiki.apache.org/confluence/display/KAFKA/KIP-",
231        ] {
232            assert2::assert!(md.contains(needle));
233        }
234        // Consumer-group heartbeat (68) maps to KIP-848.
235        let row = md
236            .lines()
237            .find(|l| l.contains("| 68 |"))
238            .expect("ConsumerGroupHeartbeat row");
239        assert2::assert!(row.contains("[KIP-848]"));
240        // Share-group fetch (78) maps to KIP-932.
241        let share = md
242            .lines()
243            .find(|l| l.contains("| 78 |"))
244            .expect("ShareFetch row");
245        assert2::assert!(share.contains("[KIP-932]"));
246    }
247}