better_service 1.0.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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1321 -0
  4. data/Rakefile +15 -0
  5. data/lib/better_service/cache_service.rb +310 -0
  6. data/lib/better_service/concerns/instrumentation.rb +242 -0
  7. data/lib/better_service/concerns/serviceable/authorizable.rb +106 -0
  8. data/lib/better_service/concerns/serviceable/cacheable.rb +97 -0
  9. data/lib/better_service/concerns/serviceable/messageable.rb +30 -0
  10. data/lib/better_service/concerns/serviceable/presentable.rb +66 -0
  11. data/lib/better_service/concerns/serviceable/transactional.rb +51 -0
  12. data/lib/better_service/concerns/serviceable/validatable.rb +58 -0
  13. data/lib/better_service/concerns/serviceable/viewable.rb +49 -0
  14. data/lib/better_service/concerns/serviceable.rb +12 -0
  15. data/lib/better_service/concerns/workflowable/callbacks.rb +116 -0
  16. data/lib/better_service/concerns/workflowable/context.rb +108 -0
  17. data/lib/better_service/concerns/workflowable/step.rb +141 -0
  18. data/lib/better_service/concerns/workflowable.rb +12 -0
  19. data/lib/better_service/configuration.rb +113 -0
  20. data/lib/better_service/errors/better_service_error.rb +271 -0
  21. data/lib/better_service/errors/configuration/configuration_error.rb +21 -0
  22. data/lib/better_service/errors/configuration/invalid_configuration_error.rb +28 -0
  23. data/lib/better_service/errors/configuration/invalid_schema_error.rb +28 -0
  24. data/lib/better_service/errors/configuration/nil_user_error.rb +37 -0
  25. data/lib/better_service/errors/configuration/schema_required_error.rb +29 -0
  26. data/lib/better_service/errors/runtime/authorization_error.rb +38 -0
  27. data/lib/better_service/errors/runtime/database_error.rb +38 -0
  28. data/lib/better_service/errors/runtime/execution_error.rb +27 -0
  29. data/lib/better_service/errors/runtime/resource_not_found_error.rb +38 -0
  30. data/lib/better_service/errors/runtime/runtime_error.rb +22 -0
  31. data/lib/better_service/errors/runtime/transaction_error.rb +34 -0
  32. data/lib/better_service/errors/runtime/validation_error.rb +42 -0
  33. data/lib/better_service/errors/workflowable/configuration/duplicate_step_error.rb +27 -0
  34. data/lib/better_service/errors/workflowable/configuration/invalid_step_error.rb +12 -0
  35. data/lib/better_service/errors/workflowable/configuration/step_not_found_error.rb +29 -0
  36. data/lib/better_service/errors/workflowable/configuration/workflow_configuration_error.rb +24 -0
  37. data/lib/better_service/errors/workflowable/runtime/rollback_error.rb +46 -0
  38. data/lib/better_service/errors/workflowable/runtime/step_execution_error.rb +47 -0
  39. data/lib/better_service/errors/workflowable/runtime/workflow_execution_error.rb +40 -0
  40. data/lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb +25 -0
  41. data/lib/better_service/railtie.rb +6 -0
  42. data/lib/better_service/services/action_service.rb +60 -0
  43. data/lib/better_service/services/base.rb +249 -0
  44. data/lib/better_service/services/create_service.rb +60 -0
  45. data/lib/better_service/services/destroy_service.rb +57 -0
  46. data/lib/better_service/services/index_service.rb +56 -0
  47. data/lib/better_service/services/show_service.rb +44 -0
  48. data/lib/better_service/services/update_service.rb +58 -0
  49. data/lib/better_service/subscribers/log_subscriber.rb +131 -0
  50. data/lib/better_service/subscribers/stats_subscriber.rb +208 -0
  51. data/lib/better_service/version.rb +3 -0
  52. data/lib/better_service/workflows/base.rb +106 -0
  53. data/lib/better_service/workflows/dsl.rb +59 -0
  54. data/lib/better_service/workflows/execution.rb +89 -0
  55. data/lib/better_service/workflows/result_builder.rb +67 -0
  56. data/lib/better_service/workflows/rollback_support.rb +44 -0
  57. data/lib/better_service/workflows/transaction_support.rb +32 -0
  58. data/lib/better_service.rb +28 -0
  59. data/lib/generators/serviceable/action_generator.rb +29 -0
  60. data/lib/generators/serviceable/create_generator.rb +27 -0
  61. data/lib/generators/serviceable/destroy_generator.rb +27 -0
  62. data/lib/generators/serviceable/index_generator.rb +27 -0
  63. data/lib/generators/serviceable/scaffold_generator.rb +70 -0
  64. data/lib/generators/serviceable/show_generator.rb +27 -0
  65. data/lib/generators/serviceable/templates/action_service.rb.tt +42 -0
  66. data/lib/generators/serviceable/templates/create_service.rb.tt +33 -0
  67. data/lib/generators/serviceable/templates/destroy_service.rb.tt +40 -0
  68. data/lib/generators/serviceable/templates/index_service.rb.tt +54 -0
  69. data/lib/generators/serviceable/templates/service_test.rb.tt +23 -0
  70. data/lib/generators/serviceable/templates/show_service.rb.tt +37 -0
  71. data/lib/generators/serviceable/templates/update_service.rb.tt +50 -0
  72. data/lib/generators/serviceable/update_generator.rb +27 -0
  73. data/lib/generators/workflowable/WORKFLOW_README +27 -0
  74. data/lib/generators/workflowable/templates/workflow.rb.tt +72 -0
  75. data/lib/generators/workflowable/templates/workflow_test.rb.tt +62 -0
  76. data/lib/generators/workflowable/workflow_generator.rb +60 -0
  77. data/lib/tasks/better_service_tasks.rake +4 -0
  78. metadata +180 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-schema"
4
+ require_relative "../concerns/serviceable/messageable"
5
+ require_relative "../concerns/serviceable/validatable"
6
+ require_relative "../concerns/serviceable/authorizable"
7
+ require_relative "../concerns/serviceable/presentable"
8
+ require_relative "../concerns/serviceable/cacheable"
9
+ require_relative "../concerns/serviceable/viewable"
10
+ require_relative "../concerns/serviceable/transactional"
11
+ require_relative "../concerns/instrumentation"
12
+
13
+ module BetterService
14
+ module Services
15
+ class Base
16
+ class_attribute :_allow_nil_user, default: false
17
+ class_attribute :_action_name, default: nil
18
+ class_attribute :_search_block, default: nil
19
+ class_attribute :_process_block, default: nil
20
+ class_attribute :_transform_block, default: nil
21
+ class_attribute :_respond_block, default: nil
22
+
23
+ include Concerns::Serviceable::Messageable
24
+ include Concerns::Serviceable::Validatable
25
+ include Concerns::Serviceable::Authorizable
26
+ include Concerns::Serviceable::Presentable
27
+ include Concerns::Serviceable::Viewable
28
+
29
+ # Prepend Transactional so it can wrap the process method
30
+ prepend Concerns::Serviceable::Transactional
31
+
32
+ # Default empty schema - subclasses should override
33
+ schema do
34
+ # Override in subclass with specific validations
35
+ end
36
+
37
+ attr_reader :user, :params
38
+
39
+ def initialize(user, params: {})
40
+ validate_user_presence!(user) unless self.class._allow_nil_user
41
+ validate_schema_presence!
42
+ @user = user
43
+ @params = safe_params_to_hash(params)
44
+ @validation_errors = {}
45
+ validate_params!
46
+ end
47
+
48
+ # DSL methods to define phase blocks
49
+ def self.search_with(&block)
50
+ self._search_block = block
51
+ end
52
+
53
+ def self.process_with(&block)
54
+ self._process_block = block
55
+ end
56
+
57
+ def self.transform_with(&block)
58
+ self._transform_block = block
59
+ end
60
+
61
+ def self.respond_with(&block)
62
+ self._respond_block = block
63
+ end
64
+
65
+ # Main entry point - executes the 5-phase flow
66
+ def call
67
+ # Validation already raises ValidationError in initialize
68
+ # Authorization already raises AuthorizationError
69
+ authorize!
70
+
71
+ data = search
72
+ processed = process(data)
73
+ transformed = transform(processed)
74
+ result = respond(transformed)
75
+
76
+ # Phase 5: Viewer (if enabled)
77
+ if respond_to?(:viewer_enabled?, true) && viewer_enabled?
78
+ view_config = execute_viewer(processed, transformed, result)
79
+ result = result.merge(view: view_config)
80
+ end
81
+
82
+ result
83
+ rescue Errors::Runtime::ValidationError, Errors::Runtime::AuthorizationError
84
+ # Let validation and authorization errors propagate without wrapping
85
+ raise
86
+ rescue ActiveRecord::RecordNotFound => e
87
+ handle_not_found_error(e)
88
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
89
+ handle_database_error(e)
90
+ rescue StandardError => e
91
+ handle_unexpected_error(e)
92
+ end
93
+
94
+ # Include Cacheable AFTER call method is defined so it can wrap it
95
+ # This must be before Instrumentation prepend so cache works correctly
96
+ include Concerns::Serviceable::Cacheable
97
+
98
+ private
99
+
100
+ # Phase 1: Search - Load raw data (override in subclass)
101
+ def search
102
+ if self.class._search_block
103
+ instance_exec(&self.class._search_block)
104
+ else
105
+ {}
106
+ end
107
+ end
108
+
109
+ # Phase 2: Process - Transform and aggregate data (override in subclass)
110
+ def process(data)
111
+ if self.class._process_block
112
+ instance_exec(data, &self.class._process_block)
113
+ else
114
+ data
115
+ end
116
+ end
117
+
118
+ # Phase 3: Transform - Handled by Presentable concern
119
+
120
+ # Phase 4: Respond - Handled by Viewable concern
121
+
122
+ # Error handlers for runtime errors
123
+ #
124
+ # These methods handle unexpected errors during service execution by raising
125
+ # appropriate runtime exceptions. They log the error and wrap it with service context.
126
+
127
+ # Handle resource not found errors (ActiveRecord::RecordNotFound)
128
+ #
129
+ # @param error [ActiveRecord::RecordNotFound] The original not found error
130
+ # @raise [Errors::Runtime::ResourceNotFoundError] Wrapped error with context
131
+ def handle_not_found_error(error)
132
+ Rails.logger.error "Resource not found in #{self.class.name}: #{error.message}" if defined?(Rails)
133
+
134
+ raise Errors::Runtime::ResourceNotFoundError.new(
135
+ "Resource not found: #{error.message}",
136
+ code: BetterService::ErrorCodes::RESOURCE_NOT_FOUND,
137
+ original_error: error,
138
+ context: { service: self.class.name, params: @params }
139
+ )
140
+ end
141
+
142
+ # Handle database errors (ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved)
143
+ #
144
+ # @param error [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved] The original database error
145
+ # @raise [Errors::Runtime::DatabaseError] Wrapped error with context
146
+ def handle_database_error(error)
147
+ Rails.logger.error "Database error in #{self.class.name}: #{error.message}" if defined?(Rails)
148
+ Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
149
+
150
+ raise Errors::Runtime::DatabaseError.new(
151
+ "Database error: #{error.message}",
152
+ code: BetterService::ErrorCodes::DATABASE_ERROR,
153
+ original_error: error,
154
+ context: { service: self.class.name, params: @params }
155
+ )
156
+ end
157
+
158
+ # Handle unexpected errors (all other StandardError)
159
+ #
160
+ # @param error [StandardError] The original unexpected error
161
+ # @raise [Errors::Runtime::ExecutionError] Wrapped error with context
162
+ def handle_unexpected_error(error)
163
+ Rails.logger.error "Unexpected error in #{self.class.name}: #{error.message}" if defined?(Rails)
164
+ Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
165
+
166
+ raise Errors::Runtime::ExecutionError.new(
167
+ "Service execution failed: #{error.message}",
168
+ code: BetterService::ErrorCodes::EXECUTION_ERROR,
169
+ original_error: error,
170
+ context: { service: self.class.name, params: @params }
171
+ )
172
+ end
173
+
174
+ def validate_user_presence!(user)
175
+ return if user.present?
176
+
177
+ raise Errors::Configuration::NilUserError,
178
+ "User cannot be nil for #{self.class.name}. " \
179
+ "Add 'allow_nil_user true' in config block if this is intentional."
180
+ end
181
+
182
+ def validate_schema_presence!
183
+ return if self.class.schema_defined?
184
+
185
+ raise Errors::Configuration::SchemaRequiredError,
186
+ "#{self.class.name} must define a schema block. " \
187
+ "Add 'schema do ... end' to validate params."
188
+ end
189
+
190
+ def safe_params_to_hash(params)
191
+ return {} if params.nil?
192
+
193
+ # Handle ActionController::Parameters by converting to unsafe hash first
194
+ if params.respond_to?(:to_unsafe_h)
195
+ params.to_unsafe_h.deep_symbolize_keys
196
+ elsif params.respond_to?(:to_h)
197
+ params.to_h.deep_symbolize_keys
198
+ elsif params.is_a?(Hash)
199
+ params.deep_symbolize_keys
200
+ else
201
+ {}
202
+ end
203
+ rescue StandardError => e
204
+ Rails.logger.warn "Failed to convert params to hash: #{e.message}" if defined?(Rails)
205
+ {}
206
+ end
207
+
208
+ def success_result(message, data = {})
209
+ # Extract metadata if provided in data, or initialize empty
210
+ provided_metadata = data.delete(:metadata) || {}
211
+
212
+ # Build metadata with action if action_name is set
213
+ metadata = {}
214
+ metadata[:action] = self.class._action_name if self.class._action_name.present?
215
+ metadata.merge!(provided_metadata)
216
+
217
+ {
218
+ success: true,
219
+ message: message,
220
+ metadata: metadata,
221
+ **data
222
+ }
223
+ end
224
+
225
+ def failure_result(message, errors = {}, code: nil)
226
+ result = {
227
+ success: false,
228
+ error: message,
229
+ errors: errors
230
+ }
231
+ result[:code] = code if code
232
+ result
233
+ end
234
+
235
+ def error_result(message, code: nil)
236
+ result = {
237
+ success: false,
238
+ errors: [message]
239
+ }
240
+ result[:code] = code if code
241
+ result
242
+ end
243
+
244
+ # Prepend Instrumentation at the end, after call method is defined
245
+ # This wraps the entire call method (including cache logic from Cacheable)
246
+ prepend Concerns::Instrumentation
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # CreateService - Specialized service for creating new resources
8
+ #
9
+ # Returns: { resource: {}, metadata: { action: :created } }
10
+ #
11
+ # Example:
12
+ # class Bookings::CreateService < BetterService::Services::CreateService
13
+ # schema do
14
+ # required(:title).filled(:string)
15
+ # required(:date).filled(:date)
16
+ # end
17
+ #
18
+ # search_with do
19
+ # {}
20
+ # end
21
+ #
22
+ # process_with do |data|
23
+ # booking = user.bookings.create!(
24
+ # title: params[:title],
25
+ # date: params[:date]
26
+ # )
27
+ # { resource: booking }
28
+ # end
29
+ # end
30
+ class CreateService < Services::Base
31
+ # Set action_name for metadata
32
+ self._action_name = :created
33
+
34
+ # Enable database transactions by default for create operations
35
+ with_transaction true
36
+
37
+ # Default empty schema - subclasses MUST override with specific validations
38
+ schema do
39
+ # Override in subclass with required fields
40
+ end
41
+
42
+ private
43
+
44
+ # Override respond to ensure resource key is present
45
+ def respond(data)
46
+ # Get base result (from custom respond_with block or default)
47
+ if self.class._respond_block
48
+ result = instance_exec(data, &self.class._respond_block)
49
+ else
50
+ result = success_result("Resource created successfully", data)
51
+ end
52
+
53
+ # Ensure resource key exists (default to nil if not provided)
54
+ result[:resource] ||= nil
55
+
56
+ result
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # DestroyService - Specialized service for deleting resources
8
+ #
9
+ # Returns: { resource: {}, metadata: { action: :deleted } }
10
+ #
11
+ # Example:
12
+ # class Bookings::DestroyService < BetterService::Services::DestroyService
13
+ # schema do
14
+ # required(:id).filled(:integer)
15
+ # end
16
+ #
17
+ # search_with do
18
+ # { resource: user.bookings.find(params[:id]) }
19
+ # end
20
+ #
21
+ # process_with do |data|
22
+ # booking = data[:resource]
23
+ # booking.destroy!
24
+ # { resource: booking }
25
+ # end
26
+ # end
27
+ class DestroyService < Services::Base
28
+ # Set action_name for metadata
29
+ self._action_name = :deleted
30
+
31
+ # Enable database transactions by default for destroy operations
32
+ with_transaction true
33
+
34
+ # Default schema - requires id parameter
35
+ schema do
36
+ required(:id).filled
37
+ end
38
+
39
+ private
40
+
41
+ # Override respond to ensure resource key is present
42
+ def respond(data)
43
+ # Get base result (from custom respond_with block or default)
44
+ if self.class._respond_block
45
+ result = instance_exec(data, &self.class._respond_block)
46
+ else
47
+ result = success_result("Resource deleted successfully", data)
48
+ end
49
+
50
+ # Ensure resource key exists (default to nil if not provided)
51
+ result[:resource] ||= nil
52
+
53
+ result
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # IndexService - Specialized service for list/collection endpoints
8
+ #
9
+ # Returns: { items: [], metadata: { action: :index, stats: {}, pagination: {} } }
10
+ #
11
+ # Example:
12
+ # class Bookings::IndexService < BetterService::Services::IndexService
13
+ # search_with do
14
+ # { items: user.bookings.to_a }
15
+ # end
16
+ #
17
+ # process_with do |data|
18
+ # {
19
+ # items: data[:items],
20
+ # metadata: {
21
+ # stats: { total: data[:items].count },
22
+ # pagination: { page: params[:page] || 1 }
23
+ # }
24
+ # }
25
+ # end
26
+ # end
27
+ class IndexService < Services::Base
28
+ # Set action_name for metadata
29
+ self._action_name = :index
30
+
31
+ # Default schema - can be overridden in subclasses
32
+ schema do
33
+ optional(:page).filled(:integer, gteq?: 1)
34
+ optional(:per_page).filled(:integer, gteq?: 1, lteq?: 100)
35
+ optional(:search).maybe(:string)
36
+ end
37
+
38
+ private
39
+
40
+ # Override respond to ensure items key is present
41
+ def respond(data)
42
+ # Get base result (from custom respond_with block or default)
43
+ if self.class._respond_block
44
+ result = instance_exec(data, &self.class._respond_block)
45
+ else
46
+ result = success_result("Operation completed successfully", data)
47
+ end
48
+
49
+ # Ensure items key exists (default to empty array if not provided)
50
+ result[:items] ||= []
51
+
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # ShowService - Specialized service for single resource detail endpoints
8
+ #
9
+ # Returns: { resource: {}, metadata: { action: :show } }
10
+ #
11
+ # Example:
12
+ # class Bookings::ShowService < BetterService::Services::ShowService
13
+ # search_with do
14
+ # { resource: user.bookings.find(params[:id]) }
15
+ # end
16
+ # end
17
+ class ShowService < Services::Base
18
+ # Set action_name for metadata
19
+ self._action_name = :show
20
+
21
+ # Default schema - requires id parameter
22
+ schema do
23
+ required(:id).filled
24
+ end
25
+
26
+ private
27
+
28
+ # Override respond to ensure resource key is present
29
+ def respond(data)
30
+ # Get base result (from custom respond_with block or default)
31
+ if self.class._respond_block
32
+ result = instance_exec(data, &self.class._respond_block)
33
+ else
34
+ result = success_result("Operation completed successfully", data)
35
+ end
36
+
37
+ # Ensure resource key exists (default to nil if not provided)
38
+ result[:resource] ||= nil
39
+
40
+ result
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # UpdateService - Specialized service for updating existing resources
8
+ #
9
+ # Returns: { resource: {}, metadata: { action: :updated } }
10
+ #
11
+ # Example:
12
+ # class Bookings::UpdateService < BetterService::Services::UpdateService
13
+ # schema do
14
+ # required(:id).filled(:integer)
15
+ # optional(:title).filled(:string)
16
+ # end
17
+ #
18
+ # search_with do
19
+ # { resource: user.bookings.find(params[:id]) }
20
+ # end
21
+ #
22
+ # process_with do |data|
23
+ # booking = data[:resource]
24
+ # booking.update!(params.except(:id))
25
+ # { resource: booking }
26
+ # end
27
+ # end
28
+ class UpdateService < Services::Base
29
+ # Set action_name for metadata
30
+ self._action_name = :updated
31
+
32
+ # Enable database transactions by default for update operations
33
+ with_transaction true
34
+
35
+ # Default schema - requires id parameter
36
+ schema do
37
+ required(:id).filled
38
+ end
39
+
40
+ private
41
+
42
+ # Override respond to ensure resource key is present
43
+ def respond(data)
44
+ # Get base result (from custom respond_with block or default)
45
+ if self.class._respond_block
46
+ result = instance_exec(data, &self.class._respond_block)
47
+ else
48
+ result = success_result("Resource updated successfully", data)
49
+ end
50
+
51
+ # Ensure resource key exists (default to nil if not provided)
52
+ result[:resource] ||= nil
53
+
54
+ result
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Subscribers
5
+ # LogSubscriber - Built-in subscriber that logs service events
6
+ #
7
+ # This subscriber logs all service events to Rails.logger.
8
+ # Enable it in configuration to get automatic logging for all services.
9
+ #
10
+ # @example Enable in initializer
11
+ # BetterService.configure do |config|
12
+ # config.log_subscriber_enabled = true
13
+ # config.log_subscriber_level = :info
14
+ # end
15
+ class LogSubscriber
16
+ class << self
17
+ # Attach the subscriber to ActiveSupport::Notifications
18
+ #
19
+ # This method is called automatically when subscriber is enabled.
20
+ # It subscribes to all service.* events.
21
+ #
22
+ # @return [void]
23
+ def attach
24
+ subscribe_to_service_events
25
+ subscribe_to_cache_events
26
+ end
27
+
28
+ private
29
+
30
+ # Subscribe to service lifecycle events
31
+ #
32
+ # @return [void]
33
+ def subscribe_to_service_events
34
+ ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
35
+ log_service_started(payload)
36
+ end
37
+
38
+ ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
39
+ log_service_completed(payload)
40
+ end
41
+
42
+ ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
43
+ log_service_failed(payload)
44
+ end
45
+ end
46
+
47
+ # Subscribe to cache events
48
+ #
49
+ # @return [void]
50
+ def subscribe_to_cache_events
51
+ ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
52
+ log_cache_hit(payload)
53
+ end
54
+
55
+ ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
56
+ log_cache_miss(payload)
57
+ end
58
+ end
59
+
60
+ # Log service started event
61
+ #
62
+ # @param payload [Hash] Event payload
63
+ # @return [void]
64
+ def log_service_started(payload)
65
+ message = "[BetterService] #{payload[:service_name]} started"
66
+ message += " (user: #{payload[:user_id]})" if payload[:user_id]
67
+
68
+ log(message)
69
+ end
70
+
71
+ # Log service completed event
72
+ #
73
+ # @param payload [Hash] Event payload
74
+ # @return [void]
75
+ def log_service_completed(payload)
76
+ message = "[BetterService] #{payload[:service_name]} completed in #{payload[:duration]}ms"
77
+ message += " (user: #{payload[:user_id]})" if payload[:user_id]
78
+ message += " [CACHED]" if payload[:cache_hit]
79
+
80
+ log(message)
81
+ end
82
+
83
+ # Log service failed event
84
+ #
85
+ # @param payload [Hash] Event payload
86
+ # @return [void]
87
+ def log_service_failed(payload)
88
+ message = "[BetterService] #{payload[:service_name]} failed after #{payload[:duration]}ms"
89
+ message += " (user: #{payload[:user_id]})" if payload[:user_id]
90
+ message += " - #{payload[:error_class]}: #{payload[:error_message]}"
91
+
92
+ log(message, :error)
93
+ end
94
+
95
+ # Log cache hit event
96
+ #
97
+ # @param payload [Hash] Event payload
98
+ # @return [void]
99
+ def log_cache_hit(payload)
100
+ message = "[BetterService::Cache] HIT - #{payload[:service_name]}"
101
+ message += " (context: #{payload[:context]})" if payload[:context]
102
+
103
+ log(message, :debug)
104
+ end
105
+
106
+ # Log cache miss event
107
+ #
108
+ # @param payload [Hash] Event payload
109
+ # @return [void]
110
+ def log_cache_miss(payload)
111
+ message = "[BetterService::Cache] MISS - #{payload[:service_name]}"
112
+ message += " (context: #{payload[:context]})" if payload[:context]
113
+
114
+ log(message, :debug)
115
+ end
116
+
117
+ # Write to Rails logger
118
+ #
119
+ # @param message [String] Log message
120
+ # @param level [Symbol] Log level (:debug, :info, :warn, :error)
121
+ # @return [void]
122
+ def log(message, level = nil)
123
+ return unless defined?(Rails) && Rails.logger
124
+
125
+ level ||= BetterService.configuration.log_subscriber_level
126
+ Rails.logger.send(level, message)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end