Skip to content

Instantly share code, notes, and snippets.

@catkins
Last active February 5, 2018 04:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save catkins/57fa35a80886cd01d39d314b412ee282 to your computer and use it in GitHub Desktop.
Save catkins/57fa35a80886cd01d39d314b412ee282 to your computer and use it in GitHub Desktop.
Golang stack dump parser

Stack dump parser

Usage

As a script

Fetch a goroutine dump from the pprof endpoint

curl -q http://myapp.com:<port>/debug/pprof/goroutine?debug=2 > goroutine.dump

Install ansicolor gem

gem install term-ansicolor

Turn the hard to read output into easier to consume JSON

ruby stack_dump_parser.rb goroutine.dump

In other ruby scripts

require_relative './stack_dump_parser.rb'

parser = StackDumpParser.new('goroutine.dump')

# print out all goroutines with a function matching /Execute/ in the stack
puts parser.goroutines.select { |goroutine| goroutine.in_stack? /Execute/ }.map(&:raw)

# put count of goroutines in each state
puts parser.goroutines.group_by(&:state).map { |(state, filtered_goroutines)| sprintf("%03d - %s", filtered_goroutines.length, state) }.sort
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment