|
require 'json' |
|
require 'term/ansicolor' |
|
|
|
class String |
|
include Term::ANSIColor |
|
end |
|
|
|
### |
|
# # USAGE: |
|
# - get a full goroutine dump from pprof endpoint |
|
# `curl -q http://myapp.com:<port>/debug/pprof/goroutine?debug=2 > goroutine.dump` |
|
# |
|
# - turn the hard to read output into easier to consume JSON |
|
# `ruby stack_dump_parser.rb goroutine.dump` |
|
|
|
class Goroutine |
|
attr_reader :number, :state, :stack, :first_line, :created_by, :wait_time |
|
|
|
def initialize(lines) |
|
@first_line = lines.shift |
|
state_matches = @first_line.match(/goroutine (?<number>\d+) \[(?<state>.*)\]/) |
|
@number = state_matches[:number] |
|
@state = state_matches[:state] |
|
|
|
if @state =~ /, \d+ minutes$/ |
|
matches = @state.match(/(?<state>.*), (?<wait_time>\d+ minutes)/) |
|
@wait_time = matches[:wait_time] |
|
@state = matches[:state] |
|
end |
|
|
|
@stack = lines.each_slice(2).map { |(a, b)| StackEntry.new(a, b) } |
|
|
|
@created_by = if @stack.last.function =~ /created by/ |
|
@stack.pop |
|
end |
|
end |
|
|
|
def in_function?(pattern) |
|
@stack.any? { |entry| entry.function =~ pattern } |
|
end |
|
|
|
def in_location?(pattern) |
|
@stack.any? { |entry| entry.location =~ pattern } |
|
end |
|
|
|
def raw |
|
stacklines = @stack.map(&:raw) |
|
stacklines.push created_by.raw if created_by |
|
"#{first_line}\n#{stacklines.join("\n")}\n\n" |
|
end |
|
|
|
def to_h |
|
{ |
|
number: number.to_i, |
|
state: state, |
|
wait_time: wait_time, |
|
stack: stack.map(&:to_h), |
|
created_by: created_by&.to_h |
|
} |
|
end |
|
end |
|
|
|
class StackEntry |
|
attr_reader :function, :location |
|
|
|
def initialize(function, location) |
|
@function = function.strip |
|
@location = location.strip |
|
end |
|
|
|
def raw |
|
"#{function}\n #{location.yellow}" |
|
end |
|
|
|
|
|
def to_h |
|
{ |
|
function: function, |
|
location: location |
|
} |
|
end |
|
end |
|
|
|
class StackDump |
|
attr_reader :goroutines |
|
|
|
def initialize(filename) |
|
@filename = filename |
|
|
|
load! |
|
end |
|
|
|
def in_function(pattern) |
|
goroutines.select { |goroutine| goroutine.in_function?(pattern) } |
|
end |
|
|
|
def in_location(pattern) |
|
goroutines.select { |goroutine| goroutine.in_location?(pattern) } |
|
end |
|
|
|
def created_by(pattern) |
|
goroutines.select { |goroutine| goroutine.created_by&.function =~ pattern } |
|
end |
|
|
|
def dump! |
|
puts JSON.pretty_generate( |
|
total: goroutines.length, |
|
goroutines: goroutines.map(&:to_h) |
|
) |
|
end |
|
|
|
private |
|
|
|
def load! |
|
all_lines = File.read(@filename) |
|
|
|
# each goroutine is split by an empty line |
|
raw_goroutines = all_lines.split("\n\n") |
|
|
|
@goroutines = raw_goroutines.map do |raw_goroutine| |
|
lines = raw_goroutine.lines |
|
first_line = lines.first |
|
# ignore first line if it's just newline |
|
lines.shift if first_line == "\n" |
|
|
|
Goroutine.new(lines.dup) |
|
end |
|
end |
|
end |
|
|
|
if $PROGRAM_NAME == __FILE__ |
|
stack_dump_path = ARGV.first |
|
StackDump.new(stack_dump_path).dump! |
|
end |