package easy_logging

  1. Overview
  2. Docs

Source file handlers.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
(*
    This file is part of easy_logging.

    This Source Code Form is subject to the terms of the Mozilla Public
    License, v. 2.0. If a copy of the MPL was not distributed with this
    file, You can obtain one at https://mozilla.org/MPL/2.0/.
*)



(**
   This is the [Handlers] module. It provides simple yet adaptable handlers implementation.

*)


open Logging_types
open Formatters
(** {1 Type definitions } *)
type log_formatter = log_item -> string
type filter= log_item -> bool

(** Type of a handler *)
type t =
  {
    mutable fmt : log_formatter;
    mutable level : level;
    mutable filters: filter list;
    output : string -> unit;
  }

(**
   A handler is made of:
   - a formatter that transforms a log item into a string.
   - a level used to filter out items.
   - an array of possible additional custom filters.
   - an [output] function, that takes a string and does the output job.

*)


(** {1 Handlers creation helpers } *)

(** Module to create handlers that [output] to stdout or [stderr]. *)
module CliHandler = struct
  let make oc level =
    {fmt = format_color;
     level = level;
     output = (fun s -> output_string oc s; flush oc);
     filters = []}
end

(** Module to create handlers that output to a file. *)
module FileHandler = struct

  (** Configuration when creating a file handler.
      - The [date_prefix] value is an optional Calender Printer String (as specified in the  {{:http://calendar.forge.ocamlcore.org/doc/Printer.html} Calendar documentation }), that will format the time at which the handler is created, and used as a prefix.
      - If the [versioning] value is not None, a versionning number is appended to the filename, based on already existing files. The integer gives the padding of versionning numbers.  THIS OPTION MIGHT DISAPPEAR IN FUTURE RELEASES.
  *)
  type config = {
    logs_folder: string; (** Path in which logs files will be written. *)
    truncate: bool;  (** Truncate/append if file already exists. *)
    file_perms: int;  (** Unix file permissions. *)
    date_prefix : string option; (** Optional date prefix format string. *)
    versioning: int option; (**  Optionaly generate new file name. *)
    suffix: string; (** Constant suffix. *)
  }

  let default_config = {
    logs_folder = "logs/";
    truncate = true;
    file_perms = 0o660;
    date_prefix = Some "%Y%m%d_";
    versioning = None;
    suffix : string = ".log"
  }


  let generate_prefix config  =
    match config.date_prefix with
    | None -> ""
    | Some f ->
      let open CalendarLib in
      let initial_tz = Time_Zone.current () in
      Time_Zone.change Local;
      let now = Calendar.now () in
      let prefix = (Printer.Calendar.sprint f now) in
      Time_Zone.change initial_tz;
      prefix

  let generate_filename config base =
    let rec find_versioned pattern i =
      let name = Printf.sprintf pattern i in
      if Sys.file_exists name
      then find_versioned pattern (i+1)
      else name
    in
    let prefix = generate_prefix config
    and suffix = config.suffix
    and folder = config.logs_folder
    in
    match config.versioning
    with
    | None -> Filename.concat folder prefix^base^suffix
    | Some i ->
      let filename_pattern_string =
        Filename.concat folder
          (Printf.sprintf "%s%s_%%0%ii%s" prefix base i suffix) in
      let filename_pattern  = Scanf.format_from_string filename_pattern_string "%i" in
      find_versioned filename_pattern 0

  let create_file config filename =
    if not (Sys.file_exists config.logs_folder)
    then
      Unix.mkdir config.logs_folder 0o775;

    let open_flags =
      if config.truncate
      then [Open_wronly; Open_creat;Open_trunc]
      else [Open_wronly; Open_creat]
    in
    open_out_gen open_flags
      config.file_perms filename

(** [make ~config level filename_base] creates a Handler that will output logs to the file created from [filename_base] and [config]
  *)
  let make ?config:(config=default_config) level filename_base =
    let filename = generate_filename config filename_base in 
    let oc = create_file config filename
    in
    {fmt = format_default;
     level = level;
     output = (fun s -> output_string oc s; flush oc);
     filters = [];
    }

(** [make_rotating ~config level filename_base max_kbytes backup_count] creates a Handler that will output logs to a rotating set of files. The files are from [filename_base] and [config]. When the size of a log file is about to exceed [max_kbytes] kb, a new file is created, and older log files are renamed. See {{:https://docs.python.org/3/library/logging.handlers.html#logging.handlers.RotatingFileHandler} the python RotatingFileHandler}.
  *)
  let make_rotating ?config:(config=default_config) level filename_base
      max_kbytes backup_count =

    let bytes_remaining = ref @@ max_kbytes * 1024
    and filename = generate_filename config filename_base in

    let make_rotating_filename n =
      if n = 0
      then filename
      else filename^"."^(string_of_int n)
    and oc = ref (create_file config filename)
    and files_nb =  ref 1 in

    let output = fun s ->
      if !bytes_remaining - String.length s < 0
      then
        begin
          close_out !oc;
          if !files_nb = backup_count + 1
          then
            begin
              Sys.remove (make_rotating_filename backup_count);
              decr files_nb
            end;
          for i = !files_nb downto 1 do
            Sys.rename (make_rotating_filename (i-1)) (make_rotating_filename i)
          done;
          oc := create_file config filename;
          incr files_nb;
          bytes_remaining := max_kbytes * 1024
        end;
      output_string !oc s; flush !oc;
      bytes_remaining := !bytes_remaining - String.length s
    in
    {fmt = format_default;
     level ;
     output ;
     filters = [];
    }


end

type config =
  {file_handlers: FileHandler.config }
let default_config = {file_handlers = FileHandler.default_config}

type desc = | Cli of level | CliErr of level
            | File of string * level | RotatingFile of string * level * int * int

let make ?config:(config=default_config) desc = match desc with
  | Cli lvl -> CliHandler.make stdout lvl
  | CliErr lvl -> CliHandler.make stderr lvl
  | File (f, lvl) -> FileHandler.make ~config:config.file_handlers lvl f
  | RotatingFile (f, lvl, max_bytes, backup_count) ->
    FileHandler.make_rotating ~config:config.file_handlers lvl f max_bytes backup_count

(** Used for quick handler creation, e.g.


    - Cli handler: outputs colored messages to stdout
    {[ let h = Handlers.make (Cli Debug) ]}
    - File handler : outputs messages to a given file
    {[ let h = Handlers.make (File ("filename", Debug)) ]}
*)



(** {1 Handlers setup } *)


(** Sets the level of a handler. *)
let set_level (h:t) lvl =
  h.level <- lvl

(** Sets the formatter of a handler. *)
let set_formatter h fmt =
  h.fmt <- fmt

(** Adds a filter to a handler. *)
let add_filter h filter =
  h.filters <- filter::h.filters


(** Auxiliary function.*)
let apply (h : t) (item: log_item) =
  if item.level >= h.level && (reduce (&&) (List.map (fun f -> f item) h.filters) true)
  then
    (
      h.output (Printf.sprintf "%s\n" (h.fmt item));
    )
OCaml

Innovation. Community. Security.