lib/elastic_apm/stacktrace_builder.rb (81 lines of code) (raw):
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you 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.
# frozen_string_literal: true
require 'elastic_apm/stacktrace/frame'
require 'elastic_apm/util/lru_cache'
module ElasticAPM
# @api private
class StacktraceBuilder
JAVA_FORMAT = /^(.+)\.([^.]+)\(([^:]+):(\d+)\)$/.freeze
RUBY_FORMAT = /^(.+?):(\d+)(?::in ['`](.+#)?(.+?)')?$/.freeze
RUBY_VERS_REGEX = %r{ruby(/gems)?[-/](\d+\.)+\d}.freeze
JRUBY_ORG_REGEX = %r{org/jruby}.freeze
GEMS_PATH =
if defined?(Bundler) && Bundler.default_bundle_dir
Bundler.bundle_path.to_s
else
Gem.dir
end
def initialize(config)
@config = config
@cache = Util::LruCache.new(2048) do |cache, frame|
build_frame(cache, frame)
end
end
attr_reader :config
def build(backtrace, type:)
Stacktrace.new.tap do |s|
s.frames = backtrace[0...config.stack_trace_limit].map do |line|
@cache[[line, type]]
end
end
end
private
def build_frame(cache, keys)
line, type = keys
abs_path, lineno, function, _module_name = parse_line(line)
frame = Stacktrace::Frame.new
frame.abs_path = abs_path
frame.filename = strip_load_path(abs_path)
frame.function = function
frame.lineno = lineno.to_i
frame.library_frame = library_frame?(config, abs_path)
line_count =
context_lines_for(config, type, library_frame: frame.library_frame)
frame.build_context line_count
cache[[line, type]] = frame
end
def parse_line(line)
ruby_match = line.match(RUBY_FORMAT)
if ruby_match
_, file, number, module_name, method = ruby_match.to_a
file.sub!(/\.class$/, '.rb')
module_name&.sub!('#', '')
else
java_match = line.match(JAVA_FORMAT)
_, module_name, method, file, number = java_match.to_a
end
[file, number, method, module_name]
end
def library_frame?(config, abs_path)
return false unless abs_path
return true if abs_path.start_with?(GEMS_PATH)
if abs_path.start_with?(config.__root_path)
return true if abs_path.start_with?("#{config.__root_path}/vendor")
return false
end
return true if abs_path.match(RUBY_VERS_REGEX)
return true if abs_path.match(JRUBY_ORG_REGEX)
false
end
def strip_load_path(path)
return nil if path.nil?
prefix =
$LOAD_PATH
.map(&:to_s)
.select { |s| path.start_with?(s) }
.max_by(&:length)
prefix ? path[prefix.chomp(File::SEPARATOR).length + 1..-1] : path
end
def context_lines_for(config, type, library_frame:)
key = "source_lines_#{type}_#{library_frame ? 'library' : 'app'}_frames"
config.send(key.to_sym)
end
end
end