diff --git a/include/qemu/log.h b/include/qemu/log.h
index b097a6cae1..a91105b2ad 100644
--- a/include/qemu/log.h
+++ b/include/qemu/log.h
@@ -45,6 +45,7 @@ static inline bool qemu_log_separate(void)
 /* LOG_TRACE (1 << 15) is defined in log-for-trace.h */
 #define CPU_LOG_TB_OP_IND  (1 << 16)
 #define CPU_LOG_TB_FPU     (1 << 17)
+#define CPU_LOG_PLUGIN     (1 << 18)
 
 /* Lock output for a series of related logs.  Since this is not needed
  * for a single qemu_log / qemu_log_mask / qemu_log_mask_and_addr, we
diff --git a/include/qemu/qemu-plugin.h b/include/qemu/qemu-plugin.h
index 719d7054a1..060e253b74 100644
--- a/include/qemu/qemu-plugin.h
+++ b/include/qemu/qemu-plugin.h
@@ -379,4 +379,10 @@ int qemu_plugin_n_vcpus(void);
 /* returns -1 in user-mode */
 int qemu_plugin_n_max_vcpus(void);
 
+/**
+ * qemu_plugin_outs() - output string via QEMU's logging system
+ * @string: a string
+ */
+void qemu_plugin_outs(const char *string);
+
 #endif /* QEMU_PLUGIN_API_H */
diff --git a/plugins/api.c b/plugins/api.c
index a9fa229369..701c232668 100644
--- a/plugins/api.c
+++ b/plugins/api.c
@@ -324,3 +324,11 @@ int qemu_plugin_n_max_vcpus(void)
     return get_ms()->smp.max_cpus;
 #endif
 }
+
+/*
+ * Plugin output
+ */
+void qemu_plugin_outs(const char *string)
+{
+    qemu_log_mask(CPU_LOG_PLUGIN, "%s", string);
+}
diff --git a/plugins/qemu-plugins.symbols b/plugins/qemu-plugins.symbols
index 267ec381b4..4bdb381f48 100644
--- a/plugins/qemu-plugins.symbols
+++ b/plugins/qemu-plugins.symbols
@@ -36,4 +36,5 @@
   qemu_plugin_vcpu_for_each;
   qemu_plugin_n_vcpus;
   qemu_plugin_n_max_vcpus;
+  qemu_plugin_outs;
 };
diff --git a/tests/plugin/bb.c b/tests/plugin/bb.c
index 4a6d6ca0bc..45e1de5bd6 100644
--- a/tests/plugin/bb.c
+++ b/tests/plugin/bb.c
@@ -10,18 +10,20 @@
 #include <string.h>
 #include <unistd.h>
 #include <stdio.h>
+#include <glib.h>
 
 #include <qemu-plugin.h>
 
 static uint64_t bb_count;
 static uint64_t insn_count;
-static int stdout_fd;
 static bool do_inline;
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    dprintf(stdout_fd, "bb's: %" PRIu64", insns: %" PRIu64 "\n",
-            bb_count, insn_count);
+    g_autofree gchar *out;
+    out = g_strdup_printf("bb's: %" PRIu64", insns: %" PRIu64 "\n",
+                          bb_count, insn_count);
+    qemu_plugin_outs(out);
 }
 
 static void vcpu_tb_exec(unsigned int cpu_index, void *udata)
@@ -56,10 +58,6 @@ QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
         do_inline = true;
     }
 
-    /* to be used when in the exit hook */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
-
     qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
     qemu_plugin_register_atexit_cb(id, plugin_exit, NULL);
     return 0;
diff --git a/tests/plugin/hotblocks.c b/tests/plugin/hotblocks.c
index 57bea765b3..1bd183849a 100644
--- a/tests/plugin/hotblocks.c
+++ b/tests/plugin/hotblocks.c
@@ -16,7 +16,6 @@
 #include <qemu-plugin.h>
 
 static bool do_inline;
-static int stdout_fd;
 
 /* Plugins need to take care of their own locking */
 static GMutex lock;
@@ -47,7 +46,7 @@ static gint cmp_exec_count(gconstpointer a, gconstpointer b)
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    GString *report = g_string_new("collected ");
+    g_autoptr(GString) report = g_string_new("collected ");
     GList *counts, *it;
     int i;
 
@@ -71,8 +70,7 @@ static void plugin_exit(qemu_plugin_id_t id, void *p)
         g_mutex_unlock(&lock);
     }
 
-    dprintf(stdout_fd, "%s", report->str);
-    g_string_free(report, true);
+    qemu_plugin_outs(report->str);
 }
 
 static void plugin_init(void)
@@ -137,10 +135,6 @@ int qemu_plugin_install(qemu_plugin_id_t id, const qemu_info_t *info,
         do_inline = true;
     }
 
-    /* to be used when in the exit hook */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
-
     plugin_init();
 
     qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
diff --git a/tests/plugin/hotpages.c b/tests/plugin/hotpages.c
index 99bb4be07c..70917ea174 100644
--- a/tests/plugin/hotpages.c
+++ b/tests/plugin/hotpages.c
@@ -22,7 +22,6 @@
 
 static uint64_t page_size = 4096;
 static uint64_t page_mask;
-static int stdout_fd;
 static int limit = 50;
 static enum qemu_plugin_mem_rw rw = QEMU_PLUGIN_MEM_RW;
 
@@ -69,7 +68,7 @@ static gint cmp_access_count(gconstpointer a, gconstpointer b)
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    GString *report = g_string_new("Addr, RCPUs, Reads, WCPUs, Writes\n");
+    g_autoptr(GString) report = g_string_new("Addr, RCPUs, Reads, WCPUs, Writes\n");
     int i;
     GList *counts;
 
@@ -91,8 +90,7 @@ static void plugin_exit(qemu_plugin_id_t id, void *p)
         g_list_free(it);
     }
 
-    dprintf(stdout_fd, "%s", report->str);
-    g_string_free(report, true);
+    qemu_plugin_outs(report->str);
 }
 
 static void plugin_init(void)
@@ -154,10 +152,6 @@ int qemu_plugin_install(qemu_plugin_id_t id, const qemu_info_t *info,
 {
     int i;
 
-    /* to be used when in the exit hook */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
-
     for (i = 0; i < argc; i++) {
         char *opt = argv[i];
         if (g_strcmp0(opt, "reads") == 0) {
@@ -167,7 +161,7 @@ int qemu_plugin_install(qemu_plugin_id_t id, const qemu_info_t *info,
         } else if (g_str_has_prefix(opt, "pagesize=")) {
             page_size = g_ascii_strtoull(opt + 9, NULL, 10);
         } else {
-            dprintf(stdout_fd, "option parsing failed: %s\n", opt);
+            fprintf(stderr, "option parsing failed: %s\n", opt);
             return -1;
         }
     }
diff --git a/tests/plugin/howvec.c b/tests/plugin/howvec.c
index 9ec0d25e3b..96db546b4d 100644
--- a/tests/plugin/howvec.c
+++ b/tests/plugin/howvec.c
@@ -29,7 +29,6 @@ typedef enum {
 } CountType;
 
 static int limit = 50;
-static int stdout_fd;
 static bool do_inline;
 static bool verbose;
 
@@ -163,7 +162,7 @@ static gint cmp_exec_count(gconstpointer a, gconstpointer b)
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    GString *report = g_string_new("Instruction Classes:\n");
+    g_autoptr(GString) report = g_string_new("Instruction Classes:\n");
     int i;
     GList *counts;
     InsnClassExecCount *class = NULL;
@@ -211,8 +210,7 @@ static void plugin_exit(qemu_plugin_id_t id, void *p)
         g_list_free(it);
     }
 
-    dprintf(stdout_fd, "%s", report->str);
-    g_string_free(report, true);
+    qemu_plugin_outs(report->str);
 }
 
 static void plugin_init(void)
@@ -270,11 +268,6 @@ static uint64_t * find_counter(struct qemu_plugin_insn *insn)
             icount->insn = qemu_plugin_insn_disas(insn);
             icount->class = class;
 
-            if (verbose) {
-                dprintf(stdout_fd, "adding for %s (%#08x @ %#20lx from %s)\n",
-                        icount->insn, opcode, qemu_plugin_insn_vaddr(insn),
-                        class->class);
-            }
             g_hash_table_insert(insns, GUINT_TO_POINTER(opcode),
                                 (gpointer) icount);
         }
@@ -350,10 +343,6 @@ QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
         }
     }
 
-    /* to be used when in the exit hook */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
-
     plugin_init();
 
     qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
