builder_apm 0.4.1 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/builder_apm/diagnose_request_controller.rb +79 -0
- data/app/controllers/builder_apm/request_details_controller.rb +3 -2
- data/app/views/builder_apm/css/_dark.html.erb +161 -118
- data/app/views/builder_apm/css/_main.html.erb +342 -272
- data/app/views/builder_apm/js/_data_fetcher.html.erb +229 -236
- data/app/views/builder_apm/js/_request_details.html.erb +368 -164
- data/builder_apm.gemspec +1 -1
- data/config/routes.rb +3 -0
- data/lib/builder_apm/configuration.rb +6 -0
- data/lib/builder_apm/doctor/ai_doctor.rb +170 -0
- data/lib/builder_apm/doctor/backtrace_reducer.rb +104 -0
- data/lib/builder_apm/doctor/bravo_chat_ai.rb +85 -0
- data/lib/builder_apm/doctor/openai_chat_gpt.rb +84 -0
- data/lib/builder_apm/methods/instrumenter.rb +33 -6
- data/lib/builder_apm/middleware/timing.rb +6 -0
- data/lib/builder_apm/models/instrumenter.rb +15 -3
- data/lib/builder_apm/version.rb +1 -1
- data/lib/generators/builder_apm/install_generator.rb +2 -2
- data/lib/generators/builder_apm/templates/builder_apm_config.rb +7 -4
- metadata +8 -4
- data/README.md +0 -29
@@ -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)
|
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
|
-
|
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]
|
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')
|
10
|
-
|
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] >
|
126
|
+
if value[:count] > 2
|
115
127
|
Thread.current[:has_n_plus_one] = true
|
116
128
|
return
|
117
129
|
end
|
data/lib/builder_apm/version.rb
CHANGED
@@ -9,8 +9,8 @@ module BuilderApm
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def copy_migrations
|
12
|
-
migration_template "create_builder_apm_requests.rb", "db/migrate/create_builder_apm_requests.rb"
|
13
|
-
migration_template "create_builder_apm_sql_queries.rb", "db/migrate/create_builder_apm_sql_queries.rb"
|
12
|
+
# migration_template "create_builder_apm_requests.rb", "db/migrate/create_builder_apm_requests.rb"
|
13
|
+
# migration_template "create_builder_apm_sql_queries.rb", "db/migrate/create_builder_apm_sql_queries.rb"
|
14
14
|
end
|
15
15
|
|
16
16
|
def copy_initializer_file
|
@@ -1,6 +1,9 @@
|
|
1
1
|
BuilderApm.configure do |config|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
+
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-
|
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
|
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.
|