Skip to content

Commit c574dd0

Browse files
Merge pull request #25 from guidograzioli/log_tail_module
add new module
2 parents 8fca7cf + 401965c commit c574dd0

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

plugins/modules/tail_grep.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (c) 2024, Red Hat Inc.
4+
# Copyright (c) 2024, Guido Grazioli <ggraziol@redhat.com>
5+
# Apache License, Version 2.0 (see LICENSE or https://www.apache.org/licenses/LICENSE-2.0)
6+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7+
8+
from __future__ import absolute_import, division, print_function
9+
10+
__metaclass__ = type
11+
12+
DOCUMENTATION = r'''
13+
---
14+
module: tail_grep
15+
short_description: Tail a logfile until a regex matcher is found or a timeout triggers
16+
description:
17+
- This module is used to follow some application logfile and return successfully when
18+
a search string or regex is found; otherwise fail after a timeout.
19+
options:
20+
path:
21+
description:
22+
- The file on the remote system to tail.
23+
type: path
24+
required: true
25+
regex:
26+
description:
27+
- The string or regular expression to search in the file.
28+
type: str
29+
required: true
30+
timeout:
31+
description:
32+
- After how many seconds to exit unsuccessfully without having found the search regex.
33+
required: false
34+
type: int
35+
default: 60
36+
from_regex:
37+
description:
38+
- Backwards from end of file, lines preceeding this string will not be considered for
39+
matching regex. By default, the whole file is read. If `$` is used, start from the
40+
first line written after the file is opened.
41+
required: false
42+
type: str
43+
default: ''
44+
delay:
45+
description:
46+
- How many seconds to wait after opening the file before starting to look for the regex.
47+
required: false
48+
type: int
49+
default: 0
50+
extends_documentation_fragment:
51+
- action_common_attributes
52+
attributes:
53+
check_mode:
54+
support: none
55+
diff_mode:
56+
support: none
57+
platform:
58+
platforms: posix
59+
author:
60+
- Guido Grazioli (@guidograzioli)
61+
'''
62+
63+
EXAMPLES = r'''
64+
- name: Tail activemq log until the successful start status code is found
65+
ansible.builtin.tail_grep:
66+
path: /var/log/activemq/artemis.log
67+
regex: AMQ220010
68+
'''
69+
70+
RETURN = r'''
71+
content:
72+
description: The full string that was matched by the search regex
73+
returned: success
74+
type: str
75+
sample: "Application has been started successfully."
76+
source:
77+
description: Actual path of file opened
78+
returned: success
79+
type: str
80+
sample: "/var/log/messages"
81+
'''
82+
83+
import errno
84+
import re
85+
import time
86+
87+
from ansible.module_utils.basic import AnsibleModule
88+
from ansible.module_utils.common.text.converters import to_native
89+
90+
91+
def amq_argument_spec():
92+
"""
93+
Returns argument_spec of options
94+
95+
:return: argument_spec dict
96+
"""
97+
return dict(
98+
path=dict(type='path', required=True),
99+
regex=dict(type='str', required=True),
100+
timeout=dict(type='int', required=False, default=60),
101+
from_regex=dict(type='str', required=False, default=''),
102+
delay=dict(type='int', required=False, default=0)
103+
)
104+
105+
106+
# from https://stackoverflow.com/a/54263201/389099
107+
def follow(file, sleep_sec=0.1, timeout_sec=60):
108+
""" Yield each line from a file as they are written.
109+
`sleep_sec` is the time to sleep after empty reads. """
110+
line = ''
111+
ts = time.time()
112+
while True:
113+
tmp = file.readline()
114+
if tmp is not None and tmp != "":
115+
line += tmp
116+
if line.endswith("\n"):
117+
yield line
118+
line = ''
119+
elif sleep_sec:
120+
time.sleep(sleep_sec)
121+
if (time.time() >= (ts + timeout_sec)):
122+
raise Exception("timeout reached without finding search string in file")
123+
124+
125+
def main():
126+
module = AnsibleModule(argument_spec=amq_argument_spec(), supports_check_mode=False)
127+
128+
source = module.params['path']
129+
regex = module.params['regex']
130+
timeout = module.params['timeout']
131+
delay = module.params['delay']
132+
133+
try:
134+
with open(source, 'r') as source_fh:
135+
time.sleep(delay)
136+
for line in follow(source_fh, 0.25, timeout):
137+
if re.match(regex, line):
138+
break
139+
except (IOError, OSError) as e:
140+
if e.errno == errno.ENOENT:
141+
msg = "file not found: %s" % source
142+
elif e.errno == errno.EACCES:
143+
msg = "file is not readable: %s" % source
144+
elif e.errno == errno.EISDIR:
145+
msg = "source is a directory and must be a file: %s" % source
146+
else:
147+
msg = "unable to read file: %s" % to_native(e, errors='surrogate_then_replace')
148+
149+
module.fail_json(msg)
150+
151+
except (Exception) as e:
152+
module.fail_json(e.args)
153+
154+
module.exit_json(content=line, source=source)
155+
156+
157+
if __name__ == '__main__':
158+
main()

0 commit comments

Comments
 (0)