<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tej Pochiraju</title>
    <description>The latest articles on Forem by Tej Pochiraju (@tejpochiraju).</description>
    <link>https://forem.com/tejpochiraju</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F395932%2F5213045f-383e-4bbd-83d3-51028be7e3fc.jpeg</url>
      <title>Forem: Tej Pochiraju</title>
      <link>https://forem.com/tejpochiraju</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tejpochiraju"/>
    <language>en</language>
    <item>
      <title>Hands On With AWS IoT, Timestream and Grafana</title>
      <dc:creator>Tej Pochiraju</dc:creator>
      <pubDate>Mon, 15 Mar 2021 06:44:57 +0000</pubDate>
      <link>https://forem.com/tejpochiraju/hands-on-with-aws-iot-timestream-and-grafana-4c73</link>
      <guid>https://forem.com/tejpochiraju/hands-on-with-aws-iot-timestream-and-grafana-4c73</guid>
      <description>&lt;p&gt;Grafana has long been a favourite of ours for creating stunning visualisations. It's lightweight, easy to get started with and, over time, has added a lot of new features without losing simplicity. &lt;/p&gt;

&lt;p&gt;Yet, we have shied from recommending Grafana to our IoT customers over, say, the &lt;a href="https://www.influxdata.com/"&gt;TICK stack from Influx&lt;/a&gt; or &lt;a href="https://opendistro.github.io/for-elasticsearch/"&gt;ODFE/Kibana&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;A typical IoT workflow has three main blocks on the cloud and a lot of product builders are thrown off by the complexity of integrating these while maintaining data security and access control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4kD2SMoA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ex3ciaf0dpzl2t9h9qy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4kD2SMoA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ex3ciaf0dpzl2t9h9qy.png" alt="Typical Cloud IoT Blocks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of these, Grafana only does visualisation and is spectacularly good at it. You need to find your own solutions for data ingress and storage. Sure, Grafana has plugins for most data sources you have heard of and many more you haven't. Yet, most of these require you to set up your own servers. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Tfz1nyj1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bv4r7s3k7ldho3ngxij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Tfz1nyj1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bv4r7s3k7ldho3ngxij.png" alt="Grafana Data Sources"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We previously explored integrating &lt;a href="https://iotready.co/blog/metal-to-alerts-with-aws-iot-timestream-quicksight/#timestream--grafana"&gt;AWS Timestream with Grafana&lt;/a&gt;. Back then, we used the managed service from Grafana.com. With AWS now offering managed services for &lt;a href="https://aws.amazon.com/iot/"&gt;IoT ingress&lt;/a&gt;, &lt;a href="https://aws.amazon.com/timestream/"&gt;time series data storage&lt;/a&gt; and &lt;a href="https://aws.amazon.com/grafana/"&gt;Grafana&lt;/a&gt;, the integration and security challenges should largely be solved, right? Let's find out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up Grafana Instance
&lt;/h3&gt;

&lt;p&gt;Like the best managed offerings from AWS, setting up and configuring Grafana is a breeze. There's a wizard led flow once you click on "Create Workspace". &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nLsT2RWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4npoi9oz7d86kohl6bj5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nLsT2RWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4npoi9oz7d86kohl6bj5.png" alt="AWS Managed Grafana"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2JWyhQ5O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kkchbdo7bap17m5yxdmf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2JWyhQ5O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kkchbdo7bap17m5yxdmf.png" alt="AWS Grafana Wizard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS uses SSO for access control - this is powered by &lt;a href="https://aws.amazon.com/organizations/"&gt;AWS Organizations&lt;/a&gt; behind the scenes. More about this in a bit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PU75JbLc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy5qemd017vt9pis08gf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PU75JbLc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy5qemd017vt9pis08gf.png" alt="AWS Grafana Auth"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;IAM policies can be enabled with a click or you can configure these yourself. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aM40YHon--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqmzmu0uuzjnayx20mtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aM40YHon--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqmzmu0uuzjnayx20mtb.png" alt="AWS Grafana IAM"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we are done with the wizard, we enable the newly created user for access. In a couple of minutes our brand new Grafana instance is ready for use. We are brought to this friendly and useful summary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zrEX1QQw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auxtu5d56f81jmsutdba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zrEX1QQw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auxtu5d56f81jmsutdba.png" alt="AWS Grafana Summary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing &amp;amp; Configuring Grafana
&lt;/h3&gt;

&lt;p&gt;While we were busy clicking Next a few times, AWS created a new Organisation, sent a confirmation email to the logged in IAM user and another invitation email to the SSO user we created. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w_EJTeKU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1w98pnzvp3c0jlq0tli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w_EJTeKU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1w98pnzvp3c0jlq0tli.png" alt="AWS Grafana SSO Email"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Accepting the invitation allows us to set up a password for this user which we will need when accessing the Grafana instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t6XZmoJm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/539ku11elr9cqwd4qc7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t6XZmoJm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/539ku11elr9cqwd4qc7a.png" alt="Grafana AWS SSO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we login, the interface is an AWS white-labeled version of the standard Grafana UI with a section for AWS specific data sources. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CqLCN5Fm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpizqq1t038urunvejex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CqLCN5Fm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpizqq1t038urunvejex.png" alt="AWS Grafana Data Sources"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As one would expect, finding and setting up our Timestream DB as the default data source is also a matter of a few clicks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QAl5tg7g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gc7woy8xbxfaqzesnvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QAl5tg7g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gc7woy8xbxfaqzesnvz.png" alt="AWS Grafana Timestream"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up Dashboards &amp;amp; Alerts
&lt;/h3&gt;

&lt;p&gt;We have &lt;a href="https://iotready.co/blog/metal-to-alerts-with-aws-iot-timestream-quicksight/#timestream-db"&gt;previously described&lt;/a&gt; how to send data to AWS IoT and from there into Timestream. Once that is configured, we send data using our trusty Python script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GiAKljB7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/laz2ol04kofabkj34o6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GiAKljB7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/laz2ol04kofabkj34o6c.png" alt="Sending IoT Data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setting up a dashboard and panel is also exactly the same as in the previous article and we end up with, surprise, a similar panel. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tO5FPc3p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pu89vtsk2ev5tqji6p2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tO5FPc3p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pu89vtsk2ev5tqji6p2e.png" alt="Grafana CPU Usage Panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The alerts workflow is the same as usual - with the exception that AWS SNS is the first option in notification channels. Once the alerts are configured, we see them on the Alerts dashboard within Grafana.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GYKNjBBN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y49j1ehwh07m35a9r2aq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GYKNjBBN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y49j1ehwh07m35a9r2aq.png" alt="Grafana Alerts Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SNS is really easy to configure from within the &lt;a href="https://console.aws.amazon.com/grafana/"&gt;AWS Grafana Console&lt;/a&gt; (where we saw the summary earlier). We will explore this integration in a future post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections
&lt;/h2&gt;

&lt;p&gt;The advent of managed services from AWS (and others) has enabled cloud infrastructure to be configured rather than developed. For most IoT applications, a product builder can probably just pick one of the following configurations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---IEXuwoR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/483z4oe9a13wka2lw5me.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---IEXuwoR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/483z4oe9a13wka2lw5me.png" alt="IoT Cloud With ODFE"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j0QizIAm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3snkwbko8bg8eswyo2oi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j0QizIAm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3snkwbko8bg8eswyo2oi.png" alt="IoT Cloud With Timestream and Grafana"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are differences to consider, of course.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ODFE has &lt;a href="https://iotready.co/blog/iot-anomaly-detection-open-distro-elasticsearch/"&gt;anomaly detection built-in&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ODFE's authentication system supports multi-tenancy and granular data source level access controls out of the box. 

&lt;ul&gt;
&lt;li&gt;Grafana has viewer and editor roles and it's not obvious how to implement data source level access control.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Grafana allows you to ingest and visualise data from multiple data sources.&lt;/li&gt;
&lt;li&gt;Grafana has a lot of visualisation plugins with a well documented flow for building new plugins.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ODFE does not have a pay-as-you-go model - you pay for the hosted ElasticSearch instance(s). Expect to pay $40-$80 per month for &amp;lt;100,000 devices. There are no limits to the number of users you can add to the system.&lt;/li&gt;
&lt;li&gt;AWS Managed Grafana &lt;a href="https://aws.amazon.com/grafana/pricing/"&gt;costs $9/editor&lt;/a&gt; and $5/viewer per month. The prices are similar to that for QuickSight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ecosystem
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Timestream is relatively new and the data model takes a little getting used to. Moreover, you have to use official AWS SDKs to access data for custom applications.&lt;/li&gt;
&lt;li&gt;ElasticSearch/ODFE, being a lot more mature, have numerous SDKs to access data and build custom interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these reasons, and to avoid vendor lock-in with Timestream, we will continue to recommend ODFE over Grafana. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions, comments? Connect with us at &lt;a href="//hello@iotready.co"&gt;hello@iotready.co&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
    </item>
    <item>
      <title>Anomaly Detection For IoT Using Open Distro For ElasticSearch</title>
      <dc:creator>Tej Pochiraju</dc:creator>
      <pubDate>Wed, 17 Feb 2021 15:23:12 +0000</pubDate>
      <link>https://forem.com/tejpochiraju/anomaly-detection-for-iot-using-open-distro-for-elasticsearch-14kj</link>
      <guid>https://forem.com/tejpochiraju/anomaly-detection-for-iot-using-open-distro-for-elasticsearch-14kj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is the third in series on using the AWS ecosystem for IoT applications. Previously, we integrated AWS IoT with &lt;a href="https://iotready.co/blog/metal-to-alerts-with-aws-iot-timestream-quicksight/" rel="noopener noreferrer"&gt;Timestream /Quicksight&lt;/a&gt; and &lt;a href="https://iotready.co/blog/metal-to-alerts-with-aws-iot-elasticsearch-kibana/" rel="noopener noreferrer"&gt;ElasticSearch/Kibana&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Anomaly detection is the foundation for applications such as Predictive Maintenance, which in turn is the driving force behind most industrial IoT deployments. Now that the &lt;em&gt;essentials&lt;/em&gt; of sensors, communication, storage and visualisation have largely been solved, attention has turned to machine learning based analytics. Cue the &lt;a href="https://aws.amazon.com/iot-analytics/" rel="noopener noreferrer"&gt;new features from AWS IoT&lt;/a&gt; and Open Distro For ElasticSearch - the latter is the focus of this article. &lt;/p&gt;

