1#![warn(missing_docs)]
43#![warn(clippy::pedantic)]
44#![allow(clippy::module_name_repetitions)]
45#![allow(clippy::missing_errors_doc)]
46#![allow(clippy::missing_panics_doc)]
47
48pub mod cache;
49pub mod cli;
50pub mod config;
51pub mod config_reload;
52pub mod error;
53pub mod metrics;
54pub mod server;
55pub mod tools;
56pub mod utils;
57
58pub use crate::config::{
59 AppConfig, EnvAppConfig, EnvLoggingConfig, EnvServerConfig, LoggingConfig, PerformanceConfig,
60 ServerConfig,
61};
62pub use crate::error::{Error, Result};
64pub use crate::server::CratesDocsServer;
66
67pub const VERSION: &str = env!("CARGO_PKG_VERSION");
71
72pub const NAME: &str = "crates-docs";
74
75pub const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
79
80#[must_use]
86pub fn user_agent() -> String {
87 if REPOSITORY.is_empty() {
88 format!("CratesDocsMCP/{VERSION}")
89 } else {
90 format!("CratesDocsMCP/{VERSION} ({REPOSITORY})")
91 }
92}
93
94pub fn init_logging_with_config(config: &crate::config::LoggingConfig) -> Result<()> {
99 use tracing_subscriber::{fmt, prelude::*, EnvFilter};
100
101 macro_rules! fmt_layer {
103 () => {
104 fmt::layer()
105 .with_writer(std::io::stderr)
106 .with_target(true)
107 .with_thread_ids(true)
108 .with_thread_names(true)
109 .compact()
110 };
111 ($writer:expr) => {
112 fmt::layer()
113 .with_writer($writer)
114 .with_target(true)
115 .with_thread_ids(true)
116 .with_thread_names(true)
117 .compact()
118 };
119 }
120
121 macro_rules! try_init {
123 ($subscriber:expr) => {
124 $subscriber
125 .try_init()
126 .map_err(|e| error::Error::initialization("logging", e.to_string()))?
127 };
128 }
129
130 let level = config.level.to_lowercase();
132 let level = match level.as_str() {
133 "trace" | "debug" | "warn" | "error" => level.clone(),
134 _ => "info".to_string(),
135 };
136
137 let filter = EnvFilter::new(level);
138
139 match (config.enable_console, config.enable_file, &config.file_path) {
141 (true, true, Some(file_path)) => {
142 let (log_dir, log_file_name) = parse_log_path(file_path);
144 ensure_log_directory(&log_dir)?;
145 let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
146
147 try_init!(tracing_subscriber::registry()
148 .with(filter)
149 .with(fmt_layer!())
150 .with(fmt_layer!(file_appender)));
151 }
152
153 (false, true, Some(file_path)) => {
154 let (log_dir, log_file_name) = parse_log_path(file_path);
156 ensure_log_directory(&log_dir)?;
157 let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
158
159 try_init!(tracing_subscriber::registry()
160 .with(filter)
161 .with(fmt_layer!(file_appender)));
162 }
163
164 _ => {
166 try_init!(tracing_subscriber::registry()
167 .with(filter)
168 .with(fmt_layer!()));
169 }
170 }
171
172 Ok(())
173}
174
175fn parse_log_path(file_path: &str) -> (std::path::PathBuf, std::ffi::OsString) {
177 let path = std::path::Path::new(file_path);
178 let log_dir = path
179 .parent()
180 .filter(|p| !p.as_os_str().is_empty())
181 .map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
182 let log_file_name = path.file_name().map_or_else(
183 || std::ffi::OsString::from("crates-docs.log"),
184 std::ffi::OsString::from,
185 );
186 (log_dir, log_file_name)
187}
188
189fn ensure_log_directory(log_dir: &std::path::Path) -> Result<()> {
191 std::fs::create_dir_all(log_dir).map_err(|e| {
192 error::Error::initialization("log_directory", format!("Failed to create: {e}"))
193 })
194}