Edit File by line
/home/barbar84/public_h.../wp-conte.../plugins/sujqvwi/AnonR/anonr.TX.../opt/sharedra.../mysql
File: queryparser.py
#!/opt/imh-python/bin/python3
[0] Fix | Delete
"""Parses MySQL general query logs"""
[1] Fix | Delete
import configparser
[2] Fix | Delete
import sys
[3] Fix | Delete
from pathlib import Path
[4] Fix | Delete
import re
[5] Fix | Delete
from datetime import datetime, timedelta
[6] Fix | Delete
import argparse
[7] Fix | Delete
from typing import IO, Union
[8] Fix | Delete
from pymysql.optionfile import Parser as PyMySQLParser
[9] Fix | Delete
[10] Fix | Delete
[11] Fix | Delete
def parse_args():
[12] Fix | Delete
parser = argparse.ArgumentParser(description=__doc__)
[13] Fix | Delete
# fmt: off
[14] Fix | Delete
parser.add_argument(
[15] Fix | Delete
"-q", "--quiet", action="store_false", dest="verbose",
[16] Fix | Delete
help="Suppress non-error output",
[17] Fix | Delete
)
[18] Fix | Delete
parser.add_argument(
[19] Fix | Delete
"-o", "--output", metavar="FILE",
[20] Fix | Delete
help="Write output to FILE (default: stdout)",
[21] Fix | Delete
)
[22] Fix | Delete
parser.add_argument(
[23] Fix | Delete
"-r", "--regex", type=re.compile, metavar="REGEX",
[24] Fix | Delete
help="Tally arbitrary REGEX string (slow)",
[25] Fix | Delete
)
[26] Fix | Delete
display = parser.add_mutually_exclusive_group()
[27] Fix | Delete
display.add_argument(
[28] Fix | Delete
"-u", "--user", metavar="USER",
[29] Fix | Delete
help="Output USER's queries instead of summary",
[30] Fix | Delete
)
[31] Fix | Delete
display.add_argument(
[32] Fix | Delete
'-s', '--sort', default='total',
[33] Fix | Delete
choices=['select', 'insert', 'update', 'replace', 'regex', 'total'],
[34] Fix | Delete
help='Sort summary by a type of query',
[35] Fix | Delete
)
[36] Fix | Delete
parser.add_argument(
[37] Fix | Delete
'filename', nargs='?',
[38] Fix | Delete
help='file to read from. optional - defaults to try stdin',
[39] Fix | Delete
)
[40] Fix | Delete
# fmt: on
[41] Fix | Delete
args = parser.parse_args()
[42] Fix | Delete
if args.filename == '-':
[43] Fix | Delete
args.filename = None
[44] Fix | Delete
return args
[45] Fix | Delete
[46] Fix | Delete
[47] Fix | Delete
class MySQLUser:
[48] Fix | Delete
"""Holds a user name and tracks numbers of queries"""
[49] Fix | Delete
[50] Fix | Delete
num_select: int
[51] Fix | Delete
num_insert: int
[52] Fix | Delete
num_update: int
[53] Fix | Delete
num_replace: int
[54] Fix | Delete
num_regex: int
[55] Fix | Delete
[56] Fix | Delete
def __init__(self):
[57] Fix | Delete
self.num_select = 0
[58] Fix | Delete
self.num_insert = 0
[59] Fix | Delete
self.num_update = 0
[60] Fix | Delete
self.num_replace = 0
[61] Fix | Delete
self.num_regex = 0
[62] Fix | Delete
[63] Fix | Delete
@property
[64] Fix | Delete
def num_total(self) -> int:
[65] Fix | Delete
return sum(
[66] Fix | Delete
(
[67] Fix | Delete
self.num_select,
[68] Fix | Delete
self.num_insert,
[69] Fix | Delete
self.num_update,
[70] Fix | Delete
self.num_replace,
[71] Fix | Delete
self.num_regex,
[72] Fix | Delete
)
[73] Fix | Delete
)
[74] Fix | Delete
[75] Fix | Delete
@staticmethod
[76] Fix | Delete
def header(qps: bool, reg: bool, file=sys.stdout):
[77] Fix | Delete
cols = ['Sel', 'Upd', 'Ins', 'Repl']
[78] Fix | Delete
if reg:
[79] Fix | Delete
cols.append('Regex')
[80] Fix | Delete
print('User'.rjust(16), end='', file=file)
[81] Fix | Delete
for col in cols:
[82] Fix | Delete
print('', f"Num{col}".rjust(8), end='', file=file)
[83] Fix | Delete
if qps:
[84] Fix | Delete
print('', f"{col}/s".rjust(8), end='', file=file)
[85] Fix | Delete
[86] Fix | Delete
def show(self, total_secs: float, reg: bool, file=sys.stdout):
[87] Fix | Delete
cols = ['select', 'update', 'insert', 'replace']
[88] Fix | Delete
if reg:
[89] Fix | Delete
cols.append('regex')
[90] Fix | Delete
for col in cols:
[91] Fix | Delete
val: int = getattr(self, f'num_{col}')
[92] Fix | Delete
print('', str(val).rjust(8), end='', file=file)
[93] Fix | Delete
if total_secs != 0:
[94] Fix | Delete
print(
[95] Fix | Delete
'',
[96] Fix | Delete
f"{int(val / total_secs)}qps".rjust(8),
[97] Fix | Delete
end='',
[98] Fix | Delete
file=file,
[99] Fix | Delete
)
[100] Fix | Delete
print(file=file)
[101] Fix | Delete
[102] Fix | Delete
[103] Fix | Delete
class TimeTracker:
[104] Fix | Delete
def __init__(self):
[105] Fix | Delete
self.first_date: Union[str, None] = None
[106] Fix | Delete
self.last_date: Union[str, None] = None
[107] Fix | Delete
self.total_time = timedelta()
[108] Fix | Delete
[109] Fix | Delete
def add_to_total(self) -> None:
[110] Fix | Delete
first = self.first_datetime
[111] Fix | Delete
last = self.last_datetime
[112] Fix | Delete
if first and last: # not None
[113] Fix | Delete
self.total_time += last - first
[114] Fix | Delete
[115] Fix | Delete
@property
[116] Fix | Delete
def first_datetime(self) -> Union[datetime, None]:
[117] Fix | Delete
if self.first_date:
[118] Fix | Delete
return self.stamp_to_datetime(self.first_date)
[119] Fix | Delete
return None
[120] Fix | Delete
[121] Fix | Delete
@property
[122] Fix | Delete
def last_datetime(self) -> Union[datetime, None]:
[123] Fix | Delete
if self.last_date:
[124] Fix | Delete
return self.stamp_to_datetime(self.last_date)
[125] Fix | Delete
return None
[126] Fix | Delete
[127] Fix | Delete
@staticmethod
[128] Fix | Delete
def stamp_to_datetime(mysql_stamp: str) -> datetime:
[129] Fix | Delete
"""convert mysql timestamp to datetime object"""
[130] Fix | Delete
return datetime.strptime(mysql_stamp, '%y%m%d %H:%M:%S')
[131] Fix | Delete
[132] Fix | Delete
def print_age(self):
[133] Fix | Delete
if first := self.first_datetime:
[134] Fix | Delete
time_delta = datetime.now() - first
[135] Fix | Delete
total_seconds = time_delta.total_seconds()
[136] Fix | Delete
print(
[137] Fix | Delete
f"First timestamp at {self.first_date}",
[138] Fix | Delete
f"({int(total_seconds / 3600)} hours,",
[139] Fix | Delete
f"{int(total_seconds / 60 % 60)} minutes,",
[140] Fix | Delete
f"{int(total_seconds % 60)} seconds ago)",
[141] Fix | Delete
file=sys.stderr,
[142] Fix | Delete
)
[143] Fix | Delete
else:
[144] Fix | Delete
print("No timestamps found in log file")
[145] Fix | Delete
[146] Fix | Delete
[147] Fix | Delete
class StateTracker:
[148] Fix | Delete
def __init__(self, verbose: bool):
[149] Fix | Delete
self.query_id = "0"
[150] Fix | Delete
self.username = "NO_SUCH_USER"
[151] Fix | Delete
self.verbose = verbose
[152] Fix | Delete
self.id_table: dict[str, str] = {}
[153] Fix | Delete
self.user_table: dict[str, MySQLUser] = {}
[154] Fix | Delete
self.times = TimeTracker()
[155] Fix | Delete
[156] Fix | Delete
def handle_match(self, line: str, match: re.Match) -> None:
[157] Fix | Delete
if parsed_date := match.group(1): # if it's got a date group
[158] Fix | Delete
if not self.times.first_date: # and we've never set a date before
[159] Fix | Delete
self.times.first_date = parsed_date # set our first date
[160] Fix | Delete
if self.verbose:
[161] Fix | Delete
self.times.print_age()
[162] Fix | Delete
self.times.last_date = parsed_date # set our last date
[163] Fix | Delete
if match.group(3) == "Connect": # if it's a connection
[164] Fix | Delete
self.query_id = match.group(2) # get the query id
[165] Fix | Delete
if self.query_id in self.id_table:
[166] Fix | Delete
# We have hit a SERIOUS problem. This likely means that
[167] Fix | Delete
# mysql restarted. We're dumping the time and query_id
[168] Fix | Delete
# lookup tables.
[169] Fix | Delete
if 'Access denied for user' in line or ' as on' in line:
[170] Fix | Delete
return
[171] Fix | Delete
self.times.add_to_total()
[172] Fix | Delete
# don't have to do the user table because that data in
[173] Fix | Delete
# theory is still good (qps = total queries / total time)
[174] Fix | Delete
self.id_table.clear()
[175] Fix | Delete
self.times.last_date = None
[176] Fix | Delete
self.times.first_date = None
[177] Fix | Delete
self.username = match.group(4) # set user_name
[178] Fix | Delete
# create the entry with user name as the value and the id as
[179] Fix | Delete
# the index
[180] Fix | Delete
self.id_table[self.query_id] = self.username
[181] Fix | Delete
# if the user name is new (could be, could already exist)
[182] Fix | Delete
if self.username not in self.user_table:
[183] Fix | Delete
# create a new counter class for it using the user name
[184] Fix | Delete
# as the lookup key
[185] Fix | Delete
self.user_table[self.username] = MySQLUser()
[186] Fix | Delete
elif match.group(3) in ("Query", "Execute"):
[187] Fix | Delete
# if this is a query ...
[188] Fix | Delete
self.query_id = match.group(2) # get the id
[189] Fix | Delete
try:
[190] Fix | Delete
# get the user name from our lookup table
[191] Fix | Delete
# (the user who started it)
[192] Fix | Delete
self.username = self.id_table[self.query_id]
[193] Fix | Delete
except KeyError:
[194] Fix | Delete
self.username = "NO_SUCH_USER"
[195] Fix | Delete
if self.username not in self.user_table:
[196] Fix | Delete
self.user_table[self.username] = MySQLUser()
[197] Fix | Delete
# get the type of query (select, insert, update, etc.)
[198] Fix | Delete
query_type = match.group(4).lower()
[199] Fix | Delete
if query_type == "select":
[200] Fix | Delete
self.user_table[self.username].num_select += 1
[201] Fix | Delete
elif query_type == "update":
[202] Fix | Delete
self.user_table[self.username].num_update += 1
[203] Fix | Delete
elif query_type == "insert":
[204] Fix | Delete
self.user_table[self.username].num_insert += 1
[205] Fix | Delete
elif query_type == "replace":
[206] Fix | Delete
self.user_table[self.username].num_replace += 1
[207] Fix | Delete
else: # must be init db, prepare, or execute
[208] Fix | Delete
query_id = match.group(2) # get the id
[209] Fix | Delete
try:
[210] Fix | Delete
# get the user name from our lookup table
[211] Fix | Delete
# (the user who started it)
[212] Fix | Delete
self.username = self.id_table[query_id]
[213] Fix | Delete
except KeyError:
[214] Fix | Delete
self.username = "NO_SUCH_USER"
[215] Fix | Delete
if self.username not in self.user_table:
[216] Fix | Delete
self.user_table[self.username] = MySQLUser()
[217] Fix | Delete
[218] Fix | Delete
def handle_user_match(self, match: re.Match) -> None:
[219] Fix | Delete
try:
[220] Fix | Delete
# dirty trick. Try to get the ID, but what if the match
[221] Fix | Delete
# wasn't a query and didn't match our regex?
[222] Fix | Delete
self.query_id = match.group(2)
[223] Fix | Delete
except Exception:
[224] Fix | Delete
# we can re-use the last query_id, which hasn't been unset
[225] Fix | Delete
# since the last matching Query! That makes the user_name
[226] Fix | Delete
# likely to be the same as well, so we reuse it
[227] Fix | Delete
pass
[228] Fix | Delete
try:
[229] Fix | Delete
# get the user name from our lookup table
[230] Fix | Delete
# (the user who started it)
[231] Fix | Delete
self.username = self.id_table[self.query_id]
[232] Fix | Delete
except KeyError:
[233] Fix | Delete
self.username = "NO_SUCH_USER"
[234] Fix | Delete
if not self.username in self.user_table:
[235] Fix | Delete
self.user_table[self.username] = MySQLUser()
[236] Fix | Delete
self.user_table[self.username].num_regex += 1
[237] Fix | Delete
[238] Fix | Delete
[239] Fix | Delete
def gen_log_path() -> Union[str, None]:
[240] Fix | Delete
"""Reads mysqld.general_log_file from my.cnf"""
[241] Fix | Delete
try:
[242] Fix | Delete
parser = PyMySQLParser(strict=False)
[243] Fix | Delete
if not parser.read('/etc/my.cnf'):
[244] Fix | Delete
return None
[245] Fix | Delete
path = Path(parser.get('mysqld', 'general_log_file')).resolve()
[246] Fix | Delete
if path == Path('/dev/null'):
[247] Fix | Delete
print("MySQL log points to /dev/null currently", file=sys.stderr)
[248] Fix | Delete
return None
[249] Fix | Delete
return str(path)
[250] Fix | Delete
except configparser.Error:
[251] Fix | Delete
return None
[252] Fix | Delete
[253] Fix | Delete
[254] Fix | Delete
def open_log(args) -> IO:
[255] Fix | Delete
"""Finds/Opens query log"""
[256] Fix | Delete
if not args.filename and sys.stdin.isatty():
[257] Fix | Delete
args.filename = gen_log_path()
[258] Fix | Delete
if args.filename is None:
[259] Fix | Delete
sys.exit("Could not get default log file from /etc/my.cnf")
[260] Fix | Delete
if args.verbose:
[261] Fix | Delete
print(
[262] Fix | Delete
f"Reading from the default log file, `{args.filename}'",
[263] Fix | Delete
file=sys.stderr,
[264] Fix | Delete
)
[265] Fix | Delete
if args.filename:
[266] Fix | Delete
try:
[267] Fix | Delete
return open(args.filename, encoding='utf-8', errors='replace')
[268] Fix | Delete
except OSError as exc:
[269] Fix | Delete
sys.exit(f"Failed to open log file `{args.filename}': {exc}")
[270] Fix | Delete
if args.verbose:
[271] Fix | Delete
print(
[272] Fix | Delete
"MySQL general query log parser reading from stdin/pipe...",
[273] Fix | Delete
file=sys.stderr,
[274] Fix | Delete
)
[275] Fix | Delete
return sys.stdin
[276] Fix | Delete
[277] Fix | Delete
[278] Fix | Delete
def parse_log(
[279] Fix | Delete
query_log: IO,
[280] Fix | Delete
user_regex: Union[re.Pattern, None],
[281] Fix | Delete
user: Union[str, None],
[282] Fix | Delete
state: StateTracker,
[283] Fix | Delete
out_file: IO,
[284] Fix | Delete
) -> StateTracker:
[285] Fix | Delete
[286] Fix | Delete
# Search entry v2, group(1)=(None|Timestamp), group(2)=(ConnectionID),
[287] Fix | Delete
# group(3)=(Connect|Query), group(4)=(UserName|QueryType)
[288] Fix | Delete
search_re = re.compile(
[289] Fix | Delete
r"([0-9]{6}[\s]+[0-9:]+)*[\s]+([0-9]+)\s"
[290] Fix | Delete
r"(Connect|Query|Init DB|Prepare|Execute)[\s]+([a-zA-Z0-9]+)"
[291] Fix | Delete
)
[292] Fix | Delete
[293] Fix | Delete
# main parser loop
[294] Fix | Delete
while line := query_log.readline():
[295] Fix | Delete
match = search_re.match(line)
[296] Fix | Delete
user_match = user_regex.search(line) if user_regex else None
[297] Fix | Delete
if not match and not user_match:
[298] Fix | Delete
continue
[299] Fix | Delete
if match:
[300] Fix | Delete
state.handle_match(line=line, match=match)
[301] Fix | Delete
if user_match:
[302] Fix | Delete
state.handle_user_match(match=match)
[303] Fix | Delete
# --user was supplied and matches this line
[304] Fix | Delete
if user and state.username == user:
[305] Fix | Delete
try:
[306] Fix | Delete
print(line, end='', file=out_file)
[307] Fix | Delete
except Exception:
[308] Fix | Delete
sys.exit(0)
[309] Fix | Delete
return state
[310] Fix | Delete
[311] Fix | Delete
[312] Fix | Delete
def summarize(
[313] Fix | Delete
state: StateTracker,
[314] Fix | Delete
sort_by: str,
[315] Fix | Delete
out_file: IO,
[316] Fix | Delete
verbose: bool,
[317] Fix | Delete
user_regex: Union[re.Pattern, None],
[318] Fix | Delete
user: Union[str, None],
[319] Fix | Delete
):
[320] Fix | Delete
if user: # we were in per-user mode. Skip summary page
[321] Fix | Delete
return
[322] Fix | Delete
if not state.times.first_date: # no timestamps found at all
[323] Fix | Delete
sys.exit("Not enough data to parse, please try a longer log file.")
[324] Fix | Delete
total_secs = state.times.total_time.total_seconds()
[325] Fix | Delete
show_reg = user_regex is not None
[326] Fix | Delete
if total_secs == 0:
[327] Fix | Delete
print('Not enough timestamps logged to display QPS', file=out_file)
[328] Fix | Delete
sorted_entries = sorted(
[329] Fix | Delete
state.user_table.items(),
[330] Fix | Delete
key=lambda x: getattr(x[1], sort_by),
[331] Fix | Delete
)
[332] Fix | Delete
if verbose:
[333] Fix | Delete
MySQLUser.header(qps=total_secs != 0, reg=show_reg, file=out_file)
[334] Fix | Delete
print(file=out_file)
[335] Fix | Delete
for username, counts in sorted_entries:
[336] Fix | Delete
print(username.rjust(16), end='', file=out_file)
[337] Fix | Delete
counts.show(total_secs=total_secs, reg=show_reg, file=out_file)
[338] Fix | Delete
[339] Fix | Delete
[340] Fix | Delete
def main():
[341] Fix | Delete
args = parse_args()
[342] Fix | Delete
# determine where to write output
[343] Fix | Delete
if args.output:
[344] Fix | Delete
out_file = open(args.output, "w", encoding='utf-8')
[345] Fix | Delete
else:
[346] Fix | Delete
out_file = sys.stdout
[347] Fix | Delete
with out_file:
[348] Fix | Delete
with open_log(args) as query_log:
[349] Fix | Delete
state = StateTracker(args.verbose)
[350] Fix | Delete
parse_log(query_log, args.regex, args.user, state, out_file)
[351] Fix | Delete
state.times.add_to_total()
[352] Fix | Delete
summarize(
[353] Fix | Delete
state,
[354] Fix | Delete
f"num_{args.sort}",
[355] Fix | Delete
out_file,
[356] Fix | Delete
args.verbose,
[357] Fix | Delete
args.regex,
[358] Fix | Delete
args.user,
[359] Fix | Delete
)
[360] Fix | Delete
[361] Fix | Delete
[362] Fix | Delete
if __name__ == '__main__':
[363] Fix | Delete
main()
[364] Fix | Delete
[365] Fix | Delete
It is recommended that you Edit text format, this type of Fix handles quite a lot in one request
Function