Skip to content

Commit 96a1d67

Browse files
feat(admin): add config and upstream health admin endpoints
Admin API endpoints: - GET /admin/config - dump current running configuration (JSON) - GET /admin/upstreams - show upstream health status with targets Graceful reload improvements: - Wire SIGHUP to trigger ConfigManager::reload() - Add SignalManager for thread-to-async signal bridging - Validate configs before applying (reject invalid, keep current) - Add auto_reload config option for file watching Trace ID improvements: - Add TinyFlake format (11-char Base58, operator-friendly) - Configurable via trace-id-format in server config - Default to TinyFlake, fallback to UUID Default config now includes 5 admin routes on port 9090: - /health, /healthz, /ready (health check) - /metrics (Prometheus) - /admin/config, /config (config dump) - /admin/upstreams, /upstreams (upstream health) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 160ff02 commit 96a1d67

18 files changed

Lines changed: 2047 additions & 113 deletions

config/sentinel.kdl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,31 @@ observability {
471471
format "json" // "json" or "pretty"
472472
timestamps #true
473473

474+
// Access log - all HTTP requests with trace_id for correlation
474475
access-log {
475476
enabled #true
476477
file "/var/log/sentinel/access.log"
477-
format "combined"
478+
format "json" // "json" or "combined"
478479
buffer-size 8192
480+
include-trace-id #true
481+
}
482+
483+
// Error log - warnings and errors
484+
error-log {
485+
enabled #true
486+
file "/var/log/sentinel/error.log"
487+
level "warn" // minimum level: "warn" or "error"
488+
buffer-size 8192
489+
}
490+
491+
// Audit log - security events (blocked requests, agent decisions, WAF)
492+
audit-log {
493+
enabled #true
494+
file "/var/log/sentinel/audit.log"
495+
buffer-size 8192
496+
log-blocked #true
497+
log-agent-decisions #true
498+
log-waf-events #true
479499
}
480500
}
481501

crates/common/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub use errors::{SentinelError, SentinelResult};
2626
pub use limits::{Limits, RateLimiter};
2727

2828
// Re-export common types
29-
pub use types::{CorrelationId, RequestId};
29+
pub use types::{CorrelationId, RequestId, TraceIdFormat};
3030

3131
// Re-export circuit breaker
3232
pub use circuit_breaker::CircuitBreaker;

crates/common/src/types.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,57 @@ pub enum TlsVersion {
207207
Tls13,
208208
}
209209

210+
/// Trace ID format selection
211+
///
212+
/// Controls how trace IDs are generated for request tracing.
213+
///
214+
/// # Formats
215+
///
216+
/// - **TinyFlake** (default): 11-character Base58 encoded ID with time prefix.
217+
/// Operator-friendly format designed for easy copying and log correlation.
218+
/// Example: `k7BxR3nVp2Ym`
219+
///
220+
/// - **UUID**: Standard 36-character UUID v4 format with dashes.
221+
/// Guaranteed unique, widely compatible.
222+
/// Example: `550e8400-e29b-41d4-a716-446655440000`
223+
///
224+
/// # Configuration
225+
///
226+
/// ```kdl
227+
/// server {
228+
/// trace-id-format "tinyflake" // or "uuid"
229+
/// }
230+
/// ```
231+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
232+
#[serde(rename_all = "lowercase")]
233+
pub enum TraceIdFormat {
234+
/// TinyFlake format: 11-char Base58, time-prefixed (default)
235+
#[default]
236+
TinyFlake,
237+
238+
/// UUID v4 format: 36-char with dashes
239+
Uuid,
240+
}
241+
242+
impl TraceIdFormat {
243+
/// Parse format from string (case-insensitive)
244+
pub fn from_str_loose(s: &str) -> Self {
245+
match s.to_lowercase().as_str() {
246+
"uuid" | "uuid4" | "uuidv4" => TraceIdFormat::Uuid,
247+
_ => TraceIdFormat::TinyFlake, // Default to TinyFlake
248+
}
249+
}
250+
}
251+
252+
impl fmt::Display for TraceIdFormat {
253+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254+
match self {
255+
TraceIdFormat::TinyFlake => write!(f, "tinyflake"),
256+
TraceIdFormat::Uuid => write!(f, "uuid"),
257+
}
258+
}
259+
}
260+
210261
impl fmt::Display for TlsVersion {
211262
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212263
match self {

crates/config/src/defaults.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ routes {
7373
service-type "builtin"
7474
builtin-handler "metrics"
7575
}
76+
77+
// Config dump endpoint on admin port
78+
route "config" {
79+
priority "high"
80+
matches {
81+
path "/admin/config"
82+
path "/config"
83+
}
84+
service-type "builtin"
85+
builtin-handler "config"
86+
}
87+
88+
// Upstream health status endpoint on admin port
89+
route "upstreams" {
90+
priority "high"
91+
matches {
92+
path "/admin/upstreams"
93+
path "/upstreams"
94+
}
95+
service-type "builtin"
96+
builtin-handler "upstreams"
97+
}
7698
}
7799
78100
limits {
@@ -107,6 +129,8 @@ pub fn create_default_config() -> Config {
107129
user: None,
108130
group: None,
109131
working_directory: None,
132+
trace_id_format: Default::default(),
133+
auto_reload: false,
110134
},
111135
listeners: vec![
112136
ListenerConfig {
@@ -183,6 +207,44 @@ pub fn create_default_config() -> Config {
183207
api_schema: None,
184208
error_pages: None,
185209
},
210+
RouteConfig {
211+
id: "config".to_string(),
212+
priority: Priority::High,
213+
matches: vec![
214+
MatchCondition::Path("/admin/config".to_string()),
215+
MatchCondition::Path("/config".to_string()),
216+
],
217+
upstream: None,
218+
service_type: ServiceType::Builtin,
219+
policies: RoutePolicies::default(),
220+
filters: vec![],
221+
builtin_handler: Some(BuiltinHandler::Config),
222+
waf_enabled: false,
223+
circuit_breaker: None,
224+
retry_policy: None,
225+
static_files: None,
226+
api_schema: None,
227+
error_pages: None,
228+
},
229+
RouteConfig {
230+
id: "upstreams".to_string(),
231+
priority: Priority::High,
232+
matches: vec![
233+
MatchCondition::Path("/admin/upstreams".to_string()),
234+
MatchCondition::Path("/upstreams".to_string()),
235+
],
236+
upstream: None,
237+
service_type: ServiceType::Builtin,
238+
policies: RoutePolicies::default(),
239+
filters: vec![],
240+
builtin_handler: Some(BuiltinHandler::Upstreams),
241+
waf_enabled: false,
242+
circuit_breaker: None,
243+
retry_policy: None,
244+
static_files: None,
245+
api_schema: None,
246+
error_pages: None,
247+
},
186248
],
187249
upstreams: HashMap::new(),
188250
filters: HashMap::new(),
@@ -209,8 +271,10 @@ mod tests {
209271
fn test_create_default_config() {
210272
let config = create_default_config();
211273
assert_eq!(config.listeners.len(), 2);
212-
assert_eq!(config.routes.len(), 3);
274+
assert_eq!(config.routes.len(), 5);
213275
assert!(config.routes.iter().any(|r| r.id == "status"));
214276
assert!(config.routes.iter().any(|r| r.id == "health"));
277+
assert!(config.routes.iter().any(|r| r.id == "config"));
278+
assert!(config.routes.iter().any(|r| r.id == "upstreams"));
215279
}
216280
}

crates/config/src/kdl_parser.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::collections::HashMap;
88
use std::path::PathBuf;
99

1010
use sentinel_common::limits::Limits;
11-
use sentinel_common::types::LoadBalancingAlgorithm;
11+
use sentinel_common::types::{LoadBalancingAlgorithm, TraceIdFormat};
1212

1313
use crate::filters::*;
1414
use crate::observability::ObservabilityConfig;
@@ -173,6 +173,10 @@ pub fn parse_kdl_document(doc: kdl::KdlDocument) -> Result<Config> {
173173

174174
/// Parse server configuration block
175175
pub fn parse_server_config(node: &kdl::KdlNode) -> Result<ServerConfig> {
176+
let trace_id_format = get_string_entry(node, "trace-id-format")
177+
.map(|s| TraceIdFormat::from_str_loose(&s))
178+
.unwrap_or_default();
179+
176180
Ok(ServerConfig {
177181
worker_threads: get_int_entry(node, "worker-threads")
178182
.map(|v| v as usize)
@@ -188,6 +192,8 @@ pub fn parse_server_config(node: &kdl::KdlNode) -> Result<ServerConfig> {
188192
user: get_string_entry(node, "user"),
189193
group: get_string_entry(node, "group"),
190194
working_directory: get_string_entry(node, "working-directory").map(PathBuf::from),
195+
trace_id_format,
196+
auto_reload: get_bool_entry(node, "auto-reload").unwrap_or(false),
191197
})
192198
}
193199

@@ -291,6 +297,8 @@ pub fn parse_routes(node: &kdl::KdlNode) -> Result<Vec<RouteConfig>> {
291297
"health" => Some(BuiltinHandler::Health),
292298
"metrics" => Some(BuiltinHandler::Metrics),
293299
"not-found" | "not_found" => Some(BuiltinHandler::NotFound),
300+
"config" => Some(BuiltinHandler::Config),
301+
"upstreams" => Some(BuiltinHandler::Upstreams),
294302
_ => None,
295303
}
296304
});

crates/config/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ pub use multi_file::{ConfigDirectory, MultiFileLoader};
6363

6464
// Observability
6565
pub use observability::{
66-
AccessLogConfig, LoggingConfig, MetricsConfig, ObservabilityConfig, TracingBackend,
67-
TracingConfig,
66+
AccessLogConfig, AuditLogConfig, ErrorLogConfig, LoggingConfig, MetricsConfig,
67+
ObservabilityConfig, TracingBackend, TracingConfig,
6868
};
6969

7070
// Routes
@@ -77,6 +77,9 @@ pub use routes::{
7777
// Server
7878
pub use server::{ListenerConfig, ListenerProtocol, ServerConfig, TlsConfig};
7979

80+
// Re-export TraceIdFormat from common for convenience
81+
pub use sentinel_common::TraceIdFormat;
82+
8083
// Upstreams
8184
pub use upstreams::{
8285
ConnectionPoolConfig, HealthCheck, UpstreamConfig, UpstreamPeer, UpstreamTarget,
@@ -387,6 +390,8 @@ impl Config {
387390
user: None,
388391
group: None,
389392
working_directory: None,
393+
trace_id_format: Default::default(),
394+
auto_reload: false,
390395
},
391396
listeners: vec![ListenerConfig {
392397
id: "http".to_string(),

crates/config/src/multi_file.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::fs;
1111
use std::path::{Path, PathBuf};
1212
use tracing::{debug, info, warn};
1313

14+
use sentinel_common::TraceIdFormat;
15+
1416
use crate::{
1517
AgentConfig, Config, Limits, ListenerConfig, ObservabilityConfig, RouteConfig, ServerConfig,
1618
UpstreamConfig, WafConfig,
@@ -497,6 +499,10 @@ fn parse_server(node: &KdlNode) -> Result<ServerConfig> {
497499
user: get_string_entry(node, "user"),
498500
group: get_string_entry(node, "group"),
499501
working_directory: get_string_entry(node, "working-directory").map(PathBuf::from),
502+
trace_id_format: get_string_entry(node, "trace-id-format")
503+
.map(|s| TraceIdFormat::from_str_loose(&s))
504+
.unwrap_or_default(),
505+
auto_reload: get_bool_entry(node, "auto-reload").unwrap_or(false),
500506
})
501507
}
502508

0 commit comments

Comments
 (0)