flare 0.4.2 → 0.5.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/README.md +60 -1
- data/app/controllers/flare/jobs_controller.rb +1 -1
- data/app/controllers/flare/requests_controller.rb +1 -1
- data/app/controllers/flare/spans_controller.rb +1 -1
- data/app/helpers/flare/application_helper.rb +23 -0
- data/app/views/flare/jobs/index.html.erb +2 -1
- data/app/views/flare/requests/index.html.erb +2 -1
- data/app/views/flare/requests/show.html.erb +16 -7
- data/app/views/flare/spans/index.html.erb +2 -1
- data/lib/flare/cli/doctor_command.rb +4 -11
- data/lib/flare/cli/local_spans.rb +110 -0
- data/lib/flare/cli/nplus1_command.rb +97 -0
- data/lib/flare/cli/project.rb +28 -0
- data/lib/flare/cli/request_command.rb +120 -0
- data/lib/flare/cli/requests_command.rb +45 -0
- data/lib/flare/cli/setup_command.rb +64 -41
- data/lib/flare/cli/status_command.rb +5 -2
- data/lib/flare/cli.rb +12 -0
- data/lib/flare/client_headers.rb +7 -3
- data/lib/flare/configuration.rb +20 -0
- data/lib/flare/export_filter.rb +273 -0
- data/lib/flare/filtering_span_processor.rb +14 -0
- data/lib/flare/http_transport.rb +4 -0
- data/lib/flare/metric_span_processor.rb +51 -35
- data/lib/flare/metric_submitter.rb +9 -1
- data/lib/flare/recording_batch_span_processor.rb +9 -1
- data/lib/flare/redis_statement_processor.rb +29 -0
- data/lib/flare/revision.rb +82 -0
- data/lib/flare/source_location.rb +21 -16
- data/lib/flare/sql_mask.rb +235 -0
- data/lib/flare/sqlite_exporter.rb +12 -8
- data/lib/flare/storage/sqlite.rb +195 -144
- data/lib/flare/storage.rb +1 -40
- data/lib/flare/store_statement.rb +136 -0
- data/lib/flare/trace_blob.rb +9 -5
- data/lib/flare/trace_exporter.rb +18 -3
- data/lib/flare/upload_url_pool.rb +2 -15
- data/lib/flare/utf8.rb +20 -0
- data/lib/flare/version.rb +1 -1
- data/lib/flare.rb +21 -2
- data/public/flare-assets/flare.css +10 -60
- metadata +45 -32
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
require_relative "sql_mask"
|
|
7
|
+
require_relative "utf8"
|
|
8
|
+
|
|
9
|
+
module Flare
|
|
10
|
+
# The shape of a Redis or MongoDB statement with its values taken out. Keeps what
|
|
11
|
+
# groups calls together: the command, a key's first segment, a document's
|
|
12
|
+
# structure. Anything it can't read comes out as a placeholder.
|
|
13
|
+
module StoreStatement
|
|
14
|
+
PLACEHOLDER = SqlMask::PLACEHOLDER
|
|
15
|
+
REDIS_SYSTEM = /redis|valkey/i
|
|
16
|
+
MORE = "..."
|
|
17
|
+
MAX_REDIS_LINES = 10
|
|
18
|
+
MAX_REDIS_BYTES = 16_384
|
|
19
|
+
MAX_MONGO_BYTES = 65_536
|
|
20
|
+
MAX_MONGO_DEPTH = 16
|
|
21
|
+
MAX_MONGO_NODES = 5_000
|
|
22
|
+
|
|
23
|
+
KEY_NAMESPACE = /\A[A-Za-z][A-Za-z_-]{0,40}[\/:]/
|
|
24
|
+
|
|
25
|
+
# Commands whose first argument is a key.
|
|
26
|
+
KEY_COMMANDS = %w[
|
|
27
|
+
APPEND DECR DECRBY DEL EXISTS EXPIRE EXPIREAT GET GETDEL GETEX GETRANGE GETSET HDEL HEXISTS HGET HGETALL
|
|
28
|
+
HINCRBY HINCRBYFLOAT HKEYS HLEN HMGET HMSET HSCAN HSET HSETNX HSTRLEN HVALS INCR INCRBY INCRBYFLOAT LINDEX
|
|
29
|
+
LINSERT LLEN LMOVE LPOP LPOS LPUSH LPUSHX LRANGE LREM LSET LTRIM MGET MSET MSETNX PERSIST PEXPIRE PEXPIREAT
|
|
30
|
+
PFADD PFCOUNT PSETEX PTTL RENAME RENAMENX RPOP RPUSH RPUSHX SADD SCARD SDIFF SET SETEX SETNX SETRANGE SINTER
|
|
31
|
+
SISMEMBER SMEMBERS SMISMEMBER SPOP SRANDMEMBER SREM SSCAN STRLEN SUBSTR SUNION TOUCH TTL TYPE UNLINK XACK XADD
|
|
32
|
+
XDEL XLEN XRANGE XREVRANGE XTRIM ZADD ZCARD ZCOUNT ZINCRBY ZLEXCOUNT ZMSCORE ZPOPMAX ZPOPMIN ZRANGE
|
|
33
|
+
ZRANGEBYLEX ZRANGEBYSCORE ZRANK ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZREVRANK
|
|
34
|
+
ZSCAN ZSCORE
|
|
35
|
+
].to_set.freeze
|
|
36
|
+
|
|
37
|
+
# Commands whose arguments aren't kept.
|
|
38
|
+
OTHER_COMMANDS = %w[
|
|
39
|
+
ACL ASKING AUTH BGSAVE BITCOUNT BLMOVE BLPOP BRPOP BZPOPMAX BZPOPMIN CLIENT CLUSTER COMMAND CONFIG COPY DBSIZE
|
|
40
|
+
DISCARD DUMP ECHO EVAL EVALSHA EVALSHA_RO EVAL_RO EXEC FCALL FLUSHALL FLUSHDB FUNCTION GEOADD GEODIST GEOHASH
|
|
41
|
+
GEOPOS GEOSEARCH GETBIT HELLO INFO KEYS LASTSAVE MEMORY MIGRATE MODULE MONITOR MOVE MULTI OBJECT PING PSUBSCRIBE
|
|
42
|
+
PUBLISH PUBSUB PUNSUBSCRIBE QUIT RANDOMKEY READONLY READWRITE RESET RESTORE ROLE SAVE SCAN SCRIPT SELECT SETBIT
|
|
43
|
+
SHUTDOWN SLAVEOF SLOWLOG SORT SPUBLISH SSUBSCRIBE SUBSCRIBE SUNSUBSCRIBE SWAPDB TIME UNSUBSCRIBE UNWATCH WAIT
|
|
44
|
+
WATCH XAUTOCLAIM XCLAIM XGROUP XINFO XPENDING XREAD XREADGROUP
|
|
45
|
+
].to_set.freeze
|
|
46
|
+
MODULE_COMMAND = /\A[A-Z]+\.[A-Z]+\z/
|
|
47
|
+
|
|
48
|
+
COLLECTION_COMMANDS = %w[
|
|
49
|
+
find count distinct aggregate insert update delete findAndModify findandmodify getMore create drop listIndexes
|
|
50
|
+
createIndexes dropIndexes
|
|
51
|
+
].to_set.freeze
|
|
52
|
+
COLLECTION_NAME = /\A[\w.-]{1,64}\z/
|
|
53
|
+
MONGO_KEY = /\A\$?[A-Za-z_][\w-]{0,63}\z/
|
|
54
|
+
|
|
55
|
+
class TooLarge < StandardError; end
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
def redis_system?(system)
|
|
60
|
+
REDIS_SYSTEM.match?(system.to_s)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A key's leading word and separator, like views/? or session:?.
|
|
64
|
+
def masked_key(key)
|
|
65
|
+
"#{key[KEY_NAMESPACE]}#{PLACEHOLDER}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The instrumentation joins a pipeline's commands with line breaks and a
|
|
69
|
+
# command's arguments with spaces, so a line break can also sit inside a value.
|
|
70
|
+
# Several lines therefore keep only command names, and only lines that start
|
|
71
|
+
# with one count. A script can't be told from its arguments, so it's left out.
|
|
72
|
+
def redis(statement, pipelined: true)
|
|
73
|
+
text = UTF8.scrub(statement.to_s.byteslice(0, MAX_REDIS_BYTES))
|
|
74
|
+
lines = pipelined ? text.split("\n") : [text.split("\n", 2).first.to_s]
|
|
75
|
+
|
|
76
|
+
keep_key = lines.size == 1
|
|
77
|
+
shapes = lines.filter_map { |line| redis_line(line, keep_key) }.chunk_while { |a, b| a == b }.map(&:first)
|
|
78
|
+
return PLACEHOLDER if shapes.empty?
|
|
79
|
+
|
|
80
|
+
kept = shapes.first(MAX_REDIS_LINES)
|
|
81
|
+
kept << MORE if lines.include?(MORE) || shapes.size > MAX_REDIS_LINES
|
|
82
|
+
kept.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def redis_line(line, keep_key)
|
|
86
|
+
command, key = line.split(" ", 3)
|
|
87
|
+
return unless command
|
|
88
|
+
|
|
89
|
+
command = command.upcase
|
|
90
|
+
key_command = KEY_COMMANDS.include?(command)
|
|
91
|
+
return unless key_command || OTHER_COMMANDS.include?(command) || MODULE_COMMAND.match?(command)
|
|
92
|
+
return command unless key
|
|
93
|
+
|
|
94
|
+
"#{command} #{keep_key && key_command ? masked_key(key) : PLACEHOLDER}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Keeps the structure of a command's JSON and masks every value. Only a leading
|
|
98
|
+
# command keeps its value, the collection name.
|
|
99
|
+
def mongo(statement)
|
|
100
|
+
text = statement.to_s
|
|
101
|
+
return PLACEHOLDER if text.empty? || text.bytesize > MAX_MONGO_BYTES
|
|
102
|
+
|
|
103
|
+
document = JSON.parse(text, max_nesting: MAX_MONGO_DEPTH)
|
|
104
|
+
return PLACEHOLDER unless document.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
budget = [MAX_MONGO_NODES]
|
|
107
|
+
masked = mongo_value(document, budget)
|
|
108
|
+
command, collection = document.first
|
|
109
|
+
if COLLECTION_COMMANDS.include?(command) && collection.is_a?(String) && COLLECTION_NAME.match?(collection)
|
|
110
|
+
masked[mongo_key(command)] = collection
|
|
111
|
+
end
|
|
112
|
+
JSON.generate(masked)
|
|
113
|
+
rescue JSON::ParserError, EncodingError, TooLarge, ArgumentError
|
|
114
|
+
PLACEHOLDER
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mongo_value(value, budget)
|
|
118
|
+
raise TooLarge if (budget[0] -= 1).negative?
|
|
119
|
+
|
|
120
|
+
case value
|
|
121
|
+
when Hash then value.each_with_object({}) { |(key, item), out| out[mongo_key(key)] = mongo_value(item, budget) }
|
|
122
|
+
when Array then value.map { |item| mongo_value(item, budget) }.uniq
|
|
123
|
+
else PLACEHOLDER
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def mongo_key(key)
|
|
128
|
+
head, dot, = key.partition(".")
|
|
129
|
+
return PLACEHOLDER unless MONGO_KEY.match?(head)
|
|
130
|
+
|
|
131
|
+
dot.empty? ? head : "#{head}.#{PLACEHOLDER}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private_class_method :redis_line, :mongo_value, :mongo_key
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/flare/trace_blob.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
|
+
require_relative "export_filter"
|
|
6
|
+
require_relative "utf8"
|
|
7
|
+
|
|
5
8
|
module Flare
|
|
6
9
|
# Value object that turns a group of OTel span_data for a single trace
|
|
7
10
|
# into the Flare-JSON wire format the server expects:
|
|
@@ -33,18 +36,19 @@ module Flare
|
|
|
33
36
|
def initialize(trace_id:, spans:)
|
|
34
37
|
@trace_id = trace_id
|
|
35
38
|
@spans = spans
|
|
39
|
+
@filter = ExportFilter.current
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def to_h
|
|
39
43
|
root = find_root
|
|
40
|
-
{
|
|
44
|
+
UTF8.scrub({
|
|
41
45
|
"trace_id" => hexify(@trace_id),
|
|
42
46
|
"trace_rule_id" => rule_id_from_spans,
|
|
43
47
|
"root_name" => root_name(root),
|
|
44
48
|
"started_at" => iso(root&.start_timestamp),
|
|
45
49
|
"duration_ms" => duration_ms(root),
|
|
46
50
|
"spans" => @spans.map { |s| span_to_h(s) }
|
|
47
|
-
}
|
|
51
|
+
})
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
private
|
|
@@ -84,17 +88,17 @@ module Flare
|
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
def root_name(root)
|
|
87
|
-
root&.name&.to_s&.slice(0, ROOT_NAME_LIMIT)
|
|
91
|
+
@filter.name(root&.name&.to_s)&.slice(0, ROOT_NAME_LIMIT)
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
def span_to_h(span)
|
|
91
95
|
{
|
|
92
96
|
"id" => hexify(span.span_id),
|
|
93
97
|
"parent_id" => root?(span) ? nil : hexify(span.parent_span_id),
|
|
94
|
-
"name" => span.name,
|
|
98
|
+
"name" => @filter.name(span.name),
|
|
95
99
|
"started_at" => iso(span.start_timestamp),
|
|
96
100
|
"duration_ms" => duration_ms(span),
|
|
97
|
-
"attributes" => span.attributes
|
|
101
|
+
"attributes" => @filter.attributes(span.attributes)
|
|
98
102
|
}
|
|
99
103
|
end
|
|
100
104
|
|
data/lib/flare/trace_exporter.rb
CHANGED
|
@@ -91,17 +91,22 @@ module Flare
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
# Errors are contained per trace so one bad trace or one transport
|
|
95
|
+
# error doesn't drop the rest of the batch.
|
|
96
|
+
def ship(blob, deadline:, body: nil, retried: false)
|
|
95
97
|
return FAILURE if blob.nil?
|
|
96
98
|
return TIMEOUT if deadline.expired?
|
|
97
99
|
|
|
100
|
+
# Encode before checkout so a trace that can't be encoded doesn't use
|
|
101
|
+
# up a URL, but skip the work when there's no URL to upload to.
|
|
102
|
+
body ||= encode(blob) unless @pool.empty?
|
|
98
103
|
entry = @pool.checkout
|
|
99
104
|
if entry.nil?
|
|
100
105
|
@pool_empty_count.increment
|
|
101
106
|
return FAILURE
|
|
102
107
|
end
|
|
103
108
|
|
|
104
|
-
body
|
|
109
|
+
body ||= encode(blob)
|
|
105
110
|
response = transport_request(:put, entry[:put_url], body, PUT_HEADERS, timeout: deadline.remaining)
|
|
106
111
|
return TIMEOUT if deadline.expired?
|
|
107
112
|
|
|
@@ -110,10 +115,16 @@ module Flare
|
|
|
110
115
|
notify(entry[:key], deadline: deadline)
|
|
111
116
|
when "403"
|
|
112
117
|
# Presigned URL probably expired; try once more with the next one.
|
|
113
|
-
retried ? record_put_failure(response) : ship(blob, deadline: deadline, retried: true)
|
|
118
|
+
retried ? record_put_failure(response) : ship(blob, deadline: deadline, body: body, retried: true)
|
|
114
119
|
else
|
|
115
120
|
record_put_failure(response)
|
|
116
121
|
end
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
return TIMEOUT if deadline.expired? || e.is_a?(HttpTransport::DeadlineExceeded)
|
|
124
|
+
|
|
125
|
+
@exception_count.increment
|
|
126
|
+
@logger.warn("[Flare::TraceExporter] trace export raised: #{e.class}: #{e.message}")
|
|
127
|
+
FAILURE
|
|
117
128
|
end
|
|
118
129
|
|
|
119
130
|
def notify(key, deadline:)
|
|
@@ -171,6 +182,10 @@ module Flare
|
|
|
171
182
|
FAILURE
|
|
172
183
|
end
|
|
173
184
|
|
|
185
|
+
def encode(blob)
|
|
186
|
+
gzip(JSON.generate(blob.to_h))
|
|
187
|
+
end
|
|
188
|
+
|
|
174
189
|
def gzip(body)
|
|
175
190
|
io = StringIO.new
|
|
176
191
|
gz = Zlib::GzipWriter.new(io)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "concurrent/atomic/atomic_fixnum"
|
|
3
4
|
require "concurrent/atomic/atomic_reference"
|
|
4
5
|
|
|
5
6
|
module Flare
|
|
@@ -15,11 +16,10 @@ module Flare
|
|
|
15
16
|
# Fork-safe: after_fork clears the pool so child processes don't reuse
|
|
16
17
|
# parent URLs (each child polls its own copy from /api/rules anyway).
|
|
17
18
|
class UploadUrlPool
|
|
18
|
-
attr_reader :
|
|
19
|
+
attr_reader :empty_count, :expired_count
|
|
19
20
|
|
|
20
21
|
def initialize
|
|
21
22
|
@entries_ref = Concurrent::AtomicReference.new([].freeze)
|
|
22
|
-
@checkouts = Concurrent::AtomicFixnum.new(0)
|
|
23
23
|
@empty_count = Concurrent::AtomicFixnum.new(0)
|
|
24
24
|
@expired_count = Concurrent::AtomicFixnum.new(0)
|
|
25
25
|
end
|
|
@@ -47,7 +47,6 @@ module Flare
|
|
|
47
47
|
next # try the next one
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
@checkouts.increment
|
|
51
50
|
return candidate
|
|
52
51
|
end
|
|
53
52
|
end
|
|
@@ -64,18 +63,6 @@ module Flare
|
|
|
64
63
|
@entries_ref.set([].freeze)
|
|
65
64
|
end
|
|
66
65
|
|
|
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
66
|
# Call from Flare.after_fork. Parent's URLs aren't usable from the
|
|
80
67
|
# child's point of view (each child should get its own from a fresh
|
|
81
68
|
# /api/rules poll), so just drop them.
|
data/lib/flare/utf8.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
# Span attributes carry raw request bytes (User-Agent, paths) and SQL, which
|
|
5
|
+
# can hold invalid UTF-8. JSON.generate raises on those, failing the whole
|
|
6
|
+
# export batch, so replace the bad bytes before serializing.
|
|
7
|
+
module UTF8
|
|
8
|
+
def self.scrub(value)
|
|
9
|
+
case value
|
|
10
|
+
when String
|
|
11
|
+
return value if value.ascii_only? || (value.valid_encoding? && value.encoding != Encoding::BINARY)
|
|
12
|
+
|
|
13
|
+
value.dup.force_encoding(Encoding::UTF_8).scrub
|
|
14
|
+
when Array then value.map { |v| scrub(v) }
|
|
15
|
+
when Hash then value.to_h { |k, v| [scrub(k), scrub(v)] }
|
|
16
|
+
else value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/flare/version.rb
CHANGED
data/lib/flare.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "flare/source_location"
|
|
|
10
10
|
require_relative "flare/metric_key"
|
|
11
11
|
require_relative "flare/metric_storage"
|
|
12
12
|
require_relative "flare/metric_span_processor"
|
|
13
|
+
require_relative "flare/redis_statement_processor"
|
|
13
14
|
require_relative "flare/metric_flusher"
|
|
14
15
|
require_relative "flare/backoff_policy"
|
|
15
16
|
require_relative "flare/metric_submitter"
|
|
@@ -181,7 +182,11 @@ module Flare
|
|
|
181
182
|
require "opentelemetry-instrumentation-rack"
|
|
182
183
|
require "opentelemetry-instrumentation-net_http"
|
|
183
184
|
require "opentelemetry-instrumentation-active_support"
|
|
184
|
-
|
|
185
|
+
begin
|
|
186
|
+
# Internal OTel path; subscribe_to_notifications handles it moving.
|
|
187
|
+
require "opentelemetry/instrumentation/active_support/span_subscriber"
|
|
188
|
+
rescue LoadError
|
|
189
|
+
end
|
|
185
190
|
require "opentelemetry-instrumentation-action_pack" if defined?(ActionController)
|
|
186
191
|
require "opentelemetry-instrumentation-action_view" if defined?(ActionView)
|
|
187
192
|
require "opentelemetry-instrumentation-active_job" if defined?(ActiveJob)
|
|
@@ -195,6 +200,9 @@ module Flare
|
|
|
195
200
|
OpenTelemetry::SDK.configure do |c|
|
|
196
201
|
c.service_name = service_name
|
|
197
202
|
|
|
203
|
+
# Reads Redis statements as spans start, so raw arguments don't reach a finished span.
|
|
204
|
+
c.add_span_processor(RedisStatementProcessor.new)
|
|
205
|
+
|
|
198
206
|
# Spans: detailed trace data stored in SQLite
|
|
199
207
|
if configuration.spans_enabled && exporter
|
|
200
208
|
c.add_span_processor(span_processor)
|
|
@@ -208,11 +216,18 @@ module Flare
|
|
|
208
216
|
"OpenTelemetry::Instrumentation::Rack" => {
|
|
209
217
|
untraced_requests: ->(env) {
|
|
210
218
|
request = Rack::Request.new(env)
|
|
211
|
-
|
|
219
|
+
# Skip Flare's own dashboard and assets, not app routes like /flares.
|
|
220
|
+
path = request.path
|
|
221
|
+
return true if path == "/flare" || path.start_with?("/flare/", "/flare-assets/")
|
|
212
222
|
|
|
213
223
|
configuration.ignore_request.call(request)
|
|
214
224
|
}
|
|
215
225
|
},
|
|
226
|
+
# The key is what groups Redis calls. RedisStatementProcessor reduces the
|
|
227
|
+
# statement to the command and the key's first segment.
|
|
228
|
+
"OpenTelemetry::Instrumentation::Redis" => {
|
|
229
|
+
db_statement: :include,
|
|
230
|
+
},
|
|
216
231
|
# Name Sidekiq job spans after the worker class (e.g. "MyWorker
|
|
217
232
|
# process") instead of the upstream default of the queue name
|
|
218
233
|
# ("default process"), matching how ActiveJob spans are named.
|
|
@@ -410,6 +425,10 @@ module Flare
|
|
|
410
425
|
}.freeze
|
|
411
426
|
|
|
412
427
|
def subscribe_to_notifications
|
|
428
|
+
unless OpenTelemetry::Instrumentation::ActiveSupport.respond_to?(:subscribe)
|
|
429
|
+
return warn "[Flare] OpenTelemetry::Instrumentation::ActiveSupport.subscribe not found. SQL, cache, mailer and custom notification spans are disabled. Check your opentelemetry-instrumentation-active_support version."
|
|
430
|
+
end
|
|
431
|
+
|
|
413
432
|
NOTIFICATION_TRANSFORMERS.each do |pattern, transformer|
|
|
414
433
|
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
|
|
415
434
|
rescue
|
|
@@ -190,21 +190,6 @@ code, pre {
|
|
|
190
190
|
opacity: 1;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
.nav-item-count {
|
|
194
|
-
margin-left: auto;
|
|
195
|
-
font-size: 0.6875rem;
|
|
196
|
-
font-weight: 500;
|
|
197
|
-
color: var(--cb-text-muted);
|
|
198
|
-
background: var(--cb-bg-muted);
|
|
199
|
-
padding: 0.125rem 0.5rem;
|
|
200
|
-
border-radius: 10px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.nav-item.active .nav-item-count {
|
|
204
|
-
background: var(--cb-text);
|
|
205
|
-
color: white;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
193
|
/* Sidebar Footer */
|
|
209
194
|
.sidebar-footer {
|
|
210
195
|
padding: 0.75rem 0.625rem;
|
|
@@ -332,21 +317,6 @@ code, pre {
|
|
|
332
317
|
box-shadow: none;
|
|
333
318
|
}
|
|
334
319
|
|
|
335
|
-
.card-header {
|
|
336
|
-
padding: 0.75rem 1rem;
|
|
337
|
-
border-bottom: 1px solid var(--cb-border);
|
|
338
|
-
display: flex;
|
|
339
|
-
align-items: center;
|
|
340
|
-
justify-content: space-between;
|
|
341
|
-
background: transparent;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
.card-title {
|
|
345
|
-
font-size: 0.8125rem;
|
|
346
|
-
font-weight: 600;
|
|
347
|
-
color: var(--cb-text);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
320
|
.card-body {
|
|
351
321
|
padding: 0;
|
|
352
322
|
}
|
|
@@ -369,6 +339,15 @@ code, pre {
|
|
|
369
339
|
border-bottom: 1px solid var(--cb-border);
|
|
370
340
|
}
|
|
371
341
|
|
|
342
|
+
.sort-link {
|
|
343
|
+
color: inherit;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.sort-link:hover,
|
|
347
|
+
.sort-link.active {
|
|
348
|
+
color: var(--cb-text);
|
|
349
|
+
}
|
|
350
|
+
|
|
372
351
|
.data-table td {
|
|
373
352
|
padding: 0.5rem 0.75rem;
|
|
374
353
|
border-bottom: 1px solid var(--cb-bg-muted);
|
|
@@ -400,11 +379,6 @@ code, pre {
|
|
|
400
379
|
letter-spacing: 0.02em;
|
|
401
380
|
}
|
|
402
381
|
|
|
403
|
-
.badge-verb {
|
|
404
|
-
background: var(--cb-bg-muted);
|
|
405
|
-
color: var(--cb-text-secondary);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
382
|
.badge-get { background: rgba(107,138,154,0.15); color: #4a7a8a; }
|
|
409
383
|
.badge-post { background: rgba(43,122,62,0.15); color: #1d6b2f; }
|
|
410
384
|
.badge-put, .badge-patch { background: rgba(196,163,90,0.15); color: #9a7d2e; }
|
|
@@ -419,15 +393,8 @@ code, pre {
|
|
|
419
393
|
.badge-error { background: rgba(197,48,48,0.15); color: #a53030; }
|
|
420
394
|
.badge-warning { background: rgba(196,163,90,0.15); color: #9a7d2e; }
|
|
421
395
|
|
|
422
|
-
.badge-type {
|
|
423
|
-
font-family: var(--cb-font);
|
|
424
|
-
font-weight: 500;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
.badge-request { background: rgba(107,138,154,0.15); color: #4a7a8a; }
|
|
428
396
|
.badge-job { background: rgba(138,154,107,0.15); color: #5a7a3d; }
|
|
429
397
|
|
|
430
|
-
.badge-clue { font-family: var(--cb-font-mono); font-size: 0.6875rem; }
|
|
431
398
|
.badge-sql { background: rgba(107,138,154,0.15); color: #4a7a8a; }
|
|
432
399
|
.badge-cache { background: rgba(43,122,62,0.15); color: #1d6b2f; }
|
|
433
400
|
.badge-view { background: rgba(196,163,90,0.15); color: #9a7d2e; }
|
|
@@ -436,7 +403,6 @@ code, pre {
|
|
|
436
403
|
.badge-exception { background: rgba(197,48,48,0.15); color: #a53030; }
|
|
437
404
|
.badge-other { background: var(--cb-bg-muted); color: var(--cb-text-secondary); }
|
|
438
405
|
.badge-redis { background: rgba(197,48,48,0.12); color: #a53030; }
|
|
439
|
-
.badge-query { background: rgba(107,138,154,0.15); color: #4a7a8a; }
|
|
440
406
|
|
|
441
407
|
/* --- Search / Filters --- */
|
|
442
408
|
.filters {
|
|
@@ -1037,15 +1003,6 @@ code, pre {
|
|
|
1037
1003
|
}
|
|
1038
1004
|
|
|
1039
1005
|
/* --- Code Blocks --- */
|
|
1040
|
-
.code-path {
|
|
1041
|
-
font-family: var(--cb-font-mono);
|
|
1042
|
-
font-size: 0.8125rem;
|
|
1043
|
-
color: var(--cb-text-secondary);
|
|
1044
|
-
background: var(--cb-bg-subtle);
|
|
1045
|
-
padding: 0.125rem 0.375rem;
|
|
1046
|
-
border-radius: var(--cb-radius-sm);
|
|
1047
|
-
display: inline-block;
|
|
1048
|
-
}
|
|
1049
1006
|
|
|
1050
1007
|
pre {
|
|
1051
1008
|
background: var(--cb-text);
|
|
@@ -1168,14 +1125,7 @@ pre {
|
|
|
1168
1125
|
border: 1px solid rgba(197,48,48,0.2);
|
|
1169
1126
|
}
|
|
1170
1127
|
|
|
1171
|
-
.alert-success {
|
|
1172
|
-
background: rgba(43,122,62,0.1);
|
|
1173
|
-
color: var(--cb-success);
|
|
1174
|
-
border: 1px solid rgba(43,122,62,0.2);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1128
|
/* --- Muted --- */
|
|
1178
|
-
.muted { color: var(--cb-text-muted); }
|
|
1179
1129
|
|
|
1180
1130
|
/* --- Responsive --- */
|
|
1181
1131
|
@media (max-width: 1024px) {
|
|
@@ -1186,7 +1136,7 @@ pre {
|
|
|
1186
1136
|
padding: 1rem 0.75rem;
|
|
1187
1137
|
justify-content: center;
|
|
1188
1138
|
}
|
|
1189
|
-
.logo-text, .nav-section-title, .nav-item span, .
|
|
1139
|
+
.logo-text, .nav-section-title, .nav-item span, .btn-clear span {
|
|
1190
1140
|
display: none;
|
|
1191
1141
|
}
|
|
1192
1142
|
.nav-item {
|