mrc0mmand / rpms / libguestfs

Forked from rpms/libguestfs 3 years ago
Clone

Blame SOURCES/0015-OCaml-tools-output-messages-into-JSON-for-machine-re.patch

3efd08
From 1b29702cefae8a66c83bc84dd15e7b858af3ab47 Mon Sep 17 00:00:00 2001
3efd08
From: Pino Toscano <ptoscano@redhat.com>
3efd08
Date: Fri, 22 Mar 2019 16:24:25 +0100
3efd08
Subject: [PATCH] OCaml tools: output messages into JSON for machine readable
3efd08
3efd08
When the machine readable mode is enabled, print all the messages
3efd08
(progress, info, warning, and errors) also as JSON in the machine
3efd08
readable stream: this way, users can easily parse the status of the
3efd08
OCaml tool, and report that back.
3efd08
3efd08
The formatting of the current date time into the RFC 3999 format is done
3efd08
in C, because of the lack of OCaml APIs for this.
3efd08
3efd08
(cherry picked from commit f79129b8dc92470e3a5597daf53c84038bd6859e)
3efd08
---
3efd08
 .gitignore                                  |   1 +
3efd08
 common/mltools/Makefile.am                  |  39 ++++++-
3efd08
 common/mltools/parse_tools_messages_test.py | 118 ++++++++++++++++++++
3efd08
 common/mltools/test-tools-messages.sh       |  28 +++++
3efd08
 common/mltools/tools_messages_tests.ml      |  46 ++++++++
3efd08
 common/mltools/tools_utils-c.c              |  51 +++++++++
3efd08
 common/mltools/tools_utils.ml               |  16 +++
3efd08
 lib/guestfs.pod                             |  19 ++++
3efd08
 8 files changed, 316 insertions(+), 2 deletions(-)
3efd08
 create mode 100644 common/mltools/parse_tools_messages_test.py
3efd08
 create mode 100755 common/mltools/test-tools-messages.sh
3efd08
 create mode 100644 common/mltools/tools_messages_tests.ml
3efd08
3efd08
diff --git a/.gitignore b/.gitignore
3efd08
index f2efcdde2..db1dbb7cc 100644
3efd08
--- a/.gitignore
3efd08
+++ b/.gitignore
3efd08
@@ -147,6 +147,7 @@ Makefile.in
3efd08
 /common/mltools/JSON_tests
3efd08
 /common/mltools/JSON_parser_tests
3efd08
 /common/mltools/machine_readable_tests
3efd08
+/common/mltools/tools_messages_tests
3efd08
 /common/mltools/tools_utils_tests
3efd08
 /common/mltools/oUnit-*
3efd08
 /common/mlutils/.depend
3efd08
diff --git a/common/mltools/Makefile.am b/common/mltools/Makefile.am
3efd08
index 37d10e610..ae78b84b7 100644
3efd08
--- a/common/mltools/Makefile.am
3efd08
+++ b/common/mltools/Makefile.am
3efd08
@@ -27,6 +27,8 @@ EXTRA_DIST = \
3efd08
 	machine_readable_tests.ml \
3efd08
 	test-getopt.sh \
3efd08
 	test-machine-readable.sh \
3efd08
+	test-tools-messages.sh \
3efd08
+	tools_messages_tests.ml \
3efd08
 	tools_utils_tests.ml
3efd08
 
3efd08
 SOURCES_MLI = \
3efd08
@@ -45,12 +47,12 @@ SOURCES_MLI = \
3efd08
 
3efd08
 SOURCES_ML = \
3efd08
 	getopt.ml \
3efd08
+	JSON.ml \
3efd08
 	tools_utils.ml \
3efd08
 	URI.ml \
3efd08
 	planner.ml \
3efd08
 	registry.ml \
3efd08
 	regedit.ml \
3efd08
-	JSON.ml \
3efd08
 	JSON_parser.ml \
3efd08
 	curl.ml \
3efd08
 	checksums.ml \
3efd08
@@ -196,6 +198,15 @@ machine_readable_tests_CPPFLAGS = \
3efd08
 machine_readable_tests_BOBJECTS = machine_readable_tests.cmo
3efd08
 machine_readable_tests_XOBJECTS = $(machine_readable_tests_BOBJECTS:.cmo=.cmx)
3efd08
 
