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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09b479494d3ac70c18b6d0611a5ee0879ff01b90c17108dcffe1eaa951a47f00'
4
- data.tar.gz: ca5f502ca1f8c7b722f1c26b31c898bf3c0e04b512fbde82d0b1e1e888006e32
3
+ metadata.gz: 29584710fa24106c9fd34cf056db068a4cc728790d891b036f58efca98f3f622
4
+ data.tar.gz: ef9627f82cf2eda9f91be114bf583440a58b76e3722e4619ac6caff143ca128b
5
5
  SHA512:
6
- metadata.gz: 2da7bc4a35999dc410ef3951325e1c61c7e67ef4a7f5028d386bb397e676edc50528e2b46209a4999dc94f95cf6ff6b7025db7d7010683e2dd73b207dcac9333
7
- data.tar.gz: dff9a33b078a75d0019bfba2d9371b9d4804ce6bcfb52ed6be75bc2ce9a3892ba57d679885fd64ac56c4904945d101078eedfbbb709114d662b0978b57218431
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", 200) # Number of raw entries to parse per job
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