sentry-ruby.rb
Sentry.init做什么
- 读取配置
- 创建
Client
- 创建
Hub
收集span - 启动
Sentry::BackgroundWorker
- 注册[[ruby的at_exit]],以便关闭时刷出所有span
# sentry-ruby-core-5.1.1/lib/sentry-ruby.rb
def init(&block)
config = Configuration.new
# 执行用户在block中对config的配置
yield(config) if block_given?
# 补充release字段
config.detect_release
# 似乎可以对config再进一步配置,文档没介绍
apply_patches(config)
client = Client.new(config)
scope = Scope.new(max_breadcrumbs: config.max_breadcrumbs)
hub = Hub.new(client, scope)
Thread.current.thread_variable_set(THREAD_LOCAL, hub)
@main_hub = hub
@background_worker = Sentry::BackgroundWorker.new(config)
if config.capture_exception_frame_locals
exception_locals_tp.enable
end
at_exit do
@background_worker.shutdown
end
en
Sentry::Client
# sentry-ruby-core-5.1.1/lib/sentry/client.rb
module Sentry
class Client
def initialize(configuration)
@configuration = configuration
@logger = configuration.logger
if transport_class = configuration.transport.transport_class
@transport = transport_class.new(configuration)
else
@transport =
case configuration.dsn&.scheme
when 'http', 'https'
HTTPTransport.new(configuration)
else
DummyTransport.new(configuration)
end
end
end
end
end
Sentry::Scope
参考 Scopes and Hubs: 每个 Event 在发送到 sentry 前,都会先合并 Scope 的 Context
241 def get_current_hub
242 # we need to assign a hub to the current thread if it doesn't have one yet
243 #
244 # ideally, we should do this proactively whenever a new thread is created
245 # but it's impossible for the SDK to keep track every new thread
246 # so we need to use this rather passive way to make sure the app doesn't crash
247 Thread.current.thread_variable_get(THREAD_LOCAL) || clone_hub_to_current_thread
248 end
264
265 # Clones the main thread's active hub and stores it to the current thread.
266 #
267 # @return [void]
268 def clone_hub_to_current_thread
269 Thread.current.thread_variable_set(THREAD_LOCAL, get_main_hub.clone)
270 end
为检查
# lib/sentry/rack/capture_exceptions.rb
module Sentry
module Rails
class CaptureExceptions < Sentry::Rack::CaptureExceptions
def finish_transaction(transaction, status_code)
binding.trace_tree(htmp: 'finish_transaction', transcode: true, return: false) do
super
end
end
end
end
end
得调用栈如下,可见 Client 在发送事件之前,会将 Scope 中的属性复制到事件对象上

