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/sampler.rb
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "concurrent/atomic/atomic_reference"
|
|
4
|
-
require "opentelemetry/sdk"
|
|
5
|
-
|
|
6
|
-
module Flare
|
|
7
|
-
# Path 1 trace sampler. At span start, iterates active rules; returns
|
|
8
|
-
# RECORD_AND_SAMPLE when one matches and the deterministic trace_id_ratio
|
|
9
|
-
# falls under the rule's rate. Otherwise RECORD_ONLY -- the span still
|
|
10
|
-
# records so MetricSpanProcessor sees it; the trace export decision for
|
|
11
|
-
# web spans is deferred to Path 2 via Flare::Marker.
|
|
12
|
-
#
|
|
13
|
-
# Used as the `root` sampler inside an OTel ParentBased sampler so root
|
|
14
|
-
# spans go through this logic but child spans inherit upstream decisions.
|
|
15
|
-
# The `local_parent_not_sampled` slot of the ParentBased should point at
|
|
16
|
-
# Flare::ALWAYS_RECORD_ONLY -- the default ALWAYS_OFF would drop children
|
|
17
|
-
# of an unsampled local parent, making them NoOp spans the processors
|
|
18
|
-
# never see.
|
|
19
|
-
#
|
|
20
|
-
# Rules are pushed in via update_rules from Flare::RuleManager; the swap
|
|
21
|
-
# is atomic, and malformed rule entries are dropped with a counter so a
|
|
22
|
-
# bad server payload can't crash the tracing path.
|
|
23
|
-
class Sampler
|
|
24
|
-
Decision = OpenTelemetry::SDK::Trace::Samplers::Decision
|
|
25
|
-
Result = OpenTelemetry::SDK::Trace::Samplers::Result
|
|
26
|
-
|
|
27
|
-
RULE_ID_ATTRIBUTE = "flare.rule_id"
|
|
28
|
-
|
|
29
|
-
Rule = Struct.new(:id, :match_attributes, :rate, keyword_init: true)
|
|
30
|
-
|
|
31
|
-
attr_reader :dropped_rule_count
|
|
32
|
-
|
|
33
|
-
def initialize
|
|
34
|
-
@rules_ref = Concurrent::AtomicReference.new([].freeze)
|
|
35
|
-
@dropped_rule_count = Concurrent::AtomicFixnum.new(0)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# new_rules: an array of rule hashes from GET /api/rules, e.g.
|
|
39
|
-
# [{ "id" => 1, "match_attributes" => {...}, "rate" => 0.5 }, ...]
|
|
40
|
-
# Entries that don't validate are skipped (counted in dropped_rule_count).
|
|
41
|
-
def update_rules(new_rules)
|
|
42
|
-
validated = (new_rules || []).filter_map { |r| validate(r) }
|
|
43
|
-
@dropped_rule_count.increment((new_rules || []).length - validated.length)
|
|
44
|
-
@rules_ref.set(validated.freeze)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def rules
|
|
48
|
-
@rules_ref.get
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
|
|
52
|
-
tracestate = tracestate_from(parent_context)
|
|
53
|
-
|
|
54
|
-
rules.each do |rule|
|
|
55
|
-
next unless matches?(rule, attributes)
|
|
56
|
-
next unless trace_id_ratio(trace_id) < rule.rate
|
|
57
|
-
|
|
58
|
-
merged = (attributes || {}).merge(RULE_ID_ATTRIBUTE => rule.id)
|
|
59
|
-
return Result.new(decision: Decision::RECORD_AND_SAMPLE, attributes: merged, tracestate: tracestate)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
Result.new(decision: Decision::RECORD_ONLY, tracestate: tracestate)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def description
|
|
66
|
-
"Flare::Sampler"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Cross-language formula: last 8 bytes of the 16-byte raw trace_id as
|
|
70
|
-
# uint64-big-endian, divided by 2^64. Same in every Flare SDK so the
|
|
71
|
-
# server can reproduce the decision if it ever needs to.
|
|
72
|
-
def trace_id_ratio(trace_id)
|
|
73
|
-
bytes = trace_id.is_a?(String) ? trace_id.bytes : Array(trace_id)
|
|
74
|
-
tail = bytes.last(8)
|
|
75
|
-
n = 0
|
|
76
|
-
tail.each { |b| n = (n << 8) | b }
|
|
77
|
-
n.to_f / (1 << 64)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
def tracestate_from(parent_context)
|
|
83
|
-
OpenTelemetry::Trace.current_span(parent_context).context.tracestate ||
|
|
84
|
-
OpenTelemetry::Trace::Tracestate::DEFAULT
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def matches?(rule, attributes)
|
|
88
|
-
return false if attributes.nil?
|
|
89
|
-
rule.match_attributes.all? { |k, v| attributes[k] == v }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def validate(raw)
|
|
93
|
-
return nil unless raw.is_a?(Hash)
|
|
94
|
-
|
|
95
|
-
id = raw["id"] || raw[:id]
|
|
96
|
-
match = raw["match_attributes"] || raw[:match_attributes]
|
|
97
|
-
rate = raw["rate"] || raw[:rate]
|
|
98
|
-
|
|
99
|
-
return nil if id.nil?
|
|
100
|
-
return nil unless match.is_a?(Hash) && match.any?
|
|
101
|
-
return nil unless match.all? { |k, v| k.is_a?(String) && v.is_a?(String) && !v.empty? }
|
|
102
|
-
return nil unless rate.is_a?(Numeric) && rate > 0.0 && rate <= 1.0
|
|
103
|
-
|
|
104
|
-
Rule.new(id: id, match_attributes: match, rate: rate.to_f)
|
|
105
|
-
rescue StandardError
|
|
106
|
-
nil
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Tiny sampler whose should_sample? returns RECORD_ONLY for every span.
|
|
111
|
-
# Slot this into the ParentBased local_parent_not_sampled position so
|
|
112
|
-
# children of an unsampled local parent stay recording (the default
|
|
113
|
-
# ALWAYS_OFF turns them into NoOp spans no processor ever sees).
|
|
114
|
-
class AlwaysRecordOnly
|
|
115
|
-
Decision = OpenTelemetry::SDK::Trace::Samplers::Decision
|
|
116
|
-
Result = OpenTelemetry::SDK::Trace::Samplers::Result
|
|
117
|
-
|
|
118
|
-
def should_sample?(parent_context: nil, **)
|
|
119
|
-
tracestate = OpenTelemetry::Trace.current_span(parent_context).context.tracestate ||
|
|
120
|
-
OpenTelemetry::Trace::Tracestate::DEFAULT
|
|
121
|
-
Result.new(decision: Decision::RECORD_ONLY, tracestate: tracestate)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def description
|
|
125
|
-
"Flare::AlwaysRecordOnly"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
ALWAYS_RECORD_ONLY = AlwaysRecordOnly.new
|
|
130
|
-
end
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flare
|
|
4
|
-
# Utility for finding the source location (file, line, method) of app code
|
|
5
|
-
# that triggered a database query or other instrumented operation.
|
|
6
|
-
module SourceLocation
|
|
7
|
-
# How many app code lines to capture for the backtrace
|
|
8
|
-
MAX_TRACE_LINES = 8
|
|
9
|
-
|
|
10
|
-
# Patterns to filter out from backtraces (gems, framework code)
|
|
11
|
-
IGNORE_PATTERNS = [
|
|
12
|
-
/\/gems\//,
|
|
13
|
-
/\/ruby\//,
|
|
14
|
-
/\/rubygems\//,
|
|
15
|
-
/lib\/active_record/,
|
|
16
|
-
/lib\/active_support/,
|
|
17
|
-
/lib\/action_/,
|
|
18
|
-
/opentelemetry/,
|
|
19
|
-
/flare/,
|
|
20
|
-
/<internal:/,
|
|
21
|
-
/\/bin\//,
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
|
-
module_function
|
|
25
|
-
|
|
26
|
-
# Find the first app source location from the current backtrace
|
|
27
|
-
# Returns a hash with :filepath, :lineno, :function or nil
|
|
28
|
-
def find
|
|
29
|
-
backtrace = caller(2, 50)
|
|
30
|
-
return nil unless backtrace
|
|
31
|
-
|
|
32
|
-
# Find the first line that's app code (not gems/framework)
|
|
33
|
-
app_line = backtrace.find { |line| app_code?(line) }
|
|
34
|
-
return nil unless app_line
|
|
35
|
-
|
|
36
|
-
parse_backtrace_line(app_line)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find multiple app source locations from the current backtrace
|
|
40
|
-
# Returns an array of cleaned backtrace lines (up to MAX_TRACE_LINES)
|
|
41
|
-
def find_trace
|
|
42
|
-
backtrace = caller(2, 100)
|
|
43
|
-
return [] unless backtrace
|
|
44
|
-
|
|
45
|
-
# Filter to only app code lines
|
|
46
|
-
app_lines = backtrace.select { |line| app_code?(line) }
|
|
47
|
-
return [] if app_lines.empty?
|
|
48
|
-
|
|
49
|
-
# Clean and format each line, limit to MAX_TRACE_LINES
|
|
50
|
-
app_lines.first(MAX_TRACE_LINES).map { |line| clean_backtrace_line(line) }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Add source location attributes to a hash (for span attributes)
|
|
54
|
-
def add_to_attributes(attrs)
|
|
55
|
-
location = find
|
|
56
|
-
return attrs unless location
|
|
57
|
-
|
|
58
|
-
attrs["code.filepath"] = location[:filepath]
|
|
59
|
-
attrs["code.lineno"] = location[:lineno]
|
|
60
|
-
attrs["code.function"] = location[:function] if location[:function]
|
|
61
|
-
|
|
62
|
-
# Add full trace as a single string attribute
|
|
63
|
-
trace = find_trace
|
|
64
|
-
attrs["code.stacktrace"] = trace.join("\n") if trace.any?
|
|
65
|
-
|
|
66
|
-
attrs
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def app_code?(line)
|
|
70
|
-
# Must contain /app/ (Rails convention) and not match ignore patterns
|
|
71
|
-
return false unless line.include?("/app/")
|
|
72
|
-
|
|
73
|
-
IGNORE_PATTERNS.none? { |pattern| line.match?(pattern) }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def parse_backtrace_line(line)
|
|
77
|
-
# Parse: /path/to/file.rb:123:in `method_name'
|
|
78
|
-
if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
|
|
79
|
-
{
|
|
80
|
-
filepath: clean_path($1),
|
|
81
|
-
lineno: $2.to_i,
|
|
82
|
-
function: $3
|
|
83
|
-
}
|
|
84
|
-
elsif line =~ /\A(.+):(\d+)\z/
|
|
85
|
-
{
|
|
86
|
-
filepath: clean_path($1),
|
|
87
|
-
lineno: $2.to_i,
|
|
88
|
-
function: nil
|
|
89
|
-
}
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def clean_backtrace_line(line)
|
|
94
|
-
# Parse and reformat: "app/models/user.rb:42:in `find_by_email'"
|
|
95
|
-
if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
|
|
96
|
-
"#{clean_path($1)}:#{$2} in `#{$3}'"
|
|
97
|
-
elsif line =~ /\A(.+):(\d+)\z/
|
|
98
|
-
"#{clean_path($1)}:#{$2}"
|
|
99
|
-
else
|
|
100
|
-
clean_path(line)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def clean_path(path)
|
|
105
|
-
# Remove Rails.root prefix if present
|
|
106
|
-
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
107
|
-
path.sub(/\A#{Regexp.escape(Rails.root.to_s)}\//, "")
|
|
108
|
-
else
|
|
109
|
-
path
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sqlite3"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
require_relative "deadline"
|
|
7
|
-
|
|
8
|
-
module Flare
|
|
9
|
-
class SQLiteExporter
|
|
10
|
-
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
11
|
-
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
12
|
-
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
13
|
-
|
|
14
|
-
# Prune roughly every 100 exports (1% chance per export)
|
|
15
|
-
PRUNE_PROBABILITY = 0.01
|
|
16
|
-
|
|
17
|
-
def initialize(database_path)
|
|
18
|
-
@database_path = database_path
|
|
19
|
-
@mutex = Mutex.new
|
|
20
|
-
@setup = false
|
|
21
|
-
@pid = $$
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Maximum number of retry attempts when the database is busy.
|
|
25
|
-
# Mirrors ActiveRecord's retry strategy for SQLite.
|
|
26
|
-
MAX_RETRIES = 3
|
|
27
|
-
ExportDeadlineExceeded = Class.new(StandardError)
|
|
28
|
-
|
|
29
|
-
def export(span_datas, timeout: nil)
|
|
30
|
-
detect_forking
|
|
31
|
-
deadline = Deadline.new(timeout)
|
|
32
|
-
|
|
33
|
-
retries = 0
|
|
34
|
-
exported = 0
|
|
35
|
-
|
|
36
|
-
begin
|
|
37
|
-
setup_database(deadline) unless @setup
|
|
38
|
-
raise ExportDeadlineExceeded if deadline.expired?
|
|
39
|
-
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
40
|
-
|
|
41
|
-
begin
|
|
42
|
-
apply_busy_timeout(deadline)
|
|
43
|
-
connection.transaction do
|
|
44
|
-
span_datas.each do |span_data|
|
|
45
|
-
raise ExportDeadlineExceeded if deadline.expired?
|
|
46
|
-
next if should_ignore_span?(span_data)
|
|
47
|
-
|
|
48
|
-
create_span(span_data)
|
|
49
|
-
exported += 1
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
ensure
|
|
53
|
-
@mutex.unlock
|
|
54
|
-
end
|
|
55
|
-
rescue ExportDeadlineExceeded
|
|
56
|
-
return TIMEOUT
|
|
57
|
-
rescue ::SQLite3::BusyException
|
|
58
|
-
retries += 1
|
|
59
|
-
if retries <= MAX_RETRIES
|
|
60
|
-
sleep_time = 0.1 * retries
|
|
61
|
-
return TIMEOUT if deadline.remaining && sleep_time >= deadline.remaining
|
|
62
|
-
|
|
63
|
-
sleep(sleep_time)
|
|
64
|
-
retry
|
|
65
|
-
end
|
|
66
|
-
warn "[Flare] SQLite export error: database is busy after #{MAX_RETRIES} retries"
|
|
67
|
-
return FAILURE
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
Flare.log "Exported #{exported} spans to SQLite" if exported > 0 && Flare.respond_to?(:log)
|
|
71
|
-
|
|
72
|
-
# Periodically prune old data
|
|
73
|
-
maybe_prune unless deadline.expired?
|
|
74
|
-
|
|
75
|
-
deadline.expired? ? TIMEOUT : SUCCESS
|
|
76
|
-
rescue => e
|
|
77
|
-
warn "[Flare] SQLite export error: #{e.message}"
|
|
78
|
-
FAILURE
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def force_flush(timeout: nil)
|
|
82
|
-
SUCCESS
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def shutdown(timeout: nil)
|
|
86
|
-
SUCCESS
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
def detect_forking
|
|
92
|
-
return if @pid == $$
|
|
93
|
-
|
|
94
|
-
@pid = $$
|
|
95
|
-
@mutex = Mutex.new
|
|
96
|
-
close_connection
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def lock_before_deadline(deadline)
|
|
100
|
-
return @mutex.lock unless deadline.remaining
|
|
101
|
-
|
|
102
|
-
until @mutex.try_lock
|
|
103
|
-
return false if deadline.expired?
|
|
104
|
-
|
|
105
|
-
sleep([deadline.remaining, 0.001].min)
|
|
106
|
-
end
|
|
107
|
-
true
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def apply_busy_timeout(deadline)
|
|
111
|
-
remaining = deadline.remaining
|
|
112
|
-
timeout_ms = remaining ? [(remaining * 1_000).floor, 1].max : 5_000
|
|
113
|
-
connection.busy_timeout = [timeout_ms, 5_000].min
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def maybe_prune
|
|
117
|
-
return unless rand < PRUNE_PROBABILITY
|
|
118
|
-
|
|
119
|
-
Flare.storage.prune(
|
|
120
|
-
retention_hours: Flare.configuration.retention_hours,
|
|
121
|
-
max_spans: Flare.configuration.max_spans
|
|
122
|
-
)
|
|
123
|
-
rescue => e
|
|
124
|
-
warn "[Flare] Prune error: #{e.message}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def should_ignore_span?(span_data)
|
|
128
|
-
span_data.name&.start_with?("Flare::")
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def create_span(span_data)
|
|
132
|
-
now = Time.now.iso8601(6)
|
|
133
|
-
|
|
134
|
-
sql = <<~SQL
|
|
135
|
-
INSERT INTO flare_spans (name, kind, span_id, trace_id, parent_span_id, start_timestamp, end_timestamp, total_recorded_links, total_recorded_events, total_recorded_properties, created_at, updated_at)
|
|
136
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
|
-
SQL
|
|
138
|
-
|
|
139
|
-
values = [
|
|
140
|
-
span_data.name,
|
|
141
|
-
span_data.kind.to_s,
|
|
142
|
-
span_data.hex_span_id,
|
|
143
|
-
span_data.hex_trace_id,
|
|
144
|
-
span_data.hex_parent_span_id,
|
|
145
|
-
span_data.start_timestamp,
|
|
146
|
-
span_data.end_timestamp,
|
|
147
|
-
span_data.total_recorded_links,
|
|
148
|
-
span_data.total_recorded_events,
|
|
149
|
-
span_data.total_recorded_attributes,
|
|
150
|
-
now,
|
|
151
|
-
now
|
|
152
|
-
]
|
|
153
|
-
|
|
154
|
-
connection.execute(sql, values)
|
|
155
|
-
span_record_id = connection.last_insert_row_id
|
|
156
|
-
|
|
157
|
-
span_data.events&.each do |span_event|
|
|
158
|
-
create_event(span_record_id, span_event)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
create_properties("Flare::Span", span_record_id, span_data.attributes)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def create_event(span_record_id, span_event)
|
|
165
|
-
now = Time.now.iso8601(6)
|
|
166
|
-
timestamp = span_event.timestamp ? Time.at(span_event.timestamp / 1_000_000_000.0).iso8601(6) : now
|
|
167
|
-
|
|
168
|
-
sql = <<~SQL
|
|
169
|
-
INSERT INTO flare_events (span_id, name, created_at, updated_at)
|
|
170
|
-
VALUES (?, ?, ?, ?)
|
|
171
|
-
SQL
|
|
172
|
-
|
|
173
|
-
connection.execute(sql, [span_record_id, span_event.name, timestamp, now])
|
|
174
|
-
event_record_id = connection.last_insert_row_id
|
|
175
|
-
create_properties("Flare::Event", event_record_id, span_event.attributes)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def create_properties(owner_type, owner_id, attributes)
|
|
179
|
-
return unless attributes
|
|
180
|
-
|
|
181
|
-
now = Time.now.iso8601(6)
|
|
182
|
-
|
|
183
|
-
sql = <<~SQL
|
|
184
|
-
INSERT INTO flare_properties (key, value, value_type, owner_type, owner_id, created_at, updated_at)
|
|
185
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
186
|
-
SQL
|
|
187
|
-
|
|
188
|
-
attributes.each do |key, value|
|
|
189
|
-
next if value.nil?
|
|
190
|
-
|
|
191
|
-
value_type = determine_value_type(value)
|
|
192
|
-
serialized_value = JSON.generate(value)
|
|
193
|
-
connection.execute(sql, [key, serialized_value, value_type, owner_type, owner_id, now, now])
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def determine_value_type(value)
|
|
198
|
-
case value
|
|
199
|
-
when String then 0 # string
|
|
200
|
-
when Integer then 1 # integer
|
|
201
|
-
when Float then 2 # float
|
|
202
|
-
when TrueClass, FalseClass then 3 # boolean
|
|
203
|
-
when Array then 4 # array
|
|
204
|
-
else 0 # default to string
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def setup_database(deadline)
|
|
209
|
-
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
210
|
-
|
|
211
|
-
begin
|
|
212
|
-
return if @setup
|
|
213
|
-
|
|
214
|
-
db = connection
|
|
215
|
-
configure_pragmas(db, deadline)
|
|
216
|
-
|
|
217
|
-
execute_setup(db, deadline, <<~SQL)
|
|
218
|
-
CREATE TABLE IF NOT EXISTS flare_spans (
|
|
219
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
220
|
-
name TEXT NOT NULL,
|
|
221
|
-
kind TEXT NOT NULL,
|
|
222
|
-
span_id TEXT NOT NULL,
|
|
223
|
-
trace_id TEXT NOT NULL,
|
|
224
|
-
parent_span_id TEXT,
|
|
225
|
-
start_timestamp INTEGER NOT NULL,
|
|
226
|
-
end_timestamp INTEGER NOT NULL,
|
|
227
|
-
total_recorded_properties INTEGER NOT NULL DEFAULT 0,
|
|
228
|
-
total_recorded_events INTEGER NOT NULL DEFAULT 0,
|
|
229
|
-
total_recorded_links INTEGER NOT NULL DEFAULT 0,
|
|
230
|
-
created_at TEXT NOT NULL,
|
|
231
|
-
updated_at TEXT NOT NULL
|
|
232
|
-
)
|
|
233
|
-
SQL
|
|
234
|
-
|
|
235
|
-
execute_setup(db, deadline, <<~SQL)
|
|
236
|
-
CREATE INDEX IF NOT EXISTS idx_spans_span_id ON flare_spans(span_id)
|
|
237
|
-
SQL
|
|
238
|
-
|
|
239
|
-
execute_setup(db, deadline, <<~SQL)
|
|
240
|
-
CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON flare_spans(trace_id)
|
|
241
|
-
SQL
|
|
242
|
-
|
|
243
|
-
execute_setup(db, deadline, <<~SQL)
|
|
244
|
-
CREATE INDEX IF NOT EXISTS idx_spans_parent_span_id ON flare_spans(parent_span_id)
|
|
245
|
-
SQL
|
|
246
|
-
|
|
247
|
-
execute_setup(db, deadline, <<~SQL)
|
|
248
|
-
CREATE INDEX IF NOT EXISTS idx_spans_created_at ON flare_spans(created_at)
|
|
249
|
-
SQL
|
|
250
|
-
|
|
251
|
-
execute_setup(db, deadline, <<~SQL)
|
|
252
|
-
CREATE TABLE IF NOT EXISTS flare_events (
|
|
253
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
254
|
-
span_id INTEGER NOT NULL,
|
|
255
|
-
name TEXT NOT NULL,
|
|
256
|
-
created_at TEXT NOT NULL,
|
|
257
|
-
updated_at TEXT NOT NULL,
|
|
258
|
-
FOREIGN KEY (span_id) REFERENCES flare_spans(id)
|
|
259
|
-
)
|
|
260
|
-
SQL
|
|
261
|
-
|
|
262
|
-
execute_setup(db, deadline, <<~SQL)
|
|
263
|
-
CREATE INDEX IF NOT EXISTS idx_events_span_id ON flare_events(span_id)
|
|
264
|
-
SQL
|
|
265
|
-
|
|
266
|
-
execute_setup(db, deadline, <<~SQL)
|
|
267
|
-
CREATE TABLE IF NOT EXISTS flare_properties (
|
|
268
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
269
|
-
key TEXT NOT NULL,
|
|
270
|
-
value TEXT,
|
|
271
|
-
value_type INTEGER NOT NULL DEFAULT 0,
|
|
272
|
-
owner_type TEXT NOT NULL,
|
|
273
|
-
owner_id INTEGER NOT NULL,
|
|
274
|
-
created_at TEXT NOT NULL,
|
|
275
|
-
updated_at TEXT NOT NULL
|
|
276
|
-
)
|
|
277
|
-
SQL
|
|
278
|
-
|
|
279
|
-
execute_setup(db, deadline, <<~SQL)
|
|
280
|
-
CREATE INDEX IF NOT EXISTS idx_properties_owner ON flare_properties(owner_type, owner_id)
|
|
281
|
-
SQL
|
|
282
|
-
|
|
283
|
-
execute_setup(db, deadline, <<~SQL)
|
|
284
|
-
CREATE INDEX IF NOT EXISTS idx_properties_key ON flare_properties(key)
|
|
285
|
-
SQL
|
|
286
|
-
|
|
287
|
-
close_connection # avoid inheriting connection across fork
|
|
288
|
-
@setup = true
|
|
289
|
-
ensure
|
|
290
|
-
@mutex.unlock
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Applies the same SQLite pragmas that ActiveRecord uses for good
|
|
295
|
-
# concurrency and performance with threaded/multi-process access.
|
|
296
|
-
def configure_pragmas(db, deadline)
|
|
297
|
-
execute_setup(db, deadline, "PRAGMA journal_mode=WAL")
|
|
298
|
-
execute_setup(db, deadline, "PRAGMA synchronous=NORMAL")
|
|
299
|
-
execute_setup(db, deadline, "PRAGMA mmap_size=134217728") # 128MB
|
|
300
|
-
execute_setup(db, deadline, "PRAGMA journal_size_limit=67108864") # 64MB
|
|
301
|
-
execute_setup(db, deadline, "PRAGMA cache_size=2000")
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def execute_setup(db, deadline, statement)
|
|
305
|
-
raise ExportDeadlineExceeded if deadline.expired?
|
|
306
|
-
|
|
307
|
-
apply_busy_timeout(deadline)
|
|
308
|
-
db.execute(statement)
|
|
309
|
-
raise ExportDeadlineExceeded if deadline.expired?
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def connection_key
|
|
313
|
-
:"flare_sqlite_db_#{@database_path.hash}"
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def connection
|
|
317
|
-
Thread.current[connection_key] ||= begin
|
|
318
|
-
dir = File.dirname(@database_path)
|
|
319
|
-
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
320
|
-
db = ::SQLite3::Database.new(@database_path, results_as_hash: true)
|
|
321
|
-
db.busy_timeout = 5000
|
|
322
|
-
db
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def close_connection
|
|
327
|
-
key = connection_key
|
|
328
|
-
if db = Thread.current[key]
|
|
329
|
-
db.close rescue nil
|
|
330
|
-
Thread.current[key] = nil
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
end
|