builder_apm 0.4.2 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,170 @@
1
+ require 'builder_apm/doctor/openai_chat_gpt'
2
+ require 'builder_apm/doctor/bravo_chat_ai'
3
+
4
+ module BuilderApm
5
+ module Doctor
6
+ class AiDoctor
7
+ def initialize
8
+ api_key = BuilderApm.configuration.api_key
9
+ case BuilderApm.configuration.api
10
+ when "OpenAi"
11
+ @ai_client = OpenAIChatGPT.new(api_key, "gpt-3.5-turbo-16k")
12
+ else
13
+ @ai_client = BravoChatAi.new(api_key)
14
+ end
15
+ @role = "You are an AI trained to analyse and optimise web requests and database queries for a Ruby on Rails api. Your task is to identify any performance issues in the given request data, suggest possible solutions and show examples, Also show which files/methods should be looked into further"
16
+ @ai_client.role(@role)
17
+ end
18
+
19
+ def diagnose(request_data)
20
+ diagnosis = if request_data["status"] == 500
21
+ diagnose_error(reduced_backtrace(request_data), backtrace_error(request_data) )
22
+ else
23
+ diagnose_backtrace(reduced_backtrace(request_data))
24
+ end
25
+ diagnosis
26
+ end
27
+
28
+ def diagnose_error(backtrace, error)
29
+ prompt = "Please review this error and stack trace for errors and provide feedback, followed by the path/filename of the rails app file causing the error and then any other files that need further investigation
30
+ eg
31
+ <h2>{the error}</h2>
32
+ <h3>Problem found:</h3>
33
+ <p>{details for the problem found}</p>
34
+
35
+ <h3>Solutions:</h3>
36
+ <p>{list of solutions}</p>
37
+
38
+ <dl><dt>Main Error File : </dt><dd class=\"main_file\"><input type=\"checkbox\" value=\"{path/filename4.ext:[line_number]}\" name=\"deeper_dive_filename\" />{path/filename.ext:line_number }</dd></dl>
39
+ <dl><dt>Addtional Files to review:{do not include the duplicate files regardless of different line number}</dt>
40
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename1.ext:[line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename1.ext:[line_number, line_number]}</dd>
41
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename2.ext:[line_number, line_number, line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename2.ext:[line_number, line_number, line_number, line_number]}</dd>
42
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename3.ext:[line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename3.ext:[line_number, line_number]}</dd>
43
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename4.ext:[line_number]}\" name=\"deeper_dive_filename\" />{path/filename4.ext:[line_number]}</dd>
44
+ </dl>
45
+
46
+ Error:
47
+ #{error}
48
+
49
+ Stack Trace:
50
+ #{backtrace}"
51
+ @ai_client.role("You are an AI trained to analyse and diagnose web requests for a Ruby on Rails api. Your task is to identify the error in the given request data, suggest possible solutions, Also show which files/methods should be looked into further")
52
+ @ai_client.chat(prompt)
53
+ end
54
+
55
+ def diagnose_backtrace(backtrace)
56
+
57
+ prompt = "Here is the request data, durations in milliseconds: \n#{backtrace}
58
+ Analyze this Stack trace and provide any potential performance issues and solutions.
59
+ The issues where more investigation into the code is required, include a list of files that are worth reviewing, unless the file path is part of gem or active_admin then skip that file and work out where in the rails app the trigger was and include that instead- path and file only (agregated, don't repeat a file even if its another line within that file).
60
+ eg
61
+ <h2>High Level Issues</h2>
62
+ <h3>{Issue type found}</h3>
63
+ <p>{times and where issue type was found}</p>
64
+ <p>{details of what this issue is and how it can effect the performance}</p>
65
+ {list of possibly solutions to fix said issue}
66
+
67
+ <h3>{Issue type found}</h3>
68
+ <p>{times and where issue type was found}</p>
69
+ <p>{details of what this issue is and how it can effect the performance}</p>
70
+ {list of possibly solutions to fix said issue}
71
+ [repeat Issue type found until no more issues type are left]
72
+
73
+
74
+ <dl><dt>Main Error File : </dt><dd class=\"main_file\"><input type=\"checkbox\" value=\"{path/filename4.ext:[line_number]}\" name=\"deeper_dive_filename\" />{path/filename.ext:line_number }</dd></dl>
75
+ <dl><dt>Addtional Files to review:{do not include the duplicate files regardless of different line number}</dt>
76
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename1.ext:[line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename1.ext:[line_number, line_number]}</dd>
77
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename2.ext:[line_number, line_number, line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename2.ext:[line_number, line_number, line_number, line_number]}</dd>
78
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename3.ext:[line_number, line_number]}\" name=\"deeper_dive_filename\" />{path/filename3.ext:[line_number, line_number]}</dd>
79
+ <dd class=\"other_file\"><input type=\"checkbox\" value=\"{path/filename4.ext:[line_number]}\" name=\"deeper_dive_filename\" />{path/filename4.ext:[line_number]}</dd>
80
+ </dl>
81
+
82
+ "
83
+
84
+ @ai_client.chat(prompt)
85
+ end
86
+
87
+ def deeper_analysis(request_data, diagnosis, files)
88
+ targeted_code = files.map do |file_n_line|
89
+ parts = file_n_line&.split(':')
90
+ next if parts&.length < 2
91
+
92
+ file = parts.first
93
+ line = parts.last.to_i
94
+ extract_lines_from_file(file, line)
95
+ end
96
+ messages = [
97
+ {
98
+ role: "user",
99
+ content: "Here is the request data, durations in milliseconds: \n#{reduced_backtrace(request_data)}"
100
+ },
101
+ {
102
+ role: "assistant",
103
+ content: diagnosis
104
+ },
105
+ {
106
+ role: "user",
107
+ content: "this is the code block mentioned in the list of files:
108
+ #{targeted_code.join("")}
109
+
110
+ #{deep_anaysis_call_to_action(request_data)}, no solution should include altering files that have a path folder called gems, As I am unable to edit those files
111
+ eg
112
+ <p>{description of what the fix is}</p>
113
+
114
+ <pre>{code fix 1}</pre>
115
+ <p>{notes / comments}</p>
116
+ [repeat for each code fix presented]"
117
+ },
118
+ ]
119
+
120
+ @ai_client.chat(messages)
121
+ end
122
+
123
+ private
124
+
125
+ def deep_anaysis_call_to_action(request_data)
126
+ return "Provide a code fix for this issue" unless request_data["status"] == 500
127
+
128
+ "Exception Error:
129
+ #{request_data["exception_message"]}
130
+
131
+ Exception Backtrace:
132
+ #{request_data["exception_backtrace"]}
133
+ Provide a code fix for this Exception Error
134
+ "
135
+ end
136
+
137
+ def extract_lines_from_file(file_path, target_line)
138
+ # debugger
139
+ return [] unless File.exist?(file_path)
140
+
141
+ start_line = [target_line - 20, 1].max
142
+ end_line = target_line + 19
143
+
144
+ extracted_lines = ["
145
+ This is the file reported as having an issue #{file_path} on line #{target_line}.\n",
146
+ "##{file_path}\n"]
147
+
148
+ File.open(file_path, 'r') do |file|
149
+ file.each_line.drop(start_line - 1).take(end_line - start_line + 1).each_with_index do |line, index|
150
+ extracted_lines << "Line #{start_line + index}: #{line}"
151
+ end
152
+ end
153
+
154
+ extracted_lines.join("")
155
+ end
156
+
157
+ def backtrace_error(request_data)
158
+ {
159
+ exception_message: request_data["exception_message"],
160
+ exception_backtrace: request_data["exception_backtrace"]
161
+ }.to_s
162
+ end
163
+
164
+ def reduced_backtrace(request_data)
165
+ reducer = BuilderApm::Doctor::BacktraceReducer.new
166
+ reducer.reduce_backtrace(data: request_data)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+ require 'digest'
3
+
4
+ module BuilderApm
5
+ module Doctor
6
+ class BacktraceReducer
7
+ TOKEN_LIMIT = 10500
8
+
9
+ def initialize
10
+ @token_count = 0
11
+ @ref_count = 0
12
+ end
13
+
14
+ def reduce_method(method)
15
+ return nil if over_limit?
16
+
17
+ reduced_method = build_reduced_method(method)
18
+ return nil if reduced_method["dur"] < 1 && reduced_method["dur"] > 0
19
+
20
+ increment_token_count(reduced_method.to_json)
21
+
22
+ process_sql_events(method, reduced_method)
23
+ process_children(method, reduced_method)
24
+
25
+ reduced_method
26
+ end
27
+
28
+ def build_reduced_method(method)
29
+ {
30
+ "dur" => method.fetch("duration", 0.0).to_f.round(3),
31
+ "method" => method.fetch("method", nil),
32
+ "line" => method.fetch("method_line", nil),
33
+ "trigger" => method.fetch("triggering_line", nil),
34
+ "sql" => [],
35
+ "children" => []
36
+ }
37
+ end
38
+
39
+ def process_sql_events(method, reduced_method)
40
+ method.fetch("sql_events", []).each do |event|
41
+ sql_event = reduce_event(event)
42
+ return if over_limit? || (sql_event["dur"] < 1 && sql_event["dur"] > 0)
43
+
44
+ reduced_method["sql"] << sql_event
45
+ increment_token_count(sql_event.to_s)
46
+ end
47
+ end
48
+
49
+ def process_children(method, reduced_method)
50
+ method.fetch("children", []).each do |child|
51
+ reduced_child = reduce_method(child)
52
+ return if over_limit?
53
+
54
+ reduced_method["children"] << reduced_child unless reduced_child.nil?
55
+ end
56
+ end
57
+
58
+ def reduce_event(event)
59
+ params = event.fetch("params", [])
60
+ params = short_hash(params) if params.size > 3
61
+ # sql = truncate_string(event.fetch("sql", ""), 400)
62
+ sql = truncate_string(event.fetch("sql", "").gsub("\"", ""), 400)
63
+
64
+ {
65
+ "dur" => event.fetch("duration", 0.0).to_f.round(3),
66
+ "sql" => sql,
67
+ "params" => params,
68
+ "records" => event.fetch("record_count", 0),
69
+ "trigger" => event.fetch("triggering_line", nil)
70
+ }
71
+ end
72
+
73
+ def reduce_json(json)
74
+ json.map { |method| reduce_method(method) }.compact
75
+ end
76
+
77
+ def short_hash(input_string)
78
+ Digest::MD5.hexdigest(input_string.join(','))[0...8]
79
+ end
80
+
81
+ def truncate_string(str, length)
82
+ str.length > length ? str[0...length] + "..." : str
83
+ end
84
+
85
+ def count_tokens(string)
86
+ tokens = string.scan(/[\w']+|[.,!?;:]/)
87
+ tokens = tokens.flat_map { |token| token.split(/(?<=[.,!?;:])$/) }
88
+ tokens.length + 17
89
+ end
90
+
91
+ def increment_token_count(string)
92
+ @token_count += count_tokens(string)
93
+ end
94
+
95
+ def over_limit?
96
+ @token_count + @ref_count > TOKEN_LIMIT
97
+ end
98
+
99
+ def reduce_backtrace(data: {})
100
+ reduce_json(data["stack"]).to_s
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,85 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module BuilderApm
6
+ module Doctor
7
+ class BravoChatAi
8
+ MAX_RETRIES = 3
9
+ TIMEOUT_IN_SECONDS = 300 # equivalent to 5 minutes
10
+
11
+ def initialize(api_key)
12
+ @api_key = api_key
13
+ @uri = URI.parse("https://models-gateway.builder.ai/api/v1/openai/deployments/gpt-35-turbo/chat/completions")
14
+ @role = "You are to be my assistant"
15
+ @temperature = 0.2
16
+ end
17
+
18
+ def role(ai_role = nil)
19
+ @role = ai_role unless ai_role.nil?
20
+
21
+ @role
22
+ end
23
+
24
+ def temperature(temp = nil)
25
+ @temperature = temp unless temp.nil?
26
+
27
+ @temperature
28
+ end
29
+
30
+ def chat(messages)
31
+ request = Net::HTTP::Post.new(@uri)
32
+ request.content_type = "application/json"
33
+ request["api-key"] = @api_key
34
+ request.body = JSON.dump({
35
+ "temperature" => temperature,
36
+ "messages" => construct_messages(messages)
37
+ })
38
+
39
+ req_options = {
40
+ use_ssl: @uri.scheme == "https",
41
+ }
42
+
43
+ begin
44
+ retries ||= 0
45
+ response = Net::HTTP.start(@uri.hostname, @uri.port, req_options) do |http|
46
+ http.read_timeout = TIMEOUT_IN_SECONDS
47
+ http.request(request)
48
+ end
49
+ rescue Net::ReadTimeout => e
50
+ if retries < MAX_RETRIES
51
+ retries += 1
52
+ retry
53
+ else
54
+ raise e
55
+ end
56
+ end
57
+
58
+ begin
59
+ JSON.parse(response.body)["choices"].first["message"]["content"]
60
+ rescue => e
61
+ error = JSON.parse(response.body)["error"]
62
+ output = error["message"]
63
+ output += "\n\nThis file is too large and should be looked at breaking it down into smaller files / services" if error["code"] == "context_length_exceeded"
64
+
65
+ output
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def construct_messages(messages)
72
+ messages_array = case messages
73
+ when String
74
+ [{"role" => "user", "content" => messages}]
75
+ when Array
76
+ messages.map { |hash| hash.transform_keys(&:to_s) }
77
+ else
78
+ raise ArgumentError, "Unsupported message format"
79
+ end
80
+
81
+ [{"role" => "system", "content" => role}] + messages_array
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,84 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module BuilderApm
6
+ module Doctor
7
+ class OpenAIChatGPT
8
+
9
+ MAX_RETRIES = 3
10
+ TIMEOUT_IN_SECONDS = 300 # equivalent to 5 minutes
11
+
12
+ def initialize(api_key, model = "gpt-3.5-turbo")
13
+ @api_key = api_key
14
+ @uri = URI.parse("https://api.openai.com/v1/chat/completions")
15
+ @role = "You are to be my assistant"
16
+ @model = model
17
+ @temperature = 0.2
18
+ end
19
+
20
+ def role(ai_role = nil)
21
+ @role = ai_role unless ai_role.nil?
22
+
23
+ @role
24
+ end
25
+
26
+ def temperature(temp = nil)
27
+ @temperature = temp unless temp.nil?
28
+
29
+ @temperature
30
+ end
31
+
32
+ def chat(messages)
33
+ request = Net::HTTP::Post.new(@uri)
34
+ request.content_type = "application/json"
35
+ request["Authorization"] = "Bearer #{@api_key}"
36
+
37
+ request.body = JSON.dump({
38
+ "model" => @model,
39
+ "temperature" => temperature,
40
+ "messages" => construct_messages(messages)
41
+ })
42
+
43
+ req_options = {
44
+ use_ssl: @uri.scheme == "https",
45
+ }
46
+
47
+ begin
48
+ retries ||= 0
49
+ response = Net::HTTP.start(@uri.hostname, @uri.port, req_options) do |http|
50
+ http.read_timeout = TIMEOUT_IN_SECONDS
51
+ http.request(request)
52
+ end
53
+ rescue Net::ReadTimeout => e
54
+ if retries < MAX_RETRIES
55
+ retries += 1
56
+ retry
57
+ else
58
+ raise e
59
+ end
60
+ end
61
+
62
+ data = JSON.parse(response.body)["choices"]
63
+
64
+ raise data.dig("error", "message") if data.is_a?(Hash) && data.dig("error", "message")
65
+
66
+ data.dig(0, "message", "content")
67
+ end
68
+
69
+ private
70
+ def construct_messages(messages)
71
+ messages_array = case messages
72
+ when String
73
+ [{"role" => "user", "content" => messages}]
74
+ when Array
75
+ messages.map { |hash| hash.transform_keys(&:to_s) }
76
+ else
77
+ raise ArgumentError, "Unsupported message format"
78
+ end
79
+
80
+ [{"role" => "system", "content" => role}] + messages_array
81
+ end
82
+ end
83
+ end
84
+ end
@@ -2,6 +2,7 @@ module BuilderApm
2
2
  module Methods
3
3
  class Instrumenter
4
4
  def initialize(root_path: Rails.root.to_s)
5
+ @this_gem_path = File.expand_path("../../..", __dir__)
5
6
  @root_path = root_path
6
7
  @call_times = {}
7
8
  end
@@ -17,28 +18,54 @@ module BuilderApm
17
18
 
18
19
  def setup_trace
19
20
  me = self
20
- TracePoint.new(:call, :return) do |tp|
21
+ TracePoint.new(:call, :return, :end, :raise) do |tp|
22
+ starttime = Time.now.to_f * 1000
21
23
  me.process_trace_point(tp) if me.valid_trace_point?(tp)
24
+ duration = (Time.now.to_f * 1000) - starttime
25
+
26
+ Thread.current["method_tracing"] = (Thread.current["method_tracing"] ||= 0) + duration
22
27
  end
23
28
  end
24
29
 
25
30
  def valid_trace_point?(tp)
26
- !Thread.current[:request_id].nil? && tp.path.start_with?(@root_path) && !tp.path.include?('controller.rb')
31
+ return !Thread.current[:request_id].nil? && tp.path.start_with?(@root_path)
32
+
33
+ # !Thread.current[:request_id].nil? && not_controller_endpoint(tp) && valid_gem_path(tp)
34
+ end
35
+
36
+ def not_controller_endpoint(tp)
37
+ start_controller = Thread.current[:stack]&.first || nil
38
+
39
+ return false unless start_controller
40
+
41
+ "#{tp.defined_class}##{tp.method_id}" != start_controller[:method]
42
+ end
43
+
44
+ def valid_gem_path(tp)
45
+ not_this_gem = !tp.path.start_with?(@this_gem_path)
46
+ is_a_tracked_gem = tp.path.split(File::SEPARATOR).any? { |folder| folder.start_with?(*gems_to_track) }
47
+ is_a_rails_app_file = tp.path.start_with?(@root_path)
48
+
49
+ not_this_gem && (is_a_tracked_gem || is_a_rails_app_file)
27
50
  end
28
51
 
29
52
  def process_trace_point(tp)
30
- if tp.event == :call
53
+ if tp.event == :call || tp.event == :b_call || tp.event == :c_call
31
54
  process_call_event(tp)
32
- elsif tp.event == :return
55
+ else
33
56
  process_return_event(tp)
34
57
  end
35
58
  end
36
59
 
37
60
  private
38
61
 
62
+ def gems_to_track
63
+ @gems_to_track = BuilderApm.configuration.gems_to_track
64
+ end
65
+
39
66
  def process_call_event(tp)
40
67
  method_id = "#{tp.defined_class}##{tp.method_id}"
41
- @call_times[method_id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
68
+ (@call_times[method_id]||= []) << Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
69
  caller_info = caller_locations(4,1).first
43
70
  calling_file_path = caller_info.absolute_path
44
71
  calling_line_number = caller_info.lineno
@@ -59,7 +86,7 @@ module BuilderApm
59
86
  method_id = "#{tp.defined_class}##{tp.method_id}"
60
87
 
61
88
  if @call_times.key?(method_id)
62
- elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @call_times[method_id]
89
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @call_times[method_id].pop
63
90
  elapsed_time_in_ms = (elapsed_time * 1000).round(3)
64
91
  @call_times.delete(method_id)
65
92
 
@@ -12,6 +12,8 @@ module BuilderApm
12
12
  Thread.current[:n_plus_one_duration] = 0
13
13
  Thread.current[:has_n_plus_one] = false
14
14
  Thread.current[:db_runtime] = 0
15
+ Thread.current[:method_tracing] = 0
16
+ Thread.current[:db_tracing] = 0
15
17
  start_time = Time.now.to_f * 1000
16
18
 
17
19
  @status, @headers, @response = @app.call(env)
@@ -36,6 +38,8 @@ module BuilderApm
36
38
  Thread.current[:db_runtime] = nil
37
39
  Thread.current[:stack] = nil
38
40
  Thread.current[:sql_event_id] = nil
41
+ Thread.current[:method_tracing] = nil
42
+ Thread.current[:db_tracing] = nil
39
43
  end
40
44
 
41
45
  def handle_timing(start_time, end_time, request_id)
@@ -53,6 +57,8 @@ module BuilderApm
53
57
  data[:stack][0][:start_time] = start_time
54
58
  data[:stack][0][:end_time] = end_time
55
59
  data[:stack][0][:duration] = end_time - start_time
60
+ data[:method_tracing] = Thread.current[:method_tracing]
61
+ data[:db_tracing] = Thread.current[:db_tracing]
56
62
 
57
63
  save_to_redis(data)
58
64
  end
@@ -6,8 +6,20 @@ module BuilderApm
6
6
  end
7
7
 
8
8
  def subscribe_to_notifications
9
- ActiveSupport::Notifications.subscribe('sql.active_record') { |*args| handle_sql_active_record(*args) }
10
- ActiveSupport::Notifications.subscribe('instantiation.active_record') { |*args| handle_instantiation_active_record(*args) }
9
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
10
+ starttime = Time.now.to_f * 1000
11
+ handle_sql_active_record(*args)
12
+ duration = (Time.now.to_f * 1000) - starttime
13
+
14
+ Thread.current[:db_tracing] = (Thread.current[:db_tracing] ||= 0) + duration
15
+ end
16
+ ActiveSupport::Notifications.subscribe('instantiation.active_record') do |*args|
17
+ starttime = Time.now.to_f * 1000
18
+ handle_instantiation_active_record(*args)
19
+ duration = (Time.now.to_f * 1000) - starttime
20
+
21
+ Thread.current[:db_tracing] = (Thread.current[:db_tracing] ||= 0) + duration
22
+ end
11
23
  end
12
24
 
13
25
  private
@@ -111,7 +123,7 @@ module BuilderApm
111
123
 
112
124
  # If any N+1 issue is found, set 'has_n_plus_one' to true and return
113
125
  queries_count.each do |_, value|
114
- if value[:count] > 1
126
+ if value[:count] > 2
115
127
  Thread.current[:has_n_plus_one] = true
116
128
  return
117
129
  end
@@ -1,3 +1,3 @@
1
1
  module BuilderApm
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -1,6 +1,9 @@
1
1
  BuilderApm.configure do |config|
2
- # config.redis_url = 'redis://localhost:6379/0'
3
- # config.enable_controller_profiler = true
4
- # config.enable_active_record_profiler = true
5
- # config.enable_methods_profiler = true
2
+ config.redis_url = 'redis://localhost:6379/0'
3
+ config.enable_controller_profiler = true
4
+ config.enable_active_record_profiler = true
5
+ config.enable_methods_profiler = true
6
+ # config.api_key = ENV["OPENAI_API_KEY"]
7
+ # config.api = ENV["API_AI"] # "Bravo" - internal Ai | "OpenAi" - use openai ChatGPT
8
+ config.gems_to_track = ["bx_block_"] #partial and full gem names can be used.
6
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: builder_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Ketelle
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-28 00:00:00.000000000 Z
11
+ date: 2023-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -55,10 +55,10 @@ files:
55
55
  - ".rspec"
56
56
  - Gemfile
57
57
  - Gemfile.lock
58
- - README.md
59
58
  - Rakefile
60
59
  - app/controllers/builder_apm/application_controller.rb
61
60
  - app/controllers/builder_apm/dashboard_controller.rb
61
+ - app/controllers/builder_apm/diagnose_request_controller.rb
62
62
  - app/controllers/builder_apm/error_requests_controller.rb
63
63
  - app/controllers/builder_apm/n_plus_one_controller.rb
64
64
  - app/controllers/builder_apm/recent_requests_controller.rb
@@ -96,6 +96,10 @@ files:
96
96
  - lib/builder_apm.rb
97
97
  - lib/builder_apm/configuration.rb
98
98
  - lib/builder_apm/controllers/instrumenter.rb
99
+ - lib/builder_apm/doctor/ai_doctor.rb
100
+ - lib/builder_apm/doctor/backtrace_reducer.rb
101
+ - lib/builder_apm/doctor/bravo_chat_ai.rb
102
+ - lib/builder_apm/doctor/openai_chat_gpt.rb
99
103
  - lib/builder_apm/engine.rb
100
104
  - lib/builder_apm/methods/instrumenter.rb
101
105
  - lib/builder_apm/middleware/timing.rb
@@ -126,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
130
  - !ruby/object:Gem::Version
127
131
  version: '0'
128
132
  requirements: []
129
- rubygems_version: 3.0.9
133
+ rubygems_version: 3.1.0
130
134
  signing_key:
131
135
  specification_version: 4
132
136
  summary: Write a short summary, because RubyGems requires one.
data/README.md DELETED
@@ -1,29 +0,0 @@
1
- # BuilderApm
2
-
3
-
4
-
5
- ## Installation
6
-
7
- Add this line to your application's Gemfile:
8
-
9
- ```ruby
10
- gem 'builder_apm'
11
- ```
12
-
13
- And then execute:
14
-
15
- $ bundle install
16
-
17
- Or install it yourself as:
18
-
19
- $ gem install builder_apm
20
-
21
- run
22
- ```
23
- rails generate builder_apm:install
24
- ```
25
-
26
- ## Usage
27
-
28
- TODO: Write usage instructions here
29
-