miniapm 1.0.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/lib/generators/miniapm/install_generator.rb +27 -0
- data/lib/generators/miniapm/templates/README +19 -0
- data/lib/generators/miniapm/templates/initializer.rb +60 -0
- data/lib/miniapm/configuration.rb +176 -0
- data/lib/miniapm/context.rb +138 -0
- data/lib/miniapm/error_event.rb +130 -0
- data/lib/miniapm/exporters/errors.rb +67 -0
- data/lib/miniapm/exporters/otlp.rb +90 -0
- data/lib/miniapm/instrumentations/activejob.rb +271 -0
- data/lib/miniapm/instrumentations/activerecord.rb +123 -0
- data/lib/miniapm/instrumentations/base.rb +61 -0
- data/lib/miniapm/instrumentations/cache.rb +85 -0
- data/lib/miniapm/instrumentations/http/faraday.rb +112 -0
- data/lib/miniapm/instrumentations/http/httparty.rb +84 -0
- data/lib/miniapm/instrumentations/http/net_http.rb +99 -0
- data/lib/miniapm/instrumentations/rails/controller.rb +129 -0
- data/lib/miniapm/instrumentations/rails/railtie.rb +42 -0
- data/lib/miniapm/instrumentations/redis/redis.rb +135 -0
- data/lib/miniapm/instrumentations/redis/redis_client.rb +116 -0
- data/lib/miniapm/instrumentations/registry.rb +90 -0
- data/lib/miniapm/instrumentations/search/elasticsearch.rb +121 -0
- data/lib/miniapm/instrumentations/search/opensearch.rb +120 -0
- data/lib/miniapm/instrumentations/search/searchkick.rb +119 -0
- data/lib/miniapm/instrumentations/sidekiq.rb +185 -0
- data/lib/miniapm/middleware/error_handler.rb +120 -0
- data/lib/miniapm/middleware/rack.rb +103 -0
- data/lib/miniapm/span.rb +289 -0
- data/lib/miniapm/testing.rb +209 -0
- data/lib/miniapm/trace.rb +26 -0
- data/lib/miniapm/transport/batch_sender.rb +345 -0
- data/lib/miniapm/transport/http.rb +45 -0
- data/lib/miniapm/version.rb +5 -0
- data/lib/miniapm.rb +184 -0
- metadata +183 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
# Testing utilities for capturing and inspecting spans/errors in tests
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# require 'miniapm/testing'
|
|
8
|
+
#
|
|
9
|
+
# RSpec.describe "MyFeature" do
|
|
10
|
+
# before { MiniAPM::Testing.enable! }
|
|
11
|
+
# after { MiniAPM::Testing.disable! }
|
|
12
|
+
#
|
|
13
|
+
# it "tracks spans" do
|
|
14
|
+
# perform_action
|
|
15
|
+
# expect(MiniAPM::Testing.spans).to include(
|
|
16
|
+
# having_attributes(name: /process_action/)
|
|
17
|
+
# )
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module Testing
|
|
22
|
+
class << self
|
|
23
|
+
# Enable test mode - captures spans/errors instead of sending them
|
|
24
|
+
def enable!
|
|
25
|
+
@enabled = true
|
|
26
|
+
@spans = []
|
|
27
|
+
@errors = []
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
|
|
30
|
+
# Stub the record methods to capture instead of send
|
|
31
|
+
install_test_hooks!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Disable test mode and restore normal behavior
|
|
35
|
+
def disable!
|
|
36
|
+
@enabled = false
|
|
37
|
+
clear!
|
|
38
|
+
uninstall_test_hooks!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if test mode is enabled
|
|
42
|
+
def enabled?
|
|
43
|
+
@enabled || false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get all captured spans
|
|
47
|
+
def spans
|
|
48
|
+
@mutex.synchronize { @spans.dup }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get all captured errors
|
|
52
|
+
def errors
|
|
53
|
+
@mutex.synchronize { @errors.dup }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Alias for spans
|
|
57
|
+
def recorded_spans
|
|
58
|
+
spans
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Alias for errors
|
|
62
|
+
def recorded_errors
|
|
63
|
+
errors
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Clear all captured data
|
|
67
|
+
def clear!
|
|
68
|
+
@mutex&.synchronize do
|
|
69
|
+
@spans = []
|
|
70
|
+
@errors = []
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Find spans matching criteria
|
|
75
|
+
def find_spans(name: nil, category: nil, &block)
|
|
76
|
+
result = spans
|
|
77
|
+
result = result.select { |s| s.name.match?(name) } if name
|
|
78
|
+
result = result.select { |s| s.category == category } if category
|
|
79
|
+
result = result.select(&block) if block_given?
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Find errors matching criteria
|
|
84
|
+
def find_errors(exception_class: nil, message: nil, &block)
|
|
85
|
+
result = errors
|
|
86
|
+
result = result.select { |e| e.exception_class == exception_class } if exception_class
|
|
87
|
+
result = result.select { |e| e.message.match?(message) } if message
|
|
88
|
+
result = result.select(&block) if block_given?
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if any span matches
|
|
93
|
+
def span_recorded?(name: nil, category: nil)
|
|
94
|
+
find_spans(name: name, category: category).any?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if any error matches
|
|
98
|
+
def error_recorded?(exception_class: nil, message: nil)
|
|
99
|
+
find_errors(exception_class: exception_class, message: message).any?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Record a span (called by test hooks)
|
|
103
|
+
def record_span(span)
|
|
104
|
+
return unless enabled?
|
|
105
|
+
|
|
106
|
+
@mutex.synchronize { @spans << span }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Record an error (called by test hooks)
|
|
110
|
+
def record_error(error)
|
|
111
|
+
return unless enabled?
|
|
112
|
+
|
|
113
|
+
@mutex.synchronize { @errors << error }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def install_test_hooks!
|
|
119
|
+
# Guard against double installation
|
|
120
|
+
return if @hooks_installed
|
|
121
|
+
|
|
122
|
+
# Store original methods
|
|
123
|
+
@original_record_span = MiniAPM.method(:record_span)
|
|
124
|
+
@original_record_error = MiniAPM.method(:record_error)
|
|
125
|
+
|
|
126
|
+
# Replace with test versions (suppress warnings)
|
|
127
|
+
testing = self
|
|
128
|
+
original_verbose = $VERBOSE
|
|
129
|
+
begin
|
|
130
|
+
$VERBOSE = nil
|
|
131
|
+
MiniAPM.define_singleton_method(:record_span) do |span|
|
|
132
|
+
testing.record_span(span)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
MiniAPM.define_singleton_method(:record_error) do |exception, context: {}|
|
|
136
|
+
error_event = ErrorEvent.from_exception(exception, context)
|
|
137
|
+
testing.record_error(error_event)
|
|
138
|
+
end
|
|
139
|
+
ensure
|
|
140
|
+
$VERBOSE = original_verbose
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
@hooks_installed = true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def uninstall_test_hooks!
|
|
147
|
+
return unless @hooks_installed
|
|
148
|
+
|
|
149
|
+
original_verbose = $VERBOSE
|
|
150
|
+
begin
|
|
151
|
+
$VERBOSE = nil
|
|
152
|
+
MiniAPM.define_singleton_method(:record_span, @original_record_span)
|
|
153
|
+
MiniAPM.define_singleton_method(:record_error, @original_record_error)
|
|
154
|
+
ensure
|
|
155
|
+
$VERBOSE = original_verbose
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
@original_record_span = nil
|
|
159
|
+
@original_record_error = nil
|
|
160
|
+
@hooks_installed = false
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# RSpec integration
|
|
167
|
+
if defined?(RSpec)
|
|
168
|
+
RSpec.configure do |config|
|
|
169
|
+
config.before(:each, :miniapm) do
|
|
170
|
+
MiniAPM::Testing.enable!
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
config.after(:each, :miniapm) do
|
|
174
|
+
MiniAPM::Testing.disable!
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Minitest integration
|
|
180
|
+
if defined?(Minitest)
|
|
181
|
+
module MiniAPM
|
|
182
|
+
module Testing
|
|
183
|
+
module MinitestHooks
|
|
184
|
+
def before_setup
|
|
185
|
+
super
|
|
186
|
+
MiniAPM::Testing.enable! if self.class.miniapm_enabled?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def after_teardown
|
|
190
|
+
MiniAPM::Testing.disable! if self.class.miniapm_enabled?
|
|
191
|
+
super
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
module MinitestClassMethods
|
|
196
|
+
def miniapm_enabled?
|
|
197
|
+
@miniapm_enabled || false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def enable_miniapm!
|
|
201
|
+
@miniapm_enabled = true
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
Minitest::Test.prepend(MiniAPM::Testing::MinitestHooks)
|
|
208
|
+
Minitest::Test.extend(MiniAPM::Testing::MinitestClassMethods)
|
|
209
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module MiniAPM
|
|
6
|
+
class Trace
|
|
7
|
+
attr_reader :trace_id
|
|
8
|
+
attr_accessor :sampled
|
|
9
|
+
|
|
10
|
+
def initialize(trace_id: nil, sampled: nil)
|
|
11
|
+
@trace_id = trace_id || SecureRandom.hex(16)
|
|
12
|
+
@sampled = sampled.nil? ? should_sample? : sampled
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def sampled?
|
|
17
|
+
@sampled
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def should_sample?
|
|
23
|
+
rand < MiniAPM.configuration.sample_rate
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module MiniAPM
|
|
6
|
+
module Transport
|
|
7
|
+
class BatchSender
|
|
8
|
+
SHUTDOWN_TIMEOUT = 5 # seconds to wait for flush on shutdown
|
|
9
|
+
MAX_RETRY_ATTEMPTS = 3
|
|
10
|
+
BASE_RETRY_DELAY = 1.0 # seconds
|
|
11
|
+
MAX_CONCURRENT_SENDS = 4
|
|
12
|
+
|
|
13
|
+
# Circuit breaker settings
|
|
14
|
+
CIRCUIT_FAILURE_THRESHOLD = 5 # failures before opening circuit
|
|
15
|
+
CIRCUIT_RESET_TIMEOUT = 60 # seconds before trying again
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def start!
|
|
19
|
+
@start_mutex ||= Mutex.new
|
|
20
|
+
|
|
21
|
+
@start_mutex.synchronize do
|
|
22
|
+
return if @started
|
|
23
|
+
|
|
24
|
+
@started = true
|
|
25
|
+
@shutdown = false
|
|
26
|
+
|
|
27
|
+
@queues = {
|
|
28
|
+
span: Queue.new,
|
|
29
|
+
error: Queue.new
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@mutex = Mutex.new
|
|
33
|
+
@batches = { span: [], error: [] }
|
|
34
|
+
@last_flush = Time.now
|
|
35
|
+
|
|
36
|
+
# Stats for monitoring
|
|
37
|
+
reset_stats!
|
|
38
|
+
|
|
39
|
+
# Bounded thread pool for sending
|
|
40
|
+
@send_queue = Queue.new
|
|
41
|
+
@send_threads = MAX_CONCURRENT_SENDS.times.map do |i|
|
|
42
|
+
Thread.new { send_worker_loop(i) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
start_worker_thread!
|
|
46
|
+
setup_shutdown_hook!
|
|
47
|
+
|
|
48
|
+
MiniAPM.logger.debug { "MiniAPM BatchSender started" }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stop!
|
|
53
|
+
@start_mutex ||= Mutex.new
|
|
54
|
+
|
|
55
|
+
@start_mutex.synchronize do
|
|
56
|
+
return unless @started
|
|
57
|
+
|
|
58
|
+
@shutdown = true
|
|
59
|
+
|
|
60
|
+
# Drain queues and flush remaining data
|
|
61
|
+
drain_queues_to_batches!
|
|
62
|
+
flush_all!
|
|
63
|
+
|
|
64
|
+
# Wait for worker thread
|
|
65
|
+
@worker_thread&.join(SHUTDOWN_TIMEOUT)
|
|
66
|
+
|
|
67
|
+
# Signal send threads to stop and wait
|
|
68
|
+
MAX_CONCURRENT_SENDS.times { @send_queue << :shutdown }
|
|
69
|
+
@send_threads&.each { |t| t.join(SHUTDOWN_TIMEOUT) }
|
|
70
|
+
|
|
71
|
+
@started = false
|
|
72
|
+
|
|
73
|
+
MiniAPM.logger.debug { "MiniAPM BatchSender stopped" }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def enqueue(type, item)
|
|
78
|
+
return unless @started
|
|
79
|
+
|
|
80
|
+
queue = @queues[type]
|
|
81
|
+
return unless queue
|
|
82
|
+
|
|
83
|
+
config = MiniAPM.configuration
|
|
84
|
+
|
|
85
|
+
# Drop if queue is full (backpressure)
|
|
86
|
+
if queue.size >= config.max_queue_size
|
|
87
|
+
increment_stat(:dropped, type)
|
|
88
|
+
MiniAPM.logger.warn { "MiniAPM: Queue full, dropping #{type}" }
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
queue << item
|
|
93
|
+
increment_stat(:enqueued, type)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def started?
|
|
97
|
+
@started || false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Force flush all pending data (useful for testing)
|
|
101
|
+
def flush!
|
|
102
|
+
return unless @started
|
|
103
|
+
|
|
104
|
+
# First, drain queues into batches
|
|
105
|
+
drain_queues_to_batches!
|
|
106
|
+
|
|
107
|
+
# Then flush all batches
|
|
108
|
+
flush_all!
|
|
109
|
+
|
|
110
|
+
# Wait for send queue to drain
|
|
111
|
+
deadline = Time.now + 5 # 5 second timeout
|
|
112
|
+
while @send_queue&.size&.positive? && Time.now < deadline
|
|
113
|
+
sleep 0.1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get current stats
|
|
118
|
+
def stats
|
|
119
|
+
@mutex.synchronize { @stats.dup }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Reset stats
|
|
123
|
+
def reset_stats!
|
|
124
|
+
@mutex.synchronize do
|
|
125
|
+
@stats = {
|
|
126
|
+
enqueued: { span: 0, error: 0 },
|
|
127
|
+
sent: { span: 0, error: 0 },
|
|
128
|
+
dropped: { span: 0, error: 0 },
|
|
129
|
+
failed: { span: 0, error: 0 },
|
|
130
|
+
retries: 0,
|
|
131
|
+
circuit_opened: 0
|
|
132
|
+
}
|
|
133
|
+
# Circuit breaker state
|
|
134
|
+
@circuit_failures = 0
|
|
135
|
+
@circuit_opened_at = nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check if circuit breaker allows requests
|
|
140
|
+
def circuit_open?
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
return false if @circuit_failures < CIRCUIT_FAILURE_THRESHOLD
|
|
143
|
+
|
|
144
|
+
# Check if we should try again (half-open state)
|
|
145
|
+
if @circuit_opened_at && (Time.now - @circuit_opened_at) >= CIRCUIT_RESET_TIMEOUT
|
|
146
|
+
MiniAPM.logger.info { "MiniAPM: Circuit breaker half-open, attempting request" }
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def record_circuit_success!
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
@circuit_failures = 0
|
|
157
|
+
@circuit_opened_at = nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def record_circuit_failure!
|
|
162
|
+
@mutex.synchronize do
|
|
163
|
+
@circuit_failures += 1
|
|
164
|
+
if @circuit_failures >= CIRCUIT_FAILURE_THRESHOLD && @circuit_opened_at.nil?
|
|
165
|
+
@circuit_opened_at = Time.now
|
|
166
|
+
@stats[:circuit_opened] += 1
|
|
167
|
+
MiniAPM.logger.warn { "MiniAPM: Circuit breaker opened after #{@circuit_failures} failures" }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def drain_queues_to_batches!
|
|
175
|
+
@queues.each do |type, queue|
|
|
176
|
+
@mutex.synchronize do
|
|
177
|
+
while !queue.empty?
|
|
178
|
+
begin
|
|
179
|
+
item = queue.pop(true) # non-blocking
|
|
180
|
+
@batches[type] << item
|
|
181
|
+
rescue ThreadError
|
|
182
|
+
break
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def increment_stat(stat, type = nil)
|
|
190
|
+
@mutex.synchronize do
|
|
191
|
+
if type
|
|
192
|
+
@stats[stat][type] += 1
|
|
193
|
+
else
|
|
194
|
+
@stats[stat] += 1
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def start_worker_thread!
|
|
200
|
+
@worker_thread = Thread.new do
|
|
201
|
+
Thread.current.name = "miniapm-batcher"
|
|
202
|
+
Thread.current.report_on_exception = false
|
|
203
|
+
|
|
204
|
+
until @shutdown
|
|
205
|
+
begin
|
|
206
|
+
process_queues
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
MiniAPM.logger.error { "MiniAPM worker error: #{e.class}: #{e.message}" }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
sleep 0.1 # Small sleep to prevent busy-waiting
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def send_worker_loop(worker_id)
|
|
217
|
+
Thread.current.name = "miniapm-sender-#{worker_id}"
|
|
218
|
+
Thread.current.report_on_exception = false
|
|
219
|
+
|
|
220
|
+
loop do
|
|
221
|
+
work = @send_queue.pop
|
|
222
|
+
break if work == :shutdown
|
|
223
|
+
|
|
224
|
+
type, items = work
|
|
225
|
+
send_with_retry(type, items)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def process_queues
|
|
230
|
+
config = MiniAPM.configuration
|
|
231
|
+
|
|
232
|
+
@queues.each do |type, queue|
|
|
233
|
+
# Drain queue into batch
|
|
234
|
+
@mutex.synchronize do
|
|
235
|
+
while !queue.empty? && @batches[type].size < config.batch_size
|
|
236
|
+
begin
|
|
237
|
+
item = queue.pop(true) # non-blocking
|
|
238
|
+
@batches[type] << item
|
|
239
|
+
rescue ThreadError
|
|
240
|
+
break
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Flush if batch is full or timer expired
|
|
246
|
+
flush_type!(type) if should_flush?(type)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def should_flush?(type)
|
|
251
|
+
@mutex.synchronize do
|
|
252
|
+
batch = @batches[type]
|
|
253
|
+
return false if batch.empty?
|
|
254
|
+
|
|
255
|
+
config = MiniAPM.configuration
|
|
256
|
+
batch.size >= config.batch_size ||
|
|
257
|
+
(Time.now - @last_flush) >= config.flush_interval
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def flush_type!(type)
|
|
262
|
+
items = nil
|
|
263
|
+
|
|
264
|
+
@mutex.synchronize do
|
|
265
|
+
batch = @batches[type]
|
|
266
|
+
return if batch.empty?
|
|
267
|
+
|
|
268
|
+
items = batch.dup
|
|
269
|
+
batch.clear
|
|
270
|
+
@last_flush = Time.now
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
return unless items && !items.empty?
|
|
274
|
+
|
|
275
|
+
# Queue for sending (bounded by thread pool)
|
|
276
|
+
@send_queue << [type, items]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def flush_all!
|
|
280
|
+
@queues.each_key { |type| flush_type!(type) }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def send_with_retry(type, items)
|
|
284
|
+
# Check circuit breaker first
|
|
285
|
+
if circuit_open?
|
|
286
|
+
MiniAPM.logger.debug { "MiniAPM: Circuit open, dropping #{items.size} #{type}(s)" }
|
|
287
|
+
increment_stat(:dropped, type)
|
|
288
|
+
return false
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
attempts = 0
|
|
292
|
+
|
|
293
|
+
loop do
|
|
294
|
+
attempts += 1
|
|
295
|
+
result = send_batch(type, items)
|
|
296
|
+
|
|
297
|
+
if result[:success]
|
|
298
|
+
@mutex.synchronize { @stats[:sent][type] += items.size }
|
|
299
|
+
record_circuit_success!
|
|
300
|
+
return true
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Don't retry on client errors (4xx)
|
|
304
|
+
if result[:status] && result[:status] >= 400 && result[:status] < 500
|
|
305
|
+
MiniAPM.logger.warn { "MiniAPM: Client error #{result[:status]}, not retrying" }
|
|
306
|
+
increment_stat(:failed, type)
|
|
307
|
+
return false
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Check if we should retry
|
|
311
|
+
if attempts >= MAX_RETRY_ATTEMPTS
|
|
312
|
+
MiniAPM.logger.error { "MiniAPM: Failed to send #{type} after #{attempts} attempts" }
|
|
313
|
+
increment_stat(:failed, type)
|
|
314
|
+
record_circuit_failure!
|
|
315
|
+
return false
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Exponential backoff
|
|
319
|
+
delay = BASE_RETRY_DELAY * (2 ** (attempts - 1))
|
|
320
|
+
delay += rand * delay * 0.1 # Add jitter
|
|
321
|
+
increment_stat(:retries)
|
|
322
|
+
MiniAPM.logger.debug { "MiniAPM: Retrying #{type} in #{delay.round(2)}s (attempt #{attempts})" }
|
|
323
|
+
sleep delay
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def send_batch(type, items)
|
|
328
|
+
case type
|
|
329
|
+
when :span
|
|
330
|
+
Exporters::OTLP.export(items)
|
|
331
|
+
when :error
|
|
332
|
+
Exporters::Errors.export_batch(items)
|
|
333
|
+
end
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
MiniAPM.logger.error { "MiniAPM send error: #{e.class}: #{e.message}" }
|
|
336
|
+
{ success: false, error: e }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def setup_shutdown_hook!
|
|
340
|
+
at_exit { stop! }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module MiniAPM
|
|
8
|
+
module Transport
|
|
9
|
+
class HTTP
|
|
10
|
+
DEFAULT_TIMEOUT = 10
|
|
11
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def post(url, payload, headers: {})
|
|
15
|
+
uri = URI.parse(url)
|
|
16
|
+
|
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
18
|
+
http.use_ssl = uri.scheme == "https"
|
|
19
|
+
http.open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
20
|
+
http.read_timeout = DEFAULT_TIMEOUT
|
|
21
|
+
http.write_timeout = DEFAULT_TIMEOUT if http.respond_to?(:write_timeout=)
|
|
22
|
+
|
|
23
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
24
|
+
request["Content-Type"] = "application/json"
|
|
25
|
+
request["User-Agent"] = "miniapm-ruby/#{MiniAPM::VERSION}"
|
|
26
|
+
|
|
27
|
+
headers.each { |key, value| request[key] = value }
|
|
28
|
+
|
|
29
|
+
request.body = payload.is_a?(String) ? payload : JSON.generate(payload)
|
|
30
|
+
|
|
31
|
+
response = http.request(request)
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
status: response.code.to_i,
|
|
35
|
+
body: response.body,
|
|
36
|
+
success: response.is_a?(Net::HTTPSuccess)
|
|
37
|
+
}
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
MiniAPM.logger.warn { "MiniAPM HTTP error: #{e.class}: #{e.message}" }
|
|
40
|
+
{ status: 0, body: e.message, success: false, error: e }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|