goodsorcery 0.1.0 → 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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +37 -2
  4. data/lib/generators/goodsorcery/install/templates/initializer.rb +7 -0
  5. data/lib/goodsorcery/client.rb +15 -1
  6. data/lib/goodsorcery/configuration/performance_settings.rb +37 -0
  7. data/lib/goodsorcery/configuration/validation.rb +10 -0
  8. data/lib/goodsorcery/configuration.rb +3 -0
  9. data/lib/goodsorcery/defaults.rb +6 -0
  10. data/lib/goodsorcery/integrations/active_job.rb +4 -1
  11. data/lib/goodsorcery/integrations/rails.rb +3 -2
  12. data/lib/goodsorcery/integrations/sidekiq.rb +18 -2
  13. data/lib/goodsorcery/performance/agent.rb +104 -0
  14. data/lib/goodsorcery/performance/aggregator.rb +63 -0
  15. data/lib/goodsorcery/performance/call_site.rb +61 -0
  16. data/lib/goodsorcery/performance/flusher.rb +224 -0
  17. data/lib/goodsorcery/performance/histogram.rb +23 -0
  18. data/lib/goodsorcery/performance/job_timing.rb +106 -0
  19. data/lib/goodsorcery/performance/measurement.rb +117 -0
  20. data/lib/goodsorcery/performance/net_http.rb +37 -0
  21. data/lib/goodsorcery/performance/payloads.rb +110 -0
  22. data/lib/goodsorcery/performance/process_label.rb +22 -0
  23. data/lib/goodsorcery/performance/recorder.rb +202 -0
  24. data/lib/goodsorcery/performance/span_list.rb +156 -0
  25. data/lib/goodsorcery/performance/sql_normalizer.rb +149 -0
  26. data/lib/goodsorcery/performance/subscribers.rb +140 -0
  27. data/lib/goodsorcery/performance/trace_buffer.rb +78 -0
  28. data/lib/goodsorcery/performance/trace_builder.rb +121 -0
  29. data/lib/goodsorcery/performance/transaction_stats.rb +93 -0
  30. data/lib/goodsorcery/performance/uuid_v7.rb +27 -0
  31. data/lib/goodsorcery/performance.rb +158 -0
  32. data/lib/goodsorcery/rack/middleware.rb +10 -3
  33. data/lib/goodsorcery/transport.rb +12 -1
  34. data/lib/goodsorcery/version.rb +1 -1
  35. data/lib/goodsorcery.rb +17 -0
  36. 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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GoodSorcery
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  NOTIFIER_NAME = "goodsorcery-ruby"
6
6
  end
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.1.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