&lt;h2&gt;
  
  
  What are we going to build?
&lt;/h2&gt;

&lt;p&gt;We will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simulate a smart grid sensor capable of measuring current, voltage, tempterature and humidity&lt;/li&gt;
&lt;li&gt;Train an anomaly detector in ODFE on each of these metrics or &lt;code&gt;features&lt;/code&gt; as ODFE calls them. &lt;/li&gt;
&lt;li&gt;Simulate various grades of anomalies and verify that detector is working fine&lt;/li&gt;
&lt;li&gt;Integrate the anomaly detector with Kibana's alerts (&lt;a href="https://dev.to/blog/metal-to-alerts-with-aws-iot-elasticsearch-kibana"&gt;previously discussed here&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Simulated Smart Grid Sensor
&lt;/h3&gt;

&lt;p&gt;Our simulated sensor helps monitor and predict failures in the medium voltage (MV) transmission grid. The sensor has the following nominal specifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voltage between 23kV and 25kV&lt;/li&gt;
&lt;li&gt;Current between 0A and 600A&lt;/li&gt;
&lt;li&gt;Temperature between 30C and 100C&lt;/li&gt;
&lt;li&gt;Humidity between 20% and 80%&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Values in these ranges are considered &lt;strong&gt;good&lt;/strong&gt;. Anything outside is an &lt;strong&gt;anomaly&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Anomaly Detection in ODFE
&lt;/h3&gt;

&lt;p&gt;ODFE uses the Random Cut Forest (RCF) algorithm for anomaly detection. RCF is an unsupervised algorithm which analyses the data and identifies patterns. Data points that do not fit into these patterns are classified as anomalies and can include, amongst others,:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spikes&lt;/li&gt;
&lt;li&gt;Changes in periodicity&lt;/li&gt;
&lt;li&gt;Unclassifiable data points&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each anomaly is given a score - low scores correspond to &lt;em&gt;normal&lt;/em&gt; and high scores to &lt;em&gt;anomalous&lt;/em&gt; data points. Read more about RCF in these references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://opendistro.github.io/for-elasticsearch/blog/odfe-updates/2019/11/real-time-anomaly-detection-in-open-distro-for-elasticsearch/" rel="noopener noreferrer"&gt;Real-time anomaly detection in ODFE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/randomcutforest.html" rel="noopener noreferrer"&gt;RCF with AWS Sagemaker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freecontent.manning.com/the-randomcutforest-algorithm/" rel="noopener noreferrer"&gt;RCF Algorithm on Manning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  ElasticSearch Instance
&lt;/h4&gt;

&lt;p&gt;Assuming you followed the previous post, you will already have an ElasticSearch instance running. However, we need at least 2 CPU cores to use anomaly detection. I am using a &lt;code&gt;t2.medium&lt;/code&gt; instance for this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Our First Anomaly Detector
&lt;/h2&gt;

&lt;p&gt;Like before, we will start our simulator to inject sensor data into ElasticSearch. I started a script to simulate 21 sensors sending data every 10s.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;23000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The injected data looks a bit like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7mhhhaduwem3plblppqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7mhhhaduwem3plblppqx.png" alt="Nominal Current Timeseries Chart"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialisation / Training
&lt;/h3&gt;

&lt;p&gt;The ODFE documentation has an &lt;a href="https://opendistro.github.io/for-elasticsearch-docs/docs/ad/#get-started-with-anomaly-detection" rel="noopener noreferrer"&gt;excellent guide&lt;/a&gt; for setting up a detector. Following that we end up with a configuration that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbecnctdcu8ckurtumfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbecnctdcu8ckurtumfy.png" alt="Anomaly Detector Configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I picked &lt;code&gt;Detector Interval = 5 minutes&lt;/code&gt; and &lt;code&gt;Window Delay = 2 minutes&lt;/code&gt; 

&lt;ul&gt;
&lt;li&gt;The documentation suggests smaller intervals make the system more real-time but consume more CPU, which sounds about right.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;You are allowed to add up to 5 features per detector - this seems to be an ODFE limitation rather than that of the RCF algorithm itself.&lt;/li&gt;

&lt;li&gt;I have chosen to track the &lt;code&gt;max()&lt;/code&gt; value for each metric. You can use any of the standard ElasticSearch aggregations.&lt;/li&gt;

&lt;li&gt;Once configured, the detector took between 30-60 minutes to initialise and go live.&lt;/li&gt;

&lt;li&gt;I made the mistake of trying to enable the detector on a &lt;code&gt;t2.small&lt;/code&gt; instance and kept running into an &lt;code&gt;unknown error&lt;/code&gt;. This disappeared once I changed the instance size to &lt;code&gt;t2.medium&lt;/code&gt;. &lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Anomaly Generation
&lt;/h3&gt;

&lt;p&gt;Once the detector was &lt;strong&gt;Live&lt;/strong&gt;, I started generating anomalies in about 60% of the data points using the following snippet. Note that I am randomly introducing anomalies into one or all of the four metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;23000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;23000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;23000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;23000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting timeseries charts, in their glorious randomness, look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyvvmt99qhnc8nom2wae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyvvmt99qhnc8nom2wae.png" alt="Anomalous Metrics Timeseries Charts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Anomaly Detection
&lt;/h3&gt;

&lt;p&gt;And just like that, the detector is triggered within the first time interval. This is great - with little knowledge of machine learning and zero code, we set up a self-taught anomaly detector!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmux471rfcdujzwy0hj2o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmux471rfcdujzwy0hj2o.png" alt="Anomaly Detector Results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Hang On...
&lt;/h3&gt;

&lt;p&gt;If you examine the anomaly grades, you will notice that the grade reduces in each time interval until the detector no longer considers the signals to be anomalous. This reminds me of an old joke,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Said the guru to his disciple, "Next year is going to be really difficult for you. You will not meet your family or friends for a long time and you will witness a lot of suffering. In fact you won't be able to step outside your own house!". "And the year after?", asked the disciple. The guru replied, "You will get used to it".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Jokes aside, &lt;strong&gt;why&lt;/strong&gt; is this happening? &lt;/p&gt;

&lt;p&gt;The anomaly detector, as mentioned above, is self-taught. And it keeps learning - even as anomalous data streams in. As the kind folk at ODFE explained to me, if 5% of your data is anomalous, is it really anomalous or, in fact, the &lt;em&gt;new normal&lt;/em&gt;? The anomaly detector, naturally, adapts to this new normal and gives these signals a decreasing grade until they are fully &lt;em&gt;normalised&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This makes sense, it's just not what I expected. &lt;/p&gt;

&lt;h3&gt;
  
  
  How do you solve this?
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If your data has infrequent anomalies, there's nothing to fix. The existing plugin already works well!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Freezing the anomaly detection data model by stopping the learning phase should solve this problem. I have opened a &lt;a href="https://github.com/opendistro-for-elasticsearch/anomaly-detection/issues/388" rel="noopener noreferrer"&gt;feature request&lt;/a&gt; for this very use case.&lt;/p&gt;

&lt;p&gt;A good suggestion from the ODFE team was to use a combination of rule based detection algorithms and ML based anomaly detection. This makes sense, especially since there are a few other issues with this domain-agnostic approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All signals, across all devices, in a time interval are given a single anomaly grade. We may need to classify these anomalies for priorities and, more importantly, identify the specific devices which are reporting anomalous data.&lt;/li&gt;
&lt;li&gt;We may need different anomaly detection for each SKU. E.g. 300A current is anomalous for a sensor rated at 200A but normal for a 500A sensor. With ODFE, we would need to send data from each SKU to a different index and set up separate detectors for each.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integrating Alerts
&lt;/h2&gt;

&lt;p&gt;Once an anomaly detector has been set up, it can be used as a source in the existing Alerts plugin for ODFE. We have previously discussed this - all that changes is that we define our monitor using our new anomaly detector. Yes, that's all!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcu83k0iprv506f4gyra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcu83k0iprv506f4gyra.png" alt="Alerts using Anomaly Detector"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and Next Steps
&lt;/h2&gt;

&lt;p&gt;Anomaly detection is a relatively new feature in ODFE and is already really good at actually detecting anomalies and does not get tripped unless the anomalies are frequent or persistent. If the &lt;a href="https://github.com/opendistro-for-elasticsearch/anomaly-detection/issues/388" rel="noopener noreferrer"&gt;feature request&lt;/a&gt; is accepted and built, we are in job done territory for simple use cases. &lt;/p&gt;

&lt;p&gt;For sensitive applications like smart grids and perhaps industrial monitoring, we are exploring solutions that combine intelligence on the cloud and at the edge. Over the coming weeks and months, we will write about our work with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rule based calibration and detection at the edge&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sciencedirect.com/science/article/pii/S0888613X96001168" rel="noopener noreferrer"&gt;Fuzzy logic&lt;/a&gt; based fault diagnosis at the edge&lt;/li&gt;
&lt;li&gt;ML at the edge using projects such as &lt;a href="https://www.tinyml.org/" rel="noopener noreferrer"&gt;TinyML&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ideas, questions or corrections?
&lt;/h2&gt;

