##! The Bro logging interface. ##! ##! See XXX for a introduction to Bro's logging framework. module Log; # Log::ID and Log::Writer are defined in bro.init due to circular dependencies. export { ## If true, is local logging is by default enabled for all filters. const enable_local_logging = T &redef; ## If true, is remote logging is by default enabled for all filters. const enable_remote_logging = T &redef; ## Default writer to use if a filter does not specify ## anything else. const default_writer = WRITER_ASCII &redef; ## Type defining the content of a logging stream. type Stream: record { ## A record type defining the log's columns. columns: any; ## Event that will be raised once for each log entry. ## The event receives a single same parameter, an instance of type ``columns``. ev: any &optional; }; ## Default function for building the path values for log filters if not ## speficied otherwise by a filter. The default implementation uses ``id`` ## to derive a name. ## ## id: The log stream. ## path: A suggested path value, which may be either the filter's ``path`` ## if defined or a fall-back generated internally. ## rec: An instance of the streams's ``columns`` type with its ## fields set to the values to logged. ## ## Returns: The path to be used for the filter. global default_path_func: function(id: ID, path: string, rec: any) : string &redef; ## Filter customizing logging. type Filter: record { ## Descriptive name to reference this filter. name: string; ## The writer to use. writer: Writer &default=default_writer; ## Predicate indicating whether a log entry should be recorded. ## If not given, all entries are recorded. ## ## rec: An instance of the streams's ``columns`` type with its ## fields set to the values to logged. ## ## Returns: True if the entry is to be recorded. pred: function(rec: any): bool &optional; ## Output path for recording entries matching this ## filter. ## ## The specific interpretation of the string is up to ## the used writer, and may for example be the destination ## file name. Generally, filenames are expected to given ## without any extensions; writers will add appropiate ## extensions automatically. path: string &optional; ## A function returning the output path for recording entries ## matching this filter. This is similar to ``path`` yet allows ## to compute the string dynamically. It is ok to return ## different strings for separate calls, but be careful: it's ## easy to flood the disk by returning a new string for each ## connection ... ## ## id: The log stream. ## path: A suggested path value, which may be either the filter's ``path`` ## if defined or a fall-back generated internally. ## rec: An instance of the streams's ``columns`` type with its ## fields set to the values to logged. ## ## Returns: The path to be used for the filter. path_func: function(id: ID, path: string, rec: any): string &optional; ## Subset of column names to record. If not given, all ## columns are recorded. include: set[string] &optional; ## Subset of column names to exclude from recording. If not given, ## all columns are recorded. exclude: set[string] &optional; ## If true, entries are recorded locally. log_local: bool &default=enable_local_logging; ## If true, entries are passed on to remote peers. log_remote: bool &default=enable_remote_logging; }; # Log rotation support. ## Information passed into rotation callback functions. type RotationInfo: record { writer: Writer; ##< Writer. fname: string; ##< Full name of the rotated file. path: string; ##< Original path value. open: time; ##< Time when opened. close: time; ##< Time when closed. terminating: bool; ##< True if rotation occured due to Bro shutting down. }; ## Default rotation interval. Zero disables rotation. const default_rotation_interval = 0secs &redef; ## Default naming format for timestamps embedded into filenames. Uses a strftime() style. const default_rotation_date_format = "%Y-%m-%d-%H-%M-%S" &redef; ## Default shell command to run on rotated files. Empty for none. const default_rotation_postprocessor_cmd = "" &redef; ## Specifies the default postprocessor function per writer type. Entries in this ## table are initialized by each writer type. const default_rotation_postprocessors: table[Writer] of function(info: RotationInfo) : bool &redef; ## Type for controlling file rotation. type RotationControl: record { ## Rotation interval. interv: interval &default=default_rotation_interval; ## Callback function to trigger for rotated files. If not set, the default ## comes out of default_rotation_postprocessors. postprocessor: function(info: RotationInfo) : bool &optional; }; ## Specifies rotation parameters per ``(id, path)`` tuple. ## If a pair is not found in this table, default values defined in ## ``RotationControl`` are used. const rotation_control: table[Writer, string] of RotationControl &default=[] &redef; ## Sentinel value for indicating that a filter was not found when looked up. const no_filter: Filter = [$name=""]; # Sentinel. # TODO: Document. global create_stream: function(id: ID, stream: Stream) : bool; global enable_stream: function(id: ID) : bool; global disable_stream: function(id: ID) : bool; global add_filter: function(id: ID, filter: Filter) : bool; global remove_filter: function(id: ID, name: string) : bool; global get_filter: function(id: ID, name: string) : Filter; # Returns no_filter if not found. global write: function(id: ID, columns: any) : bool; global set_buf: function(id: ID, buffered: bool): bool; global flush: function(id: ID): bool; global add_default_filter: function(id: ID) : bool; global remove_default_filter: function(id: ID) : bool; global run_rotation_postprocessor_cmd: function(info: RotationInfo, npath: string) : bool; } # We keep a script-level copy of all filters so that we can manipulate them. global filters: table[ID, string] of Filter; @load base/logging.bif # Needs Filter and Stream defined. module Log; # Used internally by the log manager. function __default_rotation_postprocessor(info: RotationInfo) : bool { if ( info$writer in default_rotation_postprocessors ) return default_rotation_postprocessors[info$writer](info); } function default_path_func(id: ID, path: string, rec: any) : string { local id_str = fmt("%s", id); local parts = split1(id_str, /::/); if ( |parts| == 2 ) { # TODO: the core shouldn't be suggesting paths anymore. Only # statically defined paths should be sent into here. This # is only to cope with the core generated paths. if ( to_lower(parts[2]) != path ) return path; # Example: Notice::LOG -> "notice" if ( parts[2] == "LOG" ) { local module_parts = split_n(parts[1], /[^A-Z][A-Z][a-z]*/, T, 4); local output = ""; if ( 1 in module_parts ) output = module_parts[1]; if ( 2 in module_parts && module_parts[2] != "" ) output = cat(output, sub_bytes(module_parts[2],1,1), "_", sub_bytes(module_parts[2], 2, |module_parts[2]|)); if ( 3 in module_parts && module_parts[3] != "" ) output = cat(output, "_", module_parts[3]); if ( 4 in module_parts && module_parts[4] != "" ) output = cat(output, sub_bytes(module_parts[4],1,1), "_", sub_bytes(module_parts[4], 2, |module_parts[4]|)); # TODO: There seems to be some problem with the split function # not putting \0 at the end of the string. fmt will make # a better internal string. return fmt("%s", to_lower(output)); } # Example: Notice::POLICY_LOG -> "notice_policy" if ( /_LOG$/ in parts[2] ) parts[2] = sub(parts[2], /_LOG$/, ""); return cat(to_lower(parts[1]),"_",to_lower(parts[2])); } else { # In case there is a logging stream in the global namespace. return to_lower(id_str); } } # Run post-processor on file. If there isn't any postprocessor defined, # we move the file to a nicer name. function run_rotation_postprocessor_cmd(info: RotationInfo, npath: string) : bool { local pp_cmd = default_rotation_postprocessor_cmd; if ( pp_cmd == "" ) return T; # The date format is hard-coded here to provide a standardized # script interface. system(fmt("%s %s %s %s %s %d", pp_cmd, npath, info$path, strftime("%y-%m-%d_%H.%M.%S", info$open), strftime("%y-%m-%d_%H.%M.%S", info$close), info$terminating)); return T; } function create_stream(id: ID, stream: Stream) : bool { if ( ! __create_stream(id, stream) ) return F; return add_default_filter(id); } function disable_stream(id: ID) : bool { if ( ! __disable_stream(id) ) return F; } function add_filter(id: ID, filter: Filter) : bool { # This is a work-around for the fact that we can't forward-declare # the default_path_func and then use it as &default in the record # definition. if ( ! filter?$path_func ) filter$path_func = default_path_func; filters[id, filter$name] = filter; return __add_filter(id, filter); } function remove_filter(id: ID, name: string) : bool { delete filters[id, name]; return __remove_filter(id, name); } function get_filter(id: ID, name: string) : Filter { if ( [id, name] in filters ) return filters[id, name]; return no_filter; } function write(id: ID, columns: any) : bool { return __write(id, columns); } function set_buf(id: ID, buffered: bool): bool { return __set_buf(id, buffered); } function flush(id: ID): bool { return __flush(id); } function add_default_filter(id: ID) : bool { return add_filter(id, [$name="default"]); } function remove_default_filter(id: ID) : bool { return remove_filter(id, "default"); }