├── .gitignore ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.markdown ├── astack.py ├── images ├── screenshot-aggregate.png └── screenshot-grep.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *~ 3 | *pyc 4 | *#* 5 | /test/ 6 | *.egg-info 7 | /dist/ 8 | /build/ 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | HubSpot, Inc 2 | 3 | Michael Axiak 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.0.2) 2012-10-25 2 | - Added -i flag to set input 3 | - Added -g to grep and display with colors 4 | - Added --pretty to force it to display with colors 5 | - Added colors to everything 6 | 7 | 0.0.1) 2012-10-24 8 | - Initial release 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 HubSpot, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.markdown 2 | include LICENSE 3 | include CHANGELOG 4 | include AUTHORS 5 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## astack 2 | 3 | You've heard of jstack and pstack, well this is a new tool 4 | called astack. 5 | 6 | astack is a swiss army knife of easily getting thread dumps 7 | from JVMs and analyzing them from the command line. 8 | 9 | Guiding principles: 10 | 11 | - No complicated network setup (e.g. JMX) 12 | - Designed for linux (sorry OS X) 13 | - Single file script for ease of copying 14 | 15 | Dependencies: 16 | 17 | - gdb 18 | 19 | ### Screenshots 20 | 21 | ![Using the aggregation option](https://github.com/HubSpot/astack/raw/master/images/screenshot-aggregate.png) 22 | 23 | ![Using the grep option](https://github.com/HubSpot/astack/raw/master/images/screenshot-grep.png) 24 | 25 | ### Synopsis 26 | 27 | There are two steps to invoking `astack`: (1) select which process to inspect and 28 | (2) specify what action to take. There are two ways to select which process: 29 | 30 | 1. `-p PID` - Select based on PID 31 | 1. `-n NAME` - Match based on case insensitive search of command line 32 | 33 | Note that name searching does not work if more than one process match. 34 | 35 | Once you have a process, there are a few options you can take to get different 36 | output: 37 | 38 | 1. `-r` - Just get the raw stacktrace as if you sent a SIGQUIT to the java process and captured the stdout with some extra info. 39 | 1. `-a NLINES` - Group and count the threads by `NLINES` of stack and display them in order of occurrence with one representative thread. 40 | 1. `-s NSAMPLES` - Sample the threaddump a few times and display which ones are most active (most oftenly in RUNNABLE state). 41 | 42 | Usage description from the process itself: 43 | 44 | Usage: astack [options] 45 | 46 | Options: 47 | -h, --help show this help message and exit 48 | -p PID, --pid=PID process pid 49 | -n NAME, --process-name=NAME 50 | match name of process 51 | -r, --raw print the raw stacktrace and exit 52 | -u, --upgrade automatically upgrade 53 | -a AGG, --aggregate=AGG 54 | Aggregate stacktraces (specify the number of lines to 55 | aggregate) 56 | -s SAMPLE, --sample=SAMPLE 57 | Sample stacktraces to the most active ones (specify 58 | the number of seconds) 59 | -i INPUT, --input=INPUT 60 | read stacktrace from file (or - with stdin) 61 | --pretty, --pretty Force colors 62 | -g MATCH, --grep=MATCH 63 | Show only threads that match text 64 | 65 | ### Theory of Operation 66 | 67 | Rather than hooking into the JVM and asking it for a stack trace via instrumentation, 68 | this script takes the approach of sending a `SIGQUIT` signal and extracting the stacktrace 69 | from the JVM while it's running. The way it does this is by using `gdb` to temporarily 70 | redirect stdout while the JVM is spouting out the thread dump, and switching it back when 71 | it's done. This does mean that it can occasionally get some artifacts if your JVM is 72 | rapidly sending output to stdout. In most typical scenarios (e.g. log lines) you wouldn't 73 | see any interference. 74 | 75 | The advantage of using this technique is that even when a JVM is under heavy load and cannot 76 | fulfill a instrumentation approach, the low level response to a `SIGQUIT` signal is still 77 | functional. In most cases, thread dumps are most useful when the JVM is at its limit, so 78 | this technique can get interesting results very easily. 79 | 80 | ### Install 81 | 82 | $ sudo pip install astack 83 | 84 | ### Ubuntu 10.04 and up support 85 | 86 | On Ubuntu 10.04 and up you'll need to run this (as root): 87 | 88 | # echo 0 > /proc/sys/kernel/yama/ptrace_scope 89 | 90 | ### Bugs & Issues 91 | 92 | Feel free to file any issues with github's [issues](https://github.com/HubSpot/astack/issues/) page. 93 | 94 | ### License 95 | 96 | MIT License, copyright HubSpot 2012. See the bundled [LICENSE](https://github.com/HubSpot/astack/blob/master/LICENSE) file for details. 97 | -------------------------------------------------------------------------------- /astack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from optparse import OptionParser 3 | from subprocess import Popen, PIPE 4 | import datetime 5 | import tempfile 6 | import urllib 7 | import time 8 | import sys 9 | import os 10 | import re 11 | 12 | __all__ = ('main',) 13 | 14 | DEVNULL = open(os.devnull, 'w') 15 | 16 | old_fd_re = re.compile(r'\$2 = (\d+)') 17 | heap_info_re = re.compile(r'space (?:\w+), (?:\d+)% used \[0x\w+,0x\w+,0x\w+') 18 | 19 | MOVED_BACK = True 20 | START, END = 1, 2 21 | OLD_FD = None 22 | USE_COLOR = False 23 | 24 | 25 | def main(): 26 | global MOVED_BACK, USE_COLOR 27 | options = parse_args() 28 | 29 | if options.upgrade: 30 | return autoupgrade() 31 | if options.pretty: 32 | USE_COLOR = True 33 | 34 | if not options.pid: 35 | sys.argv = [sys.argv[0], '--help'] 36 | print "No process specified - use -p or -n " 37 | return parse_args() 38 | 39 | if options.raw: 40 | print colorize_stacktrace(add_os_thread_info(options.pid, get_stack_trace(options))) 41 | elif options.agg: 42 | print aggregate(add_os_thread_info(options.pid, get_stack_trace(options)), int(options.agg)) 43 | elif options.grep: 44 | print grep(add_os_thread_info(options.pid, get_stack_trace(options)), options.grep) 45 | elif options.sample: 46 | print sample(options.pid, 4, 10, int(float(options.sample) / float(10)), options.jstack) 47 | else: 48 | print aggregate(add_os_thread_info(options.pid, get_stack_trace(options)), 10) 49 | 50 | 51 | def get_stack_trace(options): 52 | if options.pid: 53 | return get_stack_trace_from_pid(options.pid, options.jstack) 54 | elif options.input: 55 | return get_stack_trace_from_file(options.input) 56 | else: 57 | print "Can't get stacktrace" 58 | parse_args() 59 | raise Exception() 60 | 61 | 62 | def get_stack_trace_from_file(filename): 63 | if filename.strip() == '-': 64 | return sys.stdin.read() 65 | elif filename.startswith('http:'): 66 | return urllib.urlopen(filename).read() 67 | else: 68 | with open(filename) as f: 69 | return f.read() 70 | 71 | 72 | def get_stack_trace_from_pid(pid, use_jstack): 73 | if use_jstack: 74 | return Popen('{jstack} {pid}'.format(jstack=find_jstack(pid), pid=pid), stdout=PIPE, stdin=DEVNULL, shell=True).communicate()[0] 75 | else: 76 | with tempfile.NamedTemporaryFile() as stackfile: 77 | try: 78 | MOVED_BACK = False 79 | move_stdout(pid, stackfile.name) 80 | os.kill(pid, 3) 81 | return read_stack_trace(stackfile) 82 | finally: 83 | if not MOVED_BACK: 84 | move_stdout(pid, edge=END) 85 | 86 | 87 | def find_jstack(pid): 88 | default = Popen("which jstack", stdout=PIPE, stdin=DEVNULL, shell=True).communicate()[0] 89 | if default.strip(): 90 | return default.strip() 91 | if os.environ.get('JAVA_HOME'): 92 | return os.path.join(os.environ['JAVA_HOME'], 'bin', 'jstack') 93 | jvm_bin_path = os.path.dirname(os.readlink('/proc/{pid}/exe'.format(pid=pid))) 94 | if jvm_bin_path.strip(): 95 | return os.path.join(jvm_bin_path.strip(), 'jstack') 96 | raise RuntimeError("Could not find jstack - do you have it installed in $JAVA_HOME?") 97 | 98 | 99 | def read_stack_trace(stackfile): 100 | stackfile.seek(0) 101 | lines = [] 102 | started_heap = False 103 | started_stack = False 104 | while True: 105 | where = stackfile.tell() 106 | line = stackfile.readline() 107 | if not line: 108 | time.sleep(0.1) 109 | stackfile.seek(where) 110 | else: 111 | if line.startswith('Full thread dump'): 112 | started_stack = True 113 | if started_stack: 114 | lines.append(line) 115 | if line.rstrip() == 'Heap': 116 | started_heap = True 117 | elif not started_heap and heap_info_re.search(line): 118 | started_heap = True 119 | elif started_heap and line == '\n': 120 | return ''.join(lines) 121 | 122 | 123 | def move_stdout(pid, new_file=None, edge=START): 124 | global OLD_FD 125 | exe = os.readlink('/proc/{pid}/exe'.format(pid=pid)) 126 | format = GDB_BATCH_FORMAT[edge] 127 | 128 | gdb_batch = format.format( 129 | exe=exe, 130 | pid=pid, 131 | oldfd=OLD_FD, 132 | flags=os.O_RDWR | os.O_APPEND, 133 | stdout=new_file) 134 | 135 | with tempfile.NamedTemporaryFile() as f: 136 | f.write(gdb_batch) 137 | f.flush() 138 | output = Popen('gdb -batch -x {file}'.format(file=f.name), stdout=PIPE, stdin=DEVNULL, shell=True).communicate()[0] 139 | OLD_FD = old_fd_re.search(output).group(1) 140 | 141 | 142 | GDB_BATCH_FORMAT = { 143 | START: """ 144 | file {exe} 145 | attach {pid} 146 | call open("{stdout}", {flags}) 147 | call dup(1) 148 | call dup2($1, 1) 149 | call close($1) 150 | detach 151 | """, 152 | END: """ 153 | file {exe} 154 | attach {pid} 155 | call dup2({oldfd}, 1) 156 | call close({oldfd}) 157 | detach 158 | """ 159 | } 160 | 161 | 162 | def add_os_thread_info(pid, stacktrace): 163 | _thread_re = re.compile(r'^\S.*\snid=0x([0-9a-f]+)\s', re.I) 164 | lines = [] 165 | thread_info = get_os_thread_info(pid) 166 | for line in stacktrace.splitlines(): 167 | m = _thread_re.search(line) 168 | if m: 169 | nid = int(m.group(1), 16) 170 | else: 171 | lines.append(line) 172 | continue 173 | if nid in thread_info: 174 | cpu, start_time = thread_info[nid] 175 | line += ' cpu={cpu} start={start_time}'.format(cpu=cpu, start_time=start_time.strftime("%Y-%m-%d %H:%M:%S")) 176 | lines.append(line) 177 | return '\n'.join(lines) 178 | 179 | 180 | def aggregate(stacktrace, nlines): 181 | threads = split_threads(stacktrace) 182 | counter = {} 183 | example = {} 184 | cpu_totals = {} 185 | for thread in threads: 186 | if not thread.strip(): 187 | continue 188 | thread_info = get_thread_info(thread) 189 | stack = get_stack(thread) 190 | top = ''.join(stack[:nlines]) 191 | counter[top] = counter.get(top, 0) + 1 192 | cpu_totals[top] = cpu_totals.get(top, 0) + (thread_info.get('cpu') or 0) 193 | example[top] = thread 194 | items = sorted(counter.items(), key=lambda x: x[1]) 195 | 196 | return '\n\n'.join("{0} times ({1}% total cpu)\n{2}".format(count, cpu_totals.get(key), colorize_thread(example.get(key))) 197 | for key, count in items) 198 | 199 | 200 | def grep(stacktrace, text): 201 | threads = [] 202 | text_re = re.compile(r"({0})".format(re.escape(text))) 203 | for thread in split_threads(stacktrace): 204 | if text.lower() not in thread.lower(): 205 | continue 206 | if USE_COLOR: 207 | thread = text_re.sub(colored(r'\1', 'red', attrs=['bold']), thread) 208 | threads.append(thread) 209 | return '\n\n'.join(colorize_thread(thread) for thread in threads) 210 | 211 | 212 | def sample(pid, nlines, samples, wait_time, use_jstack): 213 | sys.stdout.write("Sampling.") 214 | sys.stdout.flush() 215 | thread_runnable_counts = {} 216 | thread_stacks = {} 217 | for _ in range(samples): 218 | sys.stdout.write(".") 219 | sys.stdout.flush() 220 | threads = split_threads(add_os_thread_info(pid, get_stack_trace_from_pid(pid, use_jstack))) 221 | for thread in threads: 222 | thread_info = get_thread_info(thread) 223 | if thread_info.get('status', '').lower().strip() != 'runnable': 224 | continue 225 | thread_id = thread_info.get('thread_id') 226 | 227 | thread_runnable_counts[thread_id] = thread_runnable_counts.get(thread_id, 0) + 1 228 | thread_stacks.setdefault(thread_id, []).append(thread) 229 | time.sleep(wait_time) 230 | 231 | items = sorted(thread_runnable_counts.items(), key=lambda x: x[1]) 232 | 233 | threads = [] 234 | 235 | for tid, count in items: 236 | stack = thread_stacks[tid][-1] 237 | stack = stack.replace('runnable', '{0:0.1f}% runnable'.format(float(count) / samples * 100), 1) 238 | threads.append(stack) 239 | 240 | print 241 | 242 | return aggregate('\n\n'.join(threads), nlines) 243 | 244 | 245 | def split_threads(stacktrace, get_ends=False): 246 | begin, end = [], [] 247 | threads = [] 248 | current_thread = [] 249 | _thread_line_re = re.compile(r'^\S.*\sprio=.*\stid=') 250 | for line in stacktrace.splitlines(): 251 | if _thread_line_re.search(line): 252 | current_thread.append(line) 253 | elif not line.strip(): 254 | threads.append('\n'.join(current_thread).strip()) 255 | current_thread = [] 256 | end = [] 257 | elif current_thread: 258 | current_thread.append(line) 259 | elif not threads: 260 | begin.append(line) 261 | elif not current_thread: 262 | end.append(line) 263 | 264 | if current_thread: 265 | threads.append('\n'.join(current_thread).strip()) 266 | if get_ends: 267 | return '\n'.join(begin), threads, '\n'.join(end) 268 | else: 269 | return threads 270 | 271 | 272 | def get_thread_info(thread): 273 | _unwanted_hex_re = re.compile(r'\[0x[0-9a-f]+\]\s') 274 | thread_re = re.compile(r'^"([^"]+)" (daemon)?\s*prio=(\d+) tid=0x([0-9a-f]+) nid=0x([0-9a-f]+) (.+?)\s*(?:cpu=([.\d]+) start=(.+))?$') 275 | text = _unwanted_hex_re.sub('', thread.split('\n', 1)[0]) 276 | m = thread_re.search(text) 277 | if not m: 278 | return {} 279 | return { 280 | 'name': m.group(1), 281 | 'daemon': bool(m.group(2)), 282 | 'priority': int(m.group(3)), 283 | 'thread_id': int(m.group(4), 16), 284 | 'native_id': int(m.group(5), 16), 285 | 'status': m.group(6).strip(), 286 | 'cpu': float(m.group(7)) if m.group(7) else None, 287 | 'start_time': m.group(8).strip() if m.group(8) else None, 288 | } 289 | 290 | 291 | def colorize_thread(thread, line=0): 292 | if not USE_COLOR: 293 | return thread 294 | head = thread.strip().split("\n", 1) 295 | if len(head) == 1: 296 | head, tail = head[0].strip(), '' 297 | else: 298 | head, tail = head 299 | thread_info = get_thread_info(head) 300 | if not thread_info: 301 | if line < 2: 302 | return head + colorize_thread(tail, line + 1) 303 | else: 304 | return thread 305 | 306 | return '"{name}" {daemon}prio={priority} tid=0x{thread_id:0x} nid=0x{native_id:0x} {status} {cpu}{start_time}\n{tail}'.format( 307 | name=colored(thread_info.get('name', ''), 'blue'), 308 | daemon=colored('daemon ' if thread_info.get('daemon') else '', 'yellow'), 309 | priority=thread_info.get('priority', 0), 310 | thread_id=thread_info.get('thread_id', 0), 311 | native_id=thread_info.get("native_id", 0), 312 | status=colored(thread_info.get('status'), ['white','green']['runnable' in thread_info.get('status','').lower()]), 313 | cpu="cpu={0:.1f}% ".format(thread_info.get('cpu')) if thread_info.get('cpu') else '', 314 | start_time="start={0}".format(thread_info.get('start_time')) if thread_info.get('start_time') else '', 315 | tail=colorize_tail(tail) 316 | ) 317 | 318 | 319 | def colorize_tail(thread_tail): 320 | blocked = re.compile(r'(\b)BLOCKED(\b)') 321 | return blocked.sub(r'\1' + colored('BLOCKED', 'red', attrs=['bold', 'underline']) + r'\2', thread_tail) 322 | 323 | 324 | def colorize_stacktrace(stacktrace): 325 | if not USE_COLOR: 326 | return stacktrace 327 | begin, threads, end = split_threads(stacktrace, True) 328 | return begin.strip() + '\n\n' + '\n\n'.join(colorize_thread(thread) for thread in threads).strip() + '\n\n' + end.strip() 329 | 330 | 331 | def get_stack(thread): 332 | _at_re = re.compile(r'^\s+at ') 333 | stack = [] 334 | for line in thread.splitlines()[3:]: 335 | if _at_re.search(line): 336 | stack.append(line.strip()) 337 | return stack 338 | 339 | 340 | def indent_text(text, indent=4): 341 | indent = ' ' * indent 342 | return '\n'.join(indent + line for line in text.splitlines()) 343 | 344 | 345 | def get_os_thread_info(pid): 346 | pid = str(pid) 347 | result = {} 348 | output = Popen("ps -e -T -o pid,spid,pcpu,etime", stdout=PIPE, shell=True).communicate()[0] 349 | for line in output.splitlines()[1:]: 350 | row = line.split() 351 | if row[0] != pid: 352 | continue 353 | spid = int(row[1]) 354 | cpu = float(row[2]) 355 | try: 356 | start = parse_etime(row[3]) 357 | except Exception, e: 358 | raise Exception("Couldn't parse etime: {0}".format(row[3])) 359 | result[spid] = (cpu, start) 360 | return result 361 | 362 | 363 | def parse_etime(etime): 364 | info = etime.split('-', 1) 365 | days = hours = minutes = seconds = 0 366 | if len(info) == 2: 367 | days = int(info[0].lstrip('0') or 0) 368 | info = info[-1].split(':') 369 | seconds = int(info[-1].lstrip('0') or 0) 370 | if len(info) > 1: 371 | minutes = int(info[-2].lstrip('0') or 0) 372 | if len(info) > 2: 373 | hours = int(info[-3].lstrip('0') or 0) 374 | return datetime.datetime.now() - datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) 375 | 376 | 377 | def parse_args(): 378 | parser = OptionParser() 379 | parser.add_option("-p", "--pid", dest="pid", default=None, 380 | help="process pid", metavar="PID") 381 | parser.add_option("-n", "--process-name", dest="name", default=None, 382 | help="match name of process", metavar="NAME") 383 | parser.add_option("-r", "--raw", action="store_true", 384 | dest="raw", default=False, help="print the raw stacktrace and exit") 385 | parser.add_option("-u", "--upgrade", action="store_true", 386 | dest="upgrade", default=False, help="automatically upgrade") 387 | parser.add_option("-a", "--aggregate", default=None, dest="agg", 388 | help="Aggregate stacktraces (specify the number of lines to aggregate)") 389 | parser.add_option("-s", "--sample", default=None, dest="sample", 390 | help="Sample stacktraces to the most active ones (specify the number of seconds)") 391 | parser.add_option("-i", "--input", default=None, dest="input", metavar="INPUT", 392 | help="read stacktrace from file (or - with stdin)") 393 | parser.add_option("--pretty", "--pretty", action="store_true", 394 | dest="pretty", default=False, help="Force colors") 395 | parser.add_option("-g", "--grep", dest="grep", default=None, 396 | help="Show only threads that match text", metavar="MATCH") 397 | parser.add_option("-F", "--force", dest="jstack", action="store_false", 398 | default=True, help="Use gdb to forcibly obtain the stacktrace [DANGEROUS]") 399 | options, args = parser.parse_args() 400 | 401 | if os.isatty(sys.stdout.fileno()): 402 | options.pretty = True 403 | 404 | if bool(options.pid) and bool(options.name): 405 | parser.error("please specify pid or name, not both") 406 | if options.pid: 407 | options.pid = int(options.pid) 408 | elif options.name: 409 | lines = Popen("ps aux", 410 | shell=True, 411 | stdout=PIPE).communicate()[0].splitlines() 412 | potential = [line for line in lines 413 | if options.name.lower() in line.lower() and 414 | line.split()[1] != str(os.getpid())] 415 | 416 | if len(potential) != 1: 417 | parser.error("didn't get one process matched: {0}".format(len(potential))) 418 | options.pid = int(potential[0].split()[1]) 419 | 420 | return options 421 | 422 | 423 | def autoupgrade(): 424 | print "About to update from git.hubteam.com..." 425 | contents = urllib.urlopen('https://github.com/HubSpot/astack/raw/master/astack.py').read() 426 | with open(__file__, 'r') as f: 427 | if f.read() == contents: 428 | print "Nothing has changed... exiting" 429 | return 430 | os.rename(__file__, __file__ + '.bak') 431 | print "Renamed {0} to {1}".format(__file__, __file__ + '.bak') 432 | print "Saved new {0}".format(__file__) 433 | with open(__file__, 'w+') as f: 434 | f.write(contents) 435 | os.chmod(__file__, 0755) 436 | 437 | 438 | ATTRIBUTES = dict( 439 | list(zip([ 440 | 'bold', 441 | 'dark', 442 | '', 443 | 'underline', 444 | 'blink', 445 | '', 446 | 'reverse', 447 | 'concealed' 448 | ], 449 | list(range(1, 9)) 450 | )) 451 | ) 452 | del ATTRIBUTES[''] 453 | 454 | 455 | HIGHLIGHTS = dict( 456 | list(zip([ 457 | 'on_grey', 458 | 'on_red', 459 | 'on_green', 460 | 'on_yellow', 461 | 'on_blue', 462 | 'on_magenta', 463 | 'on_cyan', 464 | 'on_white' 465 | ], 466 | list(range(40, 48)) 467 | )) 468 | ) 469 | 470 | 471 | COLORS = dict( 472 | list(zip([ 473 | 'grey', 474 | 'red', 475 | 'green', 476 | 'yellow', 477 | 'blue', 478 | 'magenta', 479 | 'cyan', 480 | 'white', 481 | ], 482 | list(range(30, 38)) 483 | )) 484 | ) 485 | 486 | 487 | RESET = '\033[0m' 488 | 489 | 490 | def colored(text, color=None, on_color=None, attrs=None): 491 | if os.getenv('ANSI_COLORS_DISABLED') is None: 492 | fmt_str = '\033[%dm%s' 493 | if color is not None: 494 | text = fmt_str % (COLORS[color], text) 495 | 496 | if on_color is not None: 497 | text = fmt_str % (HIGHLIGHTS[on_color], text) 498 | 499 | if attrs is not None: 500 | for attr in attrs: 501 | text = fmt_str % (ATTRIBUTES[attr], text) 502 | 503 | text += RESET 504 | return text 505 | 506 | 507 | if __name__ == '__main__': 508 | main() 509 | -------------------------------------------------------------------------------- /images/screenshot-aggregate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/astack/f446cc2eb29dc96b5b9b64c026c3de5017e02169/images/screenshot-aggregate.png -------------------------------------------------------------------------------- /images/screenshot-grep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/astack/f446cc2eb29dc96b5b9b64c026c3de5017e02169/images/screenshot-grep.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 - 3 | from setuptools import setup 4 | 5 | setup( 6 | name='astack', 7 | version='0.0.6', 8 | description='Simple stacktrace analysis tool for the JVM', 9 | long_description=open('README.markdown').read(), 10 | author='Michael Axiak', 11 | author_email='mike@axiak.net', 12 | license=open('LICENSE').read(), 13 | url='https://github.com/HubSpot/astack/', 14 | classifiers=[ 15 | 'Development Status :: 4 - Beta', 16 | 'Environment :: Other Environment', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | ], 20 | py_modules=['astack'], 21 | entry_points={ 22 | 'console_scripts': 23 | ['astack=astack:main'], 24 | }) 25 | --------------------------------------------------------------------------------