first iteration of agent-style worker and sensor containers.
This commit is contained in:
79
crates/sensor/src/agent_main.rs
Normal file
79
crates/sensor/src/agent_main.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! Attune Universal Sensor Agent.
|
||||
|
||||
use anyhow::Result;
|
||||
use attune_common::agent_bootstrap::{bootstrap_runtime_env, print_detect_only_report};
|
||||
use attune_common::config::Config;
|
||||
use attune_sensor::startup::{
|
||||
apply_sensor_name_override, init_tracing, log_config_details, run_sensor_service,
|
||||
set_config_path,
|
||||
};
|
||||
use clap::Parser;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "attune-sensor-agent")]
|
||||
#[command(
|
||||
version,
|
||||
about = "Attune Universal Sensor Agent - Injected into runtime containers to auto-detect sensor runtimes"
|
||||
)]
|
||||
struct Args {
|
||||
/// Path to configuration file (optional)
|
||||
#[arg(short, long)]
|
||||
config: Option<String>,
|
||||
|
||||
/// Sensor worker name override
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Run runtime detection, print results, and exit
|
||||
#[arg(long)]
|
||||
detect_only: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
attune_common::auth::install_crypto_provider();
|
||||
init_tracing(tracing::Level::INFO);
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
info!("Starting Attune Universal Sensor Agent");
|
||||
info!(
|
||||
"Agent binary: attune-sensor-agent {}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
// Safe: no async runtime or worker threads are running yet.
|
||||
std::env::set_var("ATTUNE_SENSOR_AGENT_MODE", "true");
|
||||
std::env::set_var("ATTUNE_SENSOR_AGENT_BINARY_NAME", "attune-sensor-agent");
|
||||
std::env::set_var(
|
||||
"ATTUNE_SENSOR_AGENT_BINARY_VERSION",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
|
||||
let bootstrap = bootstrap_runtime_env("ATTUNE_SENSOR_RUNTIMES");
|
||||
|
||||
if args.detect_only {
|
||||
print_detect_only_report("ATTUNE_SENSOR_RUNTIMES", &bootstrap);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
set_config_path(args.config.as_deref());
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
runtime.block_on(async_main(args))
|
||||
}
|
||||
|
||||
async fn async_main(args: Args) -> Result<()> {
|
||||
let mut config = Config::load()?;
|
||||
config.validate()?;
|
||||
|
||||
if let Some(name) = args.name {
|
||||
apply_sensor_name_override(&mut config, name);
|
||||
}
|
||||
|
||||
log_config_details(&config);
|
||||
run_sensor_service(config, "Attune Sensor Agent is ready").await?;
|
||||
info!("Attune Sensor Agent shutdown complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod rule_lifecycle_listener;
|
||||
pub mod sensor_manager;
|
||||
pub mod sensor_worker_registration;
|
||||
pub mod service;
|
||||
pub mod startup;
|
||||
|
||||
// Re-export template resolver from common crate
|
||||
pub mod template_resolver {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
//! Attune Sensor Service
|
||||
//!
|
||||
//! The Sensor Service monitors for trigger conditions and generates events.
|
||||
//! It executes custom sensor code, manages sensor lifecycle, and publishes
|
||||
//! events to the message queue for rule matching and enforcement creation.
|
||||
|
||||
use anyhow::Result;
|
||||
use attune_common::config::Config;
|
||||
use attune_sensor::service::SensorService;
|
||||
use attune_sensor::startup::{
|
||||
init_tracing, log_config_details, run_sensor_service, set_config_path,
|
||||
};
|
||||
use clap::Parser;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "attune-sensor")]
|
||||
@@ -26,114 +25,23 @@ struct Args {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Install HMAC-only JWT crypto provider (must be before any token operations)
|
||||
attune_common::auth::install_crypto_provider();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize tracing with specified log level
|
||||
let log_level = args.log_level.parse().unwrap_or(tracing::Level::INFO);
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
init_tracing(log_level);
|
||||
|
||||
info!("Starting Attune Sensor Service");
|
||||
info!("Version: {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Load configuration
|
||||
if let Some(config_path) = args.config {
|
||||
info!("Loading configuration from: {}", config_path);
|
||||
std::env::set_var("ATTUNE_CONFIG", config_path);
|
||||
}
|
||||
set_config_path(args.config.as_deref());
|
||||
|
||||
let config = Config::load()?;
|
||||
config.validate()?;
|
||||
|
||||
info!("Configuration loaded successfully");
|
||||
info!("Environment: {}", config.environment);
|
||||
info!("Database: {}", mask_connection_string(&config.database.url));
|
||||
if let Some(ref mq_config) = config.message_queue {
|
||||
info!("Message Queue: {}", mask_connection_string(&mq_config.url));
|
||||
}
|
||||
|
||||
// Create and start sensor service
|
||||
let service = SensorService::new(config).await?;
|
||||
|
||||
info!("Sensor Service initialized successfully");
|
||||
|
||||
// Start the service (spawns background tasks and returns)
|
||||
info!("Starting Sensor Service components...");
|
||||
service.start().await?;
|
||||
|
||||
info!("Attune Sensor Service is ready");
|
||||
|
||||
// Setup signal handlers for graceful shutdown
|
||||
let mut sigint = signal(SignalKind::interrupt())?;
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => {
|
||||
info!("Received SIGINT signal");
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
info!("Received SIGTERM signal");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Shutting down gracefully...");
|
||||
|
||||
// Stop the service: deregister worker, stop sensors, clean up connections
|
||||
if let Err(e) = service.stop().await {
|
||||
error!("Error during shutdown: {}", e);
|
||||
}
|
||||
|
||||
log_config_details(&config);
|
||||
run_sensor_service(config, "Attune Sensor Service is ready").await?;
|
||||
info!("Attune Sensor Service shutdown complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mask sensitive parts of connection strings for logging
|
||||
fn mask_connection_string(url: &str) -> String {
|
||||
if let Some(at_pos) = url.find('@') {
|
||||
if let Some(proto_end) = url.find("://") {
|
||||
let protocol = &url[..proto_end + 3];
|
||||
let host_and_path = &url[at_pos..];
|
||||
return format!("{}***:***{}", protocol, host_and_path);
|
||||
}
|
||||
}
|
||||
"***:***@***".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mask_connection_string() {
|
||||
let url = "postgresql://user:password@localhost:5432/attune";
|
||||
let masked = mask_connection_string(url);
|
||||
assert!(!masked.contains("user"));
|
||||
assert!(!masked.contains("password"));
|
||||
assert!(masked.contains("@localhost"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_connection_string_no_credentials() {
|
||||
let url = "postgresql://localhost:5432/attune";
|
||||
let masked = mask_connection_string(url);
|
||||
assert_eq!(masked, "***:***@***");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_rabbitmq_connection() {
|
||||
let url = "amqp://admin:secret@rabbitmq:5672/%2F";
|
||||
let masked = mask_connection_string(url);
|
||||
assert!(!masked.contains("admin"));
|
||||
assert!(!masked.contains("secret"));
|
||||
assert!(masked.contains("@rabbitmq"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! - Monitoring sensor health and restarting failed sensors
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use attune_common::models::{Id, Sensor, Trigger};
|
||||
use attune_common::models::{runtime::RuntimeExecutionConfig, Id, Sensor, Trigger};
|
||||
use attune_common::repositories::{FindById, List, RuntimeRepository};
|
||||
|
||||
use sqlx::{PgPool, Row};
|
||||
@@ -162,6 +162,127 @@ impl SensorManager {
|
||||
Ok(enabled_sensors)
|
||||
}
|
||||
|
||||
async fn ensure_runtime_environment(
|
||||
&self,
|
||||
exec_config: &RuntimeExecutionConfig,
|
||||
pack_dir: &std::path::Path,
|
||||
env_dir: &std::path::Path,
|
||||
) -> Result<()> {
|
||||
let env_cfg = match &exec_config.environment {
|
||||
Some(cfg) if cfg.env_type != "none" => cfg,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let vars = exec_config.build_template_vars_with_env(pack_dir, Some(env_dir));
|
||||
|
||||
if !env_dir.exists() {
|
||||
if env_cfg.create_command.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Runtime environment '{}' requires create_command but none is configured",
|
||||
env_cfg.env_type
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(parent) = env_dir.parent() {
|
||||
tokio::fs::create_dir_all(parent).await.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to create runtime environment parent directory {}: {}",
|
||||
parent.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let resolved_cmd =
|
||||
RuntimeExecutionConfig::resolve_command(&env_cfg.create_command, &vars);
|
||||
let (program, args) = resolved_cmd
|
||||
.split_first()
|
||||
.ok_or_else(|| anyhow!("Empty create_command for runtime environment"))?;
|
||||
|
||||
info!(
|
||||
"Creating sensor runtime environment at {}: {:?}",
|
||||
env_dir.display(),
|
||||
resolved_cmd
|
||||
);
|
||||
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.current_dir(pack_dir)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to run create command '{}': {}", program, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!(
|
||||
"Runtime environment creation failed (exit {}): {}",
|
||||
output.status.code().unwrap_or(-1),
|
||||
stderr.trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let dep_cfg = match &exec_config.dependencies {
|
||||
Some(cfg) => cfg,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let manifest_path = pack_dir.join(&dep_cfg.manifest_file);
|
||||
if !manifest_path.exists() || dep_cfg.install_command.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let install_marker = env_dir.join(".attune_sensor_deps_installed");
|
||||
if install_marker.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let resolved_cmd = RuntimeExecutionConfig::resolve_command(&dep_cfg.install_command, &vars);
|
||||
let (program, args) = resolved_cmd
|
||||
.split_first()
|
||||
.ok_or_else(|| anyhow!("Empty install_command for runtime dependencies"))?;
|
||||
|
||||
info!(
|
||||
"Installing sensor runtime dependencies for {} using {:?}",
|
||||
pack_dir.display(),
|
||||
resolved_cmd
|
||||
);
|
||||
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.current_dir(pack_dir)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to run dependency install command '{}': {}",
|
||||
program,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!(
|
||||
"Runtime dependency installation failed (exit {}): {}",
|
||||
output.status.code().unwrap_or(-1),
|
||||
stderr.trim()
|
||||
));
|
||||
}
|
||||
|
||||
tokio::fs::write(&install_marker, b"ok")
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to write dependency install marker {}: {}",
|
||||
install_marker.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a sensor instance
|
||||
async fn start_sensor(&self, sensor: Sensor) -> Result<()> {
|
||||
info!("Starting sensor {} ({})", sensor.r#ref, sensor.id);
|
||||
@@ -231,6 +352,12 @@ impl SensorManager {
|
||||
|
||||
let exec_config = runtime.parsed_execution_config();
|
||||
let rt_name = runtime.name.to_lowercase();
|
||||
let runtime_env_suffix = runtime
|
||||
.r#ref
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.filter(|suffix| !suffix.is_empty())
|
||||
.unwrap_or(&rt_name);
|
||||
|
||||
info!(
|
||||
"Sensor {} runtime details: id={}, ref='{}', name='{}', execution_config={}",
|
||||
@@ -242,7 +369,19 @@ impl SensorManager {
|
||||
let pack_dir = std::path::PathBuf::from(&self.inner.packs_base_dir).join(pack_ref);
|
||||
let env_dir = std::path::PathBuf::from(&self.inner.runtime_envs_dir)
|
||||
.join(pack_ref)
|
||||
.join(&rt_name);
|
||||
.join(runtime_env_suffix);
|
||||
if let Err(e) = self
|
||||
.ensure_runtime_environment(&exec_config, &pack_dir, &env_dir)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to ensure sensor runtime environment for {} at {}: {}",
|
||||
sensor.r#ref,
|
||||
env_dir.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let env_dir_opt = if env_dir.exists() {
|
||||
Some(env_dir.as_path())
|
||||
} else {
|
||||
@@ -354,15 +493,31 @@ impl SensorManager {
|
||||
|
||||
// Start the standalone sensor with token and configuration
|
||||
// Pass sensor ref (e.g., "core.interval_timer_sensor") for proper identification
|
||||
let mut child = cmd
|
||||
.env("ATTUNE_API_URL", &self.inner.api_url)
|
||||
cmd.env("ATTUNE_API_URL", &self.inner.api_url)
|
||||
.env("ATTUNE_API_TOKEN", &token_response.token)
|
||||
.env("ATTUNE_SENSOR_ID", sensor.id.to_string())
|
||||
.env("ATTUNE_SENSOR_REF", &sensor.r#ref)
|
||||
.env("ATTUNE_SENSOR_TRIGGERS", &trigger_instances_json)
|
||||
.env("ATTUNE_MQ_URL", &self.inner.mq_url)
|
||||
.env("ATTUNE_MQ_EXCHANGE", "attune.events")
|
||||
.env("ATTUNE_LOG_LEVEL", "info")
|
||||
.env("ATTUNE_LOG_LEVEL", "info");
|
||||
|
||||
if !exec_config.env_vars.is_empty() {
|
||||
let vars = exec_config.build_template_vars_with_env(&pack_dir, env_dir_opt);
|
||||
for (key, value_template) in &exec_config.env_vars {
|
||||
let resolved = attune_common::models::RuntimeExecutionConfig::resolve_template(
|
||||
value_template,
|
||||
&vars,
|
||||
);
|
||||
debug!(
|
||||
"Setting sensor runtime env var: {}={} (template: {})",
|
||||
key, resolved, value_template
|
||||
);
|
||||
cmd.env(key, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -371,13 +526,14 @@ impl SensorManager {
|
||||
anyhow!(
|
||||
"Failed to start sensor process for '{}': {} \
|
||||
(binary='{}', is_native={}, runtime_ref='{}', \
|
||||
interpreter_config='{}')",
|
||||
interpreter_config='{}', env_dir='{}')",
|
||||
sensor.r#ref,
|
||||
e,
|
||||
spawn_binary,
|
||||
is_native,
|
||||
runtime.r#ref,
|
||||
interpreter_binary
|
||||
interpreter_binary,
|
||||
env_dir.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ use sqlx::{PgPool, Row};
|
||||
use std::collections::HashMap;
|
||||
use tracing::{debug, info};
|
||||
|
||||
const ATTUNE_SENSOR_AGENT_MODE_ENV: &str = "ATTUNE_SENSOR_AGENT_MODE";
|
||||
const ATTUNE_SENSOR_AGENT_BINARY_NAME_ENV: &str = "ATTUNE_SENSOR_AGENT_BINARY_NAME";
|
||||
const ATTUNE_SENSOR_AGENT_BINARY_VERSION_ENV: &str = "ATTUNE_SENSOR_AGENT_BINARY_VERSION";
|
||||
|
||||
/// Sensor worker registration manager
|
||||
pub struct SensorWorkerRegistration {
|
||||
pool: PgPool,
|
||||
@@ -25,6 +29,33 @@ pub struct SensorWorkerRegistration {
|
||||
}
|
||||
|
||||
impl SensorWorkerRegistration {
|
||||
fn env_truthy(name: &str) -> bool {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.map(|value| matches!(value.trim().to_ascii_lowercase().as_str(), "1" | "true"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn inject_agent_capabilities(capabilities: &mut HashMap<String, serde_json::Value>) {
|
||||
if Self::env_truthy(ATTUNE_SENSOR_AGENT_MODE_ENV) {
|
||||
capabilities.insert("agent_mode".to_string(), json!(true));
|
||||
}
|
||||
|
||||
if let Ok(binary_name) = std::env::var(ATTUNE_SENSOR_AGENT_BINARY_NAME_ENV) {
|
||||
let binary_name = binary_name.trim();
|
||||
if !binary_name.is_empty() {
|
||||
capabilities.insert("agent_binary_name".to_string(), json!(binary_name));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(binary_version) = std::env::var(ATTUNE_SENSOR_AGENT_BINARY_VERSION_ENV) {
|
||||
let binary_version = binary_version.trim();
|
||||
if !binary_version.is_empty() {
|
||||
capabilities.insert("agent_binary_version".to_string(), json!(binary_version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sensor worker registration manager
|
||||
pub fn new(pool: PgPool, config: &Config) -> Self {
|
||||
let worker_name = config
|
||||
@@ -67,6 +98,8 @@ impl SensorWorkerRegistration {
|
||||
json!(env!("CARGO_PKG_VERSION")),
|
||||
);
|
||||
|
||||
Self::inject_agent_capabilities(&mut capabilities);
|
||||
|
||||
// Placeholder for runtimes (will be detected asynchronously)
|
||||
capabilities.insert("runtimes".to_string(), json!(Vec::<String>::new()));
|
||||
|
||||
@@ -351,4 +384,28 @@ mod tests {
|
||||
|
||||
registration.deregister().await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_agent_capabilities_from_env() {
|
||||
std::env::set_var(ATTUNE_SENSOR_AGENT_MODE_ENV, "1");
|
||||
std::env::set_var(ATTUNE_SENSOR_AGENT_BINARY_NAME_ENV, "attune-sensor-agent");
|
||||
std::env::set_var(ATTUNE_SENSOR_AGENT_BINARY_VERSION_ENV, "1.2.3");
|
||||
|
||||
let mut capabilities = HashMap::new();
|
||||
SensorWorkerRegistration::inject_agent_capabilities(&mut capabilities);
|
||||
|
||||
assert_eq!(capabilities.get("agent_mode"), Some(&json!(true)));
|
||||
assert_eq!(
|
||||
capabilities.get("agent_binary_name"),
|
||||
Some(&json!("attune-sensor-agent"))
|
||||
);
|
||||
assert_eq!(
|
||||
capabilities.get("agent_binary_version"),
|
||||
Some(&json!("1.2.3"))
|
||||
);
|
||||
|
||||
std::env::remove_var(ATTUNE_SENSOR_AGENT_MODE_ENV);
|
||||
std::env::remove_var(ATTUNE_SENSOR_AGENT_BINARY_NAME_ENV);
|
||||
std::env::remove_var(ATTUNE_SENSOR_AGENT_BINARY_VERSION_ENV);
|
||||
}
|
||||
}
|
||||
|
||||
119
crates/sensor/src/startup.rs
Normal file
119
crates/sensor/src/startup.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::service::SensorService;
|
||||
use anyhow::Result;
|
||||
use attune_common::config::{Config, SensorConfig};
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use tracing::{error, info};
|
||||
|
||||
pub fn init_tracing(log_level: tracing::Level) {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
}
|
||||
|
||||
pub fn set_config_path(config_path: Option<&str>) {
|
||||
if let Some(config_path) = config_path {
|
||||
info!("Loading configuration from: {}", config_path);
|
||||
std::env::set_var("ATTUNE_CONFIG", config_path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_sensor_name_override(config: &mut Config, name: String) {
|
||||
if let Some(ref mut sensor_config) = config.sensor {
|
||||
sensor_config.worker_name = Some(name);
|
||||
} else {
|
||||
config.sensor = Some(SensorConfig {
|
||||
worker_name: Some(name),
|
||||
host: None,
|
||||
capabilities: None,
|
||||
max_concurrent_sensors: None,
|
||||
heartbeat_interval: 30,
|
||||
poll_interval: 30,
|
||||
sensor_timeout: 30,
|
||||
shutdown_timeout: 30,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_config_details(config: &Config) {
|
||||
info!("Configuration loaded successfully");
|
||||
info!("Environment: {}", config.environment);
|
||||
info!("Database: {}", mask_connection_string(&config.database.url));
|
||||
if let Some(ref mq_config) = config.message_queue {
|
||||
info!("Message Queue: {}", mask_connection_string(&mq_config.url));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_sensor_service(config: Config, ready_message: &str) -> Result<()> {
|
||||
let service = SensorService::new(config).await?;
|
||||
|
||||
info!("Sensor Service initialized successfully");
|
||||
info!("Starting Sensor Service components...");
|
||||
service.start().await?;
|
||||
info!("{}", ready_message);
|
||||
|
||||
let mut sigint = signal(SignalKind::interrupt())?;
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => {
|
||||
info!("Received SIGINT signal");
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
info!("Received SIGTERM signal");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Shutting down gracefully...");
|
||||
|
||||
if let Err(e) = service.stop().await {
|
||||
error!("Error during shutdown: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mask sensitive parts of connection strings for logging.
|
||||
pub fn mask_connection_string(url: &str) -> String {
|
||||
if let Some(at_pos) = url.find('@') {
|
||||
if let Some(proto_end) = url.find("://") {
|
||||
let protocol = &url[..proto_end + 3];
|
||||
let host_and_path = &url[at_pos..];
|
||||
return format!("{}***:***{}", protocol, host_and_path);
|
||||
}
|
||||
}
|
||||
"***:***@***".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mask_connection_string() {
|
||||
let url = "postgresql://user:password@localhost:5432/attune";
|
||||
let masked = mask_connection_string(url);
|
||||
assert!(!masked.contains("user"));
|
||||
assert!(!masked.contains("password"));
|
||||
assert!(masked.contains("@localhost"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_connection_string_no_credentials() {
|
||||
let url = "postgresql://localhost:5432/attune";
|
||||
let masked = mask_connection_string(url);
|
||||
assert_eq!(masked, "***:***@***");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_rabbitmq_connection() {
|
||||
let url = "amqp://admin:secret@rabbitmq:5672/%2F";
|
||||
let masked = mask_connection_string(url);
|
||||
assert!(!masked.contains("admin"));
|
||||
assert!(!masked.contains("secret"));
|
||||
assert!(masked.contains("@rabbitmq"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user