mirror of
https://github.com/telemt/telemt.git
synced 2026-06-25 04:11:10 +03:00
Add bounded file logging rotation and retention #832
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
|
|
||||||
# Table of contents
|
# Table of contents
|
||||||
- [Top-level keys](#top-level-keys)
|
- [Top-level keys](#top-level-keys)
|
||||||
|
- [logging](#logging)
|
||||||
- [general](#general)
|
- [general](#general)
|
||||||
- [general.modes](#generalmodes)
|
- [general.modes](#generalmodes)
|
||||||
- [general.links](#generallinks)
|
- [general.links](#generallinks)
|
||||||
@@ -35,6 +36,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| --- | ---- | ------- | ---------- |
|
| --- | ---- | ------- | ---------- |
|
||||||
| [`include`](#include) | `String` (special directive) | — | `✔` |
|
| [`include`](#include) | `String` (special directive) | — | `✔` |
|
||||||
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) | `✘` |
|
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) | `✘` |
|
||||||
|
| [`logging`](#logging) | Table | default values | `✘` |
|
||||||
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` | `✘` |
|
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` | `✘` |
|
||||||
| [`default_dc`](#default_dc) | `u8` | — (effective fallback: `2` in ME routing) | `✘` |
|
| [`default_dc`](#default_dc) | `u8` | — (effective fallback: `2` in ME routing) | `✘` |
|
||||||
| [`beobachten`](#beobachten) | `bool` | `true` | `✘` |
|
| [`beobachten`](#beobachten) | `bool` | `true` | `✘` |
|
||||||
@@ -83,6 +85,84 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
default_dc = 2
|
default_dc = 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# [logging]
|
||||||
|
|
||||||
|
| Key | Type | Default | Hot-Reload |
|
||||||
|
| --- | ---- | ------- | ---------- |
|
||||||
|
| [`destination`](#loggingdestination) | `"stderr"` / `"syslog"` / `"file"` | `"stderr"` | `✘` |
|
||||||
|
| [`path`](#loggingpath) | `String` | — | `✘` |
|
||||||
|
| [`rotation`](#loggingrotation) | `"never"` / `"minutely"` / `"hourly"` / `"daily"` / `"weekly"` | `"never"` | `✘` |
|
||||||
|
| [`max_size_bytes`](#loggingmax_size_bytes) | `u64` | `0` | `✘` |
|
||||||
|
| [`max_files`](#loggingmax_files) | `usize` | `0` | `✘` |
|
||||||
|
| [`max_age_secs`](#loggingmax_age_secs) | `u64` | `0` | `✘` |
|
||||||
|
|
||||||
|
## logging.destination
|
||||||
|
- **Constraints / validation**: Must be `stderr`, `syslog`, or `file`. `syslog` is supported only on Unix platforms. `file` requires `logging.path`.
|
||||||
|
- **Description**: Selects the runtime log destination. CLI flags override this value.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
```
|
||||||
|
## logging.path
|
||||||
|
- **Constraints / validation**: Required when `logging.destination = "file"`; must not be empty.
|
||||||
|
- **Description**: File path used for file logging. With time rotation, the file name is used as the rolling prefix.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
```
|
||||||
|
## logging.rotation
|
||||||
|
- **Constraints / validation**: Must be `never`, `minutely`, `hourly`, `daily`, or `weekly`.
|
||||||
|
- **Description**: Time-based file rotation interval. `weekly` rotates at the Sunday UTC boundary. `never` writes to the exact `logging.path` unless size rotation is enabled.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
rotation = "daily"
|
||||||
|
```
|
||||||
|
## logging.max_size_bytes
|
||||||
|
- **Constraints / validation**: `0` disables size rotation.
|
||||||
|
- **Description**: Rotates file logs before writing the next record when the active file is non-empty and that record would exceed this byte limit. Records are written whole and are not split.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
max_size_bytes = 104857600
|
||||||
|
```
|
||||||
|
## logging.max_files
|
||||||
|
- **Constraints / validation**: `0` disables count-based retention.
|
||||||
|
- **Description**: Keeps at most this many matching file logs, counting the active file and rotated archives. The active file is never deleted by retention cleanup.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
rotation = "daily"
|
||||||
|
max_files = 14
|
||||||
|
```
|
||||||
|
## logging.max_age_secs
|
||||||
|
- **Constraints / validation**: `0` disables age-based retention.
|
||||||
|
- **Description**: Removes rotated file logs older than this many seconds based on file modification time. The active file is never deleted by retention cleanup.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/var/log/telemt.log"
|
||||||
|
rotation = "daily"
|
||||||
|
max_age_secs = 1209600
|
||||||
|
```
|
||||||
|
|
||||||
# [general]
|
# [general]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+118
-1
@@ -114,6 +114,7 @@ fn normalize_exclusive_mask_target(target: &str, field: &str) -> Result<String>
|
|||||||
|
|
||||||
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
||||||
"general",
|
"general",
|
||||||
|
"logging",
|
||||||
"network",
|
"network",
|
||||||
"server",
|
"server",
|
||||||
"timeouts",
|
"timeouts",
|
||||||
@@ -459,6 +460,14 @@ const UPSTREAM_CONFIG_KEYS: &[&str] = &[
|
|||||||
const PROXY_MODES_CONFIG_KEYS: &[&str] = &["classic", "secure", "tls"];
|
const PROXY_MODES_CONFIG_KEYS: &[&str] = &["classic", "secure", "tls"];
|
||||||
const TELEMETRY_CONFIG_KEYS: &[&str] = &["core_enabled", "user_enabled", "me_level"];
|
const TELEMETRY_CONFIG_KEYS: &[&str] = &["core_enabled", "user_enabled", "me_level"];
|
||||||
const LINKS_CONFIG_KEYS: &[&str] = &["show", "public_host", "public_port"];
|
const LINKS_CONFIG_KEYS: &[&str] = &["show", "public_host", "public_port"];
|
||||||
|
const LOGGING_CONFIG_KEYS: &[&str] = &[
|
||||||
|
"destination",
|
||||||
|
"path",
|
||||||
|
"rotation",
|
||||||
|
"max_size_bytes",
|
||||||
|
"max_files",
|
||||||
|
"max_age_secs",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UnknownConfigKey {
|
struct UnknownConfigKey {
|
||||||
@@ -500,6 +509,7 @@ fn known_config_keys_for_suggestion() -> Vec<&'static str> {
|
|||||||
PROXY_MODES_CONFIG_KEYS,
|
PROXY_MODES_CONFIG_KEYS,
|
||||||
TELEMETRY_CONFIG_KEYS,
|
TELEMETRY_CONFIG_KEYS,
|
||||||
LINKS_CONFIG_KEYS,
|
LINKS_CONFIG_KEYS,
|
||||||
|
LOGGING_CONFIG_KEYS,
|
||||||
] {
|
] {
|
||||||
keys.extend_from_slice(group);
|
keys.extend_from_slice(group);
|
||||||
}
|
}
|
||||||
@@ -633,6 +643,13 @@ fn collect_unknown_config_keys(parsed_toml: &toml::Value) -> Vec<UnknownConfigKe
|
|||||||
&["general", "links"],
|
&["general", "links"],
|
||||||
LINKS_CONFIG_KEYS,
|
LINKS_CONFIG_KEYS,
|
||||||
);
|
);
|
||||||
|
check_known_table(
|
||||||
|
parsed_toml,
|
||||||
|
&mut unknown,
|
||||||
|
&known_for_suggestion,
|
||||||
|
&["logging"],
|
||||||
|
LOGGING_CONFIG_KEYS,
|
||||||
|
);
|
||||||
check_known_table(
|
check_known_table(
|
||||||
parsed_toml,
|
parsed_toml,
|
||||||
&mut unknown,
|
&mut unknown,
|
||||||
@@ -998,6 +1015,24 @@ fn sanitize_ad_tag(ad_tag: &mut Option<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_logging_config(logging: &LoggingConfig) -> Result<()> {
|
||||||
|
if let Some(path) = logging.path.as_ref()
|
||||||
|
&& path.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"logging.path cannot be empty when provided".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(logging.destination, LoggingDestination::File) && logging.path.is_none() {
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"logging.path must be set when logging.destination=\"file\"".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
|
fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
|
||||||
let has_enabled_shadowsocks = config.upstreams.iter().any(|upstream| {
|
let has_enabled_shadowsocks = config.upstreams.iter().any(|upstream| {
|
||||||
upstream.enabled && matches!(upstream.upstream_type, UpstreamType::Shadowsocks { .. })
|
upstream.enabled && matches!(upstream.upstream_type, UpstreamType::Shadowsocks { .. })
|
||||||
@@ -1058,13 +1093,17 @@ fn normalize_upstream_family_policy(config: &mut ProxyConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============= Main Config =============
|
// Main runtime configuration loaded from TOML.
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ProxyConfig {
|
pub struct ProxyConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub general: GeneralConfig,
|
pub general: GeneralConfig,
|
||||||
|
|
||||||
|
/// Runtime logging destination, rotation, and retention configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub logging: LoggingConfig,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub network: NetworkConfig,
|
pub network: NetworkConfig,
|
||||||
|
|
||||||
@@ -2288,6 +2327,7 @@ impl ProxyConfig {
|
|||||||
.entry("203".to_string())
|
.entry("203".to_string())
|
||||||
.or_insert_with(|| vec!["91.105.192.100:443".to_string()]);
|
.or_insert_with(|| vec!["91.105.192.100:443".to_string()]);
|
||||||
|
|
||||||
|
validate_logging_config(&config.logging)?;
|
||||||
validate_upstreams(&config)?;
|
validate_upstreams(&config)?;
|
||||||
config.rebuild_runtime_user_auth()?;
|
config.rebuild_runtime_user_auth()?;
|
||||||
|
|
||||||
@@ -2313,6 +2353,8 @@ impl ProxyConfig {
|
|||||||
return Err(ProxyError::Config("No users configured".to_string()));
|
return Err(ProxyError::Config("No users configured".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_logging_config(&self.logging)?;
|
||||||
|
|
||||||
if !self.general.modes.classic && !self.general.modes.secure && !self.general.modes.tls {
|
if !self.general.modes.classic && !self.general.modes.secure && !self.general.modes.tls {
|
||||||
return Err(ProxyError::Config("No modes enabled".to_string()));
|
return Err(ProxyError::Config("No modes enabled".to_string()));
|
||||||
}
|
}
|
||||||
@@ -2409,6 +2451,21 @@ mod tests {
|
|||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_config_error_from_temp_toml(toml: &str) -> String {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let dir = std::env::temp_dir().join(format!("telemt_load_cfg_error_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("config.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let error = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
let _ = std::fs::remove_dir(dir);
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_defaults_remain_unchanged_for_present_sections() {
|
fn serde_defaults_remain_unchanged_for_present_sections() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
@@ -2419,6 +2476,7 @@ mod tests {
|
|||||||
"#;
|
"#;
|
||||||
let cfg: ProxyConfig = toml::from_str(toml).unwrap();
|
let cfg: ProxyConfig = toml::from_str(toml).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cfg.logging, LoggingConfig::default());
|
||||||
assert_eq!(cfg.network.ipv6, default_network_ipv6());
|
assert_eq!(cfg.network.ipv6, default_network_ipv6());
|
||||||
assert_eq!(cfg.network.stun_use, default_true());
|
assert_eq!(cfg.network.stun_use, default_true());
|
||||||
assert_eq!(cfg.network.stun_tcp_fallback, default_stun_tcp_fallback());
|
assert_eq!(cfg.network.stun_tcp_fallback, default_stun_tcp_fallback());
|
||||||
@@ -2586,6 +2644,65 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_config_is_loaded_from_strict_config() {
|
||||||
|
let cfg = load_config_from_temp_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
config_strict = true
|
||||||
|
|
||||||
|
[general.modes]
|
||||||
|
classic = false
|
||||||
|
secure = false
|
||||||
|
tls = true
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
path = "/tmp/telemt.log"
|
||||||
|
rotation = "daily"
|
||||||
|
max_size_bytes = 1024
|
||||||
|
max_files = 3
|
||||||
|
max_age_secs = 60
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(cfg.logging.destination, LoggingDestination::File);
|
||||||
|
assert_eq!(cfg.logging.path.as_deref(), Some("/tmp/telemt.log"));
|
||||||
|
assert_eq!(cfg.logging.rotation, LogRotation::Daily);
|
||||||
|
assert_eq!(cfg.logging.max_size_bytes, 1024);
|
||||||
|
assert_eq!(cfg.logging.max_files, 3);
|
||||||
|
assert_eq!(cfg.logging.max_age_secs, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_logging_requires_path() {
|
||||||
|
let error = load_config_error_from_temp_toml(
|
||||||
|
r#"
|
||||||
|
[general.modes]
|
||||||
|
classic = false
|
||||||
|
secure = false
|
||||||
|
tls = true
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
destination = "file"
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(error.contains("logging.path must be set"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn impl_defaults_are_sourced_from_default_helpers() {
|
fn impl_defaults_are_sourced_from_default_helpers() {
|
||||||
let network = NetworkConfig::default();
|
let network = NetworkConfig::default();
|
||||||
|
|||||||
@@ -63,6 +63,86 @@ impl std::fmt::Display for LogLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logging output destination.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LoggingDestination {
|
||||||
|
/// Write logs to stderr.
|
||||||
|
#[default]
|
||||||
|
Stderr,
|
||||||
|
/// Write logs to syslog on Unix platforms.
|
||||||
|
Syslog,
|
||||||
|
/// Write logs to a file.
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time-based log rotation interval for file logging.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LogRotation {
|
||||||
|
/// Do not rotate logs by time.
|
||||||
|
#[default]
|
||||||
|
Never,
|
||||||
|
/// Rotate once per minute.
|
||||||
|
Minutely,
|
||||||
|
/// Rotate once per hour.
|
||||||
|
Hourly,
|
||||||
|
/// Rotate once per day.
|
||||||
|
Daily,
|
||||||
|
/// Rotate once per week.
|
||||||
|
Weekly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogRotation {
|
||||||
|
/// Parse a CLI rotation value.
|
||||||
|
pub fn from_cli_arg(value: &str) -> Option<Self> {
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"never" | "none" | "off" => Some(Self::Never),
|
||||||
|
"minutely" | "minute" => Some(Self::Minutely),
|
||||||
|
"hourly" | "hour" => Some(Self::Hourly),
|
||||||
|
"daily" | "day" => Some(Self::Daily),
|
||||||
|
"weekly" | "week" => Some(Self::Weekly),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File logging and retention settings.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct LoggingConfig {
|
||||||
|
/// Effective logging destination.
|
||||||
|
#[serde(default)]
|
||||||
|
pub destination: LoggingDestination,
|
||||||
|
/// File path used when `destination = "file"`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: Option<String>,
|
||||||
|
/// Time rotation interval for file logs.
|
||||||
|
#[serde(default)]
|
||||||
|
pub rotation: LogRotation,
|
||||||
|
/// Maximum active log file size before rotating. `0` disables size rotation.
|
||||||
|
#[serde(default)]
|
||||||
|
pub max_size_bytes: u64,
|
||||||
|
/// Maximum number of matching log files to keep. `0` disables count retention.
|
||||||
|
#[serde(default)]
|
||||||
|
pub max_files: usize,
|
||||||
|
/// Maximum age for rotated log files in seconds. `0` disables age retention.
|
||||||
|
#[serde(default)]
|
||||||
|
pub max_age_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoggingConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
destination: LoggingDestination::Stderr,
|
||||||
|
path: None,
|
||||||
|
rotation: LogRotation::Never,
|
||||||
|
max_size_bytes: 0,
|
||||||
|
max_files: 0,
|
||||||
|
max_age_secs: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Middle-End telemetry verbosity level.
|
/// Middle-End telemetry verbosity level.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
|||||||
+222
-101
@@ -5,16 +5,41 @@
|
|||||||
//! - syslog (Unix only, for traditional init systems)
|
//! - syslog (Unix only, for traditional init systems)
|
||||||
//! - file (with optional rotation)
|
//! - file (with optional rotation)
|
||||||
|
|
||||||
#![allow(dead_code)] // Infrastructure module - used via CLI flags
|
// Infrastructure module used via CLI flags.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::config::{LogRotation, LoggingConfig, LoggingDestination};
|
||||||
|
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tracing_subscriber::{EnvFilter, fmt, reload};
|
use tracing_subscriber::{EnvFilter, fmt, reload};
|
||||||
|
|
||||||
|
// Submodules:
|
||||||
|
// - file: bounded file appender for size and retention controls.
|
||||||
|
mod file;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// File logging and retention options resolved from config and CLI.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FileLogOptions {
|
||||||
|
/// Log file path or rolling filename prefix path.
|
||||||
|
pub path: String,
|
||||||
|
/// Time rotation interval.
|
||||||
|
pub rotation: LogRotation,
|
||||||
|
/// Maximum active file size before size rotation. `0` disables it.
|
||||||
|
pub max_size_bytes: u64,
|
||||||
|
/// Maximum number of matching log files to keep. `0` disables it.
|
||||||
|
pub max_files: usize,
|
||||||
|
/// Maximum rotated file age in seconds. `0` disables it.
|
||||||
|
pub max_age_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Log destination configuration.
|
/// Log destination configuration.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub enum LogDestination {
|
pub enum LogDestination {
|
||||||
/// Log to stderr (default, captured by systemd journald).
|
/// Log to stderr (default, captured by systemd journald).
|
||||||
#[default]
|
#[default]
|
||||||
@@ -24,12 +49,29 @@ pub enum LogDestination {
|
|||||||
Syslog,
|
Syslog,
|
||||||
/// Log to a file with optional rotation.
|
/// Log to a file with optional rotation.
|
||||||
File {
|
File {
|
||||||
path: String,
|
/// Resolved file logging options.
|
||||||
/// Rotate daily if true.
|
options: FileLogOptions,
|
||||||
rotate_daily: bool,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum LogCliDestination {
|
||||||
|
Stderr,
|
||||||
|
Syslog,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logging-related CLI overrides.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct LogCliOptions {
|
||||||
|
destination: Option<LogCliDestination>,
|
||||||
|
path: Option<String>,
|
||||||
|
rotation: Option<LogRotation>,
|
||||||
|
max_size_bytes: Option<u64>,
|
||||||
|
max_files: Option<usize>,
|
||||||
|
max_age_secs: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Logging options parsed from CLI/config.
|
/// Logging options parsed from CLI/config.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct LoggingOptions {
|
pub struct LoggingOptions {
|
||||||
@@ -101,23 +143,29 @@ pub fn init_logging(
|
|||||||
(filter_handle, LoggingGuard::noop())
|
(filter_handle, LoggingGuard::noop())
|
||||||
}
|
}
|
||||||
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
LogDestination::File { options } => {
|
||||||
let (non_blocking, guard) = if *rotate_daily {
|
let (non_blocking, guard) = if options.max_size_bytes > 0
|
||||||
// Extract directory and filename prefix
|
|| options.max_files > 0
|
||||||
let path = Path::new(path);
|
|| options.max_age_secs > 0
|
||||||
let dir = path.parent().unwrap_or(Path::new("/var/log"));
|
{
|
||||||
let prefix = path
|
let file_appender = file::BoundedFileAppender::new(options.clone())
|
||||||
.file_name()
|
.expect("Failed to open log file");
|
||||||
.and_then(|s| s.to_str())
|
tracing_appender::non_blocking(file_appender)
|
||||||
.unwrap_or("telemt");
|
} else if !matches!(options.rotation, LogRotation::Never) {
|
||||||
|
let path = Path::new(&options.path);
|
||||||
let file_appender = tracing_appender::rolling::daily(dir, prefix);
|
let dir = log_file_dir(path);
|
||||||
|
let prefix = log_file_name(path);
|
||||||
|
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
|
||||||
|
.rotation(to_tracing_rotation(options.rotation))
|
||||||
|
.filename_prefix(prefix)
|
||||||
|
.build(dir)
|
||||||
|
.expect("Failed to open log file");
|
||||||
tracing_appender::non_blocking(file_appender)
|
tracing_appender::non_blocking(file_appender)
|
||||||
} else {
|
} else {
|
||||||
let file = std::fs::OpenOptions::new()
|
let file = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(path)
|
.open(&options.path)
|
||||||
.expect("Failed to open log file");
|
.expect("Failed to open log file");
|
||||||
tracing_appender::non_blocking(file)
|
tracing_appender::non_blocking(file)
|
||||||
};
|
};
|
||||||
@@ -137,6 +185,28 @@ pub fn init_logging(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_file_dir(path: &Path) -> &Path {
|
||||||
|
path.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_file_name(path: &Path) -> &str {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("telemt")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tracing_rotation(rotation: LogRotation) -> tracing_appender::rolling::Rotation {
|
||||||
|
match rotation {
|
||||||
|
LogRotation::Never => tracing_appender::rolling::Rotation::NEVER,
|
||||||
|
LogRotation::Minutely => tracing_appender::rolling::Rotation::MINUTELY,
|
||||||
|
LogRotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
|
||||||
|
LogRotation::Daily => tracing_appender::rolling::Rotation::DAILY,
|
||||||
|
LogRotation::Weekly => tracing_appender::rolling::Rotation::WEEKLY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Syslog writer for tracing.
|
/// Syslog writer for tracing.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -223,121 +293,172 @@ impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogMakeWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse log destination from CLI arguments.
|
/// Parse logging overrides from CLI arguments.
|
||||||
pub fn parse_log_destination(args: &[String]) -> LogDestination {
|
pub fn parse_log_cli_options(args: &[String]) -> Result<LogCliOptions, String> {
|
||||||
|
let mut options = LogCliOptions::default();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
match args[i].as_str() {
|
match args[i].as_str() {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
"--syslog" => {
|
"--syslog" => {
|
||||||
return LogDestination::Syslog;
|
options.destination = Some(LogCliDestination::Syslog);
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
"--syslog" => {
|
||||||
|
options.destination = Some(LogCliDestination::Syslog);
|
||||||
}
|
}
|
||||||
"--log-file" => {
|
"--log-file" => {
|
||||||
i += 1;
|
i += 1;
|
||||||
if i < args.len() {
|
if i < args.len() {
|
||||||
return LogDestination::File {
|
options.destination = Some(LogCliDestination::File);
|
||||||
path: args[i].clone(),
|
options.path = Some(args[i].clone());
|
||||||
rotate_daily: false,
|
} else {
|
||||||
};
|
return Err("Missing value for --log-file".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--log-file=") => {
|
s if s.starts_with("--log-file=") => {
|
||||||
return LogDestination::File {
|
options.destination = Some(LogCliDestination::File);
|
||||||
path: s.trim_start_matches("--log-file=").to_string(),
|
options.path = Some(s.trim_start_matches("--log-file=").to_string());
|
||||||
rotate_daily: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
"--log-file-daily" => {
|
"--log-file-daily" => {
|
||||||
i += 1;
|
i += 1;
|
||||||
if i < args.len() {
|
if i < args.len() {
|
||||||
return LogDestination::File {
|
options.destination = Some(LogCliDestination::File);
|
||||||
path: args[i].clone(),
|
options.path = Some(args[i].clone());
|
||||||
rotate_daily: true,
|
options.rotation = Some(LogRotation::Daily);
|
||||||
};
|
} else {
|
||||||
|
return Err("Missing value for --log-file-daily".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--log-file-daily=") => {
|
s if s.starts_with("--log-file-daily=") => {
|
||||||
return LogDestination::File {
|
options.destination = Some(LogCliDestination::File);
|
||||||
path: s.trim_start_matches("--log-file-daily=").to_string(),
|
options.path = Some(s.trim_start_matches("--log-file-daily=").to_string());
|
||||||
rotate_daily: true,
|
options.rotation = Some(LogRotation::Daily);
|
||||||
};
|
}
|
||||||
|
"--log-rotation" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
options.rotation = Some(parse_rotation_cli_value(&args[i])?);
|
||||||
|
} else {
|
||||||
|
return Err("Missing value for --log-rotation".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-rotation=") => {
|
||||||
|
options.rotation = Some(parse_rotation_cli_value(
|
||||||
|
s.trim_start_matches("--log-rotation="),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
"--log-max-size-bytes" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
options.max_size_bytes =
|
||||||
|
Some(parse_u64_cli_value("--log-max-size-bytes", &args[i])?);
|
||||||
|
} else {
|
||||||
|
return Err("Missing value for --log-max-size-bytes".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-max-size-bytes=") => {
|
||||||
|
options.max_size_bytes = Some(parse_u64_cli_value(
|
||||||
|
"--log-max-size-bytes",
|
||||||
|
s.trim_start_matches("--log-max-size-bytes="),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
"--log-max-files" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
options.max_files = Some(parse_usize_cli_value("--log-max-files", &args[i])?);
|
||||||
|
} else {
|
||||||
|
return Err("Missing value for --log-max-files".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-max-files=") => {
|
||||||
|
options.max_files = Some(parse_usize_cli_value(
|
||||||
|
"--log-max-files",
|
||||||
|
s.trim_start_matches("--log-max-files="),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
"--log-max-age-secs" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
options.max_age_secs =
|
||||||
|
Some(parse_u64_cli_value("--log-max-age-secs", &args[i])?);
|
||||||
|
} else {
|
||||||
|
return Err("Missing value for --log-max-age-secs".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-max-age-secs=") => {
|
||||||
|
options.max_age_secs = Some(parse_u64_cli_value(
|
||||||
|
"--log-max-age-secs",
|
||||||
|
s.trim_start_matches("--log-max-age-secs="),
|
||||||
|
)?);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
LogDestination::Stderr
|
Ok(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
fn parse_rotation_cli_value(value: &str) -> Result<LogRotation, String> {
|
||||||
mod tests {
|
LogRotation::from_cli_arg(value).ok_or_else(|| {
|
||||||
use super::*;
|
format!(
|
||||||
|
"Invalid --log-rotation value '{value}'. Expected never|minutely|hourly|daily|weekly"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
fn parse_u64_cli_value(flag: &str, value: &str) -> Result<u64, String> {
|
||||||
fn test_parse_log_destination_default() {
|
value
|
||||||
let args: Vec<String> = vec![];
|
.parse::<u64>()
|
||||||
assert!(matches!(
|
.map_err(|_| format!("Invalid {flag} value '{value}'. Expected unsigned integer"))
|
||||||
parse_log_destination(&args),
|
}
|
||||||
LogDestination::Stderr
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
fn parse_usize_cli_value(flag: &str, value: &str) -> Result<usize, String> {
|
||||||
fn test_parse_log_destination_file() {
|
value
|
||||||
let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()];
|
.parse::<usize>()
|
||||||
match parse_log_destination(&args) {
|
.map_err(|_| format!("Invalid {flag} value '{value}'. Expected unsigned integer"))
|
||||||
LogDestination::File { path, rotate_daily } => {
|
}
|
||||||
assert_eq!(path, "/var/log/telemt.log");
|
|
||||||
assert!(!rotate_daily);
|
/// Resolve effective logging destination from config and CLI overrides.
|
||||||
|
pub fn resolve_log_destination(
|
||||||
|
config: &LoggingConfig,
|
||||||
|
cli: &LogCliOptions,
|
||||||
|
) -> Result<LogDestination, String> {
|
||||||
|
let destination = cli.destination.unwrap_or(match config.destination {
|
||||||
|
LoggingDestination::Stderr => LogCliDestination::Stderr,
|
||||||
|
LoggingDestination::Syslog => LogCliDestination::Syslog,
|
||||||
|
LoggingDestination::File => LogCliDestination::File,
|
||||||
|
});
|
||||||
|
|
||||||
|
match destination {
|
||||||
|
LogCliDestination::Stderr => Ok(LogDestination::Stderr),
|
||||||
|
LogCliDestination::Syslog => {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
Ok(LogDestination::Syslog)
|
||||||
}
|
}
|
||||||
_ => panic!("Expected File destination"),
|
#[cfg(not(unix))]
|
||||||
}
|
{
|
||||||
}
|
Err("Syslog logging is only supported on Unix platforms".to_string())
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_file_daily() {
|
|
||||||
let args = vec!["--log-file-daily=/var/log/telemt".to_string()];
|
|
||||||
match parse_log_destination(&args) {
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
assert_eq!(path, "/var/log/telemt");
|
|
||||||
assert!(rotate_daily);
|
|
||||||
}
|
}
|
||||||
_ => panic!("Expected File destination"),
|
|
||||||
}
|
}
|
||||||
}
|
LogCliDestination::File => {
|
||||||
|
let path = cli.path.as_ref().or(config.path.as_ref()).ok_or_else(|| {
|
||||||
|
"logging.path or --log-file must be set when file logging is enabled".to_string()
|
||||||
|
})?;
|
||||||
|
if path.trim().is_empty() {
|
||||||
|
return Err("Log file path cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
Ok(LogDestination::File {
|
||||||
#[test]
|
options: FileLogOptions {
|
||||||
fn test_parse_log_destination_syslog() {
|
path: path.clone(),
|
||||||
let args = vec!["--syslog".to_string()];
|
rotation: cli.rotation.unwrap_or(config.rotation),
|
||||||
assert!(matches!(
|
max_size_bytes: cli.max_size_bytes.unwrap_or(config.max_size_bytes),
|
||||||
parse_log_destination(&args),
|
max_files: cli.max_files.unwrap_or(config.max_files),
|
||||||
LogDestination::Syslog
|
max_age_secs: cli.max_age_secs.unwrap_or(config.max_age_secs),
|
||||||
));
|
},
|
||||||
}
|
})
|
||||||
|
}
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn test_syslog_priority_for_level_mapping() {
|
|
||||||
assert_eq!(
|
|
||||||
syslog_priority_for_level(&tracing::Level::ERROR),
|
|
||||||
libc::LOG_ERR
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
syslog_priority_for_level(&tracing::Level::WARN),
|
|
||||||
libc::LOG_WARNING
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
syslog_priority_for_level(&tracing::Level::INFO),
|
|
||||||
libc::LOG_INFO
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
syslog_priority_for_level(&tracing::Level::DEBUG),
|
|
||||||
libc::LOG_DEBUG
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
syslog_priority_for_level(&tracing::Level::TRACE),
|
|
||||||
libc::LOG_DEBUG
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Datelike, Duration as ChronoDuration, Utc};
|
||||||
|
|
||||||
|
use crate::config::LogRotation;
|
||||||
|
|
||||||
|
use super::FileLogOptions;
|
||||||
|
|
||||||
|
const CLEANUP_INTERVAL_SECS: i64 = 60;
|
||||||
|
|
||||||
|
/// File appender with size rotation and local retention cleanup.
|
||||||
|
pub(crate) struct BoundedFileAppender {
|
||||||
|
options: FileLogOptions,
|
||||||
|
dir: PathBuf,
|
||||||
|
base_name: String,
|
||||||
|
current_path: PathBuf,
|
||||||
|
current_size: u64,
|
||||||
|
last_cleanup: DateTime<Utc>,
|
||||||
|
file: Option<File>,
|
||||||
|
now: Box<dyn Fn() -> DateTime<Utc> + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundedFileAppender {
|
||||||
|
pub(crate) fn new(options: FileLogOptions) -> io::Result<Self> {
|
||||||
|
Self::with_now(options, Box::new(Utc::now))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_now(
|
||||||
|
options: FileLogOptions,
|
||||||
|
now: Box<dyn Fn() -> DateTime<Utc> + Send + Sync>,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
let path = Path::new(&options.path);
|
||||||
|
let dir = path
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.to_path_buf();
|
||||||
|
let base_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("telemt")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let start = now();
|
||||||
|
let current_path = active_path_for(&dir, &base_name, options.rotation, &start);
|
||||||
|
let (file, current_size) = open_append_file(¤t_path)?;
|
||||||
|
let mut appender = Self {
|
||||||
|
options,
|
||||||
|
dir,
|
||||||
|
base_name,
|
||||||
|
current_path,
|
||||||
|
current_size,
|
||||||
|
last_cleanup: start,
|
||||||
|
file: Some(file),
|
||||||
|
now,
|
||||||
|
};
|
||||||
|
appender.cleanup(&start);
|
||||||
|
Ok(appender)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now(&self) -> DateTime<Utc> {
|
||||||
|
(self.now)()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_active_path(&mut self, now: &DateTime<Utc>) -> io::Result<bool> {
|
||||||
|
let next_path = active_path_for(&self.dir, &self.base_name, self.options.rotation, now);
|
||||||
|
if next_path == self.current_path {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.close_current()?;
|
||||||
|
self.current_path = next_path;
|
||||||
|
self.open_current()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_for_size(&mut self, now: &DateTime<Utc>) -> io::Result<()> {
|
||||||
|
self.close_current()?;
|
||||||
|
if self.current_path.exists() {
|
||||||
|
let archive_path = self.archive_path(now);
|
||||||
|
fs::rename(&self.current_path, archive_path)?;
|
||||||
|
}
|
||||||
|
self.open_current()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn archive_path(&self, now: &DateTime<Utc>) -> PathBuf {
|
||||||
|
let file_name = self
|
||||||
|
.current_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or(&self.base_name);
|
||||||
|
let stamp = now.format("%Y%m%d%H%M%S");
|
||||||
|
for seq in 0..1000 {
|
||||||
|
let candidate = self.dir.join(format!("{file_name}.{stamp}.{seq}"));
|
||||||
|
if !candidate.exists() {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dir.join(format!("{file_name}.{stamp}.overflow"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_current(&mut self) -> io::Result<()> {
|
||||||
|
let (file, current_size) = open_append_file(&self.current_path)?;
|
||||||
|
self.file = Some(file);
|
||||||
|
self.current_size = current_size;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_current(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(mut file) = self.file.take() {
|
||||||
|
file.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_rotate_for_size(&self, incoming_len: usize) -> bool {
|
||||||
|
self.options.max_size_bytes > 0
|
||||||
|
&& self.current_size > 0
|
||||||
|
&& self.current_size.saturating_add(incoming_len as u64) > self.options.max_size_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_due(&self, now: &DateTime<Utc>) -> bool {
|
||||||
|
self.options.max_age_secs > 0
|
||||||
|
&& now.signed_duration_since(self.last_cleanup)
|
||||||
|
>= ChronoDuration::seconds(CLEANUP_INTERVAL_SECS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, now: &DateTime<Utc>) {
|
||||||
|
self.last_cleanup = now.clone();
|
||||||
|
let Ok(entries) = fs::read_dir(&self.dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
let prefix = format!("{}.", self.base_name);
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let Ok(file_type) = entry.file_type() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !file_type.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_current = path == self.current_path;
|
||||||
|
let Some(name) = entry.file_name().to_str().map(|name| name.to_string()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !is_current && !name.starts_with(&prefix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(metadata) = entry.metadata() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let modified = metadata.modified().unwrap_or(UNIX_EPOCH);
|
||||||
|
candidates.push(LogFileCandidate {
|
||||||
|
path,
|
||||||
|
modified,
|
||||||
|
is_current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.options.max_age_secs > 0 {
|
||||||
|
let cutoff = system_time_from_utc(now)
|
||||||
|
.checked_sub(Duration::from_secs(self.options.max_age_secs))
|
||||||
|
.unwrap_or(UNIX_EPOCH);
|
||||||
|
candidates.retain(|candidate| {
|
||||||
|
if candidate.is_current || candidate.modified >= cutoff {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
let _ = fs::remove_file(&candidate.path);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.options.max_files > 0 && candidates.len() > self.options.max_files {
|
||||||
|
let mut archives: Vec<_> = candidates
|
||||||
|
.into_iter()
|
||||||
|
.filter(|candidate| !candidate.is_current)
|
||||||
|
.collect();
|
||||||
|
archives.sort_by_key(|candidate| candidate.modified);
|
||||||
|
let mut total = archives.len() + 1;
|
||||||
|
for candidate in archives {
|
||||||
|
if total <= self.options.max_files {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let _ = fs::remove_file(candidate.path);
|
||||||
|
total -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for BoundedFileAppender {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let now = self.now();
|
||||||
|
let rotated_by_time = self.refresh_active_path(&now)?;
|
||||||
|
if self.should_rotate_for_size(buf.len()) {
|
||||||
|
self.rotate_for_size(&now)?;
|
||||||
|
self.cleanup(&now);
|
||||||
|
} else if rotated_by_time || self.cleanup_due(&now) {
|
||||||
|
self.cleanup(&now);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = self.file.as_mut() else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"bounded log file is not open",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
file.write_all(buf)?;
|
||||||
|
self.current_size = self.current_size.saturating_add(buf.len() as u64);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(file) = self.file.as_mut() {
|
||||||
|
file.flush()
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogFileCandidate {
|
||||||
|
path: PathBuf,
|
||||||
|
modified: SystemTime,
|
||||||
|
is_current: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_append_file(path: &Path) -> io::Result<(File, u64)> {
|
||||||
|
let mut options = OpenOptions::new();
|
||||||
|
options.create(true).append(true);
|
||||||
|
|
||||||
|
let file = match options.open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(error) => {
|
||||||
|
let Some(parent) = path.parent().filter(|parent| !parent.as_os_str().is_empty()) else {
|
||||||
|
return Err(error);
|
||||||
|
};
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
options.open(path)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let current_size = file.metadata()?.len();
|
||||||
|
Ok((file, current_size))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_path_for(
|
||||||
|
dir: &Path,
|
||||||
|
base_name: &str,
|
||||||
|
rotation: LogRotation,
|
||||||
|
now: &DateTime<Utc>,
|
||||||
|
) -> PathBuf {
|
||||||
|
match rotation {
|
||||||
|
LogRotation::Never => dir.join(base_name),
|
||||||
|
LogRotation::Minutely
|
||||||
|
| LogRotation::Hourly
|
||||||
|
| LogRotation::Daily
|
||||||
|
| LogRotation::Weekly => dir.join(format!(
|
||||||
|
"{base_name}.{}",
|
||||||
|
period_suffix_for(rotation, now)
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn period_suffix_for(rotation: LogRotation, now: &DateTime<Utc>) -> String {
|
||||||
|
match rotation {
|
||||||
|
LogRotation::Never | LogRotation::Daily => now.format("%Y-%m-%d").to_string(),
|
||||||
|
LogRotation::Hourly => now.format("%Y-%m-%d-%H").to_string(),
|
||||||
|
LogRotation::Minutely => now.format("%Y-%m-%d-%H-%M").to_string(),
|
||||||
|
LogRotation::Weekly => {
|
||||||
|
let days_since_sunday = now.weekday().num_days_from_sunday() as i64;
|
||||||
|
let week_start = now.date_naive() - ChronoDuration::days(days_since_sunday);
|
||||||
|
week_start.format("%Y-%m-%d").to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_time_from_utc(now: &DateTime<Utc>) -> SystemTime {
|
||||||
|
let duration = Duration::new(now.timestamp().unsigned_abs(), now.timestamp_subsec_nanos());
|
||||||
|
if now.timestamp() >= 0 {
|
||||||
|
UNIX_EPOCH + duration
|
||||||
|
} else {
|
||||||
|
UNIX_EPOCH - duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fixed_now() -> DateTime<Utc> {
|
||||||
|
DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_secs(10))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(path: PathBuf) -> FileLogOptions {
|
||||||
|
FileLogOptions {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
rotation: LogRotation::Never,
|
||||||
|
max_size_bytes: 0,
|
||||||
|
max_files: 0,
|
||||||
|
max_age_secs: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matching_logs(dir: &Path) -> Vec<PathBuf> {
|
||||||
|
let mut files: Vec<_> = fs::read_dir(dir)
|
||||||
|
.unwrap()
|
||||||
|
.flatten()
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.filter(|path| {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.map(|name| name.starts_with("telemt.log"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
files.sort();
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn size_rotation_keeps_latest_write_in_active_file() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let path = dir.path().join("telemt.log");
|
||||||
|
let mut options = options(path.clone());
|
||||||
|
options.max_size_bytes = 6;
|
||||||
|
|
||||||
|
let mut appender = BoundedFileAppender::with_now(options, Box::new(fixed_now)).unwrap();
|
||||||
|
appender.write_all(b"abc\n").unwrap();
|
||||||
|
appender.write_all(b"def\n").unwrap();
|
||||||
|
appender.flush().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read_to_string(path).unwrap(), "def\n");
|
||||||
|
assert_eq!(matching_logs(dir.path()).len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn max_files_retention_removes_oldest_archives() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let path = dir.path().join("telemt.log");
|
||||||
|
let mut options = options(path);
|
||||||
|
options.max_size_bytes = 4;
|
||||||
|
options.max_files = 2;
|
||||||
|
|
||||||
|
let mut appender = BoundedFileAppender::with_now(options, Box::new(fixed_now)).unwrap();
|
||||||
|
for line in [b"aa\n", b"bb\n", b"cc\n", b"dd\n"] {
|
||||||
|
appender.write_all(line).unwrap();
|
||||||
|
}
|
||||||
|
appender.flush().unwrap();
|
||||||
|
|
||||||
|
assert!(matching_logs(dir.path()).len() <= 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn max_age_retention_removes_old_archives() {
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let path = dir.path().join("telemt.log");
|
||||||
|
let old_archive = dir.path().join("telemt.log.20000101000000.0");
|
||||||
|
fs::write(&old_archive, "old").unwrap();
|
||||||
|
|
||||||
|
let c_path = CString::new(old_archive.as_os_str().as_bytes()).unwrap();
|
||||||
|
let times = [
|
||||||
|
libc::timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
libc::timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let rc = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
|
||||||
|
assert_eq!(rc, 0);
|
||||||
|
|
||||||
|
let mut options = options(path);
|
||||||
|
options.max_age_secs = 1;
|
||||||
|
let _appender = BoundedFileAppender::with_now(options, Box::new(fixed_now)).unwrap();
|
||||||
|
|
||||||
|
assert!(!old_archive.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_default() {
|
||||||
|
let args: Vec<String> = vec![];
|
||||||
|
let options = parse_log_cli_options(&args).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resolve_log_destination(&LoggingConfig::default(), &options).unwrap(),
|
||||||
|
LogDestination::Stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_file() {
|
||||||
|
let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()];
|
||||||
|
let options = parse_log_cli_options(&args).unwrap();
|
||||||
|
match resolve_log_destination(&LoggingConfig::default(), &options).unwrap() {
|
||||||
|
LogDestination::File { options } => {
|
||||||
|
assert_eq!(options.path, "/var/log/telemt.log");
|
||||||
|
assert_eq!(options.rotation, LogRotation::Never);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File destination"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_file_daily() {
|
||||||
|
let args = vec!["--log-file-daily=/var/log/telemt".to_string()];
|
||||||
|
let options = parse_log_cli_options(&args).unwrap();
|
||||||
|
match resolve_log_destination(&LoggingConfig::default(), &options).unwrap() {
|
||||||
|
LogDestination::File { options } => {
|
||||||
|
assert_eq!(options.path, "/var/log/telemt");
|
||||||
|
assert_eq!(options.rotation, LogRotation::Daily);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File destination"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_bounds() {
|
||||||
|
let args = vec![
|
||||||
|
"--log-file=/var/log/telemt.log".to_string(),
|
||||||
|
"--log-rotation=hourly".to_string(),
|
||||||
|
"--log-max-size-bytes=1024".to_string(),
|
||||||
|
"--log-max-files=3".to_string(),
|
||||||
|
"--log-max-age-secs=60".to_string(),
|
||||||
|
];
|
||||||
|
let options = parse_log_cli_options(&args).unwrap();
|
||||||
|
match resolve_log_destination(&LoggingConfig::default(), &options).unwrap() {
|
||||||
|
LogDestination::File { options } => {
|
||||||
|
assert_eq!(options.rotation, LogRotation::Hourly);
|
||||||
|
assert_eq!(options.max_size_bytes, 1024);
|
||||||
|
assert_eq!(options.max_files, 3);
|
||||||
|
assert_eq!(options.max_age_secs, 60);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File destination"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_rejects_bad_rotation() {
|
||||||
|
let args = vec!["--log-rotation=yearly".to_string()];
|
||||||
|
assert!(parse_log_cli_options(&args).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_cli_options_syslog() {
|
||||||
|
let args = vec!["--syslog".to_string()];
|
||||||
|
let options = parse_log_cli_options(&args).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resolve_log_destination(&LoggingConfig::default(), &options).unwrap(),
|
||||||
|
LogDestination::Syslog
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_syslog_priority_for_level_mapping() {
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::ERROR),
|
||||||
|
libc::LOG_ERR
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::WARN),
|
||||||
|
libc::LOG_WARNING
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::INFO),
|
||||||
|
libc::LOG_INFO
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::DEBUG),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::TRACE),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
}
|
||||||
+26
-7
@@ -9,7 +9,7 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::logging::LogDestination;
|
use crate::logging::LogCliOptions;
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
ProxyConfigData, fetch_proxy_config_with_raw_via_upstream, load_proxy_config_cache,
|
ProxyConfigData, fetch_proxy_config_with_raw_via_upstream, load_proxy_config_cache,
|
||||||
@@ -113,7 +113,7 @@ pub(crate) struct CliArgs {
|
|||||||
pub data_path: Option<PathBuf>,
|
pub data_path: Option<PathBuf>,
|
||||||
pub silent: bool,
|
pub silent: bool,
|
||||||
pub log_level: Option<String>,
|
pub log_level: Option<String>,
|
||||||
pub log_destination: LogDestination,
|
pub log_cli_options: LogCliOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_cli() -> CliArgs {
|
pub(crate) fn parse_cli() -> CliArgs {
|
||||||
@@ -125,8 +125,13 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
|
||||||
// Parse log destination
|
let log_cli_options = match crate::logging::parse_log_cli_options(&args) {
|
||||||
let log_destination = crate::logging::parse_log_destination(&args);
|
Ok(options) => options,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("[telemt] {error}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check for --init first (handled before tokio)
|
// Check for --init first (handled before tokio)
|
||||||
if let Some(init_opts) = cli::parse_init_args(&args) {
|
if let Some(init_opts) = cli::parse_init_args(&args) {
|
||||||
@@ -184,7 +189,16 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
s if s.starts_with("--log-file=") || s.starts_with("--log-file-daily=") => {}
|
s if s.starts_with("--log-file=") || s.starts_with("--log-file-daily=") => {}
|
||||||
#[cfg(unix)]
|
"--log-rotation"
|
||||||
|
| "--log-max-size-bytes"
|
||||||
|
| "--log-max-files"
|
||||||
|
| "--log-max-age-secs" => {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-rotation=")
|
||||||
|
|| s.starts_with("--log-max-size-bytes=")
|
||||||
|
|| s.starts_with("--log-max-files=")
|
||||||
|
|| s.starts_with("--log-max-age-secs=") => {}
|
||||||
"--syslog" => {}
|
"--syslog" => {}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
print_help();
|
print_help();
|
||||||
@@ -198,7 +212,8 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
"--daemon" | "-d" | "--foreground" | "-f" => {}
|
"--daemon" | "-d" | "--foreground" | "-f" => {}
|
||||||
s if s.starts_with("--pid-file") => {
|
s if s.starts_with("--pid-file") => {
|
||||||
if !s.contains('=') {
|
if !s.contains('=') {
|
||||||
i += 1; // skip value
|
// Skip the pid-file value consumed by daemon argument parsing.
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--run-as-user") => {
|
s if s.starts_with("--run-as-user") => {
|
||||||
@@ -230,7 +245,7 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
data_path,
|
data_path,
|
||||||
silent,
|
silent,
|
||||||
log_level,
|
log_level,
|
||||||
log_destination,
|
log_cli_options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +275,10 @@ fn print_help() {
|
|||||||
eprintln!("Logging options:");
|
eprintln!("Logging options:");
|
||||||
eprintln!(" --log-file <PATH> Log to file (default: stderr)");
|
eprintln!(" --log-file <PATH> Log to file (default: stderr)");
|
||||||
eprintln!(" --log-file-daily <PATH> Log to file with daily rotation");
|
eprintln!(" --log-file-daily <PATH> Log to file with daily rotation");
|
||||||
|
eprintln!(" --log-rotation <MODE> never|minutely|hourly|daily|weekly");
|
||||||
|
eprintln!(" --log-max-size-bytes N Rotate file logs when active file exceeds N bytes");
|
||||||
|
eprintln!(" --log-max-files N Keep at most N matching file logs (0 disables)");
|
||||||
|
eprintln!(" --log-max-age-secs N Remove rotated file logs older than N seconds");
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
eprintln!(" --syslog Log to syslog (Unix only)");
|
eprintln!(" --syslog Log to syslog (Unix only)");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|||||||
+9
-1
@@ -108,7 +108,7 @@ async fn run_telemt_core(
|
|||||||
let data_path = cli_args.data_path;
|
let data_path = cli_args.data_path;
|
||||||
let cli_silent = cli_args.silent;
|
let cli_silent = cli_args.silent;
|
||||||
let cli_log_level = cli_args.log_level;
|
let cli_log_level = cli_args.log_level;
|
||||||
let log_destination = cli_args.log_destination;
|
let log_cli_options = cli_args.log_cli_options;
|
||||||
let startup_cwd = match std::env::current_dir() {
|
let startup_cwd = match std::env::current_dir() {
|
||||||
Ok(cwd) => cwd,
|
Ok(cwd) => cwd,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -331,6 +331,14 @@ async fn run_telemt_core(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let initial_filter_spec = runtime_tasks::log_filter_spec(has_rust_log, &effective_log_level);
|
let initial_filter_spec = runtime_tasks::log_filter_spec(has_rust_log, &effective_log_level);
|
||||||
|
let log_destination =
|
||||||
|
match crate::logging::resolve_log_destination(&config.logging, &log_cli_options) {
|
||||||
|
Ok(destination) => destination,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("[telemt] {error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
let (filter_layer, filter_handle) =
|
let (filter_layer, filter_handle) =
|
||||||
reload::Layer::new(EnvFilter::new(initial_filter_spec.clone()));
|
reload::Layer::new(EnvFilter::new(initial_filter_spec.clone()));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
|
|||||||
Reference in New Issue
Block a user