sentry-ruby 5.10.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +3 -1
- data/Gemfile +12 -13
- data/README.md +26 -11
- data/Rakefile +9 -11
- data/bin/console +2 -0
- data/lib/sentry/attachment.rb +40 -0
- data/lib/sentry/background_worker.rb +11 -5
- data/lib/sentry/backpressure_monitor.rb +45 -0
- data/lib/sentry/backtrace.rb +12 -9
- data/lib/sentry/baggage.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
- data/lib/sentry/breadcrumb.rb +13 -6
- data/lib/sentry/check_in_event.rb +61 -0
- data/lib/sentry/client.rb +214 -25
- data/lib/sentry/configuration.rb +221 -38
- data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +77 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +4 -4
- data/lib/sentry/envelope/item.rb +88 -0
- data/lib/sentry/envelope.rb +2 -68
- data/lib/sentry/error_event.rb +2 -2
- data/lib/sentry/event.rb +28 -47
- data/lib/sentry/excon/middleware.rb +77 -0
- data/lib/sentry/excon.rb +10 -0
- data/lib/sentry/faraday.rb +77 -0
- data/lib/sentry/graphql.rb +9 -0
- data/lib/sentry/hub.rb +138 -6
- data/lib/sentry/integrable.rb +10 -0
- data/lib/sentry/interface.rb +1 -0
- data/lib/sentry/interfaces/exception.rb +5 -3
- data/lib/sentry/interfaces/mechanism.rb +20 -0
- data/lib/sentry/interfaces/request.rb +8 -8
- data/lib/sentry/interfaces/single_exception.rb +13 -9
- data/lib/sentry/interfaces/stacktrace.rb +3 -1
- data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
- data/lib/sentry/linecache.rb +3 -3
- data/lib/sentry/log_event.rb +206 -0
- data/lib/sentry/log_event_buffer.rb +75 -0
- data/lib/sentry/logger.rb +1 -1
- data/lib/sentry/metrics/aggregator.rb +248 -0
- data/lib/sentry/metrics/configuration.rb +47 -0
- data/lib/sentry/metrics/counter_metric.rb +25 -0
- data/lib/sentry/metrics/distribution_metric.rb +25 -0
- data/lib/sentry/metrics/gauge_metric.rb +35 -0
- data/lib/sentry/metrics/local_aggregator.rb +53 -0
- data/lib/sentry/metrics/metric.rb +19 -0
- data/lib/sentry/metrics/set_metric.rb +28 -0
- data/lib/sentry/metrics/timing.rb +51 -0
- data/lib/sentry/metrics.rb +56 -0
- data/lib/sentry/net/http.rb +27 -44
- data/lib/sentry/profiler/helpers.rb +46 -0
- data/lib/sentry/profiler.rb +41 -60
- data/lib/sentry/propagation_context.rb +135 -0
- data/lib/sentry/puma.rb +12 -5
- data/lib/sentry/rack/capture_exceptions.rb +17 -8
- data/lib/sentry/rack.rb +2 -2
- data/lib/sentry/rake.rb +4 -15
- data/lib/sentry/redis.rb +10 -4
- data/lib/sentry/release_detector.rb +5 -5
- data/lib/sentry/rspec.rb +91 -0
- data/lib/sentry/scope.rb +75 -39
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +15 -43
- data/lib/sentry/span.rb +92 -8
- data/lib/sentry/std_lib_logger.rb +50 -0
- data/lib/sentry/structured_logger.rb +138 -0
- data/lib/sentry/test_helper.rb +42 -13
- data/lib/sentry/threaded_periodic_worker.rb +39 -0
- data/lib/sentry/transaction.rb +44 -43
- data/lib/sentry/transaction_event.rb +10 -6
- data/lib/sentry/transport/configuration.rb +73 -1
- data/lib/sentry/transport/http_transport.rb +71 -41
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +53 -49
- data/lib/sentry/utils/argument_checking_helper.rb +12 -0
- data/lib/sentry/utils/env_helper.rb +21 -0
- data/lib/sentry/utils/http_tracing.rb +74 -0
- data/lib/sentry/utils/logging_helper.rb +10 -7
- data/lib/sentry/utils/real_ip.rb +2 -2
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/utils/uuid.rb +13 -0
- data/lib/sentry/vernier/output.rb +89 -0
- data/lib/sentry/vernier/profiler.rb +132 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +206 -35
- data/sentry-ruby-core.gemspec +3 -1
- data/sentry-ruby.gemspec +15 -6
- metadata +61 -11
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# Event type that represents a log entry with its attributes
|
5
|
+
#
|
6
|
+
# @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload
|
7
|
+
class LogEvent
|
8
|
+
TYPE = "log"
|
9
|
+
|
10
|
+
DEFAULT_PARAMETERS = [].freeze
|
11
|
+
DEFAULT_ATTRIBUTES = {}.freeze
|
12
|
+
|
13
|
+
SERIALIZEABLE_ATTRIBUTES = %i[
|
14
|
+
level
|
15
|
+
body
|
16
|
+
timestamp
|
17
|
+
environment
|
18
|
+
release
|
19
|
+
server_name
|
20
|
+
trace_id
|
21
|
+
attributes
|
22
|
+
contexts
|
23
|
+
]
|
24
|
+
|
25
|
+
SENTRY_ATTRIBUTES = {
|
26
|
+
"sentry.trace.parent_span_id" => :parent_span_id,
|
27
|
+
"sentry.environment" => :environment,
|
28
|
+
"sentry.release" => :release,
|
29
|
+
"sentry.address" => :server_name,
|
30
|
+
"sentry.sdk.name" => :sdk_name,
|
31
|
+
"sentry.sdk.version" => :sdk_version,
|
32
|
+
"sentry.message.template" => :template
|
33
|
+
}
|
34
|
+
|
35
|
+
USER_ATTRIBUTES = {
|
36
|
+
"user.id" => :user_id,
|
37
|
+
"user.name" => :user_username,
|
38
|
+
"user.email" => :user_email
|
39
|
+
}
|
40
|
+
|
41
|
+
LEVELS = %i[trace debug info warn error fatal].freeze
|
42
|
+
|
43
|
+
attr_accessor :level, :body, :template, :attributes, :user
|
44
|
+
|
45
|
+
attr_reader :configuration, *SERIALIZEABLE_ATTRIBUTES
|
46
|
+
|
47
|
+
SERIALIZERS = %i[
|
48
|
+
attributes
|
49
|
+
body
|
50
|
+
level
|
51
|
+
parent_span_id
|
52
|
+
sdk_name
|
53
|
+
sdk_version
|
54
|
+
timestamp
|
55
|
+
trace_id
|
56
|
+
user_id
|
57
|
+
user_username
|
58
|
+
user_email
|
59
|
+
].map { |name| [name, :"serialize_#{name}"] }.to_h
|
60
|
+
|
61
|
+
VALUE_TYPES = Hash.new("string").merge!({
|
62
|
+
TrueClass => "boolean",
|
63
|
+
FalseClass => "boolean",
|
64
|
+
Integer => "integer",
|
65
|
+
Float => "double"
|
66
|
+
}).freeze
|
67
|
+
|
68
|
+
TOKEN_REGEXP = /%\{(\w+)\}/
|
69
|
+
|
70
|
+
def initialize(configuration: Sentry.configuration, **options)
|
71
|
+
@configuration = configuration
|
72
|
+
@type = TYPE
|
73
|
+
@server_name = configuration.server_name
|
74
|
+
@environment = configuration.environment
|
75
|
+
@release = configuration.release
|
76
|
+
@timestamp = Sentry.utc_now
|
77
|
+
@level = options.fetch(:level)
|
78
|
+
@body = options[:body]
|
79
|
+
@template = @body if is_template?
|
80
|
+
@attributes = options[:attributes] || DEFAULT_ATTRIBUTES
|
81
|
+
@user = options[:user] || {}
|
82
|
+
@contexts = {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_hash
|
86
|
+
SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo|
|
87
|
+
memo[name] = serialize(name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def serialize(name)
|
94
|
+
serializer = SERIALIZERS[name]
|
95
|
+
|
96
|
+
if serializer
|
97
|
+
__send__(serializer)
|
98
|
+
else
|
99
|
+
public_send(name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def serialize_level
|
104
|
+
level.to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
def serialize_sdk_name
|
108
|
+
Sentry.sdk_meta["name"]
|
109
|
+
end
|
110
|
+
|
111
|
+
def serialize_sdk_version
|
112
|
+
Sentry.sdk_meta["version"]
|
113
|
+
end
|
114
|
+
|
115
|
+
def serialize_timestamp
|
116
|
+
timestamp.to_f
|
117
|
+
end
|
118
|
+
|
119
|
+
def serialize_trace_id
|
120
|
+
contexts.dig(:trace, :trace_id)
|
121
|
+
end
|
122
|
+
|
123
|
+
def serialize_parent_span_id
|
124
|
+
contexts.dig(:trace, :parent_span_id)
|
125
|
+
end
|
126
|
+
|
127
|
+
def serialize_body
|
128
|
+
if parameters.empty?
|
129
|
+
body
|
130
|
+
elsif parameters.is_a?(Hash)
|
131
|
+
body % parameters
|
132
|
+
else
|
133
|
+
sprintf(body, *parameters)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def serialize_user_id
|
138
|
+
user[:id]
|
139
|
+
end
|
140
|
+
|
141
|
+
def serialize_user_username
|
142
|
+
user[:username]
|
143
|
+
end
|
144
|
+
|
145
|
+
def serialize_user_email
|
146
|
+
user[:email]
|
147
|
+
end
|
148
|
+
|
149
|
+
def serialize_attributes
|
150
|
+
hash = {}
|
151
|
+
|
152
|
+
attributes.each do |key, value|
|
153
|
+
hash[key] = attribute_hash(value)
|
154
|
+
end
|
155
|
+
|
156
|
+
SENTRY_ATTRIBUTES.each do |key, name|
|
157
|
+
if (value = serialize(name))
|
158
|
+
hash[key] = attribute_hash(value)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
USER_ATTRIBUTES.each do |key, name|
|
163
|
+
if (value = serialize(name))
|
164
|
+
hash[key] = value
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
hash
|
169
|
+
end
|
170
|
+
|
171
|
+
def attribute_hash(value)
|
172
|
+
{ value: value, type: value_type(value) }
|
173
|
+
end
|
174
|
+
|
175
|
+
def value_type(value)
|
176
|
+
VALUE_TYPES[value.class]
|
177
|
+
end
|
178
|
+
|
179
|
+
def parameters
|
180
|
+
@parameters ||= begin
|
181
|
+
return DEFAULT_PARAMETERS unless template
|
182
|
+
|
183
|
+
parameters = template_tokens.empty? ?
|
184
|
+
attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens)
|
185
|
+
|
186
|
+
if parameters.is_a?(Hash)
|
187
|
+
parameters.each do |key, value|
|
188
|
+
attributes["sentry.message.parameter.#{key}"] = value
|
189
|
+
end
|
190
|
+
else
|
191
|
+
parameters.each_with_index do |param, index|
|
192
|
+
attributes["sentry.message.parameter.#{index}"] = param
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def template_tokens
|
199
|
+
@template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym)
|
200
|
+
end
|
201
|
+
|
202
|
+
def is_template?
|
203
|
+
body.include?("%s") || TOKEN_REGEXP.match?(body)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sentry/threaded_periodic_worker"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
# LogEventBuffer buffers log events and sends them to Sentry in a single envelope.
|
7
|
+
#
|
8
|
+
# This is used internally by the `Sentry::Client`.
|
9
|
+
#
|
10
|
+
# @!visibility private
|
11
|
+
class LogEventBuffer < ThreadedPeriodicWorker
|
12
|
+
FLUSH_INTERVAL = 5 # seconds
|
13
|
+
DEFAULT_MAX_EVENTS = 100
|
14
|
+
|
15
|
+
# @!visibility private
|
16
|
+
attr_reader :pending_events
|
17
|
+
|
18
|
+
def initialize(configuration, client)
|
19
|
+
super(configuration.sdk_logger, FLUSH_INTERVAL)
|
20
|
+
|
21
|
+
@client = client
|
22
|
+
@pending_events = []
|
23
|
+
@max_events = configuration.max_log_events || DEFAULT_MAX_EVENTS
|
24
|
+
@mutex = Mutex.new
|
25
|
+
|
26
|
+
log_debug("[Logging] Initialized buffer with max_events=#{@max_events}, flush_interval=#{FLUSH_INTERVAL}s")
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
ensure_thread
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def flush
|
35
|
+
@mutex.synchronize do
|
36
|
+
return if empty?
|
37
|
+
|
38
|
+
log_debug("[LogEventBuffer] flushing #{size} log events")
|
39
|
+
|
40
|
+
send_events
|
41
|
+
end
|
42
|
+
|
43
|
+
log_debug("[LogEventBuffer] flushed #{size} log events")
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
alias_method :run, :flush
|
48
|
+
|
49
|
+
def add_event(event)
|
50
|
+
raise ArgumentError, "expected a LogEvent, got #{event.class}" unless event.is_a?(LogEvent)
|
51
|
+
|
52
|
+
@mutex.synchronize do
|
53
|
+
@pending_events << event
|
54
|
+
send_events if size >= @max_events
|
55
|
+
end
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def empty?
|
61
|
+
@pending_events.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def size
|
65
|
+
@pending_events.size
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def send_events
|
71
|
+
@client.send_logs(@pending_events)
|
72
|
+
@pending_events.clear
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/sentry/logger.rb
CHANGED
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class Aggregator < ThreadedPeriodicWorker
|
6
|
+
FLUSH_INTERVAL = 5
|
7
|
+
ROLLUP_IN_SECONDS = 10
|
8
|
+
|
9
|
+
# this is how far removed from user code in the backtrace we are
|
10
|
+
# when we record code locations
|
11
|
+
DEFAULT_STACKLEVEL = 4
|
12
|
+
|
13
|
+
KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.]+/
|
14
|
+
UNIT_SANITIZATION_REGEX = /[^a-zA-Z0-9_]+/
|
15
|
+
TAG_KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.\/]+/
|
16
|
+
|
17
|
+
TAG_VALUE_SANITIZATION_MAP = {
|
18
|
+
"\n" => "\\n",
|
19
|
+
"\r" => "\\r",
|
20
|
+
"\t" => "\\t",
|
21
|
+
"\\" => "\\\\",
|
22
|
+
"|" => "\\u{7c}",
|
23
|
+
"," => "\\u{2c}"
|
24
|
+
}
|
25
|
+
|
26
|
+
METRIC_TYPES = {
|
27
|
+
c: CounterMetric,
|
28
|
+
d: DistributionMetric,
|
29
|
+
g: GaugeMetric,
|
30
|
+
s: SetMetric
|
31
|
+
}
|
32
|
+
|
33
|
+
# exposed only for testing
|
34
|
+
attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
|
35
|
+
|
36
|
+
def initialize(configuration, client)
|
37
|
+
super(configuration.sdk_logger, FLUSH_INTERVAL)
|
38
|
+
@client = client
|
39
|
+
@before_emit = configuration.metrics.before_emit
|
40
|
+
@enable_code_locations = configuration.metrics.enable_code_locations
|
41
|
+
@stacktrace_builder = configuration.stacktrace_builder
|
42
|
+
|
43
|
+
@default_tags = {}
|
44
|
+
@default_tags["release"] = configuration.release if configuration.release
|
45
|
+
@default_tags["environment"] = configuration.environment if configuration.environment
|
46
|
+
|
47
|
+
@mutex = Mutex.new
|
48
|
+
|
49
|
+
# a nested hash of timestamp -> bucket keys -> Metric instance
|
50
|
+
@buckets = {}
|
51
|
+
|
52
|
+
# the flush interval needs to be shifted once per startup to create jittering
|
53
|
+
@flush_shift = Random.rand * ROLLUP_IN_SECONDS
|
54
|
+
|
55
|
+
# a nested hash of timestamp (start of day) -> meta keys -> frame
|
56
|
+
@code_locations = {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def add(type,
|
60
|
+
key,
|
61
|
+
value,
|
62
|
+
unit: "none",
|
63
|
+
tags: {},
|
64
|
+
timestamp: nil,
|
65
|
+
stacklevel: nil)
|
66
|
+
return unless ensure_thread
|
67
|
+
return unless METRIC_TYPES.keys.include?(type)
|
68
|
+
|
69
|
+
updated_tags = get_updated_tags(tags)
|
70
|
+
return if @before_emit && !@before_emit.call(key, updated_tags)
|
71
|
+
|
72
|
+
timestamp ||= Sentry.utc_now
|
73
|
+
|
74
|
+
# this is integer division and thus takes the floor of the division
|
75
|
+
# and buckets into 10 second intervals
|
76
|
+
bucket_timestamp = (timestamp.to_i / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS
|
77
|
+
|
78
|
+
serialized_tags = serialize_tags(updated_tags)
|
79
|
+
bucket_key = [type, key, unit, serialized_tags]
|
80
|
+
|
81
|
+
added = @mutex.synchronize do
|
82
|
+
record_code_location(type, key, unit, timestamp, stacklevel: stacklevel) if @enable_code_locations
|
83
|
+
process_bucket(bucket_timestamp, bucket_key, type, value)
|
84
|
+
end
|
85
|
+
|
86
|
+
# for sets, we pass on if there was a new entry to the local gauge
|
87
|
+
local_value = type == :s ? added : value
|
88
|
+
process_span_aggregator(bucket_key, local_value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def flush(force: false)
|
92
|
+
flushable_buckets = get_flushable_buckets!(force)
|
93
|
+
code_locations = get_code_locations!
|
94
|
+
return if flushable_buckets.empty? && code_locations.empty?
|
95
|
+
|
96
|
+
envelope = Envelope.new
|
97
|
+
|
98
|
+
unless flushable_buckets.empty?
|
99
|
+
payload = serialize_buckets(flushable_buckets)
|
100
|
+
envelope.add_item(
|
101
|
+
{ type: "statsd", length: payload.bytesize },
|
102
|
+
payload
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
unless code_locations.empty?
|
107
|
+
code_locations.each do |timestamp, locations|
|
108
|
+
payload = serialize_locations(timestamp, locations)
|
109
|
+
envelope.add_item(
|
110
|
+
{ type: "metric_meta", content_type: "application/json" },
|
111
|
+
payload
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
@client.capture_envelope(envelope)
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :run, :flush
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# important to sort for key consistency
|
124
|
+
def serialize_tags(tags)
|
125
|
+
tags.flat_map do |k, v|
|
126
|
+
if v.is_a?(Array)
|
127
|
+
v.map { |x| [k.to_s, x.to_s] }
|
128
|
+
else
|
129
|
+
[[k.to_s, v.to_s]]
|
130
|
+
end
|
131
|
+
end.sort
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_flushable_buckets!(force)
|
135
|
+
@mutex.synchronize do
|
136
|
+
flushable_buckets = {}
|
137
|
+
|
138
|
+
if force
|
139
|
+
flushable_buckets = @buckets
|
140
|
+
@buckets = {}
|
141
|
+
else
|
142
|
+
cutoff = Sentry.utc_now.to_i - ROLLUP_IN_SECONDS - @flush_shift
|
143
|
+
flushable_buckets = @buckets.select { |k, _| k <= cutoff }
|
144
|
+
@buckets.reject! { |k, _| k <= cutoff }
|
145
|
+
end
|
146
|
+
|
147
|
+
flushable_buckets
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_code_locations!
|
152
|
+
@mutex.synchronize do
|
153
|
+
code_locations = @code_locations
|
154
|
+
@code_locations = {}
|
155
|
+
code_locations
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# serialize buckets to statsd format
|
160
|
+
def serialize_buckets(buckets)
|
161
|
+
buckets.map do |timestamp, timestamp_buckets|
|
162
|
+
timestamp_buckets.map do |metric_key, metric|
|
163
|
+
type, key, unit, tags = metric_key
|
164
|
+
values = metric.serialize.join(":")
|
165
|
+
sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(",")
|
166
|
+
|
167
|
+
"#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}"
|
168
|
+
end
|
169
|
+
end.flatten.join("\n")
|
170
|
+
end
|
171
|
+
|
172
|
+
def serialize_locations(timestamp, locations)
|
173
|
+
mapping = locations.map do |meta_key, location|
|
174
|
+
type, key, unit = meta_key
|
175
|
+
mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}"
|
176
|
+
|
177
|
+
# note this needs to be an array but it really doesn't serve a purpose right now
|
178
|
+
[mri, [location.merge(type: "location")]]
|
179
|
+
end.to_h
|
180
|
+
|
181
|
+
{ timestamp: timestamp, mapping: mapping }
|
182
|
+
end
|
183
|
+
|
184
|
+
def sanitize_key(key)
|
185
|
+
key.gsub(KEY_SANITIZATION_REGEX, "_")
|
186
|
+
end
|
187
|
+
|
188
|
+
def sanitize_unit(unit)
|
189
|
+
unit.gsub(UNIT_SANITIZATION_REGEX, "")
|
190
|
+
end
|
191
|
+
|
192
|
+
def sanitize_tag_key(key)
|
193
|
+
key.gsub(TAG_KEY_SANITIZATION_REGEX, "")
|
194
|
+
end
|
195
|
+
|
196
|
+
def sanitize_tag_value(value)
|
197
|
+
value.chars.map { |c| TAG_VALUE_SANITIZATION_MAP[c] || c }.join
|
198
|
+
end
|
199
|
+
|
200
|
+
def get_transaction_name
|
201
|
+
scope = Sentry.get_current_scope
|
202
|
+
return nil unless scope && scope.transaction_name
|
203
|
+
return nil if scope.transaction_source_low_quality?
|
204
|
+
|
205
|
+
scope.transaction_name
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_updated_tags(tags)
|
209
|
+
updated_tags = @default_tags.merge(tags)
|
210
|
+
|
211
|
+
transaction_name = get_transaction_name
|
212
|
+
updated_tags["transaction"] = transaction_name if transaction_name
|
213
|
+
|
214
|
+
updated_tags
|
215
|
+
end
|
216
|
+
|
217
|
+
def process_span_aggregator(key, value)
|
218
|
+
scope = Sentry.get_current_scope
|
219
|
+
return nil unless scope && scope.span
|
220
|
+
return nil if scope.transaction_source_low_quality?
|
221
|
+
|
222
|
+
scope.span.metrics_local_aggregator.add(key, value)
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_bucket(timestamp, key, type, value)
|
226
|
+
@buckets[timestamp] ||= {}
|
227
|
+
|
228
|
+
if (metric = @buckets[timestamp][key])
|
229
|
+
old_weight = metric.weight
|
230
|
+
metric.add(value)
|
231
|
+
metric.weight - old_weight
|
232
|
+
else
|
233
|
+
metric = METRIC_TYPES[type].new(value)
|
234
|
+
@buckets[timestamp][key] = metric
|
235
|
+
metric.weight
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def record_code_location(type, key, unit, timestamp, stacklevel: nil)
|
240
|
+
meta_key = [type, key, unit]
|
241
|
+
start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i
|
242
|
+
|
243
|
+
@code_locations[start_of_day] ||= {}
|
244
|
+
@code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[stacklevel || DEFAULT_STACKLEVEL])
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class Configuration
|
6
|
+
include ArgumentCheckingHelper
|
7
|
+
|
8
|
+
# Enable metrics usage.
|
9
|
+
# Starts a new {Sentry::Metrics::Aggregator} instance to aggregate metrics
|
10
|
+
# and a thread to aggregate flush every 5 seconds.
|
11
|
+
# @return [Boolean]
|
12
|
+
attr_accessor :enabled
|
13
|
+
|
14
|
+
# Enable code location reporting.
|
15
|
+
# Will be sent once per day.
|
16
|
+
# True by default.
|
17
|
+
# @return [Boolean]
|
18
|
+
attr_accessor :enable_code_locations
|
19
|
+
|
20
|
+
# Optional Proc, called before emitting a metric to the aggregator.
|
21
|
+
# Use it to filter keys (return false/nil) or update tags.
|
22
|
+
# Make sure to return true at the end.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# config.metrics.before_emit = lambda do |key, tags|
|
26
|
+
# return nil if key == 'foo'
|
27
|
+
# tags[:bar] = 42
|
28
|
+
# tags.delete(:baz)
|
29
|
+
# true
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @return [Proc, nil]
|
33
|
+
attr_reader :before_emit
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@enabled = false
|
37
|
+
@enable_code_locations = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def before_emit=(value)
|
41
|
+
check_callable!("metrics.before_emit", value)
|
42
|
+
|
43
|
+
@before_emit = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class CounterMetric < Metric
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(value)
|
13
|
+
@value += value.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize
|
17
|
+
[value]
|
18
|
+
end
|
19
|
+
|
20
|
+
def weight
|
21
|
+
1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class DistributionMetric < Metric
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = [value.to_f]
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(value)
|
13
|
+
@value << value.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize
|
17
|
+
value
|
18
|
+
end
|
19
|
+
|
20
|
+
def weight
|
21
|
+
value.size
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class GaugeMetric < Metric
|
6
|
+
attr_reader :last, :min, :max, :sum, :count
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
value = value.to_f
|
10
|
+
@last = value
|
11
|
+
@min = value
|
12
|
+
@max = value
|
13
|
+
@sum = value
|
14
|
+
@count = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(value)
|
18
|
+
value = value.to_f
|
19
|
+
@last = value
|
20
|
+
@min = [@min, value].min
|
21
|
+
@max = [@max, value].max
|
22
|
+
@sum += value
|
23
|
+
@count += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialize
|
27
|
+
[last, min, max, sum, count]
|
28
|
+
end
|
29
|
+
|
30
|
+
def weight
|
31
|
+
5
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|