callstacking-rails 0.1.29 → 0.1.31

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a645231d70a30c9296b1ffbd7f99ce8295f571bd7641b6389354f4fbd43ff153
4
- data.tar.gz: 50d96385483deacfa0856b4af8cc8e59450d521d3237310735fa759de37f3bcb
3
+ metadata.gz: 26c64da0b1b2bb170e40ee319c04d9d8b090e4801d4aa61ecdb1728e4edb0a10
4
+ data.tar.gz: 619d6983c3aa94affb3d31c7c0e4a8532565c219bd9d14832a50b9fb46f813b7
5
5
  SHA512:
6
- metadata.gz: 0f7dddf5d3c1f944ce982d2ce5a803cc984dbcf757b29856f289039df9aa09608749c7998f567ca3da6ce0b8edd32289894cc5edd2f1afd8090b6c3416cb4a79
7
- data.tar.gz: 8c692f7133164c596e0a39550867a0ee882a882e719c449564c16619fb485a522fbd77150a74ec86ff00b04b3a2ed946f57dcdd7f6385abcb59840e229ecbf63
6
+ metadata.gz: fa3fc9c4d8e079df650a5efefe6704d6682f770ebd35516ff501ddd3c896f3df69e871f7d7908855d6074c0536cff05346d31a7c74024884633c685ad3bc6909
7
+ data.tar.gz: 0c253b57e8ff3044e17df80fc9bab0b3cc6cb49efc5aeba2d6900b8b27857fde19cb06fbdce5fae05da70371f6922ce7c6d4fc7419cc824c099f10df75aad7aa
data/README.md CHANGED
@@ -45,25 +45,18 @@ $ bundle
45
45
  ```
46
46
 
47
47
  ## CLI Setup
48
- Register an account at callstacking.com:
49
48
 
50
- > callstacking-rails register
49
+ *Step 1:*
50
+ > bundle exec callstacking-rails register
51
+
52
+ The above command will open a browser window and allow you to register an account at callstacking.com.
53
+
54
+ *Step 2:*
55
+ > bundle exec callstacking-rails setup
51
56
 
52
- Opens a browser window to register as a callstacking.com user.
57
+ This interactively prompts you for your callstacking.com username/password.
53
58
 
54
- > callstacking-rails setup
55
-
56
- Interactively prompts you for your callstacking.com username/password.
57
- Stores auth details in `~/.callstacking`.
58
-
59
- You can have multiple environments.
60
- The default is `development`.
61
-
62
- The `development:` section in the `~/.callstacking` config contains your credentials.
63
-
64
- By setting the RAILS_ENV environment you can maintain multiple settings.
65
-
66
- Questions? Create an issue: https://github.com/callstacking/callstacking-rails/issues
59
+ The auth details are stored in `~/.callstacking`.
67
60
 
68
61
  ## Enabling Tracing
69
62
 
@@ -78,7 +71,7 @@ and for a `debug` param to be set to `1`:
78
71
  class ApplicationController < ActionController::Base
79
72
  include Callstacking::Rails::Helpers::InstrumentHelper
80
73
 
81
- around_action :callstacking_setup, if: -> { current_user&.admin? && params[:debug] == '1' }
74
+ prepend_around_action :callstacking_setup, if: -> { current_user&.admin? && params[:debug] == '1' }
82
75
  ```
83
76
 
84
77
  For the above setup, you would you have to be authenticated as an admin and would append `?debug=1`
@@ -91,6 +84,8 @@ e.g.
91
84
 
92
85
  The local Rails server log outputs the trace URL.
93
86
 
87
+ <img width="1141" alt="screenshot of trace url output in Rails logs" src="https://user-images.githubusercontent.com/4600/236599713-49a82a83-d8c0-4e51-9442-5da1fbca7f9c.png">
88
+
94
89
  ## Production Environment
95
90
 
96
91
  For production, you can provide the auth token via the `CALLSTACKING_API_TOKEN` environment variable.
@@ -103,14 +98,20 @@ The traces are recorded at https://callstacking.com/traces
103
98
 
