├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── grafannotate
├── __init__.py
├── annotation.py
└── cli.py
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── test_annotation.py
└── test_cli.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # Jupyter Notebook
74 | .ipynb_checkpoints
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # Environments
83 | .env
84 | .venv
85 | env/
86 | venv/
87 | ENV/
88 | env.bak/
89 | venv.bak/
90 |
91 | # Spyder project settings
92 | .spyderproject
93 | .spyproject
94 |
95 | # Rope project settings
96 | .ropeproject
97 |
98 | # mkdocs documentation
99 | /site
100 |
101 | # mypy
102 | .mypy_cache/
103 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | dist: xenial
3 | language: python
4 | python:
5 | - 3.6
6 | - 3.7
7 | - 3.8
8 | matrix:
9 | include:
10 | - python: 3.8
11 | env: TOXENV=flake8
12 | install:
13 | - pip install tox-travis
14 | - pip install coveralls
15 | script:
16 | - tox
17 | after_success: coveralls
18 | deploy:
19 | provider: pypi
20 | distributions: sdist bdist_wheel
21 | skip_existing: true
22 | user: timbirk
23 | password:
24 | secure: H1MEew3qePw0SZ5TrqVTd9agYo7t+oOOBSkyXhVx2OLwQfrB0P+6EBm4I1l/iSVdKTkoCMkB1Kprqc9pN+1uHlbVw/35WNa071DfddwoQMBWRmK6zstDEsVyJ1XQEVTB3CIhBnRzZti1qLWmY3RmKFYHdXuvJSUNQPXtgy7rPY0yhdf0fbuNPiFykD8QJgt3NSjV9jOUz0mru9XiPxlFh/ehSXb0ZeIQUK1x/4ywR+p8n6ve4U3LY1WqJf/641ysrgFqWwuPjf5fj7AbrnKj2slHq6WMNulWdo/oDe1l4/ZjKcieRu5amLv4uZoJcq4x7FpQSU/hJv75kFYXJxXhnVOiLwVosfaKmvya3K6WwaFqW13KW+GBijXxHSchFNUhd8wnWPxgRl40OT9dvYB01HaB+xQQeBuID65mF9/zBA/fBGmlTrmWVT8z+UDqB7r5mncCLkb7PhlQQNhmNIASWt5fTf5A7aII2zEQ/h/q/EABJaFwaMWftaYk0ZBIDd0Ms4UJXmyZUxmB2XPe8AdYqJIHCCUdGlUZmEQCiM+Pd/joa/Ska2UBR/bnnxUH1FAOOERqaXk+8qlTkAYv3fHF7kvDKGA7IUSeMa9wQjR61Tk4Ifd5tuHfVT1fkZnGvBDF0VybFoy5oQmnURBr/izC7iY7IVFPb4NW7eJWsIjkTZA=
25 | on:
26 | tags: true
27 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 DevOps Makers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.py
2 | include README.md
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `grafannotate`
2 |
3 | [](https://travis-ci.org/devopsmakers/python-grafannotate)
4 | [](https://coveralls.io/github/devopsmakers/python-grafannotate?branch=master)
5 | [](https://badge.fury.io/py/grafannotate)
6 |
7 | A CLI tool to send Grafana annotations to various destinations.
8 |
9 | ## Installation
10 | ```
11 | pip install grafannotate
12 | ```
13 |
14 | ## Usage
15 |
16 | ```
17 | grafannotate --help
18 | Usage: grafannotate [OPTIONS]
19 |
20 | Send Grafana annotations to various endpoints
21 |
22 | Options:
23 | -u, --uri TEXT URI to send annotation to. Default:
24 | "http://localhost:3000/api/annotations".
25 | -k, --api-key TEXT Grafana API key to pass in Authorisation header
26 | -T, --title TEXT Event title. Default: "event".
27 | -t, --tag TEXT Event tags (can be used multiple times).
28 | -d, --description TEXT Event description body. Optional.
29 | -s, --start INTEGER Start timestamp (unix secs). Default: current
30 | timestamp.
31 | -e, --end INTEGER End timestamp (unix secs). Optional.
32 | --help Show this message and exit.
33 | ```
34 |
35 | ### Examples
36 | - Send an annotation to Grafana API for current time
37 | ```
38 | grafannotate --uri http://user:password@grafana:3000/api/annotations --tag my_tag --title "Event Title"
39 | ```
40 |
41 | - Send an annotation to Grafana API for a time region
42 | ```
43 | grafannotate --uri http://user:password@grafana:3000/api/annotations --tag my_tag --title "Event Title" --start 1557222057 --end 1557222259
44 | ```
45 |
46 | - Send an annotation to Grafana API with an extended description
47 | ```
48 | grafannotate --uri http://user:password@grafana:3000/api/annotations --tag my_tag --title "Event Title" --description "Some longer description
with newlines
and links"
49 | ```
50 |
51 | - Pipe output to an annotation description
52 | ```
53 | START_TIME=`date +%s`
54 | command_with_output | grafannotate --uri http://user:password@grafana:3000/api/annotations --tag my_tag --title "Event Title" --start $START_TIME
55 | ```
56 |
57 | - Send an annotation to Grafana API using Authorization header
58 | ```
59 | GRAFANA_API_TOKEN="some_generated_api_token"
60 | grafannotate --uri http://grafana:3000/api/annotations --tag my_tag --title "Event Title" --api-key $GRAFANA_API_TOKEN
61 | ```
62 |
--------------------------------------------------------------------------------
/grafannotate/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devopsmakers/python-grafannotate/ef09dc74dc97b30a8bcb2eb362f6346d539b918a/grafannotate/__init__.py
--------------------------------------------------------------------------------
/grafannotate/annotation.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import time
3 | from influxdb import InfluxDBClient
4 | from urllib.parse import urlparse
5 |
6 | CURRENT_TIMESTAMP = int(time.time())
7 |
8 |
9 | class Annotation:
10 | """
11 | An annotation that we want to create
12 | """
13 | def __init__(self, title, tags, description='', start=CURRENT_TIMESTAMP, end=CURRENT_TIMESTAMP):
14 | if len(tags) == 0:
15 | raise ValueError('Annotations must have at least one tag.')
16 |
17 | if end < start:
18 | raise ValueError('Annotation end time cannot be before start time.')
19 |
20 | self.title = title
21 | self.tags = tags
22 | self.description = description
23 | self.start = start
24 | self.end = end
25 |
26 | def web(self):
27 | """
28 | Returns an annotation object formatted for grafana API
29 | """
30 | annotation_event = {}
31 | annotation_event['text'] = '%s\n\n%s' % (self.title, self.description)
32 | annotation_event['tags'] = self.tags
33 | annotation_event['time'] = int(round(self.start * 1000))
34 | if self.start < self.end:
35 | annotation_event['isRegion'] = True
36 | annotation_event['timeEnd'] = int(round(self.end * 1000))
37 | return annotation_event
38 |
39 | def influxdb(self):
40 | """
41 | Returns an annotation object formatted for InfluxDB
42 | """
43 | tags_field = ';'.join(self.tags)
44 | annotation_event = {}
45 | annotation_event['measurement'] = 'events'
46 | annotation_event['fields'] = {
47 | 'title': self.title,
48 | 'text': self.description,
49 | 'tags': tags_field
50 | }
51 | return [annotation_event]
52 |
53 | def send(self, url, api_key):
54 | """
55 | Send the annotation to a destination based on url
56 | """
57 | url_parts = urlparse(url)
58 | if 'http' in url_parts.scheme:
59 | return self.send_to_web(url_parts, api_key)
60 | elif 'influx' in url_parts.scheme:
61 | return self.send_to_influxdb(url_parts)
62 | else:
63 | raise NotImplementedError('Scheme %s not recognised in uri %s' %
64 | (url_parts.scheme, url))
65 |
66 | def send_to_web(self, url_parts, api_key):
67 | """
68 | POST event to an endpoint in Grafana Annotations API format
69 | """
70 | event_data = self.web()
71 | result_data = {'event_data': event_data}
72 | url = url_parts.geturl()
73 | auth_tuple = None
74 | req_headers = {}
75 | if api_key is not None:
76 | req_headers['Authorization'] = "Bearer %s" % api_key
77 |
78 | if url_parts.username and url_parts.password:
79 | auth_tuple = (url_parts.username, url_parts.password)
80 | url_host_port = url_parts.netloc.split('@')[1]
81 | url = '%s://%s%s' % (url_parts.scheme, url_host_port, url_parts.path)
82 |
83 | post_result = requests.post(url, json=event_data, auth=auth_tuple, headers=req_headers, timeout=5)
84 |
85 | if post_result.status_code > 299:
86 | raise Exception('Received %s response, sending event failed' % post_result.status_code)
87 |
88 | if 'id' in post_result.json():
89 | result_data['id'] = post_result.json()['id']
90 |
91 | if 'message' in post_result.json():
92 | result_data['message'] = post_result.json()['message']
93 |
94 | return result_data
95 |
96 | def send_to_influxdb(self, url_parts):
97 | event_data = self.influxdb()
98 | result_data = {'event_data': event_data}
99 | client = InfluxDBClient(url_parts.hostname,
100 | url_parts.port or 8086,
101 | url_parts.username or '',
102 | url_parts.password or '',
103 | url_parts.path.replace('/', '', 1) or 'events')
104 |
105 | if client.write_points(event_data):
106 | result_data['message'] = 'Annotation added'
107 | else:
108 | result_data['message'] = 'Annotation failed'
109 |
110 | return result_data
111 |
--------------------------------------------------------------------------------
/grafannotate/cli.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import click
3 | import logging
4 | import time
5 |
6 | from grafannotate.annotation import Annotation
7 |
8 | CURRENT_TIMESTAMP = int(time.time())
9 |
10 |
11 | @click.command()
12 | @click.option('-u', '--uri', 'annotate_uri',
13 | default='http://localhost:3000/api/annotations',
14 | help='URI to send annotation to. Default: "http://localhost:3000/api/annotations".')
15 | @click.option('-k', '--api-key', 'api_key', default=None,
16 | help='Grafana API key to pass in Authorisation header')
17 | @click.option('-T', '--title', 'title', default='event', help='Event title. Default: "event".')
18 | @click.option('-t', '--tag', 'tags', multiple=True, help='Event tags (can be used multiple times).')
19 | @click.option('-d', '--description', 'description', help='Event description body. Optional.')
20 | @click.option('-s', '--start', 'start_time', default=CURRENT_TIMESTAMP,
21 | help='Start timestamp (unix secs). Default: current timestamp.')
22 | @click.option('-e', '--end', 'end_time', default=CURRENT_TIMESTAMP,
23 | help='End timestamp (unix secs). Optional.')
24 | @click.option('--debug/--no-debug', default=False,
25 | help='Set debug logging on')
26 | def main(annotate_uri, api_key, title, tags, description, start_time, end_time, debug):
27 | """
28 | Send Grafana annotations to various endpoints
29 | """
30 |
31 | log_level = logging.INFO
32 | if debug:
33 | log_level = logging.DEBUG
34 |
35 | logging.basicConfig(format=' [%(levelname)s] %(message)s', level=log_level)
36 |
37 | try:
38 | if description is None:
39 | if not sys.stdin.isatty():
40 | description = "".join([line for line in iter(sys.stdin.readline, '')])
41 | else:
42 | description = ""
43 |
44 | this_annotation = Annotation(title, tags, description, start_time, end_time)
45 | result = this_annotation.send(annotate_uri, api_key)
46 |
47 | if result['event_data']:
48 | logging.debug(result['event_data'])
49 | if result['message']:
50 | logging.info(result['message'])
51 |
52 | except Exception as e:
53 | logging.exception(e)
54 | """
55 | We could exit 1 here but we really don't want to cause a job to
56 | fail just because we couldn't send an event.
57 | """
58 |
59 | sys.exit(0)
60 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # python-grafannotate
2 | # ---------------
3 | # A CLI tool to add annotations to Grafana
4 | #
5 | # Author: Tim Birkett
6 | # Website: https://github.com/devopsmakers/python-grafannotate
7 | # License: MIT License (see LICENSE file)
8 |
9 | import codecs
10 | from setuptools import find_packages, setup
11 |
12 | dependencies = [
13 | 'click==7.0',
14 | 'requests==2.22.0',
15 | 'influxdb==5.2.3'
16 | ]
17 |
18 | setup(
19 | name='grafannotate',
20 | version='0.3.0',
21 | url='https://github.com/devopsmakers/python-grafannotate',
22 | license='MIT',
23 | author='Tim Birkett',
24 | author_email='tim.birkett@devopsmakers.com',
25 | description='Send annotations to Grafana',
26 | long_description=codecs.open('README.md', encoding='utf-8').read(),
27 | long_description_content_type='text/markdown',
28 | packages=find_packages(exclude=['tests']),
29 | include_package_data=True,
30 | zip_safe=False,
31 | platforms='any',
32 | install_requires=dependencies,
33 | entry_points={
34 | 'console_scripts': [
35 | 'grafannotate = grafannotate.cli:main',
36 | ],
37 | },
38 | classifiers=[
39 | # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers
40 | 'Development Status :: 5 - Production/Stable',
41 | 'Topic :: Utilities',
42 | 'Environment :: Console',
43 | 'Intended Audience :: Information Technology',
44 | 'Intended Audience :: System Administrators',
45 | 'License :: OSI Approved :: MIT License',
46 | 'Operating System :: OS Independent',
47 | 'Programming Language :: Python',
48 | 'Programming Language :: Python :: 3',
49 | ]
50 | )
51 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devopsmakers/python-grafannotate/ef09dc74dc97b30a8bcb2eb362f6346d539b918a/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_annotation.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import time
3 | import mock
4 | import requests_mock
5 |
6 | from grafannotate.annotation import Annotation
7 |
8 | CURRENT_TIMESTAMP = int(time.time())
9 |
10 |
11 | def test_annotation_without_values():
12 | with pytest.raises(TypeError):
13 | Annotation()
14 |
15 |
16 | def test_annotation_without_tags():
17 | with pytest.raises(ValueError, match='must have at least one tag.'):
18 | Annotation('event', [], '')
19 |
20 |
21 | def test_annotation_without_times():
22 | test_annotation = Annotation('event', ['events'], '')
23 | assert test_annotation.start == test_annotation.end
24 |
25 |
26 | def test_annotation_with_bad_end_time():
27 | with pytest.raises(ValueError, match='end time cannot be before start time.'):
28 | Annotation('event', ['test'], '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - 300)
29 |
30 |
31 | def test_annotation_web():
32 | start_time = CURRENT_TIMESTAMP
33 | end_time = CURRENT_TIMESTAMP + 600
34 | test_annotation = Annotation('event', ['test'], 'testing', start_time, end_time)
35 | test_web_annotation = test_annotation.web()
36 | assert test_web_annotation['time'] == int(round(start_time * 1000))
37 | assert test_web_annotation['timeEnd'] == int(round(end_time * 1000))
38 | assert test_web_annotation['isRegion'] is True
39 | assert test_web_annotation['tags'] == ['test']
40 | assert test_web_annotation['text'] == 'event\n\ntesting'
41 |
42 |
43 | def test_annotation_influxdb():
44 | start_time = CURRENT_TIMESTAMP
45 | end_time = CURRENT_TIMESTAMP + 600
46 | test_annotation = Annotation('event', ['test', 'influx'], 'testing', start_time, end_time)
47 | test_influxdb_annotation = test_annotation.influxdb()
48 | assert test_influxdb_annotation[0]['measurement'] == 'events'
49 | assert test_influxdb_annotation[0]['fields'] == {
50 | 'tags': 'test;influx',
51 | 'text': 'testing',
52 | 'title': 'event'
53 | }
54 |
55 |
56 | def test_annotation_fail_to_send_to_web():
57 | url = "http://user:pass@localhost"
58 | test_annotation = Annotation('event', ['test'], 'testing')
59 | with pytest.raises(Exception, match='NewConnectionError'):
60 | test_annotation.send(url, None)
61 |
62 |
63 | def test_annotation_send_to_web():
64 | url = "http://localhost:3000/api/annotations"
65 | with requests_mock.Mocker() as m:
66 | m.register_uri(
67 | requests_mock.POST,
68 | url,
69 | status_code=200,
70 | json={'message': 'Annotation added', 'id': '12345'}
71 | )
72 | test_annotation = Annotation('event', ['test'], 'testing', 1559332960, 1559332970)
73 | assert test_annotation.send(url, None) == {
74 | 'event_data': {
75 | 'isRegion': True,
76 | 'tags': ['test'],
77 | 'text': 'event\n\ntesting',
78 | 'time': 1559332960000,
79 | 'timeEnd': 1559332970000
80 | },
81 | 'id': '12345',
82 | 'message': 'Annotation added'
83 | }
84 |
85 |
86 | def test_annotation_send_to_web_with_api_key():
87 | url = "http://localhost:3000/api/annotations"
88 | api_key = "307c1ac4-4e7c-4eb4-a56f-3547eeff0e4b"
89 | with requests_mock.Mocker() as m:
90 | m.register_uri(
91 | requests_mock.POST,
92 | url,
93 | request_headers={'Authorization': "Bearer %s" % api_key},
94 | status_code=200,
95 | json={'message': 'Annotation added'}
96 | )
97 | test_annotation = Annotation('event', ['test'], 'testing', 1559332960, 1559332970)
98 | assert test_annotation.send(url, api_key) == {
99 | 'event_data': {
100 | 'isRegion': True,
101 | 'tags': ['test'],
102 | 'text': 'event\n\ntesting',
103 | 'time': 1559332960000,
104 | 'timeEnd': 1559332970000
105 | },
106 | 'message': 'Annotation added'
107 | }
108 |
109 |
110 | def test_annotation_error_sending_to_web():
111 | url = "http://localhost:3000/api/annotations"
112 | with requests_mock.Mocker() as m:
113 | m.register_uri(
114 | requests_mock.POST,
115 | url,
116 | status_code=400
117 | )
118 | test_annotation = Annotation('event', ['test'], 'testing', 1559332960, 1559332960)
119 | with pytest.raises(Exception, match='Received 400 response, sending event failed'):
120 | test_annotation.send(url, None)
121 |
122 |
123 | def test_annotation_fail_to_send_to_influxdb():
124 | url = "influx://user:pass@localhost"
125 | test_annotation = Annotation('event', ['test'], 'testing')
126 | with pytest.raises(Exception, match='Failed to establish a new connection'):
127 | test_annotation.send(url, None)
128 |
129 |
130 | @mock.patch('influxdb.InfluxDBClient.write_points')
131 | def test_annotation_send_to_influxdb(mock_write_points):
132 | url = "influx://user:pass@localhost"
133 | test_annotation = Annotation('event', ['test'], 'testing')
134 | mock_write_points.return_value = True
135 | assert test_annotation.send(url, None) == {
136 | 'event_data': [{
137 | 'fields': {
138 | 'tags': 'test',
139 | 'text': 'testing',
140 | 'title': 'event'
141 | },
142 | 'measurement': 'events'
143 | }],
144 | 'message': 'Annotation added'
145 | }
146 |
147 |
148 | @mock.patch('influxdb.InfluxDBClient.write_points')
149 | def test_annotation_send_to_influxdb_fail(mock_write_points):
150 | url = "influx://user:pass@localhost"
151 | test_annotation = Annotation('event', ['test'], 'testing')
152 | mock_write_points.return_value = False
153 | assert test_annotation.send(url, None) == {
154 | 'event_data': [{
155 | 'fields': {
156 | 'tags': 'test',
157 | 'text': 'testing',
158 | 'title': 'event'
159 | },
160 | 'measurement': 'events'
161 | }],
162 | 'message': 'Annotation failed'
163 | }
164 |
165 |
166 | def test_annotation_send_bad_url():
167 | url = "s3://user:pass@localhost"
168 | test_annotation = Annotation('event', ['test'], 'testing')
169 | with pytest.raises(NotImplementedError, match='Scheme s3 not recognised'):
170 | test_annotation.send(url, None)
171 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import time
3 | import mock
4 |
5 | from click.testing import CliRunner
6 | from grafannotate import cli
7 |
8 | CURRENT_TIMESTAMP = int(time.time())
9 |
10 |
11 | @pytest.fixture
12 | def runner():
13 | return CliRunner()
14 |
15 |
16 | def test_cli(runner, caplog):
17 | result = runner.invoke(cli.main)
18 | assert result.exit_code == 0
19 | assert 'must have at least one tag' in caplog.text
20 |
21 |
22 | def test_cli_with_tag(runner, caplog):
23 | result = runner.invoke(cli.main, ['--tag', 'event'])
24 | assert result.exit_code == 0
25 | assert 'NewConnectionError' in caplog.text
26 |
27 |
28 | @mock.patch('grafannotate.cli.Annotation.send', autospec=True)
29 | def test_cli_with_debug_mock(mock_send, runner, caplog):
30 | return_data = {
31 | 'event_data': {
32 | 'isRegion': True,
33 | 'tags': ['test'],
34 | 'text': 'event\n\ntesting',
35 | 'time': 1559332960000,
36 | 'timeEnd': 1559332970000
37 | },
38 | 'id': '12345',
39 | 'message': 'Annotation added'
40 | }
41 | mock_send.return_value = return_data
42 | caplog.set_level('DEBUG')
43 | result = runner.invoke(cli.main, ['--tag', 'event'])
44 | assert result.exit_code == 0
45 | assert return_data['message'] in caplog.text
46 | assert str(return_data['event_data']) in caplog.text
47 |
48 |
49 | def test_cli_with_debug(runner, caplog):
50 | result = runner.invoke(cli.main, ['--tag', 'event', '--debug'])
51 | assert result.exit_code == 0
52 | assert 'NewConnectionError' in caplog.text
53 |
54 |
55 | def test_cli_with_bad_end_time(runner, caplog):
56 | result = runner.invoke(cli.main, ['--tag', 'event', '--end', 0])
57 | assert result.exit_code == 0
58 | assert 'end time cannot be before start time' in caplog.text
59 |
60 |
61 | def test_cli_with_bad_uri(runner, caplog):
62 | result = runner.invoke(cli.main, ['--tag', 'event', '--uri', 'blob://localhost'])
63 | assert result.exit_code == 0
64 | assert 'Scheme blob not recognised' in caplog.text
65 |
66 |
67 | def test_cli_with_user_pass(runner, caplog):
68 | result = runner.invoke(cli.main, ['--tag', 'event', '--uri', 'http://user:pass@localhost'])
69 | assert result.exit_code == 0
70 | assert 'NewConnectionError' in caplog.text
71 |
72 |
73 | def test_cli_with_api_key(runner, caplog):
74 | result = runner.invoke(cli.main, ['--tag', 'event', '--uri', 'http://localhost', '--api-key', 'aTestKey'])
75 | assert result.exit_code == 0
76 | assert 'NewConnectionError' in caplog.text
77 |
78 |
79 | def test_cli_with_end_time(runner, caplog):
80 | result = runner.invoke(cli.main, ['--tag', 'event', '--end', CURRENT_TIMESTAMP + 600])
81 | assert result.exit_code == 0
82 | assert 'NewConnectionError' in caplog.text
83 |
84 |
85 | def test_cli_with_influx(runner, caplog):
86 | result = runner.invoke(cli.main, ['--tag', 'event', '--uri', 'influx://localhost:8086'])
87 | assert result.exit_code == 0
88 | assert 'NewConnectionError' in caplog.text
89 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=py{36,37,38}, flake8
3 |
4 | [testenv]
5 | commands=py.test --cov grafannotate -vv {posargs}
6 | deps=
7 | pytest
8 | pytest-cov
9 | mock
10 | requests-mock
11 |
12 | [testenv:flake8]
13 | basepython = python3.8
14 | deps =
15 | flake8
16 | commands =
17 | flake8 grafannotate tests --max-line-length=120
18 |
--------------------------------------------------------------------------------