diff --git a/config/install/monitoring.settings.yml b/config/install/monitoring.settings.yml
index 305013d..6ec0698 100644
--- a/config/install/monitoring.settings.yml
+++ b/config/install/monitoring.settings.yml
@@ -1,2 +1,3 @@
 sensor_call_logging: on_request
 cron_run_sensors: FALSE
+rrd_file_path: ''
diff --git a/config/schema/monitoring.schema.yml b/config/schema/monitoring.schema.yml
index 15984b5..8f3b239 100644
--- a/config/schema/monitoring.schema.yml
+++ b/config/schema/monitoring.schema.yml
@@ -9,6 +9,9 @@ monitoring.settings:
     cron_run_sensors:
       type: boolean
       label: 'Run sensors during cron runs'
+    rrd_file_path:
+          type: label
+          label: 'RRD files path'
 
 # Config schema for sensor config entity.
 monitoring.sensor_config.*:
diff --git a/modules/demo/monitoring_demo.info.yml b/modules/demo/monitoring_demo.info.yml
index 940c070..2fc06e3 100644
--- a/modules/demo/monitoring_demo.info.yml
+++ b/modules/demo/monitoring_demo.info.yml
@@ -14,3 +14,4 @@ dependencies:
  - basic_auth
  - dblog
  - file
+ - monitoring_rrd
diff --git a/modules/demo/monitoring_demo.install b/modules/demo/monitoring_demo.install
index 01a0d63..939917e 100644
--- a/modules/demo/monitoring_demo.install
+++ b/modules/demo/monitoring_demo.install
@@ -3,6 +3,7 @@
  * @file
  *   Install file of the monitoring_demo module.
  */
+
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\search_api\Entity\Index;
@@ -60,14 +61,19 @@ function monitoring_demo_install() {
     \Drupal::logger('sensor_demo')->info($random->name());
     \Drupal::logger('sensor_demo')->warning($random->name());
   }
-  for ($i = 0; $i < 50; $i++) {
-    db_insert('watchdog')->fields(array(
-      'type' => 'page not found',
-      'message' => '@uri',
-      'variables' => serialize(['@uri' => 'not/found']),
-      'location' => 'http://example.com/not/found',
-      'timestamp' => REQUEST_TIME,
-    ))->execute();
+
+  // Insert randomized data into watchdog to generate some RRD graph examples.
+  $days = [50, 4, 22, 16, 8];
+  foreach ($days as $count) {
+    for ($i = 0; $i < $count; $i++) {
+      db_insert('watchdog')->fields([
+        'type' => 'page not found',
+        'message' => '@uri',
+        'variables' => serialize(['@uri' => 'not/found']),
+        'location' => 'http://example.com/not/found',
+        'timestamp' => REQUEST_TIME - (86400 * $i),
+      ])->execute();
+    }
   }
 
   // Enable Enabled modules and Disappeared sensors for the "interactive" demo.
@@ -101,6 +107,15 @@ function monitoring_demo_install() {
       '%derivative_path' => 'hash://styles/preview/5678.jpeg',
     ]
   );
+
+  // Create a demo folder where to store RRD data and set the path in config.
+  $rrd_filename = \Drupal::config('monitoring.settings')->get('rrd_file_path') ?: DRUPAL_ROOT . '/sites/default/files/monitoring_rrd_files';
+  file_prepare_directory($rrd_filename, FILE_CREATE_DIRECTORY);
+  \Drupal::configFactory()->getEditable('monitoring.settings')
+    ->set('rrd_file_path', $rrd_filename)
+    ->save();
+  // Initialise the RRD data.
+  monitoring_rrd_init_rrd();
 }
 
 
