flare 0.4.0 → 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.
Potentially problematic release.
This version of flare might be problematic. Click here for more details.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/flare.gemspec +66 -0
- data/lib/flare/active_record.rb +102 -0
- data/lib/flare/collection.rb +47 -0
- data/lib/flare/configuration.rb +64 -123
- data/lib/flare/index_builder.rb +24 -0
- data/lib/flare/session.rb +142 -0
- data/lib/flare/tasks.rb +18 -0
- data/lib/flare.rb +19 -537
- data/test/helper.rb +10 -0
- data/test/test_flare.rb +7 -0
- metadata +89 -242
- checksums.yaml +0 -7
- data/CHANGELOG.md +0 -3
- data/LICENSE.txt +0 -21
- data/README.md +0 -148
- data/app/controllers/flare/application_controller.rb +0 -30
- data/app/controllers/flare/jobs_controller.rb +0 -55
- data/app/controllers/flare/requests_controller.rb +0 -73
- data/app/controllers/flare/spans_controller.rb +0 -101
- data/app/helpers/flare/application_helper.rb +0 -168
- data/app/views/flare/jobs/index.html.erb +0 -69
- data/app/views/flare/jobs/show.html.erb +0 -323
- data/app/views/flare/requests/index.html.erb +0 -120
- data/app/views/flare/requests/show.html.erb +0 -498
- data/app/views/flare/spans/index.html.erb +0 -112
- data/app/views/flare/spans/show.html.erb +0 -184
- data/app/views/layouts/flare/application.html.erb +0 -126
- data/config/routes.rb +0 -20
- data/exe/flare +0 -9
- data/lib/flare/backoff_policy.rb +0 -73
- data/lib/flare/cli/doctor_command.rb +0 -129
- data/lib/flare/cli/output.rb +0 -45
- data/lib/flare/cli/setup_command.rb +0 -404
- data/lib/flare/cli/status_command.rb +0 -47
- data/lib/flare/cli.rb +0 -50
- data/lib/flare/client_headers.rb +0 -39
- data/lib/flare/deadline.rb +0 -27
- data/lib/flare/engine.rb +0 -45
- data/lib/flare/filtering_span_processor.rb +0 -429
- data/lib/flare/http_metrics_config.rb +0 -101
- data/lib/flare/http_transport.rb +0 -76
- data/lib/flare/lifecycle.rb +0 -39
- data/lib/flare/marker.rb +0 -106
- data/lib/flare/metric_counter.rb +0 -51
- data/lib/flare/metric_flusher.rb +0 -279
- data/lib/flare/metric_key.rb +0 -42
- data/lib/flare/metric_span_processor.rb +0 -470
- data/lib/flare/metric_storage.rb +0 -67
- data/lib/flare/metric_submitter.rb +0 -239
- data/lib/flare/recording_batch_span_processor.rb +0 -238
- data/lib/flare/rule_manager.rb +0 -141
- data/lib/flare/sampler.rb +0 -130
- data/lib/flare/source_location.rb +0 -113
- data/lib/flare/sqlite_exporter.rb +0 -334
- data/lib/flare/storage/sqlite.rb +0 -794
- data/lib/flare/storage.rb +0 -54
- data/lib/flare/trace_blob.rb +0 -116
- data/lib/flare/trace_exporter.rb +0 -182
- data/lib/flare/trace_health_reporter.rb +0 -74
- data/lib/flare/upload_url_pool.rb +0 -108
- data/lib/flare/version.rb +0 -5
- data/lib/flare/web_marker_subscriber.rb +0 -76
- data/public/flare-assets/flare.css +0 -1245
- data/public/flare-assets/images/flipper.png +0 -0
data/lib/flare/storage.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flare
|
|
4
|
-
module Storage
|
|
5
|
-
class Base
|
|
6
|
-
def save_case(attributes)
|
|
7
|
-
raise NotImplementedError
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def save_clues(clues)
|
|
11
|
-
raise NotImplementedError
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def find_case(uuid)
|
|
15
|
-
raise NotImplementedError
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def list_cases(type: nil, status: nil, method: nil, name: nil, origin: nil, limit: 50, offset: 0)
|
|
19
|
-
raise NotImplementedError
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def clues_for_case(case_uuid)
|
|
23
|
-
raise NotImplementedError
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def list_clues(type: nil, search: nil, limit: 50, offset: 0)
|
|
27
|
-
raise NotImplementedError
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def find_clue(id)
|
|
31
|
-
raise NotImplementedError
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def prune(retention_hours:, max_cases:)
|
|
35
|
-
raise NotImplementedError
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def clear_all
|
|
39
|
-
raise NotImplementedError
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def count_cases(type: nil, status: nil, method: nil, name: nil, origin: nil)
|
|
43
|
-
raise NotImplementedError
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def count_clues(type: nil, search: nil)
|
|
47
|
-
raise NotImplementedError
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# storage/sqlite is loaded on demand when spans are enabled
|
|
54
|
-
# to avoid requiring sqlite3 in production environments
|
data/lib/flare/trace_blob.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Flare
|
|
6
|
-
# Value object that turns a group of OTel span_data for a single trace
|
|
7
|
-
# into the Flare-JSON wire format the server expects:
|
|
8
|
-
#
|
|
9
|
-
# {
|
|
10
|
-
# "trace_id": "<hex>",
|
|
11
|
-
# "trace_rule_id": <int|nil>,
|
|
12
|
-
# "root_name": "<string>",
|
|
13
|
-
# "started_at": "<iso8601>",
|
|
14
|
-
# "duration_ms": <int>,
|
|
15
|
-
# "spans": [
|
|
16
|
-
# { "id", "parent_id", "name", "started_at",
|
|
17
|
-
# "duration_ms", "attributes" }
|
|
18
|
-
# ]
|
|
19
|
-
# }
|
|
20
|
-
#
|
|
21
|
-
# The trace_rule_id is read from any span carrying the
|
|
22
|
-
# `flare.rule_id` attribute (Path 1 sets it on the sampled root, Path 2
|
|
23
|
-
# sets it on the rack owner span via WebMarkerSubscriber).
|
|
24
|
-
class TraceBlob
|
|
25
|
-
ZERO_SPAN_ID = ("\x00".b * 8).freeze
|
|
26
|
-
ROOT_NAME_LIMIT = 255
|
|
27
|
-
|
|
28
|
-
def self.build(trace_id:, spans:)
|
|
29
|
-
return nil if spans.nil? || spans.empty?
|
|
30
|
-
new(trace_id: trace_id, spans: spans)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def initialize(trace_id:, spans:)
|
|
34
|
-
@trace_id = trace_id
|
|
35
|
-
@spans = spans
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def to_h
|
|
39
|
-
root = find_root
|
|
40
|
-
{
|
|
41
|
-
"trace_id" => hexify(@trace_id),
|
|
42
|
-
"trace_rule_id" => rule_id_from_spans,
|
|
43
|
-
"root_name" => root_name(root),
|
|
44
|
-
"started_at" => iso(root&.start_timestamp),
|
|
45
|
-
"duration_ms" => duration_ms(root),
|
|
46
|
-
"spans" => @spans.map { |s| span_to_h(s) }
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def find_root
|
|
53
|
-
@spans.find { |s| entry_span?(s) && rule_id_attribute(s) } ||
|
|
54
|
-
@spans.find { |s| root?(s) } ||
|
|
55
|
-
@spans.find { |s| entry_span?(s) } ||
|
|
56
|
-
@spans.find { |s| rule_id_attribute(s) } ||
|
|
57
|
-
@spans.first
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def root?(span)
|
|
61
|
-
pid = span.parent_span_id
|
|
62
|
-
pid.nil? || pid.empty? || pid == ZERO_SPAN_ID
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def entry_span?(span)
|
|
66
|
-
return false unless span.respond_to?(:kind)
|
|
67
|
-
|
|
68
|
-
span.kind == :server || span.kind == :consumer
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def rule_id_from_spans
|
|
72
|
-
@spans.each do |s|
|
|
73
|
-
value = rule_id_attribute(s)
|
|
74
|
-
return value if value
|
|
75
|
-
end
|
|
76
|
-
nil
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def rule_id_attribute(span)
|
|
80
|
-
attrs = span.attributes
|
|
81
|
-
return nil unless attrs
|
|
82
|
-
|
|
83
|
-
attrs[Sampler::RULE_ID_ATTRIBUTE] || attrs[Sampler::RULE_ID_ATTRIBUTE.to_sym]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def root_name(root)
|
|
87
|
-
root&.name&.to_s&.slice(0, ROOT_NAME_LIMIT)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def span_to_h(span)
|
|
91
|
-
{
|
|
92
|
-
"id" => hexify(span.span_id),
|
|
93
|
-
"parent_id" => root?(span) ? nil : hexify(span.parent_span_id),
|
|
94
|
-
"name" => span.name,
|
|
95
|
-
"started_at" => iso(span.start_timestamp),
|
|
96
|
-
"duration_ms" => duration_ms(span),
|
|
97
|
-
"attributes" => span.attributes || {}
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def hexify(bytes)
|
|
102
|
-
return nil if bytes.nil?
|
|
103
|
-
bytes.unpack1("H*")
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def iso(nanos)
|
|
107
|
-
return nil if nanos.nil?
|
|
108
|
-
Time.at(nanos / 1_000_000_000.0).utc.iso8601(6)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def duration_ms(span)
|
|
112
|
-
return nil unless span && span.start_timestamp && span.end_timestamp
|
|
113
|
-
((span.end_timestamp - span.start_timestamp) / 1_000_000).to_i
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
data/lib/flare/trace_exporter.rb
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "zlib"
|
|
5
|
-
require "stringio"
|
|
6
|
-
require "logger"
|
|
7
|
-
require "uri"
|
|
8
|
-
require "concurrent/atomic/atomic_fixnum"
|
|
9
|
-
require "opentelemetry/sdk"
|
|
10
|
-
|
|
11
|
-
require_relative "client_headers"
|
|
12
|
-
require_relative "sampler"
|
|
13
|
-
require_relative "trace_blob"
|
|
14
|
-
require_relative "http_transport"
|
|
15
|
-
require_relative "deadline"
|
|
16
|
-
|
|
17
|
-
module Flare
|
|
18
|
-
# Custom OTel exporter. For each batch FilteringSpanProcessor hands over:
|
|
19
|
-
#
|
|
20
|
-
# 1. Group spans by trace_id.
|
|
21
|
-
# 2. For each trace, build a Flare::TraceBlob and gzip-JSON-encode it.
|
|
22
|
-
# 3. Check out a presigned R2 PUT URL from UploadUrlPool.
|
|
23
|
-
# 4. PUT the gzipped body straight to R2 -- Flare's server is NOT in
|
|
24
|
-
# the trace-bytes path.
|
|
25
|
-
# 5. After R2 returns 200, POST /api/traces { key } using the
|
|
26
|
-
# customer's push token + Flare-Project / Flare-Environment headers.
|
|
27
|
-
# That's the self-notify hop the design swapped in for the CF Worker.
|
|
28
|
-
#
|
|
29
|
-
# 403 from R2 means the presigned URL expired between issue and use;
|
|
30
|
-
# discard, check out the next URL, retry once. Pool empty -> FAILURE.
|
|
31
|
-
# Notify-POST failure is logged + counted but doesn't fail the export
|
|
32
|
-
# (the blob is in R2, just won't be processed; incoming/* lifecycle
|
|
33
|
-
# cleans it up in 1hr).
|
|
34
|
-
class TraceExporter
|
|
35
|
-
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
36
|
-
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
37
|
-
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
38
|
-
|
|
39
|
-
PUT_HEADERS = {
|
|
40
|
-
"Content-Type" => "application/json",
|
|
41
|
-
"Content-Encoding" => "gzip"
|
|
42
|
-
}.freeze
|
|
43
|
-
|
|
44
|
-
attr_reader :put_failure_count, :notify_failure_count, :pool_empty_count, :exception_count
|
|
45
|
-
|
|
46
|
-
def initialize(pool:, notify_url:, api_key:, project:, environment:,
|
|
47
|
-
transport: nil, logger: nil)
|
|
48
|
-
@pool = pool
|
|
49
|
-
@notify_url = notify_url.to_s
|
|
50
|
-
@api_key = api_key
|
|
51
|
-
@project = project
|
|
52
|
-
@environment = environment
|
|
53
|
-
@transport = transport || HttpTransport.new
|
|
54
|
-
@logger = logger || Logger.new($stderr, level: Logger::WARN)
|
|
55
|
-
|
|
56
|
-
@put_failure_count = Concurrent::AtomicFixnum.new(0)
|
|
57
|
-
@notify_failure_count = Concurrent::AtomicFixnum.new(0)
|
|
58
|
-
@pool_empty_count = Concurrent::AtomicFixnum.new(0)
|
|
59
|
-
@exception_count = Concurrent::AtomicFixnum.new(0)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def export(spans, timeout: nil)
|
|
63
|
-
deadline = Deadline.new(timeout)
|
|
64
|
-
grouped = spans.group_by(&:trace_id)
|
|
65
|
-
return SUCCESS if grouped.empty?
|
|
66
|
-
|
|
67
|
-
overall = SUCCESS
|
|
68
|
-
grouped.each do |trace_id, group|
|
|
69
|
-
return TIMEOUT if deadline.expired?
|
|
70
|
-
|
|
71
|
-
result = ship(TraceBlob.build(trace_id: trace_id, spans: group), deadline: deadline)
|
|
72
|
-
return TIMEOUT if result == TIMEOUT || deadline.expired?
|
|
73
|
-
overall = FAILURE if result == FAILURE
|
|
74
|
-
end
|
|
75
|
-
overall
|
|
76
|
-
rescue StandardError => e
|
|
77
|
-
return TIMEOUT if deadline&.expired? || e.is_a?(HttpTransport::DeadlineExceeded)
|
|
78
|
-
|
|
79
|
-
@exception_count.increment
|
|
80
|
-
@logger.warn("[Flare::TraceExporter] export raised: #{e.class}: #{e.message}")
|
|
81
|
-
FAILURE
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def force_flush(timeout: nil)
|
|
85
|
-
SUCCESS
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def shutdown(timeout: nil)
|
|
89
|
-
SUCCESS
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
def ship(blob, deadline:, retried: false)
|
|
95
|
-
return FAILURE if blob.nil?
|
|
96
|
-
return TIMEOUT if deadline.expired?
|
|
97
|
-
|
|
98
|
-
entry = @pool.checkout
|
|
99
|
-
if entry.nil?
|
|
100
|
-
@pool_empty_count.increment
|
|
101
|
-
return FAILURE
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
body = gzip(JSON.generate(blob.to_h))
|
|
105
|
-
response = transport_request(:put, entry[:put_url], body, PUT_HEADERS, timeout: deadline.remaining)
|
|
106
|
-
return TIMEOUT if deadline.expired?
|
|
107
|
-
|
|
108
|
-
case response.code
|
|
109
|
-
when "200", "204"
|
|
110
|
-
notify(entry[:key], deadline: deadline)
|
|
111
|
-
when "403"
|
|
112
|
-
# Presigned URL probably expired; try once more with the next one.
|
|
113
|
-
retried ? record_put_failure(response) : ship(blob, deadline: deadline, retried: true)
|
|
114
|
-
else
|
|
115
|
-
record_put_failure(response)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def notify(key, deadline:)
|
|
120
|
-
return TIMEOUT if deadline.expired?
|
|
121
|
-
|
|
122
|
-
response = transport_request(
|
|
123
|
-
:post,
|
|
124
|
-
@notify_url,
|
|
125
|
-
JSON.generate(key: key),
|
|
126
|
-
notify_headers,
|
|
127
|
-
timeout: deadline.remaining
|
|
128
|
-
)
|
|
129
|
-
return TIMEOUT if deadline.expired?
|
|
130
|
-
return SUCCESS if response.code == "202"
|
|
131
|
-
|
|
132
|
-
@notify_failure_count.increment
|
|
133
|
-
@logger.warn("[Flare::TraceExporter] notify failed: HTTP #{response.code}")
|
|
134
|
-
SUCCESS
|
|
135
|
-
rescue StandardError => e
|
|
136
|
-
return TIMEOUT if deadline.expired? || e.is_a?(HttpTransport::DeadlineExceeded)
|
|
137
|
-
|
|
138
|
-
@notify_failure_count.increment
|
|
139
|
-
@logger.warn("[Flare::TraceExporter] notify exception: #{e.class}: #{e.message}")
|
|
140
|
-
SUCCESS
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def transport_request(method, *args, timeout:)
|
|
144
|
-
parameters = @transport.method(method).parameters
|
|
145
|
-
accepts_timeout = parameters.any? do |type, name|
|
|
146
|
-
type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
if accepts_timeout
|
|
150
|
-
@transport.public_send(method, *args, timeout: timeout)
|
|
151
|
-
else
|
|
152
|
-
@transport.public_send(method, *args)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Identifies the client on the Flare-API notify POST. The presigned R2
|
|
157
|
-
# PUT in #ship deliberately uses PUT_HEADERS only -- adding these there
|
|
158
|
-
# could invalidate the signed-header set.
|
|
159
|
-
def notify_headers
|
|
160
|
-
ClientHeaders.to_h.merge(
|
|
161
|
-
"Content-Type" => "application/json",
|
|
162
|
-
"Authorization" => "Bearer #{@api_key}",
|
|
163
|
-
"Flare-Project" => @project,
|
|
164
|
-
"Flare-Environment" => @environment
|
|
165
|
-
)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def record_put_failure(response)
|
|
169
|
-
@put_failure_count.increment
|
|
170
|
-
@logger.warn("[Flare::TraceExporter] PUT failed: HTTP #{response.code}")
|
|
171
|
-
FAILURE
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def gzip(body)
|
|
175
|
-
io = StringIO.new
|
|
176
|
-
gz = Zlib::GzipWriter.new(io)
|
|
177
|
-
gz.write(body)
|
|
178
|
-
gz.close
|
|
179
|
-
io.string
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "metric_key"
|
|
4
|
-
|
|
5
|
-
module Flare
|
|
6
|
-
# Records client-side tracing health into MetricStorage so flare-web can
|
|
7
|
-
# warn when local buffering, URL exhaustion, or export errors reduce trace
|
|
8
|
-
# fidelity.
|
|
9
|
-
class TraceHealthReporter
|
|
10
|
-
NAMESPACE = "sdk"
|
|
11
|
-
SERVICE = "flare-ruby"
|
|
12
|
-
TARGET = "tracing"
|
|
13
|
-
|
|
14
|
-
def initialize(processor:, pool:, exporter:)
|
|
15
|
-
@processor = processor
|
|
16
|
-
@pool = pool
|
|
17
|
-
@exporter = exporter
|
|
18
|
-
@last = {}
|
|
19
|
-
@mutex = Mutex.new
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def record(storage, bucket: Time.now.utc)
|
|
23
|
-
@mutex.synchronize do
|
|
24
|
-
record_counter(storage, bucket, "dropped_spans", @processor.dropped_count.value)
|
|
25
|
-
record_counter(storage, bucket, "export_failures", @processor.failed_export_count.value)
|
|
26
|
-
record_counter(storage, bucket, "processor_exceptions", @processor.exception_count.value)
|
|
27
|
-
|
|
28
|
-
record_counter(storage, bucket, "upload_url_pool_empty", @pool.empty_count.value)
|
|
29
|
-
record_counter(storage, bucket, "upload_url_expired", @pool.expired_count.value)
|
|
30
|
-
|
|
31
|
-
record_counter(storage, bucket, "r2_put_failures", @exporter.put_failure_count.value)
|
|
32
|
-
record_counter(storage, bucket, "notify_failures", @exporter.notify_failure_count.value)
|
|
33
|
-
record_counter(storage, bucket, "trace_pool_empty", @exporter.pool_empty_count.value)
|
|
34
|
-
record_counter(storage, bucket, "trace_export_exceptions", @exporter.exception_count.value)
|
|
35
|
-
|
|
36
|
-
buffer_size = @processor.buffer_size
|
|
37
|
-
buffer_high_watermark = @processor.buffer_high_watermark.value
|
|
38
|
-
record_gauge(storage, bucket, "buffer_size", buffer_size)
|
|
39
|
-
record_gauge(storage, bucket, "buffer_high_watermark", buffer_high_watermark)
|
|
40
|
-
record_gauge(storage, bucket, "buffer_limit", @processor.max_queue) if buffer_size.positive? || buffer_high_watermark.positive?
|
|
41
|
-
@processor.reset_buffer_high_watermark
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def record_counter(storage, bucket, operation, current)
|
|
48
|
-
previous = @last.fetch(operation, 0)
|
|
49
|
-
@last[operation] = current
|
|
50
|
-
delta = current - previous
|
|
51
|
-
return unless delta.positive?
|
|
52
|
-
|
|
53
|
-
storage.add(key(bucket, operation), count: delta, sum_ms: 0, error_count: 0)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def record_gauge(storage, bucket, operation, value)
|
|
57
|
-
storage.add(key(bucket, operation), count: 1, sum_ms: value, error_count: 0)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def key(bucket, operation)
|
|
61
|
-
MetricKey.new(
|
|
62
|
-
bucket: bucket_time(bucket),
|
|
63
|
-
namespace: NAMESPACE,
|
|
64
|
-
service: SERVICE,
|
|
65
|
-
target: TARGET,
|
|
66
|
-
operation: operation
|
|
67
|
-
)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def bucket_time(time)
|
|
71
|
-
Time.utc(time.year, time.month, time.day, time.hour, time.min, 0)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "concurrent/atomic/atomic_reference"
|
|
4
|
-
|
|
5
|
-
module Flare
|
|
6
|
-
# Thread-safe pool of presigned R2 PUT URLs the RuleManager fills from
|
|
7
|
-
# the /api/rules response. TraceExporter checks one out before each
|
|
8
|
-
# upload; if the pool is empty (no active rules, no fresh URLs) it
|
|
9
|
-
# returns nil and the exporter gives up on that batch -- caller decides
|
|
10
|
-
# what to do.
|
|
11
|
-
#
|
|
12
|
-
# Each entry is a Hash: { upload_id:, key:, put_url:, expires_at: }.
|
|
13
|
-
# expires_at is a Time; entries past their expiry are skipped on checkout.
|
|
14
|
-
#
|
|
15
|
-
# Fork-safe: after_fork clears the pool so child processes don't reuse
|
|
16
|
-
# parent URLs (each child polls its own copy from /api/rules anyway).
|
|
17
|
-
class UploadUrlPool
|
|
18
|
-
attr_reader :checkouts, :empty_count, :expired_count
|
|
19
|
-
|
|
20
|
-
def initialize
|
|
21
|
-
@entries_ref = Concurrent::AtomicReference.new([].freeze)
|
|
22
|
-
@checkouts = Concurrent::AtomicFixnum.new(0)
|
|
23
|
-
@empty_count = Concurrent::AtomicFixnum.new(0)
|
|
24
|
-
@expired_count = Concurrent::AtomicFixnum.new(0)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def replace(entries)
|
|
28
|
-
normalized = (entries || []).filter_map { |raw| normalize(raw) }
|
|
29
|
-
@entries_ref.set(normalized.freeze)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def checkout
|
|
33
|
-
now = Time.now
|
|
34
|
-
loop do
|
|
35
|
-
current = @entries_ref.get
|
|
36
|
-
if current.empty?
|
|
37
|
-
@empty_count.increment
|
|
38
|
-
return nil
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
candidate, *rest = current
|
|
42
|
-
next_state = rest.freeze
|
|
43
|
-
next unless @entries_ref.compare_and_set(current, next_state)
|
|
44
|
-
|
|
45
|
-
if expired?(candidate, now)
|
|
46
|
-
@expired_count.increment
|
|
47
|
-
next # try the next one
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@checkouts.increment
|
|
51
|
-
return candidate
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def size
|
|
56
|
-
@entries_ref.get.length
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def empty?
|
|
60
|
-
size.zero?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def clear
|
|
64
|
-
@entries_ref.set([].freeze)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Drop URLs that have already passed their expires_at. Cheap; safe to
|
|
68
|
-
# call from RuleManager's scheduler in between polls.
|
|
69
|
-
def sweep
|
|
70
|
-
now = Time.now
|
|
71
|
-
current = @entries_ref.get
|
|
72
|
-
live = current.reject { |e| expired?(e, now) }
|
|
73
|
-
return 0 if live.length == current.length
|
|
74
|
-
|
|
75
|
-
@entries_ref.set(live.freeze)
|
|
76
|
-
current.length - live.length
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Call from Flare.after_fork. Parent's URLs aren't usable from the
|
|
80
|
-
# child's point of view (each child should get its own from a fresh
|
|
81
|
-
# /api/rules poll), so just drop them.
|
|
82
|
-
def after_fork
|
|
83
|
-
clear
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def normalize(raw)
|
|
89
|
-
h = raw.is_a?(Hash) ? raw : nil
|
|
90
|
-
return nil unless h
|
|
91
|
-
|
|
92
|
-
upload_id = h[:upload_id] || h["upload_id"]
|
|
93
|
-
key = h[:key] || h["key"]
|
|
94
|
-
put_url = h[:put_url] || h["put_url"]
|
|
95
|
-
expires_at = h[:expires_at] || h["expires_at"]
|
|
96
|
-
return nil if upload_id.nil? || key.nil? || put_url.nil?
|
|
97
|
-
|
|
98
|
-
expires_at = Time.iso8601(expires_at) if expires_at.is_a?(String)
|
|
99
|
-
{ upload_id: upload_id, key: key, put_url: put_url, expires_at: expires_at }
|
|
100
|
-
rescue StandardError
|
|
101
|
-
nil
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def expired?(entry, now)
|
|
105
|
-
entry[:expires_at] && entry[:expires_at] <= now
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
data/lib/flare/version.rb
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "opentelemetry/sdk"
|
|
4
|
-
|
|
5
|
-
module Flare
|
|
6
|
-
# Path 2: ActiveSupport::Notifications subscriber that fires on
|
|
7
|
-
# `start_processing.action_controller`, after Rails has routed to a
|
|
8
|
-
# controller#action. At that point the rack server span's start
|
|
9
|
-
# attributes don't yet carry code.namespace/code.function -- only the
|
|
10
|
-
# ActionPack instrumentation adds them, and Flare::Sampler's start-time
|
|
11
|
-
# decision (RECORD_ONLY) was already locked in.
|
|
12
|
-
#
|
|
13
|
-
# The subscriber consults the same sampler's rule set, finds any whose
|
|
14
|
-
# match_attributes match the now-known controller/action, applies the
|
|
15
|
-
# deterministic trace_id_ratio gate (CAF-1: no rate bypass on Path 2),
|
|
16
|
-
# and on pass calls marker.mark(trace_id, owner_span_id:, rule_id:).
|
|
17
|
-
# FilteringSpanProcessor then forwards every span in the trace to the
|
|
18
|
-
# exporter and unmarks when the owner (this rack span) finishes.
|
|
19
|
-
class WebMarkerSubscriber
|
|
20
|
-
NOTIFICATION = "start_processing.action_controller"
|
|
21
|
-
|
|
22
|
-
def initialize(sampler:, marker:)
|
|
23
|
-
@sampler = sampler
|
|
24
|
-
@marker = marker
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def start
|
|
28
|
-
@subscriber = ActiveSupport::Notifications.subscribe(NOTIFICATION) do |*, payload|
|
|
29
|
-
handle(payload)
|
|
30
|
-
end
|
|
31
|
-
self
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def stop
|
|
35
|
-
ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
|
|
36
|
-
@subscriber = nil
|
|
37
|
-
self
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Public for tests so they don't have to drive ActiveSupport::Notifications.
|
|
41
|
-
# current_span lets tests inject a context; in production it's the
|
|
42
|
-
# rack server span on the current thread.
|
|
43
|
-
def handle(payload, current_span: OpenTelemetry::Trace.current_span)
|
|
44
|
-
return unless current_span
|
|
45
|
-
ctx = current_span.context
|
|
46
|
-
return unless ctx && ctx.valid?
|
|
47
|
-
|
|
48
|
-
attrs = candidate_attributes(payload)
|
|
49
|
-
return if attrs.empty?
|
|
50
|
-
|
|
51
|
-
@sampler.rules.each do |rule|
|
|
52
|
-
next unless matches?(rule, attrs)
|
|
53
|
-
next unless @sampler.trace_id_ratio(ctx.trace_id) < rule.rate
|
|
54
|
-
|
|
55
|
-
current_span.set_attribute(Flare::Sampler::RULE_ID_ATTRIBUTE, rule.id) if current_span.respond_to?(:set_attribute)
|
|
56
|
-
@marker.mark(ctx.trace_id, owner_span_id: ctx.span_id, rule_id: rule.id)
|
|
57
|
-
break
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def candidate_attributes(payload)
|
|
64
|
-
controller = payload[:controller] || payload["controller"]
|
|
65
|
-
action = payload[:action] || payload["action"]
|
|
66
|
-
{
|
|
67
|
-
"code.namespace" => controller,
|
|
68
|
-
"code.function" => action
|
|
69
|
-
}.compact
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def matches?(rule, attrs)
|
|
73
|
-
rule.match_attributes.all? { |k, v| attrs[k] == v }
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|