use std::sync::Arc;
use std::time::Duration;
use crate::event::Event;
use crate::level::Level;
use crate::transport::Transport;
pub type BeforeSend = Arc<dyn Fn(Event) -> Option<Event> + Send + Sync>;
#[derive(Clone)]
pub struct Config {
pub api_key: Option<String>,
pub environment: String,
pub release: Option<String>,
pub host: String,
pub min_level: Level,
pub timeout: Duration,
pub enabled: bool,
pub batch_size: usize,
pub flush_interval: Duration,
pub max_queue_size: usize,
pub shutdown_timeout: Duration,
pub max_breadcrumbs: usize,
pub attach_stacktrace: bool,
pub panic_hook: bool,
pub in_app_include: Vec<String>,
pub in_app_exclude: Vec<String>,
pub debug: bool,
pub before_send: Option<BeforeSend>,
pub before_send_fail_open: bool,
pub allow_insecure_transport: bool,
pub transport: Option<Arc<dyn Transport>>,
}
impl Default for Config {
fn default() -> Self {
Config {
api_key: None,
environment: "production".to_string(),
release: None,
host: "https://errsight.com".to_string(),
min_level: Level::Warning,
timeout: Duration::from_secs(5),
enabled: true,
batch_size: 10,
flush_interval: Duration::from_secs(2),
max_queue_size: 1_000,
shutdown_timeout: Duration::from_secs(5),
max_breadcrumbs: 100,
attach_stacktrace: false,
panic_hook: true,
in_app_include: Vec::new(),
in_app_exclude: Vec::new(),
debug: false,
before_send: None,
before_send_fail_open: false,
allow_insecure_transport: false,
transport: None,
}
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder {
config: Config::from_env(),
}
}
pub fn from_env() -> Self {
let mut c = Config::default();
if let Ok(k) = std::env::var("ERRSIGHT_API_KEY") {
if !k.trim().is_empty() {
c.api_key = Some(k);
}
}
if let Ok(env) = std::env::var("ERRSIGHT_ENV") {
if !env.trim().is_empty() {
c.environment = env;
}
}
if let Ok(host) = std::env::var("ERRSIGHT_HOST") {
if !host.trim().is_empty() {
c.host = host;
}
}
if let Ok(rel) = std::env::var("ERRSIGHT_RELEASE") {
if !rel.trim().is_empty() {
c.release = Some(rel);
}
}
if let Ok(dbg) = std::env::var("ERRSIGHT_DEBUG") {
c.debug = matches!(
dbg.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
);
}
c
}
pub fn enabled(&self) -> bool {
self.enabled
&& self
.api_key
.as_deref()
.map(|k| !k.trim().is_empty())
.unwrap_or(false)
}
pub fn events_endpoint(&self) -> String {
format!("{}/api/v1/events", self.host.trim_end_matches('/'))
}
pub fn is_insecure_remote_host(&self) -> bool {
let host = self.host.trim();
let Some(rest) = host
.strip_prefix("http://")
.or_else(|| host.strip_prefix("HTTP://"))
else {
return false; };
let authority = rest.split(['/', '?', '#']).next().unwrap_or(rest);
let hostname = authority.rsplit_once(':').map_or(authority, |(h, _)| h);
let is_loopback = matches!(hostname, "localhost" | "127.0.0.1" | "[::1]" | "::1");
!is_loopback
}
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
.field("environment", &self.environment)
.field("release", &self.release)
.field("host", &self.host)
.field("min_level", &self.min_level)
.field("timeout", &self.timeout)
.field("enabled", &self.enabled)
.field("batch_size", &self.batch_size)
.field("flush_interval", &self.flush_interval)
.field("max_queue_size", &self.max_queue_size)
.field("shutdown_timeout", &self.shutdown_timeout)
.field("max_breadcrumbs", &self.max_breadcrumbs)
.field("attach_stacktrace", &self.attach_stacktrace)
.field("panic_hook", &self.panic_hook)
.field("in_app_include", &self.in_app_include)
.field("in_app_exclude", &self.in_app_exclude)
.field("debug", &self.debug)
.field("before_send_fail_open", &self.before_send_fail_open)
.field("allow_insecure_transport", &self.allow_insecure_transport)
.field("before_send", &self.before_send.as_ref().map(|_| "<fn>"))
.field("transport", &self.transport.as_ref().map(|_| "<custom>"))
.finish()
}
}
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn from_default() -> Self {
ConfigBuilder {
config: Config::default(),
}
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.config.api_key = Some(key.into());
self
}
pub fn environment(mut self, env: impl Into<String>) -> Self {
self.config.environment = env.into();
self
}
pub fn release(mut self, release: impl Into<String>) -> Self {
self.config.release = Some(release.into());
self
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.config.host = host.into();
self
}
pub fn min_level(mut self, level: Level) -> Self {
self.config.min_level = level;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn batch_size(mut self, n: usize) -> Self {
self.config.batch_size = n.clamp(1, 100);
self
}
pub fn flush_interval(mut self, interval: Duration) -> Self {
self.config.flush_interval = interval;
self
}
pub fn max_queue_size(mut self, n: usize) -> Self {
self.config.max_queue_size = n.max(1);
self
}
pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
self.config.shutdown_timeout = timeout;
self
}
pub fn max_breadcrumbs(mut self, n: usize) -> Self {
self.config.max_breadcrumbs = n;
self
}
pub fn attach_stacktrace(mut self, yes: bool) -> Self {
self.config.attach_stacktrace = yes;
self
}
pub fn panic_hook(mut self, yes: bool) -> Self {
self.config.panic_hook = yes;
self
}
pub fn in_app_include(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.config
.in_app_include
.extend(paths.into_iter().map(Into::into));
self
}
pub fn in_app_exclude(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.config
.in_app_exclude
.extend(paths.into_iter().map(Into::into));
self
}
pub fn debug(mut self, yes: bool) -> Self {
self.config.debug = yes;
self
}
pub fn before_send<F>(mut self, f: F) -> Self
where
F: Fn(Event) -> Option<Event> + Send + Sync + 'static,
{
self.config.before_send = Some(Arc::new(f));
self
}
pub fn before_send_fail_open(mut self, yes: bool) -> Self {
self.config.before_send_fail_open = yes;
self
}
pub fn allow_insecure_transport(mut self, yes: bool) -> Self {
self.config.allow_insecure_transport = yes;
self
}
pub fn transport(mut self, transport: Arc<dyn Transport>) -> Self {
self.config.transport = Some(transport);
self
}
pub fn build(self) -> Config {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_without_key() {
let c = ConfigBuilder::from_default().build();
assert!(!c.enabled());
let c = ConfigBuilder::from_default().api_key("elp_x").build();
assert!(c.enabled());
let c = ConfigBuilder::from_default().api_key(" ").build();
assert!(!c.enabled(), "blank key must not enable");
}
#[test]
fn endpoint_trims_trailing_slash() {
let c = ConfigBuilder::from_default()
.host("https://example.com/")
.build();
assert_eq!(c.events_endpoint(), "https://example.com/api/v1/events");
}
#[test]
fn debug_redacts_key() {
let c = ConfigBuilder::from_default().api_key("elp_secret").build();
let s = format!("{c:?}");
assert!(!s.contains("elp_secret"));
assert!(s.contains("redacted"));
}
#[test]
fn batch_size_clamped_to_backend_limit() {
assert_eq!(
ConfigBuilder::from_default()
.batch_size(500)
.build()
.batch_size,
100
);
assert_eq!(
ConfigBuilder::from_default()
.batch_size(0)
.build()
.batch_size,
1
);
assert_eq!(
ConfigBuilder::from_default()
.batch_size(25)
.build()
.batch_size,
25
);
}
#[test]
fn insecure_host_detection() {
let insecure = |h: &str| {
ConfigBuilder::from_default()
.host(h)
.build()
.is_insecure_remote_host()
};
assert!(insecure("http://errsight.example.com"));
assert!(insecure("http://10.0.0.5:8080/ingest"));
assert!(!insecure("https://errsight.com"));
assert!(!insecure("http://localhost:3000"));
assert!(!insecure("http://127.0.0.1:3000"));
assert!(!insecure("http://[::1]:3000"));
}
}