1#!/usr/bin/env @PYTHON_SHEBANG@
2#
3# Print out statistics for all zil stats. This information is
4# available through the zil kstat.
5#
6# CDDL HEADER START
7#
8# The contents of this file are subject to the terms of the
9# Common Development and Distribution License, Version 1.0 only
10# (the "License").  You may not use this file except in compliance
11# with the License.
12#
13# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
14# or https://opensource.org/licenses/CDDL-1.0.
15# See the License for the specific language governing permissions
16# and limitations under the License.
17#
18# When distributing Covered Code, include this CDDL HEADER in each
19# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
20# If applicable, add the following below this CDDL HEADER, with the
21# fields enclosed by brackets "[]" replaced with your own identifying
22# information: Portions Copyright [yyyy] [name of copyright owner]
23#
24# This script must remain compatible with Python 3.6+.
25#
26
27import sys
28import subprocess
29import time
30import copy
31import os
32import re
33import signal
34from collections import defaultdict
35import argparse
36from argparse import RawTextHelpFormatter
37
38cols = {
39	# hdr:       [size,      scale,      kstat name]
40	"time":      [8,         -1,         "time"],
41	"pool":      [12,        -1,         "pool"],
42	"ds":        [12,        -1,         "dataset_name"],
43	"obj":       [12,        -1,         "objset"],
44	"cc":        [5,         1000,       "zil_commit_count"],
45	"cwc":       [5,         1000,       "zil_commit_writer_count"],
46	"ic":        [5,         1000,       "zil_itx_count"],
47	"iic":       [5,         1000,       "zil_itx_indirect_count"],
48	"iib":       [5,         1024,       "zil_itx_indirect_bytes"],
49	"icc":       [5,         1000,       "zil_itx_copied_count"],
50	"icb":       [5,         1024,       "zil_itx_copied_bytes"],
51	"inc":       [5,         1000,       "zil_itx_needcopy_count"],
52	"inb":       [5,         1024,       "zil_itx_needcopy_bytes"],
53	"idc":       [5,         1000,       "icc+inc"],
54	"idb":       [5,         1024,       "icb+inb"],
55	"iwc":       [5,         1000,       "iic+idc"],
56	"iwb":       [5,         1024,       "iib+idb"],
57	"imnc":      [6,         1000,       "zil_itx_metaslab_normal_count"],
58	"imnb":      [6,         1024,       "zil_itx_metaslab_normal_bytes"],
59	"imnw":      [6,         1024,       "zil_itx_metaslab_normal_write"],
60	"imna":      [6,         1024,       "zil_itx_metaslab_normal_alloc"],
61	"imsc":      [6,         1000,       "zil_itx_metaslab_slog_count"],
62	"imsb":      [6,         1024,       "zil_itx_metaslab_slog_bytes"],
63	"imsw":      [6,         1024,       "zil_itx_metaslab_slog_write"],
64	"imsa":      [6,         1024,       "zil_itx_metaslab_slog_alloc"],
65	"imc":       [5,         1000,       "imnc+imsc"],
66	"imb":       [5,         1024,       "imnb+imsb"],
67	"imw":       [5,         1024,       "imnw+imsw"],
68	"ima":       [5,         1024,       "imna+imsa"],
69	"se%":       [3,         100,        "imb/ima"],
70	"sen%":      [4,         100,        "imnb/imna"],
71	"ses%":      [4,         100,        "imsb/imsa"],
72	"te%":       [3,         100,        "imb/imw"],
73	"ten%":      [4,         100,        "imnb/imnw"],
74	"tes%":      [4,         100,        "imsb/imsw"],
75}
76
77hdr = ["time", "ds", "cc", "ic", "idc", "idb", "iic", "iib",
78	"imnc", "imnw", "imsc", "imsw"]
79
80ghdr = ["time", "cc", "ic", "idc", "idb", "iic", "iib",
81	"imnc", "imnw", "imsc", "imsw"]
82
83cmd = ("Usage: zilstat [-hgdv] [-i interval] [-p pool_name]")
84
85curr = {}
86diff = {}
87kstat = {}
88ds_pairs = {}
89pool_name = None
90dataset_name = None
91interval = 0
92sep = "  "
93gFlag = True
94dsFlag = False
95
96def prettynum(sz, scale, num=0):
97	suffix = [' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']
98	index = 0
99	save = 0
100
101	if scale == -1:
102		return "%*s" % (sz, num)
103
104	# Rounding error, return 0
105	elif 0 < num < 1:
106		num = 0
107
108	while num > scale and index < 5:
109		save = num
110		num = num / scale
111		index += 1
112
113	if index == 0:
114		return "%*d" % (sz, num)
115
116	if (save / scale) < 10:
117		return "%*.1f%s" % (sz - 1, num, suffix[index])
118	else:
119		return "%*d%s" % (sz - 1, num, suffix[index])
120
121def print_header():
122	global hdr
123	global sep
124	for col in hdr:
125		new_col = col
126		if interval > 0 and cols[col][1] > 100:
127			new_col += "/s"
128		sys.stdout.write("%*s%s" % (cols[col][0], new_col, sep))
129	sys.stdout.write("\n")
130
131def print_values(v):
132	global hdr
133	global sep
134	for col in hdr:
135		val = v[cols[col][2]]
136		if interval > 0 and cols[col][1] > 100:
137			val = v[cols[col][2]] // interval
138		sys.stdout.write("%s%s" % (
139			prettynum(cols[col][0], cols[col][1], val), sep))
140	sys.stdout.write("\n")
141
142def print_dict(d):
143	for pool in d:
144		for objset in d[pool]:
145			print_values(d[pool][objset])
146
147def detailed_usage():
148	sys.stderr.write("%s\n" % cmd)
149	sys.stderr.write("Field definitions are as follows:\n")
150	for key in cols:
151		sys.stderr.write("%11s : %s\n" % (key, cols[key][2]))
152	sys.stderr.write("\n")
153	sys.exit(0)
154
155def init():
156	global pool_name
157	global dataset_name
158	global interval
159	global hdr
160	global curr
161	global gFlag
162	global sep
163
164	curr = dict()
165
166	parser = argparse.ArgumentParser(description='Program to print zilstats',
167                                	 add_help=True,
168					 formatter_class=RawTextHelpFormatter,
169					 epilog="\nUsage Examples\n"\
170				 		"Note: Global zilstats is shown by default,"\
171						" if none of a|p|d option is not provided\n"\
172				 		"\tzilstat -a\n"\
173						'\tzilstat -v\n'\
174						'\tzilstat -p tank\n'\
175						'\tzilstat -d tank/d1,tank/d2,tank/zv1\n'\
176						'\tzilstat -i 1\n'\
177						'\tzilstat -s \"***\"\n'\
178						'\tzilstat -f zcwc,zimnb,zimsb\n')
179
180	parser.add_argument(
181		"-v", "--verbose",
182		action="store_true",
183		help="List field headers and definitions"
184	)
185
186	pool_grp = parser.add_mutually_exclusive_group()
187
188	pool_grp.add_argument(
189		"-a", "--all",
190		action="store_true",
191		dest="all",
192		help="Print all dataset stats"
193	)
194
195	pool_grp.add_argument(
196		"-p", "--pool",
197		type=str,
198		help="Print stats for all datasets of a speicfied pool"
199	)
200
201	pool_grp.add_argument(
202		"-d", "--dataset",
203		type=str,
204		help="Print given dataset(s) (Comma separated)"
205	)
206
207	parser.add_argument(
208		"-f", "--columns",
209		type=str,
210		help="Specify specific fields to print (see -v)"
211	)
212
213	parser.add_argument(
214		"-s", "--separator",
215		type=str,
216		help="Override default field separator with custom "
217			 "character or string"
218	)
219
220	parser.add_argument(
221		"-i", "--interval",
222		type=int,
223		dest="interval",
224		help="Print stats between specified interval"
225			 " (in seconds)"
226	)
227
228	parsed_args = parser.parse_args()
229
230	if parsed_args.verbose:
231		detailed_usage()
232
233	if parsed_args.all:
234		gFlag = False
235
236	if parsed_args.interval:
237		interval = parsed_args.interval
238
239	if parsed_args.pool:
240		pool_name = parsed_args.pool
241		gFlag = False
242
243	if parsed_args.dataset:
244		dataset_name = parsed_args.dataset
245		gFlag = False
246
247	if parsed_args.separator:
248		sep = parsed_args.separator
249
250	if gFlag:
251		hdr = ghdr
252
253	if parsed_args.columns:
254		hdr = parsed_args.columns.split(",")
255
256		invalid = []
257		for ele in hdr:
258			if ele not in cols:
259				invalid.append(ele)
260
261		if len(invalid) > 0:
262			sys.stderr.write("Invalid column definition! -- %s\n" % invalid)
263			sys.exit(1)
264
265	if pool_name and dataset_name:
266		print ("Error: Can not filter both dataset and pool")
267		sys.exit(1)
268
269def FileCheck(fname):
270	try:
271		return (open(fname))
272	except IOError:
273		print ("Unable to open zilstat proc file: " + fname)
274		sys.exit(1)
275
276if sys.platform.startswith('freebsd'):
277	# Requires py-sysctl on FreeBSD
278	import sysctl
279
280	def kstat_update(pool = None, objid = None):
281		global kstat
282		kstat = {}
283		if not pool:
284			file = "kstat.zfs.misc.zil"
285			k = [ctl for ctl in sysctl.filter(file) \
286				if ctl.type != sysctl.CTLTYPE_NODE]
287			kstat_process_str(k, file, "GLOBAL", len(file + "."))
288		elif objid:
289			file = "kstat.zfs." + pool + ".dataset.objset-" + objid
290			k = [ctl for ctl in sysctl.filter(file) if ctl.type \
291				!= sysctl.CTLTYPE_NODE]
292			kstat_process_str(k, file, objid, len(file + "."))
293		else:
294			file = "kstat.zfs." + pool + ".dataset"
295			zil_start = len(file + ".")
296			obj_start = len("kstat.zfs." + pool + ".")
297			k = [ctl for ctl in sysctl.filter(file)
298				if ctl.type != sysctl.CTLTYPE_NODE]
299			for s in k:
300				if not s or (s.name.find("zil") == -1 and \
301					s.name.find("dataset_name") == -1):
302					continue
303				name, value = s.name, s.value
304				objid = re.findall(r'0x[0-9A-F]+', \
305					name[obj_start:], re.I)[0]
306				if objid not in kstat:
307					kstat[objid] = dict()
308				zil_start = len(file + ".objset-" + \
309					objid + ".")
310				kstat[objid][name[zil_start:]] = value \
311					if (name.find("dataset_name")) \
312					else int(value)
313
314	def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0):
315			global kstat
316			if not k:
317				print("Unable to process kstat for: " + file)
318				sys.exit(1)
319			kstat[objset] = dict()
320			for s in k:
321				if not s or (s.name.find("zil") == -1 and \
322				    s.name.find("dataset_name") == -1):
323					continue
324				name, value = s.name, s.value
325				kstat[objset][name[zil_start:]] = value \
326				    if (name.find("dataset_name")) else int(value)
327
328elif sys.platform.startswith('linux'):
329	def kstat_update(pool = None, objid = None):
330		global kstat
331		kstat = {}
332		if not pool:
333			k = [line.strip() for line in \
334				FileCheck("/proc/spl/kstat/zfs/zil")]
335			kstat_process_str(k, "/proc/spl/kstat/zfs/zil")
336		elif objid:
337			file = "/proc/spl/kstat/zfs/" + pool + "/objset-" + objid
338			k = [line.strip() for line in FileCheck(file)]
339			kstat_process_str(k, file, objid)
340		else:
341			if not os.path.exists(f"/proc/spl/kstat/zfs/{pool}"):
342				print("Pool \"" + pool + "\" does not exist, Exitting")
343				sys.exit(1)
344			objsets = os.listdir(f'/proc/spl/kstat/zfs/{pool}')
345			for objid in objsets:
346				if objid.find("objset-") == -1:
347					continue
348				file = "/proc/spl/kstat/zfs/" + pool + "/" + objid
349				k = [line.strip() for line in FileCheck(file)]
350				kstat_process_str(k, file, objid.replace("objset-", ""))
351
352	def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0):
353			global kstat
354			if not k:
355				print("Unable to process kstat for: " + file)
356				sys.exit(1)
357
358			kstat[objset] = dict()
359			for s in k:
360				if not s or (s.find("zil") == -1 and \
361				    s.find("dataset_name") == -1):
362					continue
363				name, unused, value = s.split()
364				kstat[objset][name] = value \
365				    if (name == "dataset_name") else int(value)
366
367def zil_process_kstat():
368	global curr, pool_name, dataset_name, dsFlag, ds_pairs
369	curr.clear()
370	if gFlag == True:
371		kstat_update()
372		zil_build_dict()
373	else:
374		if pool_name:
375			kstat_update(pool_name)
376			zil_build_dict(pool_name)
377		elif dataset_name:
378			if dsFlag == False:
379				dsFlag = True
380				datasets = dataset_name.split(',')
381				ds_pairs = defaultdict(list)
382				for ds in datasets:
383					try:
384						objid = subprocess.check_output(['zfs',
385						    'list', '-Hpo', 'objsetid', ds], \
386						    stderr=subprocess.DEVNULL) \
387						    .decode('utf-8').strip()
388					except subprocess.CalledProcessError as e:
389						print("Command: \"zfs list -Hpo objset "\
390						+ str(ds) + "\" failed with error code:"\
391						+ str(e.returncode))
392						print("Please make sure that dataset \""\
393						+ str(ds) + "\" exists")
394						sys.exit(1)
395					if not objid:
396						continue
397					ds_pairs[ds.split('/')[0]]. \
398						append(hex(int(objid)))
399			for pool, objids in ds_pairs.items():
400				for objid in objids:
401					kstat_update(pool, objid)
402					zil_build_dict(pool)
403		else:
404			try:
405				pools = subprocess.check_output(['zpool', 'list', '-Hpo',\
406				    'name']).decode('utf-8').split()
407			except subprocess.CalledProcessError as e:
408				print("Command: \"zpool list -Hpo name\" failed with error"\
409				    "code: " + str(e.returncode))
410				sys.exit(1)
411			for pool in pools:
412				kstat_update(pool)
413				zil_build_dict(pool)
414
415def calculate_diff():
416	global curr, diff
417	prev = copy.deepcopy(curr)
418	zil_process_kstat()
419	diff = copy.deepcopy(curr)
420	for pool in curr:
421		for objset in curr[pool]:
422			for key in curr[pool][objset]:
423				if not isinstance(diff[pool][objset][key], int):
424					continue
425				# If prev is NULL, this is the
426				# first time we are here
427				if not prev:
428					diff[pool][objset][key] = 0
429				else:
430					diff[pool][objset][key] \
431						= curr[pool][objset][key] \
432						- prev[pool][objset][key]
433
434def zil_build_dict(pool = "GLOBAL"):
435	global kstat
436	for objset in kstat:
437		for key in kstat[objset]:
438			val = kstat[objset][key]
439			if pool not in curr:
440				curr[pool] = dict()
441			if objset not in curr[pool]:
442				curr[pool][objset] = dict()
443			curr[pool][objset][key] = val
444
445def zil_extend_dict():
446	global diff
447	for pool in diff:
448		for objset in diff[pool]:
449			diff[pool][objset]["pool"] = pool
450			diff[pool][objset]["objset"] = objset
451			diff[pool][objset]["time"] = time.strftime("%H:%M:%S", \
452				time.localtime())
453			diff[pool][objset]["icc+inc"] = \
454				diff[pool][objset]["zil_itx_copied_count"] + \
455				diff[pool][objset]["zil_itx_needcopy_count"]
456			diff[pool][objset]["icb+inb"] = \
457				diff[pool][objset]["zil_itx_copied_bytes"] + \
458				diff[pool][objset]["zil_itx_needcopy_bytes"]
459			diff[pool][objset]["iic+idc"] = \
460				diff[pool][objset]["zil_itx_indirect_count"] + \
461				diff[pool][objset]["zil_itx_copied_count"] + \
462				diff[pool][objset]["zil_itx_needcopy_count"]
463			diff[pool][objset]["iib+idb"] = \
464				diff[pool][objset]["zil_itx_indirect_bytes"] + \
465				diff[pool][objset]["zil_itx_copied_bytes"] + \
466				diff[pool][objset]["zil_itx_needcopy_bytes"]
467			diff[pool][objset]["imnc+imsc"] = \
468				diff[pool][objset]["zil_itx_metaslab_normal_count"] + \
469				diff[pool][objset]["zil_itx_metaslab_slog_count"]
470			diff[pool][objset]["imnb+imsb"] = \
471				diff[pool][objset]["zil_itx_metaslab_normal_bytes"] + \
472				diff[pool][objset]["zil_itx_metaslab_slog_bytes"]
473			diff[pool][objset]["imnw+imsw"] = \
474				diff[pool][objset]["zil_itx_metaslab_normal_write"] + \
475				diff[pool][objset]["zil_itx_metaslab_slog_write"]
476			diff[pool][objset]["imna+imsa"] = \
477				diff[pool][objset]["zil_itx_metaslab_normal_alloc"] + \
478				diff[pool][objset]["zil_itx_metaslab_slog_alloc"]
479			if diff[pool][objset]["imna+imsa"] > 0:
480				diff[pool][objset]["imb/ima"] = 100 * \
481					diff[pool][objset]["imnb+imsb"] // \
482					diff[pool][objset]["imna+imsa"]
483			else:
484				diff[pool][objset]["imb/ima"] = 100
485			if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0:
486				diff[pool][objset]["imnb/imna"] = 100 * \
487					diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \
488					diff[pool][objset]["zil_itx_metaslab_normal_alloc"]
489			else:
490				diff[pool][objset]["imnb/imna"] = 100
491			if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0:
492				diff[pool][objset]["imsb/imsa"] = 100 * \
493					diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \
494					diff[pool][objset]["zil_itx_metaslab_slog_alloc"]
495			else:
496				diff[pool][objset]["imsb/imsa"] = 100
497			if diff[pool][objset]["imnw+imsw"] > 0:
498				diff[pool][objset]["imb/imw"] = 100 * \
499					diff[pool][objset]["imnb+imsb"] // \
500					diff[pool][objset]["imnw+imsw"]
501			else:
502				diff[pool][objset]["imb/imw"] = 100
503			if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0:
504				diff[pool][objset]["imnb/imnw"] = 100 * \
505					diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \
506					diff[pool][objset]["zil_itx_metaslab_normal_write"]
507			else:
508				diff[pool][objset]["imnb/imnw"] = 100
509			if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0:
510				diff[pool][objset]["imsb/imsw"] = 100 * \
511					diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \
512					diff[pool][objset]["zil_itx_metaslab_slog_write"]
513			else:
514				diff[pool][objset]["imsb/imsw"] = 100
515
516def sign_handler_epipe(sig, frame):
517	print("Caught EPIPE signal: " + str(frame))
518	print("Exitting...")
519	sys.exit(0)
520
521def main():
522	global interval
523	global curr, diff
524	hprint = False
525	init()
526	signal.signal(signal.SIGINT, signal.SIG_DFL)
527	signal.signal(signal.SIGPIPE, sign_handler_epipe)
528
529	zil_process_kstat()
530	if not curr:
531		print ("Error: No stats to show")
532		sys.exit(0)
533	print_header()
534	if interval > 0:
535		time.sleep(interval)
536		while True:
537			calculate_diff()
538			if not diff:
539				print ("Error: No stats to show")
540				sys.exit(0)
541			zil_extend_dict()
542			print_dict(diff)
543			time.sleep(interval)
544	else:
545		diff = curr
546		zil_extend_dict()
547		print_dict(diff)
548
549if __name__ == '__main__':
550	main()
551
552