servus 1.0.2 → 1.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: 11fe445ce97bbd8300a08023e80eb1516011657d7412b6f5c9215f6c40aeb12d
4
- data.tar.gz: 4cf1448c26b970cea781ccc2e7c21ddf5b8bde6f7703d070b59ea0809d752d1b
3
+ metadata.gz: 10e4e061222d303e5d694d816afd1d0557eb6c4e7804dd5b454e48cddcdf0797
4
+ data.tar.gz: b6cdb1fcabe827988e6e8daf44d20d2da2649cf4b252d36ac68dd1f8f14db584
5
5
  SHA512:
6
- metadata.gz: f294661ff53e5340f52c8de15485c06c16820cb2b90b8d517c8e01322ee54fb0e7beec46f9560b3fe47d4c09b184474ccb5634368c2d902b0ab2c2106407d5bb
7
- data.tar.gz: 3772632d087a92a26dcf24268570d1a4c1fd8887df493dd0a4c39b5cf899db8d5ec8e0bd3397738d4d64934f345ae9778eb7fef98e3a7f415d3f166f31005859
6
+ metadata.gz: 61ab96b3ed1da09eb768ced10cbacc5cb6f4e8a8f4aead7ed57aa6f49dbdd016513dee1f1f2402a7a21504913b9ae5c0ef23298301fc398c3dcb6f224d2dcedc
7
+ data.tar.gz: 011eaaabc88c46604d60c9f82727e43f6152521a7f1cb64b483a1b912eb0a047eaf8607eab31b51f31a92df90333c4130af845f10a8d7263aa3bcfd3fda85f60
data/lib/servus/base.rb CHANGED
@@ -115,6 +115,40 @@ module Servus
115
115
  Response = Servus::Support::Response
116
116
  Validator = Servus::Support::Validator
117
117
 
118
+ class << self
119
+ # Assigns the logger Servus writes this service's call, outcome, and
120
+ # error lines through. It covers this class and every service below it.
121
+ #
122
+ # Build the logger from {Servus.logger} so it keeps the appenders and
123
+ # formatting the app configured, rather than replacing them.
124
+ #
125
+ # Assigning +nil+ returns the class to the logger it inherits.
126
+ #
127
+ # @param logger [::Logger, nil]
128
+ #
129
+ # @example Giving an engine's services their own logger
130
+ # class Treasury::ApplicationService < Servus::Base
131
+ # self.logger = SemanticLogger[Treasury]
132
+ # end
133
+ attr_writer :logger
134
+
135
+ # The logger for this service class: its own, else the one it inherits,
136
+ # else {Servus.logger}.
137
+ #
138
+ # @return [::Logger]
139
+ def logger
140
+ return @logger if @logger
141
+
142
+ superclass <= Servus::Base ? superclass.logger : Servus.logger
143
+ end
144
+ end
145
+
146
+ # @return [::Logger] the logger of this service's class
147
+ # @see .logger
148
+ def logger
149
+ self.class.logger
150
+ end
151
+
118
152
  # Creates a successful response with the provided data.
119
153
  #
120
154
  # Use this method to return successful results from your service's call method.
@@ -182,6 +216,46 @@ module Servus
182
216
  Response.new(false, data, error)
183
217
  end
184
218
 
219
+ # Ends the service with a failure response, from anywhere inside +call+.
220
+ #
221
+ # Builds the same response as {#failure} and returns it from the service as
222
+ # if +call+ had returned it, so it is logged, validated against the failure
223
+ # schema, and emits events exactly as a returned failure does. Unlike
224
+ # {#failure}, it does not need to be the value of +call+: a private check
225
+ # method can end the service itself, which keeps +call+ a list of steps.
226
+ #
227
+ # It is not an exception: +rescue+ does not see it, and +ensure+ still runs.
228
+ # A failure ends only the service that raised it — a nested service called
229
+ # from +call+ hands its failure back as a response, as it always has.
230
+ #
231
+ # @param message [String, nil] custom error message (uses error type's default if nil)
232
+ # @param data [Object, nil] optional structured data to attach to the failure response
233
+ # @param type [Class] error class to instantiate (must inherit from ServiceError)
234
+ # @return [void] never returns
235
+ #
236
+ # @example Ending the service from a check
237
+ # def call
238
+ # account_open!
239
+ # success(account.withdraw!(amount))
240
+ # end
241
+ #
242
+ # private
243
+ #
244
+ # def account_open!
245
+ # return if account.open?
246
+ #
247
+ # failure!("Account is closed", type: Servus::Support::Errors::UnprocessableEntityError)
248
+ # end
249
+ #
250
+ # @note Leaving an ActiveRecord +transaction+ (or +with_lock+) block this way
251
+ # commits it, as any non-exception exit does. Run checks before writing, or
252
+ # raise to roll back.
253
+ # @see #failure
254
+ # @see #error!
255
+ def failure!(message = nil, data: nil, type: Servus::Support::Errors::ServiceError)
256
+ throw :service_failure, failure(message, data:, type:)
257
+ end
258
+
185
259
  # Logs an error and raises an exception, halting service execution.