3efd08
+tools_messages_tests_SOURCES = dummy.c
3efd08
+tools_messages_tests_CPPFLAGS = \
3efd08
+	-I. \
3efd08
+	-I$(top_builddir) \
3efd08
+	-I$(shell $(OCAMLC) -where) \
3efd08
+	-I$(top_srcdir)/lib
3efd08
+tools_messages_tests_BOBJECTS = tools_messages_tests.cmo
3efd08
+tools_messages_tests_XOBJECTS = $(tools_messages_tests_BOBJECTS:.cmo=.cmx)
3efd08
+
3efd08
 # Can't call the following as <test>_OBJECTS because automake gets confused.
3efd08
 if !HAVE_OCAMLOPT
3efd08
 tools_utils_tests_THEOBJECTS = $(tools_utils_tests_BOBJECTS)
3efd08
@@ -212,6 +223,9 @@ JSON_parser_tests.cmo: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
 
3efd08
 machine_readable_tests_THEOBJECTS = $(machine_readable_tests_BOBJECTS)
3efd08
 machine_readable_tests.cmo: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
+
3efd08
+tools_messages_tests_THEOBJECTS = $(tools_messages_tests_tests_BOBJECTS)
3efd08
+tools_messages_tests.cmo: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
 else
3efd08
 tools_utils_tests_THEOBJECTS = $(tools_utils_tests_XOBJECTS)
3efd08
 tools_utils_tests.cmx: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
@@ -227,6 +241,9 @@ JSON_parser_tests.cmx: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
 
3efd08
 machine_readable_tests_THEOBJECTS = $(machine_readable_tests_XOBJECTS)
3efd08
 machine_readable_tests.cmx: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
+
3efd08
+tools_messages_tests_THEOBJECTS = $(tools_messages_tests_XOBJECTS)
3efd08
+tools_messages_tests.cmx: OCAMLPACKAGES += $(OCAMLPACKAGES_TESTS)
3efd08
 endif
3efd08
 
3efd08
 OCAMLLINKFLAGS = \
3efd08
@@ -302,14 +319,32 @@ machine_readable_tests_LINK = \
3efd08
 	  $(OCAMLPACKAGES) $(OCAMLPACKAGES_TESTS) \
3efd08
 	  $(machine_readable_tests_THEOBJECTS) -o $@
3efd08
 
3efd08
+tools_messages_tests_DEPENDENCIES = \
3efd08
+	$(tools_messages_tests_THEOBJECTS) \
3efd08
+	../mlstdutils/mlstdutils.$(MLARCHIVE) \
3efd08
+	../mlgettext/mlgettext.$(MLARCHIVE) \
3efd08
+	../mlpcre/mlpcre.$(MLARCHIVE) \
3efd08
+	$(MLTOOLS_CMA) \
3efd08
+	$(top_srcdir)/ocaml-link.sh
3efd08
+tools_messages_tests_LINK = \
3efd08
+	$(top_srcdir)/ocaml-link.sh -cclib '-lutils -lgnu' -- \
3efd08
+	  $(OCAMLFIND) $(BEST) $(OCAMLFLAGS) $(OCAMLLINKFLAGS) \
3efd08
+	  $(OCAMLPACKAGES) $(OCAMLPACKAGES_TESTS) \
3efd08
+	  $(tools_messages_tests_THEOBJECTS) -o $@
3efd08
+
3efd08
 TESTS_ENVIRONMENT = $(top_builddir)/run --test
3efd08
 
3efd08
 TESTS = \
3efd08
 	test-getopt.sh \
3efd08
 	test-machine-readable.sh
3efd08
+if HAVE_PYTHON
3efd08
+TESTS += \
3efd08
+	test-tools-messages.sh
3efd08
+endif
3efd08
 check_PROGRAMS = \
3efd08
 	getopt_tests \
3efd08
-	machine_readable_tests
3efd08
+	machine_readable_tests \
3efd08
+	tools_messages_tests
3efd08
 
3efd08
 if HAVE_OCAML_PKG_OUNIT
3efd08
 check_PROGRAMS += JSON_tests JSON_parser_tests tools_utils_tests
