flare 0.1.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 +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/app/controllers/flare/application_controller.rb +22 -0
- data/app/controllers/flare/jobs_controller.rb +55 -0
- data/app/controllers/flare/requests_controller.rb +73 -0
- data/app/controllers/flare/spans_controller.rb +101 -0
- data/app/helpers/flare/application_helper.rb +168 -0
- data/app/views/flare/jobs/index.html.erb +69 -0
- data/app/views/flare/jobs/show.html.erb +323 -0
- data/app/views/flare/requests/index.html.erb +120 -0
- data/app/views/flare/requests/show.html.erb +498 -0
- data/app/views/flare/spans/index.html.erb +112 -0
- data/app/views/flare/spans/show.html.erb +184 -0
- data/app/views/layouts/flare/application.html.erb +126 -0
- data/config/routes.rb +20 -0
- data/exe/flare +9 -0
- data/lib/flare/backoff_policy.rb +73 -0
- data/lib/flare/cli/doctor_command.rb +129 -0
- data/lib/flare/cli/output.rb +45 -0
- data/lib/flare/cli/setup_command.rb +404 -0
- data/lib/flare/cli/status_command.rb +47 -0
- data/lib/flare/cli.rb +50 -0
- data/lib/flare/configuration.rb +121 -0
- data/lib/flare/engine.rb +43 -0
- data/lib/flare/http_metrics_config.rb +101 -0
- data/lib/flare/metric_counter.rb +45 -0
- data/lib/flare/metric_flusher.rb +124 -0
- data/lib/flare/metric_key.rb +42 -0
- data/lib/flare/metric_span_processor.rb +470 -0
- data/lib/flare/metric_storage.rb +42 -0
- data/lib/flare/metric_submitter.rb +221 -0
- data/lib/flare/source_location.rb +113 -0
- data/lib/flare/sqlite_exporter.rb +279 -0
- data/lib/flare/storage/sqlite.rb +789 -0
- data/lib/flare/storage.rb +54 -0
- data/lib/flare/version.rb +5 -0
- data/lib/flare.rb +411 -0
- data/public/flare-assets/flare.css +1245 -0
- data/public/flare-assets/images/flipper.png +0 -0
- metadata +240 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
class HttpMetricsConfig
|
|
5
|
+
class HostConfig
|
|
6
|
+
attr_reader :rules
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@rules = []
|
|
10
|
+
@all = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize_copy(source)
|
|
14
|
+
@rules = source.rules.dup
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def all?
|
|
18
|
+
@all
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Track all paths for this host (normalized via normalize_path)
|
|
22
|
+
def all
|
|
23
|
+
@all = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Track paths matching this regex (normalized via normalize_path)
|
|
27
|
+
def allow(pattern)
|
|
28
|
+
@rules << {pattern: pattern, replacement: nil}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Track paths matching this regex with a custom replacement string
|
|
32
|
+
def map(pattern, replacement)
|
|
33
|
+
@rules << {pattern: pattern, replacement: replacement}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the resolved path for a given raw path, or "*" if no match.
|
|
37
|
+
# If :all, returns nil to signal "use normalize_path".
|
|
38
|
+
# If a rule matches with a replacement, returns the replacement.
|
|
39
|
+
# If a rule matches without a replacement, returns nil to signal "use normalize_path".
|
|
40
|
+
# If no rules match, returns "*".
|
|
41
|
+
def resolve(path)
|
|
42
|
+
return nil if @all
|
|
43
|
+
|
|
44
|
+
@rules.each do |rule|
|
|
45
|
+
if rule[:pattern].match?(path)
|
|
46
|
+
return rule[:replacement] # nil means use normalize_path
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
"*"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@hosts = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize_copy(source)
|
|
59
|
+
@hosts = source.instance_variable_get(:@hosts).transform_values(&:dup)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def host(hostname, mode = nil, &block)
|
|
63
|
+
config = @hosts[hostname] ||= HostConfig.new
|
|
64
|
+
|
|
65
|
+
if mode == :all
|
|
66
|
+
config.all
|
|
67
|
+
elsif block
|
|
68
|
+
yield config
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Resolve a host+path to the target path for metrics.
|
|
73
|
+
# Returns "*" for unknown hosts or unmatched paths.
|
|
74
|
+
# Returns nil to signal "use normalize_path".
|
|
75
|
+
# Returns a string for custom replacements.
|
|
76
|
+
def resolve(hostname, path)
|
|
77
|
+
host_config = @hosts[hostname]
|
|
78
|
+
return "*" unless host_config
|
|
79
|
+
|
|
80
|
+
host_config.resolve(path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
DEFAULT = new.tap do |config|
|
|
84
|
+
config.host "flare.am" do |h|
|
|
85
|
+
h.allow %r{/api/metrics}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
config.host "www.flippercloud.io" do |h|
|
|
89
|
+
h.map %r{/adapter/features/[^/]+/(boolean|actors|groups|percentage_of_actors|percentage_of_time|expression|clear)}, "/adapter/features/:name/:gate"
|
|
90
|
+
h.map %r{/adapter/features/[^/]+}, "/adapter/features/:name"
|
|
91
|
+
h.map %r{/adapter/actors/[^/]+}, "/adapter/actors/:id"
|
|
92
|
+
h.allow %r{/adapter/features}
|
|
93
|
+
h.allow %r{/adapter/import}
|
|
94
|
+
h.allow %r{/adapter/telemetry/summary}
|
|
95
|
+
h.allow %r{/adapter/telemetry}
|
|
96
|
+
h.allow %r{/adapter/events}
|
|
97
|
+
h.allow %r{/adapter/audits}
|
|
98
|
+
end
|
|
99
|
+
end.freeze
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/atomic/atomic_fixnum"
|
|
4
|
+
|
|
5
|
+
module Flare
|
|
6
|
+
# Thread-safe counter for metric aggregation.
|
|
7
|
+
# Uses atomic operations for lock-free increments.
|
|
8
|
+
#
|
|
9
|
+
# Note: Durations are stored as integer milliseconds. Sub-millisecond
|
|
10
|
+
# durations are truncated to 0. For very fast operations (e.g., cache hits),
|
|
11
|
+
# the sum_ms may undercount actual time spent.
|
|
12
|
+
class MetricCounter
|
|
13
|
+
def initialize
|
|
14
|
+
@count = Concurrent::AtomicFixnum.new(0)
|
|
15
|
+
@sum_ms = Concurrent::AtomicFixnum.new(0)
|
|
16
|
+
@error_count = Concurrent::AtomicFixnum.new(0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def increment(duration_ms:, error: false)
|
|
20
|
+
@count.increment
|
|
21
|
+
@sum_ms.increment(duration_ms.to_i)
|
|
22
|
+
@error_count.increment if error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def count
|
|
26
|
+
@count.value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sum_ms
|
|
30
|
+
@sum_ms.value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def error_count
|
|
34
|
+
@error_count.value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
count: @count.value,
|
|
40
|
+
sum_ms: @sum_ms.value,
|
|
41
|
+
error_count: @error_count.value
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/timer_task"
|
|
4
|
+
require "concurrent/executor/fixed_thread_pool"
|
|
5
|
+
|
|
6
|
+
module Flare
|
|
7
|
+
# Background threads that periodically drain in-memory metrics and submit
|
|
8
|
+
# them via HTTP. Uses concurrent-ruby TimerTask + FixedThreadPool, matching
|
|
9
|
+
# the pattern in Flipper's telemetry.
|
|
10
|
+
#
|
|
11
|
+
# Fork-safe: detects forked processes and restarts automatically.
|
|
12
|
+
class MetricFlusher
|
|
13
|
+
DEFAULT_INTERVAL = 60 # seconds
|
|
14
|
+
DEFAULT_SHUTDOWN_TIMEOUT = 5 # seconds
|
|
15
|
+
|
|
16
|
+
attr_reader :interval, :shutdown_timeout
|
|
17
|
+
|
|
18
|
+
def initialize(storage:, submitter:, interval: DEFAULT_INTERVAL, shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT)
|
|
19
|
+
@storage = storage
|
|
20
|
+
@submitter = submitter
|
|
21
|
+
@interval = interval
|
|
22
|
+
@shutdown_timeout = shutdown_timeout
|
|
23
|
+
@pid = $$
|
|
24
|
+
@stopped = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
@stopped = false
|
|
29
|
+
|
|
30
|
+
@pool = Concurrent::FixedThreadPool.new(1, {
|
|
31
|
+
max_queue: 20,
|
|
32
|
+
fallback_policy: :discard,
|
|
33
|
+
name: "flare-metrics-submit-pool".freeze,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
@timer = Concurrent::TimerTask.execute({
|
|
37
|
+
execution_interval: @interval,
|
|
38
|
+
name: "flare-metrics-drain-timer".freeze,
|
|
39
|
+
}) { post_to_pool }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop
|
|
43
|
+
return if @stopped
|
|
44
|
+
|
|
45
|
+
@stopped = true
|
|
46
|
+
|
|
47
|
+
Flare.log "Shutting down metrics flusher, draining remaining metrics..."
|
|
48
|
+
|
|
49
|
+
if @timer
|
|
50
|
+
@timer.shutdown
|
|
51
|
+
@timer.wait_for_termination(1)
|
|
52
|
+
@timer.kill unless @timer.shutdown?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if @pool
|
|
56
|
+
post_to_pool # one last drain
|
|
57
|
+
@pool.shutdown
|
|
58
|
+
pool_terminated = @pool.wait_for_termination(@shutdown_timeout)
|
|
59
|
+
@pool.kill unless pool_terminated
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Flare.log "Metrics flusher stopped"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def restart
|
|
66
|
+
@stopped = false
|
|
67
|
+
stop
|
|
68
|
+
start
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Manually trigger a flush (useful for testing or forced flushes).
|
|
72
|
+
def flush_now
|
|
73
|
+
return 0 unless @storage && @submitter
|
|
74
|
+
|
|
75
|
+
drained = @storage.drain
|
|
76
|
+
return 0 if drained.empty?
|
|
77
|
+
|
|
78
|
+
count, error = @submitter.submit(drained)
|
|
79
|
+
if error
|
|
80
|
+
warn "[Flare] Metric submission error: #{error.message}"
|
|
81
|
+
end
|
|
82
|
+
count
|
|
83
|
+
rescue => e
|
|
84
|
+
warn "[Flare] Metric flush error: #{e.message}"
|
|
85
|
+
0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def running?
|
|
89
|
+
@timer&.running? || false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Re-initialize after fork. Called automatically by MetricSpanProcessor
|
|
93
|
+
# on first span in the new process, or manually from Puma/Unicorn
|
|
94
|
+
# after_fork hooks.
|
|
95
|
+
def after_fork
|
|
96
|
+
@pid = $$
|
|
97
|
+
restart
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def post_to_pool
|
|
103
|
+
drained = @storage.drain
|
|
104
|
+
if drained.empty?
|
|
105
|
+
Flare.log "No metrics to flush"
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Flare.log "Drained #{drained.size} metric keys for submission"
|
|
110
|
+
@pool.post { submit_to_cloud(drained) }
|
|
111
|
+
rescue => e
|
|
112
|
+
warn "[Flare] Metric drain error: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def submit_to_cloud(drained)
|
|
116
|
+
_response, error = @submitter.submit(drained)
|
|
117
|
+
if error
|
|
118
|
+
warn "[Flare] Metric submission error: #{error.message}"
|
|
119
|
+
end
|
|
120
|
+
rescue => e
|
|
121
|
+
warn "[Flare] Metric submission error: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
# Identifies a unique metric for aggregation.
|
|
5
|
+
# Immutable and hashable for use as Concurrent::Map keys.
|
|
6
|
+
class MetricKey
|
|
7
|
+
attr_reader :bucket, :namespace, :service, :target, :operation
|
|
8
|
+
|
|
9
|
+
def initialize(bucket:, namespace:, service:, target:, operation:)
|
|
10
|
+
@bucket = bucket
|
|
11
|
+
@namespace = namespace.to_s.freeze
|
|
12
|
+
@service = service.to_s.freeze
|
|
13
|
+
@target = target&.to_s&.freeze
|
|
14
|
+
@operation = operation.to_s.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def eql?(other)
|
|
19
|
+
self.class.eql?(other.class) &&
|
|
20
|
+
bucket == other.bucket &&
|
|
21
|
+
namespace == other.namespace &&
|
|
22
|
+
service == other.service &&
|
|
23
|
+
target == other.target &&
|
|
24
|
+
operation == other.operation
|
|
25
|
+
end
|
|
26
|
+
alias == eql?
|
|
27
|
+
|
|
28
|
+
def hash
|
|
29
|
+
[self.class, bucket, namespace, service, target, operation].hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
bucket: bucket,
|
|
35
|
+
namespace: namespace,
|
|
36
|
+
service: service,
|
|
37
|
+
target: target,
|
|
38
|
+
operation: operation
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|