186
260
  #
187
261
  # Use this method when you need to immediately halt execution with an exception
@@ -254,7 +328,7 @@ module Servus
254
328
 
255
329
  # Wrap execution in catch block to handle guard failures
256
330
  result = catch(:guard_failure) do
257
- benchmark(**args) { instance.send(:call) }
331
+ benchmark(**args) { catch(:service_failure) { instance.send(:call) } }
258
332
  end
259
333
 
260
334
  if result.is_a?(Servus::Support::Errors::GuardError)
data/lib/servus/config.rb CHANGED
@@ -76,6 +76,20 @@ module Servus
76
76
  # @return [Boolean] true to require payload schemas, false to allow schema-less events
77
77
  attr_accessor :require_event_payload_schema
78
78
 
79
+ # The logger Servus writes through.
80
+ #
81
+ # Left +nil+ (the default), Servus resolves one itself: `Rails.logger`
82
+ # when Rails is loaded, otherwise a `$stdout` logger. Read the resolved
83
+ # logger through {Servus.logger}.
84
+ #
85
+ # @return [::Logger, nil] the configured logger, or nil when none is set
86
+ #
87
+ # @example
88
+ # Servus.configure do |config|
89
+ # config.logger = SemanticLogger[Servus]
90
+ # end
91
+ attr_accessor :logger
92
+
79
93
  # The ordered list of routers that resolve invocations for events.
80
94
  #
81
95
  # The Bus iterates routers in order, collects invocations, deduplicates
@@ -181,6 +195,21 @@ module Servus
181
195
  @config ||= Config.new
182
196
  end
183
197
 
198
+ # The logger Servus writes through.
199
+ #
200
+ # Build a service's own logger from this one rather than replacing it, so
201
+ # the app's appenders and formatting are kept.
202
+ #
203
+ # @return [::Logger]
204
+ #
205
+ # @example A per-service logger that adds a tag (SemanticLogger)
206
+ # class Treasury::ApplicationService < Servus::Base
207
+ # self.logger = Servus.logger.tagged(engine: 'treasury')
208
+ # end
209
+ def self.logger
210
+ Servus::Support::Logger.logger
211
+ end
212
+
184
213
  # Yields the configuration for modification.
185
214
  #
186
215
  # @yieldparam config [Servus::Config] the configuration object to modify
@@ -6,15 +6,16 @@ module Servus
6
6
  module Support
7
7
  # Logger class for logging service calls and results
8
8
  class Logger
9
- # Returns the logger instance depending on the environment
9
+ # The logger Servus writes through: the one set in {Servus::Config#logger},
10
+ # else `Rails.logger`, else a `$stdout` logger. Resolved on every read, so
11
+ # Servus follows Rails if the application swaps its logger after boot.
10
12
  #
11
- # @return [Logger] The logger instance
13
+ # Lines about a service go to that service's {Servus::Base.logger} instead.
14
+ #
15
+ # @return [::Logger] The logger instance
16
+ # @see Servus.logger
12
17
  def self.logger
13
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
14
- Rails.logger
15
- else
16
- @logger ||= ::Logger.new($stdout)
17
- end
18
+ Servus.config.logger || rails_logger || stdout_logger
18
19
  end
19
20
 
20
21
  # Logs a call to a service.
@@ -27,7 +28,7 @@ module Servus
27
28
  # @param args [Hash] The arguments passed to the service
