diff --git a/content/datalab/tutorials/Monitoring/Getting started.ipynb b/content/datalab/tutorials/Monitoring/Getting started.ipynb new file mode 100644 index 000000000..eced25e7f --- /dev/null +++ b/content/datalab/tutorials/Monitoring/Getting started.ipynb @@ -0,0 +1,750 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting started with the Monitoring API\n", + "\n", + "Cloud Datalab provides an environment for working with your data. This includes data that is being managed within the Monitoring API. This notebook introduces some of the APIs that Cloud Datalab provides for working with the monitoring data, and allows you to try them out on your own project.\n", + "\n", + "The main focus of this API is to allow you to query timeseries data for your monitored resources. There are also commands to help you list the available metrics and resource types." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing the API" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from gcp.stackdriver import monitoring as gcm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List names of Compute Engine CPU metrics\n", + "\n", + "Here we can see that `instance_name` is a metric label." + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
Metric typeKindValueLabels
compute.googleapis.com/instance/cpu/reserved_coresGAUGEDOUBLEinstance_name
compute.googleapis.com/instance/cpu/usage_timeDELTADOUBLEinstance_name
compute.googleapis.com/instance/cpu/utilizationGAUGEDOUBLEinstance_name
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 89, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%monitoring list metrics --type compute*/cpu/*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List monitored resource types related to GCE" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
Resource typeLabels
gce_diskproject_id, disk_id, zone
gce_instanceproject_id, instance_id, zone
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%monitoring list resource_types --type gce*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying timeseries data\n", + "\n", + "The `gcm.Query` class allows users to query and access the monitoring timeseries data. The results of the query can be accessed as a pandas `DataFrame` object. This is a widely used library for data manipulation.\n", + "\n", + "http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initializing the query\n", + "\n", + "During intialization, the metric type and the time interval need to be specified. For interactive use, the metric type has a default value. The simplest way to specify the time interval that ends `now` is to use the arguments `days`, `hours`, and `minutes`." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "q = gcm.Query('compute.googleapis.com/instance/cpu/utilization', hours=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Getting the metadata\n", + "\n", + "The method `labels_as_dataframe()` allows you to look at the labels for the given metric, and the values that exist.\n", + "\n", + "This helps you understand the structure of the timeseries data, and makes it easier to modify the query." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
resourceresource.labelsmetric.labels
typeproject_idzoneinstance_idinstance_name
0gce_instancemonitoring-datalabus-central1-b2292393497529867090gae-datalab-main-j642
1gce_instancemonitoring-datalabus-east1-d5437900963820317613analyst2
\n", + "
" + ], + "text/plain": [ + " resource resource.labels \\\n", + " type project_id zone instance_id \n", + "0 gce_instance monitoring-datalab us-central1-b 2292393497529867090 \n", + "1 gce_instance monitoring-datalab us-east1-d 5437900963820317613 \n", + "\n", + " metric.labels \n", + " instance_name \n", + "0 gae-datalab-main-j642 \n", + "1 analyst2 " + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "q.labels_as_dataframe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering the query\n", + "\n", + "Here, we filter the query to instances whose names begin with `'gae-'`. These are instances which host `Cloud Datlab`.\n", + "\n", + "**Note**: If running locally, you may need to replace 'gae-' by the name of an instance in your project.\n", + "You can find the name of instances in your project in the last column of the results from `labels_as_dataframe` above." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gae-datalab-main-j642
2016-04-07 18:07:01.7630.042580
2016-04-07 18:08:01.7630.045067
2016-04-07 18:09:01.7630.041776
2016-04-07 18:10:01.7630.053421
2016-04-07 18:11:01.7630.075612
\n", + "
" + ], + "text/plain": [ + " gae-datalab-main-j642\n", + "2016-04-07 18:07:01.763 0.042580\n", + "2016-04-07 18:08:01.763 0.045067\n", + "2016-04-07 18:09:01.763 0.041776\n", + "2016-04-07 18:10:01.763 0.053421\n", + "2016-04-07 18:11:01.763 0.075612" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Filter the query to instances with the specified prefix.\n", + "q1 = q.select_metrics(instance_name_prefix='gae-')\n", + "\n", + "# If you know the exact name of the instance, you can also use:\n", + "# q1 = q.select_metrics(instance_name='" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# N.B. A useful trick is to assign the return value of plot to _ \n", + "# so that you don't get text printed before the plot itself.\n", + "\n", + "_ = df1.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Aligning the query\n", + "\n", + "For multiple timeseries, aligning the data is recommended. The alignment offset can be specified using the arguments `hours`, `minutes`, and `seconds`." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeMAAAFXCAYAAACRLCZbAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xl8G+WdP/DPzEgjyZIs2fERx0mc4JDbCeQCQhIKoSQ0\nlFIIJFyFpktpt2S3bH+0WyBQSloWKGG7bGmbAg3QQqAtLUfKAgmlgQC5CDj34dx24tvWrZFm5veH\npJGd+JBsHXN83y94WZbG0mNHj77P+X0YWZZlEEIIISRv2HwXgBBCCDE6CsaEEEJInlEwJoQQQvKM\ngjEhhBCSZxSMCSGEkDyjYEwIIYTkWUrBeOPGjVi4cCEWLFiA1atX93jN5s2bcc011+Cqq67Crbfe\nmtFCEkIIIXrG9LfPWJIkLFiwAGvWrEFZWRkWL16MVatWobq6WrnG6/Vi6dKleO6551BeXo62tjYU\nFxdnvfCEEEKIHvTbM66trUVVVRUqKythNpuxaNEibNiwods1b775Jq644gqUl5cDAAViQgghJA39\nBuPGxkZUVFQo35eXl6OpqanbNUePHkVnZyduvfVWXHfddfjb3/6W+ZISQgghOmXKxJOIoog9e/bg\n+eefRyAQwNKlS3H++eejqqoqE09PCCGE6Fq/wbi8vBwNDQ3K942NjSgrKzvrmqKiIlgsFlgsFsyY\nMQP79u3rMxjLsgyGYQZRdEIIIUQf+g3GNTU1OH78OOrr61FaWop169Zh1apV3a6ZP38+Vq5cCVEU\nIQgCamtr8c1vfrPP52UYBs3N3sGVnpAsKC110nuTEBXTch0tLXX2eH+/wZjjOKxYsQLLli2DLMtY\nvHgxqqursXbtWjAMgyVLlqC6uhpz5szB1VdfDZZlccMNN2DMmDEZ/yUIIYQQPep3a1M2abVlQ/RN\ny61uQoxAy3W0t54xZeAihBBC8oyCMSGEEJJnFIwJIYSQPKNgTAghhOQZBWNCCCEkzzKSgYsQQkj+\nvf32W9i3bw/uvvuHaf3cjh3bYTabMXnyFADAK6/8EW+++TpMJhPcbjd+/OMHUF4+NBtFJnHUMyaE\nEB0ZSGbDHTu2Y+fOWuX7sWPH49lnX8SaNS/hS1+6DE8//ctMFpH0gHrGhBCiAj/+8f9Dc3MTBCGM\n66+/EV/96jX48pfn4frrl+Ljjz+C1WrFI488gaKiImza9CGef/5ZRKNRuFwuPPDAShQVFSnPFQgE\ncNttN2Lt2tfAcRwCAT9uu+0mrF37Gl577U94/fXXYDKZMGrUaHznO3fh9df/Ao4z4b333sb3v38P\nzj9/uvJckybV4N13/y8ffxJDoWBMCCFxr75/CFv3NfV/YRpmji/DDZf1n5Hw3nsfhNPpRDgcxh13\nfAOXXHIpQqEgamqm4Nvf/lc8/fT/4M03/4pvfGMZpk49H6tXrwEAvPXW3/DHPz6Pu+76vvJcBQUF\nmDZtOj755CPMmXMJ1q9/F5deehk4jsMf//g8/vznN2EymeD3+2C3O/C1r12HgoICLF16y1nleuut\n13HhhbMz9vcgPaNgTAghKvDqqy/hww//CQBoamrCiRMnYDbzuOiiOQCAceMmYNu2LfHHT+OBB/4b\nra0tiEajqKgYdtbzXXXV1/DSSy9izpxL8Pe/v4n//M8VAIAxY87FT35yH+bN+xLmzv1Sn2V6552/\nY//+ffjf/12dwd+U9ISCMSGExN1w2ZiUerGZtmPHdnz22TasXr0GPM9j+fI7IQhhmEzJj2iOYyGK\nUQDAk08+jhtvvBWzZ8/Bjh3b8fvf/+6s56ypmYrTpx/Fjh3bIUkSRo0aDQB4/PFf4vPPP8NHH23E\nCy88hxdeeKXHMm3duhkvvrgGv/rV6m7lINlBC7gIISTP/H4fnE4neJ7HsWNHsXv3LgCxo2Z7vt6P\nkpISALEV1L1ZsOAreOih+7Fo0dXK8zU2nsb550/Hd7+7HH6/H8FgAAUFBfD7/crPHTiwD7/4xSN4\n9NFVcLncmfo1SR+ouUMIIXl2wQWz8be//QW33HIDRo6sQk1NbItRbyujly27A/ff/yMUFrowbdoM\nnD59qsfrrrjiSjzzzG9w+eVXAABEUcRPf7oiHnhlXH/9UtjtDlx88Tzcf/+PsGnTRnz/+/fguedW\nIxQKYsWKH0GWZQwdWoFHHnkiK787iaFTmwg5g5ZPhCGkq3/8Yz02bfoQ99//UL6LklFarqMDPs+Y\nEEKI9vz3fz+OTz/9BL/4Be0R1gLqGRNyBi23ugkxAi3XUTrPmBBCCFEpCsaEEEJInlEwJoQQQvKM\ngjEhhBCSZxSMCSHEAHbs2I4f/vDuPq85ePAAPvlkU0ae6+2338KTTz6WVhkH4tlnf4vt27em9TOP\nPvozHDt2FAAQjUbx2GM/w403Xotbbrke//znP7pd+8EHGzB37kzs378PQOxv9J3vLMM3vrEEt99+\nEzZseC8jvwdtbSKEEIPo73TFQ4cOYN++PbjooosH/Vyxa9I/zjFd3/rWnWn/zI9+dJ9y+/nnn0Vx\n8RC8/PJrAACPp1N5LBAI4M9/fgWTJtUo99lsNqxY8VNUVg5HS0sLvvWtW3DhhRfBbncM4regYEwI\nIaqwZs0zePfdt1FUVIzS0jKMGzcBDocDb7zxGqLRKCorR2DFip/CYrGgo6MDv/jFz9HY2AgA+Ld/\n+w/U1Ew96zk//fRjPPXUKlittm6P7927G7/85RMQBAEWiwX33vsgKiqG4ZlnfgNBELBz5xe45ZZv\noqKi4qzrRowY2e01enquxDWNjaexfPmdaGlpxhVXXIlvfvOOs8r43HOrcepUAxoa6tHU1Ii77vo+\ndu3aiS1bPkFpaRkeffRJcByHNWuewaZNH0IQwpgxYzqWL78HAPDznz+Eiy+ei0suuQzXX381Fi5c\nhE2bPoQoinj44f/CyJFVZ73m8uV34q677sa4ceOxbt0bePnlvyiPFRa6lNvPPPNr3HzzbXjppReU\n+4YPH6HcLikpQVFRMTo6OigYE0JIprx26C3saNqZ0ec8v6wG1465qs9r9u3bg40b/4EXXngFgiBg\n2bJbMH78BFxyyWX46levAQD87ne/xltvvY7rrrsBv/zlL7Bkyc2oqZmKxsbT+MEPluMPf/hTt+cU\nBAGPPfYzPPXUb1FZORwPPPBj5bGqqtF4+ulnwLIstm3bgt/+9n+xcuVj+Jd/+Q7279+L738/FugC\ngUCP13XV23MBwN69e/Dii6+C53ncccc3MHv2XIwbN/6s37+hoR5PPfVbHD5chzvvvB2PPPIEvve9\nf8e9996jHAN53XVLcPvt/wIAePzxh/Hxxx9h9uw5Zz1XUVExnnvuD/jrX/+Ml19+ET/60f29/t19\nPh8AYPXqX2PHju0YPnwE7r77hygqKsKBA/vQ1NSEiy66uFsw7mrPnl3xhtLwXl8jVRSMCSEkz2pr\nv8CcOZfAZDLBZDLh4ovnAgAOHz6E3/3u1/D5vAgGg5g16yIAwLZtW3Ds2BHlIIlAIIBQKASr1ao8\n57FjRzFsWKUSKK644kq8+eZfAQA+nxcrVz6IkyePg2EYiKLYY7lSua6va2bOvABOZyzJxSWXXIba\n2s97DMYXXjgbLMuiunoMZBmYNetCAEB19RicOhXLu719+xa89NKLCIdD8Pt9GDZsZI/BeN68SwEA\n48aNx8aN/zjr8a5EMYrm5iZMmXIeli+/G6+88kf86lf/jfvu+wmeeupJ3HdfMo3omfmxWlpasHLl\ng1ix4qd9vkaqKBgTQkjctWOu6rcXmyuyDPzsZw/h0UefwDnnjMHbb7+FHTu2xx+TsXr182cdbfgf\n/7EcHR1tGDduIq699vpen/uZZ36D6dNn4Oc/fxynT5/C8uXfGfB1fV1z5pwxwwCvvfYnvPnmX8Ew\nDB5/PJaq02w2K9d3/Z1iwT0KQRCwatVjeO65P6CkpBSvvPI8AgGhxzLzfOy5WJZTGgZd/y5d54td\nLjesVhsuuSQWwC+99HKsW/cGgsEAjhypw/LldwKQ0draih//+Af4r/9ahXHjxsPv9+GHP/w+7rzz\nLkyYMKnXv3M6KBgTQkieTZkyFY8//ghuueV2RKNRfPzxh7j66msRDAZQXFyCaDSKd999G6WlZQBi\nPcdXX30ZN910K4DYCt9zzx2LVaueUp5TEAScPn0KDQ31GDasEuvXv6M85vP5UFISe651695Q7j/z\nKEW/v+fruurtuYDYmcherxc8b8bGjR/g3nsfxLhx4/tsKPSUoVkQBDBMbD43EAjgnXfewdy5l/b6\nHGfq+nc508UXz8Vnn23DtGkzsG3bFowadQ4KCux46631yjXLl9+J5cvvxtix4xGNRnHvvffgyiuv\nUoJ4JlAwJoSQPBs/fiLmzJmH22+/EcXFQ1BdfS6cTgf+5V++gzvuuA1FRUWYOHEyAoFYoPz3f/8B\nVq16FLfddiMkScTUqdPw//7ff3Z7Tp7n8cMf3od77vl3WK02TJ16HurrAwCAm276Bn72swfx/PPP\ndhvqPf/8GfjDH9Zg2bKbccst38RNN92GlSsfOOu6rnp7LgCYMGES7rvvHjQ3N2HBgq/0OER9pp5W\nYDscDlx11TW49dYbMGRICWpqanr4SQBIffV24mW+853lWLnyAfzP/6yC2+3Gvfc+2GOZEm2E999/\nD7W1n8Pr9eDvf38DDMPg3nt/gjFjzk35tXssDx0UQUh3Wk5CT7QrGAzCZrMhHA7he9/7Nn70o/tw\n7rnj8l0sVRpsHb3ttqV49NEnMXRoRQZLlRo6QpEQQlTsscd+hqNHDyMSieDKK6+iQJwld9/9PYwZ\nc25eAnFfqGdMyBmoZ0yIumm5jtIRioQQQohKUTAmhBBC8oyCMSGEEJJnFIwJIYSQPKNgTAghhOQZ\nBWNCCCEkzygYE0IIIXlGwZgQQgjJMwrGhBBCSJ5RMCaEEELyjIIxIYQQkmcpBeONGzdi4cKFWLBg\nAVavXn3W41u2bMGMGTPw9a9/HV//+tfx9NNPZ7yghBBCiF71e2qTJEl4+OGHsWbNGpSVlWHx4sWY\nP38+qquru103Y8YM/OY3v0n5hetOdqDQwqVfYg2JREWcag1gZHnPicEJUas9R9sgRCWcN6Yk30XJ\nqoYWPw7Vd2LOlAqwPZyjS0iu9Nszrq2tRVVVFSorK2E2m7Fo0SJs2LBh0C/8g19uRGtnaNDPo1bH\nTnvx0zXb8JPfb8VnB5rzXRxCUiJERLz47n78Yu3neOovtfAFI/kuUtZ8VHsKP12zFWve3ofPD7bk\nuzjE4PoNxo2NjaioSJ77WF5ejqamprOu27FjB772ta/h29/+Ng4dOtTvC4uSjG37z34erYuKEt74\n6AhWvrAN9S1+AMAHO+rzXCpC+lff7MPDL2zDPz6rh9nEQpZjPWS9CUdEPLtuD577+14w8d7wp3sa\n81wqYnQZWcA1adIkfPDBB3j99ddx880343vf+17/L8wAW/fpKxg3tPjx8xe3428fHUGhnccPlpyH\nMZUu7D7ShjaPfkcBiLbJsowPdtTjp89vQ32zH5dOq8QPlpwHANh1RF/B+FSrHytf2IZNO0+jaqgT\nD31rFiqGFOCLQy0IhqP5Lh4xsH7njMvLy9HQ0KB839jYiLKysm7X2O125fYll1yChx56CB0dHXC7\n3b0+b82YEnxxsAWyiUNZUcFAyq4akiTjzY8O44V1eyBEJVw6fTi+/fUpcNjMEGTgqVc/x+eH27Dk\ny+PyXVSSot4OANcbb0DAU69+jk92noKzwIx7bpmBi2oqIEoynH/dhb3H2lFS4lB6kFr2wWcn8as/\nfY6QIOKqi0dj2dWTYDZxuHTGSLz0zj4cOu3DZTNG5LuYJEV6q6P9BuOamhocP34c9fX1KC0txbp1\n67Bq1apu17S0tKCkJLbQo7a2FgD6DMQAcPHUSnxxsAXvfnwEC2aNHGj5866lI4jn/r4X+453wGEz\n446vTsT0cWUI+kII+kIYX1kI3szinU+P4ktTaZGIFpSWOtHc7M13MbLuwIkOrH5zN9o8YYwb4cYd\nX52I4kKr8rtPqHJjy94mfLGvEZUl9n6eTb0iUREvrz+IDz5vgJXn8J2vTcKsCeXoaA8AAGqqYp9V\n720+qtwm6qblOtpbI6LfYMxxHFasWIFly5ZBlmUsXrwY1dXVWLt2LRiGwZIlS/DOO+/g5Zdfhslk\ngtVqxZNPPtlvgWbXVODXf/kCW/c1aTIYy7KMD2tPYe2GgwgJIs4/twTfWDgeLjvf7TqbxYSZ48uw\naedp7D/egQlVRXkqMSExoiThrY+P4Y1NRwAA18wdjasuGgWW7d5QnDS6GFv2NmH34VbNBuPG9gB+\n/dddON7kw4gyB/71mskoL+4+EldeXIDRFU7sOdIOT0BAYQHfy7MRkj39BmMAmDdvHubNm9ftvqVL\nlyq3b775Ztx8881pvbDLYcH4kUXYe6wdrZ0hDHFZ0/r5fOr0hbHm7X34oq4VNguHby2agNmTh/Y6\nlDd3yjBs2nkaH9U2UDAmedXmCWH1G7tx4GQnhhRa8O2rJ+Hc4T33BiePHgIgNm98hQYbzNv2NeH3\nb+9FMCxi3tRhuOnyc8Gbe95OecGEchw55cW2fU24bNrwHJeUkDxn4Jo5Pjb3rKVV1Vv3NWHFs1vw\nRV0rJlQV4afLLsDFNRV9zqmdO9yF8iIbtu1vRiBEi0RIfmzf34wHn9uCAyc7MX1cKX6ybFavgRgA\nipwWVJbasf9EB4SImMOSDk5UlPDSewfw9N92QZRk3HHVRNx+5fheAzEAzJxQDga0qprkT16D8bRx\npWA0sqraF4zgt2/sxq//tgtCRMTNXx6LHyw9L6UePcMwmDOlApGohC17qbKT3BIiIl54Zz9+9ded\niEQl3LZwHP71msmwW839/uzk0cWIRCUcONmRg5IOXktHEI/8YTvWbz+JYSV2rLhtJi6aPLTfnyty\nWjC+qgiHTnaipTOYg5IS0l1eg3FhAY/xI4twuMGj6gQgOw+34oFnN2PznkZUDyvET5bNwvzpw9Na\njDV7cgUYBviwtqH/iwnJkJPNPjz8/DZ8sKMew0vtWHH7TFxyXmXKq6OVoerD6t/itONgM37y+604\ncsqL2ZOHYsU3ZqQ1133BxHIAwJa96u8cEP1Jac44m2aOL8PeY+3Ytl99C7lCQhSvvn8IH3zeAI5l\ncO28c3DlhSPBsem3YYqcFtScMwS1da042eTD8DJHFkpMSExi7/Da9w8hEpUwf9pw3HBZNcym9FLQ\nnjvcBbOJxW4VJ/+IihJe23gY/7f5OMwmFt+8cjzmTOl76qgn08eV4sV39uPT3Y34yoVVWSotIT3L\n+6lNah2qPnCiAw8+twUffN4Q61HcNgNXzR41oECcMHfKMADARztPZaqYhJzFF4zgf1/biRffPQDe\nxGL5tTW4+YqxaQdiAODNHMaNcKO+2Y92bzgLpR2cNk8Ij720A/+3+TjKiwtw/zdmYO7UYQPaF223\nmjGleghONvtQ3+zLQmkJ6V3ee8aJoWq1rKqOREX89cMjeGfzcYABvnJhFb42ZzTMpsG3W6aOGQJn\ngRkf7zqNxV+qhonLe1uI6Mz+4+1Y/eYetHu77x0ejMmji7HrSBt2HWlVGpRqsPNwK3735h74ghHM\nmlCG2xaOh80yuI+0CyaWY8fBFmze24hrS2n0iuSOKqKBmlZV/8+fa/F/m4+j1G3Dj2+ejsVfqs5I\nIAYAE8fioklD4QtGKDE9ybg3Nh3BYy/vQKdPwNfnjsY9N54/6EAMAJPOic0b71ZJakxRkvDaxjo8\n+eoXCAlR3HrFWNx59aRBB2IAmDqmBBaew6e7GyHLcgZKS0jSyT5GXFQRjNUyVF1X34ndR9sxfqQb\nDy2bhTHDXRl/jblTYodu0FA1yaSGFj/+9uERFDkt+M+bp+GrF48+K4nHQA0bUoAipwW7j7RBkvIf\noH712i689fExlLqtuO/WGbh02vCMpeu0mDlMO7cELZ0hHG7wZOQ5CQGAww0ePPDsll4fV0UwVsuq\n6vXbTwIAvjp7FCx8ds5arix14Jxhhdh5uFWVc3BEm9q8sXpzydRhGW9EMgyDyaOL4Q9FcfR0flMQ\nnmzy4fNDLaiuLMSDt89E1dDM5ye+YGJsKxTtOSaZ1NgW6PNxVQRjIP9D1e3eMLbta0JlqR3js5wl\na86UCsgy8PEu6h2TzPD6Y+cOO+3ZSeU4aXQxAGD3kdasPH+qNsf36S+YORIFKeyTHoiJo4rgsJmx\ndV8TREnKymsQ4/EGhD4fV00wzvdQ9T92nIQoyfjyjBFZP6Fm1vhy8CYWH9aeonkpkhGeeEXPVl7l\niaOKwTD5PVJRlmV8ursRVp7DlOohWXsdE8di5vgyePwC9h3TRrITon6eQKTPx1UTjPM5VB2Jivhg\nRwPsVpOy8T+bCqwmTB9Xhqb2IA6coMpOBi/bwdhhM2N0RSHq6j15S+laV+9BqyeEaWNL+0xtmQmJ\nz4FP95zO6usQ4/BopWcM5G+o+tM9jfAFI7jkvEpYslzJE+ZNjS/kqqWhajJ4yWHq7AzdArEtTpIs\nY++x9qy9Rl82x+dwc9FgHjPcheJCCz470IxIVDt5uYl6+bTSMwbyM1QtyzLWbzsJlmFw2bTKnL3u\n2BFulLlt2Lq/CcEwHR5BBifR6nbasnf8XyI1Zj7mjUVJwtZ9jXAWmHNy8hnLMLhgQjmCYRG1dfmd\nJyf64AkI4PrY4aCqYJyPoeoDJzpwosmHaeNKM7InM1UMw+DiKRUQInR4BBk8byACE8fAZsneyM7o\nYU7YLCbsOtKW87UOe4+1wxOIYOb4spwly0kOVVP9JIPn8QtwFvQ+cqWqYAzkfqh6/bbYdqYvz8j9\nGaYXTx4KBjRUTQbPGxDgLOCzuviQY1lMrCpCS2cITe25Pdlo8+7cDVEnjChzYFiJHV8caqWjT8mg\neYORPtd0qC4Y53KouqUjiM8ONqNqqBNjKjOf4KM/xYVWTDqnGHUNHtS3+HP++kQ/PAEha4u3upp0\nTmyLUy5XVQsREdsPNGNIoRXVOaynDMPggglliIoSdhxsztnrEv0JR0SEBbHPrYeqC8a5HKp+f0c9\nZBm4fHrmMvika1481+8m6h2TAQoLIoSIlNXFWwmT4/uNdx3O3TxqbV0rQoKIWRPL0jq2NBNoqJpk\nQmKPsaaGqYHcDFWHBREbP29AoZ3HrAm5G/o609QxJXDYzPh41ylERUowQNKX7W1NXZW4bBhaXIB9\nxzty9n5NrKK+MJ4ZK5fKigpwzrBC7Dnahk5/31tTCOmNN76SWlPD1EBuhqo/3n0agXAUXzpvWMYO\nghgIs4nFhZPK4QlEaNUmGZBcBmMg1jsOR0QcPNmZ9dcKhKL4oq4VlSV2DC+1Z/31enLBhHLIMrBN\nZce8Eu3QbM8420PVse1MJ8CxDC49P3fbmXqjnHNMQ9VkAJQ9xn1U9EyarMwbZ7/x+NmBZkRFCbMm\nludtKmnmhDIwDCUAIQPnUeqoxnrGQHaHqvccbcep1gBmTSiDy2HJ+POna0SZA6OGOlFb14oOHx0e\nQdKTbHXnpmc8bkQRTByTkyMVN8cDYC5XUZ/J7bBgQlUR6uo9aO7I7Spyog/eYP+jV6oNxtkcqn5v\n2wkAwOUzRmT8uQdq7pQKSLKMT3ZR65ukRxmmzsECLgCw8BzOHe7G8UZfVudRO/0C9hxrxznDClHm\ntmXtdVJxQXxdCeUEIAORSoY81QbjbA1VN7YFUFvXiurKQoyuKMzY8w7WBRPLYTax2EiHR5A0JRaH\n5KpnDCRXVe/JYu94695GyHJ+e8UJ08eVwsQxtKqaDEgq6zpUG4yB7AxVb9ieSPKhnl4xABRYzZg+\nthSNbQEcqs/+whiiH7lewAUkj1TM5rzx5j2NYBhgVvxzIJ8KrGZMqS5BfbMfJ5t8+S4O0Zhkg1mD\nPWMg80PVwXAUH+08hSKnBdPGlmbkOTNp7pTY4REf0kIukgavv/+Vmpk2oswBl53H7iNtkLIwktPU\nEURdgwcTqopUsa4DoD3HZOA8AQG8ie3zICJVB+NMD1V/tPMUQoKIS8+vzFl+23SMqypCicuKrXub\nEBIo/R5JjScQgYXnsn6sYFcMw2DS6GJ4AhGcaMx8T3FLDk9oStXU6iGw8Bw272mkqSSSllTS1aov\nIp0hU0PVkixjw/aTMJtYXHLesEwULeNYhsGcmgqEIyK27qU9jSQ13oCAwhz2ihMS88a7j2Z+3njz\n3kaYOBbTx+Z/iDqBN3OYPrYUrZ4Q6uo9+S4O0QhZluENRPoduVJ9MM7UUHVtXSua2oO4cGJ5The6\npGt2TezwiA930lA16V+ioudyvjhh4qjspMY82eRDfbMfU6qHoMBqyuhzD9aFylA17XogqQkJIiJR\nCYV95KUGNBCMMzVUvUGF25l6UuKyYeKoIhw62YlTrXR4BOlbIByFKMl5aWAW2nlUlTtx8GRnRqdV\nPlXSX6pniDphwqgiOAvM2LqvCaJE6WtJ/1LJvgVoIBgDgx+qrm/xY/fRdowf6caIMkcmi5YVc6fG\nM3JR75j0w+PP7R7jM00+pxiiJGPf8Y6MPJ8sy9i8pxFWnsOU6iEZec5M4lgWM8eXwRuIYO/R9nwX\nh2hAqlsPNRGMBztUrZVeccL555bAbjXh452nqfVN+pSPPcZdKfPGhzMzb1xX70GrJ4RpY0tzuiAt\nHYkDK2hVNUlFqlsPNRGMBzNU7Q9F8PGu0yhxWXHemJIslTCzzCYOF04cik6/gJ11uTs3lmiP0jPO\nUzCurnTBwnMZ22+cmItV4xB1QnVlIYYUWrH9QDOEiJjv4hCVS2WPMaCRYAwMfKh64xcNEKISLps2\nHCybn0TzAzFH2XPckOeSEDVLdT4qW0wciwkji9DYHhx03mZRkrB1XxOcBWZMGFWUoRJmHsMwuGBi\nOcKCSCetkX6lmjteM8F4IEPVoiTh/e0nwZtZzJtakcXSZV7VUCdGljtQW9dK56iSXimt7n5WamZT\nIhvXYA+O2Hu0Hd5ABDPHl4Fj1f3RdCElACEpSpzY1N+6DnW/47sYyFD15wdb0OoJ4+LJFSiw5qfn\nMBhzpww+neyiAAAgAElEQVSDKNHhEaR3+UiFeabkkYqDC8abVZjoozfDyxyoLLGjtq4FgVAk38Uh\nKubV05xxQrpD1e9ti+WhvnzG8KyVKZsumFgOE8fgw9oGyvhDeuSJ94zzkfQjobyoAKVuK/Yea0NU\nHNiCQyEiYvuBZgwptKK60pXhEmbHBRPLERVlbD/QnO+iEBXT1damhHSGqo83enHgRAcmjy5GxRB7\nDkqXeQ6bGdPGluJUawCHGyjjDzlbIi+1I4/BGAAmjx6CYFgc8Pu0tq4VIUHEBRPLwfaRMlBNEj34\nzTRUTfrgCURg5TmYTX3vDtBUME5nqHq9xnvFCXOnxPYc0+ERpCeegACHzZz3OdbJowc3VK2lIeqE\nUrcN1ZWF2HusHR2+cL6LQ1TKExBSmkbSVDAGUhuq9gQEfLqnEeXFBZh8jvoSB6RjQlURhhRasGVv\nI8ICbaMg3aWS8zYXxlcVgWOZAS3iCoSi+KKuFZUldgwv1dYo1gUTyiHLoFzypEeyLMOXYh3VXDBO\nZaj6nzvqERUlXD59uGaGvHrDsgwurqlASBAzeq4z0T5RkuALRlSRa91mMaF6WCGOnvLAF0xvQdNn\nB5oRFSXMmlje56k2ajRzQjkYJnawBSFnSiddreaCcX9D1VFRwvs76mGzcJg9eWgeSph5F9fQOcfk\nbL5gLB90PhdvdTXpnCGQAexJ8xSnzfFEH1oaok5w2XlMHFWMww0eNLUH8l0cojLppKvVXDAG+h6q\n3ra/CZ0+AXNqhsFmUdeJLwNV6rZhQlURDpzoQGMbVXgSk1i8lc89xl0p88ZppMbs9IWx51g7qocV\nosxty1bRsuqCCbSQi/QsnXS1KQXjjRs3YuHChViwYAFWr17d63W1tbWYNGkS3n333RSLOjB9DVWv\n33YSDID50yuzWoZcmxvPyEWHR5AENewx7qpqqBMOmxm7jrSmvBVv674myDIwS4O94oRpY0th4lh8\nuqeRtiCSblLNvgWkEIwlScLDDz+MZ599Fm+99RbWrVuHurq6Hq974oknMGfOnAEUOT29DVXXNXTi\ncIMHU8eUoKyoIOvlyKXzx5YCAA6d7MxzSYhaJIOxOoapWYbBpNHF6PAJqG9J7fjPzXsawTDArPho\nlxYVWE2YOmYITrUGcKLJl+/iEBVJJw9Av8G4trYWVVVVqKyshNlsxqJFi7Bhw4azrnvxxRexYMEC\nFBcXD6DI6etpqHqDTrYz9cRi5uCwmSk1JlF4/fk9saknk9NIjdnUEURdgwcTqorgcliyXbSsoqFq\n0hNlKikTPePGxkZUVCTzOpeXl6Opqemsa9avX4+bbrop3bIO2JlD1e3eMLbua0JliR0TqtSbZH4w\n3A4enX7az0hiPHk+JKInE0elvt94iwb3FvdmSvUQ2CwctuxthERD1SQu1RObACAjK5x+/vOf4557\n7lG+T3XepLTUOeDXLAUwZUwJvjjYAtnEYcv+ZoiSjGu+NAZlZYUDfl41Ky0qwMlmP5wuG6y8Phan\nqdVg3pu5Eo1Xs6rhRaopb2mpE6MqCnHwRAcK3QWw9HEm8bYDzTCbWCyYfQ7sNvU0KAZq9pRh2LD1\nBFp8EUzSeH4DLVDLe74v4Xh62NEji1FcaO3z2n4/0cvLy9HQkDzGr7GxEWVl3ed3du3ahbvvvhuy\nLKO9vR0bN26EyWTC/Pnz+3zu5mZvfy/fp6nnDMEXB1uwbmMd3tt2AnarCZOr3IN+XrWy8bEPtrpj\nbZpdeaoFpaVOTbyHmlpj87LRcERV5R0/wo2jpzz4+LMTvSbdOdnkw/HTXkwbW4qAL4SAL71zytVo\n6jnF2LD1BDZsPoYyp3qmDvRIK3W0Jb7dLRwIozkc6yX31ojod5i6pqYGx48fR319PQRBwLp1684K\nshs2bMCGDRvw/vvvY+HChXjwwQf7DcSZkBiqfmPTUXgDEcw7b1ifLXGtczliFbyTUu8RxIapOZZB\ngVVdoySTUjjFKXH04IU6GKJOGF0RG5Eb7LnORD+8gQjsVhNMXP8bl/qtxRzHYcWKFVi2bBlkWcbi\nxYtRXV2NtWvXgmEYLFmyJCOFHojEquq9x9rBMgwuO19/C7e6cttji1w6fbSIi8QWcDkKzKrLMjd2\nuAu8ie01GMuyjM17GmHlOUyp1s9wboEl9qFLeapJgicgpLzAMqUm9bx58zBv3rxu9y1durTHax95\n5JGUXjhTZo4vw95j7Zg2tgRDXH2PyWud0jOmFdUEsYpeqsLpCrOJw7iRRdh5uBVtntBZc2V19R60\nekKYPXkoeB2NZDEME19kSfWTAJIUy0s9tDi1bbaazMDV1ezJQ7HooirccOmYfBcl61zxTEvU8iZC\nRERIEFWzx/hMk/rY4vRpPP2lnoaoE9wOCzp9Aq2oJvCFIpCRelIezQdj3szhukuqUaLCHkKmueN7\nManlTdJJs5cPvR2pKEoStu5rgrPAjAmj9LcF0eXgIcmy8u9DjCvddLWaD8ZGUmhPLOCiYGx03mDq\nyQTyoWJIAYoLLdhztA2SlOwl7j3aDm8ggpnjy/J+BnM2JNd10OiV0SWybzlT3Lanv9qgYzaLCRYz\nRxWdwBPPvpXKaTD5wDAMJo8uhj8UxZHTHuX+T3WU6KMniXUdHdRgNrxEXupC6hnrk4sWiBCkl4A+\nXyaPjq2U3h0/xUmIiPjsQDOGFFpRXenKZ9GyRplKogaz4aWTfQugYKw5LjsPT0DoNvRHjEdtJzb1\nZMKoIjAMsCt+vnFtXStCgogLJparbjtWprgdtMiSxChnGRtlAZfRuBwWyHLyw5gYk3JIhEqHqQHA\nbjXjnGGFOFzvQSAU1f0QNQDlwIsOGr0yPG+Qesa65qZFXARdD4lQb88YACaNKoYky9h+oAm1da2o\nLLFjeKk938XKGreD6ieJodXUOpdM/EHDYEbmTeOc1HxK5Kb+ywd1iIoSZk0sB6PTIWoAcNjM4FiG\nhqkJPAEBDAM4rNQz1iVXfOsErdY0Nk9AAG9iVZ+LfXSFEwUWk7LNQ89D1EBsFbnLwdMCLgJvIAKH\nzQyWTa3xScFYY9yUEpMgtpraWcCrvpfJsSwmxpN7VA8rNMRpYy67BZ1+IeWjZIk+eQNCWgssKRhr\nTDLxB7W8jUqWZXj8EdXuMT7T1DElAICLJg/Nc0lyw+3gERVl+EPRfBeF5ElUlOAPRVNevAWkeFAE\nUY/kPkbqGRtVSBARFSXVL95KuGjyUJS4rDh3hDvfRcmJRB3t8IbhSDH7EtEXXzD9dLXUM9aYxJF5\nNExtXFrYY9wVyzAYN7JIt3uLz6Rk4aJFloaV7h5jgIKx5rAMg0K7mVZrGpiyx1jlK6mNikaviJJ9\nK42pJArGGuRy0AIRI9NCKkwjoyxcZCB1lIKxBrntPCJRCcGwmO+ikDxQhqk1soDLaGj7IfEMIA8A\nBWMNosQfxpas6NQzVqNkFi6qn0ZFPWODoJa3sSlp9igYq5KzgAfLMJSf2sCSwZh6xrpGPWNj86R5\nTirJLZaNL7L0Uv00quR549Qz1rVEz5hWaxpTYqUm7WFVL1pkaWzegACOZVBgST2VBwVjDaKTYYzN\nExBgs5hgNlH1VavkIkvKwmVE3kAEjgJzWulqqTZrkMtOw9RG5g1EVH9ak9G5nbHRq3ZqMBuSJ828\n1AAFY01SMvxQRTccSZZjh0TQfLGquSiHvGFFoiJCgph2g5mCsQaZTRzsVhOlxDQgfzACWaZtTWpH\nWbiMS8m+RT1jYyi005mpRjSQZAIk95TDImgqyXA8A8yQR8FYo9wOC/yhKCJRKd9FITlEe4y1QZlK\n8lLP2GiS25pomNoQEpXdQ0PVhuIZQDIBknvKMDX1jA1noLnjKRhrVGKBCA2DGUtiPooSfqhbod0M\nBrTI0oiSc8bUMzYESvxhTHRikzZwLAunnaeTmwxooOeNUzDWKCXxBw1TGwot4NIOt52nxrIBDSQv\nNUDBWLNoH6MxKQu4aJha9VwOC8IRkbJwGQxtbTIYl4NObjIiT0AAwwAOK/WM1Y5Gr4zJ4xdgNrGw\n8lxaP0fBWKPctJrakDyBCJw2M1g29Zy3JD+UBjOd3mQo3kAEzjTzUgMUjDXLZjHBxLG0QMRgvH6B\nFm9pRKLBTDsejENOpKsdQB2lYKxRDMPA7eBpCMxAoqKEQDhKe4w1gnY8GE84IkKISgNKV0vBWMNc\nDh4evwCJzkw1BNpjrC1uJx11ajQD3WMMUDDWNJfdAlGS4QtG8l0UkgO0x1hb3PbEIksapjaKge4x\nBigYa1oiJSa1vI0hWdFpmFoLkkedUjA2Cm88L7UzzbzUAAVjTXMn9hrTAhFDSFZ06hlrgYlj4bCZ\naV2HgSijVzbqGRuKi85MNRTPICo6yQ+3g1JiGokyekU9Y2NRDougym4Ig6noJD9cDguCYRHhiJjv\nopAcGGj2LSDFYLxx40YsXLgQCxYswOrVq896fMOGDbj66qtxzTXX4Nprr8Unn3ySdkFI+lyU4cdQ\nlNXUtIBLM9yUttZQBrOAy9TfBZIk4eGHH8aaNWtQVlaGxYsXY/78+aiurlaumT17NubPnw8A2L9/\nP+666y689957aReGpIf2MRqLkpeagrFmuJ3JtLVlRQV5Lg3JtqxubaqtrUVVVRUqKythNpuxaNEi\nbNiwods1NptNuR0IBFBUVJR2QUj6EmemUqvbGDyBCEwcA5slvZy3JH9oKslYvH4BFp4Db06/jvbb\nM25sbERFRYXyfXl5OXbu3HnWdevXr8cTTzyBlpYWPPvss2kXhKSPY1k4C2i1plEk0uylm/OW5I+b\nFlkaiicgDHjrYb/BOFWXX345Lr/8cmzbtg333HMP3nnnnX5/prTUmamXN6whbhtOt/rpb5lhavx7\neoMRDC9zqLJspGdV8SAsSDL9u2WY2v6eshxLwFRd6R5Q2foNxuXl5WhoaFC+b2xsRFlZWa/Xz5gx\nA6Ioor29vd/h6uZmbxpFJT2xW00IhkWcqG+Hlc9Y28rQSkudqntvhgURYUGEzcyprmykD9HYKuqG\nJh/9u2WQGutoIBRBVJRh4/uuo70F6n7njGtqanD8+HHU19dDEASsW7dOWayVcPz4ceX27t27AYDm\njXPEZacV1UZAqTC1KXmmMc0Z651nEIu3gBR6xhzHYcWKFVi2bBlkWcbixYtRXV2NtWvXgmEYLFmy\nBO+88w5ef/11mM1m2Gw2PPnkkwMqDElf1zmpclqtqVse5ZAI2mOsJWYThwKLieaMDWCwDeaUxjXn\nzZuHefPmdbtv6dKlyu077rgDd9xxx4AKQAaHVmsaw2D2L5L8cjstVD8NwONP5AEYWIOZMnBpnJIS\nk4apdY32GGuXy87DH4oiEqUsXHqm9IwHmDuegrHGKXPGNAyma5QKU7vcdLqaISSHqalnbEjJik7D\nYHo2mJy3JL8So1cdNHqla55BpqulYKxxSkpMqui65hlkq5vkT2KRZYeXGsx6NtgFXBSMNc7Cc7Dy\nHDpoCEzXqGesXW460MUQBpOXGqBgrAsuh4X2MepcIuetZQA5b0l+0Y4HY/AEBBRYTDBxAwurFIx1\nwGXn4QtEEBWlfBeFZMlgct6S/Eqc3EQLuPTN6xcGvJIaoGCsC24HDxnJYRKiL7IswxuI0B5jjXLb\nE8coUs9YryRZhjcYGdSaDgrGOpBcxEWVXY8C4ShESab5Yo2idR365w9GIMuDS8pDwVgHXI7EnBRV\ndj3y+GkltdbRug59S25rop6xoSUTf1Bl1yOvkpeaesZaVeTg4aV1Hbrli29rclDP2NjclBJT1+jE\nJu1LJP7wUB3VJeoZEwCUElPvMlHRSX4ltzdRHdWjRCNrMKNXFIx1IDlnTMPUeqQcEkHD1JqVPOqU\n6qgeKaNXNuoZG5rdZgbHMjQEplN0fKL2uanBrGtK9i3qGRsbyzAotPM0BKZTNEytfcphEVRHdSkT\nDWYKxjrhdvDo9Ichy3K+i0IyLDFMbR/EEBjJr2R+auoZ65E3EAEDwEHD1MRltyAqygiEo/kuCskw\nbzACu3XgOW9J/rmpZ6xr3oAAu80MlmUG/BxUu3WCEn/ol8cv0B5jjbPyHHgzS3PGOpWJOkrBWCco\n8Yc+iZIEfzBCe4w1jmEYuO0W2n6oQ6IkwR+KDmolNUDBWDdclPhDl3zBKGTQ4i09cDl4eAICRImy\ncOmJLwMrqQEKxrrhpsQfukR7jPXD7bBAlgGPn05X05NM7XagYKwTya0TNEytJ54MJBMg6uCiFdW6\nlKl0tRSMdSIxZ0yJP/RF2b9IPWPNoxXV+pTcY0w9YwJKialXyolNtIBL8ygLlz5549MO1DMmAAAT\nx8JhM9MCLp1JDoHRMLXWKYssqWesK95gZkavKBjriMvOU0XXmcRiHxqm1j43bT/UJY/SM6ZhahLn\ncvAIhKMQImK+i0IyhM4y1g/KT61PtICLnMVlp73GeuMJCOBYBgVWU76LQgYpkdKU5oz1xRuIZKSO\nUjDWkeTWCQrGeuH1R+CwmcEyA895S9SBYZj4gS5UP/XEExAyUkcpGOsIzUnpjzco0BC1jrgcPDx+\nARKdrqYb3kBm6igFYx2hOSl9iURFBMMiCu20klov3A4LRElWUigSbYtEpYzVUQrGOqIcFkHDYLpA\ne4z1x22nTHl6kskFlhSMdUSZM6aKrgseWkmtO3TUqb4kGsyZyANAwVhH3HRyk64k9xjTMLVeUINZ\nX7xKKkzqGZMurDwH3sRS4g+doD3G+lNEB7roiieDGfIoGOsIwzBwOXh00KkwupDJik7UQVlkSaNX\nuqCMXlHPmJzJ5bDEtk5ItHVC62gBl/4kh6kpGOtBIi91Js4bp2CsMy47D1kGvEHaOqF1Xn/mKjpR\nB4fNDI5laM5YJ7wZyksNUDDWncTWCars2udResY0TK0XbGIqieqnLnhoARfpTSFtndANT0AAb2Jh\nMXP5LgrJIJfdgk6/AJmycGmeNxCBiWNh5QdfRykY64ySEpMWcWleIs0eQ3mpdcXt4BEVZfhD0XwX\nhQxSrI6aM1JHUwrGGzduxMKFC7FgwQKsXr36rMfffPNNXH311bj66qtx4403Yv/+/YMuGBkYOsBc\nH2RZhscfoZXUOpTIB9DhpQaz1nkCQsYWWPYbjCVJwsMPP4xnn30Wb731FtatW4e6urpu14wYMQJ/\n/OMf8cYbb+C73/0uVqxYkZHCkfRRSkx9CAkioqKEQlq8pTtKFi4avdK0sCBCiEhwZigpT7/BuLa2\nFlVVVaisrITZbMaiRYuwYcOGbtecd955cDqdyu3GxsaMFI6kz00ZfnTBS3uMdctNo1e6oNRRW456\nxo2NjaioqFC+Ly8vR1NTU6/X/+lPf8K8efMyUjiSvtgcIyUV0DoP7THWLbeyyJIazFqm1NEM9YxN\nGXmWuE8//RSvvfYaXnrppUw+LUkDyzIoLODhoVa3pil7jCkY647LTked6kEm81IDKQTj8vJyNDQ0\nKN83NjairKzsrOv27duHBx54AM888wxcLldKL15a6kyjqCRVQ9w21Df7UFLioJW4A5Tv96Zc1woA\nqBxamPeykMwyWWI9qVBEon/bQcj73+5IGwBgWLkzI2XpNxjX1NTg+PHjqK+vR2lpKdatW4dVq1Z1\nu6ahoQH/9m//hsceewwjR45M+cWbm73pl5j0y2E1ISyIOFHfAZslo4MfhlBa6sz7e7O+Mf76opj3\nspDMkiQZDAM0tvnp33aA1FVHpbTK0lvg7veTmuM4rFixAsuWLYMsy1i8eDGqq6uxdu1aMAyDJUuW\n4Omnn0ZnZyceeughyLIMk8mEP//5zykXjmRWYZcV1RSMtSkxTE1zxvrDsgxcdp62NmmcJ1FHM7Tj\nIaVP6nnz5p21KGvp0qXK7ZUrV2LlypUZKRAZvK4rqocWF+S5NGQgErnFaTW1PrkcFjS0+CHLMk0l\naVTiIBenLUdbm4j20AIR7fPQAi5dc9t5RKISgmHKwqVVmT5vnIKxDlHiD+3zBmJTDGYTVVE9Us41\npgazZnkCAixmDpYM5KUGKBjrUjKpAM1JaZUnEKHTmnSM9hprnzeQ2XS1FIx1yEUnN2maJMuxBPSU\nClO3KAuXtsmJOprBaSQKxjqUGKb2UO5bTfIHI5DlzC0MIepD+am1LRgWERXljI5eUTDWId7MwWYx\nUUpMjUqm2aOesV4lT26iOqpF3mDmF1hSMNYpt4OnITCN8mV4lSZRH2WYmnrGmuT1x7c1ZSgvNUDB\nWLdcdh6+YARRUcp3UUiakodE0DC1XhXazWBA6zq0ypPhvNQABWPdSmyd8NBQteZkOrMPUR+OZeEs\nMNOOB43KxhGnFIx1KrGIi1re2pPpZAJEndwOC9VPjcrGEacUjHUqsVqT5qS0h4apjcHlsCAcESkL\nlwZl44hTCsY65bbTPkatorOMjSHZYKY6qjWJ3PGZnEqiYKxTLsrwo1negAAGgIP2GetacnsT1VGt\nSazryGQdpWCsU8nEH9Tq1hpPIAJHgRksS6f56JmbEn9oVjZyx1Mw1ilKRK9d3oBA5xgbgIumkjTL\nm4Xc8RSMdcpuNcHEMbSAS2OiogR/KErnGBtA8txxCsZaEssdH8n4mg4KxjrFMAxcdp4Wh2iMl1Jh\nGoYyZ0zrOjQlEIpCkuWMN5gpGOuYy2FBp0+ALMv5LgpJkbLH2EbBWO9okaU2JepophvMFIx1zGXn\nIUoyfPFl+ET9Emn2MpnzlqiTiWPhsJlp9EpjPP7MZ98CKBjrmktJRk+VXSu8WcjsQ9TL7eBpkaXG\nJOoozRmTlLnttEBEayjhh7G4HBYEw1GEI2K+i0JS5M3CIREABWNdK6Q5Kc1JnmVMw9RGkGwwUx3V\nCo/SM6ZhapKiREpMSvyhHdk4mo2oF+UD0J5s1VEKxjqWXK1JFV0raJjaWNw0eqU5ypwxraYmqUqk\nxKTEH9rhCUTAsQxsFi7fRSE5kNhrTOs6tMOr5KU2ZfR5KRjrWKGdBwOq6FriDQixfzeG8lIbgYvy\nU2uOJyDAYTODYzMbPikY65iJY+EoMKOD5ow1I5ZmjxZvGYWLesaak606SsFY51x2Hh5qdWtCWBAR\njoi0eMtAEqupac5YG0RJgj+Y+bzUAAVj3YvtYxRpH6MGKKkwKRgbBm/mUGAxUc9YI3zBKGQg4yc2\nARSMdY/2MWoH7TE2JpeDp56xRigN5iwc5ELBWOcSiT8oJab6eahnbEhuhwX+UBSRKI1eqZ2y9dBG\nPWOSJjcdYK4Z3iwloCfqRucaa4cni0ecUjDWOTqmTTu8QTokwoiULFw0eqV62cpLDVAw1r1k4g+q\n6GqXSFuajVY3US9a16Ed2cpLDVAw1j3K8KMdydXUNExtJG4n5afWimzueKBgrHOU4Uc7PFk6J5Wo\nm4v2GmuGl+aMyUBZeRMsZg4eanWrntcvwMJzsJgpL7WR0OiVdngCAliGQYE1s3mpAQrGhuBy8LQ4\nRAM8ASErWyaIutEiS+3w+gU4Csxgs5A7noKxAbjsPLx+AaIk5bsopBeyLMMbiNDiLQOy8iZYeY7m\njDXAG4hkJfsWQMHYEFwOC2Qk5zuI+gTDUYiSTNuaDMrlsNBRpyoXFSUEwtGsremgYGwAya0T1PJW\nq2xumSDq57bz8AYiiIo0eqVW3izXUQrGBkBzUupHe4yNLbG9yUNrO1Qrmwk/gBSD8caNG7Fw4UIs\nWLAAq1evPuvxw4cPY+nSpaipqcHvf//7jBeSDI4rkRKTKrpq0YlNxpbc3kR1VK08Wc4D0O/6bEmS\n8PDDD2PNmjUoKyvD4sWLMX/+fFRXVyvXuN1u3H///Vi/fn1WCkkGJ5n7lnrGakXD1MaW3N5EdVSt\nvP54Hc3S6FW/PePa2lpUVVWhsrISZrMZixYtwoYNG7pdU1xcjMmTJ8NkyvzeKzJ4iaFP2t6kXolD\nImgBlzG5aSpJ9fI+TN3Y2IiKigrl+/LycjQ1NWWlMCQ7Eq1uSvyhXtkeAiPqphwWQXVUtZQTm2g1\nNRmoxCZ1SompXtlMs0fUT5lKojqqWnmfMy4vL0dDQ4PyfWNjI8rKyjLy4qWlzow8D+mf22mBNxil\nv3mKcv13CkViW1pGjyyGiaM2stEUOKwAgIAgUR1NUa7/TkJUBhCro/YsZMrrNxjX1NTg+PHjqK+v\nR2lpKdatW4dVq1b1er0syym/eHOzN+VryeA4C8xoaPGjqckDJgup3PSktNSZ8/dma2cQdqsJ7W3+\nnL4uUQdZlsGbWTS1+elzMQX5qKMtHQGYOAZ+bxABX2jAz9NbI6LfYMxxHFasWIFly5ZBlmUsXrwY\n1dXVWLt2LRiGwZIlS9DS0oLrrrsOfr8fLMvihRdewLp162C32wdcYJJZbjuPY6e9CIajKLDSvKTa\nePwCDVEbGMMwcNstlJhHxTx+Ac4CPmudmZSWP8+bNw/z5s3rdt/SpUuV2yUlJfjnP/+Z2ZKRjEom\n/hAoGKuMKEnwByMYNqQg30UheeRy8DhU3wlRksCxNFWhNt5gBOVFtqw9P/2LGwQl/lAvXzAKGdnb\nv0i0weWwQJYBj59yyKtNOCIiLIhZ3XpIwdggKPGHemV7/yLRBlpRrV7eHGw9pGBsEIV22seoVomE\nH7TH2NjctNdYtZKHRFDPmAxSotVNiejVx0N7jAmS+alp9Ep9lNGrLNZRCsYGoSzgoiEw1fHQMDVB\n8uQm6hmrT2Ie35mF/cUJFIwNwkVnGqtWLuajiPq5qWesWkodpZ4xGSyziYPdaqLV1CqktLqpZ2xo\nlJ9avbxZzksNUDA2FJfDQq1uFcrFfBRRP7vVBBPH0mpqFcrFQS4UjA3EZefhD0URiUr5LgrpwhuI\ngGUYFFjpCFIjYxgGbgdPPWMVysW6DgrGBuKifYyq5AkIcMZP1iLG5nLw8PgFSGnk+CfZ5w1EwJtZ\nWHgua69BwdhA3IksXNTyVhVvQKD5YgIgVkdFSYYvQFm41MQbEOC0ZbeOUjA2kMScJC3iUo9IVEQw\nLMmswi0AABZwSURBVKLQTiupSdfEHzR6pRayLMPjj2S9jlIwNhBKiak+ucjsQ7Sj64EuRB1Cgoio\nKGW9jlIwNpDEXmOq6OqRi1WaRDtc1GBWnVzlAaBgbCCJfYw0TK0eiT3GlH2LAF2GqamOqoYnB3uM\nAQrGhkLD1OpDe4xJVzRnrD7JnjEFY5IhNkssqQC1utUjOWdMw9Sk6zA11VG1yFUdpWBsIImkAnRy\nk3rQIRGkK4fNDI5laPRKRRKfl9kevaJgbDCUVEBdlLOMaZiaAGAZBi4HT8PUKpKLvNQABWPDcVFS\nAVVJLA7J5tFsRFtcdgs6/QJkajCrQiZWU0elKA6012HD8Y29XkPJcA0mmRJToEVDKuAJCDCbWFiz\nmGaPaIvbwePIKRn+UBQOaqTl3UC2H8qyjOZgC/a0HcC+tgM40F6HsBh7nqXTF/X4MxSMDabrmakj\nyhx5Lg3xBQQUFpjBUF5qEufqsqKagnH+eQMR2CwczKa+G8zBaBD72+uwt3U/9rYdRGuoTXmsvKAU\n44vHYmLx2F5/noKxwdCZqeohyzI8gQgqS+z5LgpREbeShSuM4aXUYO5LWBTgE/zwRXzwRQLwCT74\nowFYOSuKrW4UW90osrhh5gbeqPH0kpdakiUc957E3tYD2NN2AEc9xyHJsRPxbCYrziutwYTiczGh\neCyG2Ir7fR0KxioTiobRGe5ER9iDjnBn/H9P7D7BA1ESwTIsOIaLf2XBsbHbyvdM9+9Zlot9ZVi0\nCmGYhrdia0cLfEdL4DAXwGG2w262w8nHvtrNBWAZfSwnkGUZESkCE2vK+e8kyRICkSA8ghe+iA9e\nwQeP4INP8MEb8aEj5AUz5jQ8Nh6/3LEDLBgwDAMGDFgmcZtV7mMY5oxrWOX+xFcTy8HEmmBmTDCx\nZphYDuYzvsbuN8HMmnr8GrtthjkPfzOS3Gus5u1NkixBluVu771MPGcgGowH1/j/QjzIRnzd7g+K\nQXSGvIhIqa19cZodKLYWoSgeoLvdthTBbi7o8XeQ5dj6mpIKKwCgPdSBvW0HsbdtP/a3HYI/GgAA\nMGAwqnAEJhSPxYQhY1HlHAGOTW/qKW/BeHvDTnR0+M+6P5V/VAY9XxP72dijDBjE/ku8Wbr+ZPLN\n09NjiQBnYjhwrAkmlgPHcPGvse/T/ZCSZAlewR8PtF0C7BlBNySGen0OExP7IBVlEZIsQZTFtMqQ\nYB4GHIoexqHDPT/OgEGByQZHPDg7zPZY0OYdsMeDt8Nsh4O3K4HcyllyPtQaFgV0hjvRGfagM+xB\nh+BRbnd2uS3EKyzP8bByltj/JgssXb9yFlhMsa9D2lwQQzjr8cTtiBSBV/Al/4/4evzeF/ErLeXe\ncIVAEMCB9sYc/MXSxzIseCV4m8FzZiVQm1kzzFyX22d8z7NmmLhkYO+p3srofZFSX8uXWDAwc+Z4\nnTizIZG8bWJNMDEmpWGRyns01oCLQhAFCJIQ+ypGEBYFCFIk/n3isfj3UuzxiCjEG0Wx1zXFPz+6\nlqXbfQzXvawsB9HsA2Px45SnBR3hAsiyDEmWIUOOBUHIkGWpy32x7xOPd7tWliEh/lWWlN8rIkXi\nv0skdlsUuvzOEUTESPzrmd9HIEixa8/UNSizyu1EYxHxhmXXz+T4Jy7DQJZl+COBPt8PCWbWhEKr\nE0PtZWd8FjngMBfAbrYjGA2hPdSOtlAH2sIdaA+1o97XgGPeEz0+J8+aUWQtUnrSxfHbNtYB2dmE\n4JAjeHjzP3Han6ynRRY3ppZOxoQhYzGuaAzs5oJ+y94XRs7Tkr0bXvluPl42Yxgw3YJzLICb4gE8\nGcgBoDPsgUfw9hk8C0w2uC2u+P+FcMW/Ju9z9dh6iwVlKV4JRYhS4ntR+Zq4RpRFdPrC+OVfPse4\nKhe+Mns4/JEAfBE//IIf3ogffqVFGvuaagXhGA5WkwU8y8NissDC8rBwPCwmPnYfZ4HFxMfvt4BP\n3DZZYOHi13S5LyqJZwXVjjO+76vhwoBBIe+Ay1IIu9mOqBRFSAwjHA0jKIYQjoaVIJ1pVs6KQt4B\nB+9QvjrNZ99ubpWw6qXdWDBrJK6/tDr+wdn9g1Y+42vsfqnbB7Qcv0+SZYiyiKgURUSKxr9GlNtR\n5b7uX6NSFBE5iqgYRVTu8rgY+9nYc8Rvi8nbA20M5kusziaDXmKkgAWLiJQMthExktJ73gg4hlMa\nYHy8scWzPMycCSzDAYn3YLf3Krrdh/hXKf43PfM9LUMGA7bHhn732w44eDssHI/SUieam71p/S6J\nDlF7OB6kQ+1oD3WgLRQL1m3hDvgjgV5/3syacW7ROZhYPA4TiseivKB0QB2Q0lJnj/fnrWd823mL\n4fV1/zBNpQL01naQIQPKmyB+jxz7KkGOv2m6vEr8e7nLGyTxaKLXGZVEiJKIqByNfxW7fI2e9b0o\nSwiLYUSjyftlWUYh78RIZ2UPATYZdHluYCubE8PRqRLtEmTfMYidLkwaMr7f6yVZQjAa6jJcFA/Y\nXYaMYgE8oAQ7r+BFiygg2kPrORPs5gIMsRXBxRfCZYn/H7/tjn/vNDv6HSaS4v9eoWg49jV+22Jn\n0dTWofw+ofhjsdshmFkeTt4OJ++Ak3fCaY7dLuSdcJjtKc9PNdQ3A2Dgslti/4YMoKU11aIkKsE6\nKkWVQNZzAO/9vdDnx1kvH3aSLHZpTIiISpH410R5xG4NizMbH4nvg5EQJEjgWR52cwGKOB48awbP\n8bH/WR48Z441GLs9ZoaF5WHm+PhjZuVaWZYRPaNRFI1/XkSl+GdJogGklD9xTRSBsIBNu+sxxMVj\nzPBCpUfPxqcs2K7TFF3u73YNGDBdHk9Me3Qd3Uj8Pon7eI5P3o7fn+5Qq5qxDAuXxQmXxYlRhSN7\nvCYUDaMjnAjQHTjYdAof7z6NmcMn4PZ5F8PMZi9k5i0YLxo3P+2WDRk8jmXhtPMpp8RkmViL1W4u\nQHmaryVKIgRJQFgUEI6GEZYEhKMCwmKsV5q4T4jf1/VxluGUwKoEWb4QhZbCjFUIlmFhM9lgM9m6\n3V9a6kSzJfvvTa2f2MSxsVEgKyz5LoquSJKMf677BxyVLixbOD3fxTEUq8mCoaZyDLXHPu2sviZs\nPLkLo8adk9VADNACLkNy2Xk0tQez/jocy8HGxoMdfV6fRcnsQ/u9SRcsy6DQzlNKTBVQkvLYs99g\npqWSBuRy8AhHRISE7Awjk/6davVjx8FmANrtGZPscTss6PBRFq58U9LV5iB3PPWMDchtT26dsBbT\nWyCXfMEIXv/oCD7YUQ9RklFzzhCMLOt5QQcxLredx7HTXgTDURRYqbGWL7nKSw1QMDYkV5ekAuXF\ng1uOT1ITFSW8/1k93tx0BP5QFGVFNtxw6Ricf24JZd8iZ+manIeCce61eUL4ZPdpbDvQBCA3o1cU\njA3IZU/mpybZJcsyPj/UglffP4TG9iAKLCYsvWwMLps+HCaOZolIz7pm4RpGGdpyIhwR8dmBZny8\n8xT2HG2HDMBsYvGl84Ypn5nZRMHYgLSQ4UcPjjd68cr7h7D3WDtYhsH8acNx9ZxROZl/ItpGdTQ3\nZFnGwZOd2LTzFLbua0JIiO2dH1PpwsU1QzFzfDkKrLkJkxSMDSixerfDT6s1s6HDF8ZfNx7GR7Wn\nIAOYUj0EN1w6hno4JGXKVBLV0axo6Qzi412n8fHO02jqiO0sKS604PIZwzF7cgWG5mH6joKxASWG\nwDzU6s4oISLina0n8PdPjiEcEVFZaseSy8Zg8ugh+S4a0RjqGWdeSIhi+/5mbNp5CvuOdwAAeDOL\niyYNxcU1QzG+qghsHtdvUDA2IFd8NXWqiT9I32RZxua9jfjLB3Vo9YThLDBjyWVjMHdqBTiW5oVJ\n+txdjlEkAyfJMg4c78CmnaewbX8zwpHYMPS4EW7MrhmKGePKYLOoIwyqoxQkpyw8ByvPUas7Aw7V\nd+KVDQdR1+CBiWNw5QUjseiiUTmbZyL6VGg3gwEddTpQTe2B2DD0rtNo6YylXS5xWbGwZiQumjwU\nZf+/vXuPiavKAzj+nRkeoS1gyxRo0Ja0Whc39bVdu203QsAWjCUFebTRaCKm9q8m0NEm0JDGF2JJ\ntImaCEnrHzVqmlUwtmssYmCyrUhZNdToP9u0C2J5FMqjxe487tk/hrmdgWGGgnQe/D4JmXvvnAP3\ncO5vzj1nZs65Iy7Ab7j95BVjkUpcFsuovB81Z1dGf+cfrRfo+MX11YeNf0qmJGsdK0MwyEX4MRmN\nxC+Jllm4ptCU4trvdib6xvhvz1XGJuyMXbcxNmFj7LqN8Qk7w2M36B64Brg6Hn/fsIqtG1K55647\ngjoMHYg0xotU4tIY+ocn+E/v6Exz8c+LviClj9/tPuZrST33c0qBU1NomnKtRKUp/UfTtzXXtlPh\nVK7Hac9pCk1BlNGAyWQg2mTEZDISZTIQZTJO/nhvD03YuTZ2Qz/umU9TiuZzPZw+14PdoZGeGs/u\nnHtYf9cdf/w/USxqictiuTx0nWP//MW1CI57cRv9cXJb+T4GTC6veHMhHZPx5jXtvt5NJiNRRo9t\n93NGg89YcR8zudfcNnBzAQuDazpPfalEA1OOe6Rn8tFowKlpjE/YGZ+wMTrZqHo2smPX7YxN2Bif\nsBFoUrIok4GMNcvZuiGVv6xPJjYmPBa7kMZ4kVoR73pPqub4v4N8JuFpeXwsRZlr+dufU0P6bluE\nr9Upy+gZuMa/ui4H+1SCLi42ioQl0aQsTyRhSQzJ5qVEG1ydivglMSQsnfxZEkNcrCksJ9IJ2nrG\ngKzaFESXh65z5nzfgsx9q6ZseC6N6evPKR/pDLh6siaj645b3zYaMBmNmIye+9OfM072hE2Td+Ka\nprA7FU6nhkNTOBwaDs9tTcPhdG3HxEYxfu1/2J2aK71TudJOPq5LS2T7X+8iNjo87rhFeNI0xeDI\n7zDZkzSA3tMEvHqZ7uf0Y57PT/ZQAX10yXU9azg9r23NtT/1utfTaDf37U4NpSmvnrdSrnNWyqNH\nrjz3XaNUnr15dzqDwTX/c/ySaL1RTVgaM9nYRhMd5R1rc1nPOFTMtJ7xrBpjq9VKTU0NSimKiop4\n4YUXpqV57bXXsFqtxMXFUVtbS0ZGRsCTCtd/pohs4RzoQiwG4RyjMzXGAb93oWkar776KkePHuXk\nyZOcOnWKCxcueKVpa2uju7ub06dP88orr3Do0KE/5qyFEEKIRSBgY9zV1cWaNWtIS0sjOjqaJ554\ngpaWFq80LS0tFBQUAPDAAw8wPj7OlStXFuaMhRBCiAgTsDHu7+9n1apV+n5KSgoDAwNeaQYGBkhN\nTfVK09/f/weephBCCBG5ZHogIYQQIsgCfrUpJSWF3377Td/v7+8nOTnZK01ycjJ9fX36fl9fHykp\nKQH/+ExvZAsRbHJtChHaIi1GA/aMN2zYQHd3N729vdhsNk6dOkVOTo5XmpycHJqamgD48ccfSUhI\nwGw2L8wZCyGEEBEmYM/YZDJRXV1NWVkZSimKi4tZt24dn3zyCQaDgV27dpGZmUlbWxvbtm0jLi6O\nN95443acuxBCCBERgjrphxBCCCFkOkwvVVVVtLa2kpSUxBdffKEfP378OB999BFRUVFkZmby4osv\neuWz2Ww8/fTT2O127HY7OTk57N+/H4DR0VEqKiro7e3lzjvv5MiRI8THB/+9jrmW1U3TNIqKikhJ\nSeH9998H4N133+XEiRMkJbnW762oqODRRx9d+MIIn/VZUVHBpUuXANd1mJiYSGNj46zyQmjW51zL\nGSkxOts6BYnRsKOE7ty5c+rnn39WO3bs0I+1t7er5557TtntdqWUUkNDQz7zTkxMKKWUcjgcqqSk\nRHV2diqllDp8+LBqaGhQSilVX1+v6urqFrIIszafsiql1AcffKAsFovau3evfuydd95Rx44dW7iT\nFjPyVZ+eamtr1XvvvXdLeUOxPudTzkiIUU/+yqqUxGi4ka82edi4cSMJCQlexz7++GP27NlDVJRr\nEGHFihU+88bFuZbOs9lsaJpGYmIi4JoQpbCwEIDCwkK+/vrrhTr9WzKfsvb19dHW1kZJScm055S8\n6xEUvurT05dffsmOHTtuOW+o1ed8yhkJMerJX1klRsOPNMYBXLp0ic7OTkpLS3nmmWc4f/484Jro\nZO/evXo6TdMoKChg69atPPLII9x9990ADA8P658sX7lyJcPDw7e/ELM027LW1NRw4MABnyujfPjh\nh+zcuZODBw8yPh78uWOrqqrYsmUL+fn5+rGKigoKCwspLCwkOztbfyGeymq1kpeXR25uLg0NDfrx\n0dFRysrKyM3N5fnnnw+JcvrT2dmJ2Wxm9erVwPT69CfU6tOfQOWMhBh1C1TWcI/Rrq4uiouLKSgo\noLi4WH8tmipSYhSkMQ7I6XQyOjrKiRMneOmllygvLwdc362ur6/X0xmNRpqamrBarXR2dtLR0eHz\n94Xy0l6zKWtraytms5mMjIxpd9hPPfUULS0tfP7555jN5pD4VP2TTz7J0aNHvY69/fbbNDY20tjY\nSG5uLtu2bZuWz9+c7A0NDWzevJmvvvqKTZs2eV0HoejkyZNePaip1+5MQrE+/QlUzkiIUTd/ZY2E\nGK2rq6O8vJympib27dvH4cOHp+WLpBgFaYwDSk1NZfv27QDcf//9GI1Grl69OmP6ZcuWkZmZyU8/\n/QRAUlKSPk/34ODgjEO/oWA2Zf3+++/55ptvyMnJwWKx8N1333HgwAHANaztfiErLS2d8W72dprr\nUJ+/OdlDdVjTF6fTSXNzM48//vgt5w3F+pzJrZQznGMUApc1EmI0OTlZ782Oj4/7nEQqUmLUTRrj\nKabeST722GO0t7cDcPHiRRwOB8uXL/dKMzw8rF84N27c4OzZs/oSktnZ2Xz22WcANDY2TpswJZjm\nUtb9+/fT2tpKS0sLb731Fps2bdLvWgcHB/V0zc3NrF+/foFLMD/+hvr8zck+NDQUksOavt4LPHPm\nDGvXrg04I56vvKFan3MpZ6TEKAQuayTEqMVioba2lqysLOrq6rBYLED4x6g/0hh7sFgs7N69m4sX\nL5KVlcWnn35KUVERPT095OfnY7FYePPNNwHvi2JwcJBnn32WgoICSktLyc7OZvPmzQDs2bOHs2fP\nkpubS3t7u8+1oINhrmX1p66ujvz8fHbu3ElHRweVlZULXYx5mevw7VShMKzpqz7Bd89/an3OlDcU\n63Ou5YyUGIXZ1elMQrFOfTl48CDV1dW0trZSWVlJVVUVEN4xGlCQPsUtxG3z66+/Tvt6iMPhUFu2\nbFF9fX0+8/zwww+qrKxM36+vr1f19fVKKaXy8vLU4OCgUkqpgYEBlZeXt0BnLsTiMDVGH3roIa/n\nH3744Wl5Ii1GpWcsIp6aw1CfvznZQ3lYU4hwNDVG09PT9Q/Yffvtt6Snp0/LE2kxKtNhiojm/gDL\nyMgIZrOZffv2UVRURGVlJQ8++CC7du3S0w4MDFBdXa0Pg1mtVl5//XV9Tnb38OXIyAjl5eVcvnyZ\ntLQ0jhw54vdDYkKImfmK0XvvvZeXX34Zu91ObGwshw4d4r777ovoGJXGWAghhAgyGaYWQgghgkwa\nYyGEECLIpDEWQgghgkwaYyGEECLIpDEWQgghgkwaYyGEECLIpDEWQgghgkwaYyGEECLI/g+aYvWD\nVp8juwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Align the query to have data every 5 minutes.\n", + "q2 = q.align('ALIGN_MEAN', minutes=5)\n", + "\n", + "df2 = q2.as_dataframe(label='instance_name')\n", + "_ = df2.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Displaying the results as a heatmap" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "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\nVtc6H3zwgZ588km9/PLLWrp0afh04m/74osvNHfuXE2bNk1jx47Vz372MxUXF6tZs2ZauXKlpK8m\nAxcsWKDi4mIdOnQovPzb2rZtq6KiIg0dOlSzZ8+uc9z37dsnSXrsscc0ePBg3Xrrrdq1a5ck6aOP\nPtL27dvrPHpz/fr14UlfAAAAAAAAfDc0S3QRj6DUe8TiunXrlJ2dreTkZCUnJ6tnz56SpE8++USP\nPfaY9u7dq4MHD+riiy+WJK1evVqfffaZnPvqmzpw4IAOHjyo5s2bh7f52WefqWPHjurYsaMkacCA\nAZo/f76krybUbr/9dn3++eeSpMrKyhq7GrJeXev87Gc/U6tWrSRJl156qd5991116dIlYhtZWVlK\nSEjQWWedJedc+Ps888wztXXr1vD3PHv2bB08eFB79+7VD3/4Q/3rv/5rxLYuvfRSSdLZZ5+t5cuX\n1/h9fe3o0aPavn27fvKTn+iOO+7QnDlz9Lvf/U6//e1vNXnyZP3ud78Lr/v1WH+trKxM48aN04MP\nPljn1wAAAAAAAIAt8XTE4jFdY9E5pzvuuEPTp0/XmWeeqcWLF2vt2rXh1+bPn6/k5ORqn3Pddddp\n165dOvvsszVs2LCIybCvPf7447rooos0depUbd26Vddcc80xr1fXOt++xmIoFNJzzz2nBQsWKBQK\nqbCwUJKUkpISfj0p6f+GKyEhQZWVlaqoqNC9996roqIipaena+rUqTp8+HCNzV9vKyEhQUePHo0Y\nl29eX7FNmzZq3rx5eDKyb9++WrRokcrLy/X3v/9dw4cPl3NOO3bs0I033qjp06erS5cu2r9/v264\n4QaNGTNG55xzTo0dAAAAAAAAsCnB0sTieeedp9/85jcaNWqUjhw5ojfffFNDhgzRgQMHdMIJJ+jI\nkSMqLi5Wenq6pK+OBHz22Wd13XXXSfrqTsWdO3eudupvRUWFSkpK9MUXX6hjx45asmRJ+LV9+/aF\nt1VUVBRenpqaqv3794ef79+/v8b1vqm2bUnS22+/rb179yolJUXLly/X5MmT1aVLFw0bNqzWsahp\nMvTw4cMKhUJq06aNysvLtXTpUuXk5NS6jW+r65ToXr16ac2aNbrooov0zjvvKDMzUy1atNCaNWvC\n6wwfPlx33nmnfvzjH+vIkSO66aabNGjQoPCEJAAAAAAAAL47kuLo5i31XmOxa9eu6tWrlwYMGKD/\n+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\nS2PMe7npMMaxZ+13BWIn5BwnvwPAd8XOnTvVrl27oDOiYrEZAIDvE2v7amu9AGAZN29BTF1//fVB\nJ0TYv39cOXNSAAAZeUlEQVS/Hn74YY0dO1bFxcXVXovXA3gtNm/fvl3jx4/XlClTtG/fPt15553q\n37+/xo4dq507dwadF2H79u266667zPRK0p49e6o9du/erSuvvFJffvml9uzZE3Rejaw1r1q1Kvzx\nvn379Otf/1r9+/fXmDFjtGPHjgDLamexed++fZoyZYr69u2r7t2768ILL9Rll12mKVOmaO/evUHn\nRbDWW5943FfXJV57Le6rrTVb+9tC4u8LH6z1Sjb31daaLe6rLTbXJl731YgNjlhEo23YsKHG5c45\n3XDDDXrrrbc8F9Xtl7/8pTp16qRu3bpp4cKFSk5O1sMPP6yUlBTl5eVp8eLFQSdGsNh87bXXKjs7\nWwcPHlRRUZEGDx6s/v37a/ny5Vq7dq1+//vfB51YjbVeSercubNOOumkastKS0uVnp6uUCikFStW\nBFRWO2vN3/z5uuuuu3TCCSfoqquu0rJly7R27VpNmzYt4MJIFpuvu+46XXjhhcrLy9OJJ54oSfrn\nP/+pxYsXa82aNfrDH/4QcGF11nole/tqa72SzX21tWaL+2qLzdb21dZ6JZv7amvNFvfV1pot7qsR\nG0lBB8C+/Px8XXDBBTXeUj4e/2dl8+bN4T/ievfurenTp+uaa67R9OnTAy6rncXm3bt3a/jw4ZKk\n559/XqNGjZIkDR8+XIsWLQoyrUbWeiVp3LhxevvttzVu3Ljw9Vh69eqlN954I+Cy2lls/tqHH36o\nl156SdJX/1CMt39w18RK85YtWzR79uxqy0488USNGjUqLn/+rPVK9vbV1nolm/tqa80W99UWm63t\nq631fpuVffU3WWi2uK+21mxxX43YYGIRjZaZmal7771Xp556asRrPXr08B9Uj4qKClVVVSkh4asr\nAfziF79Qenq6rr76ah04cCDguppZbP7mDmbgwIHVXqusrPSdUy9rvZJUUFCgfv366YEHHlCHDh30\ny1/+UqFQKOisOllr3rlzp55++mk557Rv3z4558K9VVVVAdfVzGJzRkaGZs2apby8vPAFwHfs2KGi\noiJ16NAh4LpI1nole/tqa72SzX21tWaL+2qLzdb21dZ6JZv7amvNFvfV1pot7qsRG1xjEY128803\n17ozmTBhguea+vXs2VNr1qyptmzw4MG6/fbblZycHFBV3Sw2Z2dnq7y8XJJ02223hZd//vnnOu20\n04LKqpW13q+1b99eTzzxhLp3766CggIdOnQo6KR6WWq+6qqrVF5ergMHDmjw4MHavXu3pK9OS/nR\nj34UcF3NLDY/+uij2rNnj66++mpdcMEF6t69u4YPH64vv/xSjz32WNB5Eaz1Svb21dZ6JZv7amvN\nFvfVFpslW/tqyV6vxX21tWaL+2przRb31YgNrrEIAN8Rhw4d0ubNm3XmmWcGndJgFpsBAPg+sbav\nttYLANZxxCKa1Lcv4FrbBV3jhbVeiWYfrPVKXzUed9xx4T+iaW56Vt8XdT2PR9aarfVK9pqt9Uo0\n+2CtV7LbbG1fbalXsvu+qOt5vLHWK9lrttaLpsXEIprUCy+8UOfzeGOtV6LZB2u9Es0+WOuVaPbB\nWq9kr9lar0SzD9Z6JZp9sNYr0eyDtV7JXrO1XjQtToUGAAAAAAAAELXEiRMnTgw6ArZVVFQoISEh\nfFewNWvW6PXXX9fevXtrvENU0Kz1SjT7YK1XotkHa72SzeaNGzeG735ogbVeyV6ztV6JZh+s9Uo0\n+2CtV6LZl5KSEjnn1KxZM23ZskXvvPOOqqqq1K5du6DTamWt2VovYoOJRTRaXl6ecnNzddxxx+mp\np57S888/r4yMDL366qv69NNP9dOf/jToxGqs9Uo0+2CtV6LZB2u9ks3mSy65RC+//LJ27typE044\nQW3btg06qU7WeiV7zdZ6JZp9sNYr0eyDtV6JZh8KCwt17733qqioSM2aNdM999yjiooKPfPMM6qo\nqNC5554bdGIEa83WehFDDmik3Nzc8Md5eXnu4MGDzjnnjhw54i6//PKgsmplrdc5mn2w1usczT5Y\n63XOZvPAgQPdxx9/7B555BHXu3dv179/fzdz5kz3xRdfBJ1WI2u9ztlrttbrHM0+WOt1jmYfrPU6\nR7MP/fr1cwcPHnS7du1y3bp1czt37nTOOVdeXl7tb6V4Yq3ZWi9ih5u3oNFatGihTz75RJLUpk0b\nHT58WJJUWVkpF4eX8LTWK9Hsg7VeiWYfrPVKNptDoZDOPPNM3XbbbVq2bJnuu+8+7dy5Uz//+c81\ndOjQoPMiWOuV7DVb65Vo9sFar0SzD9Z6JZp9SEhI0HHHHadWrVrpuOOOU+vWrSVJxx9/fPhyMfHG\nWrO1XsQON29Bo23cuFHjxo1T586dJUnr1q3TBRdcoI8//lgjRoxQ//79Ay6szlqvRLMP1nolmn2w\n1ivZbB40aJBefPHFiOXOOf3Xf/2XunfvHkBV7az1SvaarfVKNPtgrVei2QdrvRLNPowZM0aSdODA\nAbVo0UKHDx9W7969tXr1ah05ckRTpkwJuDCStWZrvYgdJhbRJCorK/XWW2/pH//4hyorK9W+fXtd\nfPHFatWqVdBpNbLWK9Hsg7VeiWYfrPVK9pqLi4vjcsKzNtZ6JXvN1nolmn2w1ivR7IO1XolmHyoq\nKrRkyRKdcMIJ4etDrlu3TqeffrqGDh2qlJSUoBMjWGu21ovYYWIRAAAAAAAAQNS4xiJi6vrrrw86\nISrWeiWafbDWK9Hsg7VeiWYfrPVK9pqt9Uo0+2CtV6LZB2u9Es0+WOuV7DVb60XjJAUdAPs2bNhQ\n43LnnDZu3Oi5pn7WeiWafbDWK9Hsg7VeiWYfrPVK9pqt9Uo0+2CtV6LZB2u9Es0+WOuV7DVb60Xs\nMLGIRsvPz9cFF1xQ491G9+7dG0BR3az1SjT7YK1XotkHa70SzT5Y65XsNVvrlWj2wVqvRLMP1nol\nmn2w1ivZa7bWixhyQCPl5ua6//3f/63xtaysLL8xDWCt1zmafbDW6xzNPljrdY5mH6z1Omev2Vqv\nczT7YK3XOZp9sNbrHM0+WOt1zl6ztV7ETuLEiRMnBj25Cdvatm2rtm3bqk2bNhGvdezYUaeffnoA\nVbWz1ivR7IO1XolmH6z1SjT7YK1XstdsrVei2QdrvRLNPljrlWj2wVqvZK/ZWi9ih7tCAwAAAAAA\nAIgad4UGAAAAAAAAEDUmFgEAAAAAAABEjYlFAAAAAAAAAFFjYhGNtmLFCh0+fDjojAaz1ivR7IO1\nXolmH6z1SjT7YK1XstdsrVei2QdrvRLNPljrlWj2wVqvZK/ZWi9ih7tCo9EGDRqk//zP/9Qnn3yi\nZs2aqWPHjkpIiN85a2u9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6JZp9sNYr0eyDtV6JZh+s9Ur2mq31\nInaYWESjLV26VEVFRTp48KDmz5+vKVOm6PPPP1dqaqoyMjKCzotgrVei2QdrvRLNPljrlWj2wVqv\nZK/ZWq9Esw/WeiWafbDWK9Hsg7VeyV6ztV7EkAMaadCgQdWel5WVuWeeecZdddVVLisrK6Cq2lnr\ndY5mH6z1OkezD9Z6naPZB2u9ztlrttbrHM0+WOt1jmYfrPU6R7MP1nqds9dsrRexw8QiGm3gwIG1\nvrZlyxaPJQ1jrdc5mn2w1usczT5Y63WOZh+s9Tpnr9lar3M0+2Ct1zmafbDW6xzNPljrdc5es7Ve\nxA6nQqPRTjvtNJ188sk1vtaqVSvPNfWz1ivR7IO1XolmH6z1SjT7YK1XstdsrVei2QdrvRLNPljr\nlWj2wVqvZK/ZWi9iJ+Scc0FH4Ltjz549kqTWrVsHXNIw1nolmn2w1ivR7IO1XolmH6z1SvaarfVK\nNPtgrVei2QdrvRLNPljrlew1W+tF00oKOgD2lZSU6KGHHtLq1avVqlUrOee0f/9+XXTRRRozZkyt\n/4sRFGu9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6JZp9sNYr0eyDtV6JZh+s9Ur2mq31IoZ8nXON766r\nrrrKLVmyxB09ejS87OjRo+6VV15xV155ZYBlNbPW6xzNPljrdY5mH6z1OkezD9Z6nbPXbK3XOZp9\nsNbrHM0+WOt1jmYfrPU6Z6/ZWi9iJyHoiU3Yt3v3bvXr10+JiYnhZYmJicrNzQ0fEh1PrPVKNPtg\nrVei2QdrvRLNPljrlew1W+uVaPbBWq9Esw/WeiWafbDWK9lrttaL2OHmLWi0devWac2aNWrTpo1C\noZDKy8u1adMmzZgxQy1atNBll10WdGI11nolmn2w1ivR7IO1XolmH6z1SvaarfVKNPtgrVei2Qdr\nvRLNPljrlew1W+tF7HDzFjRaRUWFFi5cqBUrVqisrEySlJaWpl69eunKK69USkpKwIXVWeuVaPbB\nWq9Esw/WeiWafbDWK9lrttYr0eyDtV6JZh+s9Uo0+2CtV7LXbK0XscPEIgAAAAAAAICocY1FxERe\nXl7QCVGx1ivR7IO1XolmH6z1SjT7YK1XstdsrVei2QdrvRLNPljrlWj2wVqvZK/ZWi+aBhOLiAlr\nB8Ja65Vo9sFar0SzD9Z6JZp9sNYr2Wu21ivR7IO1XolmH6z1SjT7YK1XstdsrRdNg4lFxESPHj2C\nToiKtV6JZh+s9Uo0+2CtV6LZB2u9kr1ma70SzT5Y65Vo9sFar0SzD9Z6JXvN1nrRNLgrNGKiVatW\nSktLCzqjwaz1SjT7YK1XotkHa70SzT5Y65XsNVvrlWj2wVqvRLMP1nolmn2w1ivZa7bWi6aRFHQA\n7NuwYUO158453XjjjZoxY4acc+rSpUtAZTWz1ivR7IO1XolmH6z1SjT7YK1XstdsrVei2QdrvRLN\nPljrlWj2wVqvZK/ZWi9ih7tCo9E6d+6sbt26KTk5Obzs/fff17/8y78oFArp2WefDbAukrVeiWYf\nrPVKNPtgrVei2QdrvZK9Zmu9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6EUMOaKQ//elPbtiwYW7lypXh\nZT179gywqG7Wep2j2Qdrvc7R7IO1Xudo9sFar3P2mq31OkezD9Z6naPZB2u9ztHsg7Ve5+w1W+tF\n7HDzFjRaTk6OZs6cqbffflujR49WSUmJQqFQ0Fm1stYr0eyDtV6JZh+s9Uo0+2CtV7LXbK1XotkH\na70SzT5Y65Vo9sFar2Sv2VovYodTodGkNmzYoN/+9rf6+9//rjVr1gSdUy9rvRLNPljrlWj2wVqv\nRLMP1nole83WeiWafbDWK9Hsg7VeiWYfrPVK9pqt9aJpMbGIJuecU3l5uVq0aBF0SoNY65Vo9sFa\nr0SzD9Z6JZp9sNYr2Wu21ivR7IO1XolmH6z1SjT7YK1XstdsrRdNh7tCo0n85S9/0fLly1VaWipJ\nSk9PV3Z2trKysgIuq5m1XolmH6z1SjT7YK1XotkHa72SvWZrvRLNPljrlWj2wVqvRLMP1nole83W\nehEbHLGIRrv//vv1j3/8Q4MGDVJ6erokqbS0VC+++KI6deqk8ePHB1xYnbVeiWYfrPVKNPtgrVei\n2QdrvZK9Zmu9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6EUM+7hCD77Y+ffrUuLyqqspdeumlnmvqZ63X\nOZp9sNbrHM0+WOt1jmYfrPU6Z6/ZWq9zNPtgrdc5mn2w1usczT5Y63XOXrO1XsQOd4VGo6WkpGj9\n+vURyz/44AM1a9YsgKK6WeuVaPbBWq9Esw/WeiWafbDWK9lrttYr0eyDtV6JZh+s9Uo0+2CtV7LX\nbK0XscOp0Gi0DRs2aOLEiSovL1f79u0lSdu2bVPLli1199136+yzzw64sDprvRLNPljrlWj2wVqv\nRLMP1nole83WeiWafbDWK9Hsg7VeiWYfrPVK9pqt9SJ2mFhEk/nnP/9Z7aKtJ554YsBFdbPWK9Hs\ng7VeiWYfrPVKNPtgrVey12ytV6LZB2u9Es0+WOuVaPbBWq9kr9laL2Ig2DOx8V31xBNPBJ0QFWu9\nztHsg7Ve52j2wVqvczT7YK3XOXvN1nqdo9kHa73O0eyDtV7naPbBWq9z9pqt9aJpcI1FxMQbb7wR\ndEJUrPVKNPtgrVei2QdrvRLNPljrlew1W+uVaPbBWq9Esw/WeiWafbDWK9lrttaLpsHEImLCGTvD\n3lqvRLMP1nolmn2w1ivR7IO1Xsles7VeiWYfrPVKNPtgrVei2QdrvZK9Zmu9aBpcYxEx4ZxTKBQK\nOqPBrPVKNPtgrVei2QdrvRLNPljrlew1W+uVaPbBWq9Esw/WeiWafbDWK9lrttaLpsERi2i0ZcuW\nac+ePZKkXbt2ady4cRowYIBuvfVWbd++PeC6SNZ6JZp9sNYr0eyDtV6JZh+s9Ur2mq31SjT7YK1X\notkHa70SzT5Y65XsNVvrRQw19UUb8f1z2WWXhT++5ZZb3NNPP+22bdvmFi1a5K699toAy2pmrdc5\nmn2w1usczT5Y63WOZh+s9Tpnr9lar3M0+2Ct1zmafbDW6xzNPljrdc5es7VexA5HLKLRKisrwx9v\n3rxZ1157rdq3b6/Bgwdr165dAZbVzFqvRLMP1nolmn2w1ivR7IO1Xsles7VeiWYfrPVKNPtgrVei\n2QdrvZK9Zmu9iB0mFtFoF154oR5//HEdOnRI3bt317JlyyRJa9asUcuWLQOui2StV6LZB2u9Es0+\nWOuVaPbBWq9kr9lar0SzD9Z6JZp9sNYr0eyDtV7JXrO1XsQON29Box05ckQzZszQokWLJEnbt29X\n8+bN1atXL40ZM0YnnXRSwIXVWeuVaPbBWq9Esw/WeiWafbDWK9lrttYr0eyDtV6JZh+s9Uo0+2Ct\nV7LXbK0XscPEIprUvn37dPToUbVp0ybolAax1ivR7IO1XolmH6z1SjT7YK1XstdsrVei2QdrvRLN\nPljrlWj2wVqvZK/ZWi+aFqdCo0m1bNmy2i+TTZs2BVhTP2u9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6\nJZp9sNYr0eyDtV6JZh+s9Ur2mq31omkxsYiYuu6664JOiIq1XolmH6z1SjT7YK1XotkHa72SvWZr\nvRLNPljrlWj2wVqvRLMP1nole83WetE4SUEHwL777ruvxuXOOe3du9dzTf2s9Uo0+2CtV6LZB2u9\nEs0+WOuV7DVb65Vo9sFar0SzD9Z6JZp9sNYr2Wu21ovYYWIRjbZo0SLdcccdSklJiXjtlVdeCaCo\nbtZ6JZp9sNYr0eyDtV6JZh+s9Ur2mq31SjT7YK1XotkHa70SzT5Y65XsNVvrRewwsYhG69q1q374\nwx/qvPPOi3jt97//fQBFdbPWK9Hsg7VeiWYfrPVKNPtgrVey12ytV6LZB2u9Es0+WOuVaPbBWq9k\nr9laL2KHu0Kj0fbs2aNmzZqpefPmQac0iLVeiWYfrPVKNPtgrVei2QdrvZK9Zmu9Es0+WOuVaPbB\nWq9Esw/WeiV7zdZ6ETtMLAIAAAAAAACIGqdCo9H27dunmTNnavny5dq1a5dCoZDatm2r7OxsjRo1\nSq1atQo6sRprvRLNPljrlWj2wVqvRLMP1nole83WeiWafbDWK9Hsg7VeiWYfrPVK9pqt9SKGHNBI\nBQUFbubMma6srCy8rKyszM2cOdONGDEiwLKaWet1jmYfrPU6R7MP1nqdo9kHa73O2Wu21usczT5Y\n63WOZh+s9TpHsw/Wep2z12ytF7HDxCIarU+fPsf0WlCs9TpHsw/Wep2j2Qdrvc7R7IO1XufsNVvr\ndY5mH6z1OkezD9Z6naPZB2u9ztlrttaL2EkI+ohJ2JeRkaFZs2Zpx44d4WU7duxQYWGhOnToEGBZ\nzaz1SjT7YK1XotkHa70SzT5Y65XsNVvrlWj2wVqvRLMP1nolmn2w1ivZa7bWi9jh5i1otC+//FKF\nhYVasWKFdu3aJUlq166devXqpZEjR6p169YBF1ZnrVei2QdrvRLNPljrlWj2wVqvZK/ZWq9Esw/W\neiWafbDWK9Hsg7VeyV6ztV7EDhOLAAAAAAAAAKLGqdBoEps2bdLq1at14MCBastXrVoVUFHdrPVK\nNPtgrVei2QdrvRLNPljrlew1W+uVaPbBWq9Esw/WeiWafbDWK9lrttaL2GBiEY327LPP6sYbb9Tc\nuXN1+eWXa/ny5eHXHn300QDLamatV6LZB2u9Es0+WOuVaPbBWq9kr9lar0SzD9Z6JZp9sNYr0eyD\ntV7JXrO1XsROUtABsG/BggUqKipSamqqtmzZotGjR2vr1q3693//d8XjmfbWeiWafbDWK9Hsg7Ve\niWYfrPVK9pqt9Uo0+2CtV6LZB2u9Es0+WOuV7DVb60XsMLGIRquqqlJqaqok6eSTT9bcuXM1evRo\nlZSUxOUvFGu9Es0+WOuVaPbBWq9Esw/WeiV7zdZ6JZp9sNYr0eyDtV6JZh+s9Ur2mq31InY4FRqN\n1q5dO/3tb38LP09NTdXMmTO1e/duffLJJwGW1cxar0SzD9Z6JZp9sNYr0eyDtV7JXrO1XolmH6z1\nSjT7YK1XotkHa72SvWZrvYgd7gqNRtu+fbsSExN14oknRrz27rvv6ic/+UkAVbWz1ivR7IO1Xolm\nH6z1SjT7YK1XstdsrVei2QdrvRLNPljrlWj2wVqvZK/ZWi9ih4lFAAAAAAAAAFHjVGgAAAAAAAAA\nUWNiEQAAAAAAAEDUmFgEAAAAAAAAEDUmFgEAAAAAAABE7f8DZQBcQder8+MAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib\n", + "import seaborn\n", + "\n", + "# Set the size of the heatmap to have a better aspect ratio.\n", + "matplotlib.pyplot.figure(figsize=df2.shape)\n", + "_ = seaborn.heatmap(df2.T, xticklabels=df2.index.map(str), cmap='YlGnBu')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reducing the query\n", + "\n", + "In order to aggregate the timeseries data, the `reduce()` method can be used. The fields to be retained after aggregation must be specified in the method.\n", + "\n", + "For example, to aggregate the results by the zone, `'resource.zone'` can be specified." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
us-central1-bus-east1-d
2016-04-07 17:52:000.0428960.012093
2016-04-07 17:57:000.0426620.011775
2016-04-07 18:02:000.0457160.015109
2016-04-07 18:07:000.0412200.029259
2016-04-07 18:12:000.0516910.477413
\n", + "
" + ], + "text/plain": [ + " us-central1-b us-east1-d\n", + "2016-04-07 17:52:00 0.042896 0.012093\n", + "2016-04-07 17:57:00 0.042662 0.011775\n", + "2016-04-07 18:02:00 0.045716 0.015109\n", + "2016-04-07 18:07:00 0.041220 0.029259\n", + "2016-04-07 18:12:00 0.051691 0.477413" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "q3 = q2.reduce('REDUCE_MEAN', 'resource.zone')\n", + "df3 = q3.as_dataframe('zone')\n", + "df3.tail(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Multi-level headers\n", + "\n", + "If you don't provide any labels to `as_dataframe`, it returns all the resource and metric labels present in the timeseries as a multi-level header.\n", + "\n", + "This allows you to filter, and aggregate the data more easily." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
resource_typegce_instance
project_idmonitoring-datalab
zoneus-central1-bus-east1-d
instance_id22923934975298670905437900963820317613
instance_namegae-datalab-main-j642analyst2
2016-04-07 17:52:000.0428960.012093
2016-04-07 17:57:000.0426620.011775
2016-04-07 18:02:000.0457160.015109
2016-04-07 18:07:000.0412200.029259
2016-04-07 18:12:000.0516910.477413
\n", + "
" + ], + "text/plain": [ + "resource_type gce_instance \n", + "project_id monitoring-datalab \n", + "zone us-central1-b us-east1-d\n", + "instance_id 2292393497529867090 5437900963820317613\n", + "instance_name gae-datalab-main-j642 analyst2\n", + "2016-04-07 17:52:00 0.042896 0.012093\n", + "2016-04-07 17:57:00 0.042662 0.011775\n", + "2016-04-07 18:02:00 0.045716 0.015109\n", + "2016-04-07 18:07:00 0.041220 0.029259\n", + "2016-04-07 18:12:00 0.051691 0.477413" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multi_level_df = q2.as_dataframe()\n", + "multi_level_df.tail(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filter the dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
resource_typegce_instance
project_idmonitoring-datalab
zoneus-central1-b
instance_id2292393497529867090
instance_namegae-datalab-main-j642
2016-04-07 17:52:000.042896
2016-04-07 17:57:000.042662
2016-04-07 18:02:000.045716
2016-04-07 18:07:000.041220
2016-04-07 18:12:000.051691
\n", + "
" + ], + "text/plain": [ + "resource_type gce_instance\n", + "project_id monitoring-datalab\n", + "zone us-central1-b\n", + "instance_id 2292393497529867090\n", + "instance_name gae-datalab-main-j642\n", + "2016-04-07 17:52:00 0.042896\n", + "2016-04-07 17:57:00 0.042662\n", + "2016-04-07 18:02:00 0.045716\n", + "2016-04-07 18:07:00 0.041220\n", + "2016-04-07 18:12:00 0.051691" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multi_level_df.filter(regex='gae-datalab-main').tail(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Aggregate columns in the datframe" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
zoneus-central1-bus-east1-d
2016-04-07 17:52:000.0428960.012093
2016-04-07 17:57:000.0426620.011775
2016-04-07 18:02:000.0457160.015109
2016-04-07 18:07:000.0412200.029259
2016-04-07 18:12:000.0516910.477413
\n", + "
" + ], + "text/plain": [ + "zone us-central1-b us-east1-d\n", + "2016-04-07 17:52:00 0.042896 0.012093\n", + "2016-04-07 17:57:00 0.042662 0.011775\n", + "2016-04-07 18:02:00 0.045716 0.015109\n", + "2016-04-07 18:07:00 0.041220 0.029259\n", + "2016-04-07 18:12:00 0.051691 0.477413" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multi_level_df.groupby(level='zone', axis=1).mean().tail(5)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/sources/lib/api/gcp/stackdriver/__init__.py b/sources/lib/api/gcp/stackdriver/__init__.py new file mode 100644 index 000000000..c522016c6 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Cloud Platform library - Stackdriver Functionality.""" diff --git a/sources/lib/api/gcp/stackdriver/monitoring/__init__.py b/sources/lib/api/gcp/stackdriver/monitoring/__init__.py new file mode 100644 index 000000000..487ee6c07 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Cloud Platform library - Monitoring Functionality.""" + +from ._timeseries import Query diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/__init__.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/__init__.py new file mode 100644 index 000000000..2ae899f62 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Monitoring API V3.""" + +from .client import Client +from .group import Group +from .label import LabelDescriptor +from .metric import Metric, MetricDescriptor +from .resource import Resource, ResourceDescriptor +from .timeseries import Query diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/api.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/api.py new file mode 100644 index 000000000..53eba7d58 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/api.py @@ -0,0 +1,279 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""HTTP API wrapper for the Google Monitoring API.""" + +from gcp._util import Http + + +class Api(object): + """A helper class to issue Monitoring API HTTP requests.""" + + _ENDPOINT = 'https://monitoring.googleapis.com/v3/' + _GROUPS_PATH = 'projects/%s/groups/%s' + _GROUP_MEMBERS_PATH = '{}/members'.format(_GROUPS_PATH) + _METRIC_PATH = 'projects/%s/metricDescriptors/%s' + _RESOURCE_PATH = 'projects/%s/monitoredResourceDescriptors/%s' + _TIMESERIES_PATH = 'projects/%s/timeSeries' + + def __init__(self, credentials): + """Initializes the Monitoring helper with credentials. + + Args: + credentials: The credentials to use to authorize requests. + """ + self._credentials = credentials + + def groups_get(self, group_id, project_id): + """Issues a request to retrieve information about a group. + + Args: + group_id: The ID of the group. + project_id: The project ID to use to fetch the results. + + Returns: + A parsed result object. + """ + url = self._ENDPOINT + self._GROUPS_PATH % (project_id, group_id) + return Http.request(url, credentials=self._credentials) + + def groups_list(self, project_id, children_of_group=None, + ancestors_of_group=None, descendants_of_group=None, + page_size=None, page_token=None): + """Issues a request to list the groups in the project. + + At most one of the parameters children_of_group, ancestors_of_group, and + descendants_of_group may be specified to narrow the query. + + Args: + project_id: The project ID to use to fetch the results. + children_of_group: The ID of the group whose children are to be listed. + ancestors_of_group: The ID of the group whose ancestors are to be listed. + descendants_of_group: The ID of the group whose descendants are to be + listed. + page_size: The maximum number of results to return per page. + page_token: An optional token to continue the retrieval. + + Returns: + A parsed result object. + """ + url = self._ENDPOINT + self._GROUPS_PATH % (project_id, '') + args = {} + + if children_of_group is not None: + args['childrenOfGroup'] = self._GROUPS_PATH % (project_id, + children_of_group) + if ancestors_of_group is not None: + args['ancestorsOfGroup'] = self._GROUPS_PATH % (project_id, + ancestors_of_group) + if descendants_of_group is not None: + args['descendantsOfGroup'] = self._GROUPS_PATH % (project_id, + descendants_of_group) + if page_size is not None: + args['pageSize'] = page_size + if page_token is not None: + args['pageToken'] = page_token + + return Http.request(url, args=args, credentials=self._credentials) + + def groups_members_list( + self, group_id, project_id, end_time=None, start_time=None, filter=None, + page_size=None, page_token=None): + """Issues a request to retrieve the members of a group. + + Args: + group_id: The ID of the group. + project_id: The project ID to use to fetch the results. + end_time: The end time (inclusive) of the group membership. + start_time: The start time (exclusive) of the group membership. + filter: A filter to restrict the resources returned. E.g.: + 'resource.type = "gce_instance"' + page_size: The maximum number of results to return per page. + page_token: An optional token to continue the retrieval. + + Returns: + A parsed result object. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + url = self._ENDPOINT + self._GROUP_MEMBERS_PATH % (project_id, group_id) + args = {} + if end_time is not None: + args['interval.endTime'] = _format_timestamp_as_string(end_time) + if start_time is not None: + args['interval.startTime'] = _format_timestamp_as_string(start_time) + if filter is not None: + args['filter'] = filter + if page_size is not None: + args['pageSize'] = page_size + if page_token is not None: + args['pageToken'] = page_token + return Http.request(url, args=args, credentials=self._credentials) + + def metric_descriptors_get(self, metric_id, project_id): + """Issues a request to retrieve information about a metric. + + Args: + metric_id: The ID of the metric. + project_id: The project ID to use to fetch the results. + + Returns: + A parsed result object. + """ + url = self._ENDPOINT + self._METRIC_PATH % (project_id, metric_id) + return Http.request(url, credentials=self._credentials) + + def metric_descriptors_list( + self, project_id, filter=None, page_size=None, page_token=None): + """Issues a request to list the descriptors of all metrics in the project. + + Args: + project_id: The project ID to use to fetch the results. + filter: A filter to restrict the metrics returned. E.g.: + 'metric.type = starts_with("compute")' + page_size: The maximum number of results to return per page. + page_token: An optional token to continue the retrieval. + + Returns: + A parsed result object. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + url = self._ENDPOINT + self._METRIC_PATH % (project_id, '') + args = {} + if filter is not None: + args['filter'] = filter + if page_size is not None: + args['pageSize'] = page_size + if page_token is not None: + args['pageToken'] = page_token + return Http.request(url, args=args, credentials=self._credentials) + + def monitored_resource_descriptors_get(self, resource_type, project_id): + """Issues a request to retrieve information about a resource type. + + Args: + resource_type: The type of resource to get the descriptor for. + project_id: The project ID to use to fetch the results. + + Returns: + A parsed result object. + """ + url = self._ENDPOINT + self._RESOURCE_PATH % (project_id, resource_type) + return Http.request(url, credentials=self._credentials) + + def monitored_resource_descriptors_list( + self, project_id, filter=None, page_size=None, page_token=None): + """Issues a request to list the descriptors of available resource types. + + Args: + project_id: The project ID to use to fetch the results. + filter: A filter for the resource descriptors. E.g.: + 'resource.type = starts_with("gce_")' + page_size: The maximum number of results to return per page. + page_token: An optional token to continue the retrieval. + + Returns: + A parsed result object. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + url = self._ENDPOINT + self._RESOURCE_PATH % (project_id, '') + args = {} + if filter is not None: + args['filter'] = filter + if page_size is not None: + args['pageSize'] = page_size + if page_token is not None: + args['pageToken'] = page_token + return Http.request(url, args=args, credentials=self._credentials) + + def time_series_list( + self, project_id, filter, end_time, start_time=None, + per_series_aligner=None, alignment_period_seconds=None, + cross_series_reducer=None, group_by_fields=(), + view=None, + page_size=None, + page_token=None): + """Issues a request to retrieve metric data. + + Args: + project_id: The project ID to use to fetch the results. + filter: The filter for the resource and metrics to fetch. + end_time: The end time (inclusive) of the timeseries data. + start_time: The start time (exclusive) of the timeseries data. + per_series_aligner: An alignment period for the timeseries data. + alignment_period_seconds: An int specifying the alignment period. + cross_series_reducer: The reduce method for aggregating multiple + timeseries. + group_by_fields: An iterable of fields to preserve when + cross_series_reducer is specified. E.g.: ["resource.zone"] + view: Specifies which information is returned about the time series. + Must be one of "FULL" or "HEADERS". + page_size: The maximum number of results to return per page. + page_token: An optional token to continue the retrieval. + + Returns: + A parsed result object. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + + url = self._ENDPOINT + self._TIMESERIES_PATH % project_id + + # Assemble the arguments for the RPC. + args = { + 'filter': filter, + 'interval.endTime': _format_timestamp_as_string(end_time), + } + if start_time is not None: + args['interval.startTime'] = _format_timestamp_as_string(start_time) + if per_series_aligner is not None: + args['aggregation.perSeriesAligner'] = per_series_aligner + if alignment_period_seconds is not None: + args['aggregation.alignmentPeriod'] = '{:d}s'.format( + alignment_period_seconds) + if cross_series_reducer is not None: + args['aggregation.crossSeriesReducer'] = cross_series_reducer + if view is not None: + args['view'] = view + if page_size is not None: + args['pageSize'] = page_size + if page_token is not None: + args['pageToken'] = page_token + + # Convert to a list before adding repeated fields. + args = args.items() + + args.extend(('aggregation.groupByFields', field) + for field in group_by_fields) + + return Http.request(url, args=args, credentials=self._credentials) + + +def _format_timestamp_as_string(timestamp): + """Converts a datetime object to a string as required by the API. + + Args: + timestamp: A Python datetime object or a timestamp string in RFC3339 + UTC "Zulu" format. + + Returns: + The string version of the timestamp. For example: + "2016-02-17T19:18:01.763000Z". + """ + if isinstance(timestamp, basestring): + return timestamp + + if timestamp.tzinfo is not None: + # Convert to UTC and remove the time zone info. + timestamp = timestamp.replace(tzinfo=None) - timestamp.utcoffset() + + return timestamp.isoformat() + 'Z' diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/client.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/client.py new file mode 100644 index 000000000..1b659a0d0 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/client.py @@ -0,0 +1,76 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Client for interacting with the Google Monitoring API.""" + +from oauth2client.client import GoogleCredentials + +from gcp._util import RequestException + +from .group import Group +from .metric import MetricDescriptor +from .resource import ResourceDescriptor +from .timeseries import Query + +SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] + + +# TODO(rimey): Add docstrings. +class Client(object): + + def __init__(self, project, credentials=None): + if credentials is None: + credentials = GoogleCredentials.get_application_default() + if credentials.create_scoped_required(): + credentials = credentials.create_scoped(SCOPES) + + self.project = project + self.credentials = credentials + + def query(self, + metric_type=Query.DEFAULT_METRIC_TYPE, + resource_type=None, + **kwargs): + return Query(self, metric_type, resource_type, **kwargs) + + def fetch_metric_descriptor(self, metric_type): + return MetricDescriptor.fetch(self, metric_type) + + def lookup_metric_descriptor(self, metric_type): + try: + return MetricDescriptor.fetch(self, metric_type) + except RequestException as e: + if e.status == 404: + return None + raise + + def list_metric_descriptors(self, filter=None): + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + return MetricDescriptor.list(self, filter) + + def fetch_resource_descriptor(self, resource_type): + return ResourceDescriptor.fetch(self, resource_type) + + def list_resource_descriptors(self, filter=None): + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + return ResourceDescriptor.list(self, filter) + + def group(self, group_id): + return Group(self, group_id) + + def fetch_group(self, group_id): + return Group.fetch(self, group_id) + + def list_groups(self): + return Group.list(self) diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/group.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/group.py new file mode 100644 index 000000000..735473663 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/group.py @@ -0,0 +1,244 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Groups for the Google Monitoring API.""" + +# Features intentionally omitted from this first version of the client library: +# - Creating, updating, and deleting groups. +# - Listing the groups for a resource. + +from .api import Api +from .resource import Resource + + +class Group(object): + """Groups define dynamic collections of monitored resources. + + Attributes: + id: The ID of the group. When creating a group, this field is ignored + and a new ID is created. + project_id: The project ID or number where the group is defined. + name: The fully qualified name of the group in the format + "projects//groups/". This is a read-only property. + If the group ID is not defined, the value is an empty string. + display_name: A user-assigned name for this group. + parent_name: The name (not ID) of the group's parent, if it has one. + filter: The filter string used to determine which monitored resources + belong to this group. + is_cluster: Whether the service should consider this group a cluster and + perform additional analysis on it. + """ + + def __init__(self, client, group_id=None): + """Initializes a Group instance. + + Args: + client: The Client to use. + group_id: An optional ID for the group. This is ignored when creating a + new group (not yet implemented). + """ + self._client = client + self.id = group_id + + self.display_name = '' + self.parent_name = '' + self.filter = '' + self.is_cluster = False + + def __repr__(self): + return ''.format(self.display_name) + + @property + def name(self): + if not self.id: + return '' + return 'projects/{}/groups/{}'.format(self.project_id, self.id) + + @property + def project_id(self): + return self._client.project + + @property + def _api(self): + return Api(self._client.credentials) + + @classmethod + def fetch(cls, client, group_id): + """Looks up a group by ID. + + Args: + client: The Client to use. + group_id: The ID of the group. + + Returns: + A Group instance with all attributes populated. + + Raises: + RequestException with status == 404 if the group does not exist. + """ + group = cls(client, group_id) + group.reload() + return group + + @classmethod + def list(cls, client): + """Lists all groups defined on the project ID. + + Args: + client: The Client to use. + + Returns: + A list of Group instances. + """ + return cls._list(client) + + @classmethod + def _list(cls, client, children_of_group=None, ancestors_of_group=None, + descendants_of_group=None): + """Lists all groups defined on the project ID. + + Args: + client: The Client to use. + children_of_group: The ID of the group whose children are to be listed. + ancestors_of_group: The ID of the group whose ancestors are to be listed. + descendants_of_group: The ID of the group whose descendants are to be + listed. + + Returns: + A list of Group instances. + """ + api = Api(client.credentials) + project_id = client.project + + def groups(): + page_token = None + while True: + list_info = api.groups_list(project_id, children_of_group, + ancestors_of_group, descendants_of_group, + page_token=page_token) + for info in list_info.get('group', []): + yield cls._from_dict(client, info) + + page_token = list_info.get('nextPageToken') + if not page_token: + break + + return list(groups()) + + @classmethod + def _from_dict(cls, client, info): + """Constructs a Group instance from the parsed JSON representation. + + The project IDs specified in client and info must match. + + Args: + client: The Client to use. + info: A dict parsed from the JSON wire-format representation. + + Returns: + A Group instance with all attributes populated. + """ + group = cls(client) + group._init_from_dict(info) + return group + + def reload(self): + """Fetches all the other attributes based on the project and group ID. + + Raises: + RequestException with status == 404 if the group does not exist. + """ + if not self.id: + raise ValueError('Group ID not specified.') + info = self._api.groups_get(self.id, self.project_id) + self._init_from_dict(info) + + def _init_from_dict(self, info): + """Initializes all attributes from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + """ + _, project_id, _, self.id = info['name'].split('/') + assert self.project_id == project_id + self.display_name = info.get('displayName', '') + self.parent_name = info.get('parentName', '') + self.filter = info.get('filter', '') + self.is_cluster = info.get('isCluster', False) + + def children(self): + """Lists all children of this group. + + Returns: + A list of Group instances. + """ + if not self.id: + raise ValueError('Group ID not specified.') + return self._list(self._client, children_of_group=self.id) + + def ancestors(self): + """Lists all ancestors of this group. + + Returns: + A list of Group instances. + """ + if not self.id: + raise ValueError('Group ID not specified.') + return self._list(self._client, ancestors_of_group=self.id) + + def descendants(self): + """Lists all descendants of this group. + + Returns: + A list of Group instances. + """ + if not self.id: + raise ValueError('Group ID not specified.') + return self._list(self._client, descendants_of_group=self.id) + + def members(self, filter=None, end_time=None, start_time=None): + """Lists all resources matching this group. + + Args: + filter: An optional filter string describing the members to be returned. + end_time: The end time (inclusive) of the time interval for which results + should be returned, as either a Python datetime object or a timestamp + string in RFC3339 UTC "Zulu" format. Only members that were part of + the group during the specified interval are included in the response. + start_time: The start time (exclusive) of the time interval for which + results should be returned, as either a datetime object or a + timestamp string. + + Returns: + A list of Resource instances. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + + if not self.id: + raise ValueError('Group ID not specified.') + + def resources(): + page_token = None + while True: + list_info = self._api.groups_members_list( + self.id, project_id=self.project_id, filter=filter, + end_time=end_time, start_time=start_time, page_token=page_token) + for info in list_info.get('members', []): + yield Resource._from_dict(info) + + page_token = list_info.get('nextPageToken') + if not page_token: + break + + return list(resources()) diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/label.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/label.py new file mode 100644 index 000000000..08168a950 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/label.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LabelDescriptor and related classes.""" + +import collections + + +class LabelDescriptor(collections.namedtuple('LabelDescriptor', + 'key value_type description')): + """Schema specification and documentation for a single label. + + Attributes: + key: The name of the label. + value_type: The type of the label. It must be one of: + ['STRING', 'BOOL', 'INT64']. + description: A human-readable description for the label. + """ + __slots__ = () + + @classmethod + def _from_dict(cls, info): + """Constructs a LabelDescriptor from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + + Returns: + A LabelDescriptor instance. + """ + return cls( + info.get('key', ''), + info.get('valueType', 'STRING'), + info.get('description', ''), + ) diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/metric.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/metric.py new file mode 100644 index 000000000..5a1e7d4c8 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/metric.py @@ -0,0 +1,147 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Metric Descriptors for the Google Monitoring API.""" + +# Features intentionally omitted from this first version of the client library: +# - Creating and deleting metric descriptors. + +import collections + +from .api import Api +from .label import LabelDescriptor + + +class MetricDescriptor( + collections.namedtuple('MetricDescriptor', + ('name type labels metric_kind value_type unit' + ' description display_name'))): + """Defines a metric type and its schema. + + MetricDescriptor instances are immutable. + + Attributes: + name: The "resource name" of the metric descriptor. For example: + "projects//metricDescriptors/" + type: The metric type including a DNS name prefix. For example: + "compute.googleapis.com/instance/cpu/utilization" + labels: A sequence of label descriptors specifying the labels used to + identify a specific instance of this metric. + metric_kind: The kind of measurement. It must be one of: + ['GAUGE', 'DELTA', 'CUMULATIVE']. + value_type: The value type of the metric. It must be one of: + ['BOOL', 'INT64', 'DOUBLE', 'STRING', 'DISTRIBUTION', 'MONEY']. + unit: A string specifying the unit in which the metric value is reported. + description: A detailed description of the metric. + display_name: A concise name for the metric. + """ + __slots__ = () + + @classmethod + def fetch(cls, client, metric_type): + """Looks up a metric descriptor by type. + + Args: + client: The Client to use. + metric_type: The metric type. + + Returns: + A MetricDescriptor instance. + + Raises: + RequestException with status == 404 if the metric descriptor + is not found. + """ + api = Api(client.credentials) + info = api.metric_descriptors_get(metric_type, client.project) + return cls._from_dict(info) + + @classmethod + def list(cls, client, filter=None): + """Lists all metric descriptors. + + Args: + client: The Client to use. + filter: An optional filter string describing the metric descriptors to + be returned. + + Returns: + A list of MetricDescriptor instances. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + + api = Api(client.credentials) + project_id = client.project + + def descriptors(): + page_token = None + while True: + list_info = api.metric_descriptors_list(project_id, filter=filter, + page_token=page_token) + for info in list_info.get('metricDescriptors', []): + yield cls._from_dict(info) + + page_token = list_info.get('nextPageToken') + if not page_token: + break + + return list(descriptors()) + + @classmethod + def _from_dict(cls, info): + """Constructs a MetricDescriptor from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + + Returns: + A MetricDescriptor instance. + """ + return cls( + type=info.get('type', ''), + name=info.get('name', ''), + description=info.get('description', ''), + display_name=info.get('displayName', ''), + labels=tuple(LabelDescriptor._from_dict(label) + for label in info.get('labels', [])), + metric_kind=info['metricKind'], + value_type=info['valueType'], + unit=info.get('unit', ''), + ) + + +class Metric(collections.namedtuple('Metric', 'type labels')): + """A specific metric identified by specifying values for all labels. + + Attributes: + type: The metric type. + labels: A dictionary of label values for all labels enumerated in the + associated metric descriptor. + """ + __slots__ = () + + @classmethod + def _from_dict(cls, info): + """Constructs a Metric from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + + Returns: + A Metric instance. + """ + return cls( + type=info.get('type', ''), + labels=info.get('labels', {}), + ) diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/resource.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/resource.py new file mode 100644 index 000000000..ff9d7cb0a --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/resource.py @@ -0,0 +1,135 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Resource Descriptors for the Google Monitoring API.""" + +import collections + +from .api import Api +from .label import LabelDescriptor + + +class ResourceDescriptor( + collections.namedtuple('ResourceDescriptor', + 'name type display_name description labels')): + """Defines a monitored resource type and its schema. + + ResourceDescriptor instances are immutable. + + Attributes: + name: The "resource name" of the monitored resource descriptor: + "projects//monitoredResourceDescriptors/" + type: The monitored resource type. + display_name: A concise name that might be displayed in user interfaces. + description: A detailed description that might be used in documentation. + labels: A sequence of label descriptors specifying the labels used to + identify a specific instance of this monitored resource. + """ + __slots__ = () + + @classmethod + def fetch(cls, client, resource_type): + """Looks up a resource descriptor by type. + + Args: + client: The Client to use. + resource_type: The resource type. + + Returns: + A ResourceDescriptor instance. + + Raises: + RequestException with status == 404 if the resource descriptor + is not found. + """ + api = Api(client.credentials) + info = api.monitored_resource_descriptors_get(resource_type, + client.project) + return cls._from_dict(info) + + @classmethod + def list(cls, client, filter=None): + """Lists all resource descriptors. + + Args: + client: The Client to use. + filter: An optional filter string describing the resource descriptors to + be returned. + + Returns: + A list of ResourceDescriptor instances. + """ + # Allow "filter" as a parameter name: pylint: disable=redefined-builtin + + api = Api(client.credentials) + project_id = client.project + + def descriptors(): + page_token = None + while True: + list_info = api.monitored_resource_descriptors_list( + project_id, filter=filter, page_token=page_token) + for info in list_info.get('resourceDescriptors', []): + yield cls._from_dict(info) + + page_token = list_info.get('nextPageToken') + if not page_token: + break + + return list(descriptors()) + + @classmethod + def _from_dict(cls, info): + """Constructs a ResourceDescriptor from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + + Returns: + A ResourceDescriptor instance. + """ + return cls( + name=info.get('name', ''), + type=info.get('type', ''), + display_name=info.get('displayName', ''), + description=info.get('description', ''), + labels=tuple(LabelDescriptor._from_dict(label) + for label in info.get('labels', [])), + ) + + +class Resource(collections.namedtuple('Resource', 'type labels')): + """A monitored resource identified by specifying values for all labels. + + Attributes: + type: The resource type. + labels: A dictionary of label values for all labels enumerated in the + associated resource descriptor. + """ + __slots__ = () + + @classmethod + def _from_dict(cls, info): + """Constructs a Resource from the parsed JSON representation. + + Args: + info: A dict parsed from the JSON wire-format representation. + + Returns: + A Resource instance. + """ + return cls( + type=info.get('type', ''), + labels=info.get('labels', {}), + ) diff --git a/sources/lib/api/gcp/stackdriver/monitoring/_impl/timeseries.py b/sources/lib/api/gcp/stackdriver/monitoring/_impl/timeseries.py new file mode 100644 index 000000000..cd748b027 --- /dev/null +++ b/sources/lib/api/gcp/stackdriver/monitoring/_impl/timeseries.py @@ -0,0 +1,585 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Time series in the Google Monitoring API.""" + +# Features intentionally omitted from this first version of the client library: +# - Creating time series. +# - Natural representation for distribution values. + +import collections +import copy +import datetime +import itertools + +from .api import Api +from .metric import Metric +from .resource import Resource + +TOP_RESOURCE_LABELS = [ + 'project_id', + 'aws_account', + 'location', + 'region', + 'zone', +] + + +class Query(object): + """Query object for listing time series. + + Attributes: + metric_type: The metric type name. + filter: The filter string as constructed from the metric type, resource + type, and selectors for the group ID, monitored projects, resource + labels, and metric labels. + """ + + DEFAULT_METRIC_TYPE = 'compute.googleapis.com/instance/cpu/utilization' + + def __init__(self, client, + metric_type=DEFAULT_METRIC_TYPE, + end_time=None, start_time=None, + days=0, hours=0, minutes=0): + """Initializes the core query parameters. + + Args: + client: The Client to use. + metric_type: The metric type name. The default value is + "compute.googleapis.com/instance/cpu/utilization", but + please note that this default value is provided only for + demonstration purposes and is subject to change. + end_time: The end time (inclusive) of the time interval for which results + should be returned, as a Python datetime object. The default is the + start of the current minute. If the days/hours/minutes parameters are + not used, the end time can alternatively be provided as a timestamp + string in RFC3339 UTC "Zulu" format. + start_time: An optional start time (exclusive) of the time interval for + which results should be returned, as either a datetime object or a + timestamp string. If omitted and no non-zero duration is specified, + the interval is a point in time. If any of days, hours, or minutes + is non-zero, these are combined and subtracted from the end time to + determine the start time. + days: The number of days in the time interval. + hours: The number of hours in the time interval. + minutes: The number of minutes in the time interval. + """ + if end_time is None: + end_time = datetime.datetime.utcnow().replace(second=0, microsecond=0) + + if days or hours or minutes: + if start_time is not None: + raise ValueError('Duration and start time both specified.') + start_time = end_time - datetime.timedelta(days=days, + hours=hours, + minutes=minutes) + + self._client = client + self._end_time = end_time + self._start_time = start_time + self._filter = _Filter(metric_type) + + self._per_series_aligner = None + self._alignment_period_seconds = None + self._cross_series_reducer = None + self._group_by_fields = () + + def __iter__(self): + return self.iter() + + @property + def metric_type(self): + return self._filter.metric_type + + @property + def filter(self): + return str(self._filter) + + def select_group(self, group_id=None, display_name=None): + """Copies the query and adds filtering by group. + + Exactly one of group_id and display_name must be specified. + + Args: + group_id: The ID of a group to filter by. + display_name: The display name of a group to filter by. If this is + specified, information about the available groups is retrieved + from the service to allow the group ID to be determined. + + Returns: + The new Query object. + + Raises: + ValueError: The given display name did not match exactly one group. + """ + if not ((group_id is None) ^ (display_name is None)): + raise ValueError( + 'Exactly one of "group_id" and "display_name" must be specified.') + + if display_name is not None: + matching_groups = [g for g in self._client.list_groups() + if g.display_name == display_name] + if len(matching_groups) != 1: + raise ValueError('%d groups have the display_name %r.' % ( + len(matching_groups), display_name)) + group_id = matching_groups[0].id + + new_query = self.copy() + new_query._filter.group_id = group_id + return new_query + + def select_projects(self, *args): + """Copies the query and adds filtering by monitored projects. + + Examples: + + query = query.select_projects('project-1') + query = query.select_projects('project-a', 'project-b', 'project-c') + + Args: + *args: Project IDs limiting the resources to be included in the query. + + Returns: + The new Query object. + """ + new_query = self.copy() + new_query._filter.projects = args + return new_query + + def select_resources(self, *args, **kwargs): + """Copies the query and adds filtering by resource labels. + + Examples: + + query = query.select_resources(zone='us-central1-a') + query = query.select_resources(zone_prefix='europe-') + query = query.select_resources(resource_type='gce_instance') + + A keyword argument