solid_log-service 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/cable.yml +1 -4
- data/config.ru +53 -4
- data/lib/solid_log/service/job_processor.rb +14 -14
- data/lib/solid_log/service/rack_app.rb +382 -0
- data/lib/solid_log/service/scheduler.rb +22 -17
- data/lib/solid_log/service/version.rb +1 -1
- data/lib/solid_log/service.rb +18 -1
- metadata +64 -60
- data/app/controllers/solid_log/api/base_controller.rb +0 -80
- data/app/controllers/solid_log/api/v1/entries_controller.rb +0 -55
- data/app/controllers/solid_log/api/v1/facets_controller.rb +0 -41
- data/app/controllers/solid_log/api/v1/health_controller.rb +0 -29
- data/app/controllers/solid_log/api/v1/ingest_controller.rb +0 -80
- data/app/controllers/solid_log/api/v1/search_controller.rb +0 -31
- data/app/controllers/solid_log/api/v1/timelines_controller.rb +0 -43
- data/app/jobs/solid_log/application_job.rb +0 -4
- data/app/jobs/solid_log/cache_cleanup_job.rb +0 -16
- data/app/jobs/solid_log/field_analysis_job.rb +0 -26
- data/app/jobs/solid_log/parser_job.rb +0 -130
- data/app/jobs/solid_log/retention_job.rb +0 -24
- data/bin/solid_log_service +0 -75
- data/config/routes.rb +0 -26
- data/lib/solid_log/service/application.rb +0 -72
- data/lib/solid_log/service/engine.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c115e88f98776a378b9c5938a31e7c4d09e26d255079da4847ce6b8a406671e
|
|
4
|
+
data.tar.gz: 5f77c6da8f89be28d3e08854013b4a980107b0aeba18342c6dbf91e55629bbfa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5521a9483b8a616e7df54cfc30849b9cde8aca8be35bba06ad5905eb3724822d068e478e8bdad880b4587d529bc3a48551a30b2770e7c6b9abf3fdd428971aa
|
|
7
|
+
data.tar.gz: baa483b28aaf4e0e009355ace039ee6ab59811acc5a51b5f52b7467e71a3978e46fe3bc9de8391e48af6f84be306168f0c5de7e707b9b5b5e92aa28abadda823
|
data/config/cable.yml
CHANGED
data/config.ru
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
|
+
# This file is used by Rack-based servers to start the application.
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
|
|
4
|
+
# Only require bundler/setup if not using global gems (development/test)
|
|
5
|
+
# In production Docker, gems are installed globally and SKIP_BUNDLER is set
|
|
6
|
+
require 'bundler/setup' unless ENV['SKIP_BUNDLER'] == 'true'
|
|
7
|
+
|
|
8
|
+
require 'active_support'
|
|
9
|
+
require 'active_support/core_ext'
|
|
10
|
+
require 'active_record'
|
|
11
|
+
require 'action_cable'
|
|
12
|
+
|
|
13
|
+
# Set up ActiveRecord database connection
|
|
14
|
+
url = ENV["SOLIDLOG_DATABASE_URL"] || ENV["DATABASE_URL"]
|
|
15
|
+
adapter = ENV["SOLIDLOG_DB_ADAPTER"] || ENV["DB_ADAPTER"] || "sqlite3"
|
|
16
|
+
pool = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
17
|
+
|
|
18
|
+
db_config = if url&.include?("://")
|
|
19
|
+
{ url: url, pool: pool }
|
|
20
|
+
else
|
|
21
|
+
{ adapter: adapter, database: url || "storage/production_log.sqlite3", pool: pool }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ActiveRecord::Base.establish_connection(db_config)
|
|
25
|
+
|
|
26
|
+
# Load solid_log gems
|
|
27
|
+
require 'solid_log/core'
|
|
1
28
|
require_relative 'lib/solid_log/service'
|
|
2
|
-
require_relative 'lib/solid_log/service/application'
|
|
3
29
|
|
|
4
|
-
#
|
|
5
|
-
|
|
30
|
+
# Configure ActionCable for live-tailing
|
|
31
|
+
cable_config_path = File.join(__dir__, 'config', 'cable.yml')
|
|
32
|
+
if File.exist?(cable_config_path)
|
|
33
|
+
require 'yaml'
|
|
34
|
+
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'production'
|
|
35
|
+
cable_config = YAML.load_file(cable_config_path)[env]
|
|
36
|
+
ActionCable.server.config.cable = cable_config if cable_config
|
|
37
|
+
end
|
|
38
|
+
ActionCable.server.config.logger = SolidLog::Service.logger
|
|
39
|
+
|
|
40
|
+
# Load configuration file if it exists
|
|
41
|
+
config_file = File.join(__dir__, 'config', 'solid_log_service.rb')
|
|
42
|
+
require config_file if File.exist?(config_file)
|
|
43
|
+
|
|
44
|
+
# Run pending migrations (auto-migrate unless disabled)
|
|
45
|
+
unless ENV['SKIP_AUTO_MIGRATE'] == 'true'
|
|
46
|
+
SolidLog::MigrationRunner.run_pending_migrations
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Start job processor
|
|
50
|
+
SolidLog::Service.start!
|
|
51
|
+
|
|
52
|
+
# Shutdown hook
|
|
53
|
+
at_exit { SolidLog::Service.stop! }
|
|
6
54
|
|
|
7
|
-
|
|
55
|
+
# Run Rack app
|
|
56
|
+
run SolidLog::Service::RackApp.new
|
|
@@ -36,11 +36,11 @@ module SolidLog
|
|
|
36
36
|
@scheduler = Scheduler.new(configuration)
|
|
37
37
|
@scheduler.start
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
SolidLog::Service.logger.info "SolidLog::Service: Started built-in Scheduler"
|
|
40
|
+
SolidLog::Service.logger.info " Parser interval: #{configuration.parser_interval}s"
|
|
41
|
+
SolidLog::Service.logger.info " Cache cleanup interval: #{configuration.cache_cleanup_interval}s"
|
|
42
|
+
SolidLog::Service.logger.info " Retention hour: #{configuration.retention_hour}:00"
|
|
43
|
+
SolidLog::Service.logger.info " Field analysis hour: #{configuration.field_analysis_hour}:00"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def stop_scheduler
|
|
@@ -58,23 +58,23 @@ module SolidLog
|
|
|
58
58
|
# SolidQueue::RecurringTask.create!(
|
|
59
59
|
# key: 'solidlog_parser',
|
|
60
60
|
# schedule: 'every 10 seconds',
|
|
61
|
-
# class_name: 'SolidLog::
|
|
61
|
+
# class_name: 'SolidLog::Core::Jobs::ParseJob'
|
|
62
62
|
# )
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
SolidLog::Service.logger.info "SolidLog::Service: Using ActiveJob for background processing"
|
|
65
|
+
SolidLog::Service.logger.info " Make sure to configure recurring jobs in your host application"
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def setup_manual
|
|
69
69
|
# User manages scheduling via cron or other external scheduler
|
|
70
70
|
# No setup needed
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
SolidLog::Service.logger.info "SolidLog::Service: Manual job mode (no auto-scheduling)"
|
|
73
|
+
SolidLog::Service.logger.info " Set up cron jobs to run:"
|
|
74
|
+
SolidLog::Service.logger.info " - rails solid_log:parse_logs (every 10 seconds recommended)"
|
|
75
|
+
SolidLog::Service.logger.info " - rails solid_log:cache_cleanup (hourly recommended)"
|
|
76
|
+
SolidLog::Service.logger.info " - rails solid_log:retention (daily recommended)"
|
|
77
|
+
SolidLog::Service.logger.info " - rails solid_log:field_analysis (daily recommended)"
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module SolidLog
|
|
5
|
+
module Service
|
|
6
|
+
class RackApp
|
|
7
|
+
def call(env)
|
|
8
|
+
request = Rack::Request.new(env)
|
|
9
|
+
method = request.request_method
|
|
10
|
+
path = request.path_info
|
|
11
|
+
|
|
12
|
+
# Route matching
|
|
13
|
+
route(method, path, request)
|
|
14
|
+
rescue JSON::ParserError => e
|
|
15
|
+
bad_request("Invalid JSON: #{e.message}")
|
|
16
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
17
|
+
unprocessable_entity("Validation error", e.record.errors.full_messages)
|
|
18
|
+
rescue => e
|
|
19
|
+
internal_error(e)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Router - matches HTTP method and path to handler using pattern matching
|
|
25
|
+
def route(method, path, request)
|
|
26
|
+
# Split path into segments for easier matching
|
|
27
|
+
segments = path.split("/").reject(&:empty?)
|
|
28
|
+
|
|
29
|
+
# Pattern match on [method, segments]
|
|
30
|
+
case [method, segments]
|
|
31
|
+
# POST routes
|
|
32
|
+
in ["POST", ["api", "v1", "ingest"]]
|
|
33
|
+
handle_ingest(request)
|
|
34
|
+
in ["POST", ["api", "v1", "search"]]
|
|
35
|
+
handle_search(request)
|
|
36
|
+
|
|
37
|
+
# GET routes - static
|
|
38
|
+
in ["GET", ["api", "v1", "entries"]]
|
|
39
|
+
handle_entries_index(request)
|
|
40
|
+
in ["GET", ["api", "v1", "facets"]]
|
|
41
|
+
handle_facets(request)
|
|
42
|
+
in ["GET", ["api", "v1", "facets", "all"]]
|
|
43
|
+
handle_facets_all(request)
|
|
44
|
+
in ["GET", ["health"]] | ["GET", ["api", "v1", "health"]]
|
|
45
|
+
handle_health(request)
|
|
46
|
+
in ["GET", ["cable"]]
|
|
47
|
+
ActionCable.server.call(request.env)
|
|
48
|
+
|
|
49
|
+
# GET routes - with parameters
|
|
50
|
+
in ["GET", ["api", "v1", "entries", id]]
|
|
51
|
+
handle_entries_show(request, id)
|
|
52
|
+
in ["GET", ["api", "v1", "timeline", "request", request_id]]
|
|
53
|
+
handle_timeline_request(request, request_id)
|
|
54
|
+
in ["GET", ["api", "v1", "timeline", "job", job_id]]
|
|
55
|
+
handle_timeline_job(request, job_id)
|
|
56
|
+
|
|
57
|
+
else
|
|
58
|
+
not_found
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# POST /api/v1/ingest
|
|
63
|
+
def handle_ingest(request)
|
|
64
|
+
token = authenticate!(request)
|
|
65
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
66
|
+
|
|
67
|
+
payload = parse_ingest_payload(request)
|
|
68
|
+
|
|
69
|
+
if payload.blank?
|
|
70
|
+
return bad_request("Empty payload")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
entries = Array.wrap(payload)
|
|
74
|
+
|
|
75
|
+
max = SolidLog.configuration.max_batch_size
|
|
76
|
+
if entries.size > max
|
|
77
|
+
return response(413, {
|
|
78
|
+
error: "Batch too large",
|
|
79
|
+
max_size: max,
|
|
80
|
+
received: entries.size
|
|
81
|
+
})
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Create raw entries
|
|
85
|
+
raw_entries = entries.map do |entry|
|
|
86
|
+
{
|
|
87
|
+
token_id: token.id,
|
|
88
|
+
payload: entry.to_json,
|
|
89
|
+
received_at: Time.current,
|
|
90
|
+
parsed: false
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Bulk insert
|
|
95
|
+
SolidLog.without_logging do
|
|
96
|
+
SolidLog::RawEntry.insert_all(raw_entries)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
token.touch_last_used!
|
|
100
|
+
|
|
101
|
+
response(202, {
|
|
102
|
+
status: "accepted",
|
|
103
|
+
count: entries.size,
|
|
104
|
+
message: "Log entries queued for processing"
|
|
105
|
+
})
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# GET /api/v1/entries
|
|
109
|
+
def handle_entries_index(request)
|
|
110
|
+
token = authenticate!(request)
|
|
111
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
112
|
+
|
|
113
|
+
search_params = build_filter_params(request)
|
|
114
|
+
search_service = SolidLog::SearchService.new(search_params)
|
|
115
|
+
entries = search_service.search
|
|
116
|
+
|
|
117
|
+
token.touch_last_used!
|
|
118
|
+
|
|
119
|
+
response(200, {
|
|
120
|
+
entries: entries.as_json(methods: [:extra_fields_hash]),
|
|
121
|
+
total: entries.count,
|
|
122
|
+
limit: request.params["limit"]&.to_i || 100
|
|
123
|
+
})
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# GET /api/v1/entries/:id
|
|
127
|
+
def handle_entries_show(request, id)
|
|
128
|
+
token = authenticate!(request)
|
|
129
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
130
|
+
|
|
131
|
+
entry = SolidLog::Entry.find(id)
|
|
132
|
+
token.touch_last_used!
|
|
133
|
+
|
|
134
|
+
response(200, {
|
|
135
|
+
entry: entry.as_json(methods: [:extra_fields_hash])
|
|
136
|
+
})
|
|
137
|
+
rescue ActiveRecord::RecordNotFound
|
|
138
|
+
not_found("Entry not found")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# GET /api/v1/facets?field=level or GET /api/v1/facets (returns all)
|
|
142
|
+
def handle_facets(request)
|
|
143
|
+
token = authenticate!(request)
|
|
144
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
145
|
+
|
|
146
|
+
field = request.params["field"]
|
|
147
|
+
|
|
148
|
+
# If no field parameter, return all facets (same as /facets/all)
|
|
149
|
+
if field.blank?
|
|
150
|
+
return handle_facets_all(request)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
limit = request.params["limit"]&.to_i || 100
|
|
154
|
+
facets = SolidLog::Entry.facets_for(field, limit: limit)
|
|
155
|
+
|
|
156
|
+
token.touch_last_used!
|
|
157
|
+
|
|
158
|
+
response(200, {
|
|
159
|
+
field: field,
|
|
160
|
+
values: facets,
|
|
161
|
+
total: facets.size
|
|
162
|
+
})
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# GET /api/v1/facets/all
|
|
166
|
+
def handle_facets_all(request)
|
|
167
|
+
token = authenticate!(request)
|
|
168
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
169
|
+
|
|
170
|
+
facets = {
|
|
171
|
+
level: SolidLog::Entry.facets_for("level"),
|
|
172
|
+
app: SolidLog::Entry.facets_for("app"),
|
|
173
|
+
env: SolidLog::Entry.facets_for("env"),
|
|
174
|
+
controller: SolidLog::Entry.facets_for("controller", limit: 50),
|
|
175
|
+
action: SolidLog::Entry.facets_for("action", limit: 50),
|
|
176
|
+
method: SolidLog::Entry.facets_for("method"),
|
|
177
|
+
status_code: SolidLog::Entry.facets_for("status_code")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
token.touch_last_used!
|
|
181
|
+
|
|
182
|
+
response(200, { facets: facets })
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# POST /api/v1/search
|
|
186
|
+
def handle_search(request)
|
|
187
|
+
token = authenticate!(request)
|
|
188
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
189
|
+
|
|
190
|
+
# Parse JSON body
|
|
191
|
+
body = request.body.read
|
|
192
|
+
params = body.present? ? JSON.parse(body) : {}
|
|
193
|
+
|
|
194
|
+
query = params["q"] || params["query"] || request.params["q"] || request.params["query"]
|
|
195
|
+
if query.blank?
|
|
196
|
+
return bad_request("Query parameter required")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
search_params = {
|
|
200
|
+
query: query,
|
|
201
|
+
limit: params["limit"] || request.params["limit"]
|
|
202
|
+
}.compact
|
|
203
|
+
|
|
204
|
+
search_service = SolidLog::SearchService.new(search_params)
|
|
205
|
+
entries = search_service.search
|
|
206
|
+
|
|
207
|
+
token.touch_last_used!
|
|
208
|
+
|
|
209
|
+
response(200, {
|
|
210
|
+
query: query,
|
|
211
|
+
entries: entries.as_json(methods: [:extra_fields_hash]),
|
|
212
|
+
total: entries.count,
|
|
213
|
+
limit: (params["limit"] || request.params["limit"])&.to_i || 100
|
|
214
|
+
})
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# GET /api/v1/timelines/request/:request_id
|
|
218
|
+
def handle_timeline_request(request, request_id)
|
|
219
|
+
token = authenticate!(request)
|
|
220
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
221
|
+
|
|
222
|
+
if request_id.blank?
|
|
223
|
+
return bad_request("Request ID required")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
entries = SolidLog::CorrelationService.request_timeline(request_id)
|
|
227
|
+
stats = SolidLog::CorrelationService.request_stats(request_id)
|
|
228
|
+
|
|
229
|
+
token.touch_last_used!
|
|
230
|
+
|
|
231
|
+
response(200, {
|
|
232
|
+
request_id: request_id,
|
|
233
|
+
entries: entries.as_json(methods: [:extra_fields_hash]),
|
|
234
|
+
stats: stats
|
|
235
|
+
})
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# GET /api/v1/timelines/job/:job_id
|
|
239
|
+
def handle_timeline_job(request, job_id)
|
|
240
|
+
token = authenticate!(request)
|
|
241
|
+
return token unless token.is_a?(SolidLog::Token)
|
|
242
|
+
|
|
243
|
+
if job_id.blank?
|
|
244
|
+
return bad_request("Job ID required")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
entries = SolidLog::CorrelationService.job_timeline(job_id)
|
|
248
|
+
stats = SolidLog::CorrelationService.job_stats(job_id)
|
|
249
|
+
|
|
250
|
+
token.touch_last_used!
|
|
251
|
+
|
|
252
|
+
response(200, {
|
|
253
|
+
job_id: job_id,
|
|
254
|
+
entries: entries.as_json(methods: [:extra_fields_hash]),
|
|
255
|
+
stats: stats
|
|
256
|
+
})
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# GET /health or /api/v1/health (no authentication)
|
|
260
|
+
def handle_health(request)
|
|
261
|
+
metrics = SolidLog::HealthService.metrics
|
|
262
|
+
|
|
263
|
+
status_code = case metrics[:parsing][:health_status]
|
|
264
|
+
when "critical"
|
|
265
|
+
503
|
|
266
|
+
when "warning", "degraded"
|
|
267
|
+
200
|
|
268
|
+
else
|
|
269
|
+
200
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
response(status_code, {
|
|
273
|
+
status: metrics[:parsing][:health_status],
|
|
274
|
+
timestamp: Time.current.iso8601,
|
|
275
|
+
metrics: metrics
|
|
276
|
+
})
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Authentication
|
|
280
|
+
def authenticate!(request)
|
|
281
|
+
header = request.get_header("HTTP_AUTHORIZATION")
|
|
282
|
+
unless header&.match?(/\A(Bearer|bearer) /)
|
|
283
|
+
return unauthorized("Missing or invalid Authorization header")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
token_value = header.sub(/\A(Bearer|bearer) /, "")
|
|
287
|
+
token = SolidLog::Token.authenticate(token_value)
|
|
288
|
+
|
|
289
|
+
unless token
|
|
290
|
+
return unauthorized("Invalid token")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
token
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Parse ingest payload (supports JSON, JSON array, and NDJSON)
|
|
297
|
+
def parse_ingest_payload(request)
|
|
298
|
+
# Check for _json param (Rails-style JSON array parsing)
|
|
299
|
+
return request.params["_json"] if request.params["_json"]
|
|
300
|
+
|
|
301
|
+
body = request.body.read
|
|
302
|
+
return [] if body.blank?
|
|
303
|
+
|
|
304
|
+
# Check if it's NDJSON (multiple lines) or regular JSON
|
|
305
|
+
if body.include?("\n")
|
|
306
|
+
# NDJSON format
|
|
307
|
+
body.lines.map do |line|
|
|
308
|
+
JSON.parse(line.strip) unless line.strip.empty?
|
|
309
|
+
end.compact
|
|
310
|
+
else
|
|
311
|
+
# Regular JSON (single entry or array)
|
|
312
|
+
JSON.parse(body)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Build filter params from request
|
|
317
|
+
def build_filter_params(request)
|
|
318
|
+
params = request.params
|
|
319
|
+
search_params = {}
|
|
320
|
+
|
|
321
|
+
# Handle filters hash if present
|
|
322
|
+
if params["filters"].is_a?(Hash)
|
|
323
|
+
filters = params["filters"]
|
|
324
|
+
search_params[:levels] = [filters["level"]].compact if filters["level"].to_s.present?
|
|
325
|
+
search_params[:app] = filters["app"] if filters["app"].to_s.present?
|
|
326
|
+
search_params[:env] = filters["env"] if filters["env"].to_s.present?
|
|
327
|
+
search_params[:controller] = filters["controller"] if filters["controller"].to_s.present?
|
|
328
|
+
search_params[:action] = filters["action"] if filters["action"].to_s.present?
|
|
329
|
+
search_params[:path] = filters["path"] if filters["path"].to_s.present?
|
|
330
|
+
search_params[:method] = filters["method"] if filters["method"].to_s.present?
|
|
331
|
+
search_params[:status_code] = filters["status_code"] if filters["status_code"].to_s.present?
|
|
332
|
+
search_params[:start_time] = filters["start_time"] if filters["start_time"].to_s.present?
|
|
333
|
+
search_params[:end_time] = filters["end_time"] if filters["end_time"].to_s.present?
|
|
334
|
+
search_params[:min_duration] = filters["min_duration"] if filters["min_duration"].to_s.present?
|
|
335
|
+
search_params[:max_duration] = filters["max_duration"] if filters["max_duration"].to_s.present?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
search_params[:query] = params["q"] if params["q"].to_s.present?
|
|
339
|
+
search_params[:limit] = params["limit"] if params["limit"].to_s.present?
|
|
340
|
+
|
|
341
|
+
search_params
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Response helpers
|
|
345
|
+
def response(status, data)
|
|
346
|
+
[status, json_headers, [JSON.generate(data)]]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def unauthorized(message = "Unauthorized")
|
|
350
|
+
[401, json_headers, [JSON.generate({ error: message })]]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def bad_request(message)
|
|
354
|
+
[400, json_headers, [JSON.generate({ error: message })]]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def not_found(message = "Not found")
|
|
358
|
+
[404, json_headers, [JSON.generate({ error: message })]]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def unprocessable_entity(error, details = nil)
|
|
362
|
+
data = { error: error }
|
|
363
|
+
data[:details] = details if details
|
|
364
|
+
[422, json_headers, [JSON.generate(data)]]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def internal_error(exception)
|
|
368
|
+
SolidLog::Service.logger.error "SolidLog API Error: #{exception.message}"
|
|
369
|
+
SolidLog::Service.logger.error exception.backtrace.join("\n")
|
|
370
|
+
|
|
371
|
+
[500, json_headers, [JSON.generate({
|
|
372
|
+
error: "Internal server error",
|
|
373
|
+
message: exception.message
|
|
374
|
+
})]]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def json_headers
|
|
378
|
+
{ "Content-Type" => "application/json" }
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
@@ -20,7 +20,7 @@ module SolidLog
|
|
|
20
20
|
@running = true
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
SolidLog::Service.logger.info "SolidLog::Service::Scheduler starting..."
|
|
24
24
|
|
|
25
25
|
# Parser job - frequent (configurable, default 10s)
|
|
26
26
|
thread = Thread.new { parser_loop }
|
|
@@ -37,13 +37,13 @@ module SolidLog
|
|
|
37
37
|
thread.abort_on_exception = true
|
|
38
38
|
@threads << thread
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
SolidLog::Service.logger.info "SolidLog::Service::Scheduler started with #{@threads.size} threads"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def stop
|
|
44
44
|
return unless @running
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
SolidLog::Service.logger.info "SolidLog::Service::Scheduler stopping..."
|
|
47
47
|
|
|
48
48
|
@mutex.synchronize do
|
|
49
49
|
@running = false
|
|
@@ -56,7 +56,7 @@ module SolidLog
|
|
|
56
56
|
@threads.each { |t| t.kill if t.alive? }
|
|
57
57
|
@threads.clear
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
SolidLog::Service.logger.info "SolidLog::Service::Scheduler stopped"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def running?
|
|
@@ -70,10 +70,11 @@ module SolidLog
|
|
|
70
70
|
break unless @running
|
|
71
71
|
|
|
72
72
|
begin
|
|
73
|
-
|
|
73
|
+
# Use core's ParseJob (works with or without ActiveJob)
|
|
74
|
+
SolidLog::Core::Jobs::ParseJob.perform_now(batch_size: @config.parser_batch_size)
|
|
74
75
|
rescue => e
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
SolidLog::Service.logger.error "SolidLog::Scheduler: Parser job failed: #{e.message}"
|
|
77
|
+
SolidLog::Service.logger.error e.backtrace.join("\n")
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
sleep @config.parser_interval
|
|
@@ -85,10 +86,11 @@ module SolidLog
|
|
|
85
86
|
break unless @running
|
|
86
87
|
|
|
87
88
|
begin
|
|
88
|
-
|
|
89
|
+
# Use core's CacheCleanupJob (works with or without ActiveJob)
|
|
90
|
+
SolidLog::Core::Jobs::CacheCleanupJob.perform_now
|
|
89
91
|
rescue => e
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
SolidLog::Service.logger.error "SolidLog::Scheduler: Cache cleanup failed: #{e.message}"
|
|
93
|
+
SolidLog::Service.logger.error e.backtrace.join("\n")
|
|
92
94
|
end
|
|
93
95
|
|
|
94
96
|
sleep @config.cache_cleanup_interval
|
|
@@ -122,22 +124,25 @@ module SolidLog
|
|
|
122
124
|
case job_name
|
|
123
125
|
when :retention
|
|
124
126
|
begin
|
|
125
|
-
|
|
127
|
+
# Use core's RetentionJob (works with or without ActiveJob)
|
|
128
|
+
SolidLog::Core::Jobs::RetentionJob.perform_now(
|
|
126
129
|
retention_days: @config.retention_days,
|
|
127
|
-
error_retention_days: @config.error_retention_days
|
|
130
|
+
error_retention_days: @config.error_retention_days,
|
|
131
|
+
max_entries: SolidLog.configuration.max_entries
|
|
128
132
|
)
|
|
129
133
|
rescue => e
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
SolidLog::Service.logger.error "SolidLog::Scheduler: Retention job failed: #{e.message}"
|
|
135
|
+
SolidLog::Service.logger.error e.backtrace.join("\n")
|
|
132
136
|
end
|
|
133
137
|
when :field_analysis
|
|
134
138
|
begin
|
|
135
|
-
|
|
139
|
+
# Use core's FieldAnalysisJob (works with or without ActiveJob)
|
|
140
|
+
SolidLog::Core::Jobs::FieldAnalysisJob.perform_now(
|
|
136
141
|
auto_promote: @config.auto_promote_fields
|
|
137
142
|
)
|
|
138
143
|
rescue => e
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
SolidLog::Service.logger.error "SolidLog::Scheduler: Field analysis failed: #{e.message}"
|
|
145
|
+
SolidLog::Service.logger.error e.backtrace.join("\n")
|
|
141
146
|
end
|
|
142
147
|
end
|
|
143
148
|
end
|
data/lib/solid_log/service.rb
CHANGED
|
@@ -3,7 +3,7 @@ require_relative "service/version"
|
|
|
3
3
|
require_relative "service/configuration"
|
|
4
4
|
require_relative "service/scheduler"
|
|
5
5
|
require_relative "service/job_processor"
|
|
6
|
-
require_relative "service/
|
|
6
|
+
require_relative "service/rack_app"
|
|
7
7
|
|
|
8
8
|
module SolidLog
|
|
9
9
|
module Service
|
|
@@ -23,8 +23,25 @@ module SolidLog
|
|
|
23
23
|
@configuration = Configuration.new
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Logger - delegates to SolidLog.logger (from core)
|
|
27
|
+
def logger
|
|
28
|
+
SolidLog.logger
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def logger=(logger)
|
|
32
|
+
SolidLog.logger = logger
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
# Start the service (job processor)
|
|
27
36
|
def start!
|
|
37
|
+
# Configure core logger if not already set
|
|
38
|
+
unless SolidLog.logger
|
|
39
|
+
require "logger"
|
|
40
|
+
SolidLog.logger = Logger.new(STDOUT).tap do |log|
|
|
41
|
+
log.level = ENV.fetch("LOG_LEVEL", "info").to_sym
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
JobProcessor.setup
|
|
29
46
|
end
|
|
30
47
|
|