├── .github ├── CODEOWNERS └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── datadog_callback.py └── requirements.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @DataDog/container-ecosystems @DataDog/agent-delivery 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'python' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 26 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | # Initializes the CodeQL tools for scanning. 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v2 35 | with: 36 | languages: ${{ matrix.language }} 37 | # If you wish to specify custom queries, you can do so here or in a config file. 38 | # By default, queries listed here will override any specified in a config file. 39 | # Prefix the list here with "+" to use these queries and those in the config file. 40 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 41 | 42 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 43 | # If this step fails, then you should remove it and run the build manually 44 | - name: Autobuild 45 | uses: github/codeql-action/autobuild@v2 46 | 47 | - name: Perform CodeQL Analysis 48 | uses: github/codeql-action/analyze@v2 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | # 2.8.1 / 2024-04-05 5 | 6 | * [BUGFIX] Fix `import name 'cli' from '__main__' error [#70][] (Thanks [@tcaddy][]) 7 | 8 | # 2.8.0 / 2022-04-05 9 | 10 | * [BUGFIX] Replace @ in events to avoid triggering notifications [#68][] 11 | * [IMPROVEMENT] Provide better message on import errors to ease debugging [#65][] 12 | * [IMPROVEMENT] Add requirements.txt file [#64][] 13 | 14 | # 2.7.0 / 2021-05-10 15 | 16 | * [IMPROVEMENT] Explain Ansible/Datadog hostname mismatch, allow overriding hostname. See [#60][] 17 | 18 | # 2.6.0 / 2020-11-25 19 | * [BUGFIX] Cast api_key as string in datadog.initialize [#54][] 20 | * [BUGFIX] Add hook to handle incompatibilities between Ansible 2.7 and 2.8 [#47][] (Thanks [@rsdcobalt][]) 21 | 22 | # 2.5.1 / 2019-05-17 23 | * [IMPROVEMENT] Use PyYaml's "FullLoader" to avoid unsafe load. See [#45][] (thanks to [@brandonshough][]) 24 | 25 | # 2.5.0 / 2019-02-07 26 | * [FEATURE] Add support for "site" configuration. 27 | 28 | # 2.4.2 / 2018-06-08 29 | - [BUGFIX] Fix yaml import broken by Ansible 2.5. 30 | 31 | # 2.4.1 / 2018-02-07 32 | - [BUGFIX] Avoid printing error about the conf file when using a vault. See [#34][] 33 | 34 | # 2.4.0 / 2018-02-07 35 | - [FEATURE] Add support of python3 (and still fully support python2). See [#33][] (thanks to [@DSpeichert][]) 36 | 37 | # 2.3.0 / 2018-01-12 38 | - [FEATURE] Don't send event about error when "ignore_errors" is True. See [#30][] 39 | 40 | # 2.2.0 / 2017-12-27 41 | - [FEATURE] Set log level to warning for the datadog and request packages. See [#24][] (thanks to [@n0ts][]) 42 | - [FEATURE] Allow users to set a custom location for the configuration file. 43 | - [FEATURE] Added environment variables for configuring datadog api key. See [#22][] (thanks to [@pyconsult][]) 44 | 45 | # 2.1.0 / 2017-12-26 46 | - [FEATURE] Disable callback if required python packages aren't installed. See [#28][] (thanks to [@dobber][]) 47 | 48 | # 2.0.0 / 2017-12-12 49 | - [FEATURE] Add support for getting api_key from hostvars and thus from vault. See [#25][] 50 | - [BREAKING CHANGE] Drop support for ansible <2.0 51 | 52 | # 1.0.2 / 2017-12-11 53 | * [BUGFIX] Avoid failure when using ansible 2.4. See [#27][] 54 | 55 | # 1.0.1 / 2016-06-06 56 | * [BUGFIX] Avoid failure when `res['invocation']` has no `module_name` key. See [#19][] 57 | 58 | # 1.0.0 / 2016-06-02 59 | First release, compatible with Ansible v1 & v2 60 | 61 | 62 | [#19]: https://github.com/DataDog/ansible-datadog-callback/issues/19 63 | [#22]: https://github.com/DataDog/ansible-datadog-callback/issues/22 64 | [#24]: https://github.com/DataDog/ansible-datadog-callback/issues/24 65 | [#25]: https://github.com/DataDog/ansible-datadog-callback/issues/25 66 | [#27]: https://github.com/DataDog/ansible-datadog-callback/issues/27 67 | [#28]: https://github.com/DataDog/ansible-datadog-callback/issues/28 68 | [#30]: https://github.com/DataDog/ansible-datadog-callback/issues/30 69 | [#33]: https://github.com/DataDog/ansible-datadog-callback/issues/33 70 | [#34]: https://github.com/DataDog/ansible-datadog-callback/issues/34 71 | [#45]: https://github.com/DataDog/ansible-datadog-callback/issues/45 72 | [#47]: https://github.com/DataDog/ansible-datadog-callback/issues/47 73 | [#54]: https://github.com/DataDog/ansible-datadog-callback/issues/54 74 | [#60]: https://github.com/DataDog/ansible-datadog-callback/issues/60 75 | [#64]: https://github.com/DataDog/ansible-datadog-callback/issues/64 76 | [#65]: https://github.com/DataDog/ansible-datadog-callback/issues/65 77 | [#68]: https://github.com/DataDog/ansible-datadog-callback/issues/68 78 | [#70]: https://github.com/DataDog/ansible-datadog-callback/issues/70 79 | [@DSpeichert]: https://github.com/DSpeichert 80 | [@brandonshough]: https://github.com/brandonshough 81 | [@dobber]: https://github.com/dobber 82 | [@n0ts]: https://github.com/n0ts 83 | [@pyconsult]: https://github.com/pyconsult 84 | [@rsdcobalt]: https://github.com/rsdcobalt 85 | [@tcaddy]: https://github.com/tcaddy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-datadog-callback 2 | 3 | A callback to send Ansible events and metrics to Datadog. 4 | 5 | ## Requirements 6 | 7 | Ansible >= 2.0 and Python packages listed in the `requirements.txt` file. 8 | 9 | Ansible <= 1.9 is no longer supported by this callback. The latest compatible 10 | version is tagged with `1.0.2`. 11 | 12 | For Mac OS X users: If you're running an older version of OS-installed python (e.g. python 2.7.10), you may need to [upgrade](https://github.com/kennethreitz/requests/issues/3883#issuecomment-281182498) to a newer version of OpenSSL (`pip install pyopenssl idna`). 13 | 14 | ## Installation 15 | 16 | 1. Install dependencies by running `pip install -r requirements.txt`. 17 | 2. Copy `datadog_callback.py` to your playbook callback directory (by default 18 | `callback_plugins/` in your playbook's root directory). Create the directory 19 | if it doesn't exist. 20 | 3. You have 3 ways to set your API key. The callback will first use the 21 | environment variable, then the configuration file, then hostvars/vault. 22 | 23 | ##### Using environment variable 24 | 25 | Set the environment variable `DATADOG_API_KEY`. 26 | 27 | Optionally to send data to Datadog EU, you can set the environment 28 | variable `DATADOG_SITE=datadoghq.eu`. 29 | 30 | To send data to a custom URL you can set the environment 31 | variable `DATADOG_URL=`. 32 | 33 | ##### Using a yaml file 34 | 35 | Create a `datadog_callback.yml` file alongside `datadog_callback.py`, 36 | and set its contents with your [API key](https://app.datadoghq.com/account/settings#api), 37 | as following: 38 | 39 | ``` 40 | api_key: 41 | 42 | # optionally to send data to Datadog EU add the following setting 43 | site: datadoghq.eu 44 | # optionally to send data to a custom URL add the following setting 45 | url: 46 | ``` 47 | 48 | You can specify a custom location for the configuration file using the 49 | `ANSIBLE_DATADOG_CALLBACK_CONF_FILE` environment file. 50 | 51 | For example: 52 | ``` 53 | ANSIBLE_DATADOG_CALLBACK_CONF_FILE=/etc/datadog/callback_conf.yaml ansible-playbook ... 54 | ``` 55 | 56 | ##### Using ansible hostvars and vault 57 | 58 | Alternatively you can use the hostvars of the host ansible is being run from (preferably in the vault file): 59 | ``` 60 | datadog_api_key: 61 | 62 | # Optionally to send data to Datadog EU add the following setting 63 | datadog_site: datadoghq.eu 64 | 65 | # Optionally to send data to a custom URL add the following setting 66 | datadog_url: 67 | ``` 68 | 69 | 3. Be sure to whitelist the plugin in your ansible.cfg 70 | ``` 71 | [defaults] 72 | callback_whitelist = datadog_callback 73 | ``` 74 | 75 | You should start seeing Ansible events and metrics appear on Datadog when your playbook is run. 76 | 77 | ## Inventory hostnames vs Datadog hostnames 78 | 79 | By default, the events reported for individual hosts use inventory hostnames 80 | as the value for the event `host` tag. This can lead to problems when Ansible 81 | inventory hostnames are different than hostnames detected by the Datadog Agent. 82 | In this case, the events are going to be reported for a seemingly non-existent 83 | host (the inventory hostname), which will then disappear after some time 84 | of inactivity. There are several possible solutions in this case. Let's assume 85 | that we have a host `some.hostname.com` which is detected as 86 | `datadog.detected.hostname.com` by the Datadog Agent: 87 | 88 | * Use Ansible [inventory aliases](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#inventory-aliases): 89 | * Original inventory file: 90 | ``` 91 | [servers] 92 | some.hostname.com 93 | ``` 94 | * Adjusted inventory file using alias: 95 | ``` 96 | [servers] 97 | datadog.detected.hostname.com ansible_host=some.hostname.com 98 | ``` 99 | * Overwrite the `get_dd_hostname` method in `datadog_callback.py`: 100 | ``` 101 | def get_dd_hostname(self, ansible_hostname): 102 | """ This function allows providing custom logic that transforms an Ansible 103 | inventory hostname to a Datadog hostname. 104 | """ 105 | dd_hostname = ansible_hostname.replace("some.", "datadog.detected.") 106 | return dd_hostname 107 | ``` 108 | 109 | ## Contributing to ansible-datadog-callback 110 | 111 | 1. Fork it 112 | 2. Create your feature branch (`git checkout -b my-new-feature`) 113 | 3. Commit your changes (`git commit -am 'Add some feature'`) 114 | 4. Push to the branch (`git push origin my-new-feature`) 115 | 5. Create new Pull Request 116 | 117 | ## Copyright 118 | 119 | Copyright (c) 2015 Datadog, Inc. See LICENSE for further details. 120 | -------------------------------------------------------------------------------- /datadog_callback.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import getpass 4 | import logging 5 | import os 6 | import time 7 | 8 | IMPORT_ERROR = None 9 | try: 10 | import datadog 11 | import yaml 12 | from packaging import version 13 | except ImportError as e: 14 | IMPORT_ERROR = str(e) 15 | 16 | 17 | import ansible 18 | from ansible.plugins.callback import CallbackBase 19 | try: 20 | from __main__ import cli 21 | except ImportError: 22 | cli = False 23 | 24 | ANSIBLE_ABOVE_28 = False 25 | if IMPORT_ERROR is None and version.parse(ansible.__version__) >= version.parse('2.8.0'): 26 | ANSIBLE_ABOVE_28 = True 27 | from ansible.context import CLIARGS 28 | 29 | DEFAULT_DD_URL = "https://api.datadoghq.com" 30 | 31 | 32 | class CallbackModule(CallbackBase): 33 | def __init__(self): 34 | if IMPORT_ERROR is not None: 35 | self.disabled = True 36 | print( 37 | 'Datadog callback disabled because of a dependency problem: {}. ' 38 | 'Please install requirements with "pip install -r requirements.txt"' 39 | .format(IMPORT_ERROR) 40 | ) 41 | else: 42 | self.disabled = False 43 | # Set logger level - datadog api and urllib3 44 | for log_name in ['requests.packages.urllib3', 'datadog.api']: 45 | self._set_logger_level(log_name) 46 | 47 | self._playbook_name = None 48 | self._start_time = time.time() 49 | self._options = None 50 | if IMPORT_ERROR is None: 51 | if ANSIBLE_ABOVE_28: 52 | self._options = CLIARGS 53 | elif cli: 54 | self._options = cli.options 55 | 56 | # self.playbook is set in the `v2_playbook_on_start` callback method 57 | self.playbook = None 58 | # self.play is set in the `playbook_on_play_start` callback method 59 | self.play = None 60 | 61 | # Set logger level 62 | def _set_logger_level(self, name, level=logging.WARNING): 63 | try: 64 | log = logging.getLogger(name) 65 | log.setLevel(level) 66 | log.propagate = False 67 | except Exception as e: 68 | # We don't want Ansible to fail on an API error 69 | print("Couldn't get logger - %s" % name) 70 | print(e) 71 | 72 | # Load parameters from conf file 73 | def _load_conf(self, file_path): 74 | conf_dict = {} 75 | if os.path.isfile(file_path): 76 | try: 77 | loader = yaml.FullLoader 78 | except AttributeError: 79 | # on pyyaml < 5.1, there's no FullLoader, 80 | # but we can still use SafeLoader 81 | loader = yaml.SafeLoader 82 | with open(file_path, 'r') as conf_file: 83 | conf_dict = yaml.load(conf_file, Loader=loader) 84 | 85 | api_key = os.environ.get('DATADOG_API_KEY', conf_dict.get('api_key', '')) 86 | dd_url = os.environ.get('DATADOG_URL', conf_dict.get('url', '')) 87 | dd_site = os.environ.get('DATADOG_SITE', conf_dict.get('site', '')) 88 | return api_key, dd_url, dd_site 89 | 90 | # Send event to Datadog 91 | def _send_event(self, title, alert_type=None, text=None, tags=None, host=None, event_type=None, event_object=None): 92 | if tags is None: 93 | tags = [] 94 | tags.extend(self.default_tags) 95 | priority = 'normal' if alert_type == 'error' else 'low' 96 | try: 97 | datadog.api.Event.create( 98 | title=title, 99 | text=text.replace('@','(@)'), # avoid notifying @ mentions 100 | alert_type=alert_type, 101 | priority=priority, 102 | tags=tags, 103 | host=host, 104 | source_type_name='ansible', 105 | event_type=event_type, 106 | event_object=event_object, 107 | ) 108 | except Exception as e: 109 | # We don't want Ansible to fail on an API error 110 | print('Couldn\'t send event "{0}" to Datadog'.format(title)) 111 | print(e) 112 | 113 | # Send event, aggregated with other task-level events from the same host 114 | def send_task_event(self, title, alert_type='info', text='', tags=None, host=None): 115 | if getattr(self, 'play', None): 116 | if tags is None: 117 | tags = [] 118 | tags.append('play:{0}'.format(self.play.name)) 119 | self._send_event( 120 | title, 121 | alert_type=alert_type, 122 | text=text, 123 | tags=tags, 124 | host=host, 125 | event_type='config_management.task', 126 | event_object=host, 127 | ) 128 | 129 | # Send event, aggregated with other playbook-level events from the same playbook and of the same type 130 | def send_playbook_event(self, title, alert_type='info', text='', tags=None, event_type=''): 131 | self._send_event( 132 | title, 133 | alert_type=alert_type, 134 | text=text, 135 | tags=tags, 136 | event_type='config_management.run.{0}'.format(event_type), 137 | event_object=self._playbook_name, 138 | ) 139 | 140 | # Send ansible metric to Datadog 141 | def send_metric(self, metric, value, tags=None, host=None): 142 | if tags is None: 143 | tags = [] 144 | tags.extend(self.default_tags) 145 | try: 146 | datadog.api.Metric.send( 147 | metric="ansible.{0}".format(metric), 148 | points=value, 149 | tags=tags, 150 | host=host, 151 | ) 152 | except Exception as e: 153 | # We don't want Ansible to fail on an API error 154 | print('Couldn\'t send metric "{0}" to Datadog'.format(metric)) 155 | print(e) 156 | 157 | # Start timer to measure playbook running time 158 | def start_timer(self): 159 | self._start_time = time.time() 160 | 161 | # Get the time elapsed since the timer was started 162 | def get_elapsed_time(self): 163 | return time.time() - self._start_time 164 | 165 | # Default tags sent with events and metrics 166 | @property 167 | def default_tags(self): 168 | return ['playbook:{0}'.format(self._playbook_name)] 169 | 170 | @staticmethod 171 | def pluralize(number, noun): 172 | if number == 1: 173 | return "{0} {1}".format(number, noun) 174 | 175 | return "{0} {1}s".format(number, noun) 176 | 177 | # format helper for event_text 178 | @staticmethod 179 | def format_result(result): 180 | res = result._result 181 | module_name = result._task.action 182 | msg = "$$$\n{0}\n$$$\n".format(res['msg']) if res.get('msg') else "" 183 | 184 | if res.get('censored'): 185 | event_text = res.get('censored') 186 | elif not res.get('invocation'): 187 | event_text = msg 188 | else: 189 | invocation = res['invocation'] 190 | event_text = "$$$\n{0}[{1}]\n$$$\n".format(module_name, invocation.get('module_args', '')) 191 | event_text += msg 192 | if 'stdout' in res: 193 | # On Ansible v2, details on internal failures of modules are not reported in the `msg`, 194 | # so we have to extract the info differently 195 | event_text += "$$$\n{0}\n{1}\n$$$\n".format( 196 | res.get('stdout', ''), res.get('stderr', '')) 197 | 198 | module_name_tag = 'module:{0}'.format(module_name) 199 | 200 | return event_text, module_name_tag 201 | 202 | def get_dd_hostname(self, ansible_hostname): 203 | """ This function allows providing custom logic that transforms an Ansible 204 | inventory hostname to a Datadog hostname. 205 | """ 206 | dd_hostname = ansible_hostname 207 | # provide your code to obtain Datadog hostname from Ansible inventory hostname 208 | return dd_hostname 209 | 210 | ### Ansible callbacks ### 211 | def v2_runner_on_failed(self, result, ignore_errors=False): 212 | host = self.get_dd_hostname(result._host.get_name()) 213 | # don't post anything if user asked to ignore errors 214 | if ignore_errors: 215 | return 216 | 217 | event_text, module_name_tag = self.format_result(result) 218 | self.send_task_event( 219 | 'Ansible task failed on "{0}"'.format(host), 220 | alert_type='error', 221 | text=event_text, 222 | tags=[module_name_tag], 223 | host=host, 224 | ) 225 | 226 | def v2_runner_on_ok(self, result): 227 | host = self.get_dd_hostname(result._host.get_name()) 228 | # Only send an event when the task has changed on the host 229 | if result._result.get('changed'): 230 | event_text, module_name_tag = self.format_result(result) 231 | self.send_task_event( 232 | 'Ansible task changed on "{0}"'.format(host), 233 | alert_type='success', 234 | text=event_text, 235 | tags=[module_name_tag], 236 | host=host, 237 | ) 238 | 239 | def v2_runner_on_unreachable(self, result): 240 | res = result._result 241 | host = self.get_dd_hostname(result._host.get_name()) 242 | event_text = "\n$$$\n{0}\n$$$\n".format(res) 243 | self.send_task_event( 244 | 'Ansible failed on unreachable host "{0}"'.format(host), 245 | alert_type='error', 246 | text=event_text, 247 | host=host, 248 | ) 249 | 250 | # Implementation compatible with Ansible v2 only 251 | def v2_playbook_on_start(self, playbook): 252 | # On Ansible v2, Ansible doesn't set `self.playbook` automatically 253 | self.playbook = playbook 254 | 255 | playbook_file_name = self.playbook._file_name 256 | if ANSIBLE_ABOVE_28: 257 | inventory = self._options['inventory'] 258 | else: 259 | inventory = self._options.inventory 260 | 261 | self.start_timer() 262 | 263 | # Set the playbook name from its filename 264 | self._playbook_name, _ = os.path.splitext( 265 | os.path.basename(playbook_file_name)) 266 | if isinstance(inventory, (list, tuple)): 267 | inventory = ','.join(inventory) 268 | self._inventory_name = ','.join([os.path.basename(os.path.realpath(name)) for name in inventory.split(',') if name]) 269 | 270 | def v2_playbook_on_play_start(self, play): 271 | # On Ansible v2, Ansible doesn't set `self.play` automatically 272 | self.play = play 273 | if self.disabled: 274 | return 275 | 276 | # Read config and hostvars 277 | config_path = os.environ.get('ANSIBLE_DATADOG_CALLBACK_CONF_FILE', os.path.join(os.path.dirname(__file__), "datadog_callback.yml")) 278 | api_key, dd_url, dd_site = self._load_conf(config_path) 279 | 280 | # If there is no api key defined in config file, try to get it from hostvars 281 | if api_key == '': 282 | hostvars = self.play.get_variable_manager()._hostvars 283 | 284 | if not hostvars: 285 | print("No api_key found in the config file ({0}) and hostvars aren't set: disabling Datadog callback plugin".format(config_path)) 286 | self.disabled = True 287 | else: 288 | try: 289 | api_key = hostvars['localhost']['datadog_api_key'] 290 | if not dd_url: 291 | dd_url = hostvars['localhost'].get('datadog_url') 292 | if not dd_site: 293 | dd_site = hostvars['localhost'].get('datadog_site') 294 | except Exception as e: 295 | print('No "api_key" found in the config file ({0}) and "datadog_api_key" is not set in the hostvars: disabling Datadog callback plugin'.format(config_path)) 296 | self.disabled = True 297 | 298 | if not dd_url: 299 | if dd_site: 300 | dd_url = "https://api."+ dd_site 301 | else: 302 | dd_url = DEFAULT_DD_URL # default to Datadog US 303 | 304 | # Set up API client and send a start event 305 | if not self.disabled: 306 | datadog.initialize(api_key=str(api_key), api_host=dd_url) 307 | 308 | self.send_playbook_event( 309 | 'Ansible play "{0}" started in playbook "{1}" by "{2}" against "{3}"'.format( 310 | self.play.name, 311 | self._playbook_name, 312 | getpass.getuser(), 313 | self._inventory_name), 314 | event_type='start', 315 | ) 316 | 317 | def playbook_on_stats(self, stats): 318 | total_tasks = 0 319 | total_updated = 0 320 | total_errors = 0 321 | error_hosts = [] 322 | for host in stats.processed: 323 | host = self.get_dd_hostname(host) 324 | # Aggregations for the event text 325 | summary = stats.summarize(host) 326 | total_tasks += sum([summary['ok'], summary['failures'], summary['skipped']]) 327 | total_updated += summary['changed'] 328 | errors = sum([summary['failures'], summary['unreachable']]) 329 | if errors > 0: 330 | error_hosts.append((host, summary['failures'], summary['unreachable'])) 331 | total_errors += errors 332 | 333 | # Send metrics for this host 334 | for metric, value in summary.items(): 335 | self.send_metric('task.{0}'.format(metric), value, host=host) 336 | 337 | # Send playbook elapsed time 338 | self.send_metric('elapsed_time', self.get_elapsed_time()) 339 | 340 | # Generate basic "Completed" event 341 | event_title = 'Ansible playbook "{0}" completed in {1}'.format( 342 | self._playbook_name, 343 | self.pluralize(int(self.get_elapsed_time()), 'second')) 344 | event_text = 'Ansible updated {0} out of {1} total, on {2}. {3} occurred.'.format( 345 | self.pluralize(total_updated, 'task'), 346 | self.pluralize(total_tasks, 'task'), 347 | self.pluralize(len(stats.processed), 'host'), 348 | self.pluralize(total_errors, 'error')) 349 | alert_type = 'success' 350 | 351 | # Add info to event if errors occurred 352 | if total_errors > 0: 353 | alert_type = 'error' 354 | event_title += ' with errors' 355 | event_text += "\nErrors occurred on the following hosts:\n%%%\n" 356 | for host, failures, unreachable in error_hosts: 357 | event_text += "- `{0}` (failure: {1}, unreachable: {2})\n".format( 358 | host, 359 | failures, 360 | unreachable) 361 | event_text += "\n%%%\n" 362 | else: 363 | event_title += ' successfully' 364 | 365 | self.send_playbook_event( 366 | event_title, 367 | alert_type=alert_type, 368 | text=event_text, 369 | event_type='end', 370 | ) 371 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | datadog 2 | packaging 3 | pyyaml>=3.10 4 | --------------------------------------------------------------------------------