summaryrefslogtreecommitdiffstats
path: root/roles/openshift_health_checker/library/search_journalctl.py
blob: 3631f71c8fcdeff1b7dffcc62229ea397025ae7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/python
"""Interface to journalctl."""

from time import time
import json
import re
import subprocess

from ansible.module_utils.basic import AnsibleModule


class InvalidMatcherRegexp(Exception):
    """Exception class for invalid matcher regexp."""
    pass


class InvalidLogEntry(Exception):
    """Exception class for invalid / non-json log entries."""
    pass


class LogInputSubprocessError(Exception):
    """Exception class for errors that occur while executing a subprocess."""
    pass


def main():
    """Scan a given list of "log_matchers" for journalctl messages containing given patterns.
    "log_matchers" is a list of dicts consisting of three keys that help fine-tune log searching:
    'start_regexp', 'regexp', and 'unit'.

    Sample "log_matchers" list:

    [
      {
        'start_regexp': r'Beginning of systemd unit',
        'regexp': r'the specific log message to find',
        'unit': 'etcd',
      }
    ]
    """
    module = AnsibleModule(
        argument_spec=dict(
            log_count_limit=dict(type="int", default=500),
            log_matchers=dict(type="list", required=True),
        ),
    )

    timestamp_limit_seconds = time() - 60 * 60  # 1 hour

    log_count_limit = module.params["log_count_limit"]
    log_matchers = module.params["log_matchers"]

    matched_regexp, errors = get_log_matches(log_matchers, log_count_limit, timestamp_limit_seconds)

    module.exit_json(
        changed=False,
        failed=bool(errors),
        errors=errors,
        matched=matched_regexp,
    )


def get_log_matches(matchers, log_count_limit, timestamp_limit_seconds):
    """Return a list of up to log_count_limit matches for each matcher.

    Log entries are only considered if newer than timestamp_limit_seconds.
    """
    matched_regexp = []
    errors = []

    for matcher in matchers:
        try:
            log_output = get_log_output(matcher)
        except LogInputSubprocessError as err:
            errors.append(str(err))
            continue

        try:
            matched = find_matches(log_output, matcher, log_count_limit, timestamp_limit_seconds)
            if matched:
                matched_regexp.append(matcher.get("regexp", ""))
        except InvalidMatcherRegexp as err:
            errors.append(str(err))
        except InvalidLogEntry as err:
            errors.append(str(err))

    return matched_regexp, errors


def get_log_output(matcher):
    """Return an iterator on the logs of a given matcher."""
    try:
        cmd_output = subprocess.Popen(list([
            '/bin/journalctl',
            '-ru', matcher.get("unit", ""),
            '--output', 'json',
        ]), stdout=subprocess.PIPE)

        return iter(cmd_output.stdout.readline, '')

    except subprocess.CalledProcessError as exc:
        msg = "Could not obtain journalctl logs for the specified systemd unit: {}: {}"
        raise LogInputSubprocessError(msg.format(matcher.get("unit", "<missing>"), str(exc)))
    except OSError as exc:
        raise LogInputSubprocessError(str(exc))


def find_matches(log_output, matcher, log_count_limit, timestamp_limit_seconds):
    """Return log messages matched in iterable log_output by a given matcher.

    Ignore any log_output items older than timestamp_limit_seconds.
    """
    try:
        regexp = re.compile(matcher.get("regexp", ""))
        start_regexp = re.compile(matcher.get("start_regexp", ""))
    except re.error as err:
        msg = "A log matcher object was provided with an invalid regular expression: {}"
        raise InvalidMatcherRegexp(msg.format(str(err)))

    matched = None

    for log_count, line in enumerate(log_output):
        if log_count >= log_count_limit:
            break

        try:
            obj = json.loads(line)

            # don't need to look past the most recent service restart
            if start_regexp.match(obj["MESSAGE"]):
                break

            log_timestamp_seconds = float(obj["__REALTIME_TIMESTAMP"]) / 1000000
            if log_timestamp_seconds < timestamp_limit_seconds:
                break

            if regexp.match(obj["MESSAGE"]):
                matched = line
                break

        except ValueError:
            msg = "Log entry for systemd unit {} contained invalid json syntax: {}"
            raise InvalidLogEntry(msg.format(matcher.get("unit"), line))

    return matched


if __name__ == '__main__':
    main()