feat(config): add obs and webhook sections to central lib/config crate, refresh app and lib
zhenyi committed
Modified lib/config/src/app.rs lib/config/src/app.rs +6 −0 View lib/config/src/app.rs
@@ -9,6 +9,7 @@
99use crate::issues;
1010use crate::jwt;
1111use crate::live;
12use crate::obs;
1213use crate::payment;
1314use crate::pow;
1415use crate::project;
@@ -19,6 +20,7 @@
1920use crate::space;
2021use crate::storage;
2122use crate::user;
23use crate::webhook;
2224
2325#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2426#[serde(default)]
@@ -47,6 +49,8 @@
4749 pub git_ssh: git::GitSshConfig,
4850 pub git_auth: git::GitAuthConfig,
4951 pub git_sync: git::SyncSettings,
52 pub webhook: webhook::WebhookConfig,
53 pub obs: obs::ObsSettings,
5054}
5155
5256impl AppConfig {
@@ -54,6 +58,8 @@
5458 self.email.is_valid()?;
5559 self.queue.is_valid()?;
5660 self.search.is_valid()?;
61 self.webhook.is_valid()?;
62 self.obs.is_valid()?;
5763 if self.chat.topic_prefix != self.gateway.topic_prefix {
5864 return Err(format!(
5965 "chat.topic_prefix ({:?}) must equal gateway.topic_prefix ({:?})",
Modified lib/config/src/lib.rs lib/config/src/lib.rs +2 −0 View lib/config/src/lib.rs
@@ -11,6 +11,7 @@
1111pub mod jwt;
1212pub mod live;
1313pub mod loader;
14pub mod obs;
1415pub mod payment;
1516pub mod pow;
1617pub mod project;
@@ -21,6 +22,7 @@
2122pub mod space;
2223pub mod storage;
2324pub mod user;
25pub mod webhook;
2426
2527pub use app::AppConfig;
2628pub use error::{ConfigError, ConfigResult};
Added lib/config/src/obs.rs lib/config/src/obs.rs +120 −0 View lib/config/src/obs.rs
@@ -0,0 +1,120 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use crate::duration::deserialize_std_duration;
6
7/// OpenObserve (OTLP) observability settings.
8///
9/// When `enabled`, the `obs` crate exports traces, logs and metrics over
10/// OTLP/HTTP to the configured endpoint. Each signal is exported to
11/// `{endpoint}/{traces|logs|metrics}` (OpenObserve's OTLP HTTP layout).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(default)]
14pub struct ObsSettings {
15 /// Master switch; when false the apps keep console-only logging.
16 pub enabled: bool,
17 /// Resource attribute `service.name`. Empty means the app supplies its own
18 /// fallback (each binary knows its own name).
19 pub service_name: String,
20 /// Resource attribute `service.version`.
21 pub service_version: String,
22 /// OpenObserve `stream-name` header for logs/traces. Empty falls back to
23 /// `service_name`; when both are empty OpenObserve routes to `default`.
24 pub stream_name: String,
25 /// OTLP/HTTP base endpoint. OpenObserve exposes it at
26 /// `http://host:5080/api/{org}/v1` (each signal is appended by the exporter).
27 pub endpoint: String,
28 /// Bearer token (OpenObserve stream key). Mutually exclusive with user/password.
29 pub token: Option<String>,
30 /// Basic-auth user (OpenObserve organization user, usually an email).
31 pub user: Option<String>,
32 /// Basic-auth password (OpenObserve API key).
33 pub password: Option<String>,
34 pub enable_logs: bool,
35 pub enable_metrics: bool,
36 pub enable_traces: bool,
37 /// Trace sampling ratio in `[0.0, 1.0]`; applied with a parent-based sampler.
38 pub traces_sample_ratio: f64,
39 pub batch_size: usize,
40 #[serde(default, deserialize_with = "deserialize_std_duration")]
41 pub batch_interval: Duration,
42 #[serde(default, deserialize_with = "deserialize_std_duration")]
43 pub timeout: Duration,
44 /// Keep the local console `fmt` layer even while exporting remotely.
45 pub console_log: bool,
46 /// EnvFilter directive for the console layer (e.g. `info,webapi=debug`).
47 /// Empty means `info`; each app may refine this with its own defaults.
48 pub log_level: String,
49}
50
51impl Default for ObsSettings {
52 fn default() -> Self {
53 Self {
54 enabled: false,
55 service_name: String::new(),
56 service_version: String::new(),
57 stream_name: String::new(),
58 endpoint: "http://127.0.0.1:5080/api/default/v1".to_string(),
59 token: None,
60 user: None,
61 password: None,
62 enable_logs: true,
63 enable_metrics: true,
64 enable_traces: true,
65 traces_sample_ratio: 1.0,
66 batch_size: 512,
67 batch_interval: Duration::from_secs(5),
68 timeout: Duration::from_secs(10),
69 console_log: true,
70 log_level: String::new(),
71 }
72 }
73}
74
75impl ObsSettings {
76 /// Fill in the per-app defaults (service name and log filter) when unset.
77 pub fn with_app_defaults(
78 mut self,
79 service_name: impl Into<String>,
80 log_level: impl Into<String>,
81 ) -> Self {
82 if self.service_name.is_empty() {
83 self.service_name = service_name.into();
84 }
85 if self.log_level.is_empty() {
86 self.log_level = log_level.into();
87 }
88 self
89 }
90
91 pub fn is_valid(&self) -> Result<(), String> {
92 if !self.enabled {
93 return Ok(());
94 }
95 if self.endpoint.is_empty() {
96 return Err("obs.endpoint must not be empty when obs.enabled".into());
97 }
98 if !(0.0..=1.0).contains(&self.traces_sample_ratio) {
99 return Err(format!(
100 "obs.traces_sample_ratio ({}) must be in [0.0, 1.0]",
101 self.traces_sample_ratio
102 ));
103 }
104 if self.token.is_some() && (self.user.is_some() || self.password.is_some()) {
105 return Err(
106 "obs: choose either token (Bearer) or user/password (Basic), not both".into(),
107 );
108 }
109 if self.user.is_some() != self.password.is_some() {
110 return Err("obs: user and password must be set together for Basic auth".into());
111 }
112 if self.batch_size == 0 {
113 return Err("obs.batch_size must be >= 1".into());
114 }
115 if self.batch_interval.is_zero() || self.timeout.is_zero() {
116 return Err("obs.batch_interval and obs.timeout must be > 0".into());
117 }
118 Ok(())
119 }
120}
Added lib/config/src/webhook.rs lib/config/src/webhook.rs +72 −0 View lib/config/src/webhook.rs
@@ -0,0 +1,72 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use crate::duration::{deserialize_opt_std_duration, deserialize_std_duration};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9pub struct WebhookConfig {
10 pub max_attempts: u32,
11 #[serde(default, deserialize_with = "deserialize_std_duration")]
12 pub base_delay: Duration,
13 #[serde(default, deserialize_with = "deserialize_std_duration")]
14 pub max_delay: Duration,
15 #[serde(default, deserialize_with = "deserialize_std_duration")]
16 pub request_timeout: Duration,
17 #[serde(default, deserialize_with = "deserialize_opt_std_duration")]
18 pub connect_timeout: Option<Duration>,
19 pub pool_max_idle_per_host: usize,
20 #[serde(default, deserialize_with = "deserialize_opt_std_duration")]
21 pub pool_idle_timeout: Option<Duration>,
22 pub follow_redirects: bool,
23 pub max_concurrent_dispatches: usize,
24}
25
26impl Default for WebhookConfig {
27 fn default() -> Self {
28 Self {
29 max_attempts: 5,
30 base_delay: Duration::from_millis(500),
31 max_delay: Duration::from_secs(30),
32 request_timeout: Duration::from_secs(10),
33 connect_timeout: Some(Duration::from_secs(2)),
34 pool_max_idle_per_host: 8,
35 pool_idle_timeout: Some(Duration::from_secs(90)),
36 follow_redirects: true,
37 max_concurrent_dispatches: 32,
38 }
39 }
40}
41
42impl WebhookConfig {
43 pub fn is_valid(&self) -> Result<(), String> {
44 if self.max_attempts == 0 {
45 return Err("webhook.max_attempts must be >= 1".into());
46 }
47 if self.base_delay.is_zero() {
48 return Err("webhook.base_delay must be > 0".into());
49 }
50 if self.max_delay < self.base_delay {
51 return Err(format!(
52 "webhook.max_delay ({:?}) must be >= webhook.base_delay ({:?})",
53 self.max_delay, self.base_delay
54 ));
55 }
56 if self.request_timeout.is_zero() {
57 return Err("webhook.request_timeout must be > 0".into());
58 }
59 if let Some(t) = self.connect_timeout
60 && t.is_zero()
61 {
62 return Err("webhook.connect_timeout, if set, must be > 0".into());
63 }
64 if self.pool_max_idle_per_host == 0 {
65 return Err("webhook.pool_max_idle_per_host must be >= 1".into());
66 }
67 if self.max_concurrent_dispatches == 0 {
68 return Err("webhook.max_concurrent_dispatches must be >= 1".into());
69 }
70 Ok(())
71 }
72}
No newline at end of file