28
29
  def self.log_call(service_class, args)
29
30
  rendered = log_parameters(args)
30
- logger.info("Calling #{service_class.name} with args: #{rendered.inspect}")
31
+ service_class.logger.info("Calling #{service_class.name} with args: #{rendered.inspect}")
31
32
  end
32
33
 
33
34
  # Logs a result from a service
@@ -48,7 +49,7 @@ module Servus
48
49
  # @param service_class [Class] The service class
49
50
  # @param duration [Float] The duration of the service call
50
51
  def self.log_success(service_class, duration)
51
- logger.info("#{service_class.name} succeeded in #{duration.round(3)}s")
52
+ service_class.logger.info("#{service_class.name} succeeded in #{duration.round(3)}s")
52
53
  end
53
54
 
54
55
  # Logs a failed result from a service
@@ -57,7 +58,7 @@ module Servus
57
58
  # @param error [Servus::Support::Errors::ServiceError] The error from the service
58
59
  # @param duration [Float] The duration of the service call
59
60
  def self.log_failure(service_class, error, duration)
60
- logger.warn("#{service_class.name} failed in #{duration.round(3)}s with error: #{error}")
61
+ service_class.logger.warn("#{service_class.name} failed in #{duration.round(3)}s with error: #{error}")
61
62
  end
62
63
 
63
64
  # Logs a guard failure from a service
@@ -65,7 +66,7 @@ module Servus
65
66
  # @param service_class [Class] The service class
66
67
  # @param error [Servus::Support::Errors::GuardError] The guard error
67
68
  def self.log_guard_failure(service_class, error)
68
- logger.warn("#{service_class.name} guard failed: #{error.message}")
69
+ service_class.logger.warn("#{service_class.name} guard failed: #{error.message}")
69
70
  end
70
71
 
71
72
  # Logs an event emission with correlation ID and duration.
@@ -83,7 +84,7 @@ module Servus
83
84
  # @param service_class [Class] The service class
84
85
  # @param error [Servus::Support::Errors::ValidationError] The validation error
85
86
  def self.log_validation_error(service_class, error)
86
- logger.error("#{service_class.name} validation error: #{error.message}")
87
+ service_class.logger.error("#{service_class.name} validation error: #{error.message}")
87
88
  end
88
89
 
89
90
  # Logs an uncaught exception from a service
@@ -91,7 +92,9 @@ module Servus
91
92
  # @param service_class [Class] The service class
92
93
  # @param exception [Exception] The uncaught exception
93
94
  def self.log_exception(service_class, exception)
94
- logger.error("#{service_class.name} uncaught exception: #{exception.class} - #{exception.message}")
95
+ service_class.logger.error(
96
+ "#{service_class.name} uncaught exception: #{exception.class} - #{exception.message}"
97
+ )
95
98
  end
96
99
 
97
100
  # Logs that a registered schema fragment was replaced with a different value.
@@ -113,7 +116,7 @@ module Servus
113
116
  # @param service_class [Class<Servus::Base>] The service whose job was not published
114
117
  # @param const_name [String] The constant the application already owns
115
118
  def self.log_job_class_conflict(service_class, const_name)
116
- logger.warn(
119
+ service_class.logger.warn(
117
120
  "#{service_class.name} did not publish its generated job as #{const_name} — that constant " \
118
121
  'is already defined by the application. The application class is unchanged; ' \
119
122
  "#{service_class.name}.call_async cannot be enqueued until the service or the job is renamed."
@@ -131,6 +134,18 @@ module Servus
131
134
  Servus.config.parameter_filter.filter(params)
132
135
  end
133
136
  end
137
+
138
+ # @return [::Logger, nil] Rails' logger, when Rails is loaded and has one
139
+ def self.rails_logger
140
+ Rails.logger if defined?(Rails) && Rails.respond_to?(:logger)
141
+ end
142
+ private_class_method :rails_logger
143
+
144
+ # @return [::Logger] the fallback logger, built once
145
+ def self.stdout_logger
146
+ @stdout_logger ||= ::Logger.new($stdout)
147
+ end
148
+ private_class_method :stdout_logger
134
149
  end
135
150
  end
136
151
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = '1.0.2'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-07 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport