Skip to main content

sideways/
lib.rs

1//! # Sideways 🦀
2//!
3//! > *Observability from the side - because crabs walk sideways, and so should your telemetry.*
4//!
5//! A production-ready telemetry library for Rust services that provides:
6//! - **Datadog tracing** via dd-trace-rs with OpenTelemetry
7//! - **StatsD metrics** via Cadence with production-ready setup
8//!
9//! ## Features
10//!
11//! - Easy one-line initialization for both tracing and metrics
12//! - Graceful degradation when services are unavailable
13//! - Environment-based configuration
14//! - Health check filtering to reduce noise
15//! - Convenient prelude module with all metrics macros
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use sideways::{init_telemetry, TelemetryConfig};
21//! use sideways::prelude::*;
22//!
23//! #[tokio::main]
24//! async fn main() {
25//!     // Initialize both Datadog tracing and StatsD metrics
26//!     let config = TelemetryConfig::from_env();
27//!     let telemetry = init_telemetry(config).await;
28//!
29//!     // Use tracing as normal
30//!     tracing::info!("Application started");
31//!
32//!     // Emit metrics using macros - no need to import cadence!
33//!     statsd_count!("requests.handled", 1, "status" => "success");
34//!
35//!     // Cleanup on shutdown
36//!     if let Some(tracer) = telemetry.tracer_provider {
37//!         let _ = tracer.shutdown();
38//!     }
39//! }
40//! ```
41
42pub mod metrics;
43pub mod prelude;
44pub mod tracing;
45
46// Re-export cadence and cadence-macros for advanced usage
47pub use cadence;
48pub use cadence_macros;
49
50use std::env;
51use thiserror::Error;
52
53#[derive(Debug, Error)]
54pub enum TelemetryError {
55    #[error("Datadog tracing disabled via DD_TRACE_ENABLED=false")]
56    DatadogDisabled,
57
58    #[error("Failed to set global subscriber: {0}")]
59    SubscriberInit(String),
60
61    #[error("Metrics disabled via METRICS_ENABLED=false")]
62    MetricsDisabled,
63
64    #[error("Failed to bind UDP socket: {0}")]
65    SocketBind(std::io::Error),
66
67    #[error("Failed to create metric sink: {0}")]
68    SinkCreation(cadence::MetricError),
69}
70
71/// Configuration for telemetry initialization
72#[derive(Debug, Clone)]
73pub struct TelemetryConfig {
74    /// Enable/disable Datadog tracing (default: true)
75    pub datadog_enabled: bool,
76    /// Datadog service name
77    pub dd_service: String,
78    /// Datadog environment
79    pub dd_env: String,
80    /// Datadog trace agent URL
81    pub dd_trace_agent_url: String,
82    /// RUST_LOG filter
83    pub rust_log: String,
84
85    /// Enable/disable metrics (default: true)
86    pub metrics_enabled: bool,
87    /// StatsD host
88    pub statsd_host: String,
89    /// StatsD port
90    pub statsd_port: u16,
91    /// Metrics prefix/namespace
92    pub metrics_prefix: String,
93    /// Global tags to append to all metrics
94    pub global_tags: Vec<(String, String)>,
95
96    /// Enable/disable Datadog log ingestion (default: true)
97    pub dd_logs_enabled: bool,
98    /// Enable JSON-formatted console logging (default: false)
99    pub json_logging: bool,
100}
101
102impl Default for TelemetryConfig {
103    fn default() -> Self {
104        Self {
105            datadog_enabled: true,
106            dd_service: "sideways-service".to_string(),
107            dd_env: "development".to_string(),
108            dd_trace_agent_url: "http://localhost:8126".to_string(),
109            rust_log: "info".to_string(),
110            metrics_enabled: true,
111            statsd_host: "localhost".to_string(),
112            statsd_port: 8125,
113            metrics_prefix: "sideways".to_string(),
114            global_tags: Vec::new(),
115            dd_logs_enabled: true,
116            json_logging: false,
117        }
118    }
119}
120
121impl TelemetryConfig {
122    /// Load configuration from environment variables
123    pub fn from_env() -> Self {
124        let mut config = Self::default();
125
126        // Check if Datadog is explicitly disabled
127        if let Ok(enabled) = env::var("DD_TRACE_ENABLED") {
128            if enabled.to_lowercase() == "false" {
129                config.datadog_enabled = false;
130            }
131        }
132
133        // Check if metrics are explicitly disabled
134        if let Ok(enabled) = env::var("METRICS_ENABLED") {
135            if enabled.to_lowercase() == "false" {
136                config.metrics_enabled = false;
137            }
138        }
139
140        // Datadog configuration
141        if let Ok(service) = env::var("DD_SERVICE") {
142            config.dd_service = service;
143        }
144        if let Ok(dd_env) = env::var("DD_ENV") {
145            config.dd_env = dd_env;
146        }
147        if let Ok(url) = env::var("DD_TRACE_AGENT_URL") {
148            config.dd_trace_agent_url = url;
149        }
150
151        // Logging configuration
152        if let Ok(rust_log) = env::var("RUST_LOG") {
153            config.rust_log = rust_log;
154        }
155
156        // Metrics configuration
157        if let Ok(host) = env::var("STATSD_HOST") {
158            config.statsd_host = host;
159        }
160        if let Ok(port) = env::var("STATSD_PORT") {
161            if let Ok(port_num) = port.parse() {
162                config.statsd_port = port_num;
163            }
164        }
165        if let Ok(prefix) = env::var("METRICS_PREFIX") {
166            config.metrics_prefix = prefix;
167        }
168        if let Ok(tags_str) = env::var("STATSD_GLOBAL_TAGS") {
169            config.global_tags = Self::parse_tags(&tags_str);
170        }
171
172        // Datadog logs configuration
173        if let Ok(enabled) = env::var("DD_LOGS_ENABLED") {
174            if enabled.to_lowercase() == "false" {
175                config.dd_logs_enabled = false;
176            }
177        }
178
179        // JSON logging configuration
180        if let Ok(enabled) = env::var("JSON_LOGGING") {
181            if enabled.to_lowercase() == "true" {
182                config.json_logging = true;
183            }
184        }
185
186        config
187    }
188
189    /// Parse tags from a string in the format "key1:value1,key2:value2"
190    fn parse_tags(tags_str: &str) -> Vec<(String, String)> {
191        tags_str
192            .split(',')
193            .filter_map(|tag| {
194                let parts: Vec<&str> = tag.trim().splitn(2, ':').collect();
195                if parts.len() == 2 {
196                    Some((parts[0].to_string(), parts[1].to_string()))
197                } else {
198                    None
199                }
200            })
201            .collect()
202    }
203
204    /// Create a builder for custom configuration
205    pub fn builder() -> TelemetryConfigBuilder {
206        TelemetryConfigBuilder::default()
207    }
208}
209
210/// Builder for TelemetryConfig
211#[derive(Debug, Default)]
212pub struct TelemetryConfigBuilder {
213    config: TelemetryConfig,
214}
215
216impl TelemetryConfigBuilder {
217    pub fn datadog_enabled(mut self, enabled: bool) -> Self {
218        self.config.datadog_enabled = enabled;
219        self
220    }
221
222    pub fn dd_service(mut self, service: impl Into<String>) -> Self {
223        self.config.dd_service = service.into();
224        self
225    }
226
227    pub fn dd_env(mut self, env: impl Into<String>) -> Self {
228        self.config.dd_env = env.into();
229        self
230    }
231
232    pub fn dd_trace_agent_url(mut self, url: impl Into<String>) -> Self {
233        self.config.dd_trace_agent_url = url.into();
234        self
235    }
236
237    pub fn rust_log(mut self, filter: impl Into<String>) -> Self {
238        self.config.rust_log = filter.into();
239        self
240    }
241
242    pub fn metrics_enabled(mut self, enabled: bool) -> Self {
243        self.config.metrics_enabled = enabled;
244        self
245    }
246
247    pub fn statsd_host(mut self, host: impl Into<String>) -> Self {
248        self.config.statsd_host = host.into();
249        self
250    }
251
252    pub fn statsd_port(mut self, port: u16) -> Self {
253        self.config.statsd_port = port;
254        self
255    }
256
257    pub fn metrics_prefix(mut self, prefix: impl Into<String>) -> Self {
258        self.config.metrics_prefix = prefix.into();
259        self
260    }
261
262    /// Set global tags that will be appended to all metrics
263    pub fn global_tags(mut self, tags: Vec<(String, String)>) -> Self {
264        self.config.global_tags = tags;
265        self
266    }
267
268    /// Add a single global tag that will be appended to all metrics
269    pub fn with_global_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
270        self.config.global_tags.push((key.into(), value.into()));
271        self
272    }
273
274    pub fn dd_logs_enabled(mut self, enabled: bool) -> Self {
275        self.config.dd_logs_enabled = enabled;
276        self
277    }
278
279    pub fn json_logging(mut self, enabled: bool) -> Self {
280        self.config.json_logging = enabled;
281        self
282    }
283
284    pub fn build(self) -> TelemetryConfig {
285        self.config
286    }
287}
288
289/// Telemetry components that need to be kept alive
290pub struct Telemetry {
291    /// Datadog tracer provider (must be kept alive and shutdown on exit)
292    pub tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
293    /// Datadog logger provider (must be kept alive and shutdown on exit)
294    pub logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
295}
296
297/// Initialize telemetry with the given configuration.
298///
299/// This will:
300/// 1. Initialize Datadog tracing (if enabled)
301/// 2. Initialize StatsD metrics (if enabled)
302/// 3. Set up console logging
303///
304/// Returns a `Telemetry` struct that must be kept alive for the duration
305/// of the application and properly shutdown on exit.
306pub async fn init_telemetry(config: TelemetryConfig) -> Telemetry {
307    eprintln!("🦀 Sideways Telemetry: Initializing...");
308
309    // Initialize Datadog tracing
310    let (tracer_provider, logger_provider) = if config.datadog_enabled {
311        match tracing::init_datadog(&config) {
312            Ok((tp, lp)) => {
313                eprintln!("✅ Sideways Telemetry: Datadog tracing initialized");
314                if lp.is_some() {
315                    eprintln!("✅ Sideways Telemetry: Datadog log ingestion initialized");
316                }
317                (Some(tp), lp)
318            }
319            Err(err) => {
320                eprintln!("⚠️  Sideways Telemetry: Datadog tracing unavailable: {}", err);
321                (None, None)
322            }
323        }
324    } else {
325        eprintln!("📊 Sideways Telemetry: Datadog tracing disabled");
326        tracing::init_console_logging(&config);
327        (None, None)
328    };
329
330    // Initialize metrics
331    if config.metrics_enabled {
332        if let Err(err) = metrics::init_metrics(&config) {
333            eprintln!("⚠️  Sideways Telemetry: Metrics unavailable: {}", err);
334        }
335    } else {
336        eprintln!("📊 Sideways Telemetry: Metrics disabled");
337    }
338
339    Telemetry {
340        tracer_provider,
341        logger_provider,
342    }
343}