1+ //! Logging config and utilities
2+ //!
3+ //! This module is only used by the main binary and provides logging config structures and setup
4+ //! helper functions
5+
6+ mod defaults;
7+ mod log_rotation_kind;
8+ mod parsers;
9+
10+ use log_rotation_kind:: LogRotationKind ;
111use schemars:: JsonSchema ;
212use serde:: Deserialize ;
13+ use std:: path:: PathBuf ;
314use tracing:: Level ;
15+ use tracing_appender:: non_blocking:: WorkerGuard ;
16+ use tracing_appender:: rolling:: RollingFileAppender ;
17+ use tracing_subscriber:: EnvFilter ;
18+ use tracing_subscriber:: fmt:: writer:: BoxMakeWriter ;
19+ use tracing_subscriber:: layer:: SubscriberExt ;
20+ use tracing_subscriber:: util:: SubscriberInitExt ;
21+
22+ use super :: Config ;
423
524/// Logging related options
625#[ derive( Debug , Deserialize , JsonSchema ) ]
@@ -10,61 +29,103 @@ pub struct Logging {
1029 default = "defaults::log_level" ,
1130 deserialize_with = "parsers::from_str"
1231 ) ]
13- #[ schemars( schema_with = "super::schemas:: level" ) ]
32+ #[ schemars( schema_with = "level" ) ]
1433 pub level : Level ,
34+
35+ /// The output path to use for logging
36+ #[ serde( default ) ]
37+ pub path : Option < PathBuf > ,
38+
39+ /// Log file rotation period to use when log file path provided
40+ /// [default: Hourly]
41+ #[ serde( default = "defaults::default_rotation" ) ]
42+ pub rotation : LogRotationKind ,
1543}
1644
1745impl Default for Logging {
1846 fn default ( ) -> Self {
1947 Self {
2048 level : defaults:: log_level ( ) ,
49+ path : None ,
50+ rotation : defaults:: default_rotation ( ) ,
2151 }
2252 }
2353}
2454
25- mod defaults {
26- use tracing:: Level ;
27-
28- pub ( super ) const fn log_level ( ) -> Level {
29- Level :: INFO
30- }
31- }
55+ impl Logging {
56+ pub fn setup ( config : & Config ) -> Result < Option < WorkerGuard > , anyhow:: Error > {
57+ let mut env_filter =
58+ EnvFilter :: from_default_env ( ) . add_directive ( config. logging . level . into ( ) ) ;
3259
33- mod parsers {
34- use std:: { fmt:: Display , marker:: PhantomData , str:: FromStr } ;
35-
36- use serde:: Deserializer ;
60+ if config. logging . level == Level :: INFO {
61+ env_filter = env_filter
62+ . add_directive ( "rmcp=warn" . parse ( ) ?)
63+ . add_directive ( "tantivy=warn" . parse ( ) ?) ;
64+ }
3765
38- pub ( super ) fn from_str < ' de , D , T > ( deserializer : D ) -> Result < T , D :: Error >
39- where
40- D : Deserializer < ' de > ,
41- T : FromStr ,
42- <T as FromStr >:: Err : Display ,
43- {
44- struct FromStrVisitor < Inner > {
45- _phantom : PhantomData < Inner > ,
66+ macro_rules! log_error {
67+ ( ) => {
68+ |e| eprintln!( "Failed to setup logging: {e:?}" )
69+ } ;
4670 }
47- impl < Inner > serde:: de:: Visitor < ' _ > for FromStrVisitor < Inner >
48- where
49- Inner : FromStr ,
50- <Inner as FromStr >:: Err : Display ,
51- {
52- type Value = Inner ;
5371
54- fn expecting ( & self , formatter : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
55- formatter. write_str ( "a string" )
56- }
72+ let ( writer, guard, with_ansi) = match config. logging . path . clone ( ) {
73+ Some ( path) => std:: fs:: create_dir_all ( & path)
74+ . map ( |_| path)
75+ . inspect_err ( log_error ! ( ) )
76+ . ok ( )
77+ . and_then ( |path| {
78+ RollingFileAppender :: builder ( )
79+ . rotation ( config. logging . rotation . clone ( ) . into ( ) )
80+ . filename_prefix ( "apollo_mcp_server" )
81+ . filename_suffix ( "log" )
82+ . build ( path)
83+ . inspect_err ( log_error ! ( ) )
84+ . ok ( )
85+ } )
86+ . map ( |appender| {
87+ let ( non_blocking_appender, guard) = tracing_appender:: non_blocking ( appender) ;
88+ (
89+ BoxMakeWriter :: new ( non_blocking_appender) ,
90+ Some ( guard) ,
91+ false ,
92+ )
93+ } )
94+ . unwrap_or_else ( || {
95+ eprintln ! ( "Log file setup failed - falling back to stderr" ) ;
96+ ( BoxMakeWriter :: new ( std:: io:: stderr) , None , true )
97+ } ) ,
98+ None => ( BoxMakeWriter :: new ( std:: io:: stdout) , None , true ) ,
99+ } ;
57100
58- fn visit_str < E > ( self , v : & str ) -> Result < Self :: Value , E >
59- where
60- E : serde:: de:: Error ,
61- {
62- Inner :: from_str ( v) . map_err ( |e| serde:: de:: Error :: custom ( e. to_string ( ) ) )
63- }
64- }
101+ tracing_subscriber:: registry ( )
102+ . with ( env_filter)
103+ . with (
104+ tracing_subscriber:: fmt:: layer ( )
105+ . with_writer ( writer)
106+ . with_ansi ( with_ansi)
107+ . with_target ( false ) ,
108+ )
109+ . init ( ) ;
65110
66- deserializer. deserialize_str ( FromStrVisitor {
67- _phantom : PhantomData ,
68- } )
111+ Ok ( guard)
69112 }
70113}
114+
115+ fn level ( generator : & mut schemars:: SchemaGenerator ) -> schemars:: Schema {
116+ /// Log level
117+ #[ derive( JsonSchema ) ]
118+ #[ schemars( rename_all = "lowercase" ) ]
119+ // This is just an intermediate type to auto create schema information for,
120+ // so it is OK if it is never used
121+ #[ allow( dead_code) ]
122+ enum Level {
123+ Trace ,
124+ Debug ,
125+ Info ,
126+ Warn ,
127+ Error ,
128+ }
129+
130+ Level :: json_schema ( generator)
131+ }
0 commit comments