200 lines
6.7 KiB
Python
200 lines
6.7 KiB
Python
|
#!/usr/bin/env python3
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||
|
# not use this file except in compliance with the License. You may obtain
|
||
|
# a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
|
# License for the specific language governing permissions and limitations
|
||
|
# under the License.
|
||
|
|
||
|
import argparse
|
||
|
import collections
|
||
|
import functools
|
||
|
import sys
|
||
|
import time
|
||
|
|
||
|
from oslo_serialization import jsonutils
|
||
|
from oslo_utils import importutils
|
||
|
|
||
|
from oslo_log import log
|
||
|
|
||
|
termcolor = importutils.try_import('termcolor')
|
||
|
|
||
|
|
||
|
_USE_COLOR = False
|
||
|
DEFAULT_LEVEL_KEY = 'levelname'
|
||
|
DEFAULT_TRACEBACK_KEY = 'traceback'
|
||
|
|
||
|
|
||
|
def main():
|
||
|
global _USE_COLOR
|
||
|
args = parse_args()
|
||
|
_USE_COLOR = args.color
|
||
|
formatter = functools.partial(
|
||
|
console_format,
|
||
|
args.prefix,
|
||
|
args.locator,
|
||
|
loggers=args.loggers,
|
||
|
levels=args.levels,
|
||
|
level_key=args.levelkey,
|
||
|
traceback_key=args.tbkey,
|
||
|
)
|
||
|
if args.lines:
|
||
|
# Read backward until we find all of our newline characters
|
||
|
# or reach the beginning of the file
|
||
|
args.file.seek(0, 2)
|
||
|
newlines = 0
|
||
|
pos = args.file.tell()
|
||
|
while newlines <= args.lines and pos > 0:
|
||
|
pos = pos - 1
|
||
|
args.file.seek(pos)
|
||
|
if args.file.read(1) == '\n':
|
||
|
newlines = newlines + 1
|
||
|
try:
|
||
|
for line in reformat_json(args.file, formatter, args.follow):
|
||
|
print(line)
|
||
|
except KeyboardInterrupt:
|
||
|
sys.exit(0)
|
||
|
|
||
|
|
||
|
def parse_args():
|
||
|
parser = argparse.ArgumentParser()
|
||
|
parser.add_argument("file",
|
||
|
nargs='?', default=sys.stdin,
|
||
|
type=argparse.FileType(),
|
||
|
help="JSON log file to read from (if not provided"
|
||
|
" standard input is used instead)")
|
||
|
parser.add_argument("--prefix",
|
||
|
default='%(asctime)s.%(msecs)03d'
|
||
|
' %(process)s %(levelname)s %(name)s',
|
||
|
help="Message prefixes")
|
||
|
parser.add_argument("--locator",
|
||
|
default='[%(funcname)s %(pathname)s:%(lineno)s]',
|
||
|
help="Locator to append to DEBUG records")
|
||
|
parser.add_argument("--levelkey",
|
||
|
default=DEFAULT_LEVEL_KEY,
|
||
|
help="Key in the JSON record where the level is held")
|
||
|
parser.add_argument("--tbkey",
|
||
|
default=DEFAULT_TRACEBACK_KEY,
|
||
|
help="Key in the JSON record where the"
|
||
|
" traceback/exception is held")
|
||
|
parser.add_argument("-c", "--color",
|
||
|
action='store_true', default=False,
|
||
|
help="Color log levels (requires `termcolor`)")
|
||
|
parser.add_argument("-f", "--follow",
|
||
|
action='store_true', default=False,
|
||
|
help="Continue parsing new data until"
|
||
|
" KeyboardInterrupt")
|
||
|
parser.add_argument("-n", "--lines",
|
||
|
required=False, type=int,
|
||
|
help="Last N number of records to view."
|
||
|
" (May show less than N records when used"
|
||
|
" in conjuction with --loggers or --levels)")
|
||
|
parser.add_argument("--loggers",
|
||
|
nargs='*', default=[],
|
||
|
help="only return results matching given logger(s)")
|
||
|
parser.add_argument("--levels",
|
||
|
nargs='*', default=[],
|
||
|
help="Only return lines matching given log level(s)")
|
||
|
args = parser.parse_args()
|
||
|
if args.color and not termcolor:
|
||
|
raise ImportError("Coloring requested but `termcolor` is not"
|
||
|
" importable")
|
||
|
return args
|
||
|
|
||
|
|
||
|
def colorise(key, text=None):
|
||
|
if text is None:
|
||
|
text = key
|
||
|
if not _USE_COLOR:
|
||
|
return text
|
||
|
colors = {
|
||
|
'exc': ('red', ['reverse', 'bold']),
|
||
|
'FATAL': ('red', ['reverse', 'bold']),
|
||
|
'ERROR': ('red', ['bold']),
|
||
|
'WARNING': ('yellow', ['bold']),
|
||
|
'WARN': ('yellow', ['bold']),
|
||
|
'INFO': ('white', ['bold']),
|
||
|
}
|
||
|
color, attrs = colors.get(key, ('', []))
|
||
|
if color:
|
||
|
return termcolor.colored(text, color=color, attrs=attrs)
|
||
|
return text
|
||
|
|
||
|
|
||
|
def warn(prefix, msg):
|
||
|
return "%s: %s" % (colorise('exc', prefix), msg)
|
||
|
|
||
|
|
||
|
def reformat_json(fh, formatter, follow=False):
|
||
|
# using readline allows interactive stdin to respond to every line
|
||
|
while True:
|
||
|
line = fh.readline()
|
||
|
if not line:
|
||
|
if follow:
|
||
|
time.sleep(0.1)
|
||
|
continue
|
||
|
else:
|
||
|
break
|
||
|
line = line.strip()
|
||
|
if not line:
|
||
|
continue
|
||
|
try:
|
||
|
record = jsonutils.loads(line)
|
||
|
except ValueError:
|
||
|
yield warn("Not JSON", line)
|
||
|
continue
|
||
|
for out_line in formatter(record):
|
||
|
yield out_line
|
||
|
|
||
|
|
||
|
def console_format(prefix, locator, record, loggers=[], levels=[],
|
||
|
level_key=DEFAULT_LEVEL_KEY,
|
||
|
traceback_key=DEFAULT_TRACEBACK_KEY):
|
||
|
# Provide an empty string to format-specifiers the record is
|
||
|
# missing, instead of failing. Doesn't work for non-string
|
||
|
# specifiers.
|
||
|
record = collections.defaultdict(str, record)
|
||
|
# skip if the record doesn't match a logger we are looking at
|
||
|
if loggers:
|
||
|
name = record.get('name')
|
||
|
if not any(name.startswith(n) for n in loggers):
|
||
|
return
|
||
|
if levels:
|
||
|
if record.get(level_key) not in levels:
|
||
|
return
|
||
|
levelname = record.get(level_key)
|
||
|
if levelname:
|
||
|
record[level_key] = colorise(levelname)
|
||
|
|
||
|
try:
|
||
|
prefix = prefix % record
|
||
|
except TypeError:
|
||
|
# Thrown when a non-string format-specifier can't be filled in.
|
||
|
# Dict comprehension cleans up the output
|
||
|
yield warn('Missing non-string placeholder in record',
|
||
|
{str(k): str(v) if isinstance(v, str) else v
|
||
|
for k, v in record.items()})
|
||
|
return
|
||
|
|
||
|
locator = ''
|
||
|
if (record.get('levelno', 100) <= log.DEBUG or levelname == 'DEBUG'):
|
||
|
locator = locator % record
|
||
|
|
||
|
yield ' '.join(x for x in [prefix, record['message'], locator] if x)
|
||
|
|
||
|
tb = record.get(traceback_key)
|
||
|
if tb:
|
||
|
if type(tb) is str:
|
||
|
tb = tb.rstrip().split("\n")
|
||
|
for tb_line in tb:
|
||
|
yield ' '.join([prefix, tb_line])
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|