flare 0.3.0 → 0.4.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/lib/flare/client_headers.rb +39 -0
- data/lib/flare/deadline.rb +27 -0
- data/lib/flare/filtering_span_processor.rb +180 -30
- data/lib/flare/http_transport.rb +24 -10
- data/lib/flare/lifecycle.rb +39 -0
- data/lib/flare/metric_flusher.rb +154 -12
- data/lib/flare/metric_storage.rb +20 -0
- data/lib/flare/metric_submitter.rb +38 -20
- data/lib/flare/recording_batch_span_processor.rb +238 -0
- data/lib/flare/rule_manager.rb +3 -2
- data/lib/flare/sqlite_exporter.rb +80 -25
- data/lib/flare/trace_exporter.rb +50 -11
- data/lib/flare/version.rb +1 -1
- data/lib/flare.rb +4 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cee6e0910ebcd04f073efc41638823f7ed4d7abf511ab71b68caead994997af1
|
|
4
|
+
data.tar.gz: f28d669e444b2abb8b6043990b8f1cfc5cbd39abc14d25942cf5f41e08a80cb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06647d35a64ae38dafb6e946b181fd87a8b8dc8e7065c5b0d4af3cb47419bd5be5dc9e930f3aa322740e824279169d9b2ae867c2b3c14dfda22c1d01be25507d
|
|
7
|
+
data.tar.gz: 837f70a2ef44f4d0f8c415cc15c6455ab5250b63e7dd06580ad640492385d8d1e6842cda3f56b036d918bb38c7b55438a2d60546a6c41fd7ce77a429b2d1793a
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
require_relative "version"
|
|
6
|
+
|
|
7
|
+
module Flare
|
|
8
|
+
# Single source of truth for the headers that identify this client and its
|
|
9
|
+
# version to the Flare API. The server parses the gem version out of the
|
|
10
|
+
# User-Agent ("Flare Ruby/X.Y.Z") to gate version-dependent features, and
|
|
11
|
+
# uses the X-Client-* headers to build a per-client picture.
|
|
12
|
+
#
|
|
13
|
+
# Safe to send on any request that targets the Flare API (metrics POST,
|
|
14
|
+
# rules GET, trace-notify POST). Do NOT send these on the presigned R2 blob
|
|
15
|
+
# PUT -- they're meaningless to R2 and can invalidate the signed-header set.
|
|
16
|
+
#
|
|
17
|
+
# Excludes Authorization / Flare-Project / Flare-Environment -- callers add
|
|
18
|
+
# those since they're request- or context-specific.
|
|
19
|
+
module ClientHeaders
|
|
20
|
+
USER_AGENT = "Flare Ruby/#{Flare::VERSION}"
|
|
21
|
+
|
|
22
|
+
def self.to_h
|
|
23
|
+
{
|
|
24
|
+
"User-Agent" => USER_AGENT,
|
|
25
|
+
"X-Client-Language" => "ruby",
|
|
26
|
+
"X-Client-Language-Version" => RUBY_VERSION,
|
|
27
|
+
"X-Client-Platform" => RUBY_PLATFORM,
|
|
28
|
+
"X-Client-Pid" => Process.pid.to_s,
|
|
29
|
+
"X-Client-Hostname" => hostname
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.hostname
|
|
34
|
+
Socket.gethostname
|
|
35
|
+
rescue StandardError
|
|
36
|
+
"unknown"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
# A small monotonic deadline shared by lifecycle operations. A nil timeout
|
|
5
|
+
# represents an unbounded operation.
|
|
6
|
+
class Deadline
|
|
7
|
+
def initialize(timeout)
|
|
8
|
+
@expires_at = monotonic_now + [timeout.to_f, 0].max unless timeout.nil?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def remaining
|
|
12
|
+
return nil unless @expires_at
|
|
13
|
+
|
|
14
|
+
[@expires_at - monotonic_now, 0].max
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def expired?
|
|
18
|
+
remaining == 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def monotonic_now
|
|
24
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,6 +4,8 @@ require "concurrent/atomic/atomic_fixnum"
|
|
|
4
4
|
require "logger"
|
|
5
5
|
require "opentelemetry/sdk"
|
|
6
6
|
|
|
7
|
+
require_relative "deadline"
|
|
8
|
+
|
|
7
9
|
module Flare
|
|
8
10
|
# BSP-shaped span processor whose filter is `sampled OR marked` instead
|
|
9
11
|
# of BSP's `sampled` (BSP early-returns on RECORD_ONLY spans -- our
|
|
@@ -17,6 +19,7 @@ module Flare
|
|
|
17
19
|
class FilteringSpanProcessor
|
|
18
20
|
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
19
21
|
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
22
|
+
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
20
23
|
|
|
21
24
|
DEFAULT_MAX_QUEUE = 5_000
|
|
22
25
|
DEFAULT_FLUSH_INTERVAL = 5 # seconds
|
|
@@ -47,6 +50,10 @@ module Flare
|
|
|
47
50
|
@mutex = Mutex.new
|
|
48
51
|
@cond = ConditionVariable.new
|
|
49
52
|
@stopped = false
|
|
53
|
+
@active_exports = 0
|
|
54
|
+
@export_completion_sequence = 0
|
|
55
|
+
@last_export_result = SUCCESS
|
|
56
|
+
@flush_owner = nil
|
|
50
57
|
@pid = $$
|
|
51
58
|
|
|
52
59
|
@dropped_count = Concurrent::AtomicFixnum.new(0)
|
|
@@ -78,19 +85,43 @@ module Flare
|
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def force_flush(timeout: nil)
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
detect_forking
|
|
89
|
+
deadline = Deadline.new(timeout)
|
|
90
|
+
return TIMEOUT unless begin_flush(deadline)
|
|
91
|
+
prior_result = @flush_prior_result
|
|
92
|
+
|
|
93
|
+
batch = snapshot_for_flush(deadline)
|
|
94
|
+
return TIMEOUT unless batch
|
|
95
|
+
|
|
96
|
+
operation = start_flush_export(batch, deadline)
|
|
97
|
+
result = wait_for_flush_export(operation, deadline)
|
|
98
|
+
[prior_result, result].max
|
|
99
|
+
ensure
|
|
100
|
+
finish_flush if @flush_owner == Thread.current
|
|
83
101
|
end
|
|
84
102
|
|
|
85
103
|
def shutdown(timeout: nil)
|
|
86
|
-
|
|
104
|
+
detect_forking
|
|
105
|
+
deadline = Deadline.new(timeout)
|
|
106
|
+
return TIMEOUT unless lock_before_deadline(deadline)
|
|
107
|
+
|
|
108
|
+
begin
|
|
87
109
|
@stopped = true
|
|
88
110
|
@cond.broadcast
|
|
111
|
+
ensure
|
|
112
|
+
@mutex.unlock
|
|
89
113
|
end
|
|
90
|
-
@worker.join(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
@worker.join(deadline.remaining || 5)
|
|
115
|
+
return TIMEOUT if @worker.alive? || deadline.expired?
|
|
116
|
+
|
|
117
|
+
result = force_flush(timeout: deadline.remaining)
|
|
118
|
+
return result unless result == SUCCESS
|
|
119
|
+
return TIMEOUT if deadline.expired?
|
|
120
|
+
|
|
121
|
+
exporter_result = @exporter.shutdown(timeout: deadline.remaining) if @exporter.respond_to?(:shutdown)
|
|
122
|
+
return TIMEOUT if deadline.expired?
|
|
123
|
+
|
|
124
|
+
exporter_result || SUCCESS
|
|
94
125
|
end
|
|
95
126
|
|
|
96
127
|
def buffer_size
|
|
@@ -124,7 +155,8 @@ module Flare
|
|
|
124
155
|
until stopped?
|
|
125
156
|
@mutex.synchronize do
|
|
126
157
|
timeout = next_wait_timeout
|
|
127
|
-
@
|
|
158
|
+
waiting_for_export = @flush_owner || @active_exports.positive?
|
|
159
|
+
@cond.wait(@mutex, timeout) if (@ready_queue.empty? || waiting_for_export) && !@stopped
|
|
128
160
|
end
|
|
129
161
|
drain_and_export
|
|
130
162
|
end
|
|
@@ -134,30 +166,141 @@ module Flare
|
|
|
134
166
|
@mutex.synchronize { @stopped }
|
|
135
167
|
end
|
|
136
168
|
|
|
137
|
-
def drain_and_export
|
|
169
|
+
def drain_and_export
|
|
138
170
|
batch = nil
|
|
139
171
|
@mutex.synchronize do
|
|
140
172
|
promote_due_delayed_traces
|
|
173
|
+
return if @ready_queue.empty? || @flush_owner || @active_exports.positive?
|
|
141
174
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
batch = @ready_queue
|
|
176
|
+
@ready_queue = []
|
|
177
|
+
@active_exports += 1
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result = export_batch(batch, timeout: @export_timeout)
|
|
181
|
+
ensure
|
|
182
|
+
export_finished(result || FAILURE) if batch
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def begin_flush(deadline)
|
|
186
|
+
return false unless lock_before_deadline(deadline)
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
initial_sequence = @export_completion_sequence
|
|
190
|
+
while @flush_owner && @flush_owner != Thread.current
|
|
191
|
+
return false if deadline.expired?
|
|
192
|
+
|
|
193
|
+
@cond.wait(@mutex, deadline.remaining)
|
|
149
194
|
end
|
|
195
|
+
@flush_owner = Thread.current
|
|
196
|
+
|
|
197
|
+
while @active_exports.positive?
|
|
198
|
+
return false if deadline.expired?
|
|
150
199
|
|
|
151
|
-
|
|
200
|
+
@cond.wait(@mutex, deadline.remaining)
|
|
201
|
+
end
|
|
202
|
+
@flush_prior_result = if @export_completion_sequence > initial_sequence
|
|
203
|
+
@last_export_result
|
|
204
|
+
else
|
|
205
|
+
SUCCESS
|
|
206
|
+
end
|
|
207
|
+
ensure
|
|
208
|
+
@mutex.unlock
|
|
209
|
+
end
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def finish_flush
|
|
214
|
+
@mutex.synchronize do
|
|
215
|
+
@flush_owner = nil
|
|
216
|
+
@cond.broadcast
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def snapshot_for_flush(deadline)
|
|
221
|
+
return unless lock_before_deadline(deadline)
|
|
222
|
+
|
|
223
|
+
begin
|
|
224
|
+
@ready_queue.concat(@pending_by_trace.values.flatten)
|
|
225
|
+
@pending_by_trace.clear
|
|
226
|
+
@trace_order.clear
|
|
227
|
+
@pending_count = 0
|
|
228
|
+
unmark_delayed_traces
|
|
229
|
+
@delayed_ready_by_trace.clear
|
|
152
230
|
batch = @ready_queue
|
|
153
231
|
@ready_queue = []
|
|
232
|
+
batch
|
|
233
|
+
ensure
|
|
234
|
+
@mutex.unlock
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def start_flush_export(batch, deadline)
|
|
239
|
+
operation = { done: false, result: nil }
|
|
240
|
+
@mutex.synchronize { @active_exports += 1 }
|
|
241
|
+
Thread.new do
|
|
242
|
+
result = batch.empty? ? SUCCESS : export_batch(batch, timeout: deadline.remaining)
|
|
243
|
+
if result == SUCCESS && !deadline.expired? && @exporter.respond_to?(:force_flush)
|
|
244
|
+
result = @exporter.force_flush(timeout: deadline.remaining)
|
|
245
|
+
end
|
|
246
|
+
result = TIMEOUT if deadline.expired?
|
|
247
|
+
operation[:result] = result
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
@exception_count.increment
|
|
250
|
+
@logger.warn("[Flare::FilteringSpanProcessor] force flush failed: #{e.class}: #{e.message}")
|
|
251
|
+
operation[:result] = FAILURE
|
|
252
|
+
ensure
|
|
253
|
+
@mutex.synchronize do
|
|
254
|
+
operation[:done] = true
|
|
255
|
+
complete_export(operation[:result])
|
|
256
|
+
end
|
|
154
257
|
end
|
|
258
|
+
operation
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def wait_for_flush_export(operation, deadline)
|
|
262
|
+
@mutex.synchronize do
|
|
263
|
+
until operation[:done]
|
|
264
|
+
return TIMEOUT if deadline.expired?
|
|
155
265
|
|
|
156
|
-
|
|
266
|
+
@cond.wait(@mutex, deadline.remaining)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
operation[:result]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def export_batch(batch, timeout:)
|
|
273
|
+
result = @exporter.export(batch, timeout: timeout)
|
|
157
274
|
@failed_export_count.increment if result != SUCCESS
|
|
275
|
+
result
|
|
158
276
|
rescue StandardError => e
|
|
159
277
|
@exception_count.increment
|
|
160
278
|
@logger.warn("[Flare::FilteringSpanProcessor] export failed: #{e.class}: #{e.message}")
|
|
279
|
+
FAILURE
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def export_finished(result)
|
|
283
|
+
@mutex.synchronize do
|
|
284
|
+
complete_export(result)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def complete_export(result)
|
|
289
|
+
@active_exports -= 1
|
|
290
|
+
@export_completion_sequence += 1
|
|
291
|
+
@last_export_result = result || FAILURE
|
|
292
|
+
@cond.broadcast
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def lock_before_deadline(deadline)
|
|
296
|
+
return @mutex.lock unless deadline.remaining
|
|
297
|
+
|
|
298
|
+
until @mutex.try_lock
|
|
299
|
+
return false if deadline.expired?
|
|
300
|
+
|
|
301
|
+
sleep([deadline.remaining, 0.001].min)
|
|
302
|
+
end
|
|
303
|
+
true
|
|
161
304
|
end
|
|
162
305
|
|
|
163
306
|
def mark_trace_ready(trace_id)
|
|
@@ -255,18 +398,25 @@ module Flare
|
|
|
255
398
|
def detect_forking
|
|
256
399
|
return if @pid == $$
|
|
257
400
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
401
|
+
# The child only retains the forking thread. Replace synchronization
|
|
402
|
+
# objects so it cannot inherit locks or active-export bookkeeping owned
|
|
403
|
+
# by vanished threads.
|
|
404
|
+
@pid = $$
|
|
405
|
+
@mutex = Mutex.new
|
|
406
|
+
@cond = ConditionVariable.new
|
|
407
|
+
@pending_by_trace = {}
|
|
408
|
+
@trace_order = []
|
|
409
|
+
@ready_queue = []
|
|
410
|
+
@delayed_ready_by_trace = {}
|
|
411
|
+
@pending_count = 0
|
|
412
|
+
@active_exports = 0
|
|
413
|
+
@export_completion_sequence = 0
|
|
414
|
+
@last_export_result = SUCCESS
|
|
415
|
+
@flush_prior_result = SUCCESS
|
|
416
|
+
@flush_owner = nil
|
|
417
|
+
@stopped = false
|
|
418
|
+
@worker = nil
|
|
419
|
+
start_worker
|
|
270
420
|
end
|
|
271
421
|
|
|
272
422
|
def start_worker
|
data/lib/flare/http_transport.rb
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "uri"
|
|
5
5
|
|
|
6
|
+
require_relative "deadline"
|
|
7
|
+
|
|
6
8
|
module Flare
|
|
7
9
|
# Tiny HTTP wrapper used by TraceExporter (and anything else that wants
|
|
8
10
|
# to PUT/POST without pulling in a heavy client). Designed for injection
|
|
9
11
|
# at the boundary so tests can swap in a recording fake; no other moving
|
|
10
12
|
# parts.
|
|
11
13
|
class HttpTransport
|
|
14
|
+
DeadlineExceeded = Class.new(StandardError)
|
|
12
15
|
DEFAULT_OPEN_TIMEOUT = 2
|
|
13
16
|
DEFAULT_READ_TIMEOUT = 5
|
|
14
17
|
DEFAULT_WRITE_TIMEOUT = 5
|
|
@@ -28,35 +31,46 @@ module Flare
|
|
|
28
31
|
@write_timeout = write_timeout
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
def get(url, headers = {})
|
|
32
|
-
request(url, nil, headers, Net::HTTP::Get)
|
|
34
|
+
def get(url, headers = {}, timeout: nil)
|
|
35
|
+
request(url, nil, headers, Net::HTTP::Get, timeout: timeout)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def put(url, body, headers = {})
|
|
36
|
-
request(url, body, headers, Net::HTTP::Put)
|
|
38
|
+
def put(url, body, headers = {}, timeout: nil)
|
|
39
|
+
request(url, body, headers, Net::HTTP::Put, timeout: timeout)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
def post(url, body, headers = {})
|
|
40
|
-
request(url, body, headers, Net::HTTP::Post)
|
|
42
|
+
def post(url, body, headers = {}, timeout: nil)
|
|
43
|
+
request(url, body, headers, Net::HTTP::Post, timeout: timeout)
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
private
|
|
44
47
|
|
|
45
|
-
def request(url, body, headers, klass)
|
|
48
|
+
def request(url, body, headers, klass, timeout: nil)
|
|
49
|
+
deadline = Deadline.new(timeout)
|
|
50
|
+
raise DeadlineExceeded if deadline.expired?
|
|
51
|
+
|
|
46
52
|
uri = URI(url)
|
|
47
53
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
48
54
|
http.use_ssl = uri.scheme == "https"
|
|
49
|
-
http.open_timeout = @open_timeout
|
|
50
|
-
http.read_timeout = @read_timeout
|
|
51
|
-
http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
|
|
55
|
+
http.open_timeout = effective_timeout(@open_timeout, deadline.remaining)
|
|
56
|
+
http.read_timeout = effective_timeout(@read_timeout, deadline.remaining)
|
|
57
|
+
http.write_timeout = effective_timeout(@write_timeout, deadline.remaining) if http.respond_to?(:write_timeout=)
|
|
52
58
|
|
|
53
59
|
req = klass.new(uri.request_uri == "" ? "/" : uri.request_uri)
|
|
54
60
|
headers.each { |k, v| req[k] = v }
|
|
55
61
|
req.body = body if body
|
|
56
62
|
|
|
57
63
|
response = http.request(req)
|
|
64
|
+
raise DeadlineExceeded if deadline.expired?
|
|
65
|
+
|
|
58
66
|
hash = response.each_header.to_h
|
|
59
67
|
Response.new(code: response.code.to_s, body: response.body, headers: hash)
|
|
60
68
|
end
|
|
69
|
+
|
|
70
|
+
def effective_timeout(configured_timeout, remaining)
|
|
71
|
+
return configured_timeout unless remaining
|
|
72
|
+
|
|
73
|
+
[configured_timeout, remaining].min
|
|
74
|
+
end
|
|
61
75
|
end
|
|
62
76
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry/sdk"
|
|
4
|
+
|
|
5
|
+
require_relative "deadline"
|
|
6
|
+
|
|
7
|
+
module Flare
|
|
8
|
+
module Lifecycle
|
|
9
|
+
# Flush all trace processors and the separate aggregated metric pipeline
|
|
10
|
+
# using one monotonic timeout budget. This is safe to call from lifecycle
|
|
11
|
+
# hooks for short-lived and fork-per-job workers.
|
|
12
|
+
def force_flush(timeout: nil)
|
|
13
|
+
deadline = Deadline.new(timeout)
|
|
14
|
+
results = []
|
|
15
|
+
|
|
16
|
+
results << tracer_provider_for_flush.force_flush(timeout: deadline.remaining)
|
|
17
|
+
return OpenTelemetry::SDK::Trace::Export::TIMEOUT if deadline.expired?
|
|
18
|
+
|
|
19
|
+
flusher = metric_flusher_for_flush
|
|
20
|
+
results << flusher.force_flush(timeout: deadline.remaining) if flusher
|
|
21
|
+
return OpenTelemetry::SDK::Trace::Export::TIMEOUT if deadline.expired?
|
|
22
|
+
|
|
23
|
+
results.max || OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
24
|
+
rescue => e
|
|
25
|
+
warn "[Flare] Telemetry flush error: #{e.message}"
|
|
26
|
+
OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tracer_provider_for_flush
|
|
30
|
+
OpenTelemetry.tracer_provider
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def metric_flusher_for_flush
|
|
34
|
+
@metric_flusher
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
extend Lifecycle
|
|
39
|
+
end
|