diff --git a/modules/monitoring_mail/monitoring_mail.module b/modules/monitoring_mail/monitoring_mail.module
index b7d1924..b85d00a 100644
--- a/modules/monitoring_mail/monitoring_mail.module
+++ b/modules/monitoring_mail/monitoring_mail.module
@@ -37,6 +37,19 @@ function monitoring_mail_mail($key, &$message, $params) {
   $body[] = 'Status: ' . $status_old . ' > ' . $status_new;
   $body[] = '';
 
+  $verbose = $result->getVerboseOutput();
+  // @todo Can we force verbosity available or reexecute forced?
+  if ($verbose) {
+    $body[] = 'Verbose:';
+    // @todo Support HTML?
+    $html = \Drupal::service('renderer')->renderPlain($result->getVerboseOutput());
+    $body[] = $html;
+    $body[] = '';
+  }
+
+  $body[] = 'Sensor settings';
+  $body[] = var_export($sensor_config->getSettings(), TRUE);
+
   $message['body'] = $body;
 
   // Set the mail sender.
@@ -45,9 +58,6 @@ function monitoring_mail_mail($key, &$message, $params) {
   $site_name = $site_config->get('name');
   $message['from'] = 'MONITORING ' . $site_name . ' <' . $site_mail . '>';
 
-  // @todo Set header to build a per-sensor thread.
-  // Set the mail header.
-
   // Set the header message id.
   $message_id = '<' . $result->getSensorId() . '.' . time() . '@' . \Drupal::request()->getHost() . '>';
   $message['headers']['message-id'] = $message_id;
@@ -74,6 +84,9 @@ function monitoring_mail_monitoring_run_sensors(array $results) {
 
   // Skip if no recipient is defined.
   if (!$recipient) {
+    drupal_set_message(t('No recipient has been provided. Please set one in the @monitoring_settings.', [
+      '@monitoring_settings' => Url::fromRoute('monitoring.settings')->toString(),
+    ]), 'warning');
     return;
   }
 
@@ -87,11 +100,21 @@ function monitoring_mail_monitoring_run_sensors(array $results) {
       continue;
     }
 
-    $status_old = SensorResultInterface::STATUS_UNKNOWN;
+    $sensor_config = $result->getSensorConfig();
+    if (\Drupal::moduleHandler()->moduleExists('monitoring_rrd')) {
+      // Skip if not eligible.
+      if (!monitoring_rrd_is_rrd($sensor_config)) {
+        return;
+      }
+
+      // Update the RRD graphs.
+      monitoring_rrd_update_rrd($result, $sensor_config);
+    }
 
     // @todo Implement interface for SensorResultEntity.
 
     // Try to load the previous log result for this sensor.
+    $status_old = SensorResultInterface::STATUS_UNKNOWN;
     if ($previous_result = monitoring_sensor_result_second_last($result->getSensorId())) {
       $status_old = $previous_result->get('sensor_status')->value;
     }
@@ -101,7 +124,7 @@ function monitoring_mail_monitoring_run_sensors(array $results) {
       // Prepare mail contexts.
       $params = [
         'result' => $result,
-        'sensor_config' => $result->getSensorConfig(),
+        'sensor_config' => $sensor_config,
         'status_old' => $status_old,
         'status_new' => $status_new,
         'last_result' => $previous_result,
diff --git a/modules/monitoring_rrd/config/install/monitoring_rrd.settings.yml b/modules/monitoring_rrd/config/install/monitoring_rrd.settings.yml
new file mode 100644
index 0000000..0ae9a8c
--- /dev/null
+++ b/modules/monitoring_rrd/config/install/monitoring_rrd.settings.yml
@@ -0,0 +1 @@
+rrd_file_path: ''
diff --git a/modules/monitoring_rrd/config/schema/monitoring_rrd.schema.yml b/modules/monitoring_rrd/config/schema/monitoring_rrd.schema.yml
new file mode 100644
index 0000000..d00abd7
--- /dev/null
+++ b/modules/monitoring_rrd/config/schema/monitoring_rrd.schema.yml
@@ -0,0 +1,8 @@
+# General settings.
+monitoring_rrd.settings:
+  type: config_object
+  label: 'Monitoring RRD settings'
+  mapping:
+    rrd_file_path:
+      type: label
+      label: 'RRD files path'
diff --git a/modules/monitoring_rrd/monitoring_rrd.info.yml b/modules/monitoring_rrd/monitoring_rrd.info.yml
new file mode 100644
index 0000000..0efde76
--- /dev/null
+++ b/modules/monitoring_rrd/monitoring_rrd.info.yml
@@ -0,0 +1,8 @@
+name: Monitoring RRD
+type: module
+description: Round Robin Database and graphs for Monitoring, with cron.
+package: Monitoring
+core: 8.x
+
+dependencies:
+ - monitoring
diff --git a/modules/monitoring_rrd/monitoring_rrd.install b/modules/monitoring_rrd/monitoring_rrd.install
new file mode 100644
index 0000000..bd9ded3
--- /dev/null
+++ b/modules/monitoring_rrd/monitoring_rrd.install
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Install file for the monitoring_rrd module.
+ */
+
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_requirements().
+ */
+function monitoring_rrd_requirements($phase) {
+
+  $requirements = [];
+
+  if (in_array($phase, ['runtime', 'install'])) {
+    // Check if php-rrd module is available. Otherwise stop install process.
+
+    $requirements['monitoring_rrd'] = [
+      'title' => t('Monitoring RRD'),
+      'severity' => REQUIREMENT_INFO,
+    ];
+
+    // Stop with error if the php-rrd module is missing.
+    if (!function_exists('rrd_create')) {
+      $requirements['monitoring_rrd']['severity'] = REQUIREMENT_ERROR;
+      $requirements['monitoring_rrd']['value'] = t('PHP RRD extension / module not installed.');
+      return $requirements;
+    }
+
+    // The RRD tools are available.
+    $requirements['monitoring_rrd']['value'] = rrd_version();
+  }
+
+  if ($phase == 'runtime') {
+    // @todo hook_requirements: no log = no mail
+    if (\Drupal::config('monitoring.settings')->get('sensor_call_logging') == 'none') {
+      // Without logging, a mail would NOT be triggered on each sensor run.
+      $requirements['monitoring_rrd_log'] = [
+        'title' => t('Monitoring Mail'),
+        'value' => 'With logging disabled, mail triggers can not be enabled.',
+        'severity' => REQUIREMENT_WARNING,
+      ];
+    }
+
+    // Check if the RRD data exists.
+    $requirements['monitoring_rrd_file'] = [
+      'title' => t('Monitoring RRD files'),
+      'severity' => REQUIREMENT_INFO,
+    ];
+
+    // Check if the user has provided a custom folder to store the RRD files.
+    $rrd_path = \Drupal::config('monitoring_rrd.settings')->get('rrd_file_path');
+    if (!isset($rrd_path) || !is_dir($rrd_path) || !is_writable($rrd_path)) {
+      $error_value = '';
+      $description = '';
+
+      if (!isset($rrd_path)) {
+        $error_value =  t('No path defined');
+        $description = t('Please provide an absolute path to a strictly internal folder where to store the @rrd_file.', [
+          '@rrd_file' => \Drupal::l(t('RRD files'), Url::fromRoute('monitoring.settings')),
+        ]);
+      }
+      elseif (!is_dir($rrd_path)) {
+        $error_value = t('Invalid path');
+        $description = t('The directory %directory does not exist. Please provide a proper absolute path to a strictly internal folder where to store the @rrd_file.', [
+          '%directory' => $rrd_path,
+          '@rrd_file' => \Drupal::l(t('RRD files'), Url::fromRoute('monitoring.settings')),
+        ]);
+      }
+      elseif (is_dir($rrd_path) && !is_writable($rrd_path)) {
+        $error_value = t('Not writable');
+        $description = t('The directory %directory is not writable.', [
+          '%directory' => $rrd_path,
+        ]);
+      }
+      // @todo check that an RRD exists for each sensor or report error.
+      elseif (monitoring_rrd_rrd_exists()) {
+        $error_value = t('No RRD');
+        $description = t('The directory %directory is not writable.', [
+          '%directory' => $rrd_path,
+        ]);
+      }
+
+      $requirements['monitoring_rrd_file']['value'] = $error_value;
+      $requirements['monitoring_rrd_file']['severity'] = REQUIREMENT_ERROR;
+      $requirements['monitoring_rrd_file']['description'] = $description;
+    }
+  }
+
+  return $requirements;
+}
diff --git a/modules/monitoring_rrd/monitoring_rrd.links.task.yml b/modules/monitoring_rrd/monitoring_rrd.links.task.yml
new file mode 100644
index 0000000..5957b6b
--- /dev/null
+++ b/modules/monitoring_rrd/monitoring_rrd.links.task.yml
@@ -0,0 +1,5 @@
+monitoring.sensor_graphs:
+  route_name: monitoring.sensor_graphs
+  base_route: monitoring.sensor_list
+  title: Graphs
+
diff --git a/modules/monitoring_rrd/monitoring_rrd.module b/modules/monitoring_rrd/monitoring_rrd.module
new file mode 100644
index 0000000..165f0c2
--- /dev/null
+++ b/modules/monitoring_rrd/monitoring_rrd.module
@@ -0,0 +1,562 @@
+<?php
+
+/**
+ * @file
+ * Module file for the monitoring_rrd project.
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\monitoring\Result\SensorResultInterface;
+use Drupal\monitoring\SensorConfigInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\monitoring\Plugin\monitoring\SensorPlugin\DatabaseAggregatorSensorPlugin;
+
+/**
+ * Check that an RRD exists for each sensor.
+ *
+ * @return bool
+ *   TRUE if an RRD exists for each sensor. FALSE otherwise.
+ */
+function monitoring_rrd_rrd_exists() {
+  $sensor_manager = monitoring_sensor_manager();
+  foreach ($sensor_manager->getAllSensorConfig() as $sensor_name => $sensor_config) {
+
+    // Return FALSE to report an error, if not eligible.
+    if (!monitoring_rrd_is_rrd($sensor_config)) {
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+/**
+ * Creates RRD sensor database files and prefills them.
+ */
+function monitoring_rrd_init_rrd() {
+  // Create a storage for each sensor.
+  $sensor_manager = monitoring_sensor_manager();
+  foreach ($sensor_manager->getAllSensorConfig() as $sensor_name => $sensor_config) {
+
+    // Skip if not eligible.
+    if (!monitoring_rrd_is_rrd($sensor_config, FALSE)) {
+      continue;
+    }
+
+    // Create RRD.
+    monitoring_rrd_rrd_create($sensor_config);
+    //continue;
+
+    // Prefill RRD values if possible.
+    $plugin = $sensor_config->getPlugin();
+    // @todo support EntityQuery.
+    if ($plugin instanceof DatabaseAggregatorSensorPlugin) {
+      // Skip if sensor caching time is greater than sensor time interval.
+      if ($sensor_config->getCachingTime() > $sensor_config->getTimeIntervalValue()) {
+        // @todo this leads to update errors. fix.
+        continue;
+      }
+
+      $query = $plugin->getHistoryQuery();
+
+      // Update the RRD database.
+      $rrd_filename = monitoring_rrd_filename($sensor_config);
+      $updater = new RRDUpdater($rrd_filename);
+      $field = $plugin->getTimeIntervalField();
+
+      // @todo check how to limit range or not?
+      try {
+        $query_result = $query
+          //->range(0, 10)
+          ->orderBy($field, 'ASC')
+          //->condition($field, time() - (86400 * 365), '>')
+          ->execute();
+      }
+      catch (Exception $e) {
+        // @todo Investigate the query errors.
+        echo 'QUERY ERROR:' . $sensor_config->id() . "\n";
+        continue;
+      }
+
+      $rows = [];
+      foreach ($query_result->fetchAll() as $key => $row) {
+        // Reformat the timestamp.
+        $date = \Drupal::service('date.formatter')->format($row->timestamp, 'short');
+
+        $updater->update([
+          "value" => $row->records_count,
+          //"critical" => $result_critical,
+          //"status" => ($result->getStatus() == 'WARNING') ? 2 : 0,
+        ], $row->field);
+      }
+
+    }
+  }
+}
+
+/**
+ * Checks if a sensor can feed RRD.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ * @param bool $runtime
+ *   Whether this function is called at creation time (FALSE) or at runtime
+ *   (TRUE) to additionally check for the file existence. Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE if the sensor is enabled and is numeric. FALSE otherwise.
+ */
+function monitoring_rrd_is_rrd(SensorConfigInterface $sensor_config, $runtime = TRUE) {
+  // Skip disabled sensors.
+  if (!$sensor_config->isEnabled()) {
+    return FALSE;
+  }
+
+  // Skip non-numeric sensors (for example core_maintenance_mode).
+  if (!$sensor_config->isNumeric()) {
+    return FALSE;
+  }
+
+  // As runtime case, skip if the file was lost (as it leads to fatal error).
+  if ($runtime) {
+    $filename = monitoring_rrd_filename($sensor_config);
+    if (!file_exists($filename)) {
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+/**
+ * Creates an RRD for a sensor.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ */
+function monitoring_rrd_rrd_create(SensorConfigInterface $sensor_config) {
+  if (!function_exists('rrd_create')) {
+    // Avoid fatal errors.
+    return;
+  }
+
+  // Skip if not eligible.
+  if (!monitoring_rrd_is_rrd($sensor_config, FALSE)) {
+    return;
+  }
+
+  // @todo Move to files/monitoring data folder.
+  // @todo mkdir folder.
+  // @todo make folder configurable.
+  $filename = monitoring_rrd_filename($sensor_config);
+
+  // Delete RRD if this file already exists.
+  if (file_exists($filename)) {
+    unlink($filename);
+  }
+
+  // The interval defines the validity of data in diagram.
+  $interval = $sensor_config->getTimeIntervalValue();
+  $caching = $sensor_config->getCachingTime();
+  // Make sure $keep is at least 300 and is the largest of the settings.
+  $keep = max($interval, $caching, 300);
+
+  $options = [
+    "--step", "300",          // Use a step-size of 5 minutes.
+    "--start", "-120 months", // This RRD started 10y ago.
+    //"DS:value:GAUGE:" . (string) ($keep * 2) . ":0:U",
+    "DS:value:GAUGE:" . (string) $keep . ":0:U",
+    "DS:time:GAUGE:" . (string) $keep . ":0:U",
+    "DS:status:GAUGE:" . (string) $keep . ":0:5",
+  ];
+
+  $per_10m = 600. / $keep;
+  $per_hour = 3600. / $keep;
+  $per_day = 86400. / $keep;
+  $per_week = (86400. * 7) / $keep;
+  $per_month = (86400. * 31) / $keep;
+
+  // Store single records per day/week/month/year.
+  if ($keep <= 300) {
+    // Single = 1d.
+    $options[] = "RRA:AVERAGE:0.5:1:" . (int) $per_day;
+  }
+  elseif ($keep <= 3600) {
+    // Single = 7d.
+    $options[] = "RRA:AVERAGE:0.5:1:" . (int) ($per_day * 7);
+  }
+  elseif ($keep <= 86400) {
+    // Single = 31d.
+    $options[] = "RRA:AVERAGE:0.5:1:" . (int) $per_month;
+  }
+  elseif ($keep <= (86400 * 31)) {
+    // Single = 12m.
+    $options[] = "RRA:AVERAGE:0.5:1:" . (int) ($per_month * 12);
+  }
+  else {
+    // @todo Define RRA for intervals > 1m.
+  }
+
+  // Define the archive data.
+  if ($keep <= (600 / 2)) {
+    // Every 10m for 2d.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_10m . ":288";
+    // Hourly for 7d.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_hour . ":168";
+    // Daily for 1y.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_day . ":365";
+  }
+  elseif ($keep <= (3600 / 2)) {
+    // Hourly for 7d.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_hour . ":168";
+    // Daily for 1y.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_day . ":365";
+  }
+  elseif ($keep <= (86400 / 2)) {
+    // Daily for 1y.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_day . ":365";
+    // Weekly for 2y.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_week . ":104";
+  }
+  elseif ($keep <= ((86400 * 7) / 2)) {
+    // Weekly for 2y.
+    $options[] = "RRA:AVERAGE:0.5:" . (int) $per_week . ":104";
+  }
+
+  // Monthly for 10y.
+  $options[] = "RRA:AVERAGE:0.5:" . (int) $per_month . ":120";
+
+  // Create RRD database file.
+  rrd_create($filename, $options);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function monitoring_rrd_monitoring_sensor_config_update(EntityInterface $entity) {
+  $changed = FALSE;
+
+  /** @var \Drupal\monitoring\SensorConfigInterface $entity */
+  // Recreation needed if the enabled sensor changes.
+  if ($entity->isEnabled() != $entity->original->isEnabled()) {
+    $changed = TRUE;
+  }
+
+   // Recreation needed if the sensor time interval changes.
+  if ($entity->getTimeIntervalValue() != $entity->original->getTimeIntervalValue()) {
+    $changed = TRUE;
+  }
+
+  // Recreation needed if the sensor caching time changes.
+  if ($entity->getCachingTime() != $entity->original->getCachingTime()) {
+    $changed = TRUE;
+  }
+
+  // @todo Don't allow caching > interval.
+
+  // Also create RRD for this sensor.
+  if ($changed) {
+    monitoring_rrd_rrd_create($entity);
+  }
+
+  // Recreate all graphs for this sensor.
+  monitoring_rrd_sensor_all_graphs($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert().
+ */
+function monitoring_rrd_monitoring_sensor_config_insert(EntityInterface $entity) {
+  /** @var \Drupal\monitoring\SensorConfigInterface $entity */
+  // Create RRD for this sensor.
+  monitoring_rrd_rrd_create($entity);
+}
+
+/**
+ * Recreates all graphs.
+ */
+function monitoring_rrd_all_graphs() {
+  $sensor_manager = monitoring_sensor_manager();
+  foreach ($sensor_manager->getEnabledSensorConfig() as $sensor_name => $sensor_config) {
+    monitoring_rrd_sensor_all_graphs($sensor_config);
+  }
+}
+
+/**
+ * Recreates all graphs of a single sensor.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ */
+function monitoring_rrd_sensor_all_graphs(SensorConfigInterface $sensor_config) {
+    // Skip if not eligible.
+    if (!monitoring_rrd_is_rrd($sensor_config)) {
+      return;
+    }
+
+    // Create all images.
+    monitoring_rrd_graph($sensor_config, '1h');
+    monitoring_rrd_graph($sensor_config, '1d');
+    monitoring_rrd_graph($sensor_config, '1w');
+    monitoring_rrd_graph($sensor_config, '1m');
+    monitoring_rrd_graph($sensor_config, '1y');
+}
+
+/**
+ * Creates a graph for sensor at interval.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ * @param string $interval
+ *   The interval time used as identifier to generate this sensor's graph.
+ */
+function monitoring_rrd_graph(SensorConfigInterface $sensor_config, $interval) {
+  $rrd_filename = monitoring_rrd_filename($sensor_config);
+  $filename = monitoring_rrd_filename_img($sensor_config) . '.' . $interval . '.png';
+
+  /*if (!file_prepare_directory($rrd_filename, FILE_CREATE_DIRECTORY)) {
+    // @todo failed... because /data folder is missing.
+    // @todo find a way to store the graphs and easily access to them.
+    return;
+  }*/
+
+  $options = [
+    "--slope-mode",
+    //"--height" => 50,
+    "--start", '-' . $interval,
+    //"--title=" . $title,
+    //"--vertical-label=" . $sensor_config->getValueLabel(),
+    //"--lower=0",
+    // Define the data to be used to draw the graph.
+    "DEF:value=" . $rrd_filename . ":value:AVERAGE",
+    "DEF:time=" . $rrd_filename . ":time:MAX",
+    "DEF:status=" . $rrd_filename . ":status:MAX",
+    // Draw the graph with the value data.
+    "AREA:value#0678be:" . $sensor_config->getValueLabel(),
+    // @todo Graph execution time with a second axis or a separate graph?
+    //"AREA:time#cccccc:" . t('Execution time'),
+  ];
+
+  // Thresholds.
+  if ($sensor_config->isDefiningThresholds()) {
+    $thresholds = $sensor_config->getThresholds();
+    foreach ($thresholds as $key => $value) {
+      if ($key == 'type') {
+        // Skip type key.
+        continue;
+      }
+
+      // Determine color. Yellow if it is ok.
+      $color = 'cccc00';
+      if (strpos($key, 'critical') !== FALSE) {
+        // Red if it is critical.
+        $color = 'cc0000';
+      }
+
+      if ($value !== NULL) {
+        // Create label.
+        $label = $key;
+        $label = str_replace('_', ' ', $label);
+        $options[] = 'HRULE:' . $value . '#' . $color . ':' . ucfirst($label) . ' ' . $value;
+        // @todo To force the bar visible:
+        //$options[] = 'CDEF:' . $key . '=value,POP,' . $value;
+        //$options[] = 'LINE:' . $key . '#' . $color . ':' . ucfirst($label) . ' ' . $value;
+      }
+    }
+  }
+
+  // @todo consider switching the graph background based on effective status?
+
+  $ret = rrd_graph($filename, $options);
+  if ($ret == FALSE) {
+    // @todo Consider this error.
+    drupal_set_message('ERR: ' . rrd_error());
+  }
+
+}
+
+/**
+ * Generates filename for RRD file.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ *
+ * @return string
+ *   The RRD file filename.
+ */
+function monitoring_rrd_filename(SensorConfigInterface $sensor_config) {
+  return monitoring_rrd_filename_img($sensor_config) . '.htaccess';
+}
+
+/**
+ * Generates filename for RRD image.
+ *
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ *
+ * @return string
+ *   The RRD image filename.
+ */
+function monitoring_rrd_filename_img(SensorConfigInterface $sensor_config) {
+  $rrd_folder_path = \Drupal::config('monitoring_rrd.settings')->get('rrd_file_path');
+  return $rrd_folder_path . '/' . $sensor_config->id();
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function monitoring_rrd_form_monitoring_sensor_config_details_form_alter(&$form, FormStateInterface $form_state) {
+  /** @var \Drupal\monitoring\SensorConfigInterface $sensor_config */
+  $sensor_config = $form_state->getFormObject()->getEntity();
+
+  // Skip sensors without RRD.
+  if (!monitoring_rrd_is_rrd($sensor_config)) {
+    return;
+  }
+
+  // Attach graph to details page.
+  $form['rrd'] = [
+    '#type' => 'item',
+    '#title' => t('RRD'),
+    //'#markup' => 'test',
+  ];
+
+  $form['rrd']['1h'] = [
+    '#theme' => 'image',
+    '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1h.png'),
+    '#alt' => 'Graph',
+    '#title' => '1h',
+    '#attributes' => [
+      'class' => 'rrd-1h',
+    ],
+  ];
+  $form['rrd']['1d'] = [
+    '#theme' => 'image',
+    '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1d.png'),
+    '#alt' => 'Graph',
+    '#title' => '1d',
+    '#attributes' => [
+      'class' => 'rrd-1d',
+    ],
+  ];
+  $form['rrd']['1w'] = [
+    '#theme' => 'image',
+    '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1w.png'),
+    '#alt' => 'Graph',
+    '#title' => '1w',
+    '#attributes' => [
+      'class' => 'rrd-1w',
+    ],
+  ];
+  $form['rrd']['1m'] = [
+    '#theme' => 'image',
+    '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1m.png'),
+    '#alt' => 'Graph',
+    '#title' => '1m',
+    '#attributes' => [
+      'class' => 'rrd-1m',
+    ],
+  ];
+  $form['rrd']['1y'] = [
+    '#theme' => 'image',
+    '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1y.png'),
+    '#alt' => 'Graph',
+    '#title' => '1y',
+    '#attributes' => [
+      'class' => 'rrd-1y',
+    ],
+  ];
+
+  // Fetch the data for plotting.
+  $rrd_filename = monitoring_rrd_filename($sensor_config);
+  $result = rrd_fetch($rrd_filename, ["AVERAGE", "--resolution", "300", "--start", "-1d", "--end", "now"]);
+
+  $data = $result['data']['value'];
+  $x = [];
+  foreach ($data as $key => $value) {
+    $x[] = $key . '>' . $value;
+  }
+
+  $form['rrd']['data'] = [
+    '#type' => 'item',
+    '#title' => t('Data'),
+    '#markup' => implode(',', $x),
+  ];
+
+}
+
+/**
+ * Updates RRD data from sensor run's result.
+ *
+ * @param \Drupal\monitoring\Result\SensorResultInterface $result
+ *   The sensor result.
+ * @param \Drupal\monitoring\SensorConfigInterface $sensor_config
+ *   The sensor.
+ */
+function monitoring_rrd_update_rrd(SensorResultInterface $result, SensorConfigInterface $sensor_config) {
+
+  // Skip if cached. RRD would throw an error.
+  if ($result->isCached()) {
+    return;
+  }
+
+  $rrd_filename = monitoring_rrd_filename($sensor_config);
+  // Update the RRD database.
+  $updater = new RRDUpdater($rrd_filename);
+  // Force convert to float...
+  // @todo deal with UNKNOWN values and skip non-numeric ones.
+  $result_value = (int) $result->getValue();
+  // @todo Graph status.
+  // @todo Graph execution time.
+  if ($result_value) {
+    //$result_critical = ($result->getStatus() == SensorResultInterface::STATUS_CRITICAL) ? 3 : 0;
+  }
+
+  $time = time();
+  $time = $result->getTimestamp();
+  $exec = $result->getExecutionTime();
+
+  // @todo Map status to numbers, critical = highest.
+
+  try {
+    $updater->update([
+      "value" => $result_value,
+      //"status" => ($result->getStatus() == 'WARNING') ? 2 : 0,
+      "time" => $exec,
+    ], $time);
+  }
+  catch (Exception $e) {
+    echo 'UPDATE ERROR: ' . $e->getMessage() . "\n";
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function monitoring_rrd_form_monitoring_settings_alter(&$form, FormStateInterface $form_state) {
+  $config = \Drupal::config('monitoring_rrd.settings');
+
+  // RRD file location.
+  $form['rrd_file_path'] = [
+    '#type' => 'textfield',
+    '#title' => t('RRD files path'),
+    '#description' => t('The RRD files are initialised on change.'),
+    '#default_value' => $config->get('rrd_file_path'),
+  ];
+
+  $form['#submit'][] = 'monitoring_rrd_form_monitoring_settings_submit';
+}
+
+/**
+ * Submits callback for Monitoring settings form.
+ */
+function monitoring_rrd_form_monitoring_settings_submit(&$form, FormStateInterface $form_state) {
+  // Trigger folder initialisation on save, if path changes.
+  if (\Drupal::config('monitoring_rrd.settings')->get('rrd_file_path') != $form_state->getValue('rrd_file_path')) {
+    monitoring_rrd_init_rrd();
+  }
+
+  \Drupal::configFactory()->getEditable('monitoring_rrd.settings')
+    ->set('rrd_file_path', $form_state->getValue('rrd_file_path'))
+    ->save();
+}
diff --git a/modules/monitoring_rrd/monitoring_rrd.routing.yml b/modules/monitoring_rrd/monitoring_rrd.routing.yml
new file mode 100644
index 0000000..876c838
--- /dev/null
+++ b/modules/monitoring_rrd/monitoring_rrd.routing.yml
@@ -0,0 +1,7 @@
+monitoring.sensor_graphs:
+  path: '/admin/reports/monitoring/graphs'
+  defaults:
+    _controller: '\Drupal\monitoring_rrd\Controller\SensorGraphs::content'
+    _title: Graphs
+  requirements:
+    _permission: 'monitoring reports'
diff --git a/modules/monitoring_rrd/src/Controller/SensorGraphs.php b/modules/monitoring_rrd/src/Controller/SensorGraphs.php
new file mode 100644
index 0000000..202474a
--- /dev/null
+++ b/modules/monitoring_rrd/src/Controller/SensorGraphs.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\monitoring_rrd\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\monitoring\Sensor\SensorManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a controller to display RRD Graphs table for each enabled sensor.
+ */
+class SensorGraphs extends ControllerBase {
+
+  /**
+   * The sensor manager.
+   *
+   * @var \Drupal\monitoring\Sensor\SensorManager
+   */
+  protected $sensorManager;
+
+  /**
+   * Constructs a SensorDetailForm object.
+   *
+   * @param \Drupal\monitoring\Sensor\SensorManager $sensor_manager
+   *   The sensor manager service.
+   */
+  public function __construct(SensorManager $sensor_manager) {
+    $this->sensorManager = $sensor_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('monitoring.sensor_manager')
+    );
+  }
+
+  /**
+   * Provides a table showing RRD graphs grouped by category and interval time.
+   *
+   * @return array
+   *   RRD graphs verbose info as render array.
+   */
+  public function content() {
+    $rows = [];
+
+    foreach ($this->sensorManager->getSensorConfigByCategories() as $category => $category_sensor_config) {
+
+      // Category grouping row.
+      $rows[] = [
+        'data' => [
+          'label' => [
+            'data' => ['#markup' => '<h3>' . $category . '</h3>'],
+            'colspan' => 7
+          ],
+        ],
+      ];
+
+      /** @var \Drupal\monitoring\SensorConfigInterface $sensor_config */
+      foreach ($category_sensor_config as $sensor_name => $sensor_config) {
+        // Skip if not eligible.
+        if (!monitoring_rrd_is_rrd($sensor_config)) {
+          continue;
+        }
+
+        $row['data']['label']['data'] = ['#markup' => '<span title="' . $sensor_config->getDescription() . '">' . $sensor_config->getLabel() . '</span>'];
+
+        $row['data']['1h']['data'] = [
+          '#theme' => 'image',
+          '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1h.png'),
+          '#alt' => 'Graph',
+          '#title' => '1h',
+          '#attributes' => [
+            'class' => 'rrd-1h',
+          ],
+        ];
+        $row['data']['1d']['data'] = [
+          '#theme' => 'image',
+          '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1d.png'),
+          '#alt' => 'Graph',
+          '#title' => '1d',
+          '#attributes' => [
+            'class' => 'rrd-1d',
+          ],
+        ];
+        $row['data']['1w']['data'] = [
+          '#theme' => 'image',
+          '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1w.png'),
+          '#alt' => 'Graph',
+          '#title' => '1w',
+          '#attributes' => [
+            'class' => 'rrd-1w',
+          ],
+        ];
+        $row['data']['1m']['data'] = [
+          '#theme' => 'image',
+          '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1m.png'),
+          '#alt' => 'Graph',
+          '#title' => '1m',
+          '#attributes' => [
+            'class' => 'rrd-1m',
+          ],
+        ];
+        $row['data']['1y']['data'] = [
+          '#theme' => 'image',
+          '#uri' => file_create_url(drupal_get_path('module', 'monitoring_rrd') . '/data/' . $sensor_config->id() . '.1y.png'),
+          '#alt' => 'Graph',
+          '#title' => '1y',
+          '#attributes' => [
+            'class' => 'rrd-1y',
+          ],
+        ];
+
+        $rows[] = $row;
+      }
+    }
+
+    $header = [
+      'label' => t('Sensor name'),
+      '1h' => t('1h'),
+      '1d' => t('1d'),
+      '1w' => t('1w'),
+      '1m' => t('1m'),
+      '1y' => t('1y'),
+    ];
+
+    $output['table'] = [
+      '#type' => 'table',
+      '#header' => $header,
+      '#rows' => $rows,
+      '#sticky' => TRUE,
+      '#attributes' => [
+        'id' => 'monitoring-sensors-graphs',
+      ],
+    ];
+
+    return $output;
+  }
+}
diff --git a/src/Plugin/monitoring/SensorPlugin/DatabaseAggregatorSensorPlugin.php b/src/Plugin/monitoring/SensorPlugin/DatabaseAggregatorSensorPlugin.php
index 411fcf6..ba52b4b 100644
--- a/src/Plugin/monitoring/SensorPlugin/DatabaseAggregatorSensorPlugin.php
+++ b/src/Plugin/monitoring/SensorPlugin/DatabaseAggregatorSensorPlugin.php
@@ -169,7 +169,7 @@ class DatabaseAggregatorSensorPlugin extends DatabaseAggregatorSensorPluginBase
    * @return \Drupal\Core\Database\Query\Select
    *   The select query object.
    */
-  protected function getHistoryQuery() {
+  public function getHistoryQuery() {
     /* @var \Drupal\Core\Database\Connection $database */
     $database = $this->getService('database');
     // Get aggregate query for the table.
@@ -186,6 +186,8 @@ class DatabaseAggregatorSensorPlugin extends DatabaseAggregatorSensorPluginBase
 
     $this->addAggregateExpression($query);
 
+    // @todo add order here?
+
     return $query;
   }
 
diff --git a/src/Result/SensorResult.php b/src/Result/SensorResult.php
index 7851f51..66f0ef1 100644
--- a/src/Result/SensorResult.php
+++ b/src/Result/SensorResult.php
@@ -127,7 +127,10 @@ class SensorResult implements SensorResultInterface {
 
 
   /**
-   * {@inheritdoc}
+   * List all existing labels.
+   *
+   * @return array
+   *   Associative array keyed by sensor status constant, its label as value.
    */
   public static function getStatusLabels() {
     return [
diff --git a/src/SensorPlugin/DatabaseAggregatorSensorPluginBase.php b/src/SensorPlugin/DatabaseAggregatorSensorPluginBase.php
index 3bff48a..e15e61e 100644
--- a/src/SensorPlugin/DatabaseAggregatorSensorPluginBase.php
+++ b/src/SensorPlugin/DatabaseAggregatorSensorPluginBase.php
@@ -56,7 +56,7 @@ abstract class DatabaseAggregatorSensorPluginBase extends SensorPluginBase {
    * @return string
    *   Time interval field.
    */
-  protected function getTimeIntervalField() {
+  public function getTimeIntervalField() {
     return $this->sensorConfig->getSetting('time_interval_field');
   }
 
diff --git a/src/SensorRunner.php b/src/SensorRunner.php
index 46b32bc..d17a62f 100644
--- a/src/SensorRunner.php
+++ b/src/SensorRunner.php
@@ -206,6 +206,11 @@ class SensorRunner {
       }
     }
 
+    if (\Drupal::moduleHandler()->moduleExists('monitoring_rrd')) {
+      // Create all graphs for this sensor.
+      monitoring_rrd_sensor_all_graphs($sensor_config);
+    }
+
     return $result;
   }
 
diff --git a/tests/src/Kernel/MonitoringCoreKernelTest.php b/tests/src/Kernel/MonitoringCoreKernelTest.php
index e0f2c96..dce6079 100644
--- a/tests/src/Kernel/MonitoringCoreKernelTest.php
+++ b/tests/src/Kernel/MonitoringCoreKernelTest.php
@@ -46,6 +46,7 @@ class MonitoringCoreKernelTest extends MonitoringUnitTestBase {
     // Checks that the cache is empty when cron is disabled.
     $this->assertTrue(!is_object($cache));
     $sensorConfig = SensorConfig::load('test_sensor');
+    /** @var \Drupal\monitoring\Result\SensorResultInterface[] $result */
     $result = \Drupal::service('monitoring.sensor_runner')->runSensors([$sensorConfig]);
     // Since after a sensor ran its result is cached, check if it is not cached.
     $this->assertTrue(!$result[0]->isCached());