3efd08
diff --git a/common/mltools/parse_tools_messages_test.py b/common/mltools/parse_tools_messages_test.py
3efd08
new file mode 100644
3efd08
index 000000000..9dcd6cae6
3efd08
--- /dev/null
3efd08
+++ b/common/mltools/parse_tools_messages_test.py
3efd08
@@ -0,0 +1,118 @@
3efd08
+# Copyright (C) 2019 Red Hat Inc.
3efd08
+#
3efd08
+# This program is free software; you can redistribute it and/or modify
3efd08
+# it under the terms of the GNU General Public License as published by
3efd08
+# the Free Software Foundation; either version 2 of the License, or
3efd08
+# (at your option) any later version.
3efd08
+#
3efd08
+# This program is distributed in the hope that it will be useful,
3efd08
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3efd08
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
3efd08
+# GNU General Public License for more details.
3efd08
+#
3efd08
+# You should have received a copy of the GNU General Public License
3efd08
+# along with this program; if not, write to the Free Software
3efd08
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
3efd08
+
3efd08
+import datetime
3efd08
+import json
3efd08
+import os
3efd08
+import sys
3efd08
+import unittest
3efd08
+
3efd08
+exe = "tools_messages_tests"
3efd08
+
3efd08
+if sys.version_info >= (3, 4):
3efd08
+    def set_fd_inheritable(fd):
3efd08
+        os.set_inheritable(fd, True)
3efd08
+else:
3efd08
+    def set_fd_inheritable(fd):
3efd08
+        pass
3efd08
+
3efd08
+
3efd08
+if sys.version_info >= (3, 0):
3efd08
+    def fdopen(fd, mode):
3efd08
+        return open(fd, mode)
3efd08
+
3efd08
+    def isModuleInstalled(mod):
3efd08
+        import importlib
3efd08
+        return bool(importlib.util.find_spec(mod))
3efd08
+else:
3efd08
+    def fdopen(fd, mode):
3efd08
+        return os.fdopen(fd, mode)
3efd08
+
3efd08
+    def isModuleInstalled(mod):
3efd08
+        import imp
3efd08
+        try:
3efd08
+            imp.find_module(mod)
3efd08
+            return True
3efd08
+        except ImportError:
3efd08
+            return False
3efd08
+
3efd08
+
3efd08
+def skipUnlessHasModule(mod):
3efd08
+    if not isModuleInstalled(mod):
3efd08
+        return unittest.skip("%s not available" % mod)
3efd08
+    return lambda func: func
3efd08
+
3efd08
+
3efd08
+def iterload(stream):
3efd08
+    dec = json.JSONDecoder()
3efd08
+    for line in stream:
3efd08
+        yield dec.raw_decode(line)
3efd08
+
3efd08
+
3efd08
+def loadJsonFromCommand(extraargs):
3efd08
+    r, w = os.pipe()
3efd08
+    set_fd_inheritable(r)
3efd08
+    r = fdopen(r, "r")
3efd08
+    set_fd_inheritable(w)
3efd08
+    w = fdopen(w, "w")
3efd08
+    pid = os.fork()
3efd08
+    if pid:
3efd08
+        w.close()
3efd08
+        l = list(iterload(r))
3efd08
+        l = [o[0] for o in l]
3efd08
+        r.close()
3efd08
+        return l
3efd08
+    else:
3efd08
+        r.close()
3efd08
+        args = ["tools_messages_tests",
3efd08
+                "--machine-readable=fd:%d" % w.fileno()] + extraargs
3efd08
+        os.execvp("./" + exe, args)
3efd08
+
3efd08
+
3efd08
+@skipUnlessHasModule('iso8601')
3efd08
+class TestParseToolsMessages(unittest.TestCase):
3efd08
+    def check_json(self, json, typ, msg):
3efd08
+        import iso8601
3efd08
+        # Check the type.
3efd08
+        jsontype = json.pop("type")
3efd08
+        self.assertEqual(jsontype, typ)
3efd08
+        # Check the message.
3efd08
+        jsonmsg = json.pop("message")
3efd08
+        self.assertEqual(jsonmsg, msg)
3efd08
+        # Check the timestamp.
3efd08
+        jsonts = json.pop("timestamp")
3efd08
+        dt = iso8601.parse_date(jsonts)
3efd08
+        now = datetime.datetime.now(dt.tzinfo)
3efd08
+        self.assertGreater(now, dt)
3efd08
+        # Check there are no more keys left (and thus not previously tested).
3efd08
+        self.assertEqual(len(json), 0)
3efd08
+
3efd08
+    def test_messages(self):
3efd08
+        objects = loadJsonFromCommand([])
3efd08
+        self.assertEqual(len(objects), 4)
3efd08
+        self.check_json(objects[0], "message", "Starting")
3efd08
+        self.check_json(objects[1], "info", "An information message")
3efd08
+        self.check_json(objects[2], "warning", "Warning: message here")
3efd08
+        self.check_json(objects[3], "message", "Finishing")
3efd08
+
3efd08
+    def test_error(self):
3efd08
+        objects = loadJsonFromCommand(["--error"])
3efd08
+        self.assertEqual(len(objects), 1)
3efd08
+        self.check_json(objects[0], "error", "Error!")
3efd08
+
3efd08
+
3efd08
+if __name__ == '__main__':
3efd08
+    unittest.main()
3efd08
diff --git a/common/mltools/test-tools-messages.sh b/common/mltools/test-tools-messages.sh
3efd08
new file mode 100755
3efd08
index 000000000..0e24d6ce9
3efd08
--- /dev/null
3efd08
+++ b/common/mltools/test-tools-messages.sh
3efd08
@@ -0,0 +1,28 @@
3efd08
+#!/bin/bash -
3efd08
+# libguestfs
3efd08
+# Copyright (C) 2019 Red Hat Inc.
3efd08
+#
3efd08
+# This program is free software; you can redistribute it and/or modify
3efd08
+# it under the terms of the GNU General Public License as published by
3efd08
+# the Free Software Foundation; either version 2 of the License, or
3efd08
+# (at your option) any later version.
3efd08
+#
3efd08
+# This program is distributed in the hope that it will be useful,
3efd08
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3efd08
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
3efd08
+# GNU General Public License for more details.
3efd08
+#
3efd08
+# You should have received a copy of the GNU General Public License
3efd08
+# along with this program; if not, write to the Free Software
3efd08
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
3efd08
+
3efd08
+# Test the --machine-readable functionality of the module Tools_utils.
3efd08
+# See also: machine_readable_tests.ml
3efd08
+
3efd08
+set -e
3efd08
+set -x
3efd08
+
3efd08
+$TEST_FUNCTIONS
3efd08
+skip_if_skipped
3efd08
+
3efd08
+$PYTHON parse_tools_messages_test.py
3efd08
diff --git a/common/mltools/tools_messages_tests.ml b/common/mltools/tools_messages_tests.ml
3efd08
new file mode 100644
3efd08
index 000000000..d5f9be89b
3efd08
--- /dev/null
3efd08
+++ b/common/mltools/tools_messages_tests.ml
3efd08
@@ -0,0 +1,46 @@
3efd08
+(*
3efd08
+ * Copyright (C) 2019 Red Hat Inc.
3efd08
+ *
3efd08
+ * This program is free software; you can redistribute it and/or modify
3efd08
+ * it under the terms of the GNU General Public License as published by
3efd08
+ * the Free Software Foundation; either version 2 of the License, or
3efd08
+ * (at your option) any later version.
3efd08
+ *
3efd08
+ * This program is distributed in the hope that it will be useful,
3efd08
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3efd08
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
3efd08
+ * GNU General Public License for more details.
3efd08
+ *
3efd08
+ * You should have received a copy of the GNU General Public License along
3efd08
+ * with this program; if not, write to the Free Software Foundation, Inc.,
3efd08
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
3efd08
+ *)
3efd08
+
3efd08
+(* Test the message output for tools of the module Tools_utils.
3efd08
+ * The tests are controlled by the test-tools-messages.sh script.
3efd08
+ *)
3efd08
+
3efd08
+open Printf
3efd08
+
3efd08
+open Std_utils
3efd08
+open Tools_utils
3efd08
+open Getopt.OptionName
3efd08
+
3efd08
+let is_error = ref false
3efd08
+
3efd08
+let args = [
3efd08
+  [ L "error" ], Getopt.Set is_error, "Only print the error";
3efd08
+]
3efd08
+let usage_msg = sprintf "%s: test the message outputs" prog
3efd08
+
3efd08
+let opthandle = create_standard_options args ~machine_readable:true usage_msg
3efd08
+let () =
3efd08
+  Getopt.parse opthandle.getopt;
3efd08
+
3efd08
+  if !is_error then
3efd08
+    error "Error!";
3efd08
+
3efd08
+  message "Starting";
3efd08
+  info "An information message";
3efd08
+  warning "Warning: message here";
3efd08
+  message "Finishing"
3efd08
diff --git a/common/mltools/tools_utils-c.c b/common/mltools/tools_utils-c.c
3efd08
index c88c95082..b015dcace 100644
3efd08
--- a/common/mltools/tools_utils-c.c
3efd08
+++ b/common/mltools/tools_utils-c.c
3efd08
@@ -23,6 +23,8 @@
3efd08
 #include <unistd.h>