&lt;p&gt;Write to us at &lt;a href="//mailto:hello@iotready.co"&gt;hello@iotready.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>aws</category>
      <category>elasticsearch</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Server Side Time Series Plots With Elixir Phoenix Using Contex</title>
      <dc:creator>Tej Pochiraju</dc:creator>
      <pubDate>Thu, 28 Jan 2021 03:44:56 +0000</pubDate>
      <link>https://forem.com/tejpochiraju/server-side-time-series-plots-with-elixir-phoenix-using-contex-52mc</link>
      <guid>https://forem.com/tejpochiraju/server-side-time-series-plots-with-elixir-phoenix-using-contex-52mc</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;At IoTReady, we are building a virtual IoT platform to help manufacturers track all of their products - whether these are born smart or not. For instance, a typical workflow we track in the Smart Grid industry looks something like this -&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A manufacturer produces a batch of, say, an insulation product&lt;/li&gt;
&lt;li&gt;The manufacturer ships certain units of this batch to a distributor&lt;/li&gt;
&lt;li&gt;An operator buys some units of this batch&lt;/li&gt;
&lt;li&gt;The operator installs the insulation product and captures notes and media (photos &amp;amp; videos)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At each stage of the flow, our mobile app scans QR codes and captures additional metadata like location and timestamps. Post installation we capture regular weather data for the installation location for analysis and preventive maintenance. &lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Operational dashboards are a lot more fun (&lt;em&gt;and useful&lt;/em&gt;) realtime, so we are building ours with &lt;a href="https://elixir-lang.org/"&gt;Elixir&lt;/a&gt; and the &lt;a href="https://phoenixframework.org/"&gt;Phoenix Framework&lt;/a&gt;. These choices deserve their own, longer, blog post. For now, we will focus on our charting library of choice. &lt;/p&gt;

&lt;p&gt;We are big fans of &lt;a href="https://plot.ly/"&gt;Plotly&lt;/a&gt; and have used it extensively in the past. However, in this case we wanted to minimise JS code and do things server side as much as we could. &lt;/p&gt;

&lt;p&gt;Step up &lt;a href="https://github.com/mindok/contex"&gt;Contex&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;We discovered Contex via a &lt;a href="https://www.elixirschool.com/blog/server-side-svg-charts-with-contex-and-liveview/"&gt;blog post&lt;/a&gt; on the excellent Elixir School site. However, that post covers bar charts and the Contex documentation is a &lt;em&gt;work-in-progress&lt;/em&gt;. Figuring out legends and version-to-documentation mismatches was particularly painful. Hence this post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our dashboard is complementary to, rather than a replacement for, BI tools like Redash or &lt;a href="https://dev.to/blog/metal-to-alerts-with-aws-iot-elasticsearch-kibana"&gt;Kibana&lt;/a&gt;. We needed something easy to use and customise that includes &lt;em&gt;some&lt;/em&gt; visualisation. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://hexdocs.pm/contex/Contex.html"&gt;Contex documentation&lt;/a&gt; can be a little difficult to wrap your head around&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/mindok/contex/tree/master/test"&gt;unit tests&lt;/a&gt; and &lt;a href="https://github.com/mindok/contex-samples"&gt;samples&lt;/a&gt;, on the other hand, are excellent resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data Source
&lt;/h2&gt;

&lt;p&gt;We use the &lt;a href="https://openweathermap.org/api"&gt;OpenWeatherMap API&lt;/a&gt; to grab basic weather data. Our &lt;code&gt;Ecto&lt;/code&gt; schema looks a bit like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="s2"&gt;"weather"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:pressure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;

  &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the query for getting recent data looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="sd"&gt;"""
Gets weather data points for a given latitude and longitude tuple.

## Examples
    iex&amp;gt; get_weather({latitude, longitude}, limit)
    {:ok, [%Weather{}]}
"""&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="no"&gt;Weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;where:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;latitude:&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;longitude:&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;order_by:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;desc:&lt;/span&gt; &lt;span class="ss"&gt;:inserted_at&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;limit:&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;select:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;temp:&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;humidity:&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pressure:&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inserted_at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query returns a &lt;code&gt;list&lt;/code&gt; of &lt;code&gt;maps&lt;/code&gt; that look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="ss"&gt;humidity:&lt;/span&gt; &lt;span class="mf"&gt;73.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~N[2021-01-27 17:00:01]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;pressure:&lt;/span&gt; &lt;span class="mf"&gt;1016.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;temp:&lt;/span&gt; &lt;span class="mf"&gt;297.27&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="ss"&gt;humidity:&lt;/span&gt; &lt;span class="mf"&gt;73.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~N[2021-01-27 17:00:01]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;pressure:&lt;/span&gt; &lt;span class="mf"&gt;1016.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;temp:&lt;/span&gt; &lt;span class="mf"&gt;297.27&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting up Contex
&lt;/h2&gt;

&lt;p&gt;Now that we have our data, time to set up &lt;code&gt;Contex&lt;/code&gt;. Since it's still early days for Contex, it's best to work with the &lt;code&gt;master&lt;/code&gt; branch off the Github repo rather than the &lt;code&gt;0.3.0&lt;/code&gt; release on Hex. For instance, the &lt;code&gt;0.3.0&lt;/code&gt; release does not include &lt;code&gt;LinePlot&lt;/code&gt;, which we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mix.exs&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:contex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;git:&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/mindok/contex"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plotting the data
&lt;/h2&gt;

&lt;p&gt;It's easiest to illustrate the plotting flow with code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get the last 100 data points for {latitude, longitude}&lt;/span&gt;
&lt;span class="n"&gt;weather_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; 


&lt;span class="n"&gt;plot_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
  &lt;span class="ss"&gt;top_margin:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;right_margin:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;bottom_margin:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;left_margin:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;show_x_axis:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;show_y_axis:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Generate the SVG chart&lt;/span&gt;
&lt;span class="n"&gt;weather_chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;weather_data&lt;/span&gt;
  &lt;span class="c1"&gt;# Flatten the map into a list of lists&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;temp:&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;humidity:&lt;/span&gt; &lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pressure:&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Assign legend titles using list indices&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"Time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Humidity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Pressure"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="c1"&gt;# Specify plot type (LinePlot), SVG dimensions, column mapping, title, label and legend&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;LinePlot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;mapping:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;x_col:&lt;/span&gt; &lt;span class="s2"&gt;"Time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;y_cols:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Humidity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Pressure"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="ss"&gt;plot_options:&lt;/span&gt; &lt;span class="n"&gt;plot_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;x_label:&lt;/span&gt; &lt;span class="s2"&gt;"Time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;legend_setting:&lt;/span&gt; &lt;span class="ss"&gt;:legend_right&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Generate SVG&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_svg&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# We are using Phoenix LiveView, so assign the chart to the socket&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:weather_chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_chart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, all that's left to do is to embed the SVG in the HTML view and this is all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="nv"&gt;@weather_chart&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We faced some clipping of the legend text but that was easy to fix with this CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.exc-legend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the SVG in all its glory :-).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.1"&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;xmlns:xlink=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/xlink"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"chart"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 600 300"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"img"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="py"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;black&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;line&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="py"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;black&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"280.0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Weather&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"280.0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"280"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-subtitle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Time&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(70,40)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 170)"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-domain"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#000"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M0.5, 6V0.5H420.5V6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;27 Jan 09:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(60.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;27 Jan 12:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(120.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;27 Jan 15:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(180.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;27 Jan 18:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(240.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;27 Jan 21:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(300.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;28 Jan 00:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(360.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;28 Jan 03:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(420.5,0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.71em"&lt;/span&gt; &lt;span class="na"&gt;dx=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;28 Jan 06:00&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-domain"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#000"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M-6,170.5H0.5V0.5H-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 170.5)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 155.04545454545453)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;100&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 139.5909090909091)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;200&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 124.13636363636364)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;300&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 108.68181818181819)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;400&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 93.22727272727273)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;500&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 77.77272727272728)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;600&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 62.31818181818181)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;700&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 46.86363636363636)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;800&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 31.409090909090907)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;900&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 15.954545454545467)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1000&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-tick"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(0, 0.5)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/line&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"-9"&lt;/span&gt; &lt;span class="na"&gt;dy=&lt;/span&gt;&lt;span class="s"&gt;"0.32em"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1100&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M  0.0 126.64227272727273 C  0.0 126.64227272727273 -2.333333333333333 126.65020606060605 0.0 126.64227272727273 C  2.333333333333333 126.6343393939394 15.333333333333334 126.58959848484848 20.0 126.57427272727273 C  24.666666666666664 126.55894696969698 35.333333333333336 126.53038181818182 40.0 126.5109090909091 C  44.666666666666664 126.49143636363637 55.333333333333336 126.41944393939394 60.0 126.40736363636364 C  64.66666666666667 126.39528333333334 75.33333333333333 126.41926363636364 80.0 126.40736363636364 C  84.66666666666667 126.39546363636364 95.33333333333333 126.31726363636363 100.0 126.30536363636364 C  104.66666666666667 126.29346363636364 115.33268518518518 126.30536363636364 120.0 126.30536363636364 C  124.66731481481482 126.30536363636364 137.67157407407407 126.30536363636364 140.00555555555556 126.30536363636364 C  142.33953703703705 126.30536363636364 137.67222222222222 126.30211818181819 140.00555555555556 126.30536363636364 C  142.3388888888889 126.30860909090909 157.67222222222222 126.32993636363638 160.00555555555556 126.33318181818183 C  162.3388888888889 126.33642727272728 136.67222222222222 126.32741212121213 160.00555555555556 126.33318181818183 C  183.3388888888889 126.33895151515152 336.6722222222222 126.37686666666666 360.00555555555553 126.38263636363635 C  383.33888888888885 126.38840606060604 360.00555555555553 126.38263636363635 360.00555555555553 126.38263636363635"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#1f77b4"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"transparent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M  0.0 154.54545454545453 C  0.0 154.54545454545453 -2.333333333333333 154.54545454545453 0.0 154.54545454545453 C  2.333333333333333 154.54545454545453 15.333333333333334 154.54545454545453 20.0 154.54545454545453 C  24.666666666666664 154.54545454545453 35.333333333333336 154.4192424242424 40.0 154.54545454545453 C  44.666666666666664 154.67166666666665 55.333333333333336 155.5010606060606 60.0 155.62727272727273 C  64.66666666666667 155.75348484848485 75.33333333333333 155.51909090909092 80.0 155.62727272727273 C  84.66666666666667 155.73545454545453 95.33333333333333 156.44636363636366 100.0 156.55454545454546 C  104.66666666666667 156.66272727272727 115.33268518518518 156.66272727272727 120.0 156.55454545454546 C  124.66731481481482 156.44636363636366 137.67157407407407 155.73545454545453 140.00555555555556 155.62727272727273 C  142.33953703703705 155.51909090909092 137.67222222222222 155.62727272727273 140.00555555555556 155.62727272727273 C  142.3388888888889 155.62727272727273 157.67222222222222 155.62727272727273 160.00555555555556 155.62727272727273 C  162.3388888888889 155.62727272727273 136.67222222222222 155.75348484848485 160.00555555555556 155.62727272727273 C  183.3388888888889 155.5010606060606 336.6722222222222 154.67166666666665 360.00555555555553 154.54545454545453 C  383.33888888888885 154.4192424242424 360.00555555555553 154.54545454545453 360.00555555555553 154.54545454545453"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#ff7f0e"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"transparent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M  0.0 13.75454545454545 C  0.0 13.75454545454545 -2.333333333333333 13.75454545454545 0.0 13.75454545454545 C  2.333333333333333 13.75454545454545 15.333333333333334 13.772575757575753 20.0 13.75454545454545 C  24.666666666666664 13.736515151515148 35.333333333333336 13.618030303030297 40.0 13.599999999999994 C  44.666666666666664 13.581969696969692 55.333333333333336 13.581969696969692 60.0 13.599999999999994 C  64.66666666666667 13.618030303030297 75.33333333333333 13.736515151515148 80.0 13.75454545454545 C  84.66666666666667 13.772575757575753 95.33333333333333 13.75454545454545 100.0 13.75454545454545 C  104.66666666666667 13.75454545454545 115.33268518518518 13.75454545454545 120.0 13.75454545454545 C  124.66731481481482 13.75454545454545 137.67157407407407 13.75454545454545 140.00555555555556 13.75454545454545 C  142.33953703703705 13.75454545454545 137.67222222222222 13.736515151515148 140.00555555555556 13.75454545454545 C  142.3388888888889 13.772575757575753 157.67222222222222 13.891060606060604 160.00555555555556 13.909090909090907 C  162.3388888888889 13.92712121212121 136.67222222222222 13.76484848484848 160.00555555555556 13.909090909090907 C  183.3388888888889 14.053333333333333 336.6722222222222 15.001212121212129 360.00555555555553 15.145454545454555 C  383.33888888888885 15.289696969696982 360.00555555555553 15.145454545454555 360.00555555555553 15.145454545454555"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#2ca02c"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"transparent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(500, 50)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"exc-legend"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill:#1f77b4;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/rect&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"23"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt; &lt;span class="na"&gt;dominant-baseline=&lt;/span&gt;&lt;span class="s"&gt;"central"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Temperature&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"21"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill:#ff7f0e;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/rect&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"23"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"30"&lt;/span&gt; &lt;span class="na"&gt;dominant-baseline=&lt;/span&gt;&lt;span class="s"&gt;"central"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Humidity&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill:#2ca02c;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/rect&amp;gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"23"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"51"&lt;/span&gt; &lt;span class="na"&gt;dominant-baseline=&lt;/span&gt;&lt;span class="s"&gt;"central"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pressure&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Dev.to does not render SVGs, you can look at the &lt;a href="https://iotready.co/blog/server-side-svg-timeseries-plots-contex-elixir/"&gt;render here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do we go from here
&lt;/h3&gt;

&lt;p&gt;We think Contex is a pretty good fit for our needs. It's &lt;em&gt;definitely&lt;/em&gt; rough around the edges and there are plenty of use cases we are yet to explore like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interactivity (not a huge deal for us) and &lt;/li&gt;
&lt;li&gt;realtime updates (big deal). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Realtime updates are easy to implement but we want verify impact on server performance but then again, this is not an immediate concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ideas, questions or corrections?
&lt;/h2&gt;

&lt;p&gt;Write to us at &lt;a href="//mailto:hello@iotready.co"&gt;hello@iotready.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>svg</category>
      <category>plotting</category>
    </item>
    <item>
      <title>From Metal To Alerts With AWS IoT, ElasticSearch 7.9 and Kibana</title>
      <dc:creator>Tej Pochiraju</dc:creator>
      <pubDate>Tue, 05 Jan 2021 08:32:26 +0000</pubDate>
      <link>https://forem.com/tejpochiraju/from-metal-to-alerts-with-aws-iot-elasticsearch-7-9-and-kibana-oa8</link>
      <guid>https://forem.com/tejpochiraju/from-metal-to-alerts-with-aws-iot-elasticsearch-7-9-and-kibana-oa8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post continues from my earlier post on &lt;a href="https://dev.to/tejpochiraju/from-metal-to-alerts-with-aws-iot-timestream-and-quicksight-2b5b"&gt;AWS IoT, Timestream and Quicksight&lt;/a&gt;. We replace Timestream and Quicksight with ElasticSearch and Kibana.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;ElasticSearch and Kibana are, &lt;em&gt;probably&lt;/em&gt;, the most commonly used stack for data analysis and visualisation. ElasticSearch is a notoriously difficult software to host and manage yourself. The managed service from AWS has long been one of the more reliable options along side the managed cloud from &lt;a href="https://elastic.co" rel="noopener noreferrer"&gt;Elastic.co&lt;/a&gt;, the creators of ElasticSearch. &lt;/p&gt;

&lt;p&gt;A lot of necessary features like authentication and alerts were only available in the enterprise edition. Until, that is, the release of Open Distro For ElasticSearch by AWS. While causing a &lt;a href="https://news.ycombinator.com/item?id=19359602" rel="noopener noreferrer"&gt;great deal of controversy and ethical dilemma&lt;/a&gt;, ODFE adds authentication, access control, alerts, anomaly detection and, now, notebooks to the open source version release.&lt;/p&gt;

&lt;p&gt;This makes ElasticSearch and Kibana:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more complete than Timestream &amp;amp; QuickSight with alerts and notebooks in the same UI&lt;/li&gt;
&lt;li&gt;more capable than Timestream &amp;amp; Grafana with anomaly detection and notebooks&lt;/li&gt;
&lt;li&gt;more extensible than using Timestream alone with the ability to ingest all kinds of data - not just timeseries. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are we going to build?
&lt;/h2&gt;

&lt;p&gt;We will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use everything we built up to step 3 of our &lt;a href="https://dev.to/blog/metal-to-alerts-with-aws-iot-elasticsearch-kibana"&gt;previous post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set up an AWS IoT rule to route shadow updates to ElasticSearch&lt;/li&gt;
&lt;li&gt;Use Kibana to create visualisations and alerts&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create ElasticSearch Instance
&lt;/h2&gt;

&lt;p&gt;We will use the &lt;a href="https://aws.amazon.com/elasticsearch-service/" rel="noopener noreferrer"&gt;AWS ElasticSearch service&lt;/a&gt; that sets up ElasticSearch and Kibana with ODFE. Since, this is just a demo we will set up a single &lt;code&gt;t2.small&lt;/code&gt; node with &lt;code&gt;10GB&lt;/code&gt; disk size under &lt;code&gt;Deployment type = Development and testing&lt;/code&gt;. We will use version &lt;code&gt;7.9&lt;/code&gt; of ElasticSearch which is the latest on the service as of this writing. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AWS seems to be tracking the releases much better these days. The latest release on &lt;code&gt;Elastic.co&lt;/code&gt; is &lt;code&gt;7.10.1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;CRITICAL SECURITY NOTE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure you read through the notes on network configuration and fine-grained access control for ODFE (not available for &lt;code&gt;t2.small&lt;/code&gt;). For a production system, you should use an instance larger than &lt;code&gt;t2.small&lt;/code&gt; to support encryption at rest and, by extension, access control. &lt;/p&gt;

&lt;p&gt;I am using &lt;code&gt;Public access&lt;/code&gt; here for the purposes of the demo as this instance will be live only for a couple of hours. Google ElasticSearch breaches for the ever increasing counts of exposed servers. &lt;/p&gt;

&lt;h3&gt;
  
  
  Create Index and Mapping
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;index&lt;/code&gt; is the highest logical level within ElasticSearch. Think of these as similar to tables in a database. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need an &lt;code&gt;index&lt;/code&gt; before ingesting data. &lt;/li&gt;
&lt;li&gt;You &lt;em&gt;do not&lt;/em&gt; need data columns and types to be defined before ingestion.&lt;/li&gt;
&lt;li&gt;You need to define &lt;code&gt;mappings&lt;/code&gt; for specific columns that you want to transform during ingestion. &lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;need&lt;/strong&gt; types and columns defined before analysis or visualisation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating an index needs an HTTP &lt;code&gt;PUT&lt;/code&gt; as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flxjrxx32oxc5k9he2kzw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flxjrxx32oxc5k9he2kzw.png" alt="ElasticSearch Index Creation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that we are specifying in the mapping that our &lt;code&gt;timestamp&lt;/code&gt; field should be interpreted by ElasticSearch as a &lt;code&gt;date&lt;/code&gt; (really, datetime) field.&lt;/p&gt;

&lt;p&gt;That's it, our ElasticSearch instance is ready to receive data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisting Shadow Updates
&lt;/h2&gt;

&lt;p&gt;Go to &lt;code&gt;AWS IoT Core -&amp;gt; Act -&amp;gt; Rules&lt;/code&gt; to get started. We will create a new rule &lt;code&gt;SendShadowsToElasticSearch&lt;/code&gt;. Like before, we will add a &lt;code&gt;filter&lt;/code&gt; and an &lt;code&gt;action&lt;/code&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Filter
&lt;/h3&gt;

&lt;p&gt;We will need add back the &lt;code&gt;timestamp&lt;/code&gt; field and we will also need the &lt;code&gt;device_id&lt;/code&gt;. So, our SQL query will now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_usage&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_freq&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_freq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_temp&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ram_usage&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ram_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ram_total&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ram_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;clientid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'$aws/things/+/shadow/update'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Action
&lt;/h3&gt;

&lt;p&gt;ElasticSearch is a first class citizen in the AWS IoT actions list. So, let's select that action and configure it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F382ysn2yz9cvs3laopec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F382ysn2yz9cvs3laopec.png" alt="AWS IoT ES Action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selected the ElasticSearch instance we created in the previous step&lt;/li&gt;
&lt;li&gt;Granted access to AWS IoT via an IAM rule&lt;/li&gt;
&lt;li&gt;Added an &lt;code&gt;id&lt;/code&gt; using the built-in &lt;code&gt;newuuid()&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;Specified our index&lt;/li&gt;
&lt;li&gt;Specified a type &lt;code&gt;_doc&lt;/code&gt; for our data which is the default built-in type for new documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As before, we will enable the &lt;code&gt;CloudWatch&lt;/code&gt; action in case of errors.&lt;/p&gt;

&lt;p&gt;Our rule will finally look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0bulwzb2xt1awv5v7mgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0bulwzb2xt1awv5v7mgm.png" alt="AWS IoT ES Rule Summary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying and Visualisation Using Kibana
&lt;/h2&gt;

&lt;p&gt;Let's start our simulators again and hop over to the Kibana to start analysing our data. The Kibana URL will be visible in your AWS ElasticSearch Console and should just be &lt;code&gt;https://ES_DOMAIN/_plugin/kibana/&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Before we can query the data, we need to create an &lt;a href="https://www.elastic.co/guide/en/kibana/current/index-patterns.html" rel="noopener noreferrer"&gt;&lt;code&gt;index pattern&lt;/code&gt;&lt;/a&gt;. Kibana should prompt you to create an index pattern as soon as you enter. If you missed that, go to &lt;code&gt;Stack Management --&amp;gt; Index Patterns&lt;/code&gt; from the left sidebar. &lt;/p&gt;

&lt;p&gt;Click on &lt;code&gt;Create index pattern&lt;/code&gt; and walk through the wizard. Since your index has been receiving some data, the columns will already be detected. Ensure that you select &lt;code&gt;timestamp&lt;/code&gt; as your date field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9xpe6ym91qxg96qd964a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9xpe6ym91qxg96qd964a.png" alt="ES Create Index Pattern"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1bi77qmgg87gw1u7pa7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1bi77qmgg87gw1u7pa7i.png" alt="ES Create Index Pattern - Date Field"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discover
&lt;/h3&gt;

&lt;p&gt;With the index pattern in place, go to &lt;code&gt;Discover&lt;/code&gt; in the sidebar to see your data streaming in. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Foa8tl9dn7yd992lidv9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Foa8tl9dn7yd992lidv9u.png" alt="Kibana - Discover"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you do see similar output, we can continue to visualisations. If you don't,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check the Cloudwatch Logs for errors&lt;/li&gt;
&lt;li&gt;Verify that your SQL syntax is correct - especially the topic&lt;/li&gt;
&lt;li&gt;Ensure your Rule action has the right index and an appropriate IAM Role&lt;/li&gt;
&lt;li&gt;Verify that your Device Shadow is getting updated by going over to AWS IoT -&amp;gt; Things -&amp;gt; my_iot_device_1 -&amp;gt; Shadow&lt;/li&gt;
&lt;li&gt;Look for errors if any on the terminal where you are running the script.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Visualisations &amp;amp; Dashboard
&lt;/h3&gt;

&lt;p&gt;Once we start receiving data, visualisations within Kibana are easy to set up. Go to &lt;code&gt;Visualisations&lt;/code&gt; in the sidebar and create a line chart for each of our metrics. Specifically,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select the index &lt;code&gt;aws_iot_demo&lt;/code&gt; as data source&lt;/li&gt;
&lt;li&gt;For the Y-axis, select &lt;code&gt;average&lt;/code&gt; as the aggregation and the metric, e.g. &lt;code&gt;cpu_usage&lt;/code&gt; under field. Give a custom label for the axis if you want.&lt;/li&gt;
&lt;li&gt;To enable timeseries visualisation, add an X-axis bucket with aggregation &lt;code&gt;Date Histogram&lt;/code&gt;, field &lt;code&gt;timestamp&lt;/code&gt; and &lt;code&gt;Auto&lt;/code&gt; interval.&lt;/li&gt;
&lt;li&gt;To enable distinction by &lt;code&gt;device_id&lt;/code&gt;, add another bucket, this time with sub-aggregation &lt;code&gt;Terms&lt;/code&gt;, field &lt;code&gt;device_id.keyword&lt;/code&gt; and the default Order By options. 

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;device_id.keyword&lt;/code&gt; indicates that ElasticSearch is indexing the strings stored in the &lt;code&gt;device_id&lt;/code&gt; column.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Click on &lt;code&gt;Update&lt;/code&gt; and your chart should look similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzkzvncuz260axsppgmmv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzkzvncuz260axsppgmmv.png" alt="Kibana Visualisation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create visualisations for the other metrics and add them to a &lt;code&gt;Dashboard&lt;/code&gt; from the sidebar.  Your dashboard should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmjqmwvh8h2xn1wszgzxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmjqmwvh8h2xn1wszgzxj.png" alt="Kibana Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unlike QuickSight, Kibana dashboards can be embedded on websites as snapshots. &lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts
&lt;/h3&gt;

&lt;p&gt;Alerts in Kibana are independent of visualisations, unlike Grafana. They are created in two stages: &lt;strong&gt;monitors&lt;/strong&gt; and &lt;strong&gt;triggers&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitors
&lt;/h3&gt;

&lt;p&gt;Monitors are queries that run on a schedule. Something like the one shown below. Queries can be defined using the UI/Visual Graph, via an &lt;a href="https://opendistro.github.io/for-elasticsearch-docs/docs/alerting/monitors/" rel="noopener noreferrer"&gt;extraction query&lt;/a&gt; or even, excitingly, anomaly detection (more on this in the next section).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F24t28q6r5tscjigeg48u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F24t28q6r5tscjigeg48u.png" alt="Kibana Alert Monitor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Triggers
&lt;/h3&gt;

&lt;p&gt;Triggers compare the value returned by the monitor with specific thresholds. If the threshold is crossed, triggers can notify certain endpoints. At the moment, the built-in destinations are limited to &lt;code&gt;Amazon SNS&lt;/code&gt;, &lt;code&gt;Amazon Chime&lt;/code&gt;, &lt;code&gt;Slack&lt;/code&gt; and &lt;code&gt;Custom webhook&lt;/code&gt;. We will create a trigger without a destination so we can see the alerts in the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flgqlh72jeinbh1ca5uob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flgqlh72jeinbh1ca5uob.png" alt="Kibana Alert Trigger"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once an alert has been triggered, it will be activated and will show up on the Alerts Dashboard. &lt;code&gt;Active&lt;/code&gt; alerts can be &lt;code&gt;Acknowledged&lt;/code&gt; to let the system know that an operator is looking into the issue. Once the metric/query is no longer in breach of the threshold, the alert is &lt;code&gt;Completed&lt;/code&gt;. For a detailed description, check &lt;a href="https://opendistro.github.io/for-elasticsearch-docs/docs/alerting/monitors/#work-with-alerts" rel="noopener noreferrer"&gt;the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fo5o93pcpaiqppsmvdsgs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fo5o93pcpaiqppsmvdsgs.png" alt="Kibana Active Alert"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Anomaly Detection
&lt;/h3&gt;

&lt;p&gt;Anomaly detection is a relatively new feature in ODFE and is getting frequent updates. As of release &lt;code&gt;7.9&lt;/code&gt; on AWS, you can set up &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;detectors&lt;/code&gt; that query all or some portion of your data at regular intervals, and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;models&lt;/code&gt; that monitor specific fields or custom queries for anomalies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will explore this feature in detail in a future post. For now, see &lt;a href="https://opendistro.github.io/for-elasticsearch/blog/odfe-updates/2019/11/real-time-anomaly-detection-in-open-distro-for-elasticsearch/" rel="noopener noreferrer"&gt;this post&lt;/a&gt; and &lt;a href="https://opendistro.github.io/for-elasticsearch-docs/docs/ad/" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; for more on anomaly detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notebooks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://opendistro.github.io/for-elasticsearch-docs/docs/kibana/notebooks/" rel="noopener noreferrer"&gt;Kibana notebooks&lt;/a&gt; are an experimental feature that let you build Jupyter-like notebooks with visualisations and descriptive text. It must be stressed that these are &lt;em&gt;nowhere&lt;/em&gt; close to the functionality of Jupyter, which of course include the power of a complete programming langauge like Python. However, they are a start and we are excited to see how they progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;ElasticSearch and Kibana are large, commendable feats of engineering that power everything from our search engines to our on-demand taxi services. The stack's versatility is clear from its suitability for time-series data too. &lt;/p&gt;

