Skip to content

Commit b8a72d1

Browse files
ndbroadbentclaude
andcommitted
Add GoodJob integration for structured job logging
Implements comprehensive GoodJob integration providing structured logging for PostgreSQL-based ActiveJob backend operations. Features: - LogStruct::Log::GoodJob class with full job metadata support - Custom Logger extending SemanticLogger for high-performance logging - LogSubscriber for capturing ActiveSupport notifications - Automatic job context extraction from thread-local variables - Performance metrics tracking (wait_time, run_time, execution_time) - Error tracking with backtrace support - Support for GoodJob-specific features (batches, cron, priorities) Architecture improvements: - Updated IntegrationInterface to return T.nilable(T::Boolean) - Added level field to CommonFields interface - Fixed all integration setup methods for consistent return types - Added private_class_method declarations for proper encapsulation Includes comprehensive test coverage for all components. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b0f6371 commit b8a72d1

29 files changed

Lines changed: 1470 additions & 53 deletions

lib/log_struct/config_struct/integrations.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class Integrations < T::Struct
5252
# Default: true
5353
prop :enable_carrierwave, T::Boolean, default: true
5454

55+
# Enable or disable GoodJob integration
56+
# Default: true
57+
prop :enable_goodjob, T::Boolean, default: true
58+
5559
# Enable SemanticLogger integration for high-performance logging
5660
# Default: true
5761
prop :enable_semantic_logger, T::Boolean, default: true

lib/log_struct/formatter.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ def process_values(arg, recursion_depth: 0)
7676
when Array
7777
result = arg.map { |value| process_values(value, recursion_depth: recursion_depth + 1) }
7878

79-
# Filter large arrays
80-
if result.size > 10
79+
# Filter large arrays, but don't truncate backtraces (arrays of strings that look like file:line)
80+
if result.size > 10 && !looks_like_backtrace?(result)
8181
result = result.take(10) + ["... and #{result.size - 10} more items"]
8282
end
8383
result
@@ -205,5 +205,19 @@ def call(severity, time, progname, log_value)
205205
def generate_json(data)
206206
"#{data.to_json}\n"
207207
end
208+
209+
# Check if an array looks like a backtrace (array of strings with file:line pattern)
210+
sig { params(array: T::Array[T.untyped]).returns(T::Boolean) }
211+
def looks_like_backtrace?(array)
212+
return false if array.empty?
213+
214+
# Check if most elements look like backtrace lines (file.rb:123 or similar patterns)
215+
backtrace_like_count = array.first(5).count do |element|
216+
element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
217+
end
218+
219+
# If at least 3 out of the first 5 elements look like backtrace lines, treat as backtrace
220+
backtrace_like_count >= 3
221+
end
208222
end
209223
end

lib/log_struct/integrations.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_relative "integrations/lograge"
1010
require_relative "integrations/shrine"
1111
require_relative "integrations/sidekiq"
12+
require_relative "integrations/good_job"
1213
require_relative "integrations/active_storage"
1314
require_relative "integrations/carrierwave"
1415
require_relative "integrations/sorbet"
@@ -26,6 +27,7 @@ def self.setup_integrations
2627
Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
2728
Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
2829
Integrations::Sidekiq.setup(config) if config.integrations.enable_sidekiq
30+
Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
2931
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
3032
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
3133
Integrations::Shrine.setup(config) if config.integrations.enable_shrine

lib/log_struct/integrations/action_mailer.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ module ActionMailer
2323
extend IntegrationInterface
2424

2525
# Set up ActionMailer structured logging
26-
sig { override.params(config: LogStruct::Configuration).void }
26+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
2727
def self.setup(config)
28-
return unless defined?(::ActionMailer)
29-
return unless config.enabled
30-
return unless config.integrations.enable_actionmailer
28+
return nil unless defined?(::ActionMailer)
29+
return nil unless config.enabled
30+
return nil unless config.integrations.enable_actionmailer
3131

3232
# Silence default ActionMailer logs (we use our own structured logging)
3333
# This is required because we replace the logging using our own callbacks
@@ -41,6 +41,8 @@ def self.setup(config)
4141
ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::EventLogging }
4242
ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::ErrorHandling }
4343
ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::Callbacks }
44+
45+
true
4446
end
4547
end
4648
end

lib/log_struct/integrations/active_job.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ module ActiveJob
1818
extend IntegrationInterface
1919

2020
# Set up ActiveJob structured logging
21-
sig { override.params(config: LogStruct::Configuration).void }
21+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
2222
def self.setup(config)
23-
return unless defined?(::ActiveJob::LogSubscriber)
24-
return unless config.enabled
25-
return unless config.integrations.enable_activejob
23+
return nil unless defined?(::ActiveJob::LogSubscriber)
24+
return nil unless config.enabled
25+
return nil unless config.integrations.enable_activejob
2626

2727
::ActiveSupport.on_load(:active_job) do
2828
# Detach the default text formatter
@@ -31,6 +31,7 @@ def self.setup(config)
3131
# Attach our structured formatter
3232
Integrations::ActiveJob::LogSubscriber.attach_to :active_job
3333
end
34+
true
3435
end
3536
end
3637
end

lib/log_struct/integrations/active_storage.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ module ActiveStorage
1313
extend IntegrationInterface
1414

