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

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