blueprint_qos/
service_builder.rs

1use std::sync::Arc;
2
3use crate::QoSConfig;
4use crate::error::Result;
5use crate::heartbeat::{HeartbeatConfig, HeartbeatConsumer};
6use crate::logging::{GrafanaConfig, LokiConfig};
7use crate::metrics::opentelemetry::OpenTelemetryConfig;
8use crate::metrics::types::MetricsConfig;
9use crate::servers::{
10    grafana::GrafanaServerConfig, loki::LokiServerConfig, prometheus::PrometheusServerConfig,
11};
12use crate::unified_service::QoSService;
13
14/// Fluent builder for assembling a `QoSService`.
15///
16/// Use this builder to enable and configure optional sub-systems such as heartbeat
17/// reporting, metrics (Prometheus/OpenTelemetry), log aggregation (Loki) and
18/// Grafana dashboards. All features are off by default and can be enabled with the
19/// corresponding `with_*` methods.
20pub struct QoSServiceBuilder<C: HeartbeatConsumer + Send + Sync + 'static> {
21    config: QoSConfig,
22    heartbeat_consumer: Option<Arc<C>>,
23    _phantom_c: std::marker::PhantomData<C>,
24    otel_config: Option<OpenTelemetryConfig>,
25    prometheus_datasource: Option<String>,
26    loki_datasource: Option<String>,
27    create_dashboard: bool,
28    http_rpc_endpoint: Option<String>,
29    ws_rpc_endpoint: Option<String>,
30    keystore_uri: Option<String>,
31}
32
33impl<C: HeartbeatConsumer + Send + Sync + 'static> Default for QoSServiceBuilder<C> {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl<C: HeartbeatConsumer + Send + Sync + 'static> QoSServiceBuilder<C> {
40    /// Creates a new `QoS` service builder with default settings.
41    ///
42    /// Initializes a builder with an empty configuration. All components (heartbeat, metrics,
43    /// logging, etc.) are disabled by default and must be explicitly configured using the
44    /// builder methods. This provides a clean starting point for building a custom `QoS`
45    /// observability setup.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            config: QoSConfig::default(),
50            heartbeat_consumer: None,
51            _phantom_c: std::marker::PhantomData,
52            otel_config: None,
53            prometheus_datasource: None,
54            loki_datasource: None,
55            create_dashboard: false,
56            http_rpc_endpoint: None,
57            ws_rpc_endpoint: None,
58            keystore_uri: None,
59        }
60    }
61
62    /// Sets the complete `QoS` configuration at once.
63    ///
64    /// This method allows you to provide a pre-configured `QoSConfig` instance,
65    /// which can be useful when loading configuration from external sources or
66    /// when you want to reuse an existing configuration. This will override any
67    /// previous component-specific configurations that were set on the builder.
68    ///
69    /// # Parameters
70    /// * `config` - A complete `QoSConfig` containing settings for all `QoS` components
71    #[must_use]
72    pub fn with_config(mut self, config: QoSConfig) -> Self {
73        self.config = config;
74        self
75    }
76
77    /// Configures the heartbeat service component.
78    ///
79    /// The heartbeat service sends periodic signals to the Tangle blockchain to indicate that
80    /// the service is alive and functioning properly. This helps prevent slashing of staked
81    /// tokens and provides operational visibility.
82    ///
83    /// # Parameters
84    /// * `config` - Configuration for the heartbeat service including service ID, blueprint ID,
85    ///   and heartbeat parameters such as interval and jitter
86    #[must_use]
87    pub fn with_heartbeat_config(mut self, config: HeartbeatConfig) -> Self {
88        self.config.heartbeat = Some(config);
89        self
90    }
91
92    /// Configures the metrics collection component.
93    ///
94    /// Metrics collection captures system resource usage (CPU, memory, disk, network) and
95    /// application-specific metrics (job execution statistics, custom metrics). These metrics
96    /// can be exported to Prometheus and visualized in Grafana dashboards.
97    ///
98    /// # Parameters
99    /// * `config` - Configuration for metrics collection including retention settings,
100    ///   collection intervals, and export options
101    #[must_use]
102    pub fn with_metrics_config(mut self, config: MetricsConfig) -> Self {
103        self.config.metrics = Some(config);
104        self
105    }
106
107    /// Configures the Loki logging integration.
108    ///
109    /// Loki is a log aggregation system that works well with Grafana for visualization.
110    /// This integration sends application logs to a Loki server, allowing centralized
111    /// log storage, querying, and correlation with metrics.
112    ///
113    /// # Parameters
114    /// * `config` - Configuration for Loki integration including server URL, authentication,
115    ///   labels, and batch settings
116    #[must_use]
117    pub fn with_loki_config(mut self, config: LokiConfig) -> Self {
118        self.config.loki = Some(config);
119        self
120    }
121
122    /// Configures the Grafana integration for dashboard visualization.
123    ///
124    /// Grafana provides powerful visualization capabilities for metrics and logs. This
125    /// configuration allows the `QoS` service to automatically create and update dashboards
126    /// that display service health, resource usage, and operational metrics.
127    ///
128    /// # Parameters
129    /// * `config` - Configuration for Grafana integration including server URL, authentication,
130    ///   and organization settings
131    #[must_use]
132    pub fn with_grafana_config(mut self, config: GrafanaConfig) -> Self {
133        self.config.grafana = Some(config);
134        self
135    }
136
137    /// Sets the heartbeat consumer implementation.
138    ///
139    /// The heartbeat consumer is responsible for processing and submitting heartbeat messages
140    /// to the Tangle blockchain. It handles the cryptographic signing of heartbeat messages
141    /// and submits them to the appropriate chain endpoint.
142    ///
143    /// This is required if heartbeat functionality is enabled.
144    ///
145    /// # Parameters
146    /// * `consumer` - Implementation of the `HeartbeatConsumer` trait that will process and
147    ///   submit heartbeats to the blockchain
148    #[must_use]
149    pub fn with_heartbeat_consumer(mut self, consumer: Arc<C>) -> Self {
150        self.heartbeat_consumer = Some(consumer);
151        self
152    }
153
154    /// Configures OpenTelemetry integration for distributed tracing and advanced metrics.
155    ///
156    /// OpenTelemetry provides a standardized way to collect and export telemetry data
157    /// (traces, metrics, logs) across services. This integration enables correlation of
158    /// traces with logs and metrics for comprehensive observability.
159    ///
160    /// # Parameters
161    /// * `config` - OpenTelemetry configuration including exporter settings, sampling,
162    ///   and resource attribution
163    #[must_use]
164    pub fn with_otel_config(mut self, config: OpenTelemetryConfig) -> Self {
165        self.otel_config = Some(config);
166        self
167    }
168
169    /// Configures automatic Grafana dashboard creation with a specific Prometheus datasource.
170    ///
171    /// This method enables the automatic creation or updating of a Grafana dashboard
172    /// during `QoS` service initialization. The dashboard will include panels for system
173    /// metrics, resource usage, and application-specific metrics sourced from the
174    /// specified Prometheus datasource.
175    ///
176    /// # Parameters
177    /// * `datasource_uid` - The Grafana UID of the Prometheus datasource to use for metrics visualization
178    #[must_use]
179    pub fn with_prometheus_datasource(mut self, datasource_uid: &str) -> Self {
180        self.prometheus_datasource = Some(datasource_uid.to_string());
181        self.create_dashboard = true;
182        self
183    }
184
185    /// Configures Grafana dashboard to include logs from a specific Loki datasource.
186    ///
187    /// When used in combination with `with_prometheus_datasource`, this enables the creation
188    /// of comprehensive dashboards that include both metrics and logs. The dashboard will
189    /// include panels that allow for correlation between metrics and logs using the same
190    /// timestamps.
191    ///
192    /// # Parameters
193    /// * `datasource_uid` - The Grafana UID of the Loki datasource to use for log visualization
194    #[must_use]
195    pub fn with_loki_datasource(mut self, datasource_uid: &str) -> Self {
196        self.loki_datasource = Some(datasource_uid.to_string());
197        self
198    }
199
200    /// Configures the managed Grafana server instance.
201    ///
202    /// If server management is enabled, this configuration will be used to start and
203    /// manage a Grafana server instance automatically. The server can be run as a
204    /// Docker container or embedded server depending on the configuration.
205    ///
206    /// # Parameters
207    /// * `config` - Configuration for the Grafana server including host, port, and authentication settings
208    #[must_use]
209    pub fn with_grafana_server_config(mut self, config: GrafanaServerConfig) -> Self {
210        self.config.grafana_server = Some(config);
211        self
212    }
213
214    /// Configures the managed Loki log aggregation server instance.
215    ///
216    /// If server management is enabled, this configuration will be used to start and
217    /// manage a Loki server instance automatically. This server will collect and store
218    /// logs from the application for later querying and visualization in Grafana.
219    ///
220    /// # Parameters
221    /// * `config` - Configuration for the Loki server including host, port, retention, and storage settings
222    #[must_use]
223    pub fn with_loki_server_config(mut self, config: LokiServerConfig) -> Self {
224        self.config.loki_server = Some(config);
225        self
226    }
227
228    /// Set the Prometheus server configuration
229    #[must_use]
230    pub fn with_prometheus_server_config(mut self, config: PrometheusServerConfig) -> Self {
231        self.config.prometheus_server = Some(config);
232        self
233    }
234
235    /// Enable or disable server management
236    #[must_use]
237    pub fn manage_servers(mut self, manage: bool) -> Self {
238        self.config.manage_servers = manage;
239        self
240    }
241
242    /// Build the `QoS` service
243    ///
244    /// # Errors
245    /// Returns an error if the heartbeat consumer is not provided or if the service initialization fails
246    /// Set the HTTP RPC endpoint for `HeartbeatService`
247    #[must_use]
248    pub fn with_http_rpc_endpoint(mut self, endpoint: String) -> Self {
249        self.http_rpc_endpoint = Some(endpoint);
250        self
251    }
252
253    /// Set the WebSocket RPC endpoint for `HeartbeatService`
254    #[must_use]
255    pub fn with_ws_rpc_endpoint(mut self, endpoint: String) -> Self {
256        self.ws_rpc_endpoint = Some(endpoint);
257        self
258    }
259
260    /// Set the Keystore URI for `HeartbeatService`
261    #[must_use]
262    pub fn with_keystore_uri(mut self, uri: String) -> Self {
263        self.keystore_uri = Some(uri);
264        self
265    }
266
267    /// Build the `QoS` service
268    ///
269    /// # Errors
270    /// Returns an error if the heartbeat consumer is not provided or if the service initialization fails
271    pub async fn build(self) -> Result<QoSService<C>> {
272        let heartbeat_consumer = self.heartbeat_consumer.ok_or_else(|| {
273            crate::error::Error::Other("Heartbeat consumer is required".to_string())
274        })?;
275
276        let ws_rpc = self.ws_rpc_endpoint.unwrap_or_default();
277        let keystore = self.keystore_uri.unwrap_or_default();
278
279        if let Some(otel_config) = self.otel_config {
280            QoSService::with_otel_config(
281                self.config,
282                heartbeat_consumer,
283                ws_rpc,
284                keystore,
285                otel_config,
286            )
287            .await
288        } else {
289            QoSService::new(self.config, heartbeat_consumer, ws_rpc, keystore).await
290        }
291    }
292}