use crate::backoff::{CircuitBreaker, RetryPolicy};
use crate::{OtlpError, OtlpResult};
use rlg::log::Log;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tonic::transport::{Channel, Endpoint};
#[derive(Debug, Clone)]
pub struct GrpcOtlpExporter {
endpoint: String,
channel: Channel,
#[allow(dead_code)]
metadata: HashMap<String, String>,
#[allow(dead_code)]
retry: RetryPolicy,
#[allow(dead_code)]
circuit: Option<Arc<CircuitBreaker>>,
}
impl GrpcOtlpExporter {
#[must_use]
pub fn builder() -> GrpcOtlpExporterBuilder {
GrpcOtlpExporterBuilder::default()
}
pub async fn export_one(&self, _record: &Log) -> OtlpResult<()> {
Err(OtlpError::GrpcNotImplemented)
}
pub async fn export_batch(
&self,
_records: &[Log],
) -> OtlpResult<()> {
Err(OtlpError::GrpcNotImplemented)
}
#[must_use]
pub fn endpoint(&self) -> &str {
&self.endpoint
}
#[must_use]
pub fn channel(&self) -> &Channel {
&self.channel
}
}
#[derive(Debug, Default, Clone)]
pub struct GrpcOtlpExporterBuilder {
endpoint: Option<String>,
metadata: HashMap<String, String>,
timeout: Option<Duration>,
max_retries: Option<u32>,
backoff_base: Option<Duration>,
circuit: Option<Arc<CircuitBreaker>>,
}
impl GrpcOtlpExporterBuilder {
#[must_use]
pub fn endpoint(mut self, url: impl Into<String>) -> Self {
self.endpoint = Some(url.into());
self
}
#[must_use]
pub fn metadata(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.metadata.insert(name.into(), value.into());
self
}
#[must_use]
pub fn timeout_secs(mut self, secs: u64) -> Self {
self.timeout = Some(Duration::from_secs(secs));
self
}
#[must_use]
pub const fn max_retries(mut self, n: u32) -> Self {
self.max_retries = Some(n);
self
}
#[must_use]
pub const fn backoff_base(mut self, base: Duration) -> Self {
self.backoff_base = Some(base);
self
}
#[must_use]
pub fn circuit(mut self, cb: Arc<CircuitBreaker>) -> Self {
self.circuit = Some(cb);
self
}
pub fn build(self) -> OtlpResult<GrpcOtlpExporter> {
let endpoint_str = self
.endpoint
.expect("GrpcOtlpExporterBuilder::endpoint is required");
let timeout =
self.timeout.unwrap_or_else(|| Duration::from_secs(10));
let endpoint = Endpoint::from_shared(endpoint_str.clone())
.map_err(|e| {
OtlpError::GrpcEndpoint(format!(
"invalid gRPC endpoint {endpoint_str:?}: {e}"
))
})?
.timeout(timeout);
let channel = endpoint.connect_lazy();
let retry = RetryPolicy {
max_retries: self.max_retries.unwrap_or(3),
base: self
.backoff_base
.unwrap_or_else(|| Duration::from_millis(200)),
max_delay: Duration::from_secs(30),
jitter: 1.0,
};
Ok(GrpcOtlpExporter {
endpoint: endpoint_str,
channel,
metadata: self.metadata,
retry,
circuit: self.circuit,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn builder_defaults() {
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.build()
.expect("valid endpoint must build");
assert_eq!(e.endpoint(), "http://localhost:4317");
assert_eq!(e.retry.max_retries, 3);
}
#[tokio::test]
async fn builder_rejects_bogus_endpoint() {
let res = GrpcOtlpExporter::builder()
.endpoint("this is not a url at all")
.build();
assert!(matches!(res, Err(OtlpError::GrpcEndpoint(_))));
}
#[tokio::test]
async fn builder_accepts_metadata_and_timeout() {
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.metadata("x-tenant", "acme")
.timeout_secs(5)
.build()
.unwrap();
assert_eq!(e.metadata.get("x-tenant").unwrap(), "acme");
}
#[tokio::test]
async fn export_returns_not_implemented() {
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.build()
.unwrap();
let log = rlg::log::Log::info("msg");
let res = e.export_one(&log).await;
assert!(matches!(res, Err(OtlpError::GrpcNotImplemented)));
let res = e.export_batch(&[log]).await;
assert!(matches!(res, Err(OtlpError::GrpcNotImplemented)));
}
#[tokio::test]
async fn channel_accessor_returns_live_reference() {
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.build()
.unwrap();
let _ch: &Channel = e.channel();
}
#[tokio::test]
async fn builder_retry_and_backoff() {
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.max_retries(5)
.backoff_base(Duration::from_millis(50))
.build()
.unwrap();
assert_eq!(e.retry.max_retries, 5);
assert_eq!(e.retry.base, Duration::from_millis(50));
}
#[tokio::test]
async fn builder_with_circuit_breaker() {
let cb =
Arc::new(CircuitBreaker::new(3, Duration::from_secs(60)));
let e = GrpcOtlpExporter::builder()
.endpoint("http://localhost:4317")
.circuit(cb)
.build()
.unwrap();
assert!(e.endpoint().starts_with("http://"));
}
}