&lt;p&gt;When comparing QuickSight, Grafana and ElasticSearch+Kibana, we would recommend:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestream + QuickSight&lt;/strong&gt; if you&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prefer costs that more linearly scale with use&lt;/li&gt;
&lt;li&gt;prefer using AWS in-house offerings&lt;/li&gt;
&lt;li&gt;have data in multiple data sources that you need to analyse without duplication&lt;/li&gt;
&lt;li&gt;don't need to visualise geospatial data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Timestream + Grafana&lt;/strong&gt; if you&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;need timeseries charts and alerts&lt;/li&gt;
&lt;li&gt;need gorgeous dashboards with easy (time) filtering&lt;/li&gt;
&lt;li&gt;don't need to create complex BI style analyses across multiple data sources&lt;/li&gt;
&lt;li&gt;don't need anomaly detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ElasticSearch + Kibana&lt;/strong&gt; if you&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;need flexibility in the type of charts and analyses&lt;/li&gt;
&lt;li&gt;need anomaly detection&lt;/li&gt;
&lt;li&gt;need to index and search text data too&lt;/li&gt;
&lt;li&gt;don't mind duplicating your data into ElasticSearch&lt;/li&gt;
&lt;li&gt;don't mind paying fixed monthly costs for your instance&lt;/li&gt;
&lt;li&gt;have the ability to secure and monitor your ES instance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ideas, questions or corrections?
&lt;/h2&gt;

&lt;p&gt;Write to us at &lt;a href="//mailto:hello@iotready.co"&gt;hello@iotready.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>From Metal To Alerts With AWS IoT, Timestream and QuickSight</title>
      <dc:creator>Tej Pochiraju</dc:creator>
      <pubDate>Wed, 23 Dec 2020 15:19:48 +0000</pubDate>
      <link>https://forem.com/tejpochiraju/from-metal-to-alerts-with-aws-iot-timestream-and-quicksight-2b5b</link>
      <guid>https://forem.com/tejpochiraju/from-metal-to-alerts-with-aws-iot-timestream-and-quicksight-2b5b</guid>
      <description>&lt;h1&gt;
  
  
  AWS IoT Demo
&lt;/h1&gt;

&lt;p&gt;A metal-to-alerts example of how to build an IoT enabled monitoring solution using only AWS PaaS offerings. &lt;/p&gt;

&lt;h1&gt;
  
  
  Why
&lt;/h1&gt;

&lt;p&gt;We have been doing this workflow for 5+ years now, much longer if you count fully custom tools. So have countless others. Yet, if you asked a new engineer to build this, you can wave goodbye to a few weeks of their effort as they navigate obtuse documentation and outdated StackOverflow answers. This is our attempt to document what's possible with PaaS tools in 2020. &lt;/p&gt;

&lt;h2&gt;
  
  
  What are we going to build?
&lt;/h2&gt;

&lt;p&gt;To achieve this, we will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a Python script that monitors system metrics (CPU, Memory, Temperature, Fan)

&lt;ol&gt;
&lt;li&gt;You could replace this with actual hardware, perhaps run this script on an Raspberry Pi or even send FreeRTOS metrics from ESP32 but we will save that for another day.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Create multiple &lt;code&gt;things&lt;/code&gt; on AWS IoT Core.
&lt;/li&gt;

&lt;li&gt;Send these metrics as shadow updates to AWS IoT every 10s (configurable)&lt;/li&gt;

&lt;li&gt;Configure AWS IoT to route shadow updates to a database&lt;/li&gt;

&lt;li&gt;Set up a visualisation tool and create dashboards using these updates &lt;/li&gt;

&lt;li&gt;Add a few simple alerts on our visualiation tool to send notifications if system metrics cross a threshold&lt;/li&gt;

&lt;/ol&gt;

&lt;h2&gt;
  
  
  Git Repo Structure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;master&lt;/code&gt; = final code + AWS IoT configuration + Grafana dashboard JSON&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1_python_script&lt;/code&gt; = Python script without AWS IoT integration (print to console)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2_aws_iot&lt;/code&gt; = Python script with AWS IoT integration (shadow updates)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1 - Python Script For System Metrics
&lt;/h2&gt;

&lt;p&gt;We are going to adapt this excellent &lt;a href="https://www.pragmaticlinux.com/2020/12/monitor-cpu-and-ram-usage-in-python-with-psutil/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; to create our system monitor script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate
echo psutil==5.8.0 &amp;gt; requirements.txt
pip install -r requirements.txt
touch sysmon.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Edit &lt;code&gt;sysmon.py&lt;/code&gt; in your preferred text editor and add in the following functions from the blog post:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_cpu_usage_pct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_cpu_frequency&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_cpu_temp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_ram_usage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_ram_total&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Next, create a &lt;code&gt;main&lt;/code&gt; function that calls each of these functions and populates a dictionary: &lt;code&gt;payload&lt;/code&gt; and prints it.

&lt;ol&gt;
&lt;li&gt;We will also add a &lt;code&gt;timestamp&lt;/code&gt; to the payload for use in visualisations later.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Add a &lt;code&gt;while(1)&lt;/code&gt; that calls this &lt;code&gt;main&lt;/code&gt; function every 10 seconds.&lt;/li&gt;

&lt;li&gt;Add an argument parser so we can pass the &lt;code&gt;interval&lt;/code&gt; and a &lt;code&gt;device_id&lt;/code&gt; as command line arguments.&lt;/li&gt;

&lt;li&gt;Run the script with &lt;code&gt;python sysmon.py 10 my_iot_device_1&lt;/code&gt;
&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;You should see output similar to:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F39xwxlty7a4dtsp73z10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F39xwxlty7a4dtsp73z10.png" alt="sysmon.py output"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2 - AWS IoT Integration
&lt;/h2&gt;

&lt;p&gt;Now, we will add in the ability to send our metrics to AWS IoT. But first, we need to register our devices or &lt;code&gt;things&lt;/code&gt; as AWS calls them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Registering the devices
&lt;/h3&gt;

&lt;p&gt;We will register the devices individually via the AWS Console. However, if you have a large number of devices to register, you may want to script it or use &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/provision-template.html" rel="noopener noreferrer"&gt;Bulk Registration&lt;/a&gt; via &lt;code&gt;aws-cli&lt;/code&gt; or the AWS IoT Core Console. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We are using &lt;code&gt;us-east-1&lt;/code&gt; aka N. Virginia for integration later with Amazon Timestream which is not yet available in all regions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;code&gt;Create a single thing&lt;/code&gt;

&lt;ol&gt;
&lt;li&gt;Give your thing a name, e.g. &lt;code&gt;my_iot_device_1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You can skip &lt;code&gt;Thing Type&lt;/code&gt; and &lt;code&gt;Group&lt;/code&gt; for this demo.&lt;/li&gt;
&lt;li&gt;Create the thing&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;One-click certificate creation (recommended)&lt;/code&gt; option to generate the certificates.

&lt;ol&gt;
&lt;li&gt;Download the generated certificates and the root CA certificate.&lt;/li&gt;
&lt;li&gt;Activate the certificates.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Attach a policy and register the &lt;code&gt;Thing&lt;/code&gt;. 

&lt;ol&gt;
&lt;li&gt;Because we are cavalier and this is a demo, we are using the following &lt;code&gt;PubSubToAny&lt;/code&gt; policy. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DO NOT&lt;/strong&gt; use this in production!
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iot:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now repeat this a couple more times so we have a few things. I am setting up 3 devices with the imaginative names: &lt;code&gt;my_iot_device_1&lt;/code&gt;, &lt;code&gt;my_iot_device_2&lt;/code&gt;, &lt;code&gt;my_iot_device_3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we will rename our certificates to match our thing names so that it's easier to script together. For instance, I am using the rename utility to bulk rename my certificates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9s0dytod4skn237pycrf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9s0dytod4skn237pycrf.png" alt="Bulk renaming certs"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding the AWS IoT SDK
&lt;/h3&gt;

&lt;p&gt;Because AWS IoT supports MQTT, we could use any MQTT client that supports X.509 certificates. However, to keep things simple, we will use the &lt;a href="https://github.com/aws/aws-iot-device-sdk-python" rel="noopener noreferrer"&gt;official Python SDK&lt;/a&gt; from AWS IoT. Specifically, we will adapt the &lt;a href="https://github.com/aws/aws-iot-device-sdk-python/blob/master/samples/basicShadow/basicShadowUpdater.py" rel="noopener noreferrer"&gt;&lt;code&gt;basicShadowUpdater.py&lt;/code&gt; sample&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Please inspect &lt;code&gt;aws_shadow_upater.py&lt;/code&gt; for the changes we are making. Primarily, we are wrapping the functionality into 2 functions:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;init_device_shadow_handler&lt;/code&gt; that takes AWS IoT specific config parameters and returns a &lt;code&gt;deviceShadowHandler&lt;/code&gt; specific to our configuration and thing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_device_shadow&lt;/code&gt; that takes our system metrics payload and wraps it into a &lt;code&gt;json&lt;/code&gt; structure that AWS IoT expects for &lt;code&gt;device shadows&lt;/code&gt;. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We will also take this opportunity to modularise our code a bit by moving the &lt;code&gt;main&lt;/code&gt; function from &lt;code&gt;sysmon.py&lt;/code&gt; into its own separate file.&lt;/li&gt;
&lt;li&gt;Within &lt;code&gt;main.py&lt;/code&gt; we are reading our AWS configuration from a combination of environment variables and the local certificates.

