From 5acd3509eb5679a26374316356fb6455c879cd5a Mon Sep 17 00:00:00 2001 From: Ulrik Strid Date: Tue, 21 Jun 2022 13:52:31 +0200 Subject: [PATCH] Split implementation to a reporter --- app/dune | 20 +++- app/prometheus_app.ml | 151 +------------------------- app/prometheus_unix.ml | 99 +---------------- prometheus-reporter.opam | 33 ++++++ reporter/dune | 13 +++ reporter/prometheus_reporter.ml | 150 +++++++++++++++++++++++++ reporter/prometheus_reporter.mli | 18 +++ reporter/prometheus_reporter_unix.ml | 98 +++++++++++++++++ reporter/prometheus_reporter_unix.mli | 55 ++++++++++ 9 files changed, 387 insertions(+), 250 deletions(-) create mode 100644 prometheus-reporter.opam create mode 100644 reporter/dune create mode 100644 reporter/prometheus_reporter.ml create mode 100644 reporter/prometheus_reporter.mli create mode 100644 reporter/prometheus_reporter_unix.ml create mode 100644 reporter/prometheus_reporter_unix.mli diff --git a/app/dune b/app/dune index debed4d..8110f95 100644 --- a/app/dune +++ b/app/dune @@ -1,13 +1,29 @@ (library (name prometheus_app) (public_name prometheus-app) - (libraries prometheus lwt cohttp-lwt astring asetmap fmt re) + (libraries + prometheus + prometheus-reporter + lwt + cohttp-lwt + astring + asetmap + fmt + re) (modules Prometheus_app) (wrapped false)) (library (name prometheus_app_unix) (public_name prometheus-app.unix) - (libraries prometheus prometheus-app cmdliner cohttp-lwt cohttp-lwt-unix logs.fmt fmt.tty) + (libraries + prometheus + prometheus-app + prometheus-reporter.unix + cmdliner + cohttp-lwt + cohttp-lwt-unix + logs.fmt + fmt.tty) (modules Prometheus_unix) (wrapped false)) diff --git a/app/prometheus_app.ml b/app/prometheus_app.ml index b882096..1356fb0 100644 --- a/app/prometheus_app.ml +++ b/app/prometheus_app.ml @@ -1,147 +1,4 @@ -open Prometheus - -let failf fmt = - Fmt.kstr failwith fmt - -module TextFormat_0_0_4 = struct - let re_unquoted_escapes = Re.compile @@ Re.set "\\\n" - let re_quoted_escapes = Re.compile @@ Re.set "\"\\\n" - - let quote g = - match Re.Group.get g 0 with - | "\\" -> "\\\\" - | "\n" -> "\\n" - | "\"" -> "\\\"" - | x -> failf "Unexpected match %S" x - - let output_metric_type f = function - | Counter -> Fmt.string f "counter" - | Gauge -> Fmt.string f "gauge" - | Summary -> Fmt.string f "summary" - | Histogram -> Fmt.string f "histogram" - - let output_unquoted f s = - Fmt.string f @@ Re.replace re_unquoted_escapes ~f:quote s - - let output_quoted f s = - Fmt.string f @@ Re.replace re_quoted_escapes ~f:quote s - - (* Fmt.float by default prints floats using scientific exponential - * notation, which loses significant data on e.g. timestamp: - * Fmt.str "%a" Fmt.float 1575363850.57 --> 1.57536e+09 *) - let float_fmt f = - Fmt.pf f "%f" - - let output_value f v = - match classify_float v with - | FP_normal | FP_subnormal | FP_zero -> float_fmt f v - | FP_infinite when v > 0.0 -> Fmt.string f "+Inf" - | FP_infinite -> Fmt.string f "-Inf" - | FP_nan -> Fmt.string f "Nan" - - let output_pairs f (label_names, label_values) = - let cont = ref false in - let output_pair name value = - if !cont then Fmt.string f ", " - else cont := true; - Fmt.pf f "%a=\"%a\"" LabelName.pp name output_quoted value - in - List.iter2 output_pair label_names label_values - - let output_labels ~label_names f = function - | [] -> () - | label_values -> Fmt.pf f "{%a}" output_pairs (label_names, label_values) - - let output_sample ~base ~label_names ~label_values f { Sample_set.ext; value; bucket } = - let label_names, label_values = match bucket with - | None -> label_names, label_values - | Some (label_name, label_value) -> - let label_value_str = Fmt.str "%a" output_value label_value in - label_name :: label_names, label_value_str :: label_values - in - Fmt.pf f "%a%s%a %a@." - MetricName.pp base ext - (output_labels ~label_names) label_values - output_value value - - let output_metric ~name ~label_names f (label_values, samples) = - List.iter (output_sample ~base:name ~label_names ~label_values f) samples - - let output f = - MetricFamilyMap.iter (fun metric samples -> - let {MetricInfo.name; metric_type; help; label_names} = metric in - Fmt.pf f - "# HELP %a %a@.\ - # TYPE %a %a@.\ - %a" - MetricName.pp name output_unquoted help - MetricName.pp name output_metric_type metric_type - (LabelSetMap.pp ~sep:Fmt.nop (output_metric ~name ~label_names)) samples - ) -end - -module Runtime = struct - let current = ref (Gc.quick_stat ()) - let update () = - current := Gc.quick_stat () - - let simple_metric ~metric_type ~help name fn = - let info = { - MetricInfo. - name = MetricName.v name; - help; - metric_type; - label_names = []; - } - in - let collect () = - LabelSetMap.singleton [] [Sample_set.sample (fn ())] - in - info, collect - - let ocaml_gc_allocated_bytes = - simple_metric ~metric_type:Counter "ocaml_gc_allocated_bytes" Gc.allocated_bytes - ~help:"Total number of bytes allocated since the program was started." - - let ocaml_gc_major_words = - simple_metric ~metric_type:Counter "ocaml_gc_major_words" (fun () -> (!current).Gc.major_words) - ~help:"Number of words allocated in the major heap since the program was started." - - let ocaml_gc_minor_collections = - simple_metric ~metric_type:Counter "ocaml_gc_minor_collections" (fun () -> float_of_int (!current).Gc.minor_collections) - ~help:"Number of minor collection cycles completed since the program was started." - - let ocaml_gc_major_collections = - simple_metric ~metric_type:Counter "ocaml_gc_major_collections" (fun () -> float_of_int (!current).Gc.major_collections) - ~help:"Number of major collection cycles completed since the program was started." - - let ocaml_gc_heap_words = - simple_metric ~metric_type:Gauge "ocaml_gc_heap_words" (fun () -> float_of_int (!current).Gc.heap_words) - ~help:"Total size of the major heap, in words." - - let ocaml_gc_compactions = - simple_metric ~metric_type:Counter "ocaml_gc_compactions" (fun () -> float_of_int (!current).Gc.compactions) - ~help:"Number of heap compactions since the program was started." - - let ocaml_gc_top_heap_words = - simple_metric ~metric_type:Counter "ocaml_gc_top_heap_words" (fun () -> float_of_int (!current).Gc.top_heap_words) - ~help:"Maximum size reached by the major heap, in words." - - let process_cpu_seconds_total = - simple_metric ~metric_type:Counter "process_cpu_seconds_total" Sys.time - ~help:"Total user and system CPU time spent in seconds." - - let metrics = [ - ocaml_gc_allocated_bytes; - ocaml_gc_major_words; - ocaml_gc_minor_collections; - ocaml_gc_major_collections; - ocaml_gc_heap_words; - ocaml_gc_compactions; - ocaml_gc_top_heap_words; - process_cpu_seconds_total; - ] -end +include Prometheus_reporter open Lwt.Infix @@ -157,9 +14,3 @@ module Cohttp(Server : Cohttp_lwt.S.Server) = struct Server.respond_string ~status:`OK ~headers ~body () | _ -> Server.respond_error ~status:`Bad_request ~body:"Bad request" () end - -let () = - CollectorRegistry.(register_pre_collect default) Runtime.update; - let add (info, collector) = - CollectorRegistry.(register default) info collector in - List.iter add Runtime.metrics diff --git a/app/prometheus_unix.ml b/app/prometheus_unix.ml index eeecab9..6401549 100644 --- a/app/prometheus_unix.ml +++ b/app/prometheus_unix.ml @@ -1,48 +1,4 @@ -open Prometheus - -module Metrics = struct - let namespace = "prometheus" - - let subsystem = "logs" - - let inc_messages = - let help = "Total number of messages logged" in - let c = - Counter.v_labels ~label_names:[ "level"; "src" ] ~help ~namespace - ~subsystem "messages_total" - in - fun lvl src -> - let lvl = Logs.level_to_string (Some lvl) in - Counter.inc_one @@ Counter.labels c [ lvl; src ] -end - -module Unix_runtime = struct - let start_time = Unix.gettimeofday () - - let simple_metric ~metric_type ~help name fn = - let info = { - MetricInfo. - name = MetricName.v name; - help; - metric_type; - label_names = []; - } - in - let collect () = - LabelSetMap.singleton [] [Sample_set.sample (fn ())] - in - info, collect - - let process_start_time_seconds = - simple_metric ~metric_type:Counter "process_start_time_seconds" (fun () -> start_time) - ~help:"Start time of the process since unix epoch in seconds." - - let metrics = [ - process_start_time_seconds; - ] -end - -type config = int option +include Prometheus_reporter_unix module Server = Prometheus_app.Cohttp(Cohttp_lwt_unix.Server) @@ -53,56 +9,3 @@ let serve = function let callback = Server.callback in let thread = Cohttp_lwt_unix.Server.create ~mode (Cohttp_lwt_unix.Server.make ~callback ()) in [thread] - -let listen_prometheus = - let open! Cmdliner in - let doc = - Arg.info ~docs:"MONITORING OPTIONS" ~docv:"PORT" ~doc: - "Port on which to provide Prometheus metrics over HTTP." - ["listen-prometheus"] - in - Arg.(value @@ opt (some int) None doc) - -let opts = listen_prometheus - -let () = - let add (info, collector) = - CollectorRegistry.(register default) info collector in - List.iter add Unix_runtime.metrics - -module Logging = struct - let inc_counter = Metrics.inc_messages - - let pp_timestamp f x = - let open Unix in - let tm = localtime x in - Fmt.pf f "%04d-%02d-%02d %02d:%02d.%02d" (tm.tm_year + 1900) (tm.tm_mon + 1) - tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec - - let reporter formatter = - let report src level ~over k msgf = - let k _ = over (); k () in - let src = Logs.Src.name src in - Metrics.inc_messages level src; - msgf @@ fun ?header ?tags:_ fmt -> - Fmt.kpf k formatter ("%a %a %a @[" ^^ fmt ^^ "@]@.") - pp_timestamp (Unix.gettimeofday ()) - Fmt.(styled `Magenta string) (Printf.sprintf "%14s" src) - Logs_fmt.pp_header (level, header) - in - { Logs.report = report } - - let set_level (src, level) = - let rec aux = function - | [] -> Logs.warn (fun f -> f "set_level: logger %S not registered; ignoring" src) - | x :: _ when Logs.Src.name x = src -> Logs.Src.set_level x (Some level) - | _ :: xs -> aux xs - in - aux (Logs.Src.list ()) - - let init ?(default_level=Logs.Info) ?(levels=[]) ?(formatter=Fmt.stderr) () = - Fmt_tty.setup_std_outputs (); - Logs.set_reporter (reporter formatter); - Logs.set_level (Some default_level); - List.iter set_level levels -end diff --git a/prometheus-reporter.opam b/prometheus-reporter.opam new file mode 100644 index 0000000..de52a6b --- /dev/null +++ b/prometheus-reporter.opam @@ -0,0 +1,33 @@ +opam-version: "2.0" +synopsis: "Client library for Prometheus monitoring" +description: """\ +Applications can enable metric reporting using the `prometheus-reporter` opam package. + +The `prometheus-reporter.unix` ocamlfind library provides the `Prometheus_reporter_unix` module, +which includes a cmdliner option. +See the `examples/example.ml` program for an example, which can be run as: + +Unikernels can use `Prometheus_reporter` instead of `Prometheus_reporter_unix` to avoid the `Unix` dependency.""" +maintainer: "talex5@gmail.com" +authors: ["Thomas Leonard" "David Scott"] +license: "Apache-2.0" +homepage: "https://github.com/mirage/prometheus" +doc: "https://mirage.github.io/prometheus/" +bug-reports: "https://github.com/mirage/prometheus/issues" +depends: [ + "ocaml" {>= "4.02.3"} + "dune" {>= "1.0"} + "prometheus" {= version} + "fmt" {>= "0.8.7"} + "re" + "lwt" {>= "2.5.0"} + "cmdliner" + "alcotest" {with-test} + "astring" + "logs" {>= "0.6.0"} +] +build: [ + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] +dev-repo: "git+https://github.com/mirage/prometheus.git" diff --git a/reporter/dune b/reporter/dune new file mode 100644 index 0000000..cba92e4 --- /dev/null +++ b/reporter/dune @@ -0,0 +1,13 @@ +(library + (name prometheus_reporter) + (public_name prometheus-reporter) + (libraries prometheus lwt astring fmt re) + (modules Prometheus_reporter) + (wrapped false)) + +(library + (name prometheus_reporter_unix) + (public_name prometheus-reporter.unix) + (libraries prometheus cmdliner logs.fmt fmt.tty) + (modules Prometheus_reporter_unix) + (wrapped false)) diff --git a/reporter/prometheus_reporter.ml b/reporter/prometheus_reporter.ml new file mode 100644 index 0000000..f5e2a61 --- /dev/null +++ b/reporter/prometheus_reporter.ml @@ -0,0 +1,150 @@ +open Prometheus + +let failf fmt = + Fmt.kstr failwith fmt + +module TextFormat_0_0_4 = struct + let re_unquoted_escapes = Re.compile @@ Re.set "\\\n" + let re_quoted_escapes = Re.compile @@ Re.set "\"\\\n" + + let quote g = + match Re.Group.get g 0 with + | "\\" -> "\\\\" + | "\n" -> "\\n" + | "\"" -> "\\\"" + | x -> failf "Unexpected match %S" x + + let output_metric_type f = function + | Counter -> Fmt.string f "counter" + | Gauge -> Fmt.string f "gauge" + | Summary -> Fmt.string f "summary" + | Histogram -> Fmt.string f "histogram" + + let output_unquoted f s = + Fmt.string f @@ Re.replace re_unquoted_escapes ~f:quote s + + let output_quoted f s = + Fmt.string f @@ Re.replace re_quoted_escapes ~f:quote s + + (* Fmt.float by default prints floats using scientific exponential + * notation, which loses significant data on e.g. timestamp: + * Fmt.str "%a" Fmt.float 1575363850.57 --> 1.57536e+09 *) + let float_fmt f = + Fmt.pf f "%f" + + let output_value f v = + match classify_float v with + | FP_normal | FP_subnormal | FP_zero -> float_fmt f v + | FP_infinite when v > 0.0 -> Fmt.string f "+Inf" + | FP_infinite -> Fmt.string f "-Inf" + | FP_nan -> Fmt.string f "Nan" + + let output_pairs f (label_names, label_values) = + let cont = ref false in + let output_pair name value = + if !cont then Fmt.string f ", " + else cont := true; + Fmt.pf f "%a=\"%a\"" LabelName.pp name output_quoted value + in + List.iter2 output_pair label_names label_values + + let output_labels ~label_names f = function + | [] -> () + | label_values -> Fmt.pf f "{%a}" output_pairs (label_names, label_values) + + let output_sample ~base ~label_names ~label_values f { Sample_set.ext; value; bucket } = + let label_names, label_values = match bucket with + | None -> label_names, label_values + | Some (label_name, label_value) -> + let label_value_str = Fmt.str "%a" output_value label_value in + label_name :: label_names, label_value_str :: label_values + in + Fmt.pf f "%a%s%a %a@." + MetricName.pp base ext + (output_labels ~label_names) label_values + output_value value + + let output_metric ~name ~label_names f (label_values, samples) = + List.iter (output_sample ~base:name ~label_names ~label_values f) samples + + let output f = + MetricFamilyMap.iter (fun metric samples -> + let {MetricInfo.name; metric_type; help; label_names} = metric in + Fmt.pf f + "# HELP %a %a@.\ + # TYPE %a %a@.\ + %a" + MetricName.pp name output_unquoted help + MetricName.pp name output_metric_type metric_type + (LabelSetMap.pp ~sep:Fmt.nop (output_metric ~name ~label_names)) samples + ) +end + +module Runtime = struct + let current = ref (Gc.quick_stat ()) + let update () = + current := Gc.quick_stat () + + let simple_metric ~metric_type ~help name fn = + let info = { + MetricInfo. + name = MetricName.v name; + help; + metric_type; + label_names = []; + } + in + let collect () = + LabelSetMap.singleton [] [Sample_set.sample (fn ())] + in + info, collect + + let ocaml_gc_allocated_bytes = + simple_metric ~metric_type:Counter "ocaml_gc_allocated_bytes" Gc.allocated_bytes + ~help:"Total number of bytes allocated since the program was started." + + let ocaml_gc_major_words = + simple_metric ~metric_type:Counter "ocaml_gc_major_words" (fun () -> (!current).Gc.major_words) + ~help:"Number of words allocated in the major heap since the program was started." + + let ocaml_gc_minor_collections = + simple_metric ~metric_type:Counter "ocaml_gc_minor_collections" (fun () -> float_of_int (!current).Gc.minor_collections) + ~help:"Number of minor collection cycles completed since the program was started." + + let ocaml_gc_major_collections = + simple_metric ~metric_type:Counter "ocaml_gc_major_collections" (fun () -> float_of_int (!current).Gc.major_collections) + ~help:"Number of major collection cycles completed since the program was started." + + let ocaml_gc_heap_words = + simple_metric ~metric_type:Gauge "ocaml_gc_heap_words" (fun () -> float_of_int (!current).Gc.heap_words) + ~help:"Total size of the major heap, in words." + + let ocaml_gc_compactions = + simple_metric ~metric_type:Counter "ocaml_gc_compactions" (fun () -> float_of_int (!current).Gc.compactions) + ~help:"Number of heap compactions since the program was started." + + let ocaml_gc_top_heap_words = + simple_metric ~metric_type:Counter "ocaml_gc_top_heap_words" (fun () -> float_of_int (!current).Gc.top_heap_words) + ~help:"Maximum size reached by the major heap, in words." + + let process_cpu_seconds_total = + simple_metric ~metric_type:Counter "process_cpu_seconds_total" Sys.time + ~help:"Total user and system CPU time spent in seconds." + + let metrics = [ + ocaml_gc_allocated_bytes; + ocaml_gc_major_words; + ocaml_gc_minor_collections; + ocaml_gc_major_collections; + ocaml_gc_heap_words; + ocaml_gc_compactions; + ocaml_gc_top_heap_words; + process_cpu_seconds_total; + ] +end + +let () = + CollectorRegistry.(register_pre_collect default) Runtime.update; + let add (info, collector) = + CollectorRegistry.(register default) info collector in + List.iter add Runtime.metrics diff --git a/reporter/prometheus_reporter.mli b/reporter/prometheus_reporter.mli new file mode 100644 index 0000000..ee4aec6 --- /dev/null +++ b/reporter/prometheus_reporter.mli @@ -0,0 +1,18 @@ +(** Report metrics for Prometheus. + See: https://prometheus.io/ + + Notes: + + - This module is intended to be used by applications that export Prometheus metrics. + Libraries should only link against the `Prometheus` module. + + - This module automatically initialises itself and registers some standard collectors relating to + GC statistics, as recommended by Prometheus. + + - This module does not depend on [Unix], and so can be used in unikernels. + *) + +module TextFormat_0_0_4 : sig + val output : Prometheus.CollectorRegistry.snapshot Fmt.t +end +(** Format a snapshot in Prometheus's text format, version 0.0.4. *) diff --git a/reporter/prometheus_reporter_unix.ml b/reporter/prometheus_reporter_unix.ml new file mode 100644 index 0000000..4b37712 --- /dev/null +++ b/reporter/prometheus_reporter_unix.ml @@ -0,0 +1,98 @@ +open Prometheus + +module Metrics = struct + let namespace = "prometheus" + + let subsystem = "logs" + + let inc_messages = + let help = "Total number of messages logged" in + let c = + Counter.v_labels ~label_names:[ "level"; "src" ] ~help ~namespace + ~subsystem "messages_total" + in + fun lvl src -> + let lvl = Logs.level_to_string (Some lvl) in + Counter.inc_one @@ Counter.labels c [ lvl; src ] +end + +module Unix_runtime = struct + let start_time = Unix.gettimeofday () + + let simple_metric ~metric_type ~help name fn = + let info = { + MetricInfo. + name = MetricName.v name; + help; + metric_type; + label_names = []; + } + in + let collect () = + LabelSetMap.singleton [] [Sample_set.sample (fn ())] + in + info, collect + + let process_start_time_seconds = + simple_metric ~metric_type:Counter "process_start_time_seconds" (fun () -> start_time) + ~help:"Start time of the process since unix epoch in seconds." + + let metrics = [ + process_start_time_seconds; + ] +end + +type config = int option + +let listen_prometheus = + let open! Cmdliner in + let doc = + Arg.info ~docs:"MONITORING OPTIONS" ~docv:"PORT" ~doc: + "Port on which to provide Prometheus metrics over HTTP." + ["listen-prometheus"] + in + Arg.(value @@ opt (some int) None doc) + +let opts = listen_prometheus + +let () = + let add (info, collector) = + CollectorRegistry.(register default) info collector in + List.iter add Unix_runtime.metrics + +module Logging = struct + let inc_counter = Metrics.inc_messages + + let pp_timestamp f x = + let open Unix in + let tm = localtime x in + Fmt.pf f "%04d-%02d-%02d %02d:%02d.%02d" (tm.tm_year + 1900) (tm.tm_mon + 1) + tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec + + let reporter formatter = + let report src level ~over k msgf = + let k _ = over (); k () in + let src = Logs.Src.name src in + Metrics.inc_messages level src; + msgf @@ fun ?header ?tags:_ fmt -> + Fmt.kpf k formatter ("%a %a %a @[" ^^ fmt ^^ "@]@.") + pp_timestamp (Unix.gettimeofday ()) + Fmt.(styled `Magenta string) (Printf.sprintf "%14s" src) + Logs_fmt.pp_header (level, header) + in + { Logs.report = report } + + let set_level (src, level) = + let rec aux = function + | [] -> Logs.warn (fun f -> f "set_level: logger %S not registered; ignoring" src) + | x :: _ when Logs.Src.name x = src -> Logs.Src.set_level x (Some level) + | _ :: xs -> aux xs + in + aux (Logs.Src.list ()) + + let init ?(default_level=Logs.Info) ?(levels=[]) ?(formatter=Fmt.stderr) () = + Fmt_tty.setup_std_outputs (); + Logs.set_reporter (reporter formatter); + Logs.set_level (Some default_level); + List.iter set_level levels +end diff --git a/reporter/prometheus_reporter_unix.mli b/reporter/prometheus_reporter_unix.mli new file mode 100644 index 0000000..2404f64 --- /dev/null +++ b/reporter/prometheus_reporter_unix.mli @@ -0,0 +1,55 @@ +(** Report metrics for Prometheus. + See: https://prometheus.io/ + + Notes: + + - This module is intended to be used by applications that export Prometheus metrics. + Libraries should only link against the `Prometheus` module. + + - This module automatically initialises itself and registers some standard collectors relating to + GC statistics, as recommended by Prometheus. + + - This extends [Prometheus_reporter] with support for cmdliner option parsing, a server pre-configured + for Unix, and a start-time metric that uses [Unix.gettimeofday]. + *) + +type config = int option + +val opts : config Cmdliner.Term.t +(** [opts] is the extra command-line options to offer Prometheus + monitoring. *) + +(** Report metrics for messages logged. *) +module Logging : sig + val init : + ?default_level:Logs.level -> + ?levels:(string * Logs.level) list -> + ?formatter:Format.formatter -> + unit -> unit + (** Initialise the Logs library with a reporter that reports prometheus metrics too. + The reporter is configured to log to stderr and the log messages include a + timestamp and the event's source. + + A server will typically use the following code to initialise logging: + {[ + let () = Prometheus_app.Logging.init () + ]} + + Or: + {[ + let () = + Prometheus_unix.Logging.init () + ~default_level:Logs.Debug + ~levels:[ + "cohttp.lwt.io", Logs.Info; + ] + ]} + @param default_level The default log-level to use (default {!Logs.Info}). + @param levels Provides levels for specific log sources. + @param formatter A custom formatter (default {!Fmt.stderr}). *) + + val inc_counter : Logs.level -> string -> unit + (** [inc_counter level src] increments the count of messages logged by [src] at [level]. + The reporter installed by [init] calls this automatically, but you might want to + use this if you use your own reporter instead. *) +end