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,95 @@
|
|
|
1
|
+
<div class="tokens-page">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div>
|
|
4
|
+
<h1>API Tokens</h1>
|
|
5
|
+
<p class="subtitle">Manage authentication tokens for log ingestion</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="page-actions">
|
|
8
|
+
<%= link_to "Create Token", new_token_path, class: "btn btn-primary" %>
|
|
9
|
+
<%= link_to "← Dashboard", dashboard_path, class: "btn btn-secondary" %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="tokens-container">
|
|
14
|
+
<% if flash[:token_plaintext].present? %>
|
|
15
|
+
<div class="alert alert-success">
|
|
16
|
+
<h3>Token Created Successfully!</h3>
|
|
17
|
+
<p><strong>IMPORTANT:</strong> This is the only time you'll see this token. Copy it now!</p>
|
|
18
|
+
<div class="token-display">
|
|
19
|
+
<code><%= flash[:token_plaintext] %></code>
|
|
20
|
+
<button onclick="copyToken('<%= flash[:token_plaintext] %>')" class="btn btn-secondary btn-small">
|
|
21
|
+
Copy
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
<p class="token-usage">
|
|
25
|
+
Use in Authorization header: <code>Bearer <%= flash[:token_plaintext] %></code>
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<% if @tokens.empty? %>
|
|
31
|
+
<div class="empty-state-large">
|
|
32
|
+
<h2>No API tokens yet</h2>
|
|
33
|
+
<p>Create a token to start ingesting logs via the HTTP API.</p>
|
|
34
|
+
<%= link_to "Create Your First Token", new_token_path, class: "btn btn-primary" %>
|
|
35
|
+
</div>
|
|
36
|
+
<% else %>
|
|
37
|
+
<div class="card">
|
|
38
|
+
<div class="card-header">
|
|
39
|
+
<h2>Active Tokens</h2>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="card-body no-padding">
|
|
42
|
+
<div class="table-responsive">
|
|
43
|
+
<table class="data-table">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Name</th>
|
|
47
|
+
<th>Created</th>
|
|
48
|
+
<th>Last Used</th>
|
|
49
|
+
<th>Actions</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<% @tokens.each do |token| %>
|
|
54
|
+
<tr>
|
|
55
|
+
<td class="token-name"><%= token.name %></td>
|
|
56
|
+
<td><%= time_ago_in_words(token.created_at) %> ago</td>
|
|
57
|
+
<td>
|
|
58
|
+
<% if token.last_used_at %>
|
|
59
|
+
<%= time_ago_in_words(token.last_used_at) %> ago
|
|
60
|
+
<% else %>
|
|
61
|
+
<span class="text-muted">Never</span>
|
|
62
|
+
<% end %>
|
|
63
|
+
</td>
|
|
64
|
+
<td class="table-actions">
|
|
65
|
+
<%= button_to "Revoke", token_path(token), method: :delete,
|
|
66
|
+
data: { confirm: "Revoke token '#{token.name}'? This cannot be undone." },
|
|
67
|
+
class: "btn-link-small text-danger" %>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<% end %>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
|
|
78
|
+
<div class="token-help">
|
|
79
|
+
<h3>Using API Tokens</h3>
|
|
80
|
+
<p>Include the token in the Authorization header of your HTTP requests:</p>
|
|
81
|
+
<pre class="code-example">curl -X POST <%= request.base_url %>/api/v1/ingest \
|
|
82
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
83
|
+
-H "Content-Type: application/json" \
|
|
84
|
+
-d '{"message":"Test log","level":"info"}'</pre>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
function copyToken(token) {
|
|
91
|
+
navigator.clipboard.writeText(token).then(() => {
|
|
92
|
+
alert('Token copied to clipboard!');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="tokens-page">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div>
|
|
4
|
+
<h1>Create API Token</h1>
|
|
5
|
+
<p class="subtitle">Generate a new token for log ingestion</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="page-actions">
|
|
8
|
+
<%= link_to "← Back", tokens_path, class: "btn btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="form-container">
|
|
13
|
+
<div class="card">
|
|
14
|
+
<div class="card-header">
|
|
15
|
+
<h2>Token Details</h2>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="card-body">
|
|
18
|
+
<%= form_with model: @token, url: tokens_path, class: "form" do |f| %>
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<%= f.label :name, "Token Name" %>
|
|
21
|
+
<%= f.text_field :name,
|
|
22
|
+
placeholder: "e.g., Production API, Staging Server",
|
|
23
|
+
class: "form-input",
|
|
24
|
+
required: true,
|
|
25
|
+
autofocus: true %>
|
|
26
|
+
<p class="form-help">Choose a descriptive name to identify this token.</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="form-actions">
|
|
30
|
+
<%= f.submit "Create Token", class: "btn btn-primary" %>
|
|
31
|
+
<%= link_to "Cancel", tokens_path, class: "btn btn-secondary" %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="info-card">
|
|
38
|
+
<h3>Important Information</h3>
|
|
39
|
+
<ul>
|
|
40
|
+
<li>The token will only be displayed <strong>once</strong> after creation</li>
|
|
41
|
+
<li>Store it securely - it cannot be retrieved later</li>
|
|
42
|
+
<li>Tokens can be revoked at any time from the tokens list</li>
|
|
43
|
+
<li>Each token is hashed and stored securely</li>
|
|
44
|
+
</ul>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Pin npm packages for SolidLog UI engine
|
|
2
|
+
|
|
3
|
+
pin "application", to: "application.js"
|
|
4
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
|
5
|
+
pin "@rails/actioncable", to: "actioncable.esm.js"
|
|
6
|
+
|
|
7
|
+
# Pin SolidLog UI JavaScript modules
|
|
8
|
+
pin "solid_log/stream_scroll", to: "solid_log/stream_scroll.js"
|
|
9
|
+
pin "solid_log/live_tail", to: "solid_log/live_tail.js"
|
|
10
|
+
pin "solid_log/jump_to_live", to: "solid_log/jump_to_live.js"
|
|
11
|
+
pin "solid_log/checkbox_dropdown", to: "solid_log/checkbox_dropdown.js"
|
|
12
|
+
pin "solid_log/timeline_histogram", to: "solid_log/timeline_histogram.js"
|
|
13
|
+
pin "solid_log/log_filters", to: "solid_log/log_filters.js"
|
|
14
|
+
pin "solid_log/filter_state", to: "solid_log/filter_state.js"
|
|
15
|
+
pin "solid_log/toast", to: "solid_log/toast.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
SolidLog::UI::Engine.routes.draw do
|
|
2
|
+
# Root - redirect to streams
|
|
3
|
+
root to: redirect("/logs/streams")
|
|
4
|
+
|
|
5
|
+
# Dashboard
|
|
6
|
+
get "dashboard", to: "dashboard#index"
|
|
7
|
+
|
|
8
|
+
# Main log viewing
|
|
9
|
+
resources :streams, only: [:index]
|
|
10
|
+
resources :entries, only: [:index, :show]
|
|
11
|
+
|
|
12
|
+
# Timeline routes for correlation
|
|
13
|
+
get "timelines/request/:request_id", to: "timelines#show_request", as: :request_timeline
|
|
14
|
+
get "timelines/job/:job_id", to: "timelines#show_job", as: :job_timeline
|
|
15
|
+
|
|
16
|
+
# Field management
|
|
17
|
+
resources :fields, only: [:index, :destroy] do
|
|
18
|
+
member do
|
|
19
|
+
post :promote
|
|
20
|
+
post :demote
|
|
21
|
+
patch :update_filter_type
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Token management
|
|
26
|
+
resources :tokens, only: [:index, :new, :create, :destroy]
|
|
27
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module SolidLog
|
|
6
|
+
module UI
|
|
7
|
+
class ApiClient
|
|
8
|
+
attr_reader :base_url, :token
|
|
9
|
+
|
|
10
|
+
def initialize(base_url: nil, token: nil)
|
|
11
|
+
@base_url = base_url || SolidLog::UI.configuration.service_url
|
|
12
|
+
@token = token || SolidLog::UI.configuration.service_token
|
|
13
|
+
|
|
14
|
+
raise ArgumentError, "base_url required for API client" if @base_url.blank?
|
|
15
|
+
raise ArgumentError, "token required for API client" if @token.blank?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GET /api/v1/entries
|
|
19
|
+
def entries(params = {})
|
|
20
|
+
get("/api/v1/entries", params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# GET /api/v1/entries/:id
|
|
24
|
+
def entry(id)
|
|
25
|
+
get("/api/v1/entries/#{id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# POST /api/v1/search
|
|
29
|
+
def search(query, params = {})
|
|
30
|
+
post("/api/v1/search", { q: query }.merge(params))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GET /api/v1/facets
|
|
34
|
+
def facets(field)
|
|
35
|
+
get("/api/v1/facets", { field: field })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# GET /api/v1/facets/all
|
|
39
|
+
def all_facets
|
|
40
|
+
get("/api/v1/facets/all")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# GET /api/v1/timelines/request/:request_id
|
|
44
|
+
def request_timeline(request_id)
|
|
45
|
+
get("/api/v1/timelines/request/#{request_id}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# GET /api/v1/timelines/job/:job_id
|
|
49
|
+
def job_timeline(job_id)
|
|
50
|
+
get("/api/v1/timelines/job/#{job_id}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# GET /api/v1/health
|
|
54
|
+
def health
|
|
55
|
+
get("/api/v1/health")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def get(path, params = {})
|
|
61
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
62
|
+
uri.query = URI.encode_www_form(params) if params.any?
|
|
63
|
+
|
|
64
|
+
request = Net::HTTP::Get.new(uri)
|
|
65
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
66
|
+
request["Content-Type"] = "application/json"
|
|
67
|
+
|
|
68
|
+
perform_request(uri, request)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def post(path, body = {})
|
|
72
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
73
|
+
|
|
74
|
+
request = Net::HTTP::Post.new(uri)
|
|
75
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
76
|
+
request["Content-Type"] = "application/json"
|
|
77
|
+
request.body = JSON.generate(body)
|
|
78
|
+
|
|
79
|
+
perform_request(uri, request)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def perform_request(uri, request)
|
|
83
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
84
|
+
http.use_ssl = (uri.scheme == "https")
|
|
85
|
+
http.open_timeout = 5
|
|
86
|
+
http.read_timeout = 30
|
|
87
|
+
|
|
88
|
+
response = http.request(request)
|
|
89
|
+
|
|
90
|
+
case response.code.to_i
|
|
91
|
+
when 200..299
|
|
92
|
+
JSON.parse(response.body)
|
|
93
|
+
when 404
|
|
94
|
+
raise NotFoundError, "Resource not found: #{uri.path}"
|
|
95
|
+
when 401
|
|
96
|
+
raise AuthenticationError, "Authentication failed. Check your service_token."
|
|
97
|
+
when 500..599
|
|
98
|
+
raise ServerError, "Server error (#{response.code}): #{response.body}"
|
|
99
|
+
else
|
|
100
|
+
raise RequestError, "Request failed (#{response.code}): #{response.body}"
|
|
101
|
+
end
|
|
102
|
+
rescue JSON::ParserError => e
|
|
103
|
+
raise ParseError, "Failed to parse JSON response: #{e.message}"
|
|
104
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
105
|
+
raise ConnectionError, "Cannot connect to service at #{@base_url}: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Custom errors
|
|
109
|
+
class RequestError < StandardError; end
|
|
110
|
+
class NotFoundError < RequestError; end
|
|
111
|
+
class AuthenticationError < RequestError; end
|
|
112
|
+
class ServerError < RequestError; end
|
|
113
|
+
class ConnectionError < RequestError; end
|
|
114
|
+
class ParseError < RequestError; end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module SolidLog
|
|
2
|
+
module UI
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :mode,
|
|
5
|
+
:service_url,
|
|
6
|
+
:service_token,
|
|
7
|
+
:database_path,
|
|
8
|
+
:websocket_enabled,
|
|
9
|
+
:stream_view_style,
|
|
10
|
+
:facet_cache_ttl,
|
|
11
|
+
:per_page,
|
|
12
|
+
:base_controller
|
|
13
|
+
attr_reader :authentication_method
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
# Mode: :direct_db (same database) or :http_api (remote service)
|
|
17
|
+
@mode = :direct_db
|
|
18
|
+
|
|
19
|
+
# HTTP API mode settings
|
|
20
|
+
@service_url = nil
|
|
21
|
+
@service_token = nil
|
|
22
|
+
|
|
23
|
+
# Direct DB mode settings
|
|
24
|
+
@database_path = nil
|
|
25
|
+
|
|
26
|
+
# Controller inheritance - defaults to ActionController::Base
|
|
27
|
+
# Set to "ApplicationController" or your app's base controller
|
|
28
|
+
@base_controller = "ActionController::Base"
|
|
29
|
+
|
|
30
|
+
# UI settings
|
|
31
|
+
# Authentication: :none, :basic, or a Proc/Symbol/String
|
|
32
|
+
# - :none - no authentication required
|
|
33
|
+
# - :basic - HTTP basic authentication (uses authenticate_with_basic_auth)
|
|
34
|
+
# - Proc/Lambda - custom authentication logic (called in controller context)
|
|
35
|
+
# - Symbol/String - method name to call on the controller
|
|
36
|
+
@authentication_method = :none
|
|
37
|
+
@websocket_enabled = true
|
|
38
|
+
@stream_view_style = :compact # :compact or :expanded
|
|
39
|
+
@facet_cache_ttl = 1.minute
|
|
40
|
+
@per_page = 100
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set authentication method with validation
|
|
44
|
+
def authentication_method=(value)
|
|
45
|
+
case value
|
|
46
|
+
when :none, :basic
|
|
47
|
+
@authentication_method = value
|
|
48
|
+
when Proc
|
|
49
|
+
@authentication_method = value
|
|
50
|
+
when Symbol, String
|
|
51
|
+
@authentication_method = value.to_sym
|
|
52
|
+
else
|
|
53
|
+
raise ArgumentError, "authentication_method must be :none, :basic, a Proc, or a Symbol/String method name"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if authentication is a proc
|
|
58
|
+
def authentication_proc?
|
|
59
|
+
authentication_method.is_a?(Proc)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if authentication is a method name
|
|
63
|
+
def authentication_method_name?
|
|
64
|
+
authentication_method.is_a?(Symbol) && ![:none, :basic].include?(authentication_method)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate configuration
|
|
68
|
+
def valid?
|
|
69
|
+
errors = []
|
|
70
|
+
|
|
71
|
+
case mode
|
|
72
|
+
when :direct_db
|
|
73
|
+
# Direct DB mode doesn't require additional config (uses core's connection)
|
|
74
|
+
when :http_api
|
|
75
|
+
errors << "service_url required for http_api mode" if service_url.blank?
|
|
76
|
+
errors << "service_token required for http_api mode" if service_token.blank?
|
|
77
|
+
else
|
|
78
|
+
errors << "mode must be :direct_db or :http_api"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Authentication validation is handled by the setter
|
|
82
|
+
|
|
83
|
+
if errors.any?
|
|
84
|
+
raise ArgumentError, "Invalid UI configuration:\n #{errors.join("\n ")}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def direct_db_mode?
|
|
91
|
+
mode == :direct_db
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def http_api_mode?
|
|
95
|
+
mode == :http_api
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require_relative "api_client"
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module UI
|
|
5
|
+
class DataSource
|
|
6
|
+
# Query entries with filters
|
|
7
|
+
def entries(filters = {})
|
|
8
|
+
if direct_db_mode?
|
|
9
|
+
query_direct_db(filters)
|
|
10
|
+
else
|
|
11
|
+
query_http_api(filters)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get single entry by ID
|
|
16
|
+
def entry(id)
|
|
17
|
+
if direct_db_mode?
|
|
18
|
+
SolidLog::Entry.find(id)
|
|
19
|
+
else
|
|
20
|
+
result = api_client.entry(id)
|
|
21
|
+
OpenStruct.new(result["entry"])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Full-text search
|
|
26
|
+
def search(query, filters = {})
|
|
27
|
+
if direct_db_mode?
|
|
28
|
+
entries = SolidLog::SearchService.search(query)
|
|
29
|
+
apply_filters(entries, filters)
|
|
30
|
+
else
|
|
31
|
+
result = api_client.search(query, filters)
|
|
32
|
+
parse_entries_response(result)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get facets for a field
|
|
37
|
+
def facets(field)
|
|
38
|
+
if direct_db_mode?
|
|
39
|
+
SolidLog::SearchService.facets_for(field)
|
|
40
|
+
else
|
|
41
|
+
result = api_client.facets(field)
|
|
42
|
+
result["values"]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get all facets
|
|
47
|
+
def all_facets
|
|
48
|
+
if direct_db_mode?
|
|
49
|
+
{
|
|
50
|
+
level: SolidLog::SearchService.facets_for("level"),
|
|
51
|
+
app: SolidLog::SearchService.facets_for("app"),
|
|
52
|
+
env: SolidLog::SearchService.facets_for("env"),
|
|
53
|
+
controller: SolidLog::SearchService.facets_for("controller"),
|
|
54
|
+
action: SolidLog::SearchService.facets_for("action"),
|
|
55
|
+
method: SolidLog::SearchService.facets_for("method"),
|
|
56
|
+
status_code: SolidLog::SearchService.facets_for("status_code")
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
result = api_client.all_facets
|
|
60
|
+
result["facets"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get request timeline
|
|
65
|
+
def request_timeline(request_id)
|
|
66
|
+
if direct_db_mode?
|
|
67
|
+
{
|
|
68
|
+
request_id: request_id,
|
|
69
|
+
entries: SolidLog::CorrelationService.request_timeline(request_id),
|
|
70
|
+
stats: SolidLog::CorrelationService.request_stats(request_id)
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
api_client.request_timeline(request_id)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get job timeline
|
|
78
|
+
def job_timeline(job_id)
|
|
79
|
+
if direct_db_mode?
|
|
80
|
+
{
|
|
81
|
+
job_id: job_id,
|
|
82
|
+
entries: SolidLog::CorrelationService.job_timeline(job_id),
|
|
83
|
+
stats: SolidLog::CorrelationService.job_stats(job_id)
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
api_client.job_timeline(job_id)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get health metrics
|
|
91
|
+
def health
|
|
92
|
+
if direct_db_mode?
|
|
93
|
+
SolidLog::HealthService.metrics
|
|
94
|
+
else
|
|
95
|
+
api_client.health["metrics"]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def direct_db_mode?
|
|
102
|
+
SolidLog::UI.configuration.direct_db_mode?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def http_api_mode?
|
|
106
|
+
SolidLog::UI.configuration.http_api_mode?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def api_client
|
|
110
|
+
@api_client ||= ApiClient.new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def query_direct_db(filters)
|
|
114
|
+
SolidLog::SearchService.query(filters).recent.limit(per_page)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def query_http_api(filters)
|
|
118
|
+
result = api_client.entries(filters.merge(limit: per_page))
|
|
119
|
+
parse_entries_response(result)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apply_filters(scope, filters)
|
|
123
|
+
scope = scope.by_level(filters[:level]) if filters[:level].present?
|
|
124
|
+
scope = scope.by_app(filters[:app]) if filters[:app].present?
|
|
125
|
+
scope = scope.by_env(filters[:env]) if filters[:env].present?
|
|
126
|
+
scope = scope.by_controller(filters[:controller]) if filters[:controller].present?
|
|
127
|
+
scope = scope.by_action(filters[:action]) if filters[:action].present?
|
|
128
|
+
scope = scope.by_path(filters[:path]) if filters[:path].present?
|
|
129
|
+
scope = scope.by_method(filters[:method]) if filters[:method].present?
|
|
130
|
+
scope = scope.by_status_code(filters[:status_code]) if filters[:status_code].present?
|
|
131
|
+
|
|
132
|
+
scope.recent.limit(per_page)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def parse_entries_response(result)
|
|
136
|
+
# Convert API response to array of OpenStruct objects
|
|
137
|
+
# This makes them compatible with views that expect ActiveRecord objects
|
|
138
|
+
(result["entries"] || []).map { |entry| OpenStruct.new(entry) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def per_page
|
|
142
|
+
SolidLog::UI.configuration.per_page
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require "importmap-rails"
|
|
2
|
+
require "turbo-rails"
|
|
3
|
+
require "stimulus-rails"
|
|
4
|
+
|
|
5
|
+
module SolidLog
|
|
6
|
+
module UI
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace SolidLog::UI
|
|
9
|
+
|
|
10
|
+
config.generators do |g|
|
|
11
|
+
g.test_framework :test_unit
|
|
12
|
+
g.assets false
|
|
13
|
+
g.helper false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Configure assets (works with both Sprockets and Propshaft)
|
|
17
|
+
initializer "solid_log_ui.assets" do |app|
|
|
18
|
+
# Add asset paths for both Sprockets and Propshaft
|
|
19
|
+
if app.config.respond_to?(:assets)
|
|
20
|
+
# Sprockets
|
|
21
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
22
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
23
|
+
app.config.assets.paths << root.join("app/assets/images")
|
|
24
|
+
|
|
25
|
+
app.config.assets.precompile += %w[
|
|
26
|
+
solid_log/**/*.css
|
|
27
|
+
solid_log/**/*.js
|
|
28
|
+
]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Propshaft
|
|
32
|
+
if Rails.application.config.respond_to?(:assets) && Rails.application.config.assets.respond_to?(:paths)
|
|
33
|
+
Rails.application.config.assets.paths << root.join("app/assets/stylesheets")
|
|
34
|
+
Rails.application.config.assets.paths << root.join("app/assets/javascripts")
|
|
35
|
+
Rails.application.config.assets.paths << root.join("app/assets/images")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Configure importmap for the engine
|
|
40
|
+
initializer "solid_log_ui.importmap", before: "importmap" do |app|
|
|
41
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
42
|
+
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set up inflections for UI acronym
|
|
46
|
+
initializer "solid_log_ui.inflections" do
|
|
47
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
48
|
+
inflect.acronym "UI"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Load configuration if it exists
|
|
53
|
+
initializer "solid_log_ui.load_config" do
|
|
54
|
+
config_file = Rails.root.join("config/initializers/solid_log_ui.rb")
|
|
55
|
+
load config_file if File.exist?(config_file)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Add SilenceMiddleware to main app's middleware stack
|
|
59
|
+
# This prevents the UI from logging its own queries/requests
|
|
60
|
+
initializer "solid_log_ui.add_middleware" do |app|
|
|
61
|
+
app.middleware.use SolidLog::SilenceMiddleware
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Register Action Cable channels
|
|
65
|
+
initializer "solid_log_ui.action_cable" do
|
|
66
|
+
engine_root = root
|
|
67
|
+
config.to_prepare do
|
|
68
|
+
# Ensure channel classes are loaded and available to ActionCable
|
|
69
|
+
Dir[engine_root.join("app/channels/**/*_channel.rb")].each do |file|
|
|
70
|
+
require_dependency file
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/solid_log/ui.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "solid_log/core"
|
|
2
|
+
require_relative "ui/version"
|
|
3
|
+
require_relative "ui/configuration"
|
|
4
|
+
require_relative "ui/data_source"
|
|
5
|
+
require_relative "ui/api_client"
|
|
6
|
+
require_relative "ui/engine" if defined?(Rails)
|
|
7
|
+
|
|
8
|
+
module SolidLog
|
|
9
|
+
module UI
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :configuration
|
|
12
|
+
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield(configuration)
|
|
19
|
+
configuration.valid?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/solid_log-ui.rb
ADDED