• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

organicveggie / saurron / 25411216837

06 May 2026 01:14AM UTC coverage: 49.671% (-0.2%) from 49.873%
25411216837

push

github

web-flow
Merge pull request #54 from organicveggie/timezone

Default to JSON and local time for logging

1 of 12 new or added lines in 2 files covered. (8.33%)

2 existing lines in 1 file now uncovered.

980 of 1973 relevant lines covered (49.67%)

93.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main.rs
1
use saurron::{cli, config, docker, http, registry, scheduler};
2
use std::sync::Arc;
3

4
use anyhow::Context as _;
5
use clap::Parser;
6
use tracing::info;
7

8
const VERSION: &str = env!("SAURRON_VERSION");
9

10
/// `tracing-subscriber` defaults to UTC timestamps regardless of the `TZ` environment variable.
11
/// This timer uses `chrono::Local` so the configured timezone is reflected in all log output.
12
struct LocalTime;
13

14
impl tracing_subscriber::fmt::time::FormatTime for LocalTime {
NEW
15
    fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
×
NEW
16
        write!(
×
NEW
17
            w,
×
18
            "{}",
NEW
19
            chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, false)
×
20
        )
21
    }
22
}
23

UNCOV
24
fn init_tracing(
×
25
    config: &config::Config,
26
) -> anyhow::Result<Vec<tracing_appender::non_blocking::WorkerGuard>> {
27
    use std::io::IsTerminal;
28
    use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};
29

30
    let level = match config.log_level {
×
31
        cli::LogLevel::Trace => tracing::Level::TRACE,
×
32
        cli::LogLevel::Debug => tracing::Level::DEBUG,
×
33
        cli::LogLevel::Info => tracing::Level::INFO,
×
34
        cli::LogLevel::Warn => tracing::Level::WARN,
×
35
        cli::LogLevel::Error => tracing::Level::ERROR,
×
36
    };
37

38
    let effective_format = match config.log_format {
×
39
        cli::LogFormat::Auto => {
40
            if std::io::stdout().is_terminal() {
×
41
                cli::LogFormat::Pretty
×
42
            } else {
NEW
43
                cli::LogFormat::Json
×
44
            }
45
        }
46
        f => f,
×
47
    };
48

49
    type BoxLayer = Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync>;
50

51
    let stdout_layer: BoxLayer = match effective_format {
×
NEW
52
        cli::LogFormat::Json => tracing_subscriber::fmt::layer()
×
NEW
53
            .with_timer(LocalTime)
×
54
            .json()
55
            .boxed(),
NEW
56
        cli::LogFormat::Pretty => tracing_subscriber::fmt::layer()
×
NEW
57
            .with_timer(LocalTime)
×
58
            .pretty()
59
            .boxed(),
60
        // tracing_logfmt hardcodes UTC and does not support a custom timer.
UNCOV
61
        cli::LogFormat::Logfmt => tracing_logfmt::layer().boxed(),
×
62
        cli::LogFormat::Auto => unreachable!(),
63
    };
64

65
    let mut guards: Vec<tracing_appender::non_blocking::WorkerGuard> = Vec::new();
×
66
    let mut layers: Vec<BoxLayer> = vec![stdout_layer];
×
67

68
    if let Some(ref path) = config.audit_log {
×
69
        let p = std::path::Path::new(path);
×
70
        let dir = p.parent().unwrap_or_else(|| std::path::Path::new("."));
×
71
        let filename = p
×
72
            .file_name()
73
            .context("audit_log path must include a filename")?
74
            .to_string_lossy()
75
            .into_owned();
76
        std::fs::create_dir_all(dir)
×
77
            .with_context(|| format!("failed to create audit log directory: {}", dir.display()))?;
×
78
        let appender = tracing_appender::rolling::never(dir, &filename);
×
79
        let (non_blocking, g) = tracing_appender::non_blocking(appender);
×
80
        guards.push(g);
×
81
        layers.push(
×
82
            tracing_subscriber::fmt::layer()
×
NEW
83
                .with_timer(LocalTime)
×
84
                .json()
×
85
                .with_writer(non_blocking)
×
86
                .with_filter(tracing_subscriber::filter::filter_fn(|meta| {
×
87
                    meta.target() == "saurron::audit"
×
88
                }))
89
                .boxed(),
×
90
        );
91
    }
92

93
    if let Some(ref path) = config.http_api.access_log {
×
94
        let p = std::path::Path::new(path);
×
95
        let dir = p.parent().unwrap_or_else(|| std::path::Path::new("."));
×
96
        let filename = p
×
97
            .file_name()
98
            .context("http_api.access_log path must include a filename")?
99
            .to_string_lossy()
100
            .into_owned();
101
        std::fs::create_dir_all(dir)
×
102
            .with_context(|| format!("failed to create access log directory: {}", dir.display()))?;
×
103
        let appender = tracing_appender::rolling::never(dir, &filename);
×
104
        let (non_blocking, g) = tracing_appender::non_blocking(appender);
×
105
        guards.push(g);
×
106
        layers.push(
×
107
            tracing_subscriber::fmt::layer()
×
NEW
108
                .with_timer(LocalTime)
×
109
                .json()
×
110
                .with_writer(non_blocking)
×
111
                .with_filter(tracing_subscriber::filter::filter_fn(|meta| {
×
112
                    meta.target() == "saurron::access"
×
113
                }))
114
                .boxed(),
×
115
        );
116
    }
117

118
    tracing_subscriber::registry()
×
119
        .with(layers)
×
120
        .with(tracing_subscriber::EnvFilter::from_default_env().add_directive(level.into()))
×
121
        .init();
122

123
    Ok(guards)
×
124
}
125

126
async fn shutdown_signal() {
×
127
    #[cfg(unix)]
128
    {
129
        use tokio::signal::unix::{SignalKind, signal};
130
        let mut sigterm =
×
131
            signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
×
132
        tokio::select! {
×
133
            _ = sigterm.recv() => { tracing::info!("SIGTERM received"); }
×
134
            _ = tokio::signal::ctrl_c() => { tracing::info!("SIGINT received"); }
×
135
        }
136
    }
137
    #[cfg(not(unix))]
138
    {
139
        tokio::signal::ctrl_c().await.ok();
140
        tracing::info!("shutdown signal received");
141
    }
142
}
143

144
#[tokio::main]
145
async fn main() -> anyhow::Result<()> {
×
146
    let args = cli::Args::parse();
×
147

148
    // Handle --generate-config before loading config or connecting to Docker,
149
    // so it works even when no config file or Docker daemon is present.
150
    if let Some(dest) = &args.generate_config {
×
151
        let content = config::generate_sample_config();
×
152
        if dest == "-" {
×
153
            print!("{content}");
×
154
        } else {
155
            std::fs::write(dest, &content)
×
156
                .with_context(|| format!("failed to write config to '{dest}'"))?;
×
157
        }
158
        return Ok(());
×
159
    }
160

161
    let (config, config_status) = config::Config::load(&args)?;
×
162
    let _guards = init_tracing(&config)?;
×
163

164
    info!(version = VERSION, "Saurron starting");
×
165
    config_status.log();
×
166
    if config_status.is_error() {
×
167
        return Err(anyhow::anyhow!("failed to load config file"));
×
168
    }
169
    config.log_settings();
×
170

171
    if config.http_api.access_log.is_some() && !config.http_api.update && !config.http_api.metrics {
×
172
        tracing::warn!(
×
173
            "http_api.access_log is configured but the HTTP API is not enabled; \
174
             access log will not be written"
175
        );
176
    }
177

178
    // Validate HTTP API token config before binding any ports.
179
    http::validate_token_config(&config.http_api)?;
×
180

181
    // Validate scheduling flags (clap catches CLI conflicts; this catches TOML combinations).
182
    let schedule_mode = scheduler::parse_schedule_mode(&config)?;
×
183

184
    match &schedule_mode {
×
185
        scheduler::ScheduleMode::RunOnce => {
186
            info!(mode = "run-once", "schedule configured");
×
187
        }
188
        scheduler::ScheduleMode::Interval(_) => {
189
            let interval = config.poll_interval.as_deref().unwrap_or("24h");
×
190
            info!(
×
191
                mode = "interval",
192
                interval,
193
                first_run = "immediate",
194
                "schedule configured"
195
            );
196
        }
197
        scheduler::ScheduleMode::Cron(_) => {
198
            let expression = config.schedule.as_deref().unwrap_or("");
×
199
            if let Some(next) = schedule_mode.next_run() {
×
200
                info!(mode = "cron", expression, next_run = %next, "schedule configured");
×
201
            }
202
        }
203
    }
204

205
    let docker = docker::DockerClient::connect(&config.docker)?;
×
206
    docker.ping().await?;
×
207
    info!("Connected to Docker daemon");
×
208

209
    let selector = docker::ContainerSelector::new(
210
        config.label_enable,
×
211
        config.global_takes_precedence,
×
212
        &config.disable_containers,
×
213
        &config.containers,
×
214
        config.include_restarting,
×
215
        config.revive_stopped,
×
216
    );
217

218
    // Initial enumeration for startup logging only.
219
    let all_containers = docker.list_containers(&selector).await?;
×
220
    let selected = docker.select_containers(&all_containers, &selector);
×
221
    info!(
×
222
        total = all_containers.len(),
×
223
        selected = selected.len(),
×
224
        "Container enumeration complete"
225
    );
226
    for c in &selected {
×
227
        info!(id = %c.id, name = %c.name, image = %c.image, state = %c.state, "Container selected");
×
228
    }
229

230
    let credentials = match (
×
231
        config.registry_username.clone(),
×
232
        config.registry_password.clone(),
×
233
    ) {
234
        (Some(u), Some(p)) => Some((u, p)),
×
235
        _ => None,
×
236
    };
237
    let registry_client =
×
238
        registry::RegistryClient::new(config.head_warn_strategy, VERSION, credentials)
×
239
            .context("failed to initialise registry client")?;
240

241
    let state = Arc::new(http::AppStateInner {
×
242
        docker,
×
243
        registry: registry_client,
×
244
        config,
×
245
        selector,
×
246
        update_lock: tokio::sync::Mutex::new(()),
×
247
    });
248

249
    let http_enabled = state.config.http_api.update || state.config.http_api.metrics;
×
250

251
    if matches!(schedule_mode, scheduler::ScheduleMode::RunOnce) {
×
252
        http::run_cycle_with_state(&state).await;
×
253
        return Ok(());
×
254
    }
255

256
    let state_for_scheduler = Arc::clone(&state);
×
257
    let scheduler_task = tokio::spawn(async move {
×
258
        scheduler::run_scheduler(schedule_mode, move || {
×
259
            let s = Arc::clone(&state_for_scheduler);
×
260
            async move {
×
261
                let _guard = s.update_lock.lock().await;
×
262
                http::run_cycle_with_state(&s).await;
×
263
            }
264
        })
265
        .await;
×
266
    });
267

268
    if http_enabled {
×
269
        tokio::select! {
×
270
            result = http::start_server(Arc::clone(&state)) => { result?; }
×
271
            _ = scheduler_task => {}
×
272
            _ = shutdown_signal() => {
×
273
                info!("Shutdown signal received; waiting for active update cycle to complete");
×
274
                let _ = state.update_lock.lock().await;
×
275
                info!("Graceful shutdown complete");
×
276
            }
277
        }
278
    } else {
279
        tokio::select! {
×
280
            _ = scheduler_task => {}
×
281
            _ = shutdown_signal() => {
×
282
                info!("Shutdown signal received; waiting for active update cycle to complete");
×
283
                let _ = state.update_lock.lock().await;
×
284
                info!("Graceful shutdown complete");
×
285
            }
286
        }
287
    }
288

289
    Ok(())
×
290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc