solid_log-service 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 +112 -0
- data/Rakefile +11 -0
- data/app/controllers/solid_log/api/base_controller.rb +80 -0
- data/app/controllers/solid_log/api/v1/entries_controller.rb +55 -0
- data/app/controllers/solid_log/api/v1/facets_controller.rb +41 -0
- data/app/controllers/solid_log/api/v1/health_controller.rb +29 -0
- data/app/controllers/solid_log/api/v1/ingest_controller.rb +80 -0
- data/app/controllers/solid_log/api/v1/search_controller.rb +31 -0
- data/app/controllers/solid_log/api/v1/timelines_controller.rb +43 -0
- data/app/jobs/solid_log/application_job.rb +4 -0
- data/app/jobs/solid_log/cache_cleanup_job.rb +16 -0
- data/app/jobs/solid_log/field_analysis_job.rb +26 -0
- data/app/jobs/solid_log/parser_job.rb +130 -0
- data/app/jobs/solid_log/retention_job.rb +24 -0
- data/bin/solid_log_service +75 -0
- data/config/cable.yml +11 -0
- data/config/routes.rb +26 -0
- data/config.ru +7 -0
- data/lib/solid_log/service/application.rb +72 -0
- data/lib/solid_log/service/configuration.rb +84 -0
- data/lib/solid_log/service/engine.rb +25 -0
- data/lib/solid_log/service/job_processor.rb +82 -0
- data/lib/solid_log/service/scheduler.rb +146 -0
- data/lib/solid_log/service/version.rb +5 -0
- data/lib/solid_log/service.rb +37 -0
- data/lib/solid_log-service.rb +2 -0
- metadata +244 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "action_cable" if defined?(Rails)
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
class ParserJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
# Process a batch of unparsed raw entries
|
|
8
|
+
def perform(batch_size: nil)
|
|
9
|
+
batch_size ||= SolidLog.configuration.parser_batch_size
|
|
10
|
+
|
|
11
|
+
SolidLog.without_logging do
|
|
12
|
+
# Claim a batch of unparsed entries
|
|
13
|
+
raw_entries = RawEntry.claim_batch(batch_size: batch_size)
|
|
14
|
+
|
|
15
|
+
return if raw_entries.empty?
|
|
16
|
+
|
|
17
|
+
Rails.logger.info "SolidLog::ParserJob: Processing #{raw_entries.size} raw entries"
|
|
18
|
+
|
|
19
|
+
# Process each entry
|
|
20
|
+
entries_to_insert = []
|
|
21
|
+
fields_to_track = {}
|
|
22
|
+
|
|
23
|
+
raw_entries.each do |raw_entry|
|
|
24
|
+
begin
|
|
25
|
+
# Parse the raw payload
|
|
26
|
+
parsed = Parser.parse(raw_entry.payload)
|
|
27
|
+
|
|
28
|
+
# Extract dynamic fields for field registry
|
|
29
|
+
extra_fields = parsed.delete(:extra_fields) || {}
|
|
30
|
+
track_fields(fields_to_track, extra_fields)
|
|
31
|
+
|
|
32
|
+
# Prepare entry for insertion
|
|
33
|
+
entry_data = {
|
|
34
|
+
raw_id: raw_entry.id,
|
|
35
|
+
timestamp: parsed[:timestamp],
|
|
36
|
+
created_at: Time.current, # When entry was parsed/created
|
|
37
|
+
level: parsed[:level],
|
|
38
|
+
app: parsed[:app],
|
|
39
|
+
env: parsed[:env],
|
|
40
|
+
message: parsed[:message],
|
|
41
|
+
request_id: parsed[:request_id],
|
|
42
|
+
job_id: parsed[:job_id],
|
|
43
|
+
duration: parsed[:duration],
|
|
44
|
+
status_code: parsed[:status_code],
|
|
45
|
+
controller: parsed[:controller],
|
|
46
|
+
action: parsed[:action],
|
|
47
|
+
path: parsed[:path],
|
|
48
|
+
method: parsed[:method],
|
|
49
|
+
extra_fields: extra_fields.to_json
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
entries_to_insert << entry_data
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
Rails.logger.error "SolidLog::ParserJob: Failed to parse entry #{raw_entry.id}: #{e.message}"
|
|
55
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
56
|
+
# Leave entry unparsed so it can be retried or investigated
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Bulk insert parsed entries
|
|
61
|
+
if entries_to_insert.any?
|
|
62
|
+
raw_ids = entries_to_insert.map { |e| e[:raw_id] }
|
|
63
|
+
|
|
64
|
+
Entry.insert_all(entries_to_insert)
|
|
65
|
+
Rails.logger.info "SolidLog::ParserJob: Inserted #{entries_to_insert.size} entries"
|
|
66
|
+
|
|
67
|
+
# Broadcast new entry IDs for live tail
|
|
68
|
+
begin
|
|
69
|
+
new_entry_ids = Entry.where(raw_id: raw_ids).pluck(:id)
|
|
70
|
+
if new_entry_ids.any?
|
|
71
|
+
ActionCable.server.broadcast(
|
|
72
|
+
"solid_log_new_entries",
|
|
73
|
+
{ entry_ids: new_entry_ids }
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
rescue NameError => e
|
|
77
|
+
Rails.logger.error "SolidLog::ParserJob: ActionCable not available: #{e.message}"
|
|
78
|
+
rescue => e
|
|
79
|
+
Rails.logger.error "SolidLog::ParserJob: Failed to broadcast: #{e.class} - #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Update field registry
|
|
84
|
+
update_field_registry(fields_to_track)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Track field occurrences for the registry
|
|
91
|
+
def track_fields(fields_hash, extra_fields)
|
|
92
|
+
extra_fields.each do |key, value|
|
|
93
|
+
fields_hash[key] ||= { values: [], count: 0 }
|
|
94
|
+
fields_hash[key][:count] += 1
|
|
95
|
+
fields_hash[key][:type] ||= infer_field_type(value)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Update the field registry with tracked fields
|
|
100
|
+
def update_field_registry(fields_hash)
|
|
101
|
+
fields_hash.each do |name, data|
|
|
102
|
+
field = Field.find_or_initialize_by(name: name)
|
|
103
|
+
field.field_type ||= data[:type]
|
|
104
|
+
field.usage_count += data[:count]
|
|
105
|
+
field.last_seen_at = Time.current
|
|
106
|
+
field.save!
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Infer field type from value
|
|
111
|
+
def infer_field_type(value)
|
|
112
|
+
case value
|
|
113
|
+
when String
|
|
114
|
+
"string"
|
|
115
|
+
when Numeric
|
|
116
|
+
"number"
|
|
117
|
+
when TrueClass, FalseClass
|
|
118
|
+
"boolean"
|
|
119
|
+
when Time, DateTime, Date
|
|
120
|
+
"datetime"
|
|
121
|
+
when Array
|
|
122
|
+
"array"
|
|
123
|
+
when Hash
|
|
124
|
+
"object"
|
|
125
|
+
else
|
|
126
|
+
"string"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module SolidLog
|
|
2
|
+
class RetentionJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(retention_days: 30, error_retention_days: 90, vacuum: false)
|
|
6
|
+
SolidLog.without_logging do
|
|
7
|
+
Rails.logger.info "SolidLog::RetentionJob: Starting cleanup (retention: #{retention_days} days, errors: #{error_retention_days} days)"
|
|
8
|
+
|
|
9
|
+
stats = RetentionService.cleanup(
|
|
10
|
+
retention_days: retention_days,
|
|
11
|
+
error_retention_days: error_retention_days
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
Rails.logger.info "SolidLog::RetentionJob: Deleted #{stats[:entries_deleted]} entries, #{stats[:raw_deleted]} raw entries, cleared #{stats[:cache_cleared]} cache entries"
|
|
15
|
+
|
|
16
|
+
if vacuum
|
|
17
|
+
Rails.logger.info "SolidLog::RetentionJob: Running VACUUM..."
|
|
18
|
+
RetentionService.vacuum_database
|
|
19
|
+
Rails.logger.info "SolidLog::RetentionJob: VACUUM complete"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'rack'
|
|
5
|
+
require_relative '../lib/solid_log/service'
|
|
6
|
+
|
|
7
|
+
# Parse command line options
|
|
8
|
+
options = {
|
|
9
|
+
port: ENV['PORT'] || SolidLog::Service.configuration.port || 3001,
|
|
10
|
+
bind: ENV['BIND'] || SolidLog::Service.configuration.bind || '0.0.0.0',
|
|
11
|
+
environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'production'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ARGV.each do |arg|
|
|
15
|
+
case arg
|
|
16
|
+
when /^--port=(\d+)$/
|
|
17
|
+
options[:port] = $1.to_i
|
|
18
|
+
when /^--bind=(.+)$/
|
|
19
|
+
options[:bind] = $1
|
|
20
|
+
when /^--environment=(.+)$/
|
|
21
|
+
options[:environment] = $1
|
|
22
|
+
when '--help', '-h'
|
|
23
|
+
puts <<~HELP
|
|
24
|
+
SolidLog Service - Standalone log ingestion and processing service
|
|
25
|
+
|
|
26
|
+
Usage: solid_log_service [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--port=PORT Port to bind to (default: 3001)
|
|
30
|
+
--bind=ADDRESS Address to bind to (default: 0.0.0.0)
|
|
31
|
+
--environment=ENV Environment (default: production)
|
|
32
|
+
--help, -h Show this help message
|
|
33
|
+
|
|
34
|
+
Environment Variables:
|
|
35
|
+
PORT Port to bind to
|
|
36
|
+
BIND Address to bind to
|
|
37
|
+
RAILS_ENV Rails environment
|
|
38
|
+
DATABASE_URL Database connection string
|
|
39
|
+
LOG_LEVEL Log level (debug, info, warn, error)
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
solid_log_service
|
|
43
|
+
solid_log_service --port=8080 --bind=127.0.0.1
|
|
44
|
+
PORT=8080 solid_log_service
|
|
45
|
+
|
|
46
|
+
Configuration:
|
|
47
|
+
Create config/solid_log_service.rb to configure the service.
|
|
48
|
+
See README.md for configuration options.
|
|
49
|
+
HELP
|
|
50
|
+
exit 0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Set environment
|
|
55
|
+
ENV['RAILS_ENV'] = options[:environment]
|
|
56
|
+
ENV['RACK_ENV'] = options[:environment]
|
|
57
|
+
|
|
58
|
+
puts "Starting SolidLog Service..."
|
|
59
|
+
puts " Environment: #{options[:environment]}"
|
|
60
|
+
puts " Binding: #{options[:bind]}:#{options[:port]}"
|
|
61
|
+
|
|
62
|
+
# Load the application
|
|
63
|
+
require_relative '../config.ru'
|
|
64
|
+
|
|
65
|
+
# Run with Puma
|
|
66
|
+
require 'puma/cli'
|
|
67
|
+
|
|
68
|
+
puma_args = [
|
|
69
|
+
'--bind', "tcp://#{options[:bind]}:#{options[:port]}",
|
|
70
|
+
'--environment', options[:environment],
|
|
71
|
+
'--workers', ENV.fetch('WEB_CONCURRENCY', '2'),
|
|
72
|
+
'--threads', "#{ENV.fetch('RAILS_MIN_THREADS', '5')}:#{ENV.fetch('RAILS_MAX_THREADS', '5')}"
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
Puma::CLI.new(puma_args).run
|
data/config/cable.yml
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
SolidLog::Service::Engine.routes.draw do
|
|
2
|
+
namespace :api do
|
|
3
|
+
namespace :v1 do
|
|
4
|
+
# Ingestion
|
|
5
|
+
post "ingest", to: "ingest#create"
|
|
6
|
+
|
|
7
|
+
# Queries
|
|
8
|
+
resources :entries, only: [:index, :show]
|
|
9
|
+
post "search", to: "search#create"
|
|
10
|
+
|
|
11
|
+
# Facets
|
|
12
|
+
get "facets", to: "facets#index"
|
|
13
|
+
get "facets/all", to: "facets#all", as: :all_facets
|
|
14
|
+
|
|
15
|
+
# Timelines
|
|
16
|
+
get "timelines/request/:request_id", to: "timelines#show_request", as: :timeline_request
|
|
17
|
+
get "timelines/job/:job_id", to: "timelines#show_job", as: :timeline_job
|
|
18
|
+
|
|
19
|
+
# Health
|
|
20
|
+
get "health", to: "health#show"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Root health check
|
|
25
|
+
get "/health", to: "api/v1/health#show"
|
|
26
|
+
end
|
data/config.ru
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "rails"
|
|
2
|
+
require "action_controller/railtie"
|
|
3
|
+
require "active_record/railtie"
|
|
4
|
+
require "active_job/railtie"
|
|
5
|
+
require "action_cable/engine"
|
|
6
|
+
|
|
7
|
+
module SolidLog
|
|
8
|
+
module Service
|
|
9
|
+
class Application < Rails::Application
|
|
10
|
+
config.load_defaults 8.0
|
|
11
|
+
config.api_only = true
|
|
12
|
+
|
|
13
|
+
# Enable caching with memory store
|
|
14
|
+
config.cache_store = :memory_store
|
|
15
|
+
|
|
16
|
+
# Load service configuration
|
|
17
|
+
config.before_initialize do
|
|
18
|
+
# Load configuration file if it exists
|
|
19
|
+
config_file = Rails.root.join("config", "solid_log_service.rb")
|
|
20
|
+
require config_file if File.exist?(config_file)
|
|
21
|
+
|
|
22
|
+
# Load Action Cable configuration
|
|
23
|
+
cable_config_path = Rails.root.join("config", "cable.yml")
|
|
24
|
+
if File.exist?(cable_config_path)
|
|
25
|
+
config.action_cable.cable = YAML.load_file(cable_config_path, aliases: true)[Rails.env]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Set up database connection
|
|
30
|
+
config.before_initialize do
|
|
31
|
+
db_config = {
|
|
32
|
+
adapter: ENV["DB_ADAPTER"] || "sqlite3",
|
|
33
|
+
database: ENV["DATABASE_URL"] || Rails.root.join("storage", "production_log.sqlite3").to_s,
|
|
34
|
+
pool: ENV.fetch("RAILS_MAX_THREADS", 5)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ActiveRecord::Base.establish_connection(db_config)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Start job processor after initialization (but not in console mode)
|
|
41
|
+
config.after_initialize do
|
|
42
|
+
unless defined?(Rails::Console)
|
|
43
|
+
SolidLog::Service.start!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Stop job processor on shutdown
|
|
48
|
+
at_exit do
|
|
49
|
+
SolidLog::Service.stop!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Eager load controllers and jobs
|
|
53
|
+
config.eager_load_paths << Rails.root.join("app", "controllers")
|
|
54
|
+
config.eager_load_paths << Rails.root.join("app", "jobs")
|
|
55
|
+
|
|
56
|
+
# CORS configuration
|
|
57
|
+
config.middleware.insert_before 0, Rack::Cors do
|
|
58
|
+
allow do
|
|
59
|
+
origins { |source, env| SolidLog::Service.configuration.cors_origins.include?(source) || SolidLog::Service.configuration.cors_origins.include?("*") }
|
|
60
|
+
resource "*",
|
|
61
|
+
headers: :any,
|
|
62
|
+
methods: [:get, :post, :put, :patch, :delete, :options, :head],
|
|
63
|
+
credentials: false
|
|
64
|
+
end
|
|
65
|
+
end if defined?(Rack::Cors)
|
|
66
|
+
|
|
67
|
+
# Logging
|
|
68
|
+
config.logger = ActiveSupport::Logger.new(STDOUT)
|
|
69
|
+
config.log_level = ENV.fetch("LOG_LEVEL", "info").to_sym
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "solid_log/core"
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Service
|
|
5
|
+
class Configuration < SolidLog::Core::Configuration
|
|
6
|
+
attr_accessor :job_mode,
|
|
7
|
+
:parser_interval,
|
|
8
|
+
:cache_cleanup_interval,
|
|
9
|
+
:retention_hour,
|
|
10
|
+
:field_analysis_hour,
|
|
11
|
+
:websocket_enabled,
|
|
12
|
+
:cors_origins,
|
|
13
|
+
:bind,
|
|
14
|
+
:port
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
# Load from ENV vars with defaults
|
|
20
|
+
# Job processing mode: :scheduler (default), :active_job, or :manual
|
|
21
|
+
@job_mode = env_to_symbol("SOLIDLOG_JOB_MODE", :scheduler)
|
|
22
|
+
|
|
23
|
+
# Scheduler intervals (only used when job_mode = :scheduler)
|
|
24
|
+
@parser_interval = env_to_int("SOLIDLOG_PARSER_INTERVAL", 10) # seconds
|
|
25
|
+
@cache_cleanup_interval = env_to_int("SOLIDLOG_CACHE_CLEANUP_INTERVAL", 3600) # seconds (1 hour)
|
|
26
|
+
@retention_hour = env_to_int("SOLIDLOG_RETENTION_HOUR", 2) # Hour of day (0-23)
|
|
27
|
+
@field_analysis_hour = env_to_int("SOLIDLOG_FIELD_ANALYSIS_HOUR", 3) # Hour of day (0-23)
|
|
28
|
+
|
|
29
|
+
# WebSocket support for live tail
|
|
30
|
+
@websocket_enabled = env_to_bool("SOLIDLOG_WEBSOCKET_ENABLED", false)
|
|
31
|
+
|
|
32
|
+
# CORS configuration for API
|
|
33
|
+
@cors_origins = env_to_array("SOLIDLOG_CORS_ORIGINS", [])
|
|
34
|
+
|
|
35
|
+
# Server configuration
|
|
36
|
+
@bind = ENV["SOLIDLOG_BIND"] || ENV["BIND"] || "0.0.0.0"
|
|
37
|
+
@port = env_to_int("SOLIDLOG_PORT") || env_to_int("PORT", 3001)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validate configuration
|
|
41
|
+
def valid?
|
|
42
|
+
errors = []
|
|
43
|
+
|
|
44
|
+
errors << "job_mode must be :scheduler, :active_job, or :manual" unless [:scheduler, :active_job, :manual].include?(job_mode)
|
|
45
|
+
errors << "parser_interval must be positive" unless parser_interval&.positive?
|
|
46
|
+
errors << "cache_cleanup_interval must be positive" unless cache_cleanup_interval&.positive?
|
|
47
|
+
errors << "retention_hour must be between 0 and 23" unless retention_hour&.between?(0, 23)
|
|
48
|
+
errors << "field_analysis_hour must be between 0 and 23" unless field_analysis_hour&.between?(0, 23)
|
|
49
|
+
|
|
50
|
+
if errors.any?
|
|
51
|
+
raise ArgumentError, "Invalid configuration:\n #{errors.join("\n ")}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def env_to_int(key, default = nil)
|
|
60
|
+
value = ENV[key]
|
|
61
|
+
return default if value.nil? || value.empty?
|
|
62
|
+
value.to_i
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def env_to_bool(key, default = false)
|
|
66
|
+
value = ENV[key]
|
|
67
|
+
return default if value.nil? || value.empty?
|
|
68
|
+
["true", "1", "yes", "on"].include?(value.downcase)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def env_to_symbol(key, default = nil)
|
|
72
|
+
value = ENV[key]
|
|
73
|
+
return default if value.nil? || value.empty?
|
|
74
|
+
value.to_sym
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def env_to_array(key, default = [])
|
|
78
|
+
value = ENV[key]
|
|
79
|
+
return default if value.nil? || value.empty?
|
|
80
|
+
value.split(",").map(&:strip)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "rails/engine"
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Service
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace SolidLog
|
|
7
|
+
|
|
8
|
+
config.generators do |g|
|
|
9
|
+
g.test_framework :minitest
|
|
10
|
+
g.fixture_replacement :factory_bot
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Ensure controllers and jobs are autoloaded
|
|
14
|
+
config.autoload_paths << root.join("app/controllers")
|
|
15
|
+
config.autoload_paths << root.join("app/jobs")
|
|
16
|
+
|
|
17
|
+
# Add SilenceMiddleware to prevent recursive logging
|
|
18
|
+
# This intercepts all requests to the service and sets Thread.current[:solid_log_silenced]
|
|
19
|
+
# so the service doesn't log its own API requests, parser jobs, etc.
|
|
20
|
+
initializer "solid_log_service.add_middleware" do |app|
|
|
21
|
+
app.middleware.use SolidLog::SilenceMiddleware
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module SolidLog
|
|
2
|
+
module Service
|
|
3
|
+
class JobProcessor
|
|
4
|
+
class << self
|
|
5
|
+
attr_reader :scheduler
|
|
6
|
+
|
|
7
|
+
def setup
|
|
8
|
+
case configuration.job_mode
|
|
9
|
+
when :scheduler
|
|
10
|
+
setup_scheduler
|
|
11
|
+
when :active_job
|
|
12
|
+
setup_active_job
|
|
13
|
+
when :manual
|
|
14
|
+
setup_manual
|
|
15
|
+
else
|
|
16
|
+
raise ArgumentError, "Invalid job_mode: #{configuration.job_mode}. Must be :scheduler, :active_job, or :manual"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stop
|
|
21
|
+
case configuration.job_mode
|
|
22
|
+
when :scheduler
|
|
23
|
+
stop_scheduler
|
|
24
|
+
when :active_job, :manual
|
|
25
|
+
# Nothing to stop
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def configuration
|
|
32
|
+
SolidLog::Service.configuration
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def setup_scheduler
|
|
36
|
+
@scheduler = Scheduler.new(configuration)
|
|
37
|
+
@scheduler.start
|
|
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"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop_scheduler
|
|
47
|
+
if @scheduler
|
|
48
|
+
@scheduler.stop
|
|
49
|
+
@scheduler = nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def setup_active_job
|
|
54
|
+
# Jobs are enqueued via host app's ActiveJob backend
|
|
55
|
+
# Host app should configure recurring jobs using their job backend
|
|
56
|
+
# Example with Solid Queue:
|
|
57
|
+
#
|
|
58
|
+
# SolidQueue::RecurringTask.create!(
|
|
59
|
+
# key: 'solidlog_parser',
|
|
60
|
+
# schedule: 'every 10 seconds',
|
|
61
|
+
# class_name: 'SolidLog::ParserJob'
|
|
62
|
+
# )
|
|
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"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def setup_manual
|
|
69
|
+
# User manages scheduling via cron or other external scheduler
|
|
70
|
+
# No setup needed
|
|
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)"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Service
|
|
5
|
+
class Scheduler
|
|
6
|
+
attr_reader :threads, :running
|
|
7
|
+
|
|
8
|
+
def initialize(config = SolidLog::Service.configuration)
|
|
9
|
+
@config = config
|
|
10
|
+
@threads = []
|
|
11
|
+
@running = false
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start
|
|
16
|
+
return if @running
|
|
17
|
+
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
return if @running
|
|
20
|
+
@running = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Rails.logger.info "SolidLog::Service::Scheduler starting..."
|
|
24
|
+
|
|
25
|
+
# Parser job - frequent (configurable, default 10s)
|
|
26
|
+
thread = Thread.new { parser_loop }
|
|
27
|
+
thread.abort_on_exception = true
|
|
28
|
+
@threads << thread
|
|
29
|
+
|
|
30
|
+
# Cache cleanup - configurable (default hourly)
|
|
31
|
+
thread = Thread.new { cache_cleanup_loop }
|
|
32
|
+
thread.abort_on_exception = true
|
|
33
|
+
@threads << thread
|
|
34
|
+
|
|
35
|
+
# Daily jobs - retention and field analysis
|
|
36
|
+
thread = Thread.new { daily_jobs_loop }
|
|
37
|
+
thread.abort_on_exception = true
|
|
38
|
+
@threads << thread
|
|
39
|
+
|
|
40
|
+
Rails.logger.info "SolidLog::Service::Scheduler started with #{@threads.size} threads"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop
|
|
44
|
+
return unless @running
|
|
45
|
+
|
|
46
|
+
Rails.logger.info "SolidLog::Service::Scheduler stopping..."
|
|
47
|
+
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
@running = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Give threads 5 seconds to finish gracefully
|
|
53
|
+
@threads.each { |t| t.join(5) }
|
|
54
|
+
|
|
55
|
+
# Force kill any remaining threads
|
|
56
|
+
@threads.each { |t| t.kill if t.alive? }
|
|
57
|
+
@threads.clear
|
|
58
|
+
|
|
59
|
+
Rails.logger.info "SolidLog::Service::Scheduler stopped"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def running?
|
|
63
|
+
@running
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def parser_loop
|
|
69
|
+
loop do
|
|
70
|
+
break unless @running
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
SolidLog::ParserJob.perform_now
|
|
74
|
+
rescue => e
|
|
75
|
+
Rails.logger.error "SolidLog::Scheduler: Parser job failed: #{e.message}"
|
|
76
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sleep @config.parser_interval
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cache_cleanup_loop
|
|
84
|
+
loop do
|
|
85
|
+
break unless @running
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
SolidLog::CacheCleanupJob.perform_now
|
|
89
|
+
rescue => e
|
|
90
|
+
Rails.logger.error "SolidLog::Scheduler: Cache cleanup failed: #{e.message}"
|
|
91
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sleep @config.cache_cleanup_interval
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def daily_jobs_loop
|
|
99
|
+
loop do
|
|
100
|
+
break unless @running
|
|
101
|
+
|
|
102
|
+
current_hour = Time.current.hour
|
|
103
|
+
|
|
104
|
+
# Run retention job at configured hour (default 2 AM)
|
|
105
|
+
if current_hour == @config.retention_hour
|
|
106
|
+
run_daily_job(:retention)
|
|
107
|
+
sleep 1.hour # Wait an hour to avoid running multiple times
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Run field analysis at configured hour (default 3 AM)
|
|
111
|
+
if current_hour == @config.field_analysis_hour
|
|
112
|
+
run_daily_job(:field_analysis)
|
|
113
|
+
sleep 1.hour # Wait an hour to avoid running multiple times
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check every 10 minutes
|
|
117
|
+
sleep 10.minutes
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def run_daily_job(job_name)
|
|
122
|
+
case job_name
|
|
123
|
+
when :retention
|
|
124
|
+
begin
|
|
125
|
+
SolidLog::RetentionJob.perform_now(
|
|
126
|
+
retention_days: @config.retention_days,
|
|
127
|
+
error_retention_days: @config.error_retention_days
|
|
128
|
+
)
|
|
129
|
+
rescue => e
|
|
130
|
+
Rails.logger.error "SolidLog::Scheduler: Retention job failed: #{e.message}"
|
|
131
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
132
|
+
end
|
|
133
|
+
when :field_analysis
|
|
134
|
+
begin
|
|
135
|
+
SolidLog::FieldAnalysisJob.perform_now(
|
|
136
|
+
auto_promote: @config.auto_promote_fields
|
|
137
|
+
)
|
|
138
|
+
rescue => e
|
|
139
|
+
Rails.logger.error "SolidLog::Scheduler: Field analysis failed: #{e.message}"
|
|
140
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|