Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cloudwatch Target #184

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Expand Up @@ -7,6 +7,7 @@ Next release

Changes
-------
* Add CloudWatch target

TBA

Expand Down
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -103,6 +103,7 @@ percentile latency:
There is a fair bit of repetition here, but once you figure out what works for
your needs, you can factor that out.
See `our Weave-specific customizations <grafanalib/weave.py>`_ for inspiration.
You will find other examples in the `docs folder <docs/>`_. For `elasticsearch <docs/example-elasticsearch.dashboard.py>`_ and for `cloudwatch <docs/example-cloudwatch.dashboard.py>`_.

Generating dashboards
=====================
Expand Down
37 changes: 37 additions & 0 deletions docs/example-cloudwatch.dashboard.py
@@ -0,0 +1,37 @@
"""
This is an exemplary Grafana board that uses a Cloudwatch datasource.

The graph shows the Average CPU utilization from an ASG named "frontend-asg"
"""

from grafanalib.core import *
from grafanalib.cloudwatch import CloudwatchTarget


dashboard = Dashboard(
title="Clowdwatch Stats",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo here but it's inconsequential

rows=[
Row(panels=[
Graph(
title="Cloudwatch ASG",
dataSource='Cloudwatch',
targets=[
CloudwatchTarget(
dimensions={
"AutoScalingGroupName": "frontend-asg",
},
metricName="CPUUtilization",
namespace="AWS/EC2",
region="eu-west-1",
statistics=["Average"],
checkParams=True,
),
],
yAxes=G.YAxes(
YAxis(format=PERCENT_UNIT_FORMAT),
YAxis(format=SHORT_FORMAT),
),
),
]),
],
).auto_panel_ids()
194 changes: 194 additions & 0 deletions grafanalib/cloudwatch.py
@@ -0,0 +1,194 @@
"""Helpers to create Cloudwatch-specific Grafana queries."""

import attr
from attr.validators import instance_of


@attr.s
class CloudwatchTarget(object):
"""
Generates Cloudwatch target JSON structure.

Grafana docs on using Cloudwatch:
https://grafana.com/docs/features/datasources/cloudwatch/
AWS docs on Cloudwatch metrics:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html

:param alias: legend alias
:param dimensions: Cloudwatch dimensions dict
:param metricName: Cloudwatch metric nam
:param namespace: CLoudwatch namespace
:param period:Cloudwatch data period
:param region: CLoudwatch region
:param refId: target reference id
:param statistics: Cloudwatch mathematic statistic
:param checkParams: If false, disable cloudwatch checks
"""
alias = attr.ib(default="")
dimensions = attr.ib(default={}, validator=instance_of(dict))
metricName = attr.ib(default="")
namespace = attr.ib(default="")
period = attr.ib(default="")
region = attr.ib(default="default")
refId = attr.ib(default="")
statistics = attr.ib(default=["Average"], validator=instance_of(list))
checkParams = attr.ib(default=True, validator=instance_of(bool))

def to_json_data(self):
if self.checkParams:
self.__checkParameters()

