goodsorcery 0.1.1 → 0.2.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/CHANGELOG.md +9 -0
- data/README.md +30 -1
- data/lib/generators/goodsorcery/install/templates/initializer.rb +7 -0
- data/lib/goodsorcery/client.rb +15 -1
- data/lib/goodsorcery/configuration/performance_settings.rb +37 -0
- data/lib/goodsorcery/configuration/validation.rb +10 -0
- data/lib/goodsorcery/configuration.rb +3 -0
- data/lib/goodsorcery/defaults.rb +4 -0
- data/lib/goodsorcery/integrations/active_job.rb +4 -1
- data/lib/goodsorcery/integrations/rails.rb +3 -2
- data/lib/goodsorcery/integrations/sidekiq.rb +18 -2
- data/lib/goodsorcery/performance/agent.rb +104 -0
- data/lib/goodsorcery/performance/aggregator.rb +63 -0
- data/lib/goodsorcery/performance/call_site.rb +61 -0
- data/lib/goodsorcery/performance/flusher.rb +224 -0
- data/lib/goodsorcery/performance/histogram.rb +23 -0
- data/lib/goodsorcery/performance/job_timing.rb +106 -0
- data/lib/goodsorcery/performance/measurement.rb +117 -0
- data/lib/goodsorcery/performance/net_http.rb +37 -0
- data/lib/goodsorcery/performance/payloads.rb +110 -0
- data/lib/goodsorcery/performance/process_label.rb +22 -0
- data/lib/goodsorcery/performance/recorder.rb +202 -0
- data/lib/goodsorcery/performance/span_list.rb +156 -0
- data/lib/goodsorcery/performance/sql_normalizer.rb +149 -0
- data/lib/goodsorcery/performance/subscribers.rb +140 -0
- data/lib/goodsorcery/performance/trace_buffer.rb +78 -0
- data/lib/goodsorcery/performance/trace_builder.rb +121 -0
- data/lib/goodsorcery/performance/transaction_stats.rb +93 -0
- data/lib/goodsorcery/performance/uuid_v7.rb +27 -0
- data/lib/goodsorcery/performance.rb +158 -0
- data/lib/goodsorcery/rack/middleware.rb +10 -3
- data/lib/goodsorcery/transport.rb +12 -1
- data/lib/goodsorcery/version.rb +1 -1
- data/lib/goodsorcery.rb +17 -0
- metadata +21 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodSorcery
|
|
4
|
+
module Performance
|
|
5
|
+
# Holds the slowest trace per transaction per minute (DESIGN.md 8.5).
|
|
6
|
+
# +candidate?+ is the cheap check made for every unit of work; only a
|
|
7
|
+
# candidate's trace is built (outside the lock), then +offer+ swaps it in
|
|
8
|
+
# if it is still the slowest. At most MAX_TRACES_PER_MINUTE traces per
|
|
9
|
+
# minute: when full, a new transaction's trace replaces the fastest one
|
|
10
|
+
# held, and only if it is slower.
|
|
11
|
+
class TraceBuffer
|
|
12
|
+
MAX_TRACES_PER_MINUTE = 30
|
|
13
|
+
MAX_MINUTES = Aggregator::MAX_PENDING_MINUTES
|
|
14
|
+
|
|
15
|
+
Entry = Struct.new(:duration_ms, :trace)
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@lock = Mutex.new
|
|
19
|
+
@minutes = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def candidate?(minute, key, duration_ms)
|
|
23
|
+
@lock.synchronize { slower?(@minutes[minute], key, duration_ms) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# => true when the trace was kept.
|
|
27
|
+
def offer(minute, key, duration_ms, trace)
|
|
28
|
+
@lock.synchronize do
|
|
29
|
+
entries = minute_entries(minute)
|
|
30
|
+
next false unless slower?(entries, key, duration_ms)
|
|
31
|
+
|
|
32
|
+
evict_fastest(entries) if !entries.key?(key) && entries.size >= MAX_TRACES_PER_MINUTE
|
|
33
|
+
entries[key] = Entry.new(duration_ms, trace)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Removes and returns the traces of the minutes up to +last_minute+
|
|
39
|
+
# (all when nil), slowest first.
|
|
40
|
+
def take(last_minute = nil)
|
|
41
|
+
@lock.synchronize do
|
|
42
|
+
minutes = @minutes.keys.select { |minute| last_minute.nil? || minute <= last_minute }
|
|
43
|
+
entries = minutes.flat_map { |minute| @minutes.delete(minute).values }
|
|
44
|
+
entries.sort_by { |entry| -entry.duration_ms }.map(&:trace)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def size
|
|
49
|
+
@lock.synchronize { @minutes.values.sum(&:size) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def slower?(entries, key, duration_ms)
|
|
55
|
+
return true if entries.nil?
|
|
56
|
+
|
|
57
|
+
held = entries[key]
|
|
58
|
+
return duration_ms > held.duration_ms if held
|
|
59
|
+
return true if entries.size < MAX_TRACES_PER_MINUTE
|
|
60
|
+
|
|
61
|
+
duration_ms > entries.values.min_by(&:duration_ms).duration_ms
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def evict_fastest(entries)
|
|
65
|
+
key, = entries.min_by { |_key, entry| entry.duration_ms }
|
|
66
|
+
entries.delete(key)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def minute_entries(minute)
|
|
70
|
+
entries = @minutes[minute]
|
|
71
|
+
return entries if entries
|
|
72
|
+
|
|
73
|
+
@minutes.delete(@minutes.keys.min) while @minutes.size >= MAX_MINUTES
|
|
74
|
+
@minutes[minute] = {}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodSorcery
|
|
4
|
+
module Performance
|
|
5
|
+
# What every trace in this process shares: the SQL normaliser, app paths,
|
|
6
|
+
# the source line cache (only with send_code_context), host and runtime.
|
|
7
|
+
class TraceContext
|
|
8
|
+
attr_reader :normalizer, :paths, :host, :runtime
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@normalizer = SqlNormalizer.new
|
|
12
|
+
@paths = Backtrace::Paths.new(config.root)
|
|
13
|
+
@line_cache = config.send_code_context ? Backtrace::LineCache.new(@paths, max_files: 32) : nil
|
|
14
|
+
@host = hostname
|
|
15
|
+
@runtime = { "ruby" => RUBY_VERSION, "rails" => rails_version }.compact
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call_site_hash(call_site)
|
|
19
|
+
call_site.to_h(@paths, line_cache: @line_cache)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def hostname
|
|
25
|
+
Text.limit(Socket.gethostname, 255)
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def rails_version
|
|
31
|
+
::Rails.version.to_s if defined?(::Rails) && ::Rails.respond_to?(:version)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Builds one trace payload (INGEST_API.md › Traces payload) from a
|
|
36
|
+
# finished recorder. Only the slowest unit of work so far in its minute
|
|
37
|
+
# gets here, so this may take a little time; it never runs inside a lock.
|
|
38
|
+
class TraceBuilder
|
|
39
|
+
MAX_TRACE_BYTES = 65_536
|
|
40
|
+
DECIMALS = 3
|
|
41
|
+
|
|
42
|
+
def initialize(recorder, minute, context)
|
|
43
|
+
@recorder = recorder
|
|
44
|
+
@minute = minute
|
|
45
|
+
@context = context
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build
|
|
49
|
+
spans = SpanList.new(@recorder, @context)
|
|
50
|
+
trace = identity.merge(timing, outcome, details, counts(spans.to_a, spans.dropped))
|
|
51
|
+
fit(trace)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def identity
|
|
57
|
+
hash = { "id" => UuidV7.generate(Time.at(@recorder.started_wall)), "kind" => @recorder.kind.to_s,
|
|
58
|
+
"name" => Text.limit(@recorder.name, 255) }
|
|
59
|
+
hash.merge(Labels.for(@recorder).except("source"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def timing
|
|
63
|
+
{ "minute" => Performance.minute_iso(@minute), "started_at" => Time.at(@recorder.started_wall).utc.iso8601(3),
|
|
64
|
+
"duration_ms" => round(@recorder.duration_ms), "db_ms" => round(@recorder.db_ms),
|
|
65
|
+
"http_ms" => round(@recorder.http_ms), "view_ms" => round(@recorder.view_ms),
|
|
66
|
+
"ruby_ms" => round(@recorder.ruby_ms) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def outcome
|
|
70
|
+
hash = { "failed" => @recorder.failed?, "error_event_id" => @recorder.error_event_id }
|
|
71
|
+
hash["status"] = @recorder.status if @recorder.kind == :web && @recorder.status
|
|
72
|
+
hash
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def details
|
|
76
|
+
section = @recorder.kind == :web ? { "request" => request } : { "job" => job }
|
|
77
|
+
gc = @recorder.gc || {}
|
|
78
|
+
section.merge("host" => @context.host, "process" => ProcessLabel.current, "runtime" => @context.runtime,
|
|
79
|
+
"gc" => gc.slice("time_ms", "count"), "allocations" => gc["allocations"]).compact
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def request
|
|
83
|
+
env = @recorder.env || {}
|
|
84
|
+
path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
|
|
85
|
+
{ "method" => @recorder.http_method && Text.limit(@recorder.http_method, 16),
|
|
86
|
+
"path" => Text.limit(path.empty? ? "/" : path, SpanList::MAX_PATH_LENGTH),
|
|
87
|
+
"request_id" => env["action_dispatch.request_id"] && Text.limit(env["action_dispatch.request_id"], 128) }
|
|
88
|
+
.compact
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def job
|
|
92
|
+
{ "adapter" => @recorder.adapter, "queue" => @recorder.queue,
|
|
93
|
+
"queue_wait_ms" => @recorder.queue_wait_ms && round(@recorder.queue_wait_ms),
|
|
94
|
+
"executions" => @recorder.job_details&.executions }.compact
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def counts(spans, dropped)
|
|
98
|
+
{ "sql_count" => @recorder.sql_count, "http_count" => @recorder.http_count,
|
|
99
|
+
"spans_dropped" => @recorder.spans_dropped + dropped, "spans" => spans }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Keeps a trace under MAX_TRACE_BYTES of JSON by dropping its shortest
|
|
103
|
+
# spans, half at a time.
|
|
104
|
+
def fit(trace)
|
|
105
|
+
halve_spans(trace) while JSON.generate(trace).bytesize > MAX_TRACE_BYTES && !trace["spans"].empty?
|
|
106
|
+
trace
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def halve_spans(trace)
|
|
110
|
+
spans = trace["spans"]
|
|
111
|
+
kept = spans.max_by(spans.size / 2) { |span| span["duration_ms"] }.sort_by { |span| span["offset_ms"] }
|
|
112
|
+
trace["spans_dropped"] += (spans - kept).sum { |span| span.fetch("count", 1) }
|
|
113
|
+
trace["spans"] = kept
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def round(value)
|
|
117
|
+
value.to_f.round(DECIMALS)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodSorcery
|
|
4
|
+
module Performance
|
|
5
|
+
# What one finished unit of work contributes to the metrics.
|
|
6
|
+
Sample = Struct.new(:kind, :name, :labels, :duration_ms, :failed, :db_ms, :http_ms, :view_ms, :ruby_ms,
|
|
7
|
+
:queue_wait_ms) do
|
|
8
|
+
def self.from(recorder)
|
|
9
|
+
new(recorder.kind, recorder.name, Labels.for(recorder), recorder.duration_ms, recorder.failed?,
|
|
10
|
+
recorder.db_ms, recorder.http_ms, recorder.view_ms, recorder.ruby_ms, recorder.queue_wait_ms)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The labels sent with a transaction: method and route for web requests,
|
|
15
|
+
# adapter and queue for jobs, and the source file of either.
|
|
16
|
+
module Labels
|
|
17
|
+
LIMITS = { "method" => 16, "route" => 255, "adapter" => 32, "queue" => 128, "source" => 500 }.freeze
|
|
18
|
+
|
|
19
|
+
def self.for(recorder)
|
|
20
|
+
labels = if recorder.kind == :web
|
|
21
|
+
{ "method" => recorder.http_method, "route" => recorder.route }
|
|
22
|
+
else
|
|
23
|
+
{ "adapter" => recorder.adapter, "queue" => recorder.queue }
|
|
24
|
+
end
|
|
25
|
+
labels["source"] = recorder.source
|
|
26
|
+
labels.each_with_object({}) do |(key, value), limited|
|
|
27
|
+
limited[key] = Text.limit(value, LIMITS.fetch(key)) unless value.nil?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# One transaction's numbers for one minute (DESIGN.md 8.4): count,
|
|
33
|
+
# failed, the summed duration and, per histogram bucket that has
|
|
34
|
+
# requests, [count, db_ms, http_ms, view_ms, ruby_ms]. Jobs add the
|
|
35
|
+
# summed queue wait and its bucket counts.
|
|
36
|
+
class TransactionStats
|
|
37
|
+
DECIMALS = 3
|
|
38
|
+
|
|
39
|
+
attr_reader :count, :failed, :duration_ms, :buckets, :queue_wait_ms, :queue_wait, :labels
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
@count = @failed = 0
|
|
43
|
+
@duration_ms = 0.0
|
|
44
|
+
@buckets = {}
|
|
45
|
+
@queue_wait_ms = nil
|
|
46
|
+
@queue_wait = {}
|
|
47
|
+
@labels = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# +keep_labels+ is false for the "(other)" row, which has none.
|
|
51
|
+
def add(sample, keep_labels: true)
|
|
52
|
+
@labels = sample.labels if keep_labels
|
|
53
|
+
@count += 1
|
|
54
|
+
@failed += 1 if sample.failed
|
|
55
|
+
@duration_ms += sample.duration_ms
|
|
56
|
+
add_bucket(sample)
|
|
57
|
+
add_queue_wait(sample.queue_wait_ms) if sample.queue_wait_ms
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h(kind, name)
|
|
61
|
+
hash = { "kind" => kind.to_s, "name" => name }.merge(@labels)
|
|
62
|
+
hash.merge!("count" => @count, "failed" => @failed, "duration_ms" => @duration_ms.round(DECIMALS),
|
|
63
|
+
"buckets" => bucket_rows)
|
|
64
|
+
return hash unless @queue_wait_ms
|
|
65
|
+
|
|
66
|
+
hash.merge("queue_wait_ms" => @queue_wait_ms.round(DECIMALS), "queue_wait" => @queue_wait.sort)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def add_bucket(sample)
|
|
72
|
+
row = (@buckets[Histogram.index(sample.duration_ms)] ||= [0, 0.0, 0.0, 0.0, 0.0])
|
|
73
|
+
row[0] += 1
|
|
74
|
+
row[1] += sample.db_ms
|
|
75
|
+
row[2] += sample.http_ms
|
|
76
|
+
row[3] += sample.view_ms
|
|
77
|
+
row[4] += sample.ruby_ms
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_queue_wait(wait_ms)
|
|
81
|
+
@queue_wait_ms = (@queue_wait_ms || 0.0) + wait_ms
|
|
82
|
+
index = Histogram.index(wait_ms)
|
|
83
|
+
@queue_wait[index] = @queue_wait.fetch(index, 0) + 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def bucket_rows
|
|
87
|
+
@buckets.sort.map do |index, (count, *layers)|
|
|
88
|
+
[index, count, *layers.map { |ms| ms.round(DECIMALS) }]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodSorcery
|
|
4
|
+
module Performance
|
|
5
|
+
# Time-ordered UUIDs (RFC 9562 version 7): the first 48 bits are the
|
|
6
|
+
# given time in Unix milliseconds, the rest random. Trace ids carry the
|
|
7
|
+
# request's start time this way, which SecureRandom.uuid_v7 (always "now")
|
|
8
|
+
# cannot do.
|
|
9
|
+
module UuidV7
|
|
10
|
+
VARIANT_NIBBLES = %w[8 9 a b].freeze
|
|
11
|
+
|
|
12
|
+
def self.generate(time = Time.now)
|
|
13
|
+
hex = hex_digits(time)
|
|
14
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 32 hex digits: 48 bits of time, the version, the variant, randomness.
|
|
18
|
+
def self.hex_digits(time)
|
|
19
|
+
milliseconds = (time.to_r * 1000).to_i & 0xFFFF_FFFF_FFFF
|
|
20
|
+
hex = format("%012x", milliseconds) + SecureRandom.random_bytes(10).unpack1("H*")
|
|
21
|
+
hex[12] = "7"
|
|
22
|
+
hex[16] = VARIANT_NIBBLES[hex[16].to_i(16) & 3]
|
|
23
|
+
hex
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "performance/histogram"
|
|
4
|
+
require_relative "performance/uuid_v7"
|
|
5
|
+
require_relative "performance/process_label"
|
|
6
|
+
require_relative "performance/sql_normalizer"
|
|
7
|
+
require_relative "performance/call_site"
|
|
8
|
+
require_relative "performance/recorder"
|
|
9
|
+
require_relative "performance/transaction_stats"
|
|
10
|
+
require_relative "performance/aggregator"
|
|
11
|
+
require_relative "performance/trace_buffer"
|
|
12
|
+
require_relative "performance/span_list"
|
|
13
|
+
require_relative "performance/trace_builder"
|
|
14
|
+
require_relative "performance/payloads"
|
|
15
|
+
require_relative "performance/flusher"
|
|
16
|
+
require_relative "performance/job_timing"
|
|
17
|
+
require_relative "performance/subscribers"
|
|
18
|
+
require_relative "performance/net_http"
|
|
19
|
+
require_relative "performance/agent"
|
|
20
|
+
require_relative "performance/measurement"
|
|
21
|
+
|
|
22
|
+
module GoodSorcery
|
|
23
|
+
# Performance monitoring (DESIGN.md 8): metrics for every request and job,
|
|
24
|
+
# and traces of the slowest ones, aggregated in process and sent once a
|
|
25
|
+
# minute. Off unless config.performance_monitoring is true; then
|
|
26
|
+
# Performance.install (the Railtie calls it after the app initialized)
|
|
27
|
+
# subscribes to Rails' notifications and instruments Net::HTTP. Nothing
|
|
28
|
+
# here ever raises into the host app.
|
|
29
|
+
module Performance
|
|
30
|
+
RECORDER_KEY = :__goodsorcery_recorder
|
|
31
|
+
SUPPRESS_KEY = :__goodsorcery_suppress_performance
|
|
32
|
+
LOCK = Mutex.new
|
|
33
|
+
|
|
34
|
+
@active = false
|
|
35
|
+
@traces = true
|
|
36
|
+
@ignore = IgnoreMatcher.new([])
|
|
37
|
+
|
|
38
|
+
extend Measurement
|
|
39
|
+
extend TestPayloads
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Starts recording when performance_monitoring is on and the
|
|
43
|
+
# environment reports (or the delivery is :test). => true when on.
|
|
44
|
+
def install(config = GoodSorcery.configuration)
|
|
45
|
+
return false unless config.performance_monitoring && GoodSorcery.client.performance_allowed?
|
|
46
|
+
|
|
47
|
+
configure(config)
|
|
48
|
+
Subscribers.subscribe
|
|
49
|
+
NetHttp.install
|
|
50
|
+
@active = true
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
log.warn("could not start performance monitoring (#{e.class})")
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The one check integrations make per request or job.
|
|
57
|
+
def active? = @active
|
|
58
|
+
|
|
59
|
+
# Stops recording in this process (the server has no APM endpoints).
|
|
60
|
+
def disable!
|
|
61
|
+
@active = false
|
|
62
|
+
@agent&.flusher&.stop!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def recording? = !Thread.current[RECORDER_KEY].nil?
|
|
66
|
+
def current_recorder = Thread.current[RECORDER_KEY]
|
|
67
|
+
def suppressed? = Thread.current[SUPPRESS_KEY] ? true : false
|
|
68
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
|
+
|
|
70
|
+
# Nothing is measured inside the block (the gem's own HTTP requests).
|
|
71
|
+
def suppress
|
|
72
|
+
previous = Thread.current[SUPPRESS_KEY]
|
|
73
|
+
Thread.current[SUPPRESS_KEY] = true
|
|
74
|
+
yield
|
|
75
|
+
ensure
|
|
76
|
+
Thread.current[SUPPRESS_KEY] = previous
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# => a new recorder for this fiber, or nil when off or already recording
|
|
80
|
+
# (a unit of work inside another is part of the outer one).
|
|
81
|
+
def start(kind)
|
|
82
|
+
return nil unless @active && Thread.current[RECORDER_KEY].nil?
|
|
83
|
+
|
|
84
|
+
Thread.current[RECORDER_KEY] = Recorder.new(kind, traces: @traces)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def finish(recorder)
|
|
90
|
+
Thread.current[RECORDER_KEY] = nil if Thread.current[RECORDER_KEY].equal?(recorder)
|
|
91
|
+
recorder.stop
|
|
92
|
+
agent.record(recorder, background: GoodSorcery.configuration.delivery != :test) if recordable?(recorder)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
log.warn_every(:apm_finish_error, 60, "could not record performance data (#{e.class})")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Sends (or, with the :test delivery, stores) what was recorded.
|
|
98
|
+
def flush_performance(include_open_minute: true)
|
|
99
|
+
current = @agent
|
|
100
|
+
return true unless current&.current?
|
|
101
|
+
|
|
102
|
+
current.flusher.flush(include_open: include_open_minute)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def shutdown(timeout)
|
|
106
|
+
current = @agent
|
|
107
|
+
current.flusher.shutdown(timeout) if current&.current?
|
|
108
|
+
true
|
|
109
|
+
rescue StandardError
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def agent
|
|
114
|
+
current = @agent
|
|
115
|
+
return current if current&.current?
|
|
116
|
+
|
|
117
|
+
LOCK.synchronize do
|
|
118
|
+
@agent = Agent.new(GoodSorcery.configuration, log) unless @agent&.current?
|
|
119
|
+
@agent
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def paths
|
|
124
|
+
@paths ||= Backtrace::Paths.new(GoodSorcery.configuration.root)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def minute_iso(minute)
|
|
128
|
+
Time.at(minute * 60).utc.strftime("%Y-%m-%dT%H:%M:00Z")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Forgets everything recorded and turns recording off. For tests.
|
|
132
|
+
def reset!
|
|
133
|
+
@active = false
|
|
134
|
+
@agent&.flusher&.stop!
|
|
135
|
+
@agent = nil
|
|
136
|
+
@paths = nil
|
|
137
|
+
Thread.current[RECORDER_KEY] = nil
|
|
138
|
+
SourceLocation.clear
|
|
139
|
+
clear_test_payloads
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def configure(config)
|
|
145
|
+
@traces = config.performance_traces
|
|
146
|
+
@ignore = IgnoreMatcher.new(config.performance_ignore_transactions)
|
|
147
|
+
@paths = Backtrace::Paths.new(config.root)
|
|
148
|
+
SourceLocation.clear
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def log = GoodSorcery.log
|
|
152
|
+
|
|
153
|
+
def recordable?(recorder)
|
|
154
|
+
@active && !recorder.broken? && !recorder.name.nil? && !@ignore.ignored?(recorder.name)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -10,7 +10,8 @@ module GoodSorcery
|
|
|
10
10
|
# "METHOD /path" breadcrumb and reports exceptions that propagate, then
|
|
11
11
|
# re-raises them. Exceptions an inner layer turned into a 5xx response
|
|
12
12
|
# (env["action_dispatch.exception"], env["sinatra.error"]) are reported
|
|
13
|
-
# too. The request itself is only read if an error is reported.
|
|
13
|
+
# too. The request itself is only read if an error is reported. With
|
|
14
|
+
# performance monitoring on, it also times the request (DESIGN.md 8.2).
|
|
14
15
|
#
|
|
15
16
|
# use GoodSorcery::Rack::Middleware
|
|
16
17
|
class Middleware
|
|
@@ -22,6 +23,14 @@ module GoodSorcery
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def call(env)
|
|
26
|
+
return call_in_scope(env) unless Performance.active?
|
|
27
|
+
|
|
28
|
+
Performance.measure_request(env) { call_in_scope(env) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def call_in_scope(env)
|
|
25
34
|
GoodSorcery.with_scope do |scope|
|
|
26
35
|
open_request(scope, env)
|
|
27
36
|
response = call_app(env)
|
|
@@ -30,8 +39,6 @@ module GoodSorcery
|
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
42
|
def open_request(scope, env)
|
|
36
43
|
scope.request_env = env
|
|
37
44
|
scope.enricher = @enricher.new(env)
|
|
@@ -52,6 +52,12 @@ module GoodSorcery
|
|
|
52
52
|
send_request(Net::HTTP::Get.new(path(VERIFY_PATH), headers))
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Sends one performance payload (DESIGN.md 8.7). The flusher decides
|
|
56
|
+
# what to do with the result and does the logging.
|
|
57
|
+
def post_apm(path_name, json)
|
|
58
|
+
send_request(post(path_name, json))
|
|
59
|
+
end
|
|
60
|
+
|
|
55
61
|
def close
|
|
56
62
|
@connection_lock.synchronize { @connection.close }
|
|
57
63
|
end
|
|
@@ -111,8 +117,13 @@ module GoodSorcery
|
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
# The persistent connection serves one thread at a time; a concurrent
|
|
114
|
-
# caller (the :sync delivery in a busy app) gets a short-lived one.
|
|
120
|
+
# caller (the :sync delivery in a busy app) gets a short-lived one. The
|
|
121
|
+
# gem's own requests are never measured by performance monitoring.
|
|
115
122
|
def perform(request)
|
|
123
|
+
Performance.suppress { perform_unmeasured(request) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def perform_unmeasured(request)
|
|
116
127
|
return Connection.new(@uri, **@timeouts).request_once(request) unless @connection_lock.try_lock
|
|
117
128
|
|
|
118
129
|
begin
|
data/lib/goodsorcery/version.rb
CHANGED
data/lib/goodsorcery.rb
CHANGED
|
@@ -51,6 +51,7 @@ require_relative "goodsorcery/delivery"
|
|
|
51
51
|
require_relative "goodsorcery/verification"
|
|
52
52
|
require_relative "goodsorcery/test_event"
|
|
53
53
|
require_relative "goodsorcery/client"
|
|
54
|
+
require_relative "goodsorcery/performance"
|
|
54
55
|
require_relative "goodsorcery/rack/middleware"
|
|
55
56
|
|
|
56
57
|
# Reports exceptions to a Good Sorcery error tracker. See README.md.
|
|
@@ -152,13 +153,29 @@ module GoodSorcery
|
|
|
152
153
|
client.test_events
|
|
153
154
|
end
|
|
154
155
|
|
|
156
|
+
# Also forgets the performance payloads kept by the :test delivery.
|
|
155
157
|
def clear_test_events
|
|
158
|
+
Performance.clear_test_payloads
|
|
156
159
|
client.clear_test_events
|
|
157
160
|
end
|
|
158
161
|
|
|
162
|
+
# Sends the recorded performance data now (with the :test delivery:
|
|
163
|
+
# builds it into test_performance_payloads). Never raises.
|
|
164
|
+
def flush_performance(include_open_minute: true)
|
|
165
|
+
Performance.flush_performance(include_open_minute: include_open_minute)
|
|
166
|
+
rescue StandardError
|
|
167
|
+
false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# [{ "path" => "/api/v1/apm/metrics", "body" => Hash }, ...] with the :test delivery.
|
|
171
|
+
def test_performance_payloads
|
|
172
|
+
Performance.test_payloads
|
|
173
|
+
end
|
|
174
|
+
|
|
159
175
|
# Forgets configuration, client and the current scope. For tests.
|
|
160
176
|
def reset!
|
|
161
177
|
shutdown(0.5)
|
|
178
|
+
Performance.reset!
|
|
162
179
|
LOCK.synchronize do
|
|
163
180
|
@client = nil
|
|
164
181
|
@configuration = nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: goodsorcery
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Q Nguyen
|
|
@@ -32,6 +32,7 @@ files:
|
|
|
32
32
|
- lib/goodsorcery/bundler_plugin.rb
|
|
33
33
|
- lib/goodsorcery/client.rb
|
|
34
34
|
- lib/goodsorcery/configuration.rb
|
|
35
|
+
- lib/goodsorcery/configuration/performance_settings.rb
|
|
35
36
|
- lib/goodsorcery/configuration/validation.rb
|
|
36
37
|
- lib/goodsorcery/defaults.rb
|
|
37
38
|
- lib/goodsorcery/delivery.rb
|
|
@@ -82,6 +83,25 @@ files:
|
|
|
82
83
|
- lib/goodsorcery/key_filter.rb
|
|
83
84
|
- lib/goodsorcery/log.rb
|
|
84
85
|
- lib/goodsorcery/masked_key.rb
|
|
86
|
+
- lib/goodsorcery/performance.rb
|
|
87
|
+
- lib/goodsorcery/performance/agent.rb
|
|
88
|
+
- lib/goodsorcery/performance/aggregator.rb
|
|
89
|
+
- lib/goodsorcery/performance/call_site.rb
|
|
90
|
+
- lib/goodsorcery/performance/flusher.rb
|
|
91
|
+
- lib/goodsorcery/performance/histogram.rb
|
|
92
|
+
- lib/goodsorcery/performance/job_timing.rb
|
|
93
|
+
- lib/goodsorcery/performance/measurement.rb
|
|
94
|
+
- lib/goodsorcery/performance/net_http.rb
|
|
95
|
+
- lib/goodsorcery/performance/payloads.rb
|
|
96
|
+
- lib/goodsorcery/performance/process_label.rb
|
|
97
|
+
- lib/goodsorcery/performance/recorder.rb
|
|
98
|
+
- lib/goodsorcery/performance/span_list.rb
|
|
99
|
+
- lib/goodsorcery/performance/sql_normalizer.rb
|
|
100
|
+
- lib/goodsorcery/performance/subscribers.rb
|
|
101
|
+
- lib/goodsorcery/performance/trace_buffer.rb
|
|
102
|
+
- lib/goodsorcery/performance/trace_builder.rb
|
|
103
|
+
- lib/goodsorcery/performance/transaction_stats.rb
|
|
104
|
+
- lib/goodsorcery/performance/uuid_v7.rb
|
|
85
105
|
- lib/goodsorcery/rack/middleware.rb
|
|
86
106
|
- lib/goodsorcery/rack/request_details.rb
|
|
87
107
|
- lib/goodsorcery/rack/request_enricher.rb
|