diff --git a/include/monitor/stats.h b/include/monitor/stats.h
new file mode 100644
index 0000000000..d4b57322eb
--- /dev/null
+++ b/include/monitor/stats.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2.
+ * See the COPYING file in the top-level directory.
+ */
+
+#ifndef STATS_H
+#define STATS_H
+
+/*
+ * Add qmp stats callbacks to the stats_callbacks list.
+ *
+ * @provider: stats provider
+ *
+ * @stats_fn: routine to query stats:
+ *    void (*stats_fn)(StatsResults *results, StatsFilter *filter, Error **errp)
+ *
+ * @schema_fn: routine to query stat schemas:
+ *    void (*schemas_fn)(StatsSchemaResult *results, Error **errp)
+ */
+void add_stats_callbacks(StatsProvider provider,
+                         void (*stats_fn)(StatsResults *, StatsFilter *,
+                                          Error **),
+                         void (*schemas_fn)(StatsSchemaResult *, Error **));
+
+/* Stats helpers routines */
+StatsResultsEntry *add_vm_stats_entry(StatsResults *, StatsProvider);
+StatsResultsEntry *add_vcpu_stats_entry(StatsResults *, StatsProvider, char *);
+StatsSchemaProvider *add_vm_stats_schema(StatsSchemaResult *, StatsProvider);
+StatsSchemaProvider *add_vcpu_stats_schema(StatsSchemaResult *, StatsProvider);
+
+bool stat_name_filter(StatsFilter *, StatsTarget, char *);
+bool stat_cpu_filter(StatsFilter *, char *);
+
+#endif /* STATS_H */
diff --git a/monitor/qmp-cmds.c b/monitor/qmp-cmds.c
index db4d186448..bd562edfb8 100644
--- a/monitor/qmp-cmds.c
+++ b/monitor/qmp-cmds.c
@@ -18,6 +18,7 @@
 #include "qemu/cutils.h"
 #include "qemu/option.h"
 #include "monitor/monitor.h"
+#include "monitor/stats.h"
 #include "sysemu/sysemu.h"
 #include "qemu/config-file.h"
 #include "qemu/uuid.h"
@@ -448,3 +449,185 @@ HumanReadableText *qmp_x_query_irq(Error **errp)
 
     return human_readable_text_from_str(buf);
 }
+
+typedef struct StatsCallbacks {
+    StatsProvider provider;
+    void (*stats_cb)(StatsResults *, StatsFilter *, Error **);
+    void (*schemas_cb)(StatsSchemaResult *, Error **);
+    QTAILQ_ENTRY(StatsCallbacks) next;
+} StatsCallbacks;
+
+static QTAILQ_HEAD(, StatsCallbacks) stats_callbacks =
+    QTAILQ_HEAD_INITIALIZER(stats_callbacks);
+
+void add_stats_callbacks(StatsProvider provider,
+                         void (*stats_fn)(StatsResults *, StatsFilter*,
+                                          Error **),
+                         void (*schemas_fn)(StatsSchemaResult *, Error **))
+{
+    StatsCallbacks *entry = g_malloc0(sizeof(*entry));
+    entry->provider = provider;
+    entry->stats_cb = stats_fn;
+    entry->schemas_cb = schemas_fn;
+
+    QTAILQ_INSERT_TAIL(&stats_callbacks, entry, next);
+}
+
+StatsResults *qmp_query_stats(StatsFilter *filter, Error **errp)
+{
+    StatsResults *stats_results = g_malloc0(sizeof(*stats_results));
+    StatsCallbacks *entry;
+    StatsProvider provider = STATS_PROVIDER__MAX;
+
+    if (filter->target == STATS_TARGET_VM &&
+        filter->u.vm.has_provider) {
+        provider = filter->u.vm.provider;
+    } else if (filter->target == STATS_TARGET_VCPU &&
+               filter->u.vcpu.has_provider) {
+        provider = filter->u.vcpu.provider;
+    }
+
+    QTAILQ_FOREACH(entry, &stats_callbacks, next) {
+        if (provider != STATS_PROVIDER__MAX && (provider != entry->provider)) {
+            continue;
+        }
+        entry->stats_cb(stats_results, filter, errp);
+    }
+
+    return stats_results;
+}
+
+StatsSchemaResult *qmp_query_stats_schemas(bool has_provider,
+                                           StatsProvider provider,
+                                           Error **errp)
+{
+    StatsSchemaResult *stats_results = g_malloc0(sizeof(*stats_results));
+    StatsCallbacks *entry;
+
+    QTAILQ_FOREACH(entry, &stats_callbacks, next) {
+        if (has_provider && (provider != entry->provider)) {
+            continue;
+        }
+        entry->schemas_cb(stats_results, errp);
+    }
+
+    return stats_results;
+}
+
+StatsResultsEntry *add_vm_stats_entry(StatsResults *stats_results,
+                                      StatsProvider provider)
+{
+    StatsResultsEntry *entry = g_malloc0(sizeof(*entry));
+    stats_results->target = STATS_TARGET_VM;
+    entry->provider = provider;
+    QAPI_LIST_PREPEND(stats_results->u.vm.list, entry);
+
+    return entry;
+}
+
+StatsResultsEntry *add_vcpu_stats_entry(StatsResults *stats_results,
+                                        StatsProvider provider,
+                                        char *path)
+{
+    StatsResultsEntry *entry = g_malloc0(sizeof(*entry));
+    stats_results->target = STATS_TARGET_VCPU;
+    entry->provider = provider;
+
+    /*
+     * Find the corresponding vCPU entry and add to its list. Else, create it.
+     */
+    VCPUResultsEntryList **tailp = &stats_results->u.vcpu.list;
+    VCPUResultsEntryList *tail;
+
+    for (tail = *tailp; tail; tail = tail->next) {
+        if (g_str_equal(tail->value->path, path)) {
+            /* Add to the existing vCPU list */
+            QAPI_LIST_PREPEND(tail->value->list, entry);
+            goto done;
+        }
+        tailp = &tail->next;
+    }
+    /* Create and populate a new entry for the vCPU */
+    VCPUResultsEntry *value = g_malloc0(sizeof(*value));
+    value->path = g_strdup(path);
+    value->list = g_malloc0(sizeof(*value->list));
+    value->list->value = entry;
+    QAPI_LIST_APPEND(tailp, value);
+
+done:
+    return entry;
+}
+
+StatsSchemaProvider *add_vm_stats_schema(StatsSchemaResult *schema_results,
+                                         StatsProvider provider)
+{
+    StatsSchemaProvider *entry = g_malloc0(sizeof(*entry));
+    schema_results->has_vm = true;
+    entry->provider = provider;
+    QAPI_LIST_PREPEND(schema_results->vm, entry);
+
+    return entry;
+}
+
+StatsSchemaProvider *add_vcpu_stats_schema(StatsSchemaResult *schema_results,
+                                           StatsProvider provider)
+{
+    StatsSchemaProvider *entry = g_malloc0(sizeof(*entry));
+    schema_results->has_vcpu = true;
+    entry->provider = provider;
+    QAPI_LIST_PREPEND(schema_results->vcpu, entry);
+
+    return entry;
+}
+
+/* True if stat doesn't match a requested name */
+bool stat_name_filter(StatsFilter *filter, StatsTarget type, char *name)
+{
+    strList *str_list = NULL;
+
+    if (type == STATS_TARGET_VM) {
+        if (filter->target != STATS_TARGET_VM) {
+            return false;
+        }
+        if (!filter->u.vm.has_fields) {
+            return false;
+        }
+        str_list = filter->u.vm.fields;
+    } else if (type == STATS_TARGET_VCPU) {
+        if (filter->target != STATS_TARGET_VCPU) {
+            return false;
+        }
+        if (!filter->u.vcpu.has_fields) {
+            return false;
+        }
+        str_list = filter->u.vcpu.fields;
+    }
+
+    for (; str_list; str_list = str_list->next) {
+        if (g_str_equal(name, str_list->value)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/* True if cpu qom path doesn't match a requested path */
+bool stat_cpu_filter(StatsFilter *filter, char *path)
+{
+    strList *list;
+
+    if (filter->target != STATS_TARGET_VCPU) {
+        return false;
+    }
+
+    if (!filter->u.vcpu.has_paths) {
+        return false;
+    }
+
+    for (list = filter->u.vcpu.paths; list; list = list->next) {
+        if (g_str_equal(list->value, path)) {
+            return false;
+        }
+    }
+    return true;
+}
diff --git a/qapi/misc.json b/qapi/misc.json
index e8054f415b..8d326464f0 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -527,3 +527,256 @@
  'data': { '*option': 'str' },
  'returns': ['CommandLineOptionInfo'],
  'allow-preconfig': true }
+
+##
+# @StatType:
+#
+# Enumeration of stat types
+# @cumulative: stat is cumulative; value can only increase.
+# @instant: stat is instantaneous; value can increase or decrease.
+# @peak: stat is the peak value; value can only increase.
+#
+# Since: 7.0
+##
+{ 'enum' : 'StatType',
+  'data' : [ 'cumulative', 'instant', 'peak',
+             'linear-hist', 'log-hist', 'unknown' ] }
+
+##
+# @StatUnit:
+#
+# Enumeration of stat units
+# @bytes: stat reported in bytes.
+# @seconds: stat reported in seconds.
+# @cycles: stat reported in clock cycles.
+# @none: no unit for this stat.
+#
+# Since: 7.0
+##
+{ 'enum' : 'StatUnit',
+  'data' : [ 'none', 'bytes', 'seconds', 'cycles', 'unknown' ] }
+
+##
+# @StatBase:
+#
+# Enumeration of stat base
+#
+# Since: 7.0
+##
+{ 'enum' : 'StatBase',
+  'data' : [ 'pow10', 'pow2', 'unknown' ] }
+
+##
+# @StatsProvider:
+#
+# Enumeration of stat providers.
+#
+# Since: 7.0
+##
+{ 'enum': 'StatsProvider',
+  'data': [ ] }
+
+##
+# @StatsTarget:
+#
+# Enumeration of stat targets.
+# @vm: stat is per vm.
+# @vcpu: stat is per vcpu.
+#
+# Since: 7.0
+##
+{ 'enum': 'StatsTarget',
+  'data': [ 'vm', 'vcpu' ] }
+
+##
+# @StatsVCPURequest:
+#
+# vcpu specific filter element.
+# @paths: list of qom paths.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsVCPURequest',
+  'base': 'StatsRequest',
+  'data': { '*paths': [ 'str' ] } }
+
+##
+# @StatsRequest:
+#
+# Stats filter element.
+# @provider: stat provider.
+# @fields: list of stat names.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsRequest',
+  'data': { '*provider': 'StatsProvider',
+            '*fields': [ 'str' ] } }
+
+##
+# @StatsFilter:
+#
+# Target specific filter.
+#
+# Since: 7.0
+##
+{ 'union': 'StatsFilter',
+  'base': { 'target': 'StatsTarget' },
+  'discriminator': 'target',
+  'data': { 'vcpu': 'StatsVCPURequest',
+            'vm': 'StatsRequest' } }
+
+##
+# @StatsValueArray:
+#
+# uint64 list for StatsValue.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsValueArray',
+  'data': { 'list' : [ 'uint64' ] } }
+
+##
+# @StatsValue:
+#
+# @scalar: stat is single uint64.
+# @list: stat is a list of uint64.
+#
+# Since: 7.0
+##
+{ 'alternate': 'StatsValue',
+  'data': { 'scalar': 'uint64',
+            'list': 'StatsValueArray' } }
+
+##
+# @Stats:
+#
+# @name: name of stat.
+# @value: stat value.
+#
+# Since: 7.0
+##
+{ 'struct': 'Stats',
+  'data': { 'name': 'str',
+            'value' : 'StatsValue' } }
+
+##
+# @StatsResultsEntry:
+#
+# @provider: stat provider.
+# @stats: list of stats.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsResultsEntry',
+  'data': { 'provider': 'StatsProvider',
+            'stats': [ 'Stats' ] } }
+
+##
+# @VCPUResultsEntry:
+#
+# @path: qom path.
+# @list: per provider @StatsResultEntry list.
+#
+# Since: 7.0
+##
+{ 'struct': 'VCPUResultsEntry',
+  'data': { 'path': 'str',
+            'list': [ 'StatsResultsEntry' ] } }
+
+##
+# @VMStatsResults:
+#
+# Since: 7.0
+##
+{ 'struct': 'VMStatsResults',
+  'data': { 'list' : [ 'StatsResultsEntry' ] } }
+
+##
+# @VCPUStatsResults:
+#
+# Since: 7.0
+##
+{ 'struct': 'VCPUStatsResults',
+  'data': { 'list': [ 'VCPUResultsEntry' ] } }
+
+##
+# @StatsResults:
+#
+# Target specific results.
+#
+# Since: 7.0
+##
+{ 'union': 'StatsResults',
+  'base': { 'target': 'StatsTarget' },
+  'discriminator': 'target',
+  'data': { 'vcpu': 'VCPUStatsResults',
+            'vm': 'VMStatsResults' } }
+
+##
+# @query-stats:
+#
+# data: @StatsFilter
+# Returns: @StatsResults
+#
+# Since: 7.0
+##
+{ 'command': 'query-stats',
+  'data': 'StatsFilter',
+  'boxed': true,
+  'returns': 'StatsResults' }
+
+##
+# @StatsSchemaValue:
+#
+# Individual stat schema.
+# @name: stat name.
+# @type: @StatType
+# @unit: @StatUnit
+# @base: @StatBase
+# @exponent: Used together with @base.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsSchemaValue',
+  'data': { 'name': 'str',
+            'type': 'StatType',
+            'unit': 'StatUnit',
+            'base': 'StatBase',
+            'exponent': 'int16' } }
+
+##
+# @StatsSchemaProvider:
+#
+# @provider: @StatsProvider.
+# @stats: list of stats.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsSchemaProvider',
+  'data': { 'provider': 'StatsProvider',
+            'stats': [ 'StatsSchemaValue' ] } }
+
+##
+# @StatsSchemaResult:
+#
+# @vm: vm stats schemas.
+# @vcpu: vcpu stats schemas.
+#
+# Since: 7.0
+##
+{ 'struct': 'StatsSchemaResult',
+  'data': { '*vm': [ 'StatsSchemaProvider' ],
+            '*vcpu': [ 'StatsSchemaProvider' ] } }
+
+##
+# @query-stats-schemas:
+#
+# Query Stats schemas.
+# Returns @StatsSchemaResult
+#
+# Since: 7.0
+##
+{ 'command': 'query-stats-schemas',
+  'data': { '*provider': 'StatsProvider' },
+  'returns': 'StatsSchemaResult' }
