use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
pub const SCHEMA_VERSION: u32 = 1;
#[inline]
fn now_nanos() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
fn format_events<T, F: Fn(&T) -> String>(events: &[T], formatter: F) -> String {
events.iter().map(formatter).collect::<Vec<_>>().join("\n")
}
pub const DEFAULT_BATCH_SIZE: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SinkType {
InfluxDb,
JsonLines,
Kafka,
Console,
}
impl SinkType {
pub fn name(&self) -> &'static str {
match self {
Self::InfluxDb => "influxdb",
Self::JsonLines => "jsonlines",
Self::Kafka => "kafka",
Self::Console => "console",
}
}
}
#[derive(Debug, Clone)]
pub struct MetricEvent {
pub measurement: String,
pub tags: HashMap<String, String>,
pub fields: HashMap<String, f64>,
pub timestamp_ns: u64,
pub correlation_id: Option<String>,
pub schema_version: u32,
}
impl MetricEvent {
pub fn new(measurement: &str) -> Self {
Self {
measurement: measurement.to_string(),
tags: HashMap::new(),
fields: HashMap::new(),
timestamp_ns: now_nanos(),
correlation_id: None,
schema_version: SCHEMA_VERSION,
}
}
pub fn with_tag(mut self, key: &str, value: &str) -> Self {
self.tags.insert(key.to_string(), value.to_string());
self
}
pub fn with_field(mut self, key: &str, value: f64) -> Self {
self.fields.insert(key.to_string(), value);
self
}
pub fn with_correlation_id(mut self, id: &str) -> Self {
self.correlation_id = Some(id.to_string());
self
}
pub fn with_timestamp(mut self, timestamp_ns: u64) -> Self {
self.timestamp_ns = timestamp_ns;
self
}
pub fn to_influx_line(&self) -> String {
let mut line = self.measurement.clone();
let mut tag_pairs: Vec<_> = self.tags.iter().collect();
tag_pairs.sort_by_key(|(k, _)| *k);
for (key, value) in tag_pairs {
line.push_str(&format!(",{}={}", escape_influx(key), escape_influx(value)));
}
line.push(' ');
let mut field_pairs: Vec<_> = self.fields.iter().collect();
field_pairs.sort_by_key(|(k, _)| *k);
let field_str: Vec<String> = field_pairs
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
line.push_str(&field_str.join(","));
line.push_str(&format!(" {}", self.timestamp_ns));
line
}
pub fn to_json(&self) -> String {
let tags_json: Vec<String> = self
.tags
.iter()
.map(|(k, v)| format!("\"{}\":\"{}\"", k, v))
.collect();
let fields_json: Vec<String> = self
.fields
.iter()
.map(|(k, v)| format!("\"{}\":{}", k, v))
.collect();
let correlation = self
.correlation_id
.as_ref()
.map(|id| format!(",\"correlation_id\":\"{}\"", id))
.unwrap_or_default();
format!(
r#"{{"measurement":"{}","tags":{{{}}},"fields":{{{}}},"timestamp_ns":{},"schema_version":{}{}}}"#,
self.measurement,
tags_json.join(","),
fields_json.join(","),
self.timestamp_ns,
self.schema_version,
correlation
)
}
}
fn escape_influx(s: &str) -> String {
s.replace(' ', "\\ ")
.replace(',', "\\,")
.replace('=', "\\=")
}
#[derive(Debug, Clone)]
pub struct EventBatch {
pub events: Vec<MetricEvent>,
pub batch_id: u64,
pub created_ns: u64,
}
impl EventBatch {
pub fn new(batch_id: u64) -> Self {
Self {
events: Vec::new(),
batch_id,
created_ns: now_nanos(),
}
}
pub fn add(&mut self, event: MetricEvent) {
self.events.push(event);
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn to_influx_batch(&self) -> String {
format_events(&self.events, MetricEvent::to_influx_line)
}
pub fn to_json_lines(&self) -> String {
format_events(&self.events, MetricEvent::to_json)
}
}
#[derive(Debug, Clone)]
pub struct SinkHealth {
pub connected: bool,
pub last_write_ns: Option<u64>,
pub events_written: u64,
pub write_errors: u64,
}
impl Default for SinkHealth {
fn default() -> Self {
Self {
connected: true,
last_write_ns: None,
events_written: 0,
write_errors: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
pub multiplier: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 100,
max_delay_ms: 10000,
multiplier: 2.0,
}
}
}
impl RetryConfig {
pub fn delay_for_attempt(&self, attempt: u32) -> u64 {
let delay = self.initial_delay_ms as f64 * self.multiplier.powi(attempt as i32);
(delay as u64).min(self.max_delay_ms)
}
}
#[derive(Debug)]
pub struct EventStreamer {
sink_type: SinkType,
batch_size: usize,
current_batch: EventBatch,
batch_counter: u64,
retry_config: RetryConfig,
health: SinkHealth,
compression: bool,
correlation_counter: u64,
output_buffer: Vec<String>,
}
impl Default for EventStreamer {
fn default() -> Self {
Self::new(SinkType::Console)
}
}
impl EventStreamer {
pub fn new(sink_type: SinkType) -> Self {
Self {
sink_type,
batch_size: DEFAULT_BATCH_SIZE,
current_batch: EventBatch::new(0),
batch_counter: 0,
retry_config: RetryConfig::default(),
health: SinkHealth::default(),
compression: false,
correlation_counter: 0,
output_buffer: Vec::new(),
}
}
pub fn with_batch_size(mut self, size: usize) -> Self {
self.batch_size = size.max(1);
self
}
pub fn with_compression(mut self, enabled: bool) -> Self {
self.compression = enabled;
self
}
pub fn with_retry(mut self, config: RetryConfig) -> Self {
self.retry_config = config;
self
}
pub fn generate_correlation_id(&mut self) -> String {
self.correlation_counter += 1;
format!("cbtop-{}-{}", std::process::id(), self.correlation_counter)
}
pub fn send(&mut self, event: MetricEvent) -> bool {
self.current_batch.add(event);
if self.current_batch.len() >= self.batch_size {
self.flush()
} else {
true
}
}
pub fn flush(&mut self) -> bool {
if self.current_batch.is_empty() {
return true;
}
let result = self.write_batch(&self.current_batch.clone());
if result {
self.health.events_written += self.current_batch.len() as u64;
self.health.last_write_ns = Some(now_nanos());
self.batch_counter += 1;
self.current_batch = EventBatch::new(self.batch_counter);
true
} else {
self.health.write_errors += 1;
false
}
}
fn write_batch(&mut self, batch: &EventBatch) -> bool {
match self.sink_type {
SinkType::Console => {
for event in &batch.events {
println!("{}", event.to_json());
}
true
}
SinkType::InfluxDb => {
self.output_buffer.push(batch.to_influx_batch());
true
}
SinkType::JsonLines => {
self.output_buffer.push(batch.to_json_lines());
true
}
SinkType::Kafka => {
self.output_buffer.push(batch.to_json_lines());
true
}
}
}
pub fn health(&self) -> &SinkHealth {
&self.health
}
pub fn is_healthy(&self) -> bool {
self.health.connected && self.health.write_errors == 0
}
pub fn output_buffer(&self) -> &[String] {
&self.output_buffer
}
pub fn clear_buffer(&mut self) {
self.output_buffer.clear();
}
pub fn events_written(&self) -> u64 {
self.health.events_written
}
pub fn pending_count(&self) -> usize {
self.current_batch.len()
}
pub fn shutdown(&mut self) -> bool {
self.flush()
}
}
pub fn compress_data(data: &[u8]) -> Vec<u8> {
data.to_vec()
}
pub fn event_from_sample(metric: &str, value: f64, tags: &[(&str, &str)]) -> MetricEvent {
let mut event = MetricEvent::new(metric).with_field("value", value);
for (key, val) in tags {
event = event.with_tag(key, val);
}
event
}
#[cfg(test)]
mod tests;