Fix/field hints (#666)

* Added missing field hints
* logger intiialization fixed
This commit is contained in:
euzu
2026-03-25 09:12:03 +01:00
committed by GitHub
parent db53da5df4
commit 2b8a4cb90f
4 changed files with 94 additions and 90 deletions
+4 -10
View File
@@ -22,9 +22,7 @@ use crate::{
model::{AppConfig, Config, Healthcheck, HealthcheckConfig, ProcessTargets, SourcesConfig},
processing::processor::exec_processing,
repository::run_startup_migrations,
utils::{
apply_config_log_level, config_file_reader, db_viewer, init_logger, request::create_client, resolve_env_var,
},
utils::{config_file_reader, db_viewer, init_logger, request::create_client, resolve_env_var},
};
use arc_swap::{access::Access, ArcSwap};
use chrono::{DateTime, Utc};
@@ -145,10 +143,6 @@ const BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP");
async fn main() {
let args = Args::parse();
// initialize the logger from CLI arg / TULIPROX_LOG env var.
// Must happen before everything or we miss log output.
init_logger(args.log_level.as_deref());
db_viewer(&args.db_viewer_args());
if args.genpwd {
@@ -161,8 +155,7 @@ async fn main() {
let mut config_paths = get_file_paths(&args);
// Phase 2: apply config-file log level if not already set by CLI / env.
apply_config_log_level(args.log_level.as_deref(), config_paths.config_file_path.as_str());
init_logger(args.log_level.as_deref(), config_paths.config_file_path.as_str());
if args.healthcheck {
let healthy = healthcheck(config_paths.config_file_path.as_str()).await;
@@ -318,7 +311,8 @@ fn get_file_paths(args: &Args) -> ConfigPaths {
match utils::read_config_file(&config_file, true, false) {
Ok(cfg) => resolve_storage_path(&home_path, cfg.storage_dir.as_deref()),
Err(err) => {
exit!("Can't read config file {config_file} while resolving storage path: {err}")
eprintln!("Can't read config file {config_file} while resolving storage path: {err}");
std::process::exit(1);
}
}
} else {
+10 -1
View File
@@ -1,6 +1,7 @@
use crate::api::model::{MetadataRetryDbKey, MetadataRetryDbValue};
use crate::repository::{BPlusTreeDiskIterator, BPlusTreeQuery, VirtualIdRecord};
use log::error;
use env_logger::{Builder, Target};
use log::{error, LevelFilter};
use serde::{Deserialize, Serialize};
use shared::model::{EpgChannel, M3uPlaylistItem, XtreamPlaylistItem};
use std::io::Write;
@@ -75,6 +76,8 @@ pub fn db_viewer(args: &DbViewerArgs<'_>) {
return;
}
init_db_viewer_logger();
let mut any_processed = false;
for request in requests {
if let Some(filename) = request.filename {
@@ -90,6 +93,12 @@ pub fn db_viewer(args: &DbViewerArgs<'_>) {
}
}
fn init_db_viewer_logger() {
let mut log_builder = Builder::from_default_env();
log_builder.target(Target::Stderr);
log_builder.filter_level(LevelFilter::Info);
let _ = log_builder.try_init();
}
fn try_dump_typed_db<K, V>(path: &Path) -> bool
where
+41 -77
View File
@@ -2,7 +2,7 @@ use crate::model::LogLevelConfig;
use crate::utils::config_file_reader;
use chrono::{Local, Offset, SecondsFormat};
use env_logger::{Builder, Target};
use log::{error, info, LevelFilter};
use log::{info, LevelFilter};
use std::fs::File;
use std::io::Write;
@@ -27,104 +27,68 @@ fn get_log_level(log_level: &str) -> LevelFilter {
}
}
fn apply_log_format(builder: &mut Builder) {
builder.format(|buf, record| {
pub fn init_logger(user_log_level: Option<&str>, config_file: &str) {
// tracing_subscriber::registry()
// .with(console_subscriber::spawn()) // Console layer
// .with(EnvFilter::from_default_env())
// .with(fmt::layer()) // stdout logging
// .init();
let env_log_level = std::env::var("TULIPROX_LOG").ok();
let mut log_builder = Builder::from_default_env();
log_builder.target(Target::Stdout);
log_builder.format(move |buf, record| {
let now = Local::now();
let timestamp = now.to_rfc3339_opts(SecondsFormat::Secs, now.offset().fix().local_minus_utc() == 0);
writeln!(buf, "[{timestamp} {} {}] {}", record.level(), record.target(), record.args())
writeln!(
buf,
"[{timestamp} {} {}] {}",
record.level(),
record.target(),
record.args()
)
});
}
fn apply_level_spec(log_builder: &mut Builder, log_level: &str) -> (LevelFilter, Vec<String>) {
// priority CLI-Argument, Env-Var, Config, Default
let log_level = user_log_level
.map(std::string::ToString::to_string) // cli-argument
.or(env_log_level) // env
.or_else(|| { // config
File::open(config_file).ok()
.and_then(|file| serde_saphyr::from_reader::<_, LogLevelConfig>(config_file_reader(file, true))
.map_err(|e| eprintln!("Failed to parse log config file: {e}"))
.ok())
.and_then(|cfg| cfg.log.and_then(|l| l.log_level))
})
.unwrap_or_else(|| "info".to_string()); // Default
let mut log_levels = vec![];
let effective_max_level;
if log_level.contains('=') {
let mut max_level = LevelFilter::Off;
for pair in log_level.split(',') {
if pair.contains('=') {
let mut kv_iter = pair.split('=').map(str::trim);
if let (Some(module), Some(level)) = (kv_iter.next(), kv_iter.next()) {
let module_level = get_log_level(level);
log_levels.push(format!("{module}={module_level}"));
log_builder.filter_module(module, module_level);
if module_level > max_level {
max_level = module_level;
}
let log_level = get_log_level(level);
log_levels.push(format!("{module}={log_level}"));
log_builder.filter_module(module, log_level);
}
} else {
let level = get_log_level(pair);
log_levels.push(level.to_string());
log_builder.filter_level(level);
if level > max_level {
max_level = level;
}
}
}
effective_max_level = max_level;
} else {
effective_max_level = get_log_level(log_level);
log_builder.filter_level(effective_max_level);
log_levels.push(log_level.to_string());
// Set the log level based on the parsed value
log_builder.filter_level(get_log_level(&log_level));
log_levels.push(log_level);
}
(effective_max_level, log_levels)
}
/// Initialize the real logger using CLI argument and `TULIPROX_LOG` env var only.
/// Call this BEFORE config paths are resolved so that all `log::error!` / `exit!` calls
/// during path resolution are visible.
/// Config-file log level is applied later via `apply_config_log_level`.
pub fn init_logger(user_log_level: Option<&str>) {
let env_log_level = std::env::var("TULIPROX_LOG").ok();
let log_level = user_log_level
.map(std::string::ToString::to_string)
.or(env_log_level)
.unwrap_or_else(|| "info".to_string());
let mut log_builder = Builder::from_default_env();
log_builder.target(Target::Stdout);
apply_log_format(&mut log_builder);
let (effective_max_level, log_levels) = apply_level_spec(&mut log_builder, &log_level);
for module in LOG_ERROR_LEVEL_MOD {
log_builder.filter_module(module, LevelFilter::Error);
}
log_builder.init(); // panics if called twice — correct, we only call this once
log::set_max_level(effective_max_level);
let _ = log_builder.try_init();
info!("Log timezone system localtime (TZ)");
info!("Log Level {}", &log_levels.join(", "));
}
/// Apply the log level from the config file if no CLI argument or env var
/// overrides it. Uses `log::set_max_level` so it works after `init_logger` has
/// already registered the `env_logger` instance.
/// Call this AFTER config paths are resolved.
pub fn apply_config_log_level(user_log_level: Option<&str>, config_file: &str) {
let env_log_level = std::env::var("TULIPROX_LOG").ok();
// Only read from config if neither CLI nor env overrides the level.
if user_log_level.is_some() || env_log_level.is_some() {
return;
}
let Some(log_level) = File::open(config_file)
.ok()
.and_then(|file| {
serde_saphyr::from_reader::<_, LogLevelConfig>(config_file_reader(file, true))
.map_err(|e| error!("Failed to parse log config file: {e}"))
.ok()
})
.and_then(|cfg| cfg.log.and_then(|l| l.log_level))
else {
return;
};
// Build a temporary builder just to parse the level spec (no try_init).
let mut dummy = Builder::from_default_env();
let (effective_max_level, log_levels) = apply_level_spec(&mut dummy, &log_level);
log::set_max_level(effective_max_level);
info!("Log Level updated from config: {}", &log_levels.join(", "));
}
+39 -2
View File
@@ -337,6 +337,10 @@
"DISCORD": {
"TEMPLATE_MESSAGE": "Template message body for Discord notifications."
},
"DISCORD_MESSAGING_CONFIG": {
"TEMPLATES": "Handlebars templates for Discord message bodies.",
"URL": "Discord webhook URL for sending notification messages."
},
"DNS_MAX_ADDRS": "Optional limit of resolved IP addresses kept per hostname. Leave empty for no explicit limit.",
"EPG_CONFIG": {
"NAME_PREFIX_SEPARATOR": "List of characters (e.g. ':', '|') that separate channel prefixes from the main title during Smart Match.",
@@ -366,6 +370,20 @@
"PROPRIETARY_DISCOVERY": "Enables proprietary HDHomeRun discovery protocols.",
"SSDP_DISCOVERY": "Enables standard SSDP discovery for HDHomeRun emulation on the local network."
},
"HD_HOME_RUN_DEVICE_CONFIG": {
"DEVICE_ID": "Unique device ID used for HDHomeRun identification. Auto-generated if not set.",
"DEVICE_TYPE": "Device type string advertised to clients.",
"DEVICE_UDN": "Unique device name (UUID) for this virtual HDHomeRun device. Auto-generated if not set.",
"FIRMWARE_NAME": "Firmware filename advertised to HDHomeRun clients.",
"FIRMWARE_VERSION": "Firmware version string advertised to HDHomeRun clients.",
"FRIENDLY_NAME": "Human-readable display name shown to HDHomeRun clients.",
"MANUFACTURER": "Manufacturer name advertised to HDHomeRun clients.",
"MODEL_NAME": "Model name string advertised to HDHomeRun clients.",
"MODEL_NUMBER": "Model number string advertised to HDHomeRun clients.",
"NAME": "Internal identifier for this virtual HDHomeRun device. Must be unique.",
"PORT": "HTTP port this virtual device listens on. Auto-assigned if 0.",
"TUNER_COUNT": "Number of virtual tuners advertised by this device. Minimum 1."
},
"HD_HOME_RUN_TARGET_OUTPUT": {
"DEVICE": "The virtual Device ID exposed by the HDHomeRun emulation.",
"USERNAME": "Login username for this account or service."
@@ -400,6 +418,7 @@
},
"LIBRARY_METADATA_READ_CONFIG": {
"JELLYFIN": "Reads NFO metadata files formatted for Jellyfin.",
"KODI": "Reads NFO metadata files formatted for Kodi.",
"PLEX": "Reads XML metadata files or sidecars formatted for Plex."
},
"LIBRARY_PLAYLIST_CONFIG": {
@@ -436,6 +455,7 @@
},
"MESSAGING_CONFIG": {
"NOTIFY_ON": "Select events (e.g. 'info', 'stats', 'error') that trigger notifications for this specific type.",
"REST_HEADERS": "Custom HTTP headers sent with REST notification requests (JSON format).",
"TELEGRAM_CHAT_IDS": "List of telegram chat IDs to receive notifications. Format: '<chat_id>' or '<chat_id>:<thread_id>'."
},
"METADATA_LOG_CONFIG": {
@@ -445,7 +465,7 @@
"SANITIZE_SENSITIVE_INFO": "Attempts to mask passwords and tokens in the logging output for security."
},
"METADATA_UPDATE_CONFIG": {
"METADATA_PATH": "Directory for metadata cache and retry-state files.\n\nRelative paths are resolved against `storage_dir`.\n\nExamples: `metadata`, `/app/data/metadata`.",
"CACHE_PATH": "Directory for metadata cache and retry-state files.\n\nRelative paths are resolved against `storage_dir`.\n\nExamples: `metadata`, `/app/data/metadata`.",
"MAX_QUEUE_SIZE": "Maximum number of metadata tasks kept in memory for one input worker.\n\nWhen this limit is reached, additional tasks are rejected to protect system stability and memory usage.",
"RETRY_DELAY": "Minimum delay for transient requeue scheduling.\n\nUse this to prevent immediate hot-loop retries when tasks are temporarily deferred.\n\nUse duration units like `2s`, `5s`, `10s`.",
"WORKER_IDLE_TIMEOUT": "How long a metadata worker waits for new tasks while idle before it can stop itself.\n\nA lower value releases resources sooner; a higher value keeps workers warm for bursty workloads.\n\nUse duration units like `30s`, `1m`, `5m`.",
@@ -479,6 +499,11 @@
"KEEP_VHOST": "If enabled, keeps the original hostname in the Host header while connecting by resolved IP.",
"REFRESH_SECS": "DNS refresh interval in seconds for provider hostnames (minimum effective value is 10)."
},
"PUSHOVER_MESSAGING_CONFIG": {
"TOKEN": "Pushover application API token.",
"URL": "Optional custom Pushover API URL. Leave empty to use the default.",
"USER": "Pushover user key identifying the notification recipient."
},
"PROVIDER_FORM": {
"URLS": "List of base URLs for this provider. They are used in order and rotated when failover is triggered.",
"DNS": {
@@ -490,6 +515,7 @@
},
"PROBE": "Probe controls technical stream analysis retries.\n\nUse this group to define retry steps, retry limits, and cooldown behavior for FFprobe-based stream checks.",
"PROBE_CONFIG": {
"BACKOFF_JITTER_PERCENT": "Random jitter added to probe retry delays.\n\nA value of 20 means delays vary by ±20%. Helps spread retries across workers to avoid thundering-herd problems.\n\nMust be between 0 and 95.",
"COOLDOWN": "Pause time after probe retries are exhausted.\n\nDuring cooldown, that item is skipped for probing. After cooldown ends, probing can start again.\n\nExamples: `1d`, `3d`, `7d`.",
"MAX_ATTEMPTS": "How many probe tries are allowed before cooldown starts.\n\nLower values reduce provider load, higher values can recover more transient failures.",
"RETRY_BACKOFF_STEP_1": "Wait time after the first probe failure.\n\nUse this as a gentle first retry step.\n\nExample: `10m`.",
@@ -535,7 +561,10 @@
"BACKOFF_MULTIPLIER": "Multiplies the base backoff after each failed attempt to create an exponential delay."
},
"REST_MESSAGING_CONFIG": {
"HEADERS": "Custom HTTP headers to send with the request (JSON format)."
"HEADERS": "Custom HTTP headers to send with the request (JSON format).",
"METHOD": "HTTP method used for the notification request (e.g. POST, PUT). Default: POST.",
"TEMPLATES": "Handlebars templates for REST notification message bodies.",
"URL": "Target URL that receives the notification webhook."
},
"REVERSE_PROXY_CONFIG": {
"FAILOVER_REDIRECT_PATTERNS": "Regex patterns determining which HTTP error responses trigger an automatic failover rotation.",
@@ -589,6 +618,14 @@
"PROCESSING_ORDER": "Determines the priority or the sequence in which target channels are matched and listed."
},
"TELEGRAM_CONFIG": {
"BOT_TOKEN": "Telegram Bot API token. Obtain this from @BotFather.",
"MARKDOWN": "If enabled, messages are sent as MarkdownV2 formatted text.",
"TEMPLATES": "Handlebars templates for Telegram message bodies."
},
"TELEGRAM_MESSAGING_CONFIG": {
"BOT_TOKEN": "Telegram Bot API token. Obtain this from @BotFather.",
"CHAT_IDS": "List of Telegram chat IDs to receive notifications. Format: '<chat_id>' or '<chat_id>:<thread_id>'.",
"MARKDOWN": "If enabled, messages are sent as MarkdownV2 formatted text.",
"TEMPLATES": "Handlebars templates for Telegram message bodies."
},
"TMDB_CONFIG": {