Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
require "sentry/utils/encoding_helper"
require "sentry/utils/logging_helper"
require "sentry/configuration"
require "sentry/logger"
require "sentry/logging"
require "sentry/event"
require "sentry/error_event"
require "sentry/transaction_event"
require "sentry/check_in_event"
require "sentry/log_event"
require "sentry/span"
require "sentry/transaction"
require "sentry/hub"
Expand Down Expand Up @@ -251,6 +252,9 @@ def init(&block)
@backpressure_monitor = config.enable_backpressure_handling ? Sentry::BackpressureMonitor.new(config, client) : nil
@metrics_aggregator = config.metrics.enabled ? Sentry::Metrics::Aggregator.new(config, client) : nil
exception_locals_tp.enable if config.include_local_variables

Sentry::Logging.setup_logger(config)

at_exit { close }
end

Expand Down Expand Up @@ -489,6 +493,19 @@ def capture_check_in(slug, status, **options)
get_current_hub.capture_check_in(slug, status, **options)
end

def capture_log(message, level: :info, **attributes)
return unless initialized?

event = LogEvent.new(
level: level,
body: message,
timestamp: Time.now.to_f,
attributes: attributes
)

capture_event(event)
end

# Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
#
# @return [Transaction, nil]
Expand Down
5 changes: 5 additions & 0 deletions sentry-ruby/lib/sentry/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ def capture_exception_frame_locals=(value)
# @!visibility private
attr_reader :errors, :gem_specs

# Experimental features configuration
# @return [Hash]
attr_accessor :_experiments

# These exceptions could enter Puma's `lowlevel_error_handler` callback and the SDK's Puma integration
# But they are mostly considered as noise and should be ignored by default
# Please see https://github.com/getsentry/sentry-ruby/pull/2026 for more information
Expand Down Expand Up @@ -452,6 +456,7 @@ def initialize
@transport = Transport::Configuration.new
@cron = Cron::Configuration.new
@metrics = Metrics::Configuration.new
@_experiments = {}
@gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)

run_post_initialization_callbacks
Expand Down
2 changes: 1 addition & 1 deletion sentry-ruby/lib/sentry/envelope/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Envelope::Item
# rate limits and client reports use the data_category rather than envelope item type
def self.data_category(type)
case type
when "session", "attachment", "transaction", "profile", "span" then type
when "session", "attachment", "transaction", "profile", "span", "log" then type
when "sessions" then "session"
when "check_in" then "monitor"
when "statsd", "metric_meta" then "metric_bucket"
Expand Down
59 changes: 59 additions & 0 deletions sentry-ruby/lib/sentry/log_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Sentry
class LogEvent < Event
TYPE = "log"

LEVELS = %i[trace debug info warn error fatal].freeze

attr_accessor :level, :body, :attributes, :trace_id

def initialize(configuration: Sentry.configuration, **options)
super(configuration: configuration)
@type = TYPE
@level = options.fetch(:level)
@body = options[:body]
@trace_id = options[:trace_id] || SecureRandom.hex(16)
@attributes = options[:attributes] || {}
end

# https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload
def to_hash
data = {}
data[:level] = @level.to_s
data[:body] = @body
data[:timestamp] = Time.parse(@timestamp).to_f
data[:trace_id] = @trace_id
data[:attributes] = serialize_attributes
data
end

private

def serialize_attributes
result = {}

@attributes.each do |key, value|
result[key.to_s] = {
value: value.to_s,
type: value_type(value)
}
end

result
end

def value_type(value)
case value
when Integer
"integer"
when TrueClass, FalseClass
"boolean"
when Float
"double"
else
"string"
end
end
end
end
17 changes: 17 additions & 0 deletions sentry-ruby/lib/sentry/logging.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "logger"
require_relative "logging/device"
require_relative "logging/handler"

module Sentry
module Logging
def self.setup_logger(config)
if config._experiments[:enable_logs]
config.logger = Device.new(handlers: [config.logger, Handler.new(config)])
end

config.logger
end
end
end
46 changes: 46 additions & 0 deletions sentry-ruby/lib/sentry/logging/device.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module Sentry
module Logging
class Device
attr_reader :handlers

def initialize(options)
@handlers = options.fetch(:handlers)
end

def trace(message, payload = {})
log(:trace, message, payload)
end

def debug(message, payload = {})
log(:debug, message, payload)
end

def info(message, payload = {})
log(:info, message, payload)
end

def warn(message, payload = {})
log(:warn, message, payload)
end

def error(message, payload = {})
log(:error, message, payload)
end

def fatal(message, payload = {})
log(:fatal, message, payload)
end

def log(level, message, payload)
handlers.each do |handler|
case handler
when Sentry::Logger
handler.public_send(level, message)
else
handler.public_send(level, message, payload)
end
end
end
end
end
end
49 changes: 49 additions & 0 deletions sentry-ruby/lib/sentry/logging/handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module Sentry
module Logging
class Handler
# https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number
LEVELS = {
"trace" => 1,
"debug" => 5,
"info" => 9,
"warn" => 13,
"error" => 17,
"fatal" => 21
}.freeze

attr_reader :config

def initialize(config)
@config = config
end

def trace(message, payload = {})
log(:trace, message, payload)
end

def debug(message, payload = {})
log(:debug, message, payload)
end

def info(message, payload = {})
log(:info, message, payload)
end

def warn(message, payload = {})
log(:warn, message, payload)
end

def error(message, payload = {})
log(:error, message, payload)
end

def fatal(message, payload = {})
log(:fatal, message, payload)
end

def log(level, message, payload)
Sentry.capture_log(message, level: level, severity: LEVELS[level], **payload)
end
end
end
end
2 changes: 1 addition & 1 deletion sentry-ruby/lib/sentry/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def apply_to_event(event, hint = nil)
event.transaction = transaction_name if transaction_name
event.transaction_info = { source: transaction_source } if transaction_source
event.fingerprint = fingerprint
event.level = level
event.level = level unless event.is_a?(LogEvent)
event.breadcrumbs = breadcrumbs
event.rack_env = rack_env if rack_env
event.attachments = attachments
Expand Down
19 changes: 15 additions & 4 deletions sentry-ruby/lib/sentry/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,21 @@ def envelope_from_event(event)

envelope = Envelope.new(envelope_headers)

envelope.add_item(
{ type: item_type, content_type: "application/json" },
event_payload
)
if event.is_a?(LogEvent)
envelope.add_item(
{
type: "log",
item_count: 1,
content_type: "application/vnd.sentry.items.log+json"
},
{ items: [event_payload] }
)
else
envelope.add_item(
{ type: item_type, content_type: "application/json" },
event_payload
)
end

if event.is_a?(TransactionEvent) && event.profile
envelope.add_item(
Expand Down
11 changes: 11 additions & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def sentry_context
envelope.add_item({ type: 'event' }, { payload: 'test' })
envelope.add_item({ type: 'statsd' }, { payload: 'test2' })
envelope.add_item({ type: 'transaction' }, { payload: 'test3' })
envelope.add_item({ type: 'log' }, { level: 'info', message: 'test4' })
envelope
end

Expand All @@ -119,6 +120,15 @@ def sentry_context
subject.send_envelope(envelope)
end

it 'includes log item in the envelope' do
log_item = envelope.items.find { |item| item.type == 'log' }

expect(log_item).not_to be_nil
expect(log_item.payload[:level]).to eq('info')
expect(log_item.payload[:message]).to eq('test4')
expect(log_item.data_category).to eq('log')
end

it 'sends envelope with spotlight transport if enabled' do
configuration.spotlight = true

Expand Down Expand Up @@ -153,6 +163,7 @@ def sentry_context
expect(subject.transport).to have_recorded_lost_event(:network_error, 'error')
expect(subject.transport).to have_recorded_lost_event(:network_error, 'metric_bucket')
expect(subject.transport).to have_recorded_lost_event(:network_error, 'transaction')
expect(subject.transport).to have_recorded_lost_event(:network_error, 'log')
end
end
end
Expand Down
1 change: 1 addition & 0 deletions sentry-ruby/spec/sentry/envelope/item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
['transaction', 'transaction'],
['span', 'span'],
['profile', 'profile'],
['log', 'log'],
['check_in', 'monitor'],
['statsd', 'metric_bucket'],
['metric_meta', 'metric_bucket'],
Expand Down
Loading
Loading