&lt;ul&gt;
&lt;li&gt;We only need the following: &lt;code&gt;export AWS_IOT_HOST=YOUR_AWS_IOT_ENDPOINT.amazonaws.com&lt;/code&gt; and &lt;code&gt;export CERTS_DIR=certs&lt;/code&gt; assuming you are keeping your certificates in &lt;code&gt;certs/&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;You will probably want to create a script or &lt;code&gt;.env&lt;/code&gt; file to set these environment variables&lt;/li&gt;
&lt;li&gt;For good measure, we are also verifying that the certificates actually exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;With this done, we stitch our two modules &lt;code&gt;sysmon.py&lt;/code&gt; and &lt;code&gt;aws_shadow_updater.py&lt;/code&gt; together and start publishing updates. If all goes well, you should see the following in your terminal and your AWS Console (go to Thing -&amp;gt; Shadows -&amp;gt; Classic Shadow)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu9ivwvdfb41xl4e26qe4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu9ivwvdfb41xl4e26qe4.png" alt="Terminal Output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsril0jkndmp46b7zm7u6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsril0jkndmp46b7zm7u6.png" alt="AWS Console"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3 - Simulating Multiple Devices
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;We are done with almost all of the coding needed to get this working. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an easy one, open up multiple terminals/tabs and start a separate process for updating the shadow for each &lt;code&gt;device&lt;/code&gt;. Something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F14iw28rkq6rdycexuqb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F14iw28rkq6rdycexuqb8.png" alt="Multiple Devices"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4 - Persisting Shadow Updates
&lt;/h2&gt;

&lt;p&gt;In order to visualise, and perhaps analyse, these metrics, we need to persist them in some form of database. Thankfully, AWS IoT has a &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-rules.html" rel="noopener noreferrer"&gt;Rules Engine&lt;/a&gt; designed for just this purpose. The Rules Engine is essentially a message router with the ability to filter messages using an SQL syntax and send them to various destinations.&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;AWS IoT Core -&amp;gt; Act -&amp;gt; Rules&lt;/code&gt; to get started. &lt;/p&gt;

&lt;p&gt;There are 2 steps to enabling rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Filter: Select the messages we want to act on.&lt;/li&gt;
&lt;li&gt;Act: Select the action(s) we want to run for each filtered message.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Filter
&lt;/h3&gt;

&lt;p&gt;AWS IoT uses a reduced &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-reference.html" rel="noopener noreferrer"&gt;SQL syntax&lt;/a&gt; for filtering messages. Points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-pub-sub-topic" rel="noopener noreferrer"&gt;shadow topic&lt;/a&gt; we are interested in is &lt;code&gt;$aws/things/thingName/shadow/update&lt;/code&gt; where we need to replace &lt;code&gt;thingName&lt;/code&gt; with the wildcard &lt;code&gt;+&lt;/code&gt;. Follow &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/topics.html" rel="noopener noreferrer"&gt;this reference&lt;/a&gt; on topics and wildcards.&lt;/li&gt;
&lt;li&gt;The content of each message contains the entire &lt;code&gt;state&lt;/code&gt; with &lt;code&gt;desired&lt;/code&gt; and &lt;code&gt;reported&lt;/code&gt; properties as well as other metadata. We will need to unpack the &lt;code&gt;reported&lt;/code&gt; property to get the data we need. Follow &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-select.html" rel="noopener noreferrer"&gt;this reference&lt;/a&gt; for more details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our SQL filter will look essentially like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_usage&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_freq&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_freq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_temp&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cpu_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ram_usage&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ram_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ram_total&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ram_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'$aws/things/+/shadow/update'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we can save this rule, we will also need to add an &lt;code&gt;action&lt;/code&gt;. Actions define what to do with the filtered messages. This depends on our choice of database. &lt;/p&gt;

&lt;h3&gt;
  
  
  Act
&lt;/h3&gt;

&lt;p&gt;AWS IoT supports a large range of actions out of the box including CloudWatch, DynamoDB, ElasticSearch, Timestream DB and custom HTTP endpoints. See the &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-rule-actions.html" rel="noopener noreferrer"&gt;full list here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To confirm that our messages are coming through and we are able to store them, we will use the shiny, new time series database from AWS - Timestream. We will also enable the &lt;code&gt;CloudWatch&lt;/code&gt; action in case of errors.&lt;/p&gt;

&lt;h4&gt;
  
  
  Timestream DB
&lt;/h4&gt;

&lt;p&gt;As of this writing Timestream is only available in 4 regions. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkl7ihzu2g7qeoiqymd2u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkl7ihzu2g7qeoiqymd2u.png" alt="Timestream Regions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;essential&lt;/strong&gt; to create the DB in the same region as your AWS IoT endpoint as the Rules Engine does not, yet, support multiple regions for the built-in actions. _You could use a Lambda function to do this for you but that's more management and cost.&lt;/p&gt;

&lt;p&gt;We will create a &lt;code&gt;Standard&lt;/code&gt; (empty) DB with the name &lt;code&gt;aws_iot_demo&lt;/code&gt;: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1d9y9gx57k49qxryjsg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1d9y9gx57k49qxryjsg7.png" alt="Create Timestream DB"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will also need a &lt;code&gt;table&lt;/code&gt; to store our data, so let's do that too:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fuvm3wffyjl2lukx8ie72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fuvm3wffyjl2lukx8ie72.png" alt="Create Timestream Table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once this is done, we can return to the rule we started creating earlier and add a new Action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fvqon5iugd25zqhbp9ruy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fvqon5iugd25zqhbp9ruy.png" alt="AWS IoT Action - Timestream"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AWS IoT Rule Action for Timestream needs at least one &lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/timestream-rule-action.html" rel="noopener noreferrer"&gt;&lt;code&gt;dimension&lt;/code&gt;&lt;/a&gt; to be specified. Dimensions can be used for grouping and filtering incoming data. &lt;/li&gt;
&lt;li&gt;I used the following &lt;code&gt;key&lt;/code&gt;:&lt;code&gt;value&lt;/code&gt; pair using a substitution template - &lt;code&gt;device_id&lt;/code&gt;: &lt;code&gt;${clientId()}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We are sending the device timestamp as part of the shadow update. If we include it as part of the &lt;code&gt;SELECT&lt;/code&gt; query in the rule, Timestream will assume that &lt;code&gt;timestamp&lt;/code&gt; is a measurement metric too. 

&lt;ul&gt;
&lt;li&gt;Instead, we will ignore the device &lt;code&gt;timestamp&lt;/code&gt; and use &lt;code&gt;${timestamp()}&lt;/code&gt; as the time parameter within the Rule Action. This generates a server timestamp.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;You will also need to create or select an appropriate IAM role that lets AWS IoT to write to Timestream.&lt;/li&gt;

&lt;li&gt;Timestream creates separate rows for each metric so each shadow update creates 5 rows. &lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  CloudWatch
&lt;/h4&gt;

&lt;p&gt;This action is triggered if/when there is an error while processing our rule. Again, follow the guided wizard to create a new &lt;code&gt;Log Group&lt;/code&gt; and assign permissions.&lt;/p&gt;

&lt;p&gt;At the end, your rule should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqr2iyolv65isje7ebvjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqr2iyolv65isje7ebvjv.png" alt="AWS IoT Rule Summary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Timestream
&lt;/h3&gt;

&lt;p&gt;Assuming we have started our simulators again, we should start to see data being stored in Timestream. Go over to AWS Console -&amp;gt; Timestream -&amp;gt; Tables -&amp;gt;  &lt;code&gt;aws_iot_demo&lt;/code&gt; -&amp;gt; Query Table. Type in the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Get the 20 most recently added data points in the past 15 minutes. You can change the time period if you're not continuously ingesting data&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"aws_iot_demo"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"aws_iot_demo"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output similar to the one below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1gc9tz5t18mtii7nqgx2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1gc9tz5t18mtii7nqgx2.png" alt="Timestream Query Output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will notice the separate rows for each metric. We will need a different query in order to combine the metrics into a single view, for instance for use with visualisation or analytics tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;time_bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cpu_usage'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_cpu_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cpu_freq'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_cpu_freq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cpu_temp'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_cpu_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ram_usage'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ram_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ram_total'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ram_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"aws_iot_demo"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"aws_iot_demo"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;BIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;time_bin&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should look something like this - &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffchwjlshs3fqqkiby7o1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffchwjlshs3fqqkiby7o1.png" alt="Timestream Combined Output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you do see similar output, you are in business and we can continue to visualisation. If you don't,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check the Cloudwatch Logs for errors&lt;/li&gt;
&lt;li&gt;Verify that your SQL syntax is correct - especially the topic&lt;/li&gt;
&lt;li&gt;Ensure your Rule action has the right table and an appropriate IAM Role&lt;/li&gt;
&lt;li&gt;Verify that your Device Shadow is getting updated by going over to AWS IoT -&amp;gt; Things -&amp;gt; my_iot_device_1 -&amp;gt; Shadow&lt;/li&gt;
&lt;li&gt;Looking for errors if any on the terminal where you are running the script.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pause For Breath
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fw4hz4xsq3qvk5k3nbm5t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fw4hz4xsq3qvk5k3nbm5t.jpg" alt="Sunrise"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have covered a lot of ground. So, let's pause and reflect. Here's what we have done so far:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Created a Python script to monitor common system metrics.&lt;/li&gt;
&lt;li&gt;Hooked up this script to AWS IoT using the SDK and &lt;code&gt;Thing&lt;/code&gt; certificates.&lt;/li&gt;
&lt;li&gt;Simulated running multiples of these devices with sending a &lt;code&gt;Shadow&lt;/code&gt; update.&lt;/li&gt;
&lt;li&gt;Created a rule to persist these device shadows to &lt;code&gt;Timestream&lt;/code&gt; and errors to &lt;code&gt;CloudWatch&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verified that we are actually getting our data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, we only have the small matter of visualising our data and setting up alerts in case any of our metrics cross critical thresholds. &lt;/p&gt;

&lt;p&gt;With a fresh cup of coffee, onwards...&lt;/p&gt;

&lt;h2&gt;
  
  
  5 - Visualisation
&lt;/h2&gt;

