Mini Shell
#!/opt/imh-python/bin/python3
import argparse
import configparser
from pathlib import Path
import sys
import re
from pymysql.optionfile import Parser as PyMySQLParser
USER_RE = re.compile(r'^# User@Host:\s+([a-z0-9]+)')
STATS_RE = re.compile(
r'^# Query_time:\s+([0-9\.]+)\s+Lock_time:\s+([0-9\.]+)\s+'
r'Rows_sent:\s+(\d+)\s+Rows_examined:\s+(\d+)'
)
def parse_args():
parser = argparse.ArgumentParser()
# fmt: off
parser.add_argument(
"-q", "--quiet", dest="quiet", action='store_true',
help="Suppress stderr output",
)
parser.add_argument(
'-H', '--no-header', dest='no_header', action='store_true',
help='Suppress column headers',
)
parser.add_argument(
"-o", "--output", metavar="FILE",
help="Write output to FILE (default: stdout)",
)
parser.add_argument(
"-u", "--user", metavar="USER", default=None,
help="Output USER's queries instead of tallys",
)
parser.add_argument(
"-a", "--average", action="store_true",
help="Print averages per query instead of totals",
)
parser.add_argument('logpath', nargs='?', help='Path to slow query log')
# fmt: on
return parser.parse_args()
class MySQLUser:
"""Holds a user name and tracks numbers of queries"""
def __init__(self, username: str):
self.username = username
self.num_queries = 0
self.query_time = 0.0
self.lock_time = 0.0
self.rows_sent = 0
self.rows_examined = 0
def add_query(self, stats_match: re.Match):
query_time, lock_time, rows_sent, rows_examined = stats_match.groups()
self.num_queries += 1
self.query_time += float(query_time)
self.lock_time += float(lock_time)
self.rows_sent += int(rows_sent)
self.rows_examined += int(rows_examined)
@classmethod
def row_header(cls, file=sys.stdout):
cls.header(
['QUERIES', 'TIME', 'LOCKTIME', 'ROWSSENT', 'ROWSRECVD'], file=file
)
@classmethod
def avg_header(cls, file=sys.stdout):
cls.header(
['QUERIES', 'TIME/Q', 'LOCKTIME/Q', 'ROWSSENT/Q', 'ROWSRECV/Q'],
file=file,
)
@staticmethod
def header(cols: list[str], file=sys.stdout):
print('USER'.rjust(16), end=' ', file=file)
print(*map(lambda x: x.rjust(10), cols), file=file)
def row_print(self, file=sys.stdout):
print(
self.username.rjust(16),
str(self.num_queries).rjust(10),
str(int(self.query_time)).rjust(10),
str(int(self.lock_time)).rjust(10),
str(self.rows_sent).rjust(10),
str(self.rows_examined).rjust(10),
file=file,
)
def avg_print(self, file=sys.stdout):
print(
self.username.rjust(16),
str(self.num_queries).rjust(10),
str(int(self.query_time / self.num_queries)).rjust(10),
str(int(self.lock_time / self.num_queries)).rjust(10),
str(int(self.rows_sent / self.num_queries)).rjust(10),
str(int(self.rows_examined / self.num_queries)).rjust(10),
file=file,
)
def default_log_path():
try:
parser = PyMySQLParser(strict=False)
if not parser.read('/etc/my.cnf'):
return None
path = Path(parser.get('mysqld', 'slow_query_log_file')).resolve()
if path == Path('/dev/null'):
print("MySQL log points to /dev/null currently", file=sys.stderr)
return None
return str(path)
except configparser.Error:
return None
def open_log(args):
# if nothing piped to stdin and no path supplied
if not args.logpath and sys.stdin.isatty():
query_log = default_log_path()
if not query_log:
print(
"Failed to get slow query log path from /etc/my.cnf",
file=sys.stderr,
)
sys.exit(1)
if not args.quiet:
print(
f"Reading from the default log file, `{query_log}'",
file=sys.stderr,
)
return open(query_log, encoding='utf-8', errors='replace')
# if something piped to stdin with no path supplied, or explicitly sent -
if not args.logpath or args.logpath == '-':
query_log = sys.stdin
if not args.quiet:
print(
"MySQL slow query log parser reading from stdin/pipe...",
file=sys.stderr,
)
return sys.stdin
return open(args.logpath, encoding='utf-8', errors='replace')
def iter_log(args):
try:
with open_log(args) as query_log:
while line := query_log.readline():
yield line
except OSError as exc:
sys.exit(exc)
def main():
args = parse_args()
if args.output: # if we've specified an output file
out_file = open(args.output, "w", encoding='utf-8')
else:
out_file = sys.stdout
with out_file:
# init user and id dictionaries
user_table: dict[str, MySQLUser] = {}
this_user = "NO_SUCH_USER"
for line in iter_log(args):
if user_match := USER_RE.match(line):
this_user = user_match.group(1)
if args.user and this_user == args.user:
print(line, end=' ', file=out_file)
elif stats_match := STATS_RE.match(line):
if this_user not in user_table:
user_table[this_user] = MySQLUser(this_user)
user_table[this_user].add_query(stats_match)
if args.user and this_user == args.user:
print(line, end=' ', file=out_file)
elif args.user and this_user == args.user:
try:
print(line, end='', file=out_file)
except Exception:
sys.exit(0)
if args.user:
return
if not args.no_header:
if args.average:
MySQLUser.avg_header(out_file)
else:
MySQLUser.row_header(out_file)
for data in sorted(user_table.values(), key=lambda x: x.num_queries):
if args.average:
data.avg_print(out_file)
else:
data.row_print(out_file)
if __name__ == '__main__':
main()
Zerion Mini Shell 1.0