linera_exporter/
config.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Configuration types for the block exporter.
5
6use std::{fmt, net::SocketAddr};
7
8use linera_rpc::config::{ExporterServiceConfig, TlsConfig};
9use serde::{
10    de::{Error, MapAccess, Visitor},
11    Deserialize, Deserializer, Serialize, Serializer,
12};
13
14/// The configuration file for the linera-exporter.
15#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
16pub struct BlockExporterConfig {
17    /// Identity for the block exporter state.
18    pub id: u32,
19
20    /// The server configuration for the linera-exporter.
21    pub service_config: ExporterServiceConfig,
22
23    /// The configuration file for the export destinations.
24    #[serde(default)]
25    pub destination_config: DestinationConfig,
26
27    /// The configuration file to impose various limits
28    /// on the resources used by the linera-exporter.
29    #[serde(default)]
30    pub limits: LimitsConfig,
31
32    /// The address to expose the `/metrics` endpoint on.
33    pub metrics_port: u16,
34}
35
36impl BlockExporterConfig {
37    /// Returns the address to expose the `/metrics` endpoint on.
38    pub fn metrics_address(&self) -> SocketAddr {
39        SocketAddr::from(([0, 0, 0, 0], self.metrics_port))
40    }
41}
42
43/// Configuration file for the exports.
44#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
45pub struct DestinationConfig {
46    /// The destination URIs to export to.
47    pub destinations: Vec<Destination>,
48    /// Export blocks to the current committee.
49    #[serde(default)]
50    pub committee_destination: bool,
51}
52
53// Each destination has an ID and a configuration.
54#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
55pub struct DestinationId {
56    address: String,
57    kind: DestinationKind,
58}
59
60impl DestinationId {
61    /// Creates a new destination ID from the address and kind.
62    pub fn new(address: String, kind: DestinationKind) -> Self {
63        Self { address, kind }
64    }
65
66    pub fn validator(address: String) -> Self {
67        Self {
68            address,
69            kind: DestinationKind::Validator,
70        }
71    }
72
73    /// Returns the address of the destination.
74    pub fn address(&self) -> &str {
75        &self.address
76    }
77
78    /// Returns the kind of the destination.
79    pub fn kind(&self) -> DestinationKind {
80        self.kind
81    }
82}
83
84/// The uri to provide export services to.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum Destination {
87    Indexer {
88        /// The gRPC network protocol.
89        tls: TlsConfig,
90        /// The host name of the target destination (IP or hostname).
91        endpoint: String,
92        /// The port number of the target destination.
93        port: u16,
94    },
95    Validator {
96        /// The host name of the target destination (IP or hostname).
97        endpoint: String,
98        /// The port number of the target destination.
99        port: u16,
100    },
101    Logging {
102        /// The log file path.
103        file_name: String,
104    },
105}
106
107/// The description for the gRPC based destination.
108/// Discriminates the export mode and the client to use.
109#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)]
110pub enum DestinationKind {
111    /// The indexer description.
112    Indexer,
113    /// The validator description.
114    Validator,
115    /// The logging target.
116    Logging,
117}
118
119impl Serialize for Destination {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: Serializer,
123    {
124        use serde::ser::SerializeMap;
125
126        match self {
127            Destination::Indexer {
128                tls,
129                endpoint,
130                port,
131            } => {
132                let mut map = serializer.serialize_map(Some(4))?;
133                map.serialize_entry("kind", "Indexer")?;
134                map.serialize_entry("tls", tls)?;
135                map.serialize_entry("endpoint", endpoint)?;
136                map.serialize_entry("port", port)?;
137                map.end()
138            }
139            Destination::Validator { endpoint, port } => {
140                let mut map = serializer.serialize_map(Some(3))?;
141                map.serialize_entry("kind", "Validator")?;
142                map.serialize_entry("endpoint", endpoint)?;
143                map.serialize_entry("port", port)?;
144                map.end()
145            }
146            Destination::Logging { file_name } => {
147                let mut map = serializer.serialize_map(Some(2))?;
148                map.serialize_entry("kind", "Logging")?;
149                map.serialize_entry("file_name", file_name)?;
150                map.end()
151            }
152        }
153    }
154}
155
156struct DestinationVisitor;
157
158impl<'de> Visitor<'de> for DestinationVisitor {
159    type Value = Destination;
160
161    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
162        formatter.write_str("a map with a 'kind' field")
163    }
164
165    fn visit_map<V>(self, mut map: V) -> Result<Destination, V::Error>
166    where
167        V: MapAccess<'de>,
168    {
169        let mut kind: Option<String> = None;
170        let mut tls: Option<TlsConfig> = None;
171        let mut endpoint: Option<String> = None;
172        let mut port: Option<u16> = None;
173        let mut file_name: Option<String> = None;
174
175        while let Some(key) = map.next_key::<String>()? {
176            match key.as_str() {
177                "kind" => {
178                    if kind.is_some() {
179                        return Err(V::Error::duplicate_field("kind"));
180                    }
181                    kind = Some(map.next_value()?);
182                }
183                "tls" => {
184                    if tls.is_some() {
185                        return Err(V::Error::duplicate_field("tls"));
186                    }
187                    tls = Some(map.next_value()?);
188                }
189                "endpoint" => {
190                    if endpoint.is_some() {
191                        return Err(V::Error::duplicate_field("endpoint"));
192                    }
193                    endpoint = Some(map.next_value()?);
194                }
195                "port" => {
196                    if port.is_some() {
197                        return Err(V::Error::duplicate_field("port"));
198                    }
199                    port = Some(map.next_value()?);
200                }
201                "file_name" => {
202                    if file_name.is_some() {
203                        return Err(V::Error::duplicate_field("file_name"));
204                    }
205                    file_name = Some(map.next_value()?);
206                }
207                _ => {
208                    // Ignore unknown fields
209                    let _: serde::de::IgnoredAny = map.next_value()?;
210                }
211            }
212        }
213
214        let kind = kind.ok_or_else(|| V::Error::missing_field("kind"))?;
215
216        match kind.as_str() {
217            "Indexer" => {
218                let tls = tls.ok_or_else(|| V::Error::missing_field("tls"))?;
219                let endpoint = endpoint.ok_or_else(|| V::Error::missing_field("endpoint"))?;
220                let port = port.ok_or_else(|| V::Error::missing_field("port"))?;
221                Ok(Destination::Indexer {
222                    tls,
223                    endpoint,
224                    port,
225                })
226            }
227            "Validator" => {
228                let endpoint = endpoint.ok_or_else(|| V::Error::missing_field("endpoint"))?;
229                let port = port.ok_or_else(|| V::Error::missing_field("port"))?;
230                Ok(Destination::Validator { endpoint, port })
231            }
232            "Logging" => {
233                let file_name = file_name.ok_or_else(|| V::Error::missing_field("file_name"))?;
234                Ok(Destination::Logging { file_name })
235            }
236            _ => Err(V::Error::unknown_variant(
237                &kind,
238                &["Indexer", "Validator", "Logging"],
239            )),
240        }
241    }
242}
243impl<'de> Deserialize<'de> for Destination {
244    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
245    where
246        D: Deserializer<'de>,
247    {
248        deserializer.deserialize_map(DestinationVisitor)
249    }
250}
251/// The configuration file to impose various limits
252/// on the resources used by the linera-exporter.
253#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
254pub struct LimitsConfig {
255    /// Time period in milliseconds between periodic persistence
256    /// to the shared storage.
257    pub persistence_period_ms: u32,
258    /// Maximum size of the work queue i.e. maximum number
259    /// of blocks queued up for exports per destination.
260    pub work_queue_size: u16,
261    /// Maximum weight of the blob cache in megabytes.
262    pub blob_cache_weight_mb: u16,
263    /// Estimated number of elements for the blob cache.
264    pub blob_cache_items_capacity: u16,
265    /// Maximum weight of the block cache in megabytes.
266    pub block_cache_weight_mb: u16,
267    /// Estimated number of elements for the block cache.
268    pub block_cache_items_capacity: u16,
269    /// Maximum weight in megabytes for the combined
270    /// cache, consisting of small miscellaneous items.
271    pub auxiliary_cache_size_mb: u16,
272}
273
274impl Default for LimitsConfig {
275    fn default() -> Self {
276        Self {
277            persistence_period_ms: 299 * 1000,
278            work_queue_size: 256,
279            blob_cache_weight_mb: 1024,
280            blob_cache_items_capacity: 8192,
281            block_cache_weight_mb: 1024,
282            block_cache_items_capacity: 8192,
283            auxiliary_cache_size_mb: 1024,
284        }
285    }
286}
287
288impl Destination {
289    pub fn address(&self) -> String {
290        match &self {
291            Destination::Indexer {
292                tls,
293                endpoint,
294                port,
295            } => {
296                let tls = match tls {
297                    TlsConfig::ClearText => "http",
298                    TlsConfig::Tls => "https",
299                };
300
301                format!("{tls}://{endpoint}:{port}")
302            }
303
304            Destination::Validator { endpoint, port } => {
305                format!("{}:{}:{}", "grpc", endpoint, port)
306            }
307
308            Destination::Logging { file_name } => file_name.to_string(),
309        }
310    }
311
312    pub fn id(&self) -> DestinationId {
313        let kind = match self {
314            Destination::Indexer { .. } => DestinationKind::Indexer,
315            Destination::Validator { .. } => DestinationKind::Validator,
316            Destination::Logging { .. } => DestinationKind::Logging,
317        };
318        DestinationId {
319            address: self.address(),
320            kind,
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn parse_from_str() {
331        let input = r#"
332                        tls = "ClearText"
333                        endpoint = "127.0.0.1"
334                        port = 8080
335                        kind = "Indexer"
336            "#
337        .to_string();
338
339        let destination: Destination = toml::from_str(&input).unwrap();
340        assert_eq!(
341            destination,
342            Destination::Indexer {
343                tls: TlsConfig::ClearText,
344                endpoint: "127.0.0.1".to_owned(),
345                port: 8080,
346            }
347        );
348
349        let input = r#"
350                        endpoint = "127.0.0.1"
351                        port = 8080
352                        kind = "Validator"
353        "#
354        .to_string();
355        let destination: Destination = toml::from_str(&input).unwrap();
356        assert_eq!(
357            destination,
358            Destination::Validator {
359                endpoint: "127.0.0.1".to_owned(),
360                port: 8080,
361            }
362        );
363
364        let input = r#"
365                        file_name = "export.log"
366                        kind = "Logging"
367        "#
368        .to_string();
369        let destination: Destination = toml::from_str(&input).unwrap();
370        assert_eq!(
371            destination,
372            Destination::Logging {
373                file_name: "export.log".to_owned(),
374            }
375        );
376    }
377}