rails-profiler 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/builds/profiler.js +81 -5
- data/app/controllers/profiler/api/jobs_controller.rb +4 -1
- data/app/controllers/profiler/api/profiles_controller.rb +4 -1
- data/app/controllers/profiler/application_controller.rb +46 -0
- data/app/controllers/profiler/profiles_controller.rb +5 -0
- data/app/views/profiler/profiles/show.html.erb +1 -1
- data/lib/profiler/current_context.rb +17 -0
- data/lib/profiler/instrumentation/active_job_instrumentation.rb +16 -0
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +8 -0
- data/lib/profiler/job_profiler.rb +10 -3
- data/lib/profiler/mcp/tools/get_profile_detail.rb +47 -1
- data/lib/profiler/mcp/tools/query_jobs.rb +8 -7
- data/lib/profiler/middleware/profiler_middleware.rb +3 -0
- data/lib/profiler/railtie.rb +5 -0
- data/lib/profiler/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44b1fd924fe2eb7f0483bc36f0e3956e81535da3ce39e60f6e29c091194f22ce
|
|
4
|
+
data.tar.gz: c840abbc0813c4e8c9516c507138e4ead25e9980f5c02a3306461b1a308ff6e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d5307f599d3822291a99d931cacb1dc70218e32a120e136cebe98790e89f803e0ad84a5520b6961279c88048a7c2262c91600d770f07639cb5e31be32ff3e5a
|
|
7
|
+
data.tar.gz: 341cfa488dd91fe1b566dfc7c0a4d28dd6ab42e5fb0eb874250ca4b0482e3bb154d398c1c0919fef09a812a1cae7f897542b5702eb097c83e80e534cbbf777bd
|
|
@@ -3577,6 +3577,47 @@
|
|
|
3577
3577
|
] });
|
|
3578
3578
|
}
|
|
3579
3579
|
|
|
3580
|
+
// app/assets/typescript/profiler/components/dashboard/tabs/JobsTab.tsx
|
|
3581
|
+
function JobsTab({ jobs }) {
|
|
3582
|
+
if (!jobs?.length) {
|
|
3583
|
+
return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
|
|
3584
|
+
/* @__PURE__ */ u3("div", { class: "profiler-empty__icon", children: "\u2699\uFE0F" }),
|
|
3585
|
+
/* @__PURE__ */ u3("h3", { class: "profiler-empty__title", children: "No jobs triggered" }),
|
|
3586
|
+
/* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Background jobs enqueued during this request will appear here." })
|
|
3587
|
+
] });
|
|
3588
|
+
}
|
|
3589
|
+
return /* @__PURE__ */ u3(k, { children: [
|
|
3590
|
+
/* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: [
|
|
3591
|
+
"Background Jobs (",
|
|
3592
|
+
jobs.length,
|
|
3593
|
+
")"
|
|
3594
|
+
] }),
|
|
3595
|
+
jobs.map((job, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "default"}`, children: [
|
|
3596
|
+
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
3597
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
|
|
3598
|
+
/* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "warning"}`, children: job.status ?? "unknown" }),
|
|
3599
|
+
/* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: job.job_class })
|
|
3600
|
+
] }),
|
|
3601
|
+
/* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
|
|
3602
|
+
job.duration?.toFixed(2),
|
|
3603
|
+
" ms"
|
|
3604
|
+
] })
|
|
3605
|
+
] }),
|
|
3606
|
+
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
3607
|
+
/* @__PURE__ */ u3("span", { class: "profiler-ajax-card__time profiler-text--muted", children: [
|
|
3608
|
+
job.queue && /* @__PURE__ */ u3("span", { children: [
|
|
3609
|
+
"Queue: ",
|
|
3610
|
+
/* @__PURE__ */ u3("strong", { children: job.queue }),
|
|
3611
|
+
" \xB7 "
|
|
3612
|
+
] }),
|
|
3613
|
+
new Date(job.started_at).toLocaleTimeString("en", { hour12: false })
|
|
3614
|
+
] }),
|
|
3615
|
+
/* @__PURE__ */ u3("a", { href: `/_profiler/profiles/${job.token}`, class: "profiler-text--sm", style: "color: var(--profiler-accent);", children: "View Job \u2192" })
|
|
3616
|
+
] })
|
|
3617
|
+
] }, index))
|
|
3618
|
+
] });
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3580
3621
|
// app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
|
|
3581
3622
|
function ProfileDashboard({ profile, initialTab, embedded }) {
|
|
3582
3623
|
const cd = profile.collectors_data || {};
|
|
@@ -3586,6 +3627,7 @@
|
|
|
3586
3627
|
const hasLogs = (cd["logs"]?.count ?? 0) > 0;
|
|
3587
3628
|
const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
|
|
3588
3629
|
const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
|
|
3630
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3589
3631
|
const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
|
|
3590
3632
|
const handleTabClick = (tab) => (e3) => {
|
|
3591
3633
|
e3.preventDefault();
|
|
@@ -3641,7 +3683,12 @@
|
|
|
3641
3683
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3642
3684
|
hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
|
|
3643
3685
|
hasRoutes && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("routes"), onClick: handleTabClick("routes"), children: "Routes" }),
|
|
3644
|
-
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" })
|
|
3686
|
+
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" }),
|
|
3687
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
3688
|
+
"Jobs (",
|
|
3689
|
+
profile.child_jobs.length,
|
|
3690
|
+
")"
|
|
3691
|
+
] })
|
|
3645
3692
|
] }),
|
|
3646
3693
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3647
3694
|
activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
@@ -3655,7 +3702,8 @@
|
|
|
3655
3702
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3656
3703
|
activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
3657
3704
|
activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
|
|
3658
|
-
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] })
|
|
3705
|
+
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
|
|
3706
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
3659
3707
|
] })
|
|
3660
3708
|
] }),
|
|
3661
3709
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -3718,13 +3766,15 @@
|
|
|
3718
3766
|
|
|
3719
3767
|
// app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
|
|
3720
3768
|
function JobProfileDashboard({ profile, initialTab, embedded }) {
|
|
3721
|
-
const validTabs = ["job", "database", "cache", "http"];
|
|
3769
|
+
const validTabs = ["job", "database", "cache", "http", "jobs"];
|
|
3722
3770
|
const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
|
|
3723
3771
|
const [activeTab, setActiveTab] = d2(defaultTab);
|
|
3724
3772
|
const cd = profile.collectors_data || {};
|
|
3725
3773
|
const hasHttp = cd["http"]?.total_requests > 0;
|
|
3774
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3726
3775
|
const jobData = cd["job"];
|
|
3727
3776
|
const isFailed = jobData?.status === "failed";
|
|
3777
|
+
const parent = profile.parent_profile;
|
|
3728
3778
|
const handleTabClick = (tab) => (e3) => {
|
|
3729
3779
|
e3.preventDefault();
|
|
3730
3780
|
setActiveTab(tab);
|
|
@@ -3759,6 +3809,26 @@
|
|
|
3759
3809
|
" MB"
|
|
3760
3810
|
] })
|
|
3761
3811
|
] })
|
|
3812
|
+
] }),
|
|
3813
|
+
parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
|
|
3814
|
+
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "Triggered by:" }),
|
|
3815
|
+
parent.profile_type === "http" ? /* @__PURE__ */ u3("span", { children: [
|
|
3816
|
+
/* @__PURE__ */ u3("strong", { children: parent.method }),
|
|
3817
|
+
" ",
|
|
3818
|
+
parent.path,
|
|
3819
|
+
" \xB7 ",
|
|
3820
|
+
parent.http_status,
|
|
3821
|
+
" \xB7 ",
|
|
3822
|
+
parent.duration?.toFixed(2),
|
|
3823
|
+
" ms"
|
|
3824
|
+
] }) : /* @__PURE__ */ u3("span", { children: [
|
|
3825
|
+
"\u2699\uFE0F ",
|
|
3826
|
+
/* @__PURE__ */ u3("strong", { children: parent.path }),
|
|
3827
|
+
" \xB7 ",
|
|
3828
|
+
parent.duration?.toFixed(2),
|
|
3829
|
+
" ms"
|
|
3830
|
+
] }),
|
|
3831
|
+
/* @__PURE__ */ u3("a", { href: `/_profiler/profiles/${parent.token}`, style: "color: var(--profiler-accent);", children: "View \u2192" })
|
|
3762
3832
|
] })
|
|
3763
3833
|
] }),
|
|
3764
3834
|
/* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
|
|
@@ -3766,13 +3836,19 @@
|
|
|
3766
3836
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
|
|
3767
3837
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
|
|
3768
3838
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3769
|
-
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" })
|
|
3839
|
+
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
|
|
3840
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
3841
|
+
"Jobs (",
|
|
3842
|
+
profile.child_jobs.length,
|
|
3843
|
+
")"
|
|
3844
|
+
] })
|
|
3770
3845
|
] }),
|
|
3771
3846
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3772
3847
|
activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
|
|
3773
3848
|
activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
|
|
3774
3849
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3775
|
-
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] })
|
|
3850
|
+
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
|
|
3851
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
3776
3852
|
] })
|
|
3777
3853
|
] }),
|
|
3778
3854
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -26,7 +26,10 @@ module Profiler
|
|
|
26
26
|
return render json: { error: "Job profile not found" }, status: :not_found
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
render json: profile.to_h
|
|
29
|
+
render json: profile.to_h.merge(
|
|
30
|
+
child_jobs: build_child_jobs(profile),
|
|
31
|
+
parent_profile: build_parent_summary(profile)
|
|
32
|
+
)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def destroy
|
|
@@ -29,7 +29,10 @@ module Profiler
|
|
|
29
29
|
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
30
30
|
recalculate_ajax_data(profile)
|
|
31
31
|
|
|
32
|
-
render json: profile.to_h
|
|
32
|
+
render json: profile.to_h.merge(
|
|
33
|
+
child_jobs: build_child_jobs(profile),
|
|
34
|
+
parent_profile: build_parent_summary(profile)
|
|
35
|
+
)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def destroy
|
|
@@ -15,5 +15,51 @@ module Profiler
|
|
|
15
15
|
render plain: "Profiler is disabled", status: :forbidden
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
def build_child_jobs(profile)
|
|
20
|
+
Profiler.storage.find_by_parent(profile.token)
|
|
21
|
+
.select { |p| p.profile_type == "job" }
|
|
22
|
+
.map do |j|
|
|
23
|
+
job_data = j.collector_data("job") || {}
|
|
24
|
+
{
|
|
25
|
+
token: j.token,
|
|
26
|
+
job_class: j.path,
|
|
27
|
+
job_id: job_data["job_id"],
|
|
28
|
+
queue: job_data["queue"],
|
|
29
|
+
status: job_data["status"],
|
|
30
|
+
duration: j.duration,
|
|
31
|
+
started_at: j.started_at&.iso8601
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_parent_summary(profile)
|
|
37
|
+
return nil unless profile.parent_token
|
|
38
|
+
|
|
39
|
+
parent = Profiler.storage.load(profile.parent_token)
|
|
40
|
+
return nil unless parent
|
|
41
|
+
|
|
42
|
+
if parent.profile_type == "job"
|
|
43
|
+
job_data = parent.collector_data("job") || {}
|
|
44
|
+
{
|
|
45
|
+
token: parent.token,
|
|
46
|
+
profile_type: "job",
|
|
47
|
+
path: parent.path,
|
|
48
|
+
status: job_data["status"],
|
|
49
|
+
duration: parent.duration,
|
|
50
|
+
started_at: parent.started_at&.iso8601
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
{
|
|
54
|
+
token: parent.token,
|
|
55
|
+
profile_type: "http",
|
|
56
|
+
method: parent.method,
|
|
57
|
+
path: parent.path,
|
|
58
|
+
http_status: parent.status,
|
|
59
|
+
duration: parent.duration,
|
|
60
|
+
started_at: parent.started_at&.iso8601
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
18
64
|
end
|
|
19
65
|
end
|
|
@@ -23,6 +23,11 @@ module Profiler
|
|
|
23
23
|
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
24
24
|
recalculate_ajax_data(@profile)
|
|
25
25
|
|
|
26
|
+
@profile_data = @profile.to_h.merge(
|
|
27
|
+
child_jobs: build_child_jobs(@profile),
|
|
28
|
+
parent_profile: build_parent_summary(@profile)
|
|
29
|
+
)
|
|
30
|
+
|
|
26
31
|
@embedded = params[:embed] == "true"
|
|
27
32
|
|
|
28
33
|
render layout: @embedded ? "profiler/embedded" : "profiler/application"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module CurrentContext
|
|
5
|
+
def self.token
|
|
6
|
+
Thread.current[:profiler_token]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.token=(value)
|
|
10
|
+
Thread.current[:profiler_token] = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.clear
|
|
14
|
+
Thread.current[:profiler_token] = nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -6,6 +6,12 @@ module Profiler
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
+
attr_accessor :profiler_parent_token
|
|
10
|
+
|
|
11
|
+
before_enqueue do |job|
|
|
12
|
+
job.profiler_parent_token = Profiler::CurrentContext.token
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
around_perform do |job, block|
|
|
10
16
|
Profiler::JobProfiler.profile(
|
|
11
17
|
job_class: job.class.name,
|
|
@@ -13,10 +19,20 @@ module Profiler
|
|
|
13
19
|
queue: job.queue_name,
|
|
14
20
|
arguments: job.arguments,
|
|
15
21
|
executions: job.executions - 1,
|
|
22
|
+
parent_token: job.profiler_parent_token,
|
|
16
23
|
&block
|
|
17
24
|
)
|
|
18
25
|
end
|
|
19
26
|
end
|
|
27
|
+
|
|
28
|
+
def serialize
|
|
29
|
+
super.merge("profiler_parent_token" => profiler_parent_token)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def deserialize(job_data)
|
|
33
|
+
super
|
|
34
|
+
self.profiler_parent_token = job_data["profiler_parent_token"]
|
|
35
|
+
end
|
|
20
36
|
end
|
|
21
37
|
end
|
|
22
38
|
end
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Profiler
|
|
4
4
|
module Instrumentation
|
|
5
|
+
class SidekiqClientMiddleware
|
|
6
|
+
def call(_worker_class, job, _queue, _redis_pool)
|
|
7
|
+
job["profiler_parent_token"] = Profiler::CurrentContext.token
|
|
8
|
+
yield
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
5
12
|
class SidekiqMiddleware
|
|
6
13
|
def call(worker, job, queue, &block)
|
|
7
14
|
Profiler::JobProfiler.profile(
|
|
@@ -10,6 +17,7 @@ module Profiler
|
|
|
10
17
|
queue: queue,
|
|
11
18
|
arguments: job["args"],
|
|
12
19
|
executions: job["retry_count"].to_i,
|
|
20
|
+
parent_token: job["profiler_parent_token"],
|
|
13
21
|
&block
|
|
14
22
|
)
|
|
15
23
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "models/profile"
|
|
4
|
+
require_relative "current_context"
|
|
4
5
|
require_relative "collectors/job_collector"
|
|
5
6
|
require_relative "collectors/database_collector"
|
|
6
7
|
require_relative "collectors/cache_collector"
|
|
@@ -14,7 +15,7 @@ module Profiler
|
|
|
14
15
|
Collectors::HttpCollector
|
|
15
16
|
].freeze
|
|
16
17
|
|
|
17
|
-
def self.profile(job_class:, job_id:, queue:, arguments:, executions:, &block)
|
|
18
|
+
def self.profile(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil, &block)
|
|
18
19
|
return block.call unless Profiler.enabled? && Profiler.configuration.track_jobs
|
|
19
20
|
|
|
20
21
|
new(
|
|
@@ -22,16 +23,18 @@ module Profiler
|
|
|
22
23
|
job_id: job_id,
|
|
23
24
|
queue: queue,
|
|
24
25
|
arguments: arguments,
|
|
25
|
-
executions: executions
|
|
26
|
+
executions: executions,
|
|
27
|
+
parent_token: parent_token
|
|
26
28
|
).run(&block)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def initialize(job_class:, job_id:, queue:, arguments:, executions:)
|
|
31
|
+
def initialize(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil)
|
|
30
32
|
@job_class = job_class
|
|
31
33
|
@job_id = job_id
|
|
32
34
|
@queue = queue
|
|
33
35
|
@arguments = arguments
|
|
34
36
|
@executions = executions
|
|
37
|
+
@parent_token = parent_token
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def run(&block)
|
|
@@ -39,6 +42,7 @@ module Profiler
|
|
|
39
42
|
profile.profile_type = "job"
|
|
40
43
|
profile.path = @job_class
|
|
41
44
|
profile.method = "JOB"
|
|
45
|
+
profile.parent_token = @parent_token if @parent_token
|
|
42
46
|
|
|
43
47
|
job_collector = Collectors::JobCollector.new(profile, {
|
|
44
48
|
job_class: @job_class,
|
|
@@ -56,6 +60,8 @@ module Profiler
|
|
|
56
60
|
job_status = "completed"
|
|
57
61
|
error_message = nil
|
|
58
62
|
|
|
63
|
+
previous_token = Profiler::CurrentContext.token
|
|
64
|
+
Profiler::CurrentContext.token = profile.token
|
|
59
65
|
begin
|
|
60
66
|
result = block.call
|
|
61
67
|
result
|
|
@@ -64,6 +70,7 @@ module Profiler
|
|
|
64
70
|
error_message = "#{e.class}: #{e.message}"
|
|
65
71
|
raise
|
|
66
72
|
ensure
|
|
73
|
+
Profiler::CurrentContext.token = previous_token
|
|
67
74
|
if Profiler.configuration.track_memory
|
|
68
75
|
profile.memory = current_memory - memory_before
|
|
69
76
|
end
|
|
@@ -63,17 +63,21 @@ module Profiler
|
|
|
63
63
|
lines += section_http(profile) if want.("http")
|
|
64
64
|
lines += section_routes(profile) if want.("routes")
|
|
65
65
|
lines += section_dumps(profile) if want.("dumps")
|
|
66
|
+
lines += section_related_jobs(profile) if want.("related_jobs")
|
|
66
67
|
lines.join("\n")
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def self.section_overview(profile)
|
|
70
71
|
lines = []
|
|
71
72
|
lines << "# Profile Details: #{profile.token}\n"
|
|
73
|
+
lines << "**Type:** #{profile.profile_type == 'job' ? 'Job' : 'HTTP Request'}"
|
|
72
74
|
lines << "**Request:** #{profile.method} #{profile.path}"
|
|
73
75
|
lines << "**Status:** #{profile.status}"
|
|
74
76
|
lines << "**Duration:** #{profile.duration.round(2)} ms"
|
|
75
77
|
lines << "**Memory:** #{(profile.memory / 1024.0 / 1024.0).round(2)} MB" if profile.memory
|
|
76
|
-
lines << "**Time:** #{profile.started_at}
|
|
78
|
+
lines << "**Time:** #{profile.started_at}"
|
|
79
|
+
lines << "**Parent Token:** #{profile.parent_token}" if profile.parent_token
|
|
80
|
+
lines << ""
|
|
77
81
|
lines
|
|
78
82
|
end
|
|
79
83
|
|
|
@@ -386,6 +390,48 @@ module Profiler
|
|
|
386
390
|
lines
|
|
387
391
|
end
|
|
388
392
|
|
|
393
|
+
def self.section_related_jobs(profile)
|
|
394
|
+
lines = []
|
|
395
|
+
|
|
396
|
+
# Parent info
|
|
397
|
+
if profile.parent_token
|
|
398
|
+
parent = Profiler.storage.load(profile.parent_token)
|
|
399
|
+
if parent
|
|
400
|
+
lines << "## Triggered By"
|
|
401
|
+
if parent.profile_type == "job"
|
|
402
|
+
job_data = parent.collector_data("job") || {}
|
|
403
|
+
lines << "- **Type:** Job"
|
|
404
|
+
lines << "- **Class:** #{job_data['job_class'] || parent.path}"
|
|
405
|
+
lines << "- **Status:** #{job_data['status']}"
|
|
406
|
+
lines << "- **Duration:** #{parent.duration.round(2)} ms"
|
|
407
|
+
lines << "- **Token:** #{parent.token}"
|
|
408
|
+
else
|
|
409
|
+
lines << "- **Type:** HTTP Request"
|
|
410
|
+
lines << "- **Request:** #{parent.method} #{parent.path}"
|
|
411
|
+
lines << "- **Status:** #{parent.status}"
|
|
412
|
+
lines << "- **Duration:** #{parent.duration.round(2)} ms"
|
|
413
|
+
lines << "- **Token:** #{parent.token}"
|
|
414
|
+
end
|
|
415
|
+
lines << ""
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Child jobs
|
|
420
|
+
child_jobs = Profiler.storage.find_by_parent(profile.token).select { |p| p.profile_type == "job" }
|
|
421
|
+
return lines if child_jobs.empty?
|
|
422
|
+
|
|
423
|
+
lines << "## Child Jobs (#{child_jobs.size})"
|
|
424
|
+
lines << ""
|
|
425
|
+
lines << "| Job Class | Status | Duration | Token |"
|
|
426
|
+
lines << "|-----------|--------|----------|-------|"
|
|
427
|
+
child_jobs.each do |job|
|
|
428
|
+
job_data = job.collector_data("job") || {}
|
|
429
|
+
lines << "| #{job_data['job_class'] || job.path} | #{job_data['status'] || '-'} | #{job.duration.round(2)} ms | #{job.token} |"
|
|
430
|
+
end
|
|
431
|
+
lines << ""
|
|
432
|
+
lines
|
|
433
|
+
end
|
|
434
|
+
|
|
389
435
|
def self.generate_curl(profile, req_data)
|
|
390
436
|
headers = req_data&.dig("headers") || {}
|
|
391
437
|
params = req_data&.dig("params") || {}
|
|
@@ -4,7 +4,7 @@ module Profiler
|
|
|
4
4
|
module MCP
|
|
5
5
|
module Tools
|
|
6
6
|
class QueryJobs
|
|
7
|
-
ALL_FIELDS = %w[time job_class queue status duration token].freeze
|
|
7
|
+
ALL_FIELDS = %w[time job_class queue status duration token parent_token].freeze
|
|
8
8
|
|
|
9
9
|
def self.call(params)
|
|
10
10
|
limit = params["limit"]&.to_i || 20
|
|
@@ -59,12 +59,13 @@ module Profiler
|
|
|
59
59
|
job_data = profile.collector_data("job") || {}
|
|
60
60
|
row = fields.map do |f|
|
|
61
61
|
case f
|
|
62
|
-
when "time"
|
|
63
|
-
when "job_class"
|
|
64
|
-
when "queue"
|
|
65
|
-
when "status"
|
|
66
|
-
when "duration"
|
|
67
|
-
when "token"
|
|
62
|
+
when "time" then profile.started_at.strftime("%H:%M:%S")
|
|
63
|
+
when "job_class" then job_data["job_class"] || profile.path
|
|
64
|
+
when "queue" then job_data["queue"] || "-"
|
|
65
|
+
when "status" then job_data["status"] || "-"
|
|
66
|
+
when "duration" then "#{profile.duration.round(2)}ms"
|
|
67
|
+
when "token" then profile.token.to_s
|
|
68
|
+
when "parent_token" then profile.parent_token || "-"
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
lines << "| #{row.join(' | ')} |"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../models/profile"
|
|
4
|
+
require_relative "../current_context"
|
|
4
5
|
require_relative "toolbar_injector"
|
|
5
6
|
|
|
6
7
|
module Profiler
|
|
@@ -14,6 +15,7 @@ module Profiler
|
|
|
14
15
|
return @app.call(env) unless should_profile?(env)
|
|
15
16
|
|
|
16
17
|
profile = Models::Profile.new(build_request(env))
|
|
18
|
+
Profiler::CurrentContext.token = profile.token
|
|
17
19
|
|
|
18
20
|
# Capture request body before app processes it
|
|
19
21
|
req_body_raw = read_rack_input(env)
|
|
@@ -64,6 +66,7 @@ module Profiler
|
|
|
64
66
|
|
|
65
67
|
# Store profile
|
|
66
68
|
Profiler.storage.save(profile.token, profile)
|
|
69
|
+
Profiler::CurrentContext.clear
|
|
67
70
|
|
|
68
71
|
# Add profiler token header
|
|
69
72
|
headers["X-Profiler-Token"] = profile.token
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -62,6 +62,11 @@ module Profiler
|
|
|
62
62
|
chain.add Profiler::Instrumentation::SidekiqMiddleware
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
|
+
Sidekiq.configure_client do |config|
|
|
66
|
+
config.client_middleware do |chain|
|
|
67
|
+
chain.add Profiler::Instrumentation::SidekiqClientMiddleware
|
|
68
|
+
end
|
|
69
|
+
end
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
if defined?(ActiveJob::Base)
|
data/lib/profiler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/profiler/collectors/routes_collector.rb
|
|
140
140
|
- lib/profiler/collectors/view_collector.rb
|
|
141
141
|
- lib/profiler/configuration.rb
|
|
142
|
+
- lib/profiler/current_context.rb
|
|
142
143
|
- lib/profiler/engine.rb
|
|
143
144
|
- lib/profiler/explain_runner.rb
|
|
144
145
|
- lib/profiler/instrumentation/active_job_instrumentation.rb
|