1515
# Set up ActiveStorage structured logging
16-
sig { override.params(config: LogStruct::Configuration).void }
16+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
1717
def self.setup(config)
18-
return unless defined?(::ActiveStorage)
19-
return unless config.enabled
20-
return unless config.integrations.enable_activestorage
18+
return nil unless defined?(::ActiveStorage)
19+
return nil unless config.enabled
20+
return nil unless config.integrations.enable_activestorage
2121

2222
# Subscribe to all ActiveStorage service events
2323
::ActiveSupport::Notifications.subscribe(/service_.*\.active_storage/) do |*args|
2424
process_active_storage_event(::ActiveSupport::Notifications::Event.new(*args), config)
2525
end
26+
27+
true
2628
end
2729

2830
private_class_method

lib/log_struct/integrations/carrierwave.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ module CarrierWave
1515
extend IntegrationInterface
1616

1717
# Set up CarrierWave structured logging
18-
sig { override.params(config: LogStruct::Configuration).void }
18+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
1919
def self.setup(config)
20-
return unless defined?(::CarrierWave)
21-
return unless config.enabled
22-
return unless config.integrations.enable_carrierwave
20+
return nil unless defined?(::CarrierWave)
21+
return nil unless config.enabled
22+
return nil unless config.integrations.enable_carrierwave
2323

2424
# Patch CarrierWave to add logging
2525
::CarrierWave::Uploader::Base.prepend(LoggingMethods)
26+
27+
true
2628
end
2729

2830
# Methods to add logging to CarrierWave operations
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
begin
5+
require "good_job"
6+
rescue LoadError
7+
# GoodJob gem is not available, integration will be skipped
8+
end
9+
10+
require_relative "good_job/logger" if defined?(::GoodJob)
11+
require_relative "good_job/log_subscriber" if defined?(::GoodJob)
12+
13+
module LogStruct
14+
module Integrations
15+
# GoodJob integration for structured logging
16+
#
17+
# GoodJob is a PostgreSQL-based ActiveJob backend that provides reliable,
18+
# scalable job processing for Rails applications. This integration provides
19+
# structured logging for all GoodJob operations.
20+
#
21+
# ## Features:
22+
# - Structured logging for job execution lifecycle
23+
# - Error tracking and retry logging
24+
# - Performance metrics and timing data
25+
# - Database operation logging
26+
# - Thread and process tracking
27+
# - Custom GoodJob logger with LogStruct formatting
28+
#
29+
# ## Integration Points:
30+
# - Replaces GoodJob.logger with LogStruct-compatible logger
31+
# - Subscribes to GoodJob's ActiveSupport notifications
32+
# - Captures job execution events, errors, and performance metrics
33+
# - Logs database operations and connection information
34+
#
35+
# ## Configuration:
36+
# The integration is automatically enabled when GoodJob is detected and
37+
# LogStruct configuration allows it. It can be disabled by setting:
38+
#
39+
# ```ruby
40+
# config.integrations.enable_goodjob = false
41+
# ```
42+
module GoodJob
43+
extend T::Sig
44+
extend IntegrationInterface
45+
46+
# Set up GoodJob structured logging
47+
#
48+
# This method configures GoodJob to use LogStruct's structured logging
49+
# by replacing the default logger and subscribing to job events.
50+
#
51+
# @param config [LogStruct::Configuration] The LogStruct configuration
52+
# @return [Boolean, nil] Returns true if setup was successful, nil if skipped
53+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
54+
def self.setup(config)
55+
return nil unless defined?(::GoodJob)
56+
return nil unless config.enabled
57+
return nil unless config.integrations.enable_goodjob
58+
59+
# Replace GoodJob's logger with our structured logger
60+
configure_logger
61+
62+
# Subscribe to GoodJob's ActiveSupport notifications
63+
subscribe_to_notifications
64+
65+
true
66+
end
67+
68+
# Configure GoodJob to use LogStruct's structured logger
69+
sig { void }
70+
def self.configure_logger
71+
return unless defined?(::GoodJob)
72+
73+
# Use T.unsafe to avoid Sorbet errors with external constants
74+
goodjob_module = T.unsafe(Object.const_get("GoodJob"))
75+
76+
# Replace GoodJob.logger with our structured logger if GoodJob is available
77+
if goodjob_module.respond_to?(:logger=)
78+
goodjob_module.logger = LogStruct::Integrations::GoodJob::Logger.new("GoodJob")
79+
end
80+
81+
# Configure error handling for thread errors if GoodJob supports it
82+
if goodjob_module.respond_to?(:on_thread_error=)
83+
goodjob_module.on_thread_error = ->(exception) do
84+
# Log the error using our structured format
85+
log_entry = LogStruct::Log::GoodJob.new(
86+
event: Event::Error,
87+
level: Level::Error,
88+
error_class: exception.class.name,
89+
error_message: exception.message,
90+
error_backtrace: exception.backtrace
91+
)
92+
93+
goodjob_module.logger.error(log_entry)
94+
end
95+
end
96+
end
97+
98+
# Subscribe to GoodJob's ActiveSupport notifications
99+
sig { void }
100+
def self.subscribe_to_notifications
101+
return unless defined?(::GoodJob)
102+
103+
# Subscribe to our custom log subscriber for GoodJob events
104+
LogStruct::Integrations::GoodJob::LogSubscriber.attach_to :good_job
105+
end
106+
107+
private_class_method :configure_logger
108+
private_class_method :subscribe_to_notifications
109+
end
110+
end
111+
end

0 commit comments

Comments
 (0)