# sentry-ruby-core-5.1.1lib/sentry/scope.rb
def apply_to_event(event, hint = nil)
event.tags = tags.merge(event.tags)
event.user = user.merge(event.user)
event.extra = extra.merge(event.extra)
event.contexts = contexts.merge(event.contexts)
event.transaction = transaction_name if transaction_name
if span
event.contexts[:trace] = span.get_trace_context
end
event.fingerprint = fingerprint
event.level = level
event.breadcrumbs = breadcrumbs
event.rack_env = rack_env if rack_env
unless @event_processors.empty?
@event_processors.each do |processor_block|
event = processor_block.call(event, hint)
end
end
event
end
Context
参考 Add Context
使用 Sentry.setcontext 或者 Scope#setcontext 所设置的键值,可以在整个链路的所有事件的事件页面底部可见,但不可搜索
默认是有以下键值
# sentry-ruby-core-5.1.1/lib/sentry/scope.rb
def set_default_value
@contexts = { :os => self.class.os_context, :runtime => self.class.runtime_context }
# ..
end
Sentry::Span 与 Sentry::Transaction
根据文档 Guidelines for Performance Monitoring 的描述,Span 类对应链路跟踪中 传统意义的 Span,而 Transaction 相比 Span,增加了 @name、@hub。并且调用 finish 时,Span 是记录结束时间,而 Transaction 则还会发送到 @hub (如果 @smapled == true)。
在实现上, Transaction 继承 Span,并且 Span.attr_accessor :transaction,注释“The Transaction object the Span belongs to”
检查 Transaction 重写了哪些 Span 方法:
Sentry::Span.instance_methods & Sentry::Transaction.instance_methods(false)
# => [:finish, :to_hash, :deep_dup]
发送事件 send_data
检查 Client 代码可发现其中有记录并发送事件的逻辑。于是加入 puts caller 检查是从那里调用的
# lib/sentry/client.rb
def capture_event(event, scope, hint = {})
# 加入此行检查哪里会调用
puts caller
# ...
if async_block = configuration.async
dispatch_async_event(async_block, event, hint)
elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
queued = dispatch_background_event(event, hint)
transport.record_lost_event(:queue_overflow, event_type) unless queued
else
send_event(event, hint)
end
event
rescue => e
# ...
end
def send_event(event, hint = nil)
# ...
transport.send_event(event)
event
rescue => e
# ...
raise
end
def dispatch_background_event(event, hint)
Sentry.background_worker.perform do
send_event(event, hint)
end
end
def dispatch_async_event(async_block, event, hint)
# ...
async_block.call(event_hash)
rescue => e
# ...
send_event(event, hint)
end
得调用栈如下
.../sentry-ruby-core-5.1.1/lib/sentry/hub.rb:144:in `capture_event'
.../sentry-ruby-core-5.1.1/lib/sentry/transaction.rb:169:in `finish'
.../sentry-ruby-core-5.1.1/lib/sentry/rack/capture_exceptions.rb:70:in `finish_transaction'
.../sentry-ruby-core-5.1.1/lib/sentry/rack/capture_exceptions.rb:38:in `block in call'
.../sentry-ruby-core-5.1.1/lib/sentry/hub.rb:58:in `with_scope'
.../sentry-ruby-core-5.1.1/lib/sentry-ruby.rb:310:in `with_scope'
.../sentry-ruby-core-5.1.1/lib/sentry/rack/capture_exceptions.rb:16:in `call'
.../actionpack-5.2.6/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
.../railties-5.2.6/lib/rails/rack/logger.rb:38:in `call_app'
.../railties-5.2.6/lib/rails/rack/logger.rb:26:in `block in call'
.../activesupport-5.2.6/lib/active_support/tagged_logging.rb:71:in `block in tagged'
.../activesupport-5.2.6/lib/active_support/tagged_logging.rb:28:in `tagged'
.../activesupport-5.2.6/lib/active_support/tagged_logging.rb:71:in `tagged'
.../railties-5.2.6/lib/rails/rack/logger.rb:26:in `call'
.../sprockets-rails-3.2.2/lib/sprockets/rails/quiet_assets.rb:13:in `call'
.../actionpack-5.2.6/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
.../actionpack-5.2.6/lib/action_dispatch/middleware/request_id.rb:27:in `call'
.../rack-2.2.3/lib/rack/method_override.rb:24:in `call'
.../rack-2.2.3/lib/rack/runtime.rb:22:in `call'
.../activesupport-5.2.6/lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
.../actionpack-5.2.6/lib/action_dispatch/middleware/executor.rb:14:in `call'
.../actionpack-5.2.6/lib/action_dispatch/middleware/static.rb:127:in `call'
.../rack-2.2.3/lib/rack/sendfile.rb:110:in `call'
.../railties-5.2.6/lib/rails/engine.rb:524:in `call'
.../puma-3.12.6/lib/puma/configuration.rb:227:in `call'
.../puma-3.12.6/lib/puma/server.rb:706:in `handle_request'
.../puma-3.12.6/lib/puma/server.rb:476:in `process_client'
.../puma-3.12.6/lib/puma/server.rb:334:in `block in run'
.../puma-3.12.6/lib/puma/thread_pool.rb:135:in `block in spawn_thread'
构造 Transaction
Sentry::Rack::CaptureExceptions 就是一个 Rack 的 middleware,它会在调用下层 middleware 前创建一个 Transaction,然后在调用后执行 Transaction#finish,内部就会将它转换成一个 TransactionEvent,交给 Hub 去发送
另外,如果下层 middleware 有抛出异常,则它会调用 capture_exception,将异常信息抽取,重新包装成一个事件来发到 sentry 服务器
# lib/sentry/rack/capture_exceptions.rb
module Sentry
module Rack
class CaptureExceptions
def initialize(app)
@app = app
end
def call(env)
return @app.call(env) unless Sentry.initialized?
Sentry.with_scope do |scope|
# ...
transaction = start_transaction(env, scope)
scope.set_span(transaction) if transaction
begin
@app.call(env)
rescue Exception => e
capture_exception(e)
finish_transaction(transaction, 500)
raise
end
# ...
finish_transaction(transaction, response[0])
response
end
end
end
end
end
在 sentry-rails 这个 gem 中,Sentry::Rack::CaptureExceptions 会派生成 Sentry::Rails::CaptureExceptions 和 Sentry::Rails::RescuedExceptionInterceptor,然后插入到 Rails 的 Rack 栈中
# sentry-rails-5.1.1/lib/sentry/rails/railtie.r
module Sentry
class Railtie < ::Rails::Railtie
# middlewares can't be injected after initialize
initializer "sentry.use_rack_middleware" do |app|
# placed after all the file-sending middlewares so we can avoid unnecessary transactions
app.config.middleware.insert_after ActionDispatch::ShowExceptions, Sentry::Rails::CaptureExceptions
# need to place as close to DebugExceptions as possible to intercept most of the exceptions, including those raised by middlewares
app.config.middleware.insert_after ActionDispatch::DebugExceptions, Sentry::Rails::RescuedExceptionInterceptor
end
end
end
分布式追踪
Sentry::Rack::CaptureExceptions 接收到请求后,会在请求头里取得 HTTPSENTRYTRACE (rack 会将 sentry-trace 转成这样的键)的值,从中解析出 trace-id,保存在新建的 Transaction 中
# sentry-ruby-core-5.1.1/lib/sentry/rack/capture_exceptions.rb
def start_transaction(env, scope)
sentry_trace = env["HTTP_SENTRY_TRACE"]
options = { name: scope.transaction_name, op: transaction_op }
transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
Sentry.start_transaction(transaction: transaction, **options)
en
而从本应用所发出的的请求,也将设置请求头 sentry-trace,其值来自当前的 Transaction
module Sentry
module Net
module HTTP
def request(req, body = nil, &block)
return super unless started?
sentry_span = start_sentry_span
set_sentry_trace_header(req, sentry_span)
super.tap do |res|
record_sentry_breadcrumb(req, res)
record_sentry_span(req, res, sentry_span)
end
end
private
def set_sentry_trace_header(req, sentry_span)
return unless sentry_span
trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
req[SENTRY_TRACE_HEADER_NAME] = trace if trace
end
end
end
end
关于release
每条跟踪 release 字段可以在 Sentry.init 时指定。如无指定则一般会从 SENTRY_RELEASE 环境变量、git commit …… 获取
# sentry-ruby-core-5.1.1/lib/sentry/release_detector.r
module Sentry
class ReleaseDetector
class << self
def detect_release(project_root:, running_on_heroku:)
detect_release_from_env ||
detect_release_from_git ||
detect_release_from_capistrano(project_root) ||
detect_release_from_heroku(running_on_heroku)
end
# ...
def detect_release_from_git
Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
end
def detect_release_from_env
ENV['SENTRY_RELEASE']
end
end
end
end