Skip to content

Commit 414133d

Browse files
committed
Initial commit
Only handle the most relevant callbacks: playbook start and end, and task completion (`ok`, `failed` and `unreachable`). The stats provided by ansible + the playbook elapsed time are sent.
0 parents  commit 414133d

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.py[cod]
2+

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2015 Datadog, Inc.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ansible-datadog-callback
2+
3+
A callback to send Ansible events and metrics to Datadog.
4+
5+
## Requirements
6+
7+
Ansible >=1.1,<2.0
8+
9+
The following python libraries are required on the Ansible server:
10+
11+
- [`datadogpy`](https://github.com/DataDog/datadogpy/)
12+
- `pyyaml` (install with `pip install pyyaml`)
13+
14+
## Installation
15+
16+
Once the required libraries (see above) have been installed on the server:
17+
18+
1. Copy `datadog_callback.py` to your playbook callback directory (by default
19+
`callback_plugins/` in your playbook's root directory). Create the directory
20+
if it doesn't exist.
21+
2. Create a `datadog_callback.yml` file alongside `datadog_callback.py`,
22+
and set its contents with your [API key](https://app.datadoghq.com/account/settings#api),
23+
as following:
24+
25+
```
26+
api_key: <your-api-key>
27+
```
28+
29+
You should start seeing Ansible events and metrics appear on Datadog when your playbook is run.
30+
31+
## Contributing to ansible-datadog-callback
32+
33+
1. Fork it
34+
2. Create your feature branch (`git checkout -b my-new-feature`)
35+
3. Commit your changes (`git commit -am 'Add some feature'`)
36+
4. Push to the branch (`git push origin my-new-feature`)
37+
5. Create new Pull Request
38+
39+
## Copyright
40+
41+
Copyright (c) 2015 Datadog, Inc. See LICENSE for further details.

datadog_callback.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import os.path
2+
import time
3+
4+
import datadog
5+
import yaml
6+
7+
8+
class CallbackModule(object):
9+
def __init__(self):
10+
# Read config and set up API client
11+
api_key, url = self._load_conf(os.path.join(os.path.dirname(__file__), "datadog_callback.yml"))
12+
datadog.initialize(api_key=api_key, api_host=url)
13+
14+
self._playbook_name = None
15+
self._start_time = time.time()
16+
17+
# Load parameters from conf file
18+
def _load_conf(self, file_path):
19+
conf_dict = {}
20+
with open(file_path, 'r') as conf_file:
21+
conf_dict = yaml.load(conf_file)
22+
23+
return conf_dict.get('api_key', ''), conf_dict.get('url', 'https://app.datadoghq.com')
24+
25+
# Send event to Datadog
26+
def _send_event(self, title, alert_type=None, text=None, tags=None, host=None, event_type=None, event_object=None):
27+
if tags is None:
28+
tags = []
29+
tags.extend(self.default_tags)
30+
priority = 'normal' if alert_type == 'error' else 'low'
31+
try:
32+
datadog.api.Event.create(
33+
title=title,
34+
text=text,
35+
alert_type=alert_type,
36+
priority=priority,
37+
tags=tags,
38+
host=host,
39+
source_type_name='ansible',
40+
event_type=event_type,
41+
event_object=event_object,
42+
)
43+
except Exception, e:
44+
# We don't want Ansible to fail on an API error
45+
print 'Couldn\'t send event "{0}" to Datadog'.format(title)
46+
print e
47+
48+
# Send event, aggregated with other task-level events from the same host
49+
def send_task_event(self, title, alert_type='info', text='', tags=None, host=None):
50+
# self.play is set by ansible
51+
if getattr(self, 'play', None):
52+
if tags is None:
53+
tags = []
54+
tags.append('play:{0}'.format(self.play.name))
55+
self._send_event(
56+
title,
57+
alert_type=alert_type,
58+
text=text,
59+
tags=tags,
60+
host=host,
61+
event_type='config_management.task',
62+
event_object=host,
63+
)
64+
65+
# Send event, aggregated with other playbook-level events from the same playbook and of the same type
66+
def send_playbook_event(self, title, alert_type='info', text='', tags=None, event_type=''):
67+
self._send_event(
68+
title,
69+
alert_type=alert_type,
70+
text=text,
71+
tags=tags,
72+
event_type='config_management.run.{0}'.format(event_type),
73+
event_object=self._playbook_name,
74+
)
75+
76+
# Send ansible metric to Datadog
77+
def send_metric(self, metric, value, tags=None, host=None):
78+
if tags is None:
79+
tags = []
80+
tags.extend(self.default_tags)
81+
try:
82+
datadog.api.Metric.send(
83+
metric="ansible.{0}".format(metric),
84+
points=value,
85+
tags=self.default_tags,
86+
host=host,
87+
)
88+
except Exception, e:
89+
# We don't want Ansible to fail on an API error
90+
print 'Couldn\'t send metric "{0}" to Datadog'.format(metric)
91+
print e
92+
93+
# Start timer to measure playbook running time
94+
def start_timer(self):
95+
self._start_time = time.time()
96+
97+
# Get the time elapsed since the timer was started
98+
def get_elapsed_time(self):
99+
return time.time() - self._start_time
100+
101+
# Default tags sent with events and metrics
102+
@property
103+
def default_tags(self):
104+
return ['playbook:{0}'.format(self._playbook_name)]
105+
106+
@staticmethod
107+
def pluralize(number, noun):
108+
if number <= 1:
109+
return "{0} {1}".format(number, noun)
110+
111+
return "{0} {1}s".format(number, noun)
112+
113+
### Ansible callbacks ###
114+
def runner_on_failed(self, host, res, ignore_errors=False):
115+
event_text = "$$$\n{0}[{1}]\n$$$\n".format(res['invocation']['module_name'], res['invocation']['module_args'])
116+
event_text += "$$$\n{0}\n$$$\n".format(res['msg'])
117+
self.send_task_event(
118+
'Ansible task failed on "{0}"'.format(host),
119+
alert_type='error',
120+
text=event_text,
121+
tags=['module:{0}'.format(res['invocation']['module_name'])],
122+
host=host,
123+
)
124+
125+
def runner_on_ok(self, host, res):
126+
# Only send an event when the task has changed on the host
127+
if res['changed']:
128+
event_text = "$$$\n{0}[{1}]\n$$$\n".format(res['invocation']['module_name'], res['invocation']['module_args'])
129+
self.send_task_event(
130+
'Ansible task changed on "{0}"'.format(host),
131+
alert_type='success',
132+
text=event_text,
133+
tags=['module:{0}'.format(res['invocation']['module_name'])],
134+
host=host,
135+
)
136+
137+
def runner_on_unreachable(self, host, res):
138+
event_text = "\n$$$\n{0}\n$$$\n".format(res)
139+
self.send_task_event(
140+
'Ansible failed on unreachable host "{0}"'.format(host),
141+
alert_type='error',
142+
text=event_text,
143+
host=host,
144+
)
145+
146+
def playbook_on_start(self):
147+
# Retrieve the playbook name from its filename
148+
self._playbook_name, _ = os.path.splitext(
149+
os.path.basename(self.playbook.filename))
150+
self.start_timer()
151+
host_list = self.playbook.inventory.host_list
152+
inventory = os.path.basename(os.path.realpath(host_list))
153+
self.send_playbook_event(
154+
'Ansible playbook "{0}" started by "{1}" against "{2}"'.format(
155+
self._playbook_name,
156+
self.playbook.remote_user,
157+
inventory),
158+
event_type='start',
159+
)
160+
161+
def playbook_on_stats(self, stats):
162+
total_tasks = 0
163+
total_updated = 0
164+
total_errors = 0
165+
error_hosts = []
166+
for host in stats.processed:
167+
# Aggregations for the event text
168+
summary = stats.summarize(host)
169+
total_tasks += sum([summary['ok'], summary['failures'], summary['skipped']])
170+
total_updated += summary['changed']
171+
errors = sum([summary['failures'], summary['unreachable']])
172+
if errors > 0:
173+
error_hosts.append((host, summary['failures'], summary['unreachable']))
174+
total_errors += errors
175+
176+
# Send metrics for this host
177+
for metric, value in summary.iteritems():
178+
self.send_metric('task.{0}'.format(metric), value, host=host)
179+
180+
# Send playbook elapsed time
181+
self.send_metric('elapsed_time', self.get_elapsed_time())
182+
183+
# Generate basic "Completed" event
184+
event_title = 'Ansible playbook "{0}" completed in {1}'.format(
185+
self._playbook_name,
186+
self.pluralize(int(self.get_elapsed_time()), 'second'))
187+
event_text = 'Ansible updated {0} out of {1} total, on {2}. {3} occurred.'.format(
188+
self.pluralize(total_updated, 'task'),
189+
self.pluralize(total_tasks, 'task'),
190+
self.pluralize(len(stats.processed), 'host'),
191+
self.pluralize(total_errors, 'error'))
192+
alert_type = 'success'
193+
194+
# Add info to event if errors occurred
195+
if total_errors > 0:
196+
alert_type = 'error'
197+
event_title += ' with errors'
198+
event_text += "\nErrors occurred on the following hosts:\n%%%\n"
199+
for host, failures, unreachable in error_hosts:
200+
event_text += "- `{0}` (failure: {1}, unreachable: {2})\n".format(
201+
host,
202+
failures,
203+
unreachable)
204+
event_text += "\n%%%\n"
205+
else:
206+
event_title += ' successfully'
207+
208+
self.send_playbook_event(
209+
event_title,
210+
alert_type=alert_type,
211+
text=event_text,
212+
event_type='end',
213+
)

0 commit comments

Comments
 (0)