-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
413 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# ansible-loki-callback: An Ansible callback plugin that logs to a loki instance | ||
|
||
## Requirements | ||
|
||
* Python3 | ||
* Ansible | ||
|
||
## Installation | ||
|
||
Download or clone the repository and install the requirements: | ||
|
||
pip install -r requirements.txt | ||
|
||
## Usage | ||
|
||
Use the following environment variables to configure the plugin: | ||
|
||
* LOKI_URL: URL to the Loki Push API endpoint (https://loki.example.com/api/v1/push) | ||
* LOKI_USERNAME: Username to authenticate at loki (optional) | ||
* LOKI_PASSWORD: Password to authenticate at loki (optional) | ||
* LOKI_DEFAULT_TAGS: A comma separated list of key:value pairs used for every log line (optional) | ||
* LOKI_ORG_ID: Loki organization id (optional) | ||
|
||
Then set `ANSIBLE_CALLBACK_PLUGINS` to the path where you downloaded or cloned the repository to. | ||
|
||
## Testing | ||
|
||
The example directory contains a test playbook that can be used to test the callback plugin. Run it using | ||
|
||
ANSIBLE_CALLBACK_PLUGINS="${PWD}" ansible-playbook -i example/inventory.yaml example/playbook.yaml -vvvvvv 2>/dev/null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
unreachable: | ||
hosts: | ||
unreachable_host: | ||
ansible_host: 1.1.1.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
- name: Test1 | ||
hosts: 127.0.0.1 | ||
connection: local | ||
gather_facts: no | ||
tasks: | ||
- name: Call github | ||
uri: | ||
url: 'https://github.com' | ||
|
||
- name: Testdiff | ||
hosts: 127.0.0.1 | ||
connection: local | ||
gather_facts: no | ||
tasks: | ||
- name: Create temp file | ||
tempfile: {} | ||
register: tempfile | ||
- name: Write tempfile | ||
copy: | ||
dest: "{{ tempfile.path }}" | ||
content: "test" | ||
|
||
- name: TestFail | ||
hosts: 127.0.0.1 | ||
connection: local | ||
gather_facts: no | ||
tasks: | ||
- name: Produce failure | ||
command: exit 1 | ||
ignore_errors: yes | ||
|
||
- name: Testskipped | ||
hosts: 127.0.0.1 | ||
connection: local | ||
gather_facts: no | ||
tasks: | ||
- name: Skip it | ||
command: exit 1 | ||
when: impossible is defined | ||
|
||
- name: Testunreachable | ||
hosts: unreachable | ||
gather_facts: no | ||
ignore_errors: yes | ||
tasks: | ||
- name: Call github | ||
uri: | ||
url: 'https://github.com' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
import datetime | ||
import logging | ||
import os | ||
|
||
import jsonpickle | ||
import logging_loki | ||
from ansible.plugins.callback import CallbackBase | ||
|
||
DOCUMENTATION = ''' | ||
callback: loki | ||
type: loki | ||
short_description: Ansible output logging to loki | ||
version_added: 0.1.0 | ||
description: | ||
- This plugin sends Ansible output to loki | ||
extends_documentation_fragment: | ||
- default_callback | ||
requirements: | ||
- set as loki in configuration | ||
options: | ||
result_format: | ||
name: Result format | ||
default: json | ||
description: Format used in results (will be set to json) | ||
pretty_results: | ||
name: Print results pretty | ||
default: False | ||
description: Whether to print results pretty (will be set to false) | ||
''' | ||
|
||
|
||
# For logging detailed data, we sometimes need to access protected object members | ||
# noinspection PyProtectedMember | ||
class CallbackModule(CallbackBase): | ||
CALLBACK_VERSION = 2.0 | ||
CALLBACK_TYPE = 'loki' | ||
CALLBACK_NAME = 'loki' | ||
ALL_METRICS = ["changed", "custom", "dark", "failures", "ignored", "ok", "processed", "rescued", "skipped"] | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
if "LOKI_URL" not in os.environ: | ||
raise "LOKI_URL environment variable not specified." | ||
|
||
auth = () | ||
if "LOKI_USERNAME" in os.environ and "LOKI_PASSWORD" in os.environ: | ||
auth = (os.environ["LOKI_USERNAME"], os.environ["LOKI_PASSWORD"]) | ||
|
||
headers = {} | ||
if "LOKI_ORG_ID" in os.environ: | ||
headers["X-Scope-OrgID"] = os.environ["LOKI_ORG_ID"] | ||
|
||
tags = {} | ||
if "LOKI_DEFAULT_TAGS" in os.environ: | ||
for tagvalue in os.environ["LOKI_DEFAULT_TAGS"].split(","): | ||
(tag, value) = tagvalue.split(":") | ||
tags[tag] = value | ||
|
||
handler = logging_loki.LokiHandler( | ||
url=os.environ["LOKI_URL"], | ||
tags=tags, | ||
auth=auth, | ||
headers=headers, | ||
level_tag="level" | ||
) | ||
|
||
self.logger = logging.getLogger("loki") | ||
self.logger.addHandler(handler) | ||
if self._display.verbosity == 0: | ||
self.logger.setLevel(logging.WARN) | ||
elif self._display.verbosity == 1: | ||
self.logger.setLevel(logging.INFO) | ||
else: | ||
self.logger.setLevel(logging.DEBUG) | ||
|
||
self.set_option("result_format", "json") | ||
self.set_option("pretty_results", False) | ||
|
||
def v2_playbook_on_start(self, playbook): | ||
self.playbook = os.path.join(playbook._basedir, playbook._file_name) | ||
self.run_timestamp = datetime.datetime.now().isoformat() | ||
self.logger.info( | ||
"Starting playbook %s" % self.playbook, | ||
extra={"tags": {"playbook": self.playbook, "run_timestamp": self.run_timestamp}} | ||
) | ||
self.logger.debug( | ||
jsonpickle.encode(playbook.__dict__), | ||
extra={"tags": {"playbook": self.playbook, "run_timestamp": self.run_timestamp, "dump": "playbook"}} | ||
) | ||
|
||
def v2_playbook_on_play_start(self, play): | ||
self.current_play = play.name | ||
self.logger.info( | ||
"Starting play %s" % play.name, | ||
extra={"tags": {"playbook": self.playbook, "run_timestamp": self.run_timestamp, "play": self.current_play}} | ||
) | ||
self.logger.debug( | ||
jsonpickle.encode(play.__dict__), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"dump": "play" | ||
} | ||
} | ||
) | ||
|
||
def v2_playbook_on_task_start(self, task, is_conditional): | ||
self.current_task = task.name | ||
self.logger.info( | ||
"Starting task %s" % self.current_task, | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
self.logger.debug( | ||
jsonpickle.encode(task.__dict__), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "task" | ||
} | ||
} | ||
) | ||
|
||
def v2_runner_on_ok(self, result): | ||
self.logger.debug( | ||
"Task %s was successful" % result.task_name, | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
self.logger.debug( | ||
self._dump_results(result._result), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "runner" | ||
} | ||
} | ||
) | ||
|
||
def v2_runner_on_failed(self, result, ignore_errors=False): | ||
level = logging.WARNING if ignore_errors else logging.ERROR | ||
self.logger.log( | ||
level, | ||
"Task %s was not successful%s: %s" % ( | ||
self.current_task, | ||
", but errors were ignored" if ignore_errors else "", | ||
result._result['msg'] | ||
), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
self.logger.debug( | ||
self._dump_results(result._result), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "runner" | ||
} | ||
} | ||
) | ||
|
||
def v2_runner_on_skipped(self, result): | ||
self.logger.info( | ||
"Task %s was skipped" % self.current_task, | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
self.logger.debug( | ||
self._dump_results(result._result), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "runner" | ||
} | ||
} | ||
) | ||
|
||
def runner_on_unreachable(self, host, result): | ||
self.logger.error( | ||
"Host %s was unreachable for task %s" % (host, self.current_task), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
self.logger.debug( | ||
self._dump_results(result), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "runner" | ||
} | ||
} | ||
) | ||
|
||
def v2_playbook_on_no_hosts_matched(self): | ||
self.logger.error( | ||
"No hosts matched for playbook %s" % self.playbook, | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp | ||
} | ||
} | ||
) | ||
|
||
def v2_on_file_diff(self, result): | ||
diff_list = result._result['diff'] | ||
self.logger.info( | ||
"Task %s produced a diff:\n%s" % (self.current_task, self._get_diff(diff_list)), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task | ||
} | ||
} | ||
) | ||
for diff in diff_list: | ||
self.logger.debug( | ||
self._serialize_diff(diff), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"play": self.current_play, | ||
"task": self.current_task, | ||
"dump": "diff" | ||
} | ||
} | ||
) | ||
|
||
def v2_playbook_on_stats(self, stats): | ||
summarize_metrics = {} | ||
host_metrics = {} | ||
for metric in self.ALL_METRICS: | ||
value = 0 | ||
for host, host_value in stats.__dict__[metric].items(): | ||
value += host_value | ||
if host not in host_metrics: | ||
host_metrics[host] = {} | ||
for m in self.ALL_METRICS: | ||
host_metrics[host][m] = 0 | ||
host_metrics[host][metric] = host_value | ||
summarize_metrics[metric] = value | ||
self.logger.info( | ||
"Stats for playbook %s" % self.playbook, | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"stats_type": "summary" | ||
} | summarize_metrics | ||
} | ||
) | ||
for host in host_metrics: | ||
self.logger.debug( | ||
"Stats for playbook %s, host %s" % (self.playbook, host), | ||
extra={ | ||
"tags": { | ||
"playbook": self.playbook, | ||
"run_timestamp": self.run_timestamp, | ||
"stats_type": "host" | ||
} | host_metrics[host] | ||
} | ||
) |
Oops, something went wrong.