Skip to main content

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