diff --git a/app/dune b/app/dune index 50c393b..debed4d 100644 --- a/app/dune +++ b/app/dune @@ -8,6 +8,6 @@ (library (name prometheus_app_unix) (public_name prometheus-app.unix) - (libraries prometheus prometheus-app cmdliner cohttp-lwt cohttp-lwt-unix) + (libraries prometheus prometheus-app cmdliner cohttp-lwt cohttp-lwt-unix logs.fmt fmt.tty) (modules Prometheus_unix) (wrapped false)) diff --git a/app/prometheus_unix.ml b/app/prometheus_unix.ml index c3a1e6e..c1a1999 100644 --- a/app/prometheus_unix.ml +++ b/app/prometheus_unix.ml @@ -1,5 +1,21 @@ 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 () @@ -53,3 +69,40 @@ 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 = + 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 Fmt.stderr ("%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=[]) () = + Fmt_tty.setup_std_outputs (); + Logs.set_reporter reporter; + Logs.set_level (Some default_level); + List.iter set_level levels +end diff --git a/app/prometheus_unix.mli b/app/prometheus_unix.mli index 7c963b1..2ee7716 100644 --- a/app/prometheus_unix.mli +++ b/app/prometheus_unix.mli @@ -23,3 +23,36 @@ val serve : config -> unit Lwt.t list 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 -> + 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. *) + + 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 diff --git a/examples/example.ml b/examples/example.ml index c7e83ca..f00a047 100644 --- a/examples/example.ml +++ b/examples/example.ml @@ -29,7 +29,16 @@ let main prometheus_config = open Cmdliner +(* Optional: configure logging *) let () = + Prometheus_unix.Logging.init () + ~default_level:Logs.Debug + ~levels:[ + "cohttp.lwt.io", Logs.Info; + ] + +let () = + Logs.info (fun f -> f "Logging initialised."); print_endline "If run with the option --listen-prometheus=9090, this program serves metrics at\n\ http://localhost:9090/metrics"; let spec = Term.(const main $ Prometheus_unix.opts) in diff --git a/prometheus-app.opam b/prometheus-app.opam index eb7e543..c7aebd5 100644 --- a/prometheus-app.opam +++ b/prometheus-app.opam @@ -1,5 +1,23 @@ opam-version: "2.0" synopsis: "Client library for Prometheus monitoring" +description: """\ +Applications can enable metric reporting using the `prometheus-app` opam package. +This depends on cohttp and can serve the metrics collected above over HTTP. + +The `prometheus-app.unix` ocamlfind library provides the `Prometheus_unix` module, +which includes a cmdliner option and pre-configured web-server. +See the `examples/example.ml` program for an example, which can be run as: + +```shell +$ dune exec -- examples/example.exe --listen-prometheus=9090 +If run with the option --listen-prometheus=9090, this program serves metrics at +http://localhost:9090/metrics +Tick! +Tick! +... +``` + +Unikernels can use `Prometheus_app` instead of `Prometheus_unix` to avoid the `Unix` dependency.""" maintainer: "talex5@gmail.com" authors: ["Thomas Leonard" "David Scott"] license: "Apache" @@ -9,7 +27,7 @@ bug-reports: "https://github.com/mirage/prometheus/issues" depends: [ "ocaml" {>= "4.02.3"} "dune" {>= "1.0"} - "prometheus" {=version} + "prometheus" {= version} "fmt" "re" "cohttp" {>= "1.0.0"} @@ -18,6 +36,9 @@ depends: [ "lwt" {>= "2.5.0"} "cmdliner" "alcotest" {with-test} + "asetmap" + "astring" + "logs" ] build: [ ["dune" "subst"] {pinned} @@ -25,22 +46,3 @@ build: [ ["dune" "runtest" "-p" name "-j" jobs] {with-test} ] dev-repo: "git+https://github.com/mirage/prometheus.git" -description: """ -Applications can enable metric reporting using the `prometheus-app` opam package. -This depends on cohttp and can serve the metrics collected above over HTTP. - -The `prometheus-app.unix` ocamlfind library provides the `Prometheus_unix` module, -which includes a cmdliner option and pre-configured web-server. -See the `examples/example.ml` program for an example, which can be run as: - -```shell -$ dune exec -- examples/example.exe --listen-prometheus=9090 -If run with the option --listen-prometheus=9090, this program serves metrics at -http://localhost:9090/metrics -Tick! -Tick! -... -``` - -Unikernels can use `Prometheus_app` instead of `Prometheus_unix` to avoid the `Unix` dependency. -"""