Skip to content

Commit 45ec961

Browse files
Merge pull request #23 from RedisLabs/michal-log_collector
RED-24457 - create debug info script
2 parents 57e2462 + 1099449 commit 45ec961

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

log_collector.py

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
#!/usr/bin/env python
2+
3+
# Redis Enterprise Cluster log collector script.
4+
# Creates a directory with output of kubectl for several API objects and for pods logs
5+
# unless pass a -n parameter will run on current namespace. Run with -h to see options
6+
7+
import argparse
8+
import yaml
9+
import logging
10+
import os
11+
import re
12+
import subprocess
13+
import sys
14+
import tarfile
15+
import time
16+
from collections import OrderedDict
17+
18+
logger = logging.getLogger("log collector")
19+
20+
output_dir = ""
21+
namespace = ""
22+
timestr = time.strftime("%Y%m%d-%H%M%S")
23+
dir_name = "redis_enterprise_k8s_debug_info_{}".format(timestr)
24+
25+
api_resources = [
26+
"RedisEnterpriseCluster",
27+
"StatefulSet",
28+
"Deployment",
29+
"Services",
30+
"ConfigMap",
31+
"Routes",
32+
"Ingress",
33+
]
34+
35+
36+
def run(configured_namespace, configured_output_path):
37+
global output_dir, namespace
38+
39+
namespace = ""
40+
if configured_namespace:
41+
namespace = configured_namespace
42+
else:
43+
namespace = get_namespace_from_config()
44+
45+
global timestr
46+
global dir_name
47+
48+
if configured_output_path:
49+
output_dir = "{}{}{}".format(configured_output_path, "/" if configured_output_path[-1] != "/" else "", dir_name)
50+
else:
51+
output_dir = "{}/{}".format(os.path.dirname(os.path.abspath(__file__)), dir_name)
52+
53+
rc, out = run_shell_command("mkdir -p {}".format(output_dir))
54+
if rc:
55+
logger.warning("Could not create output directory {} - exiting".format(out))
56+
sys.exit()
57+
58+
get_redis_enterprise_debug_info()
59+
collect_cluster_info()
60+
collect_resources_list()
61+
collect_events()
62+
collect_api_resources()
63+
collect_pods_logs()
64+
archive_files()
65+
logger.info("Finished Redis Enterprise log collector")
66+
67+
68+
def get_redis_enterprise_debug_info():
69+
"""
70+
Connects to an RS cluster node, creates and copies debug info package from the pod
71+
"""
72+
cmd = 'kubectl get pod {} --selector="redis.io/role=node" |tail -1 |cut -d " " -f1'.format(get_namespace_argument())
73+
rc, out = run_shell_command(cmd)
74+
if rc or out == "No resources found.\n":
75+
logger.warning(
76+
"Cannot find redis enterprise pod, Error: {} - Skipping collecting Redis Enterprise cluster debug info".format(
77+
out))
78+
return
79+
80+
pod = out.strip("\n")
81+
82+
cmd = "kubectl {} exec {} /opt/redislabs/bin/rladmin cluster debug_info path /tmp".format(
83+
get_namespace_argument(), pod)
84+
rc, out = run_shell_command(cmd)
85+
if "Downloading complete" not in out:
86+
logger.warning("Failed running rladmin command in pod: {}".format(out))
87+
return
88+
89+
# get the debug file name
90+
match = re.search('File (.*\.gz)', out)
91+
if match:
92+
debug_file = match.group(1)
93+
logger.info("debug info created on pod {} in path {}".format(pod, debug_file))
94+
else:
95+
logger.warning(
96+
"Failed to extract debug info name from output - "
97+
"Skipping collecting Redis Enterprise cluster debug info".format(out))
98+
return
99+
100+
# copy package from RS pod
101+
cmd = "kubectl {} cp {}:{} {}".format(get_namespace_argument(), pod, debug_file, output_dir)
102+
rc, out = run_shell_command(cmd)
103+
if rc:
104+
logger.warning(
105+
"Failed to debug info from pod to output directory".format(out))
106+
return
107+
108+
logger.info("Collected Redis Enterprise cluster debug package")
109+
110+
111+
def collect_resources_list():
112+
"""
113+
Prints the output of kubectl get all to a file
114+
"""
115+
collect_helper(cmd="kubectl get all", file_name="resources_list", resource_name="resources list")
116+
117+
118+
def collect_cluster_info():
119+
"""
120+
Prints the output of kubectl cluster-info to a file
121+
"""
122+
collect_helper(cmd="kubectl cluster-info", file_name="cluster_info", resource_name="cluster-info")
123+
124+
125+
def collect_events():
126+
"""
127+
Prints the output of kubectl cluster-info to a file
128+
"""
129+
global output_dir
130+
# events need -n parameter in kubectl
131+
if not namespace:
132+
logger.warning("Cannot collect events without namespace - skipping events collection")
133+
return
134+
cmd = "kubectl get events {}".format(get_namespace_argument())
135+
collect_helper(cmd=cmd, file_name="events", resource_name="events")
136+
137+
138+
def collect_api_resources():
139+
"""
140+
Creates file for each of the API resources with the output of kubectl get <resource> -o yaml
141+
"""
142+
logger.info("Collecting API resources:")
143+
resources_out = OrderedDict()
144+
for resource in api_resources:
145+
output = run_kubectl_get(resource)
146+
if output:
147+
resources_out[resource] = run_kubectl_get(resource)
148+
logger.info(" + {}".format(resource))
149+
150+
for entry, out in resources_out.iteritems():
151+
with open("{}/{}.yaml".format(output_dir, entry), "w+") as fp:
152+
fp.write(out)
153+
154+
155+
def collect_pods_logs():
156+
"""
157+
Collects all the pods logs from given namespace
158+
"""
159+
global output_dir
160+
logger.info("Collecting pods' logs:")
161+
logs_dir = "{}/pods".format(output_dir)
162+
rc, out = run_shell_command("mkdir -p {}".format(logs_dir))
163+
if rc:
164+
logger.warning("Failed to create directory for pods' logs - Skipping. Output: {}".format(out))
165+
return
166+
167+
pods = get_pods()
168+
if not pods:
169+
logger.warning("Could not get pods list - skipping pods logs collection")
170+
return
171+
172+
for pod in pods:
173+
cmd = "kubectl logs {} {}".format(get_namespace_argument(), pod)
174+
with open("{}/{}.log".format(logs_dir, pod), "w+") as fp:
175+
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
176+
while True:
177+
# read one line a time - we do not want to read large files to memory
178+
line = p.stdout.readline()
179+
if line:
180+
fp.write(line)
181+
else:
182+
break
183+
logger.info(" + {}".format(pod))
184+
185+
186+
def archive_files():
187+
global dir_name
188+
file_name = output_dir + ".tar.gz"
189+
190+
with tarfile.open(file_name, "w|gz") as tar:
191+
tar.add(output_dir, arcname=dir_name + ".tar.gz")
192+
logger.info("Archived files into {}".format(file_name))
193+
194+
rc, out = run_shell_command("rm -rf {}".format(output_dir))
195+
if rc:
196+
logger.warning("Failed to delete directory after archiving: {}".format(out))
197+
198+
199+
def get_pods():
200+
"""
201+
Returns list of pods names
202+
"""
203+
cmd = "kubectl get pod {} --no-headers|cut -d \" \" -f1".format(get_namespace_argument())
204+
rc, out = run_shell_command(cmd)
205+
if rc == 0:
206+
return out.split("\n")[:-1]
207+
logger.warning("Failed to get pods: {}".format(out))
208+
209+
210+
def get_namespace_argument():
211+
global namespace
212+
if namespace:
213+
return "-n {}".format(namespace)
214+
return ""
215+
216+
217+
def get_namespace_from_config():
218+
"""
219+
Returns the namespace from current context if one is set OW None
220+
"""
221+
# find namespace from config file
222+
cmd = "kubectl config view -o yaml"
223+
rc, out = run_shell_command(cmd)
224+
if rc:
225+
return
226+
227+
config = yaml.loads(out)
228+
current_context = config.get('current-context')
229+
if not current_context:
230+
return
231+
232+
for context in config.get('contexts', []):
233+
if context['name'] == current_context:
234+
if context['context'].get("namespace"):
235+
return context['context']["namespace"]
236+
break
237+
238+
239+
def collect_helper(cmd, file_name, resource_name):
240+
"""
241+
Runs command, write output to file_name, logs the resource_name
242+
"""
243+
global output_dir
244+
rc, out = run_shell_command(cmd)
245+
if rc:
246+
logger.warning("Error when running {}: {}".format(cmd, out))
247+
return
248+
path = "{}/{}".format(output_dir, file_name)
249+
with open(path, "w+") as fp:
250+
fp.write(out)
251+
logger.info("Collected {}".format(resource_name))
252+
253+
254+
def run_shell_command(cmd):
255+
"""
256+
Returns a tuple of the shell exit code, output
257+
"""
258+
try:
259+
output = subprocess.check_output(
260+
cmd,
261+
shell=True,
262+
stderr=subprocess.STDOUT)
263+
except subprocess.CalledProcessError as ex:
264+
logger.warning("Failed in shell command: {}, output: {}".format(cmd, ex.output))
265+
return ex.returncode, ex.output
266+
267+
return 0, output
268+
269+
270+
def run_kubectl_get(resource_type):
271+
"""
272+
Runs kubectl get command
273+
"""
274+
cmd = "kubectl get {} {} -o yaml".format(resource_type, get_namespace_argument())
275+
rc, out = run_shell_command(cmd)
276+
if rc == 0:
277+
return out
278+
logger.warning("Failed to get {} resource: {}".format(resource_type, out))
279+
280+
281+
if __name__ == "__main__":
282+
logger.setLevel(logging.INFO)
283+
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s')
284+
285+
parser = argparse.ArgumentParser(description='Redis Enterprise K8s log collector')
286+
parser.add_argument('-n', '--namespace', action="store", type=str)
287+
parser.add_argument('-o', '--output_dir', action="store", type=str)
288+
results = parser.parse_args()
289+
logger.info("Started Redis Enterprise k8s log collector")
290+
run(results.namespace, results.output_dir)

0 commit comments

Comments
 (0)