return {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dict.update returns None (https://docs.python.org/3/library/stdtypes.html#dict.update) which means that the target json ends up being null.

"alias": self.alias,
"dimensions": self.dimensions,
"expression": "",
"highResolution": False,
"id": "",
"metricName": self.metricName,
"namespace": self.namespace,
"period": self.period,
"refId": self.refId,
"region": self.region,
"returnData": False,
"statistics": self.statistics
}.update(self.dimensions)

def __checkParameters(self):
self.__checDimensions()
self.__checkNamespace()
self.__checkRegion()

def __checkDimensions(self):
if self.dimensions == {}:
raise Exception(
'You need to define a valid non empty dimensions dict variable'
)

def __checkNamespace(self):
if not (self.namespace in validNamespaces):
raise Exception(
'"{}" is not valid Cloudwatch namespace'.format(
self.namespace)
)

def __checkRegion(self):
if not (self.region in validRegions):
raise Exception(
'"{}" is not valid AWS region'.format(
self.region)
)


validNamespaces = [
"AWS/ApiGateway",
"AWS/AppStream",
"AWS/AppSync",
"AWS/Athena",
"AWS/Billing",
"AWS/ACMPrivateCA",
"AWS/CloudFront",
"AWS/CloudHSM",
"AWS/CloudSearch",
"AWS/Logs",
"AWS/CodeBuild",
"AWS/Cognito",
"AWS/Connect",
"AWS/DataSync",
"AWS/DMS",
"AWS/DX",
"AWS/DocDB",
"AWS/DynamoDB",
"AWS/EC2",
"AWS/EC2Spot",
"AWS/AutoScaling",
"AWS/ElasticBeanstalk",
"AWS/EBS",
"AWS/ECS",
"AWS/EFS",
"AWS/ElasticInference",
"AWS/ApplicationELB",
"AWS/ELB",
"AWS/NetworkELB",
"AWS/ElasticTranscoder",
"AWS/ElastiCache",
"AWS/ElastiCache",
"AWS/ES",
"AWS/ElasticMapReduce",
"AWS/MediaConnect",
"AWS/MediaConvert",
"AWS/MediaPackage",
"AWS/MediaTailor",
"AWS/Events",
"AWS/FSx",
"AWS/FSx",
"AWS/GameLift",
"AWS/Glue",
"AWS/Inspector",
"AWS/IoT",
"AWS/IoTAnalytics",
"AWS/ThingsGraph",
"AWS/KMS",
"AWS/KinesisAnalytics",
"AWS/Firehose",
"AWS/Kinesis",
"AWS/KinesisVideo",
"AWS/Lambda",
"AWS/Lex",
"AWS/ML",
"AWS/Kafka",
"AWS/AmazonMQ",
"AWS/Neptune",
"AWS/OpsWorks",
"AWS/Polly",
"AWS/QLDB",
"AWS/Redshift",
"AWS/RDS",
"AWS/Robomaker",
"AWS/Route53",
"AWS/SageMaker",
"AWS/SDKMetrics",
"AWS/DDoSProtection",
"AWS/SES",
"AWS/SNS",
"AWS/SQS",
"AWS/S3",
"AWS/SWF",
"AWS/States",
"AWS/StorageGateway",
"AWS/Textract",
"AWS/Transfer",
"AWS/Translate",
"AWS/TrustedAdvisor",
"AWS/NATGateway",
"AWS/TransitGateway",
"AWS/VPN",
"AWS/WorkMail",
"AWS/WorkSpaces",
]

validRegions = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use something like https://stackoverflow.com/a/38451512 here?

"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"ap-east-1",
"ap-south-1",
"ap-southeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-northeast-1",
"ap-southeast-2",
"ca-central-1",
"cn-north-1",
"cn-northwest-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"eu-north-1",
"me-south-1",
"sa-east-1",
"us-gov-east-1",
"us-gov-west-1",
"default",
]
26 changes: 26 additions & 0 deletions grafanalib/core.py
Expand Up @@ -329,6 +329,32 @@ def to_json_data(self):
}


@attr.s
class InfluxDBTarget(object):
"""
Metric to show.

:param target: Graphite way to select data
"""

query = attr.ib(default="")
format = attr.ib(default=TIME_SERIES_TARGET_FORMAT)
alias = attr.ib(default="")
measurement = attr.ib(default="")
rawQuery = True
refId = attr.ib(default="")

def to_json_data(self):
return {
'query': self.query,
'resultFormat': self.format,
'alias': self.alias,
'measurement': self.measurement,
'rawQuery': self.rawQuery,
'refId': self.refId
}


@attr.s
class Tooltip(object):

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -7,7 +7,7 @@
envlist = py27, py35, py37

[testenv]
commands = pytest --junitxml=test-results/junit-{envname}.xml
commands = pytest -o junit_family=xunit2 --junitxml=test-results/junit-{envname}.xml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating to new junit_family, related with:
pytest-dev/pytest#6179

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this a separate PR maybe? It definitely makes sense:

.tox/py37/lib/python3.7/site-packages/_pytest/junitxml.py:436
  /home/daniel/dev/grafanalib/.tox/py37/lib/python3.7/site-packages/_pytest/junitxml.py:436: PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0.
  Add 'junit_family=xunit1' to your pytest.ini file to keep the current format in future versions of pytest and silence this warning.
    _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to #236

deps =
pytest

Expand Down