(* $Id: netplex_log.ml 1588 2011-04-28 13:59:54Z gerd $ *) open Printf type level = [ `Emerg | `Alert | `Crit | `Err | `Warning | `Notice | `Info | `Debug ] let level_weight = Netlog.level_weight let level_of_string = Netlog.level_of_string let string_of_level = Netlog.string_of_level let level_names = Netlog.level_names class type generic_config = object method log_format : string method log_component : string method log_subchannel : string method log_max_level : level end let std_fmt = "[${timestamp}] [${component}] [${level}] ${message}" class default_generic_config = object method log_format = std_fmt method log_component = "*" method log_subchannel = "*" method log_max_level = (`Debug : level) end class type logger = object method log_subch : component:string -> subchannel:string -> level:level -> message:string -> unit method log : component:string -> level:level -> message:string -> unit method reopen : unit -> unit end let format_message fmt component subchannel level message = let t = Unix.time() in let nd = Netdate.create ~zone:(Netdate.localzone) t in let b = Buffer.create 100 in Buffer.add_substitute b (fun var -> match var with | "timestamp" -> Netdate.format "%c" nd | "timestamp:unix" -> sprintf "%.0f" t | "component" -> component | "subchannel" -> subchannel | "level" -> string_of_level level | "message" -> message | _ -> if (String.length var >= 10 && String.sub var 0 10 = "timestamp:") then let nd_fmt = String.sub var 10 (String.length var - 10) in Netdate.format nd_fmt nd else "" ) fmt; Buffer.contents b let ast_re = Netstring_str.regexp "[*]";; let regexp_of_pattern s = let l = Netstring_str.split_delim ast_re s in Netstring_str.regexp (String.concat ".*" (List.map (fun u -> Netstring_str.quote u) l) ^ "$") let mk_filter (gc:generic_config) = let comp_re = regexp_of_pattern gc#log_component in let subch_re = regexp_of_pattern gc#log_subchannel in fun component subchannel level -> level_weight level <= level_weight gc#log_max_level && ( (Netstring_str.string_match comp_re component 0 <> None) && (Netstring_str.string_match subch_re subchannel 0 <> None) ) class channel_logger (gc:generic_config) out : logger = let filter = mk_filter gc in object(self) method log_subch ~component ~subchannel ~level ~message = if filter component subchannel level then ( let fmt = gc#log_format in try fprintf out "%s\n%!" (format_message fmt component subchannel level message) with | error -> prerr_endline ("Netplex Catastrophic Error: Unable to write to log channel: " ^ Netexn.to_string error) ) method log = self # log_subch ~subchannel:"" method reopen() = () end let channel_logger = new channel_logger (new default_generic_config) let channel_logger_from_obj = new channel_logger class extract_generic_config cf addr = let fmt = try cf # string_param (cf # resolve_parameter addr "format") with Not_found -> std_fmt in let component = try cf # string_param (cf # resolve_parameter addr "component") with Not_found -> "*" in let subchannel = try cf # string_param (cf # resolve_parameter addr "subchannel") with Not_found -> "*" in let max_level_str = try cf # string_param (cf # resolve_parameter addr "max_level") with Not_found -> "all" in let max_level = try if String.lowercase(max_level_str) = "all" then `Debug else (level_of_string max_level_str) with | _ -> failwith ("In section " ^ cf # print addr ^ ": Bad max_level parameter value: " ^ max_level_str) in object method log_format = fmt method log_component = component method log_subchannel = subchannel method log_max_level = max_level end let stderr_logger_factory = object method name = "stderr" method create_logger cf addr _ = cf # restrict_subsections addr []; cf # restrict_parameters addr [ "type"; "format"; "component"; "subchannel"; "max_level" ]; let gc = new extract_generic_config cf addr in channel_logger_from_obj gc stderr end class file_logger (gc:generic_config) file : logger = let filter = mk_filter gc in object(self) val mutable out = open_out_gen [ Open_wronly; Open_append; Open_creat ] 0o666 file initializer Netsys_posix.register_post_fork_handler (object method name = "Netplex_log.file_logger" method run() = self # post_fork() end ) method log_subch ~component ~subchannel ~level ~message = if filter component subchannel level then ( let fmt = gc#log_format in try fprintf out "%s\n%!" (format_message fmt component subchannel level message) with | error -> prerr_endline ("Netplex Catastrophic Error: Unable to write to log file " ^ file ^ ": " ^ Netexn.to_string error) ) method log = self # log_subch ~subchannel:"" method reopen() = close_out out; try out <- open_out_gen [ Open_wronly; Open_append; Open_creat ] 0o666 file with | error -> prerr_endline ("Netplex Catastrophic Error: Unable to reopen log file " ^ file ^ ": " ^ Netexn.to_string error) method private post_fork() = close_out out end let file_logger = new file_logger (new default_generic_config) let file_logger_from_obj = new file_logger let file_logger_factory = object method name = "file" method create_logger cf addr _ = cf # restrict_subsections addr []; cf # restrict_parameters addr [ "type"; "file"; "format"; "component"; "subchannel"; "max_level" ]; let fileaddr = try cf # resolve_parameter addr "file" with | Not_found -> failwith ("File logger needs parameter 'file'") in let file = cf # string_param fileaddr in let gc = new extract_generic_config cf addr in file_logger_from_obj gc file end class type multi_file_config = object inherit generic_config method log_directory : string method log_files : (string * string * [ level | `All ] * string * string) list end let no_duplicates l = let h = Hashtbl.create 10 in List.filter (fun p -> not(Hashtbl.mem h p) && ( Hashtbl.add h p (); true)) l ;; class multi_file_logger (mfc : multi_file_config) : logger = let log_files = List.map (fun (comp_pat, subch_pat, level, file, fmt) -> let comp_re = regexp_of_pattern comp_pat in let subch_re = regexp_of_pattern subch_pat in (comp_re, subch_re, level, file, fmt) ) mfc#log_files in let filter = mk_filter (mfc :> generic_config) in object(self) val channels = Hashtbl.create 10 (* Maps files to channels *) initializer Netsys_posix.register_post_fork_handler (object method name = "Netplex_log.multi_file_logger" method run() = self # post_fork() end ) method log_subch ~component ~subchannel ~level ~message = if filter component subchannel level then ( let w = level_weight level in let files = List.map (fun (_, _, _, file, fmt) -> (file, fmt)) (List.filter (fun (comp_re, subch_re, level_pat, _, _) -> match Netstring_str.string_match comp_re component 0 with | Some _ -> ( match Netstring_str.string_match subch_re subchannel 0 with | Some _ -> ( match level_pat with | `All -> true | #level as l -> w <= level_weight l ) | None -> false ) | None -> false ) log_files ) in let files = no_duplicates files in List.iter (fun (file, fmt) -> let full_path = if file <> "/" && file.[0] = '/' then file else Filename.concat mfc#log_directory file in try let ch = try Hashtbl.find channels full_path with | Not_found -> let ch = open_out_gen [ Open_wronly; Open_append; Open_creat ] 0o666 full_path in Hashtbl.add channels full_path ch; ch in fprintf ch "%s\n%!" (format_message fmt component subchannel level message) with | error -> prerr_endline ("Netplex Catastrophic Error: Unable to write to log file " ^ full_path ^ ": " ^ Netexn.to_string error) ) files ) method log = self # log_subch ~subchannel:"" method reopen() = Hashtbl.iter (fun name ch -> close_out ch) channels; Hashtbl.clear channels method private post_fork() = self # reopen() end let multi_file_logger = new multi_file_logger let multi_file_logger_factory = object method name = "multi_file" method create_logger cf addr _ = cf # restrict_subsections addr [ "file" ]; cf # restrict_parameters addr [ "type"; "format"; "component"; "subchannel"; "max_level"; "directory" ]; let diraddr = try cf # resolve_parameter addr "directory" with | Not_found -> failwith ("Multi-file logger needs parameter 'directory'") in let dir = cf # string_param diraddr in let gc = new extract_generic_config cf addr in let log_files = List.map (fun addr -> cf # restrict_subsections addr []; cf # restrict_parameters addr [ "component"; "subchannel"; "max_level"; "file"; "format" ]; let component = try cf # string_param (cf # resolve_parameter addr "component") with | Not_found -> "*" in let subchannel = try cf # string_param (cf # resolve_parameter addr "subchannel") with | Not_found -> "*" in let max_level_str = try cf # string_param (cf # resolve_parameter addr "max_level") with | Not_found -> "all" in let max_level = try if String.lowercase(max_level_str) = "all" then `All else (level_of_string max_level_str :> [level | `All] ) with | _ -> failwith ("In section " ^ cf # print addr ^ ": Bad max_level parameter value: " ^ max_level_str) in let fmt = try cf # string_param (cf # resolve_parameter addr "format") with Not_found -> gc#log_format in let file = try cf # string_param (cf # resolve_parameter addr "file") with | Not_found -> failwith ("In section " ^ cf # print addr ^ ": Parameter 'file' is missing") in (component, subchannel, max_level, file, fmt) ) (cf # resolve_section addr "file") in let config = ( object method log_format = gc#log_format method log_component = gc#log_component method log_subchannel = gc#log_subchannel method log_max_level = gc#log_max_level method log_directory = dir method log_files = log_files end ) in multi_file_logger config end class type syslog_config = object inherit generic_config method log_identifier : string method log_facility : Netsys_posix.syslog_facility end let syslog_logger (sc:syslog_config) = let filter = mk_filter (sc :> generic_config) in let prepend = if sc#log_identifier = "" then "" else sc#log_identifier ^ ": " in object(self) method log_subch ~component ~subchannel ~level ~message = if filter component subchannel level then ( try Netsys_posix.syslog sc#log_facility level (prepend ^ message) with | error -> prerr_endline ("Netplex Catastrophic Error: Unable to write to syslog") ) method log = self # log_subch ~subchannel:"" method reopen() = () end let facilities = [ "authpriv", `Authpriv; "cron", `Cron; "daemon", `Daemon; "ftp", `Ftp; "kern", `Kern; "local0", `Local0; "local1", `Local1; "local2", `Local2; "local3", `Local3; "local4", `Local4; "local5", `Local5; "local6", `Local6; "local7", `Local7; "lpr", `Lpr; "mail", `Mail; "news", `News; "syslog", `Syslog; "user", `User; "uucp", `Uucp; "default", `Default; ] let syslog_logger_factory = object method name = "syslog" method create_logger cf addr _ = cf # restrict_subsections addr []; cf # restrict_parameters addr [ "type"; "format"; "component"; "subchannel"; "max_level"; "identifier"; "facility" ]; let identifier = try cf # string_param (cf # resolve_parameter addr "identifier") with Not_found -> "" in let facility_str = try cf # string_param (cf # resolve_parameter addr "facility") with Not_found -> "default" in let facility = try List.assoc (String.lowercase facility_str) facilities with Not_found -> failwith "Bad 'facility' parameter in syslog config" in let sc = ( object inherit extract_generic_config cf addr method log_identifier = identifier method log_facility = facility end ) in syslog_logger sc end let logger_factories = [ file_logger_factory; multi_file_logger_factory; stderr_logger_factory; syslog_logger_factory ]