builder_apm 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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/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
@@ -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.
|
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
|
-
|