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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c94d0fde51404dd97f27e8d437a9c9edab5366ba360bbd2719b0a658a28c5fe
4
- data.tar.gz: 1addb2c0e3b2471fdadccbd3741905747679b1e6475e3f6a12f6a5f3a44f5193
3
+ metadata.gz: 6c115e88f98776a378b9c5938a31e7c4d09e26d255079da4847ce6b8a406671e
4
+ data.tar.gz: 5f77c6da8f89be28d3e08854013b4a980107b0aeba18342c6dbf91e55629bbfa
5
5
  SHA512:
6
- metadata.gz: 8e3ab660ee56208d357bc6bf5baf5e81e89137df79b9fe5849fd8d3567934529335a77c264ae6de75432d07c3b1e1611827d2aa4c61fbba4c7b98edeebaccc72
7
- data.tar.gz: 7edb569df3716558642f22c9a55d680eb2f022ff6832a8364ead35824959b1354873dab3caa2b6039c5aa5430545c005ca480dbc1f26bafc5ad4c343c79bc7f6
6
+ metadata.gz: e5521a9483b8a616e7df54cfc30849b9cde8aca8be35bba06ad5905eb3724822d068e478e8bdad880b4587d529bc3a48551a30b2770e7c6b9abf3fdd428971aa
7
+ data.tar.gz: baa483b28aaf4e0e009355ace039ee6ab59811acc5a51b5f52b7467e71a3978e46fe3bc9de8391e48af6f84be306168f0c5de7e707b9b5b5e92aa28abadda823
data/config/cable.yml CHANGED
@@ -5,7 +5,4 @@ test:
5
5
  adapter: test
6
6
 
7
7
  production:
8
- adapter: solid_cable
9
- connects_to:
10
- database:
11
- writing: cable
8
+ adapter: async
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
- # Load routes
5
- require_relative 'config/routes'
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
- run SolidLog::Service::Application
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
- Rails.logger.info "SolidLog::Service: Started built-in Scheduler"
40
- Rails.logger.info " Parser interval: #{configuration.parser_interval}s"
41
- Rails.logger.info " Cache cleanup interval: #{configuration.cache_cleanup_interval}s"
42
- Rails.logger.info " Retention hour: #{configuration.retention_hour}:00"
43
- Rails.logger.info " Field analysis hour: #{configuration.field_analysis_hour}:00"
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::ParserJob'
61
+ # class_name: 'SolidLog::Core::Jobs::ParseJob'
62
62
  # )
63
63
 
64
- Rails.logger.info "SolidLog::Service: Using ActiveJob for background processing"
65
- Rails.logger.info " Make sure to configure recurring jobs in your host application"
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
- Rails.logger.info "SolidLog::Service: Manual job mode (no auto-scheduling)"
73
- Rails.logger.info " Set up cron jobs to run:"
74
- Rails.logger.info " - rails solid_log:parse_logs (every 10 seconds recommended)"
75
- Rails.logger.info " - rails solid_log:cache_cleanup (hourly recommended)"
76
- Rails.logger.info " - rails solid_log:retention (daily recommended)"
77
- Rails.logger.info " - rails solid_log:field_analysis (daily recommended)"
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
- Rails.logger.info "SolidLog::Service::Scheduler starting..."
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
- Rails.logger.info "SolidLog::Service::Scheduler started with #{@threads.size} threads"
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
- Rails.logger.info "SolidLog::Service::Scheduler stopping..."
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
- Rails.logger.info "SolidLog::Service::Scheduler stopped"
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
- SolidLog::ParserJob.perform_now
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
- Rails.logger.error "SolidLog::Scheduler: Parser job failed: #{e.message}"
76
- Rails.logger.error e.backtrace.join("\n")
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
- SolidLog::CacheCleanupJob.perform_now
89
+ # Use core's CacheCleanupJob (works with or without ActiveJob)
90
+ SolidLog::Core::Jobs::CacheCleanupJob.perform_now
89
91
  rescue => e
90
- Rails.logger.error "SolidLog::Scheduler: Cache cleanup failed: #{e.message}"
91
- Rails.logger.error e.backtrace.join("\n")
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
- SolidLog::RetentionJob.perform_now(
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
- Rails.logger.error "SolidLog::Scheduler: Retention job failed: #{e.message}"
131
- Rails.logger.error e.backtrace.join("\n")
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
- SolidLog::FieldAnalysisJob.perform_now(
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
- Rails.logger.error "SolidLog::Scheduler: Field analysis failed: #{e.message}"
140
- Rails.logger.error e.backtrace.join("\n")
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
@@ -1,5 +1,5 @@
1
1
  module SolidLog
2
2
  module Service
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.1"
4
4
  end
5
5
  end
@@ -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/engine"
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