solid_log-core 0.1.0 → 0.2.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 +4 -4
- data/lib/puma/plugin/solid_log.rb +151 -0
- data/lib/solid_log/core/configuration.rb +12 -2
- data/lib/solid_log/core/jobs/cache_cleanup_job.rb +48 -0
- data/lib/solid_log/core/jobs/field_analysis_job.rb +58 -0
- data/lib/solid_log/core/jobs/parse_job.rb +105 -0
- data/lib/solid_log/core/jobs/retention_job.rb +63 -0
- data/lib/solid_log/core/services/batch_parsing_service.rb +248 -0
- data/lib/solid_log/core/services/field_analyzer.rb +1 -1
- data/lib/solid_log/core/services/migration_generator.rb +160 -0
- data/lib/solid_log/core/services/migration_runner.rb +147 -0
- data/lib/solid_log/core/services/retention_service.rb +12 -1
- data/lib/solid_log/core/version.rb +1 -1
- data/lib/solid_log/core.rb +36 -0
- data/lib/solid_log/direct_logger.rb +8 -0
- data/lib/solid_log/models/field.rb +7 -1
- data/lib/solid_log/models/raw_entry.rb +2 -2
- data/lib/solid_log/models/token.rb +1 -1
- data/lib/solid_log/railtie.rb +71 -0
- data/lib/solid_log/silence_middleware.rb +32 -17
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29584710fa24106c9fd34cf056db068a4cc728790d891b036f58efca98f3f622
|
|
4
|
+
data.tar.gz: ef9627f82cf2eda9f91be114bf583440a58b76e3722e4619ac6caff143ca128b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3078d64ae373365594bffebd66914e29569b76142a0a92dc52ed73592507def8a6abdee08505afc55643cc7c74e9732dc798419f95d89cad2fa2d7f87d36fe1c
|
|
7
|
+
data.tar.gz: 2a7e1c672513f17cb133a737624d7b9c48b166aa507d38286b33a5e824a7697ae0d98e4e3d85e296d539986b42fce5bc6c4043b04f1a5d88a3ba90a8501c14a3
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma/plugin"
|
|
4
|
+
|
|
5
|
+
# Puma plugin for inline SolidLog parsing
|
|
6
|
+
#
|
|
7
|
+
# This plugin spawns a background thread that continuously polls for unparsed
|
|
8
|
+
# raw log entries and processes them using BatchParsingService.
|
|
9
|
+
#
|
|
10
|
+
# Usage in config/puma.rb:
|
|
11
|
+
# plugin :solid_log if ENV["SOLIDLOG_PUMA_PLUGIN_ENABLED"]
|
|
12
|
+
#
|
|
13
|
+
# Configuration:
|
|
14
|
+
# SOLIDLOG_PUMA_PLUGIN_ENABLED=true
|
|
15
|
+
# SOLIDLOG_INLINE_PARSING_ENABLED=true
|
|
16
|
+
# SOLIDLOG_PARSE_INTERVAL=10
|
|
17
|
+
# SOLIDLOG_PARSER_BATCH_SIZE=200
|
|
18
|
+
Puma::Plugin.create do
|
|
19
|
+
def start(launcher)
|
|
20
|
+
@launcher = launcher
|
|
21
|
+
|
|
22
|
+
# Only start if we're actually in Puma server mode
|
|
23
|
+
# Don't start in console, rails runner, rake tasks, etc.
|
|
24
|
+
return unless puma_server_mode?
|
|
25
|
+
|
|
26
|
+
# Only start if explicitly enabled
|
|
27
|
+
return unless enabled?
|
|
28
|
+
|
|
29
|
+
# Startup logs to STDERR to avoid recursion
|
|
30
|
+
$stderr.puts "[SolidLog Puma Plugin] Starting inline parsing"
|
|
31
|
+
$stderr.puts "[SolidLog Puma Plugin] Parse interval: #{config.parse_interval}s"
|
|
32
|
+
$stderr.puts "[SolidLog Puma Plugin] Batch size: #{config.parser_batch_size}"
|
|
33
|
+
|
|
34
|
+
# Start background thread using Puma's in_background helper
|
|
35
|
+
in_background do
|
|
36
|
+
parser_loop
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Check if we're actually running as a Puma server
|
|
43
|
+
# Returns false for console, rails runner, rake tasks, etc.
|
|
44
|
+
def puma_server_mode?
|
|
45
|
+
# Check if launcher is present and valid
|
|
46
|
+
return false unless @launcher
|
|
47
|
+
return false unless @launcher.respond_to?(:binder)
|
|
48
|
+
|
|
49
|
+
# Check if we're not in a Rails console or runner
|
|
50
|
+
# Console and runner set DISABLE_SPRING or have IRB/Runner in ARGV
|
|
51
|
+
return false if defined?(Rails::Console)
|
|
52
|
+
return false if $PROGRAM_NAME.include?('rake')
|
|
53
|
+
return false if $PROGRAM_NAME.include?('runner')
|
|
54
|
+
return false if ARGV.include?('console')
|
|
55
|
+
return false if ARGV.include?('runner')
|
|
56
|
+
return false if ARGV.include?('c')
|
|
57
|
+
|
|
58
|
+
# If we made it here, we're likely in server mode
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Main parser loop that runs continuously until Puma shutdown
|
|
63
|
+
def parser_loop
|
|
64
|
+
loop do
|
|
65
|
+
# Thread will be killed by Puma on shutdown, no need to check running state
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
# Ensure database connection in this thread
|
|
69
|
+
# Pattern from DirectLogger (lines 155-158)
|
|
70
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
|
71
|
+
# Process batch using shared service
|
|
72
|
+
stats = SolidLog::Core::Services::BatchParsingService.process_batch(
|
|
73
|
+
batch_size: config.parser_batch_size,
|
|
74
|
+
logger: logger,
|
|
75
|
+
broadcast_callback: method(:broadcast_callback)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Log stats if entries were processed (to STDERR to avoid recursion)
|
|
79
|
+
log_stats(stats) if stats[:processed] > 0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Normal sleep between cycles
|
|
83
|
+
sleep config.parse_interval
|
|
84
|
+
rescue => e
|
|
85
|
+
# Log error to STDERR to avoid recursion
|
|
86
|
+
$stderr.puts "[SolidLog Puma Plugin] ERROR: #{e.class} - #{e.message}"
|
|
87
|
+
$stderr.puts e.backtrace.first(5).join("\n") if e.backtrace
|
|
88
|
+
|
|
89
|
+
# Exponential backoff on error (max 60 seconds)
|
|
90
|
+
# Prevents tight error loops
|
|
91
|
+
backoff_time = [config.parse_interval * 2, 60].min
|
|
92
|
+
$stderr.puts "[SolidLog Puma Plugin] Backing off for #{backoff_time}s" if ENV['SOLIDLOG_DEBUG']
|
|
93
|
+
sleep backoff_time
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
rescue => e
|
|
98
|
+
# Thread-level error - log to STDERR but don't crash Puma
|
|
99
|
+
$stderr.puts "[SolidLog Puma Plugin] FATAL: #{e.class} - #{e.message}"
|
|
100
|
+
$stderr.puts e.backtrace.join("\n") if e.backtrace
|
|
101
|
+
ensure
|
|
102
|
+
$stderr.puts "[SolidLog Puma Plugin] Parser loop exited"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if plugin should be enabled
|
|
106
|
+
def enabled?
|
|
107
|
+
# Check ENV flag (explicit opt-in)
|
|
108
|
+
return false if ENV["SOLIDLOG_PUMA_PLUGIN_ENABLED"] == "false"
|
|
109
|
+
return false unless ENV["SOLIDLOG_PUMA_PLUGIN_ENABLED"]
|
|
110
|
+
|
|
111
|
+
# Check configuration flag
|
|
112
|
+
return false unless config.inline_parsing_enabled
|
|
113
|
+
|
|
114
|
+
true
|
|
115
|
+
rescue => e
|
|
116
|
+
# If SolidLog isn't loaded yet, disable
|
|
117
|
+
logger.error "SolidLog Puma Plugin: Configuration error: #{e.message}"
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get SolidLog configuration
|
|
122
|
+
def config
|
|
123
|
+
SolidLog.configuration
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get logger instance
|
|
127
|
+
def logger
|
|
128
|
+
SolidLog.logger
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Broadcast callback for ActionCable
|
|
132
|
+
def broadcast_callback(entry_ids)
|
|
133
|
+
return if entry_ids.empty?
|
|
134
|
+
|
|
135
|
+
if defined?(ActionCable) && ActionCable.server
|
|
136
|
+
ActionCable.server.broadcast(
|
|
137
|
+
"solid_log_new_entries",
|
|
138
|
+
{ entry_ids: entry_ids }
|
|
139
|
+
)
|
|
140
|
+
$stderr.puts "[SolidLog Puma Plugin] Broadcasted #{entry_ids.size} entries" if ENV['SOLIDLOG_DEBUG']
|
|
141
|
+
end
|
|
142
|
+
rescue => e
|
|
143
|
+
# Silent failure - broadcasting is optional
|
|
144
|
+
$stderr.puts "[SolidLog Puma Plugin] Broadcast failed: #{e.message}" if ENV['SOLIDLOG_DEBUG']
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Log processing stats (directly to STDERR to avoid recursion)
|
|
148
|
+
def log_stats(stats)
|
|
149
|
+
$stderr.puts "[SolidLog Puma Plugin] Processed #{stats[:processed]}, inserted #{stats[:inserted]}, errors #{stats[:errors]}" if ENV['SOLIDLOG_DEBUG']
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -4,26 +4,36 @@ module SolidLog
|
|
|
4
4
|
attr_accessor :database_url,
|
|
5
5
|
:retention_days,
|
|
6
6
|
:error_retention_days,
|
|
7
|
+
:max_entries,
|
|
7
8
|
:max_batch_size,
|
|
8
9
|
:parser_batch_size,
|
|
9
10
|
:parser_concurrency,
|
|
10
11
|
:auto_promote_fields,
|
|
11
12
|
:field_promotion_threshold,
|
|
12
13
|
:facet_cache_ttl,
|
|
13
|
-
:live_tail_mode
|
|
14
|
+
:live_tail_mode,
|
|
15
|
+
:inline_parsing_enabled,
|
|
16
|
+
:parse_interval,
|
|
17
|
+
:after_entries_inserted
|
|
14
18
|
|
|
15
19
|
def initialize
|
|
16
20
|
# Load from ENV vars with defaults
|
|
17
21
|
@database_url = ENV["SOLIDLOG_DATABASE_URL"] || ENV["DATABASE_URL"]
|
|
18
22
|
@retention_days = env_to_int("SOLIDLOG_RETENTION_DAYS", 30)
|
|
19
23
|
@error_retention_days = env_to_int("SOLIDLOG_ERROR_RETENTION_DAYS", 90)
|
|
24
|
+
@max_entries = env_to_int("SOLIDLOG_MAX_ENTRIES", nil) # Optional row limit (nil = unlimited)
|
|
20
25
|
@max_batch_size = env_to_int("SOLIDLOG_MAX_BATCH_SIZE", 1000) # For API ingestion
|
|
21
|
-
@parser_batch_size = env_to_int("SOLIDLOG_PARSER_BATCH_SIZE",
|
|
26
|
+
@parser_batch_size = env_to_int("SOLIDLOG_PARSER_BATCH_SIZE", 1000) # Number of raw entries to parse per job
|
|
22
27
|
@parser_concurrency = env_to_int("SOLIDLOG_PARSER_CONCURRENCY", 5)
|
|
23
28
|
@auto_promote_fields = env_to_bool("SOLIDLOG_AUTO_PROMOTE_FIELDS", false)
|
|
24
29
|
@field_promotion_threshold = env_to_int("SOLIDLOG_FIELD_PROMOTION_THRESHOLD", 1000)
|
|
25
30
|
@facet_cache_ttl = env_to_int("SOLIDLOG_FACET_CACHE_TTL", 300) # seconds (5 minutes)
|
|
26
31
|
@live_tail_mode = env_to_symbol("SOLIDLOG_LIVE_TAIL_MODE", :disabled) # :websocket, :polling, or :disabled
|
|
32
|
+
|
|
33
|
+
# Inline parsing configuration
|
|
34
|
+
@inline_parsing_enabled = env_to_bool("SOLIDLOG_INLINE_PARSING_ENABLED", false)
|
|
35
|
+
@parse_interval = env_to_int("SOLIDLOG_PARSE_INTERVAL", 10) # seconds
|
|
36
|
+
@after_entries_inserted = nil # Optional callback for custom broadcasting
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
# Check if database is configured
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Core
|
|
5
|
+
module Jobs
|
|
6
|
+
# Conditionally inherit from ActiveJob if available
|
|
7
|
+
if defined?(ActiveJob::Base)
|
|
8
|
+
BaseJob = ActiveJob::Base
|
|
9
|
+
else
|
|
10
|
+
# Plain Ruby class when ActiveJob not available
|
|
11
|
+
BaseJob = Class.new do
|
|
12
|
+
def self.queue_as(*); end # No-op when not using ActiveJob
|
|
13
|
+
def self.perform_later(*args, **kwargs)
|
|
14
|
+
new.perform(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
def self.perform_now(*args, **kwargs)
|
|
17
|
+
new.perform(*args, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class CacheCleanupJob < BaseJob
|
|
23
|
+
queue_as :default
|
|
24
|
+
|
|
25
|
+
def perform
|
|
26
|
+
SolidLog.without_logging do
|
|
27
|
+
expired_count = SolidLog::FacetCache.expired.count
|
|
28
|
+
|
|
29
|
+
if expired_count > 0
|
|
30
|
+
SolidLog::FacetCache.cleanup_expired!
|
|
31
|
+
logger.info "SolidLog::CacheCleanupJob: Cleaned up #{expired_count} expired cache entries"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
rescue => e
|
|
35
|
+
logger.error "SolidLog::CacheCleanupJob failed: #{e.message}"
|
|
36
|
+
# Re-raise for ActiveJob retry if available, otherwise just log
|
|
37
|
+
raise if defined?(ActiveJob::Base)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def logger
|
|
43
|
+
defined?(SolidLog) && SolidLog.respond_to?(:logger) ? SolidLog.logger : Logger.new(STDOUT)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Core
|
|
5
|
+
module Jobs
|
|
6
|
+
# Conditionally inherit from ActiveJob if available
|
|
7
|
+
if defined?(ActiveJob::Base)
|
|
8
|
+
BaseJob = ActiveJob::Base
|
|
9
|
+
else
|
|
10
|
+
# Plain Ruby class when ActiveJob not available
|
|
11
|
+
BaseJob = Class.new do
|
|
12
|
+
def self.queue_as(*); end # No-op when not using ActiveJob
|
|
13
|
+
def self.perform_later(*args, **kwargs)
|
|
14
|
+
new.perform(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
def self.perform_now(*args, **kwargs)
|
|
17
|
+
new.perform(*args, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class FieldAnalysisJob < BaseJob
|
|
23
|
+
queue_as :default
|
|
24
|
+
|
|
25
|
+
def perform(auto_promote: false)
|
|
26
|
+
SolidLog.without_logging do
|
|
27
|
+
recommendations = SolidLog::Core::FieldAnalyzer.analyze
|
|
28
|
+
|
|
29
|
+
if recommendations.any?
|
|
30
|
+
logger.info "SolidLog::FieldAnalysisJob: Found #{recommendations.size} fields for potential promotion"
|
|
31
|
+
|
|
32
|
+
recommendations.take(10).each do |rec|
|
|
33
|
+
logger.info " - #{rec[:field].name} (#{rec[:field].usage_count} uses, priority: #{rec[:priority]})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if auto_promote
|
|
37
|
+
promoted_count = SolidLog::Core::FieldAnalyzer.auto_promote_candidates
|
|
38
|
+
logger.info "SolidLog::FieldAnalysisJob: Auto-promoted #{promoted_count} fields"
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
logger.info "SolidLog::FieldAnalysisJob: No fields meet promotion threshold"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
rescue => e
|
|
45
|
+
logger.error "SolidLog::FieldAnalysisJob failed: #{e.message}"
|
|
46
|
+
# Re-raise for ActiveJob retry if available, otherwise just log
|
|
47
|
+
raise if defined?(ActiveJob::Base)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def logger
|
|
53
|
+
defined?(SolidLog) && SolidLog.respond_to?(:logger) ? SolidLog.logger : Logger.new(STDOUT)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Core
|
|
5
|
+
module Jobs
|
|
6
|
+
# Conditionally inherit from ActiveJob if available
|
|
7
|
+
# This allows the same job class to work in Rails apps AND solid_log-service
|
|
8
|
+
if defined?(ActiveJob::Base)
|
|
9
|
+
BaseJob = ActiveJob::Base
|
|
10
|
+
else
|
|
11
|
+
# Plain Ruby class when ActiveJob not available
|
|
12
|
+
BaseJob = Class.new do
|
|
13
|
+
def self.queue_as(*_args)
|
|
14
|
+
# No-op when not using ActiveJob
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.perform_later(*args, **kwargs)
|
|
18
|
+
# Synchronous execution when ActiveJob not available
|
|
19
|
+
new.perform(*args, **kwargs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.perform_now(*args, **kwargs)
|
|
23
|
+
new.perform(*args, **kwargs)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ParseJob processes batches of unparsed raw log entries.
|
|
29
|
+
#
|
|
30
|
+
# This job can be used in three contexts:
|
|
31
|
+
# 1. Rails app with Solid Queue (scheduled in config/recurring.yml)
|
|
32
|
+
# 2. Rails app with other ActiveJob backends (Sidekiq, Resque, etc.)
|
|
33
|
+
# 3. solid_log-service without Rails (called directly)
|
|
34
|
+
#
|
|
35
|
+
# @example Solid Queue (config/recurring.yml)
|
|
36
|
+
# solidlog_parse:
|
|
37
|
+
# class: SolidLog::Core::Jobs::ParseJob
|
|
38
|
+
# schedule: every 10 seconds
|
|
39
|
+
# args: [{ batch_size: 200 }]
|
|
40
|
+
#
|
|
41
|
+
# @example Direct call (service)
|
|
42
|
+
# SolidLog::Core::Jobs::ParseJob.perform_now(batch_size: 200)
|
|
43
|
+
class ParseJob < BaseJob
|
|
44
|
+
queue_as :default
|
|
45
|
+
|
|
46
|
+
# Process a batch of unparsed raw entries
|
|
47
|
+
#
|
|
48
|
+
# @param batch_size [Integer] Number of entries to process (default: config value)
|
|
49
|
+
# @return [Hash] Statistics hash with :processed, :inserted, :errors keys
|
|
50
|
+
def perform(batch_size: nil)
|
|
51
|
+
batch_size ||= SolidLog.configuration.parser_batch_size
|
|
52
|
+
|
|
53
|
+
# Delegate to BatchParsingService
|
|
54
|
+
stats = SolidLog::Core::Services::BatchParsingService.process_batch(
|
|
55
|
+
batch_size: batch_size,
|
|
56
|
+
logger: logger,
|
|
57
|
+
broadcast_callback: method(:broadcast_callback)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Log results if entries were processed
|
|
61
|
+
if stats[:processed] > 0
|
|
62
|
+
logger.info "SolidLog::ParseJob: Processed #{stats[:processed]}, inserted #{stats[:inserted]}, errors #{stats[:errors]}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
stats
|
|
66
|
+
rescue => e
|
|
67
|
+
logger.error "SolidLog::ParseJob failed: #{e.class} - #{e.message}"
|
|
68
|
+
logger.error e.backtrace.first(10).join("\n") if e.backtrace
|
|
69
|
+
|
|
70
|
+
# Re-raise for ActiveJob retry if available, otherwise just log
|
|
71
|
+
raise if defined?(ActiveJob::Base) && self.class.ancestors.include?(ActiveJob::Base)
|
|
72
|
+
|
|
73
|
+
stats || { processed: 0, inserted: 0, errors: 1 }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Get logger instance
|
|
79
|
+
def logger
|
|
80
|
+
if defined?(SolidLog) && SolidLog.respond_to?(:logger)
|
|
81
|
+
SolidLog.logger
|
|
82
|
+
else
|
|
83
|
+
Logger.new($stdout).tap { |log| log.level = Logger::INFO }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Broadcast callback for ActionCable
|
|
88
|
+
def broadcast_callback(entry_ids)
|
|
89
|
+
return if entry_ids.empty?
|
|
90
|
+
|
|
91
|
+
if defined?(ActionCable) && ActionCable.server
|
|
92
|
+
ActionCable.server.broadcast(
|
|
93
|
+
"solid_log_new_entries",
|
|
94
|
+
{ entry_ids: entry_ids }
|
|
95
|
+
)
|
|
96
|
+
logger.debug "SolidLog::ParseJob: Broadcasted #{entry_ids.size} entries via ActionCable"
|
|
97
|
+
end
|
|
98
|
+
rescue => e
|
|
99
|
+
# Silent failure - broadcasting is optional
|
|
100
|
+
logger.debug "SolidLog::ParseJob: Broadcast failed: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidLog
|
|
4
|
+
module Core
|
|
5
|
+
module Jobs
|
|
6
|
+
# Conditionally inherit from ActiveJob if available
|
|
7
|
+
if defined?(ActiveJob::Base)
|
|
8
|
+
BaseJob = ActiveJob::Base
|
|
9
|
+
else
|
|
10
|
+
# Plain Ruby class when ActiveJob not available
|
|
11
|
+
BaseJob = Class.new do
|
|
12
|
+
def self.queue_as(*); end # No-op when not using ActiveJob
|
|
13
|
+
def self.perform_later(*args, **kwargs)
|
|
14
|
+
new.perform(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
def self.perform_now(*args, **kwargs)
|
|
17
|
+
new.perform(*args, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class RetentionJob < BaseJob
|
|
23
|
+
queue_as :default
|
|
24
|
+
|
|
25
|
+
def perform(retention_days: nil, error_retention_days: nil, max_entries: nil, vacuum: false)
|
|
26
|
+
retention_days ||= SolidLog.configuration.retention_days
|
|
27
|
+
error_retention_days ||= SolidLog.configuration.error_retention_days
|
|
28
|
+
max_entries ||= SolidLog.configuration.max_entries
|
|
29
|
+
|
|
30
|
+
SolidLog.without_logging do
|
|
31
|
+
logger.info "SolidLog::RetentionJob: Starting cleanup (retention: #{retention_days} days, errors: #{error_retention_days} days, max_entries: #{max_entries || 'unlimited'})"
|
|
32
|
+
|
|
33
|
+
stats = SolidLog::Core::RetentionService.cleanup(
|
|
34
|
+
retention_days: retention_days,
|
|
35
|
+
error_retention_days: error_retention_days,
|
|
36
|
+
max_entries: max_entries
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger.info "SolidLog::RetentionJob: Deleted #{stats[:entries_deleted]} entries, #{stats[:raw_deleted]} raw entries, cleared #{stats[:cache_cleared]} cache entries"
|
|
40
|
+
|
|
41
|
+
if vacuum
|
|
42
|
+
logger.info "SolidLog::RetentionJob: Running VACUUM..."
|
|
43
|
+
SolidLog::Core::RetentionService.vacuum_database
|
|
44
|
+
logger.info "SolidLog::RetentionJob: VACUUM complete"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
stats
|
|
49
|
+
rescue => e
|
|
50
|
+
logger.error "SolidLog::RetentionJob failed: #{e.message}"
|
|
51
|
+
# Re-raise for ActiveJob retry if available, otherwise just log
|
|
52
|
+
raise if defined?(ActiveJob::Base)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def logger
|
|
58
|
+
defined?(SolidLog) && SolidLog.respond_to?(:logger) ? SolidLog.logger : Logger.new(STDOUT)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|