104
99
  For local HTML requests, once your page has rendered, you will see a `💥` icon on the right hand side.
105
100
 
106
- Click the icon and observe the full callstack context.
101
+ Click the icon and observe the trace.
102
+
103
+ ### Headless API requests
107
104
 
108
- For headless API requests, visit https://callstacking.com/traces to view your traces.
105
+ The trace URL is output via the Rails logs. https://callstacking.com/traces will updated with your latest trace.
109
106
 
110
107
  ## Tests
111
108
  ``
112
109
  rake app:test:all
113
110
  ``
114
111
 
112
+ ## Questions/Bugs/Feature Requests
113
+
114
+ Create an issue: https://github.com/callstacking/callstacking-rails/issues
115
+
115
116
  ## License
116
- The gem is available as open source under the terms of the [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
117
+ The license can be viewed at https://github.com/callstacking/callstacking-rails/blob/main/LICENSE
@@ -21,7 +21,7 @@ module Callstacking
21
21
  # https://github.com/lostisland/awesome-faraday
22
22
  @connection ||= Faraday.new(url) do |c|
23
23
  c.response :json
24
- c.use Faraday::Response::Logger, Logger.new('/tmp/callstacking-rails.log')
24
+ c.use Faraday::Response::Logger
25
25
  # c.use Faraday::Response::Logger, nil, { headers: false, bodies: false }
26
26
  c.response :follow_redirects
27
27
  c.use Faraday::Response::RaiseError # raise exceptions on 40x, 50x responses
@@ -35,7 +35,7 @@ module Callstacking
35
35
  end
36
36
  end
37
37
 
38
- def get(url, params = {})
38
+ def get(url, params = {}, headers = {})
39
39
  if async
40
40
  threads << Thread.new do
41
41
  connection.get(url, params, headers)
@@ -6,6 +6,7 @@ module Callstacking
6
6
  class Trace < Base
7
7
  CREATE_URL = "/api/v1/traces.json"
8
8
  UPDATE_URL = "/api/v1/traces/:id.json"
9
+ SHOW_URL = "/api/v1/traces/:id.json"
9
10
 
10
11
  def initialize(url, auth_token)
11
12
  super
@@ -37,6 +38,10 @@ module Callstacking
37
38
  def upsert(trace_id, traces)
38
39
  patch(UPDATE_URL.gsub(':id', trace_id), {}, traces)
39
40
  end
41
+
42
+ def show(trace_id, params = {})
43
+ get(SHOW_URL.gsub(':id', trace_id), params)
44
+ end
40
45
  end
41
46
  end
42
47
  end
@@ -13,45 +13,83 @@ require "callstacking/rails/client/trace"
13
13
  require "callstacking/rails/cli"
14
14
  require "callstacking/rails/time_based_uuid"
15
15
  require "callstacking/rails/helpers/instrument_helper"
16
+ require "callstacking/rails/logger"
16
17
 
17
18
  module Callstacking
18
19
  module Rails
19
20
  class Engine < ::Rails::Engine
20
- EXCLUDED_TEST_CLASSES = %w[test/dummy/app/models/salutation.rb test/dummy/app/controllers/application_controller.rb].freeze
21
+ EXCLUDED_TEST_CLASSES = %w[test/dummy/app/models/salutation.rb
22
+ test/dummy/app/controllers/application_controller.rb].freeze
21
23
 
22
- cattr_accessor :spans, :trace, :settings, :instrumenter, :loader
24
+ cattr_accessor :spans, :traces, :settings, :instrumenter, :loader, :lock
23
25
 
24
26
  isolate_namespace Callstacking::Rails
25
27
 
28
+ @@spans||={}
29
+ @@traces||={}
30
+ @@lock||=Mutex.new
31
+ @@instrumenter||=Instrument.new
26
32
  @@settings||=Callstacking::Rails::Settings.new
27
- @@spans||=Spans.new
28
- @@trace||=Trace.new(@@spans)
29
- @@instrumenter||=Instrument.new(@@spans)
30
33
 
31
34
  initializer "engine_name.assets.precompile" do |app|
32
35
  app.config.assets.precompile << "checkpoint_rails_manifest.js"
33
36
  end
34
37
 
35
38
  config.after_initialize do
36
- puts "Call Stacking loading (#{Callstacking::Rails::Env.environment})"
37
-
38
- @@loader = Callstacking::Rails::Loader.new(@@instrumenter, excluded: @@settings.excluded + EXCLUDED_TEST_CLASSES)
39
- @@loader.on_load
39
+ Logger.log "Call Stacking loading (#{Callstacking::Rails::Env.environment})"
40
+
41
+ spans[Thread.current.object_id]||=Spans.new
42
+ instrumenter.add_span(spans[Thread.current.object_id])
43
+
44
+ @@loader = Callstacking::Rails::Loader.new(instrumenter, excluded: settings.excluded + EXCLUDED_TEST_CLASSES)
45
+ @@loader.instrument_existing
46
+
47
+ loader.on_load
48
+ # loader.reset!
40
49
  end
41
50
 
51
+ # Serialize all tracing requests for now.
52
+ # Can enable parallel tracing later.
42
53
  def self.start_tracing(controller)
43
- @@settings.enable!
44
- @@instrumenter.enable!(@@loader.klasses.to_a)
45
- @@trace.begin_trace(controller)
54
+ Logger.log("Callstacking::Rails::Engine.start_tracing")
55
+
56
+ settings.enable!
57
+
58
+ lock.synchronize do
59
+ spans[Thread.current.object_id]||=Spans.new
60
+ span = spans[Thread.current.object_id]
61
+
62
+ instrumenter.add_span(span)
63
+
64
+ if instrumenter.instrumentation_required?
65
+ loader.reset!
66
+ instrumenter.enable!(loader.klasses.to_a)
67
+ end
68
+
69
+ traces[Thread.current.object_id] = Trace.new(span)
70
+ trace = traces[Thread.current.object_id]
71
+
72
+ trace.begin_trace(controller)
73
+ end
46
74
 
47
75
  true
48
76
  end
49
77
 
50
78
  def self.stop_tracing(controller)
51
- @@settings.disable!
52
- @@instrumenter.disable!
53
- @@trace.end_trace(controller)
79
+ Logger.log("Callstacking::Rails::Engine.stop_tracing")
80
+
81
+ settings.disable!
82
+
83
+ trace = nil
84
+ lock.synchronize do
85
+ trace = traces.delete(Thread.current.object_id)
86
+ if traces.empty?
87
+ instrumenter.disable!
88
+ end
89
+ end
54
90
 
91
+ trace&.end_trace(controller)
92
+
55
93
  true
56
94
  end
57
95
  end
@@ -7,8 +7,8 @@ module Callstacking
7
7
  attr_accessor :spans
8
8
  attr_reader :settings, :span_modules
9
9
 
10
- def initialize(spans)
11
- @spans = spans
10
+ def initialize
11
+ @spans = {}
12
12
  @span_modules = Set.new
13
13
  @settings = Callstacking::Rails::Settings.new
14
14
  end
@@ -34,6 +34,9 @@ module Callstacking
34
34
  new_method = nil
35
35
  if RUBY_VERSION < "2.7.8"
36
36
  new_method = tmp_module.define_method(method_name) do |*args, &block|
37
+ settings = tmp_module.instance_variable_get(:@settings)
38
+ return super(*args, &block) if settings.disabled?
39
+
37
40
  method_name = __method__
38
41
 
39
42
  path = method(__method__).super_method.source_location.first
@@ -42,19 +45,23 @@ module Callstacking
42
45
  p, l = caller.find { |c| c.to_s =~ /#{::Rails.root.to_s}/}&.split(':')
43
46
 
44
47
  spans = tmp_module.instance_variable_get(:@spans)
48
+ span = spans[Thread.current.object_id]
45
49
  klass = tmp_module.instance_variable_get(:@klass)
46
50
 
47
51
  arguments = Callstacking::Rails::Instrument.arguments_for(method(__method__).super_method, args)
48
52
 
49
- spans.call_entry(klass, method_name, arguments, p || path, l || line_no)
53
+ span.call_entry(klass, method_name, arguments, p || path, l || line_no)
50
54
  return_val = super(*args, &block)
51
- spans.call_return(klass, method_name, p || path, l || line_no, return_val)
55
+ span.call_return(klass, method_name, p || path, l || line_no, return_val)
52
56
 
53
57
  return_val
54
58
  end
55
59
  new_method.ruby2_keywords if new_method.respond_to?(:ruby2_keywords)
56
60
  else
57
61
  new_method = tmp_module.define_method(method_name) do |*args, **kwargs, &block|
62
+ settings = tmp_module.instance_variable_get(:@settings)
63
+ return super(*args, **kwargs, &block) if settings.disabled?
64
+
58
65
  method_name = __method__
59
66
 
60
67
  path = method(__method__).super_method.source_location.first
@@ -63,13 +70,14 @@ module Callstacking
63
70
  p, l = caller.find { |c| c.to_s =~ /#{::Rails.root.to_s}/}&.split(':')
64
71
 
65
72
  spans = tmp_module.instance_variable_get(:@spans)
73
+ span = spans[Thread.current.object_id]
66
74
  klass = tmp_module.instance_variable_get(:@klass)
67
75
 
68
76
  arguments = Callstacking::Rails::Instrument.arguments_for(method(__method__).super_method, args)
69
77
 
70
- spans.call_entry(klass, method_name, arguments, p || path, l || line_no)
78
+ span.call_entry(klass, method_name, arguments, p || path, l || line_no)
71
79
  return_val = super(*args, **kwargs, &block)
72
- spans.call_return(klass, method_name, p || path, l || line_no, return_val)
80
+ span.call_return(klass, method_name, p || path, l || line_no, return_val)
73
81
 
74
82
  return_val
75
83
  end
@@ -95,6 +103,10 @@ module Callstacking
95
103
  reset!
96
104
  end
97
105
 
106
+ def instrumentation_required?
107
+ span_modules.empty?
108
+ end
109
+
98
110
  def reset!
99
111
  span_modules.clear
100
112
  end
@@ -118,6 +130,10 @@ module Callstacking
118
130
  f.filter h
119
131
  end
120
132
 
133
+ def add_span(span)
134
+ spans[Thread.current.object_id] ||= span
135
+ end
136
+
121
137
  private
122
138
  def find_or_initialize_module(klass)
123
139
  name = klass&.name rescue nil
@@ -127,11 +143,15 @@ module Callstacking
127
143
  module_index = klass.ancestors.map(&:to_s).index(module_name)
128
144
 
129
145
  unless module_index
130
- new_module = Object.const_set(module_name, Module.new)
146
+ # Development class reload -
147
+ # ancestors are reset but module definition remains
148
+ new_module = Object.const_get(module_name) rescue nil
149
+ new_module||=Object.const_set(module_name, Module.new)
131
150
  span_modules << new_module
132
151
 
133
152
  new_module.instance_variable_set("@klass", klass)
134
153
  new_module.instance_variable_set("@spans", spans)
154
+ new_module.instance_variable_set("@settings", settings)
135
155
 
136
156
  klass.prepend new_module
137
157
  klass.singleton_class.prepend new_module if klass.class == Module
@@ -1,4 +1,5 @@
1
1
  require "rails"
2
+ require "callstacking/rails/logger"
2
3
 
3
4
  module Callstacking
4
5
  module Rails
@@ -10,25 +11,40 @@ module Callstacking
10
11
  @klasses = Set.new
11
12
  end
12
13
 
14
+ def instrument_existing
15
+ ObjectSpace.each_object(Module){|ob| filter_klass(ob, (Object.const_source_location(ob.to_s)&.first rescue nil))}
16
+ end
17
+
13
18
  def on_load
14
19
  trace = TracePoint.new(:end) do |tp|
15
20
  klass = tp.self
16
21
  path = tp.path
17
22
 
18
- excluded_klass = excluded.any? { |ex| path =~ /#{ex}/ }
19
-
20
- if path =~ /#{::Rails.root.to_s}/ &&
21
- !klasses.include?(klass) &&
22
- !excluded_klass
23
- klasses << klass
24
- end
23
+ filter_klass(klass, path)
25
24
  end
26
25
 
27
26
  trace.enable
27
+ end
28
28
 
29
+ def reset!
29
30
  instrumenter.instrument_method(ActionView::PartialRenderer, :render, application_level: false)
30
31
  instrumenter.instrument_method(ActionView::TemplateRenderer, :render, application_level: false)
31
32
  end
33
+
34
+ private
35
+ def filter_klass(klass, path)
36
+ return if klass.nil? || path.nil?
37
+ return if path == false
38
+
39
+ excluded_klass = excluded.any? { |ex| path =~ /#{ex}/ }
40
+
41
+ if path =~ /#{::Rails.root.to_s}/ &&
42
+ !klasses.include?(klass) &&
43
+ !excluded_klass
44
+ instrumenter.instrument_klass(klass)
45
+ klasses << klass
46
+ end
47
+ end
32
48
  end
33
49
  end
34
50
  end
@@ -0,0 +1,18 @@
1
+ module Callstacking
2
+ module Rails
3
+ class Logger
4
+ def self.log(message)
5
+ puts message
6
+
7
+ if ENV['GITHUB_OUTPUT'].present?
8
+ File.open(ENV['GITHUB_OUTPUT'], 'a') do |file|
9
+ # Write your progress output to the file
10
+ # This could be inside a loop or condition, depending on your needs
11
+ file.puts "::set-output name=progress_output::#{message}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -23,7 +23,9 @@ module Callstacking
23
23
  end
24
24
 
25
25
  def auth_token
26
- ENV['CALLSTACKING_API_TOKEN'] || settings[:auth_token]
26
+ x = ENV['CALLSTACKING_API_TOKEN'] || settings[:auth_token]
27
+ raise "No auth token found. #{ENV['CALLSTACKING_API_TOKEN']} Please run `callstacking login` to get one." if x.nil?
28
+ x
27
29
  end
28
30
 
29
31
  def auth_token?
@@ -35,14 +37,14 @@ module Callstacking
35
37
  end
36
38
 
37
39
  def self.enable!
38
- ::Rails.cache.write(CACHE_KEY, true)
40
+ Thread.current[CACHE_KEY] = true
39
41
  end
40
42
  def enable!
41
43
  self.class.enable!
42
44
  end
43
45
 
44
46
  def self.disable!
45
- ::Rails.cache.write(CACHE_KEY, false)
47
+ Thread.current[CACHE_KEY] = false
46
48
  end
47
49
 
48
50
  def disable!
@@ -50,11 +52,9 @@ module Callstacking
50
52
  end
51
53
 
52
54
  def enabled?
53
- return ::Rails.cache.read(CACHE_KEY) unless ::Rails.cache.read(CACHE_KEY).nil?
54
- return false if ENV[ENV_KEY] == 'false'
55
- return false if settings.nil?
56
-
57
- settings[:enabled]
55
+ return Thread.current[CACHE_KEY] if Thread.current[CACHE_KEY].present?
56
+ return ActiveRecord::Type::Boolean.new.cast(ENV[ENV_KEY]) if ENV[ENV_KEY].present?
57
+ false
58
58
  end
59
59
 
60
60
  def excluded
@@ -7,18 +7,22 @@ module Callstacking
7
7
  class Trace
8
8
  include Callstacking::Rails::Helpers::HeadsUpDisplayHelper
9
9
 
10
- attr_accessor :spans, :client, :lock
10
+ attr_accessor :spans, :client, :lock, :traces
11
11
  attr_reader :settings
12
12
  cattr_accessor :current_trace_id
13
13
  cattr_accessor :current_tuid
14
+ cattr_accessor :trace_log
14
15
 
15
16
  ICON = '💥'
16
17
  MAX_TRACE_ENTRIES = 3000
18
+
19
+ @@trace_log||={}
17
20
 
18
21
  def initialize(spans)
19
- @traces = []
20
- @spans = spans
21
- @settings = Callstacking::Rails::Settings.new
22
+
23
+ @traces = []
24
+ @spans = spans
25
+ @settings = Callstacking::Rails::Settings.new
22
26
 
23
27
  @lock = Mutex.new
24
28
  @client = Callstacking::Rails::Client::Trace.new(settings.url, settings.auth_token)
@@ -29,16 +33,20 @@ module Callstacking
29
33
 
30
34
  def begin_trace(controller)
31
35
  @trace_id, @tuid = init_uuids(controller.request&.request_id || SecureRandom.uuid, TimeBasedUUID.generate)
36
+ trace_log[@trace_id] = controller.request&.original_url
37
+
32
38
  init_callbacks(@tuid)
33
39
 
34
40
  start_request(@trace_id, @tuid,
35
41
  controller.action_name, controller.controller_name,
36
42
  controller.action_name, controller.request.format, ::Rails.root.to_s,
37
43
  controller.request&.original_url,
38
- controller.request.headers, controller.request.params)
44
+ controller.request.headers, controller.request.params, @traces)
39
45
  end
40
46
 
41
47
  def end_trace(controller)
48
+ return if @trace_id.nil? || @tuid.nil?
49
+
42
50
  complete_request(@trace_id, @tuid,
43
51
  controller.action_name, controller.controller_name,
44
52
  controller.action_name, controller.request.format,
@@ -48,6 +56,10 @@ module Callstacking
48
56
  inject_hud(@settings, controller.request, controller.response)
49
57
  end
50
58
 
59
+ def self.trace_log_clear
60
+ trace_log.clear
61
+ end
62
+
51
63
  private
52
64
 
53
65
  def init_callbacks(tuid)
@@ -136,12 +148,18 @@ module Callstacking
136
148
  lock.synchronize do
137
149
  return if traces.empty?
138
150
 
151
+ STDERR.puts "Sending #{traces.size} traces to Callstacking.io -- enabled? - #{settings.enabled?} -- #{traces.inspect}"
152
+
139
153
  client.upsert(trace_id,
140
154
  { trace_entries: traces.deep_dup })
141
155
  traces.clear
142
156
  end
143
157
  end
144
- def start_request(trace_id, tuid, method, controller, action, format, path, original_url, headers, params)
158
+ def start_request(trace_id, tuid, method, controller, action, format, path, original_url, headers, params, traces)
159
+ lock.synchronize do
160
+ traces.clear
161
+ end
162
+
145
163
  return if do_not_track_request?(original_url, format)
146
164
 
147
165
  client.create(trace_id, tuid,
@@ -167,10 +185,7 @@ module Callstacking
167
185
  end
168
186
 
169
187
  def complete_request(trace_id, tuid, method, controller, action, format, original_url, traces, max_trace_entries)
170
- if do_not_track_request?(original_url, format)
171
- traces.clear
172
- return
173
- end
188
+ return if do_not_track_request?(original_url, format)
174
189
 
175
190
  create_message(tuid, completed_request_message(method, controller, action, format),
176
191
  spans.increment_order_num, traces)
@@ -1,5 +1,5 @@
1
1
  module Callstacking
2
2
  module Rails
3
- VERSION = "0.1.29"
3
+ VERSION = "0.1.31"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: callstacking-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.29
4
+ version: 0.1.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Jones
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-06 00:00:00.000000000 Z
11
+ date: 2023-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -112,6 +112,7 @@ files:
112
112
  - lib/callstacking/rails/helpers/instrument_helper.rb
113
113
  - lib/callstacking/rails/instrument.rb
114
114
  - lib/callstacking/rails/loader.rb
115
+ - lib/callstacking/rails/logger.rb
115
116
  - lib/callstacking/rails/settings.rb
116
117
  - lib/callstacking/rails/setup.rb
117
118
  - lib/callstacking/rails/spans.rb