solid_log-ui 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +295 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/application.js +6 -0
- data/app/assets/javascripts/solid_log/checkbox_dropdown.js +171 -0
- data/app/assets/javascripts/solid_log/filter_state.js +138 -0
- data/app/assets/javascripts/solid_log/jump_to_live.js +119 -0
- data/app/assets/javascripts/solid_log/live_tail.js +476 -0
- data/app/assets/javascripts/solid_log/live_tail.js.bak +270 -0
- data/app/assets/javascripts/solid_log/log_filters.js +37 -0
- data/app/assets/javascripts/solid_log/stream_scroll.js +195 -0
- data/app/assets/javascripts/solid_log/timeline_histogram.js +162 -0
- data/app/assets/javascripts/solid_log/toast.js +50 -0
- data/app/assets/stylesheets/solid_log/application.css +1329 -0
- data/app/assets/stylesheets/solid_log/components.css +1506 -0
- data/app/assets/stylesheets/solid_log/mission_control.css +398 -0
- data/app/channels/solid_log/ui/application_cable/channel.rb +8 -0
- data/app/channels/solid_log/ui/application_cable/connection.rb +10 -0
- data/app/channels/solid_log/ui/log_stream_channel.rb +132 -0
- data/app/controllers/solid_log/ui/base_controller.rb +122 -0
- data/app/controllers/solid_log/ui/dashboard_controller.rb +32 -0
- data/app/controllers/solid_log/ui/entries_controller.rb +34 -0
- data/app/controllers/solid_log/ui/fields_controller.rb +57 -0
- data/app/controllers/solid_log/ui/streams_controller.rb +204 -0
- data/app/controllers/solid_log/ui/timelines_controller.rb +29 -0
- data/app/controllers/solid_log/ui/tokens_controller.rb +46 -0
- data/app/helpers/solid_log/ui/application_helper.rb +99 -0
- data/app/helpers/solid_log/ui/dashboard_helper.rb +46 -0
- data/app/helpers/solid_log/ui/entries_helper.rb +16 -0
- data/app/helpers/solid_log/ui/timeline_helper.rb +39 -0
- data/app/services/solid_log/ui/live_tail_broadcaster.rb +81 -0
- data/app/views/layouts/solid_log/ui/application.html.erb +53 -0
- data/app/views/solid_log/ui/dashboard/index.html.erb +178 -0
- data/app/views/solid_log/ui/entries/show.html.erb +132 -0
- data/app/views/solid_log/ui/fields/index.html.erb +133 -0
- data/app/views/solid_log/ui/shared/_checkbox_dropdown.html.erb +64 -0
- data/app/views/solid_log/ui/shared/_multiselect_filter.html.erb +37 -0
- data/app/views/solid_log/ui/shared/_toast.html.erb +7 -0
- data/app/views/solid_log/ui/shared/_toast_message.html.erb +30 -0
- data/app/views/solid_log/ui/streams/_filter_form.html.erb +207 -0
- data/app/views/solid_log/ui/streams/_footer.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_entries.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row_compact.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_row_expanded.html.erb +67 -0
- data/app/views/solid_log/ui/streams/_log_stream_content.html.erb +8 -0
- data/app/views/solid_log/ui/streams/_timeline.html.erb +68 -0
- data/app/views/solid_log/ui/streams/index.html.erb +22 -0
- data/app/views/solid_log/ui/timelines/show_job.html.erb +78 -0
- data/app/views/solid_log/ui/timelines/show_request.html.erb +88 -0
- data/app/views/solid_log/ui/tokens/index.html.erb +95 -0
- data/app/views/solid_log/ui/tokens/new.html.erb +47 -0
- data/config/importmap.rb +15 -0
- data/config/routes.rb +27 -0
- data/lib/solid_log/ui/api_client.rb +117 -0
- data/lib/solid_log/ui/configuration.rb +99 -0
- data/lib/solid_log/ui/data_source.rb +146 -0
- data/lib/solid_log/ui/engine.rb +76 -0
- data/lib/solid_log/ui/version.rb +5 -0
- data/lib/solid_log/ui.rb +27 -0
- data/lib/solid_log-ui.rb +2 -0
- metadata +290 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<div class="filter-form">
|
|
2
|
+
<h3>Filters</h3>
|
|
3
|
+
<%= form_with url: streams_path, method: :get do |f| %>
|
|
4
|
+
<div class="filter-form-content">
|
|
5
|
+
<div class="filter-group">
|
|
6
|
+
<label>Search</label>
|
|
7
|
+
<%= f.text_field "filters[query]", value: @current_filters[:query],
|
|
8
|
+
placeholder: "Search messages...",
|
|
9
|
+
class: "form-input" %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @available_filters[:levels]&.size.to_i > 1 %>
|
|
13
|
+
<div class="filter-group">
|
|
14
|
+
<label>Log Level</label>
|
|
15
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
16
|
+
field_name: "filters[levels]",
|
|
17
|
+
options: @available_filters[:levels],
|
|
18
|
+
selected: @current_filters[:levels],
|
|
19
|
+
label: "Select Log Levels",
|
|
20
|
+
id_prefix: "level" %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<% if @available_filters[:apps]&.size.to_i > 1 %>
|
|
25
|
+
<div class="filter-group">
|
|
26
|
+
<label>Application</label>
|
|
27
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
28
|
+
field_name: "filters[app]",
|
|
29
|
+
options: @available_filters[:apps],
|
|
30
|
+
selected: @current_filters[:app],
|
|
31
|
+
label: "Select Applications",
|
|
32
|
+
id_prefix: "app" %>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<% if @available_filters[:envs]&.size.to_i > 1 %>
|
|
37
|
+
<div class="filter-group">
|
|
38
|
+
<label>Environment</label>
|
|
39
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
40
|
+
field_name: "filters[env]",
|
|
41
|
+
options: @available_filters[:envs],
|
|
42
|
+
selected: @current_filters[:env],
|
|
43
|
+
label: "Select Environments",
|
|
44
|
+
id_prefix: "env" %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<% if @available_filters[:controllers]&.size.to_i > 1 %>
|
|
49
|
+
<div class="filter-group">
|
|
50
|
+
<label>Controller</label>
|
|
51
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
52
|
+
field_name: "filters[controller]",
|
|
53
|
+
options: @available_filters[:controllers],
|
|
54
|
+
selected: @current_filters[:controller],
|
|
55
|
+
label: "Select Controllers",
|
|
56
|
+
id_prefix: "controller" %>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
|
|
60
|
+
<% if @available_filters[:actions]&.size.to_i > 1 %>
|
|
61
|
+
<div class="filter-group">
|
|
62
|
+
<label>Action</label>
|
|
63
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
64
|
+
field_name: "filters[action]",
|
|
65
|
+
options: @available_filters[:actions],
|
|
66
|
+
selected: @current_filters[:action],
|
|
67
|
+
label: "Select Actions",
|
|
68
|
+
id_prefix: "action" %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
|
|
72
|
+
<% if @available_filters[:methods]&.size.to_i > 1 %>
|
|
73
|
+
<div class="filter-group">
|
|
74
|
+
<label>HTTP Method</label>
|
|
75
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
76
|
+
field_name: "filters[method]",
|
|
77
|
+
options: @available_filters[:methods],
|
|
78
|
+
selected: @current_filters[:method],
|
|
79
|
+
label: "Select Methods",
|
|
80
|
+
id_prefix: "method" %>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
|
|
84
|
+
<% if @available_filters[:paths]&.size.to_i > 1 %>
|
|
85
|
+
<div class="filter-group">
|
|
86
|
+
<label>Path</label>
|
|
87
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
88
|
+
field_name: "filters[path]",
|
|
89
|
+
options: @available_filters[:paths],
|
|
90
|
+
selected: @current_filters[:path],
|
|
91
|
+
label: "Select Paths",
|
|
92
|
+
id_prefix: "path" %>
|
|
93
|
+
</div>
|
|
94
|
+
<% end %>
|
|
95
|
+
|
|
96
|
+
<% if @available_filters[:status_codes]&.size.to_i > 1 %>
|
|
97
|
+
<div class="filter-group">
|
|
98
|
+
<label>Status Code</label>
|
|
99
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
100
|
+
field_name: "filters[status_code]",
|
|
101
|
+
options: @available_filters[:status_codes],
|
|
102
|
+
selected: @current_filters[:status_code],
|
|
103
|
+
label: "Select Status Codes",
|
|
104
|
+
id_prefix: "status_code" %>
|
|
105
|
+
</div>
|
|
106
|
+
<% end %>
|
|
107
|
+
|
|
108
|
+
<div class="filter-group">
|
|
109
|
+
<label>Duration (ms)</label>
|
|
110
|
+
<div class="time-inputs">
|
|
111
|
+
<%= f.number_field "filters[min_duration]", value: @current_filters[:min_duration],
|
|
112
|
+
placeholder: "Min",
|
|
113
|
+
class: "form-input form-input-small" %>
|
|
114
|
+
<%= f.number_field "filters[max_duration]", value: @current_filters[:max_duration],
|
|
115
|
+
placeholder: "Max",
|
|
116
|
+
class: "form-input form-input-small" %>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="filter-group">
|
|
121
|
+
<label>Time Range</label>
|
|
122
|
+
<div class="time-inputs">
|
|
123
|
+
<%= f.datetime_field "filters[start_time]", value: @current_filters[:start_time],
|
|
124
|
+
placeholder: "Start time",
|
|
125
|
+
class: "form-input form-input-small" %>
|
|
126
|
+
<%= f.datetime_field "filters[end_time]", value: @current_filters[:end_time],
|
|
127
|
+
placeholder: "End time",
|
|
128
|
+
class: "form-input form-input-small" %>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="filter-group">
|
|
133
|
+
<label>Request ID</label>
|
|
134
|
+
<%= f.text_field "filters[request_id]", value: @current_filters[:request_id],
|
|
135
|
+
placeholder: "Filter by request...",
|
|
136
|
+
class: "form-input" %>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="filter-group">
|
|
140
|
+
<label>Job ID</label>
|
|
141
|
+
<%= f.text_field "filters[job_id]", value: @current_filters[:job_id],
|
|
142
|
+
placeholder: "Filter by job...",
|
|
143
|
+
class: "form-input" %>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<%# Dynamically render promoted field filters with facets %>
|
|
147
|
+
<% @available_filters.each do |key, values| %>
|
|
148
|
+
<% next if [:levels, :apps, :envs, :controllers, :actions, :methods, :paths, :status_codes].include?(key) %>
|
|
149
|
+
<% next unless values&.size.to_i > 1 %>
|
|
150
|
+
|
|
151
|
+
<% field = SolidLog::Field.promoted.find_by(name: key.to_s) %>
|
|
152
|
+
<% next unless field %>
|
|
153
|
+
|
|
154
|
+
<div class="filter-group">
|
|
155
|
+
<label><%= key.to_s.titleize %></label>
|
|
156
|
+
|
|
157
|
+
<% case field.filter_type %>
|
|
158
|
+
<% when "multiselect" %>
|
|
159
|
+
<%= render "solid_log/ui/shared/multiselect_filter",
|
|
160
|
+
field_name: "filters[#{key}]",
|
|
161
|
+
options: values,
|
|
162
|
+
selected: Array(@current_filters[key]),
|
|
163
|
+
label: "Select #{key.to_s.titleize}",
|
|
164
|
+
id_prefix: key.to_s %>
|
|
165
|
+
|
|
166
|
+
<% when "range" %>
|
|
167
|
+
<div class="time-inputs">
|
|
168
|
+
<%= f.number_field "filters[min_#{key}]", value: @current_filters["min_#{key}".to_sym],
|
|
169
|
+
placeholder: "Min",
|
|
170
|
+
class: "form-input form-input-small" %>
|
|
171
|
+
<%= f.number_field "filters[max_#{key}]", value: @current_filters["max_#{key}".to_sym],
|
|
172
|
+
placeholder: "Max",
|
|
173
|
+
class: "form-input form-input-small" %>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<% when "contains" %>
|
|
177
|
+
<%= f.text_field "filters[#{key}]", value: @current_filters[key],
|
|
178
|
+
placeholder: "Search #{key.to_s.titleize}...",
|
|
179
|
+
class: "form-input" %>
|
|
180
|
+
|
|
181
|
+
<% when "exact" %>
|
|
182
|
+
<%= f.select "filters[#{key}]",
|
|
183
|
+
options_for_select([["All #{key.to_s.titleize}", ""]] + values.map { |v| [v, v] }, @current_filters[key]),
|
|
184
|
+
class: "form-select" %>
|
|
185
|
+
<% end %>
|
|
186
|
+
</div>
|
|
187
|
+
<% end %>
|
|
188
|
+
|
|
189
|
+
<%# Render token-based promoted fields (no facets needed) %>
|
|
190
|
+
<% SolidLog::Field.promoted.where(filter_type: "tokens").each do |field| %>
|
|
191
|
+
<% next unless SolidLog::Entry.column_names.include?(field.name) %>
|
|
192
|
+
<div class="filter-group">
|
|
193
|
+
<label><%= field.name.titleize %></label>
|
|
194
|
+
<%= f.text_field "filters[#{field.name}]", value: @current_filters[field.name.to_sym],
|
|
195
|
+
placeholder: "Enter #{field.name.titleize} (comma-separated)...",
|
|
196
|
+
class: "form-input" %>
|
|
197
|
+
<small class="form-hint">Enter multiple values separated by commas</small>
|
|
198
|
+
</div>
|
|
199
|
+
<% end %>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="filter-actions">
|
|
203
|
+
<%= f.submit "Apply Filters", class: "btn btn-primary btn-block" %>
|
|
204
|
+
<%= link_to "Clear", streams_path, class: "btn btn-secondary btn-block" %>
|
|
205
|
+
</div>
|
|
206
|
+
<% end %>
|
|
207
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="stream-footer-inline">
|
|
2
|
+
<%= render "timeline" %>
|
|
3
|
+
|
|
4
|
+
<div class="stream-footer-controls">
|
|
5
|
+
<button type="button" id="jump-to-live" class="btn btn-secondary btn-small">
|
|
6
|
+
↓ Jump to Live
|
|
7
|
+
</button>
|
|
8
|
+
|
|
9
|
+
<% if SolidLog::UI.configuration.websocket_enabled %>
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
id="live-tail-toggle"
|
|
13
|
+
class="btn btn-secondary btn-small"
|
|
14
|
+
data-live-tail-mode="websocket"
|
|
15
|
+
title="Enable live streaming of new log entries">
|
|
16
|
+
▶ Live Tail
|
|
17
|
+
</button>
|
|
18
|
+
<span id="live-tail-indicator" class="live-tail-status" style="display: none;">
|
|
19
|
+
<span class="status-dot"></span>
|
|
20
|
+
<span class="status-text">Streaming</span>
|
|
21
|
+
</span>
|
|
22
|
+
<% else %>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
id="live-tail-toggle"
|
|
26
|
+
class="btn btn-secondary btn-small"
|
|
27
|
+
data-live-tail-mode="polling"
|
|
28
|
+
title="Enable polling for new log entries (WebSocket disabled)">
|
|
29
|
+
▶ Live Tail
|
|
30
|
+
</button>
|
|
31
|
+
<span id="live-tail-indicator" class="live-tail-status" style="display: none;">
|
|
32
|
+
<span class="status-dot"></span>
|
|
33
|
+
<span class="status-text">Polling</span>
|
|
34
|
+
</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% if SolidLog::UI.configuration.stream_view_style == :expanded %>
|
|
2
|
+
<%= render "solid_log/ui/streams/log_row_expanded", entry: entry, query: defined?(query) ? query : nil %>
|
|
3
|
+
<% else %>
|
|
4
|
+
<%= render "solid_log/ui/streams/log_row_compact", entry: entry, query: defined?(query) ? query : nil %>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="log-row-compact-wrapper" data-entry-id="<%= entry.id %>">
|
|
2
|
+
<span class="log-row-timestamp"><%= entry.timestamp.strftime("%Y-%m-%d %H:%M:%S %z") %></span>
|
|
3
|
+
|
|
4
|
+
<%= link_to entry_path(entry), class: "log-row-compact-message" do %>
|
|
5
|
+
<%= defined?(query) && query.present? ? highlight_search_term(entry.message, query) : entry.message %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<% if entry.request_id.present? %>
|
|
9
|
+
<button type="button" class="log-filter-btn" data-filter-type="request_id" data-filter-value="<%= entry.request_id %>">
|
|
10
|
+
[<%= entry.request_id %>]
|
|
11
|
+
</button>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% if entry.method.present? %>
|
|
15
|
+
<button type="button" class="log-filter-btn" data-filter-type="method" data-filter-value="<%= entry.method %>">
|
|
16
|
+
[<%= entry.method %>]
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<% if entry.app.present? %>
|
|
21
|
+
<button type="button" class="log-filter-btn" data-filter-type="app" data-filter-value="<%= entry.app %>">
|
|
22
|
+
[<%= entry.app %>]
|
|
23
|
+
</button>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if entry.extra_fields_hash['ip'].present? %>
|
|
27
|
+
<button type="button" class="log-filter-btn" data-filter-type="ip" data-filter-value="<%= entry.extra_fields_hash['ip'] %>">
|
|
28
|
+
[<%= entry.extra_fields_hash['ip'] %>]
|
|
29
|
+
</button>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<% if entry.extra_fields_hash['user_id'].present? %>
|
|
33
|
+
<button type="button" class="log-filter-btn" data-filter-type="user_id" data-filter-value="<%= entry.extra_fields_hash['user_id'] %>">
|
|
34
|
+
[<%= entry.extra_fields_hash['user_id'] %>]
|
|
35
|
+
</button>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div class="log-row" data-entry-id="<%= entry.id %>">
|
|
2
|
+
<div class="log-row-header">
|
|
3
|
+
<div class="log-row-meta">
|
|
4
|
+
<%= level_badge(entry.level) %>
|
|
5
|
+
<span class="log-timestamp" title="<%= entry.created_at.iso8601 %>">
|
|
6
|
+
<%= entry.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
7
|
+
</span>
|
|
8
|
+
<% if entry.app.present? %>
|
|
9
|
+
<span class="log-app"><%= entry.app %></span>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% if entry.env.present? %>
|
|
12
|
+
<span class="log-env"><%= entry.env %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="log-row-actions">
|
|
16
|
+
<%= link_to "Details", entry_path(entry), class: "btn-link" %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="log-row-content">
|
|
21
|
+
<div class="log-message">
|
|
22
|
+
<% if defined?(query) && query.present? %>
|
|
23
|
+
<%= highlight_search_term(entry.message, query) %>
|
|
24
|
+
<% else %>
|
|
25
|
+
<%= entry.message %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<% if entry.controller.present? || entry.status_code.present? || entry.duration.present? %>
|
|
30
|
+
<div class="log-details">
|
|
31
|
+
<% if entry.controller.present? %>
|
|
32
|
+
<span class="log-detail-item">
|
|
33
|
+
<strong>Controller:</strong>
|
|
34
|
+
<%= link_to entry.controller, streams_path(filters: { controller: [entry.controller] }),
|
|
35
|
+
class: "log-detail-link" %>#<%= entry.action %>
|
|
36
|
+
</span>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% if entry.path.present? %>
|
|
39
|
+
<span class="log-detail-item">
|
|
40
|
+
<strong>Path:</strong> <%= entry.method %> <%= entry.path %>
|
|
41
|
+
</span>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% if entry.status_code.present? %>
|
|
44
|
+
<span class="log-detail-item">
|
|
45
|
+
<strong>Status:</strong> <%= http_status_badge(entry.status_code) %>
|
|
46
|
+
</span>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if entry.duration.present? %>
|
|
49
|
+
<span class="log-detail-item">
|
|
50
|
+
<strong>Duration:</strong> <%= format_duration(entry.duration) %>
|
|
51
|
+
</span>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<% if entry.correlated? %>
|
|
57
|
+
<div class="log-correlation">
|
|
58
|
+
<% if entry.request_id.present? %>
|
|
59
|
+
<%= link_to entry.request_id[0..7], streams_path(filters: { request_id: entry.request_id }),
|
|
60
|
+
class: "correlation-link",
|
|
61
|
+
title: "Filter by request_id: #{entry.request_id}" %>
|
|
62
|
+
<% end %>
|
|
63
|
+
<%= correlation_link(entry) %>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="log-stream" id="log-stream-content">
|
|
2
|
+
<%# Entries come from query in DESC order (newest first) %>
|
|
3
|
+
<%# Reverse to ASC order (oldest first) so DOM is oldest→newest top→bottom %>
|
|
4
|
+
<%# Then scroll to bottom shows newest entries %>
|
|
5
|
+
<% entries.reverse.each do |entry| %>
|
|
6
|
+
<%= render "log_row", entry: entry, query: defined?(query) ? query : nil %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<div id="timeline-container">
|
|
2
|
+
<% if defined?(timeline_data) ? timeline_data[:buckets].any? : @timeline_data[:buckets].any? %>
|
|
3
|
+
<% timeline_data ||= @timeline_data %>
|
|
4
|
+
<% current_filters ||= @current_filters %>
|
|
5
|
+
<div class="timeline-histogram" data-controller="timeline-histogram">
|
|
6
|
+
<div class="timeline-header-row">
|
|
7
|
+
<h3>Log Timeline</h3>
|
|
8
|
+
<div class="timeline-info">
|
|
9
|
+
<span class="timeline-range">
|
|
10
|
+
<%= timeline_data[:start_time].strftime("%Y-%m-%d %H:%M") %> -
|
|
11
|
+
<%= timeline_data[:end_time].strftime("%Y-%m-%d %H:%M") %>
|
|
12
|
+
</span>
|
|
13
|
+
<button type="button" class="btn-link-small" data-action="click->timeline-histogram#clearSelection">
|
|
14
|
+
Clear Time Filter
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="timeline-chart" data-timeline-histogram-target="chart">
|
|
20
|
+
<% max_count = timeline_data[:buckets].map { |b| b[:count] }.max %>
|
|
21
|
+
<% max_count = 1 if max_count == 0 %> <%# Avoid division by zero %>
|
|
22
|
+
|
|
23
|
+
<div class="timeline-bars" data-timeline-histogram-target="barsContainer">
|
|
24
|
+
<% timeline_data[:buckets].each_with_index do |bucket, index| %>
|
|
25
|
+
<div class="timeline-bar-wrapper"
|
|
26
|
+
data-timeline-histogram-target="bar"
|
|
27
|
+
data-index="<%= index %>"
|
|
28
|
+
data-start-time="<%= bucket[:start_time].iso8601 %>"
|
|
29
|
+
data-end-time="<%= bucket[:end_time].iso8601 %>"
|
|
30
|
+
data-count="<%= bucket[:count] %>"
|
|
31
|
+
data-action="mouseenter->timeline-histogram#showTooltip
|
|
32
|
+
mouseleave->timeline-histogram#hideTooltip
|
|
33
|
+
mousedown->timeline-histogram#startSelection">
|
|
34
|
+
<div class="timeline-bar"
|
|
35
|
+
style="height: <%= (bucket[:count].to_f / max_count * 100).round(2) %>%">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="timeline-selection"
|
|
42
|
+
data-timeline-histogram-target="selection"
|
|
43
|
+
style="display: none;">
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="timeline-tooltip"
|
|
48
|
+
data-timeline-histogram-target="tooltip"
|
|
49
|
+
style="display: none;">
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<%= form_with url: streams_path, method: :get, data: { timeline_histogram_target: "form" } do |f| %>
|
|
53
|
+
<% current_filters.each do |key, value| %>
|
|
54
|
+
<% next if [:start_time, :end_time].include?(key) %>
|
|
55
|
+
<% if value.is_a?(Array) %>
|
|
56
|
+
<% value.each do |v| %>
|
|
57
|
+
<%= hidden_field_tag "filters[#{key}][]", v %>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% else %>
|
|
60
|
+
<%= hidden_field_tag "filters[#{key}]", value %>
|
|
61
|
+
<% end %>
|
|
62
|
+
<% end %>
|
|
63
|
+
<%= hidden_field_tag "filters[start_time]", "", data: { timeline_histogram_target: "startTimeField" } %>
|
|
64
|
+
<%= hidden_field_tag "filters[end_time]", "", data: { timeline_histogram_target: "endTimeField" } %>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="streams-page">
|
|
2
|
+
<div class="streams-container">
|
|
3
|
+
<div class="streams-sidebar">
|
|
4
|
+
<%= render "filter_form" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="streams-main">
|
|
8
|
+
<% if @entries.empty? %>
|
|
9
|
+
<div class="empty-state-large">
|
|
10
|
+
<h2>No log entries found</h2>
|
|
11
|
+
<p>Try adjusting your filters or check that logs are being ingested.</p>
|
|
12
|
+
<%= link_to "View all logs", streams_path, class: "btn btn-primary" %>
|
|
13
|
+
</div>
|
|
14
|
+
<% else %>
|
|
15
|
+
<%= render "log_stream_content", entries: @entries, query: @current_filters[:query] %>
|
|
16
|
+
|
|
17
|
+
<!-- Stream Footer inside terminal view -->
|
|
18
|
+
<%= render "footer" %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<div class="timeline-page">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div>
|
|
4
|
+
<h1>Job Timeline</h1>
|
|
5
|
+
<p class="subtitle">Job ID: <code><%= @job_id %></code></p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="page-actions">
|
|
8
|
+
<%= link_to "← Back to Stream", streams_path, class: "btn btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="timeline-container">
|
|
13
|
+
<div class="timeline-stats">
|
|
14
|
+
<div class="stat-inline">
|
|
15
|
+
<span class="stat-inline-label">Total Entries:</span>
|
|
16
|
+
<span class="stat-inline-value"><%= @entries.size %></span>
|
|
17
|
+
</div>
|
|
18
|
+
<% if @entries.any? %>
|
|
19
|
+
<div class="stat-inline">
|
|
20
|
+
<span class="stat-inline-label">Job Duration:</span>
|
|
21
|
+
<span class="stat-inline-value">
|
|
22
|
+
<%= distance_of_time_in_words(@entries.last.created_at, @entries.first.created_at) %>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-inline">
|
|
26
|
+
<span class="stat-inline-label">Started:</span>
|
|
27
|
+
<span class="stat-inline-value"><%= @entries.last.created_at.strftime("%H:%M:%S.%L") %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="stat-inline">
|
|
30
|
+
<span class="stat-inline-label">Ended:</span>
|
|
31
|
+
<span class="stat-inline-value"><%= @entries.first.created_at.strftime("%H:%M:%S.%L") %></span>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="timeline-entries">
|
|
37
|
+
<% @entries.reverse.each_with_index do |entry, index| %>
|
|
38
|
+
<div class="timeline-entry">
|
|
39
|
+
<div class="timeline-marker">
|
|
40
|
+
<div class="timeline-dot <%= entry.level %>"></div>
|
|
41
|
+
<% if index < @entries.size - 1 %>
|
|
42
|
+
<div class="timeline-line"></div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="timeline-content">
|
|
47
|
+
<div class="timeline-header">
|
|
48
|
+
<div class="timeline-time">
|
|
49
|
+
<%= entry.created_at.strftime("%H:%M:%S.%L") %>
|
|
50
|
+
<% if index > 0 %>
|
|
51
|
+
<span class="timeline-delta">
|
|
52
|
+
+<%= ((entry.created_at - @entries.reverse[index - 1].created_at) * 1000).round(1) %>ms
|
|
53
|
+
</span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<%= level_badge(entry.level) %>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="timeline-message">
|
|
60
|
+
<%= entry.message %>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<% if entry.extra_fields.present? && entry.extra_fields != "{}" %>
|
|
64
|
+
<details class="timeline-details">
|
|
65
|
+
<summary>Additional Fields</summary>
|
|
66
|
+
<pre class="json-display-small"><%= pretty_json(entry.extra_fields) %></pre>
|
|
67
|
+
</details>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<div class="timeline-actions">
|
|
71
|
+
<%= link_to "View Details", entry_path(entry), class: "btn-link" %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<% end %>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<div class="timeline-page">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div>
|
|
4
|
+
<h1>Request Timeline</h1>
|
|
5
|
+
<p class="subtitle">Request ID: <code><%= @request_id %></code></p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="page-actions">
|
|
8
|
+
<%= link_to "← Back to Stream", streams_path, class: "btn btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="timeline-container">
|
|
13
|
+
<div class="timeline-stats">
|
|
14
|
+
<div class="stat-inline">
|
|
15
|
+
<span class="stat-inline-label">Total Entries:</span>
|
|
16
|
+
<span class="stat-inline-value"><%= @entries.size %></span>
|
|
17
|
+
</div>
|
|
18
|
+
<% if @entries.any? %>
|
|
19
|
+
<div class="stat-inline">
|
|
20
|
+
<span class="stat-inline-label">Time Span:</span>
|
|
21
|
+
<span class="stat-inline-value">
|
|
22
|
+
<%= distance_of_time_in_words(@entries.last.created_at, @entries.first.created_at) %>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-inline">
|
|
26
|
+
<span class="stat-inline-label">First Entry:</span>
|
|
27
|
+
<span class="stat-inline-value"><%= @entries.last.created_at.strftime("%H:%M:%S.%L") %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="stat-inline">
|
|
30
|
+
<span class="stat-inline-label">Last Entry:</span>
|
|
31
|
+
<span class="stat-inline-value"><%= @entries.first.created_at.strftime("%H:%M:%S.%L") %></span>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="timeline-entries">
|
|
37
|
+
<% @entries.reverse.each_with_index do |entry, index| %>
|
|
38
|
+
<div class="timeline-entry">
|
|
39
|
+
<div class="timeline-marker">
|
|
40
|
+
<div class="timeline-dot <%= entry.level %>"></div>
|
|
41
|
+
<% if index < @entries.size - 1 %>
|
|
42
|
+
<div class="timeline-line"></div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="timeline-content">
|
|
47
|
+
<div class="timeline-header">
|
|
48
|
+
<div class="timeline-time">
|
|
49
|
+
<%= entry.created_at.strftime("%H:%M:%S.%L") %>
|
|
50
|
+
<% if index > 0 %>
|
|
51
|
+
<span class="timeline-delta">
|
|
52
|
+
+<%= ((entry.created_at - @entries.reverse[index - 1].created_at) * 1000).round(1) %>ms
|
|
53
|
+
</span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<%= level_badge(entry.level) %>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="timeline-message">
|
|
60
|
+
<%= entry.message %>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<% if entry.controller.present? || entry.status_code.present? || entry.duration.present? %>
|
|
64
|
+
<div class="timeline-meta">
|
|
65
|
+
<% if entry.controller.present? %>
|
|
66
|
+
<span><strong>Action:</strong> <%= entry.controller %>#<%= entry.action %></span>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% if entry.path.present? %>
|
|
69
|
+
<span><strong>Path:</strong> <%= entry.method %> <%= entry.path %></span>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% if entry.status_code.present? %>
|
|
72
|
+
<span><strong>Status:</strong> <%= http_status_badge(entry.status_code) %></span>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% if entry.duration.present? %>
|
|
75
|
+
<span><strong>Duration:</strong> <%= format_duration(entry.duration) %></span>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
80
|
+
<div class="timeline-actions">
|
|
81
|
+
<%= link_to "View Details", entry_path(entry), class: "btn-link" %>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|