diff --git a/doc/source/general-roles.rst b/doc/source/general-roles.rst
index 02fd736a7..f8d1111e2 100644
--- a/doc/source/general-roles.rst
+++ b/doc/source/general-roles.rst
@@ -10,7 +10,9 @@ General Purpose Roles
 .. zuul:autorole:: configure-mirrors
 .. zuul:autorole:: copy-build-sshkey
 .. zuul:autorole:: download-artifact
+.. zuul:autorole:: dstat-graph
 .. zuul:autorole:: emit-job-header
+.. zuul:autorole:: ensure-dstat-graph
 .. zuul:autorole:: git-prepare-nodecache
 .. zuul:autorole:: log-inventory
 .. zuul:autorole:: mirror-workspace-git-repos
@@ -25,6 +27,7 @@ General Purpose Roles
 .. zuul:autorole:: remove-gpgkey
 .. zuul:autorole:: remove-sshkey
 .. zuul:autorole:: revoke-sudo
+.. zuul:autorole:: run-dstat
 .. zuul:autorole:: sign-artifacts
 .. zuul:autorole:: stage-output
 .. zuul:autorole:: start-zuul-console
diff --git a/roles/dstat-graph/README.rst b/roles/dstat-graph/README.rst
new file mode 100644
index 000000000..7b59c1c9c
--- /dev/null
+++ b/roles/dstat-graph/README.rst
@@ -0,0 +1,32 @@
+Run dstat_graph
+
+This requires that the :zuul:role:`run-dstat` role be previously used.
+
+Add this to a post-run playbook to run ``dstat_graph`` to graph data
+from dstat.
+
+Use the :zuul:role:`ensure-dstat-graph` in a pre-run playbook to make
+sure that dstat_graph is available (since it is not currently packaged
+in any operating system).
+
+The output will appear in ``dstat.html`` in the ``zuul-output/logs``
+directory.
+
+**Role Variables**
+
+.. zuul:rolevar:: dstat_graph_cache_path
+   :default: /opt/cache/dstat_graph
+
+   The role will check this location to see if a cached copy of
+   dstat_graph is available.
+
+.. zuul:rolevar:: dstat_graph_download_path
+   :default: /tmp/dstat_graph
+
+   If a cached copy is not available, the role will check if
+   dstat_graph was previously downloaded to this location.
+
+.. zuul:rolevar:: dstat_data_path
+   :default: "{{ ansible_user_dir }}/zuul-output/logs/dstat.csv"
+
+   The path to the dstat data file.
diff --git a/roles/dstat-graph/defaults/main.yaml b/roles/dstat-graph/defaults/main.yaml
new file mode 100644
index 000000000..18ec03672
--- /dev/null
+++ b/roles/dstat-graph/defaults/main.yaml
@@ -0,0 +1,3 @@
+dstat_graph_cache_path: /opt/cache/dstat_graph
+dstat_graph_download_path: /tmp/dstat_graph
+dstat_data_path: "{{ ansible_user_dir }}/zuul-output/logs/dstat.csv"
diff --git a/roles/dstat-graph/tasks/main.yaml b/roles/dstat-graph/tasks/main.yaml
new file mode 100644
index 000000000..f4a5a051e
--- /dev/null
+++ b/roles/dstat-graph/tasks/main.yaml
@@ -0,0 +1,20 @@
+- name: Check for cached dstat_graph
+  stat:
+    path: "{{ dstat_graph_cache_path }}"
+  register: dstat_cache
+
+- name: Set dstat_graph path
+  when: dstat_cache.stat.exists
+  set_fact:
+    dstat_path: "{{ dstat_graph_cache_path }}"
+
+- name: Set dstat_graph path
+  when: not dstat_cache.stat.exists
+  set_fact:
+    dstat_path: "{{ dstat_graph_download_path }}"
+
+- name: Run dstat_graph
+  when: dstat_path is defined
+  shell: "./generate_page.sh {{ dstat_data_path }} > {{ ansible_user_dir }}/zuul-output/logs/dstat.html"
+  args:
+    chdir: "{{ dstat_path }}"
diff --git a/roles/ensure-dstat-graph/README.rst b/roles/ensure-dstat-graph/README.rst
new file mode 100644
index 000000000..555fa1f73
--- /dev/null
+++ b/roles/ensure-dstat-graph/README.rst
@@ -0,0 +1,21 @@
+Install dstat_graph
+
+This downloads ``dstat_graph`` if it is not already present on the
+remote host.
+
+Add this to a pre-run playbook to ensure it is available for roles
+such as :zuul:role:`dstat-graph`.
+
+**Role Variables**
+
+.. zuul:rolevar:: dstat_graph_cache_path
+   :default: /opt/cache/dstat_graph
+
+   The role will check this location to see if a cached copy of
+   dstat_graph is available.
+
+.. zuul:rolevar:: dstat_graph_download_path
+   :default: /tmp/dstat_graph
+
+   If a cached copy is not available, the role will download
+   dstat_graph to this location.
diff --git a/roles/ensure-dstat-graph/defaults/main.yaml b/roles/ensure-dstat-graph/defaults/main.yaml
new file mode 100644
index 000000000..119d3b070
--- /dev/null
+++ b/roles/ensure-dstat-graph/defaults/main.yaml
@@ -0,0 +1,2 @@
+dstat_graph_cache_path: /opt/cache/dstat_graph
+dstat_graph_download_path: /tmp/dstat_graph
diff --git a/roles/ensure-dstat-graph/tasks/main.yaml b/roles/ensure-dstat-graph/tasks/main.yaml
new file mode 100644
index 000000000..77c68f3b5
--- /dev/null
+++ b/roles/ensure-dstat-graph/tasks/main.yaml
@@ -0,0 +1,11 @@
+- name: Check for cached dstat_graph
+  stat:
+    path: "{{ dstat_graph_cache_path }}"
+  register: dstat_cache
+
+- name: Clone dstat_graph
+  when: not dstat_cache.stat.exists
+  git:
+    repo: https://github.com/Dabz/dstat_graph
+    dest: "{{ dstat_graph_download_path }}"
+    version: c99e5d201ed924a816d99056f8f7d320d625b3ef
diff --git a/roles/run-dstat/README.rst b/roles/run-dstat/README.rst
new file mode 100644
index 000000000..7bcdda51d
--- /dev/null
+++ b/roles/run-dstat/README.rst
@@ -0,0 +1,13 @@
+Run dstat
+
+Add this to a pre-run playbook to run ``dstat``.
+
+The role :zuul:role:`dstat-graph` may optionally be used to graph the
+resulting data.
+
+**Role Variables**
+
+.. zuul:rolevar:: dstat_data_path
+   :default: "{{ ansible_user_dir }}/zuul-output/logs/dstat.csv"
+
+   The path to the dstat data file.
diff --git a/roles/run-dstat/defaults/main.yaml b/roles/run-dstat/defaults/main.yaml
new file mode 100644
index 000000000..7268c9326
--- /dev/null
+++ b/roles/run-dstat/defaults/main.yaml
@@ -0,0 +1 @@
+dstat_data_path: "{{ ansible_user_dir }}/zuul-output/logs/dstat.csv"
diff --git a/roles/run-dstat/tasks/main.yaml b/roles/run-dstat/tasks/main.yaml
new file mode 100644
index 000000000..9e4a44032
--- /dev/null
+++ b/roles/run-dstat/tasks/main.yaml
@@ -0,0 +1,9 @@
+- name: Install dstat
+  package:
+    name: dstat
+    state: present
+  become: true
+- name: Run dstat
+  shell: "dstat -tcmndrylpg --tcp --output {{ dstat_data_path }} >& /dev/null &"
+  args:
+    executable: /bin/bash
diff --git a/test-playbooks/dstat-graph.yaml b/test-playbooks/dstat-graph.yaml
new file mode 100644
index 000000000..24e553544
--- /dev/null
+++ b/test-playbooks/dstat-graph.yaml
@@ -0,0 +1,14 @@
+- hosts: all
+  roles:
+    - ensure-dstat-graph
+    - run-dstat
+
+# Simulate workload
+- hosts: all
+  tasks:
+    - name: Simulate workload
+      shell: "sleep 10"
+
+- hosts: all
+  roles:
+    - dstat-graph
diff --git a/zuul-tests.d/general-roles-jobs.yaml b/zuul-tests.d/general-roles-jobs.yaml
index 3ae7fe07a..ac6631813 100644
--- a/zuul-tests.d/general-roles-jobs.yaml
+++ b/zuul-tests.d/general-roles-jobs.yaml
@@ -117,6 +117,16 @@
         - name: ubuntu-xenial
           label: ubuntu-xenial
 
+- job:
+    name: zuul-jobs-test-dstat-graph
+    description: Test the dstat-graph roles
+    run: test-playbooks/dstat-graph.yaml
+    files:
+      - ^roles/ensure-dstat-graph/.*
+      - ^roles/run-dstat/.*
+      - ^roles/dstat-graph/.*
+      - ^test-playbooks/dstat-graph.yaml
+
 - job:
     name: zuul-jobs-test-multinode-roles
     description: |
@@ -326,6 +336,7 @@
         - zuul-jobs-test-base-roles-ubuntu-bionic
         - zuul-jobs-test-base-roles-ubuntu-trusty
         - zuul-jobs-test-base-roles-ubuntu-xenial
+        - zuul-jobs-test-dstat-graph
         - zuul-jobs-test-multinode-roles-centos-7
         - zuul-jobs-test-multinode-roles-debian-stretch
         - zuul-jobs-test-multinode-roles-fedora-29