Skip to content

Commit

Permalink
feat: First version
Browse files Browse the repository at this point in the history
  • Loading branch information
dploeger committed Jun 25, 2024
1 parent 7b97cd4 commit 03b8533
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 0 deletions.
30 changes: 30 additions & 0 deletions README.md
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
4 changes: 4 additions & 0 deletions example/inventory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
unreachable:
hosts:
unreachable_host:
ansible_host: 1.1.1.1
48 changes: 48 additions & 0 deletions example/playbook.yaml
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'
313 changes: 313 additions & 0 deletions loki.py
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]
}
)
Loading

0 comments on commit 03b8533

Please sign in to comment.