diff --git a/tests/plugin/insn.c b/tests/plugin/insn.c
index 9cfa7d0e53..e5fd07fb64 100644
--- a/tests/plugin/insn.c
+++ b/tests/plugin/insn.c
@@ -10,10 +10,10 @@
 #include <string.h>
 #include <unistd.h>
 #include <stdio.h>
+#include <glib.h>
 
 #include <qemu-plugin.h>
 
-static int stdout_fd;
 static uint64_t insn_count;
 static bool do_inline;
 
@@ -42,7 +42,9 @@ static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    dprintf(stdout_fd, "insns: %" PRIu64 "\n", insn_count);
+    g_autofree gchar *out;
+    out = g_strdup_printf("insns: %" PRIu64 "\n", insn_count);
+    qemu_plugin_outs(out);
 }
 
 QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
@@ -53,10 +55,6 @@ QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
         do_inline = true;
     }
 
-    /* to be used when in the exit hook */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
-
     qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
     qemu_plugin_register_atexit_cb(id, plugin_exit, NULL);
     return 0;
diff --git a/tests/plugin/mem.c b/tests/plugin/mem.c
index fdf4347c5e..d967388989 100644
--- a/tests/plugin/mem.c
+++ b/tests/plugin/mem.c
@@ -10,22 +10,25 @@
 #include <string.h>
 #include <unistd.h>
 #include <stdio.h>
+#include <glib.h>
 
 #include <qemu-plugin.h>
 
 static uint64_t mem_count;
 static uint64_t io_count;
-static int stdout_fd;
 static bool do_inline;
 static bool do_haddr;
 static enum qemu_plugin_mem_rw rw = QEMU_PLUGIN_MEM_RW;
 
 static void plugin_exit(qemu_plugin_id_t id, void *p)
 {
-    dprintf(stdout_fd, "mem accesses: %" PRIu64 "\n", mem_count);
+    g_autoptr(GString) out = g_string_new("");
+
+    g_string_printf(out, "mem accesses: %" PRIu64 "\n", mem_count);
     if (do_haddr) {
-        dprintf(stdout_fd, "io accesses: %" PRIu64 "\n", mem_count);
+        g_string_append_printf(out, "io accesses: %" PRIu64 "\n", mem_count);
     }
+    qemu_plugin_outs(out->str);
 }
 
 static void vcpu_mem(unsigned int cpu_index, qemu_plugin_meminfo_t meminfo,
@@ -87,9 +90,6 @@ QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
             do_inline = true;
         }
     }
-    /* plugin_exit might write to stdout after stdout has been closed */
-    stdout_fd = dup(STDOUT_FILENO);
-    assert(stdout_fd);
 
     qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
     qemu_plugin_register_atexit_cb(id, plugin_exit, NULL);
diff --git a/tests/tcg/Makefile.target b/tests/tcg/Makefile.target
index d25fc64300..3c7421a356 100644
--- a/tests/tcg/Makefile.target
+++ b/tests/tcg/Makefile.target
@@ -153,6 +153,7 @@ run-%: %
 run-plugin-%:
 	$(call run-test, $@, $(QEMU) $(QEMU_OPTS) \
 		-plugin $(PLUGIN_DIR)/$(call extract-plugin,$@) \
+		-d plugin -D $*.pout \
 		 $(call strip-plugin,$<), \
 	"$* on $(TARGET_NAME)")
 else
@@ -168,6 +169,7 @@ run-plugin-%:
 	  $(QEMU) -monitor none -display none \
 		  -chardev file$(COMMA)path=$@.out$(COMMA)id=output \
 	   	  -plugin $(PLUGIN_DIR)/$(call extract-plugin,$@) \
+	    	  -d plugin -D $*.pout \
 	   	  $(QEMU_OPTS) $(call strip-plugin,$<), \
 	  "$* on $(TARGET_NAME)")
 endif
diff --git a/util/log.c b/util/log.c
index 1d1b33f7d9..1ca13059ee 100644
--- a/util/log.c
+++ b/util/log.c
@@ -273,6 +273,9 @@ const QEMULogItem qemu_log_items[] = {
     { CPU_LOG_TB_NOCHAIN, "nochain",
       "do not chain compiled TBs so that \"exec\" and \"cpu\" show\n"
       "complete traces" },
+#ifdef CONFIG_PLUGIN
+    { CPU_LOG_PLUGIN, "plugin", "output from TCG plugins\n"},
+#endif
     { 0, NULL, NULL },
 };
 