&lt;p&gt;Storage and visualisation are, in fact, two separate operations that need two different software tools. However, these are often so tightly coupled that choice of one often dictates choice of the other. Here's a handy table that illustrates this.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Visualisation&lt;/th&gt;
&lt;th&gt;Comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timestream&lt;/td&gt;
&lt;td&gt;AWS QuickSight&lt;/td&gt;
&lt;td&gt;See demo below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestream&lt;/td&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;See demo below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;AWS QuickSight&lt;/td&gt;
&lt;td&gt;Needs CSV export to S3 first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;Redash&lt;/td&gt;
&lt;td&gt;Works but with limitations, demo in future post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElasticSearch&lt;/td&gt;
&lt;td&gt;Kibana&lt;/td&gt;
&lt;td&gt;Works well, demo in future post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElasticSearch&lt;/td&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;Simpler to just use Kibana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InfluxDB&lt;/td&gt;
&lt;td&gt;InfluxDB UI&lt;/td&gt;
&lt;td&gt;Works well, demo in future post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InfluxDB&lt;/td&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;Simpler to just use the built-in UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are, of course, numerous other ways to do this. We will focus on the first two in that table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestream + AWS QuickSight
&lt;/h3&gt;

&lt;p&gt;QuickSight is a managed BI tool from AWS. The &lt;a href="https://docs.aws.amazon.com/timestream/latest/developerguide/QuickSight.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; to integrate Timestream with QuickSight is a little dense. However, it's pretty straightforward if you are using &lt;code&gt;us-east-1&lt;/code&gt; as your region. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Within QuickSight, click on the user icon at the top right and then on &lt;code&gt;Manage QuickSight&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Here, go to &lt;code&gt;Security &amp;amp; Permissions&lt;/code&gt; -&amp;gt; &lt;code&gt;QuickSight access to AWS services&lt;/code&gt; and enable &lt;code&gt;Timestream&lt;/code&gt; (see image below).&lt;/li&gt;
&lt;li&gt;Next, within QuickSight, click on &lt;code&gt;New Dataset&lt;/code&gt; and select &lt;code&gt;Timestream&lt;/code&gt;. Click on &lt;code&gt;Validate Connection&lt;/code&gt; to ensure you have given the permissions and confirm.&lt;/li&gt;
&lt;li&gt;Upon confirmation, select &lt;code&gt;aws_iot_demo&lt;/code&gt; from the discovered databases and select &lt;code&gt;aws_iot_demo&lt;/code&gt; from the tables.&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;Visualise&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So far so good. I had to struggle for a while to understand how to get QuickSight to unpack the metrics from Timestream. Turns out, this is surprisingly easy if you follow this &lt;a href="https://www.youtube.com/watch?v=TzW4HWl-L8s" rel="noopener noreferrer"&gt;tutorial video&lt;/a&gt; from AWS. Essentially,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create multiple visualisations&lt;/li&gt;
&lt;li&gt;For each visualisation, add a filter on &lt;code&gt;measure_name&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;time&lt;/code&gt; to add it to the X-axis. Change period to &lt;code&gt;Aggregrate: Minute&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;measure_value::bigint&lt;/code&gt; or &lt;code&gt;measure_value::double&lt;/code&gt; depending on the metric to add it to the Y-axis. Change to &lt;code&gt;Aggregate: Average&lt;/code&gt;. 

&lt;ul&gt;
&lt;li&gt;In our case, only &lt;code&gt;cpu_usage&lt;/code&gt; is a &lt;code&gt;double&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Click on &lt;code&gt;device_id&lt;/code&gt; to add separate lines for each device. This is added to &lt;code&gt;Color&lt;/code&gt; in QuickSight.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;That's it! My dashboard looks like this - &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsjjpaky54rdr2ykgw94u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsjjpaky54rdr2ykgw94u.png" alt="QuickSight Timestream Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;QuickSight is a full-fledged business intelligence (BI) tool with the ability to integrate with multiple data sources. QuickSight also has built-in anomaly detection. This makes it an incredibly powerful tool to use for IoT visualisations and analysis. We could even bring in non-IoT data such as that from an ERP. More on this in a later post!&lt;/p&gt;

&lt;p&gt;However, QuickSight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does not support alerts&lt;/li&gt;
&lt;li&gt;Does not support calculations&lt;/li&gt;
&lt;li&gt;Has limited visualisations&lt;/li&gt;
&lt;li&gt;Does not support dashboard embeds, e.g. in a webpage&lt;/li&gt;
&lt;li&gt;Charges &lt;a href="https://aws.amazon.com/quicksight/pricing/?nc=sn&amp;amp;loc=4" rel="noopener noreferrer"&gt;per user&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mind you, the AWS IoT rules engine can be used quite easily for alerts so you &lt;em&gt;don't&lt;/em&gt; really need alerts in a separate tool. Having said that...&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestream + Grafana
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; has long been a favourite of anyone looking to create beautiful, lightweight dashboards. Grafana integrates with a zillion data sources via input plugins and has numerous visualisations via output plugins. Grafana only does visualisations and alerts but does it really well. There are open source and enterprise editions available.&lt;/p&gt;

&lt;p&gt;AWS has an upcoming managed &lt;a href="https://aws.amazon.com/grafana/" rel="noopener noreferrer"&gt;Grafana service&lt;/a&gt;. Until then, we will use the managed service from &lt;a href="https://grafana.com" rel="noopener noreferrer"&gt;Grafana Cloud&lt;/a&gt;. You could also spin up Grafana locally or on a VM somewhere with the &lt;a href="https://grafana.com/docs/grafana/latest/installation/docker/" rel="noopener noreferrer"&gt;docker image&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://www.youtube.com/watch?v=pilkz645cs4" rel="noopener noreferrer"&gt;video tutorial&lt;/a&gt; available but you will need to adapt a fair bit to our example. Assuming you have either signed up for Grafana Cloud or installed it locally, you should now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the &lt;a href="https://grafana.com/grafana/plugins/grafana-timestream-datasource/installation" rel="noopener noreferrer"&gt;Amazon Timestream plugin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Back in Grafana, add a new &lt;code&gt;Data Source&lt;/code&gt; and search for &lt;code&gt;Timestream&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;For authentication, we will use Access Key and Secret for a new IAM User.

&lt;ul&gt;
&lt;li&gt;Back in AWS, create a new user with the ~&lt;code&gt;AmazonTimestreamReadOnlyAccess&lt;/code&gt; policy attached~ &lt;code&gt;admin&lt;/code&gt; rights. For some reason, Grafana would not connect to Timestream even with the &lt;code&gt;AmazonTimestreamFullAccess&lt;/code&gt; policy attached.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Once the keys are in place, click on &lt;code&gt;Save &amp;amp; Test&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;Select &lt;code&gt;aws_iot_demo&lt;/code&gt; in the &lt;code&gt;$_database&lt;/code&gt; field to set up the default DB. Try as I might, I could not get the dropdown for &lt;code&gt;$_table&lt;/code&gt; to populate.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbaryoxop7dl55tc0w17n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbaryoxop7dl55tc0w17n.png" alt="Grafana Timestream Settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, click on &lt;code&gt;+ -&amp;gt; Dashboard&lt;/code&gt; and &lt;code&gt;+ Add new panel&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;Unlike QuickSight, Grafana allows you to build queries using &lt;strong&gt;SQL&lt;/strong&gt;. So, for our first panel, let's create a CPU Usage chart with the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CREATE_TIME_SERIES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;measure_value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_cpu_usage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__table&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__timeFilter&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;measure_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'$__measure'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsa8vl62jcfm6qihbfz2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsa8vl62jcfm6qihbfz2p.png" alt="Grafana Panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are essentially doing the same as QuickSight by defining a &lt;code&gt;where&lt;/code&gt; clause to filter by metric and creating a time series that is grouped by &lt;code&gt;device_id&lt;/code&gt;. The one, big, difference is the Grafana allows you to add multiple such queries to a single visualisation chart (panel in Grafana speak). Duplicating this panel and making the mods necessary, we end up with a dashboard very similar to that in QuickSight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5qw2f322qzmhdonq20o3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5qw2f322qzmhdonq20o3.png" alt="Grafana Timestream Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that we get dashboard wide time controls for free!&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts With Grafana
&lt;/h3&gt;

&lt;p&gt;Creating alerts with Grafana is surprisingly easy. Alerts use the same query as the panel and are created in the same UI. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1m2rer7p6jq3kpm8fcvc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1m2rer7p6jq3kpm8fcvc.png" alt="Grafana Create Alert"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default, the alert is triggered on the average of the metric but you can change it a different calculation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Filn4p3otkbikija6rf85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Filn4p3otkbikija6rf85.png" alt="Grafana Alert Options"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have multiple queries on a panel, you can even use a combination of queries!&lt;/p&gt;

&lt;p&gt;Alerts can trigger notifications to various &lt;code&gt;channels&lt;/code&gt; with built-in options for all the major chat apps, email and webhooks. For instance, if you wanted to trigger a notification within a mobile app, you would set up an API somewhere that will be triggered by a webhook configuration within Grafana. Your API is then responsible for notifying the mobile app.&lt;/p&gt;

&lt;p&gt;For more on Grafana alerts, check out &lt;a href="https://grafana.com/docs/grafana/latest/alerting/create-alerts/" rel="noopener noreferrer"&gt;the docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Once you get around the verbose documentation, and refine your search skills, it's quite straightforward to create an end-to-end flow for most IoT use cases using purely platform-as-a-service offerings. &lt;/p&gt;

&lt;p&gt;We have been running a deployment on AWS for a customer with ~46000 devices for 2+ years now, handling 15-20M messages monthly. All this for a fraction of the cost and attention this would need if we ran the infrastructure ourselves.&lt;/p&gt;

&lt;p&gt;That said, I do have a few reservations and will explore those in future posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ideas, questions or corrections?
&lt;/h2&gt;

&lt;p&gt;Write to us at &lt;a href="mailto:hello@iotready.co"&gt;hello@iotready.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>grafana</category>
      <category>quicksight</category>
    </item>
  </channel>
</rss>
