rails-profiler 0.22.0 → 0.23.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 +31 -71
- data/lib/profiler/collectors/flamegraph_collector.rb +12 -7
- data/lib/profiler/collectors/http_collector.rb +71 -27
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +26 -36
- data/lib/profiler/instrumentation/thread_context_propagation.rb +34 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 893d3965287ddebc00378c605ff16d13734a4fd97c7016d2c613c0eb3c6b709b
|
|
4
|
+
data.tar.gz: 9574f00d346286e72d4d47abdab4d60520053b5f16b6f16c701ba178c24ab4ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e0f22f1aa90112910678954253843a3d4bb245029a829359e195f99dc9e5bbc87c0f43d0333959dfa00f7d612cd83fb111c81cdb63767435751a2de26eae4bd
|
|
7
|
+
data.tar.gz: '058f8f1228758f62939eb97ee0b6124b737c053d00634971df9905bc7732c61936ece855280c14da509cc1bee981ef6a7185bc3c72652d3a2b816b97f9628765'
|
|
@@ -1664,17 +1664,14 @@
|
|
|
1664
1664
|
const [jobHasMore, setJobHasMore] = d2(false);
|
|
1665
1665
|
const [jobLoadingMore, setJobLoadingMore] = d2(false);
|
|
1666
1666
|
const [outboundRequests, setOutboundRequests] = d2([]);
|
|
1667
|
-
const [loadingHttp, setLoadingHttp] = d2(
|
|
1668
|
-
const [loadingJobs, setLoadingJobs] = d2(
|
|
1669
|
-
const [loadingOutbound, setLoadingOutbound] = d2(
|
|
1670
|
-
const [jobsLoaded, setJobsLoaded] = d2(false);
|
|
1671
|
-
const [outboundLoaded, setOutboundLoaded] = d2(false);
|
|
1667
|
+
const [loadingHttp, setLoadingHttp] = d2(initialSection() === "http");
|
|
1668
|
+
const [loadingJobs, setLoadingJobs] = d2(initialSection() === "jobs");
|
|
1669
|
+
const [loadingOutbound, setLoadingOutbound] = d2(initialSection() === "outbound");
|
|
1672
1670
|
const [error, setError] = d2(null);
|
|
1673
1671
|
const [jobsError, setJobsError] = d2(null);
|
|
1674
1672
|
const [outboundError, setOutboundError] = d2(null);
|
|
1675
1673
|
const [envData, setEnvData] = d2(void 0);
|
|
1676
|
-
const [loadingEnv, setLoadingEnv] = d2(
|
|
1677
|
-
const [envLoaded, setEnvLoaded] = d2(false);
|
|
1674
|
+
const [loadingEnv, setLoadingEnv] = d2(initialSection() === "env");
|
|
1678
1675
|
const [envError, setEnvError] = d2(null);
|
|
1679
1676
|
const [copiedToken, setCopiedToken] = d2(null);
|
|
1680
1677
|
const [httpSearch, setHttpSearch] = d2("");
|
|
@@ -1694,20 +1691,7 @@
|
|
|
1694
1691
|
const [outboundMethod, setOutboundMethod] = d2("");
|
|
1695
1692
|
const [outboundStatus, setOutboundStatus] = d2("");
|
|
1696
1693
|
y2(() => {
|
|
1697
|
-
|
|
1698
|
-
setProfiles(data.profiles);
|
|
1699
|
-
setHttpOffset(data.profiles.length);
|
|
1700
|
-
setHttpHasMore(data.has_more);
|
|
1701
|
-
setLoadingHttp(false);
|
|
1702
|
-
}).catch(() => {
|
|
1703
|
-
setError("Failed to load profiles");
|
|
1704
|
-
setLoadingHttp(false);
|
|
1705
|
-
});
|
|
1706
|
-
}, []);
|
|
1707
|
-
y2(() => {
|
|
1708
|
-
if (section === "jobs") loadJobs();
|
|
1709
|
-
if (section === "outbound") loadOutbound();
|
|
1710
|
-
if (section === "env") loadEnv();
|
|
1694
|
+
refreshSection(section);
|
|
1711
1695
|
}, []);
|
|
1712
1696
|
y2(() => {
|
|
1713
1697
|
const url = new URL(window.location.href);
|
|
@@ -1747,21 +1731,6 @@
|
|
|
1747
1731
|
setHttpLoadingMore(false);
|
|
1748
1732
|
}).catch(() => setHttpLoadingMore(false));
|
|
1749
1733
|
};
|
|
1750
|
-
const loadJobs = () => {
|
|
1751
|
-
if (jobsLoaded) return;
|
|
1752
|
-
setLoadingJobs(true);
|
|
1753
|
-
fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
1754
|
-
setJobs(data.profiles);
|
|
1755
|
-
setJobOffset(data.profiles.length);
|
|
1756
|
-
setJobHasMore(data.has_more);
|
|
1757
|
-
setLoadingJobs(false);
|
|
1758
|
-
setJobsLoaded(true);
|
|
1759
|
-
}).catch(() => {
|
|
1760
|
-
setJobsError("Failed to load job profiles");
|
|
1761
|
-
setLoadingJobs(false);
|
|
1762
|
-
setJobsLoaded(true);
|
|
1763
|
-
});
|
|
1764
|
-
};
|
|
1765
1734
|
const loadMoreJobs = () => {
|
|
1766
1735
|
setJobLoadingMore(true);
|
|
1767
1736
|
fetch(`${BASE}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
|
|
@@ -1771,40 +1740,16 @@
|
|
|
1771
1740
|
setJobLoadingMore(false);
|
|
1772
1741
|
}).catch(() => setJobLoadingMore(false));
|
|
1773
1742
|
};
|
|
1774
|
-
const loadOutbound = () => {
|
|
1775
|
-
if (outboundLoaded) return;
|
|
1776
|
-
setLoadingOutbound(true);
|
|
1777
|
-
fetch(`${BASE}/api/outbound_http`).then((res) => res.json()).then((data) => {
|
|
1778
|
-
setOutboundRequests(data);
|
|
1779
|
-
setLoadingOutbound(false);
|
|
1780
|
-
setOutboundLoaded(true);
|
|
1781
|
-
}).catch(() => {
|
|
1782
|
-
setOutboundError("Failed to load outbound HTTP requests");
|
|
1783
|
-
setLoadingOutbound(false);
|
|
1784
|
-
setOutboundLoaded(true);
|
|
1785
|
-
});
|
|
1786
|
-
};
|
|
1787
|
-
const loadEnv = () => {
|
|
1788
|
-
if (envLoaded) return;
|
|
1789
|
-
setLoadingEnv(true);
|
|
1790
|
-
fetch(`${BASE}/api/env_vars`).then((res) => res.json()).then((data) => {
|
|
1791
|
-
setEnvData(data);
|
|
1792
|
-
setLoadingEnv(false);
|
|
1793
|
-
setEnvLoaded(true);
|
|
1794
|
-
}).catch(() => {
|
|
1795
|
-
setEnvError("Failed to load environment variables");
|
|
1796
|
-
setLoadingEnv(false);
|
|
1797
|
-
setEnvLoaded(true);
|
|
1798
|
-
});
|
|
1799
|
-
};
|
|
1800
1743
|
const handleSectionChange = (s3) => {
|
|
1744
|
+
if (s3 === section) {
|
|
1745
|
+
refreshSection(s3);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1801
1748
|
setSection(s3);
|
|
1802
1749
|
const url = new URL(window.location.href);
|
|
1803
1750
|
url.searchParams.set("section", s3);
|
|
1804
1751
|
history.pushState(null, "", url.toString());
|
|
1805
|
-
|
|
1806
|
-
if (s3 === "outbound") loadOutbound();
|
|
1807
|
-
if (s3 === "env") loadEnv();
|
|
1752
|
+
refreshSection(s3);
|
|
1808
1753
|
setHttpSearch("");
|
|
1809
1754
|
setHttpMethod("");
|
|
1810
1755
|
setHttpStatus("");
|
|
@@ -1836,17 +1781,29 @@
|
|
|
1836
1781
|
});
|
|
1837
1782
|
};
|
|
1838
1783
|
const clearProfiles = () => {
|
|
1784
|
+
if (!window.confirm("Delete all HTTP profiles?")) return;
|
|
1839
1785
|
fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }).then(() => {
|
|
1840
1786
|
setProfiles([]);
|
|
1841
1787
|
});
|
|
1842
1788
|
};
|
|
1843
1789
|
const clearJobs = () => {
|
|
1790
|
+
if (!window.confirm("Delete all job profiles?")) return;
|
|
1844
1791
|
fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" }).then(() => {
|
|
1845
1792
|
setJobs([]);
|
|
1846
1793
|
});
|
|
1847
1794
|
};
|
|
1848
|
-
const
|
|
1849
|
-
if (
|
|
1795
|
+
const clearAll = () => {
|
|
1796
|
+
if (!window.confirm("Delete all HTTP and job profiles?")) return;
|
|
1797
|
+
Promise.all([
|
|
1798
|
+
fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }),
|
|
1799
|
+
fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" })
|
|
1800
|
+
]).then(() => {
|
|
1801
|
+
setProfiles([]);
|
|
1802
|
+
setJobs([]);
|
|
1803
|
+
});
|
|
1804
|
+
};
|
|
1805
|
+
const refreshSection = (s3) => {
|
|
1806
|
+
if (s3 === "http") {
|
|
1850
1807
|
setLoadingHttp(true);
|
|
1851
1808
|
fetch(`${BASE}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
1852
1809
|
setProfiles(data.profiles);
|
|
@@ -1857,7 +1814,7 @@
|
|
|
1857
1814
|
setError("Failed to load profiles");
|
|
1858
1815
|
setLoadingHttp(false);
|
|
1859
1816
|
});
|
|
1860
|
-
} else if (
|
|
1817
|
+
} else if (s3 === "jobs") {
|
|
1861
1818
|
setLoadingJobs(true);
|
|
1862
1819
|
fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
1863
1820
|
setJobs(data.profiles);
|
|
@@ -1868,7 +1825,7 @@
|
|
|
1868
1825
|
setJobsError("Failed to load job profiles");
|
|
1869
1826
|
setLoadingJobs(false);
|
|
1870
1827
|
});
|
|
1871
|
-
} else if (
|
|
1828
|
+
} else if (s3 === "outbound") {
|
|
1872
1829
|
setLoadingOutbound(true);
|
|
1873
1830
|
fetch(`${BASE}/api/outbound_http`).then((res) => res.json()).then((data) => {
|
|
1874
1831
|
setOutboundRequests(data);
|
|
@@ -1888,6 +1845,7 @@
|
|
|
1888
1845
|
});
|
|
1889
1846
|
}
|
|
1890
1847
|
};
|
|
1848
|
+
const refresh = () => refreshSection(section);
|
|
1891
1849
|
const tabClass = (s3) => `tab${section === s3 ? " active" : ""}`;
|
|
1892
1850
|
const filteredProfiles = profiles.filter((p3) => {
|
|
1893
1851
|
if (httpSearch && !p3.path.toLowerCase().includes(httpSearch.toLowerCase())) return false;
|
|
@@ -2069,7 +2027,8 @@
|
|
|
2069
2027
|
profiles.length
|
|
2070
2028
|
] }),
|
|
2071
2029
|
/* @__PURE__ */ u3("button", { class: `btn-refresh${loadingHttp ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingHttp, title: "Refresh", children: "\u21BA" }),
|
|
2072
|
-
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearProfiles, children: "Clear
|
|
2030
|
+
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearProfiles, title: "Delete HTTP profiles", children: "Clear" }),
|
|
2031
|
+
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearAll, title: "Delete all HTTP and job profiles", children: "Clear All" })
|
|
2073
2032
|
] })
|
|
2074
2033
|
] }),
|
|
2075
2034
|
filteredProfiles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
|
|
@@ -2154,7 +2113,8 @@
|
|
|
2154
2113
|
jobs.length
|
|
2155
2114
|
] }),
|
|
2156
2115
|
/* @__PURE__ */ u3("button", { class: `btn-refresh${loadingJobs ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingJobs, title: "Refresh", children: "\u21BA" }),
|
|
2157
|
-
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearJobs, children: "Clear
|
|
2116
|
+
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearJobs, title: "Delete job profiles", children: "Clear" }),
|
|
2117
|
+
/* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearAll, title: "Delete all HTTP and job profiles", children: "Clear All" })
|
|
2158
2118
|
] })
|
|
2159
2119
|
] }),
|
|
2160
2120
|
filteredJobs.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
|
|
@@ -10,6 +10,7 @@ module Profiler
|
|
|
10
10
|
super
|
|
11
11
|
@events = []
|
|
12
12
|
@subscriptions = []
|
|
13
|
+
@mutex = Mutex.new
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def icon
|
|
@@ -38,7 +39,7 @@ module Profiler
|
|
|
38
39
|
|
|
39
40
|
# Controller action
|
|
40
41
|
@subscriptions << ActiveSupport::Notifications.monotonic_subscribe("process_action.action_controller") do |_name, started, finished, _unique_id, payload|
|
|
41
|
-
|
|
42
|
+
add_event Models::TimelineEvent.new(
|
|
42
43
|
name: "#{payload[:controller]}##{payload[:action]}",
|
|
43
44
|
started_at: started,
|
|
44
45
|
finished_at: finished,
|
|
@@ -57,7 +58,7 @@ module Profiler
|
|
|
57
58
|
# Template rendering
|
|
58
59
|
@subscriptions << ActiveSupport::Notifications.monotonic_subscribe("render_template.action_view") do |_name, started, finished, _unique_id, payload|
|
|
59
60
|
identifier = short_identifier(payload[:identifier])
|
|
60
|
-
|
|
61
|
+
add_event Models::TimelineEvent.new(
|
|
61
62
|
name: "Render: #{identifier}",
|
|
62
63
|
started_at: started,
|
|
63
64
|
finished_at: finished,
|
|
@@ -69,7 +70,7 @@ module Profiler
|
|
|
69
70
|
# Partial rendering
|
|
70
71
|
@subscriptions << ActiveSupport::Notifications.monotonic_subscribe("render_partial.action_view") do |_name, started, finished, _unique_id, payload|
|
|
71
72
|
identifier = short_identifier(payload[:identifier])
|
|
72
|
-
|
|
73
|
+
add_event Models::TimelineEvent.new(
|
|
73
74
|
name: "Partial: #{identifier}",
|
|
74
75
|
started_at: started,
|
|
75
76
|
finished_at: finished,
|
|
@@ -84,7 +85,7 @@ module Profiler
|
|
|
84
85
|
next if payload[:sql] =~ /^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT)/i
|
|
85
86
|
|
|
86
87
|
sql = payload[:sql].to_s
|
|
87
|
-
|
|
88
|
+
add_event Models::TimelineEvent.new(
|
|
88
89
|
name: sql.length > 80 ? "#{sql[0, 80]}..." : sql,
|
|
89
90
|
started_at: started,
|
|
90
91
|
finished_at: finished,
|
|
@@ -98,7 +99,7 @@ module Profiler
|
|
|
98
99
|
@subscriptions << ActiveSupport::Notifications.monotonic_subscribe(event_name) do |name, started, finished, _unique_id, payload|
|
|
99
100
|
op = name.split(".").first.sub("cache_", "")
|
|
100
101
|
key = payload[:key].to_s
|
|
101
|
-
|
|
102
|
+
add_event Models::TimelineEvent.new(
|
|
102
103
|
name: "cache_#{op}: #{key.length > 60 ? "#{key[0, 60]}..." : key}",
|
|
103
104
|
started_at: started,
|
|
104
105
|
finished_at: finished,
|
|
@@ -111,7 +112,7 @@ module Profiler
|
|
|
111
112
|
|
|
112
113
|
# Called by Profiler.measure to record custom instrumentation events
|
|
113
114
|
def record_custom_event(label:, started_at:, finished_at:, metadata: {})
|
|
114
|
-
|
|
115
|
+
add_event Models::TimelineEvent.new(
|
|
115
116
|
name: label,
|
|
116
117
|
started_at: started_at,
|
|
117
118
|
finished_at: finished_at,
|
|
@@ -122,7 +123,7 @@ module Profiler
|
|
|
122
123
|
|
|
123
124
|
# Called by NetHttpInstrumentation to record outbound HTTP events
|
|
124
125
|
def record_http_event(started_at:, finished_at:, url:, method:, status:)
|
|
125
|
-
|
|
126
|
+
add_event Models::TimelineEvent.new(
|
|
126
127
|
name: "HTTP #{method} #{url}",
|
|
127
128
|
started_at: started_at,
|
|
128
129
|
finished_at: finished_at,
|
|
@@ -182,6 +183,10 @@ module Profiler
|
|
|
182
183
|
roots
|
|
183
184
|
end
|
|
184
185
|
|
|
186
|
+
def add_event(event)
|
|
187
|
+
@mutex.synchronize { @events << event }
|
|
188
|
+
end
|
|
189
|
+
|
|
185
190
|
def short_identifier(identifier)
|
|
186
191
|
return identifier.to_s unless identifier.to_s.include?("/")
|
|
187
192
|
|
|
@@ -9,6 +9,8 @@ module Profiler
|
|
|
9
9
|
def initialize(profile)
|
|
10
10
|
super
|
|
11
11
|
@requests = []
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@collected = false
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def icon
|
|
@@ -40,47 +42,88 @@ module Profiler
|
|
|
40
42
|
def collect
|
|
41
43
|
Thread.current[:profiler_http_collector] = nil
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
data = @mutex.synchronize do
|
|
46
|
+
@collected = true
|
|
47
|
+
build_data(@requests)
|
|
48
|
+
end
|
|
49
|
+
store_data(data)
|
|
50
|
+
end
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
# Called from NetHttpInstrumentation before the actual HTTP call.
|
|
53
|
+
# Returns the mutable entry so the caller can update it on completion.
|
|
54
|
+
def register_pending(payload)
|
|
55
|
+
entry = payload.merge(in_flight: true, status: 0, duration: nil,
|
|
56
|
+
response_headers: {}, response_body: nil,
|
|
57
|
+
response_body_encoding: "text", response_size: 0)
|
|
58
|
+
@mutex.synchronize { @requests << entry }
|
|
59
|
+
save_if_collected
|
|
60
|
+
entry
|
|
54
61
|
end
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
# Called from NetHttpInstrumentation after the HTTP response is received.
|
|
64
|
+
def complete_request(entry, **data)
|
|
65
|
+
@mutex.synchronize { entry.merge!(data.merge(in_flight: false)) }
|
|
66
|
+
save_if_collected
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Called from NetHttpInstrumentation when the HTTP call raises.
|
|
70
|
+
def fail_request(entry, error:, duration:)
|
|
71
|
+
@mutex.synchronize { entry.merge!(in_flight: false, status: 0, duration: duration, error: error) }
|
|
72
|
+
save_if_collected
|
|
58
73
|
end
|
|
59
74
|
|
|
60
75
|
def toolbar_summary
|
|
61
|
-
|
|
76
|
+
requests = @mutex.synchronize { @requests.dup }
|
|
77
|
+
total = requests.size
|
|
62
78
|
return { text: "0 HTTP", color: "green" } if total == 0
|
|
63
79
|
|
|
64
80
|
threshold = Profiler.configuration.slow_http_threshold
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
in_flight = requests.count { |r| r[:in_flight] }
|
|
82
|
+
errors = requests.count { |r| !r[:in_flight] && (r[:status] >= 400 || r[:status] == 0) }
|
|
83
|
+
slow = requests.count { |r| !r[:in_flight] && r[:duration] && r[:duration] >= threshold }
|
|
84
|
+
duration = requests.sum { |r| r[:duration].to_f }.round(2)
|
|
68
85
|
|
|
69
86
|
color = if errors > 0 || slow > 0
|
|
70
87
|
"red"
|
|
71
|
-
elsif total > 10
|
|
88
|
+
elsif in_flight > 0 || total > 10
|
|
72
89
|
"orange"
|
|
73
90
|
else
|
|
74
91
|
"green"
|
|
75
92
|
end
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
text = in_flight > 0 ? "#{total} HTTP (#{in_flight} pending, #{duration}ms)" : "#{total} HTTP (#{duration}ms)"
|
|
95
|
+
{ text: text, color: color }
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
private
|
|
81
99
|
|
|
82
|
-
def
|
|
83
|
-
|
|
100
|
+
def build_data(requests)
|
|
101
|
+
threshold = Profiler.configuration.slow_http_threshold
|
|
102
|
+
{
|
|
103
|
+
total_requests: requests.size,
|
|
104
|
+
total_duration: requests.sum { |r| r[:duration].to_f }.round(2),
|
|
105
|
+
slow_requests: requests.count { |r| !r[:in_flight] && r[:duration] && r[:duration] >= threshold },
|
|
106
|
+
error_requests: requests.count { |r| !r[:in_flight] && (r[:status] >= 400 || r[:status] == 0) },
|
|
107
|
+
by_host: group_by_host(requests),
|
|
108
|
+
by_status: group_by_status(requests),
|
|
109
|
+
requests: requests.map { |r| r.transform_keys(&:to_s) }
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Rebuilds and persists HTTP data after collect has already run.
|
|
114
|
+
# Called when fire-and-forget threads register or complete requests post-collect.
|
|
115
|
+
def save_if_collected
|
|
116
|
+
data = @mutex.synchronize do
|
|
117
|
+
return unless @collected
|
|
118
|
+
|
|
119
|
+
build_data(@requests)
|
|
120
|
+
end
|
|
121
|
+
store_data(data)
|
|
122
|
+
Profiler.storage.save(@profile.token, @profile)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def group_by_host(requests)
|
|
126
|
+
requests.each_with_object(Hash.new(0)) do |req, h|
|
|
84
127
|
host = begin
|
|
85
128
|
URI.parse(req[:url]).host || "unknown"
|
|
86
129
|
rescue URI::InvalidURIError
|
|
@@ -90,16 +133,17 @@ module Profiler
|
|
|
90
133
|
end
|
|
91
134
|
end
|
|
92
135
|
|
|
93
|
-
def group_by_status
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
def group_by_status(requests)
|
|
137
|
+
requests.each_with_object(Hash.new(0)) do |req, h|
|
|
138
|
+
key = if req[:in_flight]
|
|
139
|
+
"pending"
|
|
140
|
+
elsif req[:status] == 0
|
|
97
141
|
"error"
|
|
98
|
-
elsif status < 300
|
|
142
|
+
elsif req[:status] < 300
|
|
99
143
|
"2xx"
|
|
100
|
-
elsif status < 400
|
|
144
|
+
elsif req[:status] < 400
|
|
101
145
|
"3xx"
|
|
102
|
-
elsif status < 500
|
|
146
|
+
elsif req[:status] < 500
|
|
103
147
|
"4xx"
|
|
104
148
|
else
|
|
105
149
|
"5xx"
|
|
@@ -44,9 +44,28 @@ module Profiler
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
req_headers = req.to_hash.transform_values { |v| v.join(", ") }
|
|
47
|
+
req_content_type = req["content-type"].to_s
|
|
48
|
+
processed_req = req_body.empty? ? { body: nil, encoding: "text" } : NetHttpInstrumentation.process_body(req_body, req_content_type)
|
|
49
|
+
|
|
47
50
|
request_id = SecureRandom.hex(8)
|
|
48
51
|
started_at = Time.now.iso8601(3)
|
|
49
52
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
53
|
+
|
|
54
|
+
# Register the request as pending before the network call so that
|
|
55
|
+
# fire-and-forget threads appear in the UI immediately, even if
|
|
56
|
+
# collect() runs before this thread completes.
|
|
57
|
+
entry = collector.register_pending(
|
|
58
|
+
id: request_id,
|
|
59
|
+
started_at: started_at,
|
|
60
|
+
url: url,
|
|
61
|
+
method: req.method,
|
|
62
|
+
request_headers: req_headers,
|
|
63
|
+
request_body: processed_req[:body],
|
|
64
|
+
request_body_encoding: processed_req[:encoding],
|
|
65
|
+
request_size: req_body.bytesize,
|
|
66
|
+
backtrace: NetHttpInstrumentation.extract_backtrace
|
|
67
|
+
)
|
|
68
|
+
|
|
50
69
|
Thread.current[:profiler_http_recording] = true
|
|
51
70
|
|
|
52
71
|
response = super
|
|
@@ -56,29 +75,18 @@ module Profiler
|
|
|
56
75
|
resp_content_encoding = response["content-encoding"].to_s.strip.downcase
|
|
57
76
|
resp_body = NetHttpInstrumentation.decompress_body(resp_body_raw, resp_content_encoding)
|
|
58
77
|
resp_content_type = response["content-type"].to_s
|
|
59
|
-
req_content_type = req["content-type"].to_s
|
|
60
|
-
|
|
61
|
-
processed_req = req_body.empty? ? { body: nil, encoding: "text" } : NetHttpInstrumentation.process_body(req_body, req_content_type)
|
|
62
78
|
processed_resp = NetHttpInstrumentation.process_body(resp_body, resp_content_type)
|
|
63
79
|
|
|
64
80
|
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
81
|
|
|
66
|
-
collector.
|
|
67
|
-
|
|
68
|
-
started_at: started_at,
|
|
69
|
-
url: url,
|
|
70
|
-
method: req.method,
|
|
82
|
+
collector.complete_request(
|
|
83
|
+
entry,
|
|
71
84
|
status: response.code.to_i,
|
|
72
85
|
duration: duration,
|
|
73
|
-
request_headers: req_headers,
|
|
74
|
-
request_body: processed_req[:body],
|
|
75
|
-
request_body_encoding: processed_req[:encoding],
|
|
76
|
-
request_size: req_body.bytesize,
|
|
77
86
|
response_headers: response.to_hash.transform_values { |v| v.join(", ") },
|
|
78
87
|
response_body: processed_resp[:body],
|
|
79
88
|
response_body_encoding: processed_resp[:encoding],
|
|
80
|
-
response_size: resp_body_raw.bytesize
|
|
81
|
-
backtrace: NetHttpInstrumentation.extract_backtrace
|
|
89
|
+
response_size: resp_body_raw.bytesize
|
|
82
90
|
)
|
|
83
91
|
|
|
84
92
|
fg = Thread.current[:profiler_flamegraph_collector]
|
|
@@ -86,26 +94,9 @@ module Profiler
|
|
|
86
94
|
|
|
87
95
|
response
|
|
88
96
|
rescue => e
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
collector
|
|
92
|
-
id: defined?(request_id) ? request_id : SecureRandom.hex(8),
|
|
93
|
-
started_at: defined?(started_at) ? started_at : Time.now.iso8601(3),
|
|
94
|
-
url: url,
|
|
95
|
-
method: req.method,
|
|
96
|
-
status: 0,
|
|
97
|
-
duration: duration,
|
|
98
|
-
request_headers: defined?(req_headers) ? req_headers : {},
|
|
99
|
-
request_body: nil,
|
|
100
|
-
request_body_encoding: "text",
|
|
101
|
-
request_size: 0,
|
|
102
|
-
response_headers: {},
|
|
103
|
-
response_body: nil,
|
|
104
|
-
response_body_encoding: "text",
|
|
105
|
-
response_size: 0,
|
|
106
|
-
backtrace: NetHttpInstrumentation.extract_backtrace,
|
|
107
|
-
error: e.message
|
|
108
|
-
)
|
|
97
|
+
duration = defined?(t0) && t0 ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2) : 0.0
|
|
98
|
+
if defined?(entry) && entry
|
|
99
|
+
collector.fail_request(entry, error: e.message, duration: duration)
|
|
109
100
|
end
|
|
110
101
|
raise
|
|
111
102
|
ensure
|
|
@@ -166,9 +157,8 @@ module Profiler
|
|
|
166
157
|
end
|
|
167
158
|
|
|
168
159
|
def self.extract_backtrace
|
|
169
|
-
caller_locations(5,
|
|
160
|
+
caller_locations(5, 40)
|
|
170
161
|
.reject { |l| l.path.to_s.include?("net/http") || l.path.to_s.include?("profiler/instrumentation") }
|
|
171
|
-
.first(5)
|
|
172
162
|
.map { |l| "#{l.path}:#{l.lineno}:in `#{l.label}`" }
|
|
173
163
|
end
|
|
174
164
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module ThreadContextPropagation
|
|
6
|
+
PROPAGATED_KEYS = %i[
|
|
7
|
+
profiler_http_collector
|
|
8
|
+
profiler_flamegraph_collector
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(*args, &block)
|
|
12
|
+
parent_context = PROPAGATED_KEYS.filter_map do |key|
|
|
13
|
+
val = Thread.current[key]
|
|
14
|
+
[key, val] unless val.nil?
|
|
15
|
+
end.to_h
|
|
16
|
+
|
|
17
|
+
if parent_context.empty?
|
|
18
|
+
super
|
|
19
|
+
else
|
|
20
|
+
super(*args) do
|
|
21
|
+
parent_context.each { |k, v| Thread.current[k] = v }
|
|
22
|
+
begin
|
|
23
|
+
block&.call
|
|
24
|
+
ensure
|
|
25
|
+
PROPAGATED_KEYS.each { |k| Thread.current[k] = nil }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Thread.prepend(Profiler::Instrumentation::ThreadContextPropagation)
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
|
@@ -106,5 +106,6 @@ require_relative "profiler/collectors/env_collector"
|
|
|
106
106
|
require_relative "profiler/collectors/mailer_collector"
|
|
107
107
|
|
|
108
108
|
require_relative "profiler/env_override_store"
|
|
109
|
+
require_relative "profiler/instrumentation/thread_context_propagation"
|
|
109
110
|
require_relative "profiler/railtie" if defined?(Rails::Railtie)
|
|
110
111
|
require_relative "profiler/engine" if defined?(Rails::Engine)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.23.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -149,6 +149,7 @@ files:
|
|
|
149
149
|
- lib/profiler/instrumentation/active_job_instrumentation.rb
|
|
150
150
|
- lib/profiler/instrumentation/net_http_instrumentation.rb
|
|
151
151
|
- lib/profiler/instrumentation/sidekiq_middleware.rb
|
|
152
|
+
- lib/profiler/instrumentation/thread_context_propagation.rb
|
|
152
153
|
- lib/profiler/job_profiler.rb
|
|
153
154
|
- lib/profiler/mcp/body_formatter.rb
|
|
154
155
|
- lib/profiler/mcp/file_cache.rb
|