use std::time::Duration;
use bytes::Bytes;
use serde_json::{Map, Value};
use proxima_primitives::pipe::ProximaError;
use proxima_primitives::pipe::SendPipe;
use proxima_primitives::pipe::handler::into_handle;
use proxima_primitives::pipe::request::Response;
use crate::Client;
use crate::telemetry::config::{ExporterChoice, TelemetryConfig};
use crate::telemetry::pipes::{
NullPipe, OtlpHttpCodec, TelemetryPipeHandle, TelemetryRequest, into_telemetry_handle,
};
use crate::telemetry::recorder::{HasPipe, Recorder, RecorderBuilder};
#[derive(Debug, thiserror::Error)]
pub enum OtlpError {
#[error("otlp client requires an endpoint")]
MissingEndpoint,
#[error("composing the otlp transport via proxima::Client")]
Client(#[from] ProximaError),
}
pub struct OtlpClientBuilder {
endpoint: Option<String>,
headers: Vec<(String, String)>,
max_attempts: Option<u32>,
timeout: Option<Duration>,
}
impl OtlpClientBuilder {
#[must_use]
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn headers<K, V>(mut self, headers: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<String>,
{
for (name, value) in headers {
self.headers.push((name.into(), value.into()));
}
self
}
#[must_use]
pub fn retry(mut self, max_attempts: u32) -> Self {
self.max_attempts = Some(max_attempts);
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub async fn build(self) -> Result<OtlpClient, OtlpError> {
let endpoint = self.endpoint.ok_or(OtlpError::MissingEndpoint)?;
let mut spec = Map::new();
spec.insert("http".into(), Value::String(endpoint));
if !self.headers.is_empty() {
let mut request_headers = Map::new();
for (name, value) in self.headers {
request_headers.insert(name, Value::String(value));
}
let mut headers = Map::new();
headers.insert("request".into(), Value::Object(request_headers));
spec.insert("headers".into(), Value::Object(headers));
}
if let Some(timeout) = self.timeout {
spec.insert(
"timeout".into(),
Value::String(format!("{}ms", timeout.as_millis())),
);
}
if let Some(max_attempts) = self.max_attempts {
let mut retry = Map::new();
retry.insert("max_attempts".into(), Value::from(max_attempts));
spec.insert("retry".into(), Value::Object(retry));
}
let client = Client::from_value(Value::Object(spec))?;
Ok(OtlpClient {
pipe: into_telemetry_handle(OtlpHttpCodec::new(into_handle(client))),
})
}
}
pub struct OtlpClient {
pipe: TelemetryPipeHandle,
}
impl OtlpClient {
#[must_use]
pub fn http() -> OtlpClientBuilder {
OtlpClientBuilder {
endpoint: None,
headers: Vec::new(),
max_attempts: None,
timeout: None,
}
}
#[must_use]
pub fn handle(&self) -> TelemetryPipeHandle {
self.pipe.clone()
}
}
impl SendPipe for OtlpClient {
type In = TelemetryRequest;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: TelemetryRequest,
) -> impl core::future::Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
self.pipe.call(request)
}
}
pub async fn exporter_pipe(choice: &ExporterChoice) -> Result<TelemetryPipeHandle, OtlpError> {
match choice {
ExporterChoice::Noop => Ok(into_telemetry_handle(NullPipe::new())),
ExporterChoice::OtlpHttp { endpoint } => Ok(OtlpClient::http()
.endpoint(endpoint)
.build()
.await?
.handle()),
#[cfg(feature = "otlp-grpc")]
ExporterChoice::OtlpGrpc { .. } => Err(OtlpError::Client(ProximaError::Config(
"otlp/grpc transport is not yet a registered client factory".into(),
))),
}
}
pub async fn recorder_from_config(
cfg: &TelemetryConfig,
) -> Result<RecorderBuilder<HasPipe>, OtlpError> {
let pipe = exporter_pipe(&cfg.exporter).await?;
Ok(Recorder::from_config_with_pipe(cfg, pipe))
}
#[cfg(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-reactor",
feature = "runtime-prime-inbox-alloc"
))]
pub use prime_pump::{PrimePump, spawn_prime_pump};
#[cfg(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-reactor",
feature = "runtime-prime-inbox-alloc"
))]
mod prime_pump {
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use futures::channel::oneshot;
use crate::runtime::prime::os::core_shard::spawn_on_current_core;
use crate::telemetry::recorder::Recorder;
pub struct PrimePump {
stop: Arc<AtomicBool>,
done: oneshot::Receiver<()>,
recorder: Arc<Recorder>,
}
#[must_use]
pub fn spawn_prime_pump(recorder: Arc<Recorder>, flush_interval: Duration) -> PrimePump {
recorder.mark_pump_active(true);
let stop = Arc::new(AtomicBool::new(false));
let (done, done_rx) = oneshot::channel();
let task_stop = Arc::clone(&stop);
let stop_recorder = Arc::clone(&recorder);
spawn_on_current_core(Box::pin(async move {
while !task_stop.load(Ordering::Acquire) {
#[cfg(feature = "lossless-backpressure")]
{
let _ = proxima_core::time::timeout(flush_interval, recorder.pump_wait()).await;
}
#[cfg(not(feature = "lossless-backpressure"))]
{
proxima_core::time::sleep(flush_interval).await;
}
recorder.drain_async().await;
recorder.drain_instruments_async().await;
}
while recorder.drain_async().await + recorder.drain_instruments_async().await > 0 {}
recorder.mark_pump_active(false);
let _ = done.send(());
}));
PrimePump {
stop,
done: done_rx,
recorder: stop_recorder,
}
}
impl PrimePump {
pub async fn stop(self) {
self.stop.store(true, Ordering::Release);
self.recorder.signal_pump();
let _ = self.done.await;
}
}
}