3efd08
 #include <errno.h>
3efd08
 #include <error.h>
3efd08
+#include <time.h>
3efd08
+#include <string.h>
3efd08
 
3efd08
 #include <caml/alloc.h>
3efd08
 #include <caml/fail.h>
3efd08
@@ -37,6 +39,7 @@
3efd08
 extern value guestfs_int_mllib_inspect_decrypt (value gv, value gpv, value keysv);
3efd08
 extern value guestfs_int_mllib_set_echo_keys (value unitv);
3efd08
 extern value guestfs_int_mllib_set_keys_from_stdin (value unitv);
3efd08
+extern value guestfs_int_mllib_rfc3999_date_time_string (value unitv);
3efd08
 
3efd08
 /* Interface with the guestfish inspection and decryption code. */
3efd08
 int echo_keys = 0;
3efd08
@@ -103,3 +106,51 @@ guestfs_int_mllib_set_keys_from_stdin (value unitv)
3efd08
   keys_from_stdin = 1;
3efd08
   return Val_unit;
3efd08
 }
3efd08
+
3efd08
+value
3efd08
+guestfs_int_mllib_rfc3999_date_time_string (value unitv)
3efd08
+{
3efd08
+  CAMLparam1 (unitv);
3efd08
+  char buf[64];
3efd08
+  struct timespec ts;
3efd08
+  struct tm tm;
3efd08
+  size_t ret;
3efd08
+  size_t total = 0;
3efd08
+
3efd08
+  if (clock_gettime (CLOCK_REALTIME, &ts) == -1)
3efd08
+    unix_error (errno, (char *) "clock_gettime", Val_unit);
3efd08
+
3efd08
+  if (localtime_r (&ts.tv_sec, &tm) == NULL)
3efd08
+    unix_error (errno, (char *) "localtime_r", caml_copy_int64 (ts.tv_sec));
3efd08
+
3efd08
+  /* Sadly strftime does not support nanoseconds, so what we do is:
3efd08
+   * - stringify everything before the nanoseconds
3efd08
+   * - print the nanoseconds
3efd08
+   * - stringify the rest (i.e. the timezone)
3efd08
+   * then place ':' between the hours, and the minutes of the
3efd08
+   * timezone offset.
3efd08
+   */
3efd08
+
3efd08
+  ret = strftime (buf, sizeof (buf), "%Y-%m-%dT%H:%M:%S.", &tm;;
3efd08
+  if (ret == 0)
3efd08
+    unix_error (errno, (char *) "strftime", Val_unit);
3efd08
+  total += ret;
3efd08
+
3efd08
+  ret = snprintf (buf + total, sizeof (buf) - total, "%09ld", ts.tv_nsec);
3efd08
+  if (ret == 0)
3efd08
+    unix_error (errno, (char *) "sprintf", caml_copy_int64 (ts.tv_nsec));
3efd08
+  total += ret;
3efd08
+
3efd08
+  ret = strftime (buf + total, sizeof (buf) - total, "%z", &tm;;
3efd08
+  if (ret == 0)
3efd08
+    unix_error (errno, (char *) "strftime", Val_unit);
3efd08
+  total += ret;
3efd08
+
3efd08
+  /* Move the timezone minutes one character to the right, moving the
3efd08
+   * null character too.
3efd08
+   */
3efd08
+  memmove (buf + total - 1, buf + total - 2, 3);
3efd08
+  buf[total - 2] = ':';
3efd08
+
3efd08
+  CAMLreturn (caml_copy_string (buf));
3efd08
+}
3efd08
diff --git a/common/mltools/tools_utils.ml b/common/mltools/tools_utils.ml
3efd08
index 35478f39e..de42df600 100644
3efd08
--- a/common/mltools/tools_utils.ml
3efd08
+++ b/common/mltools/tools_utils.ml
3efd08
@@ -32,6 +32,7 @@ and key_store_key =
3efd08
 external c_inspect_decrypt : Guestfs.t -> int64 -> (string * key_store_key) list -> unit = "guestfs_int_mllib_inspect_decrypt"
3efd08
 external c_set_echo_keys : unit -> unit = "guestfs_int_mllib_set_echo_keys" "noalloc"
3efd08
 external c_set_keys_from_stdin : unit -> unit = "guestfs_int_mllib_set_keys_from_stdin" "noalloc"
3efd08
+external c_rfc3999_date_time_string : unit -> string = "guestfs_int_mllib_rfc3999_date_time_string"
3efd08
 
3efd08
 type machine_readable_fn = {
3efd08
   pr : 'a. ('a, unit, string, unit) format4 -> 'a;
3efd08
@@ -86,12 +87,24 @@ let ansi_magenta ?(chan = stdout) () =
3efd08
 let ansi_restore ?(chan = stdout) () =
3efd08
   if colours () || istty chan then output_string chan "\x1b[0m"
3efd08
 
3efd08
+let log_as_json msgtype msg =
3efd08
+  match machine_readable () with
3efd08
+  | None -> ()
3efd08
+  | Some { pr } ->
3efd08
+    let json = [
3efd08
+      "message", JSON.String msg;
3efd08
+      "timestamp", JSON.String (c_rfc3999_date_time_string ());
3efd08
+      "type", JSON.String msgtype;
3efd08
+    ] in
3efd08
+    pr "%s\n" (JSON.string_of_doc ~fmt:JSON.Compact json)
3efd08
+
3efd08
 (* Timestamped progress messages, used for ordinary messages when not
3efd08
  * --quiet.
3efd08
  *)
3efd08
 let start_t = Unix.gettimeofday ()
3efd08
 let message fs =
3efd08
   let display str =
3efd08
+    log_as_json "message" str;
3efd08
     if not (quiet ()) then (
3efd08
       let t = sprintf "%.1f" (Unix.gettimeofday () -. start_t) in
3efd08
       printf "[%6s] " t;
3efd08
@@ -106,6 +119,7 @@ let message fs =
3efd08
 (* Error messages etc. *)
3efd08
 let error ?(exit_code = 1) fs =
3efd08
   let display str =
3efd08
+    log_as_json "error" str;
3efd08
     let chan = stderr in
3efd08
     ansi_red ~chan ();
3efd08
     wrap ~chan (sprintf (f_"%s: error: %s") prog str);
3efd08
@@ -124,6 +138,7 @@ let error ?(exit_code = 1) fs =
3efd08
 
3efd08
 let warning fs =
3efd08
   let display str =
3efd08
+    log_as_json "warning" str;
3efd08
     let chan = stdout in
3efd08
     ansi_blue ~chan ();
3efd08
     wrap ~chan (sprintf (f_"%s: warning: %s") prog str);
3efd08
@@ -134,6 +149,7 @@ let warning fs =
3efd08
 
3efd08
 let info fs =
3efd08
   let display str =
3efd08
+    log_as_json "info" str;
3efd08
     let chan = stdout in
3efd08
     ansi_magenta ~chan ();
3efd08
     wrap ~chan (sprintf (f_"%s: %s") prog str);
3efd08
diff --git a/lib/guestfs.pod b/lib/guestfs.pod
3efd08
index f11028466..3c1d635c5 100644
3efd08
--- a/lib/guestfs.pod
3efd08
+++ b/lib/guestfs.pod
3efd08
@@ -3279,6 +3279,25 @@ Some of the tools support a I<--machine-readable> option, which is
3efd08
 generally used to make the output more machine friendly, for easier
3efd08
 parsing for example.  By default, this output goes to stdout.
3efd08
 
3efd08
+When using the I<--machine-readable> option, the progress,
3efd08
+information, warning, and error messages are also printed in JSON
3efd08
+format for easier log tracking.  Thus, it is highly recommended to
3efd08
+redirect the machine-readable output to a different stream.  The
3efd08
+format of these JSON messages is like the following (actually printed
3efd08
+within a single line, below it is indented for readability):
3efd08
+
3efd08
+ {
3efd08
+   "message": "Finishing off",
3efd08
+   "timestamp": "2019-03-22T14:46:49.067294446+01:00",
3efd08
+   "type": "message"
3efd08
+ }
3efd08
+
3efd08
+C<type> can be: C<message> for progress messages, C<info> for
3efd08
+information messages, C<warning> for warning messages, and C<error>
3efd08
+for error message.
3efd08
+C<timestamp> is the L<RFC 3999|https://www.ietf.org/rfc/rfc3339.txt>
3efd08
+timestamp of the message.
3efd08
+
3efd08
 In addition to that, a subset of these tools support an extra string
3efd08
 passed to the I<--machine-readable> option: this string specifies
3efd08
 where the machine-readable output will go.
3efd08
-- 
3efd08
2.25.4
3efd08