servus 1.0.1 → 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 +4 -4
- data/lib/servus/base.rb +75 -1
- data/lib/servus/config.rb +29 -0
- data/lib/servus/extensions/async/call.rb +97 -10
- data/lib/servus/extensions/async/errors.rb +23 -0
- data/lib/servus/extensions/async/ext.rb +28 -0
- data/lib/servus/railtie.rb +1 -1
- data/lib/servus/support/logger.rb +44 -13
- data/lib/servus/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10e4e061222d303e5d694d816afd1d0557eb6c4e7804dd5b454e48cddcdf0797
|
|
4
|
+
data.tar.gz: b6cdb1fcabe827988e6e8daf44d20d2da2649cf4b252d36ac68dd1f8f14db584
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -70,21 +70,18 @@ module Servus
|
|
|
70
70
|
# @see Servus::Base.call
|
|
71
71
|
# @see #servus_job_class
|
|
72
72
|
def call_async(**args)
|
|
73
|
-
|
|
74
|
-
job_options = args.slice(:wait, :wait_until, :queue, :priority)
|
|
75
|
-
job_options.merge!(args.delete(:job_options) || {}) # merge custom job options
|
|
76
|
-
job_options.compact!
|
|
73
|
+
refuse_nameless_job!
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
args.except!(:wait, :wait_until, :queue, :priority, :job_options)
|
|
75
|
+
job_options = extract_job_options!(args)
|
|
80
76
|
|
|
81
77
|
# The named job class identifies the service — only args are serialized.
|
|
82
78
|
job = job_options.any? ? servus_job_class.set(**job_options) : servus_job_class
|
|
83
79
|
job.perform_later(**args)
|
|
84
|
-
rescue Servus::Support::Errors::ServiceError, Servus::Events::Errors::Error
|
|
80
|
+
rescue Servus::Support::Errors::ServiceError, Servus::Events::Errors::Error, Errors::AsyncError
|
|
85
81
|
# With the :inline and :test adapters perform_later runs the service,
|
|
86
|
-
# so Servus's own errors surface here
|
|
87
|
-
#
|
|
82
|
+
# so Servus's own errors surface here — as do this extension's own,
|
|
83
|
+
# like the name-conflict refusal above. Wrapping any of them as an
|
|
84
|
+
# enqueue failure would blame the wrong layer.
|
|
88
85
|
raise
|
|
89
86
|
rescue StandardError => e
|
|
90
87
|
raise Errors::JobEnqueueError, "Failed to enqueue async job for #{self}: #{e.message}"
|
|
@@ -158,6 +155,23 @@ module Servus
|
|
|
158
155
|
|
|
159
156
|
private
|
|
160
157
|
|
|
158
|
+
# Splits ActiveJob configuration out of the combined argument hash, mutating
|
|
159
|
+
# +args+ so only the service's own arguments remain.
|
|
160
|
+
#
|
|
161
|
+
# @param args [Hash] combined service arguments and job configuration options
|
|
162
|
+
# @return [Hash] the ActiveJob options (+wait+, +queue+, …) for +set+
|
|
163
|
+
# @api private
|
|
164
|
+
def extract_job_options!(args)
|
|
165
|
+
job_options = args.slice(:wait, :wait_until, :queue, :priority)
|
|
166
|
+
job_options.merge!(args.delete(:job_options) || {}) # merge custom job options
|
|
167
|
+
job_options.compact!
|
|
168
|
+
|
|
169
|
+
# Remove special keys that shouldn't be passed to the service
|
|
170
|
+
args.except!(:wait, :wait_until, :queue, :priority, :job_options)
|
|
171
|
+
|
|
172
|
+
job_options
|
|
173
|
+
end
|
|
174
|
+
|
|
161
175
|
# Generates a named job subclass for this service and installs it as a sibling
|
|
162
176
|
# constant in the service's parent namespace.
|
|
163
177
|
#
|
|
@@ -166,6 +180,12 @@ module Servus
|
|
|
166
180
|
# {#inherited}) means Rails' production eager-load defines every service's job
|
|
167
181
|
# at boot, so worker processes can constantize the serialized job name.
|
|
168
182
|
#
|
|
183
|
+
# An app that already owns that constant keeps it: +Foo+ alongside a
|
|
184
|
+
# hand-written +FooJob+ is ordinary Rails, and overwriting it replaced a real
|
|
185
|
+
# job class with this one silently. The generated class is still returned so
|
|
186
|
+
# the service keeps working, but it is unnamed — {#call_async} refuses it with
|
|
187
|
+
# {Errors::JobNameConflictError} rather than enqueue an unresolvable job.
|
|
188
|
+
#
|
|
169
189
|
# @return [Class<Servus::Extensions::Async::Job>] the generated job class
|
|
170
190
|
# @api private
|
|
171
191
|
def build_servus_job_class
|
|
@@ -174,10 +194,77 @@ module Servus
|
|
|
174
194
|
klass = Class.new(Servus::Extensions::Async::Job)
|
|
175
195
|
klass.servus_service = self
|
|
176
196
|
|
|
177
|
-
|
|
197
|
+
publish_job_const(servus_job_const_name, klass)
|
|
178
198
|
|
|
179
199
|
klass
|
|
180
200
|
end
|
|
201
|
+
|
|
202
|
+
# @return [String] the sibling constant the service's job is published under
|
|
203
|
+
# @api private
|
|
204
|
+
def servus_job_const_name
|
|
205
|
+
"#{name.demodulize}Job"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A job with no name lost its constant to the application (see
|
|
209
|
+
# {#publish_job_const}). ActiveJob serializes a job by its class name, so
|
|
210
|
+
# enqueueing it would succeed and then fail on the worker at
|
|
211
|
+
# deserialization — refuse at the call site instead.
|
|
212
|
+
#
|
|
213
|
+
# @raise [Errors::JobNameConflictError] when the service's job is unnamed
|
|
214
|
+
# @return [void]
|
|
215
|
+
# @api private
|
|
216
|
+
def refuse_nameless_job!
|
|
217
|
+
return unless servus_job_class.name.nil?
|
|
218
|
+
|
|
219
|
+
raise Errors::JobNameConflictError.for(self, qualified_const_name(servus_job_const_name))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Installs the generated job as a sibling constant, unless the application
|
|
223
|
+
# owns that name — in which case its class is left alone and the skip is
|
|
224
|
+
# logged.
|
|
225
|
+
#
|
|
226
|
+
# @param const_name [String]
|
|
227
|
+
# @param klass [Class<Servus::Extensions::Async::Job>]
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @api private
|
|
230
|
+
def publish_job_const(const_name, klass)
|
|
231
|
+
unless job_const_available?(const_name)
|
|
232
|
+
return Servus::Support::Logger.log_job_class_conflict(self, qualified_const_name(const_name))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Reclaiming a stale generated job: drop it first so Ruby does not warn
|
|
236
|
+
# about an already-initialized constant on every reload.
|
|
237
|
+
module_parent.send(:remove_const, const_name) if module_parent.const_defined?(const_name, false)
|
|
238
|
+
module_parent.const_set(const_name, klass)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Whether this service may publish its job class as +const_name+.
|
|
242
|
+
#
|
|
243
|
+
# Free names are available, and so is a name already holding a generated job —
|
|
244
|
+
# that is our own constant from a previous definition, which survives a
|
|
245
|
+
# development reload when the parent namespace is +Object+ and therefore
|
|
246
|
+
# unmanaged by Zeitwerk.
|
|
247
|
+
#
|
|
248
|
+
# A pending autoload is treated as the app's without resolving it: forcing the
|
|
249
|
+
# load here would run app code in the middle of defining a service.
|
|
250
|
+
#
|
|
251
|
+
# @param const_name [String]
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
# @api private
|
|
254
|
+
def job_const_available?(const_name)
|
|
255
|
+
return true unless module_parent.const_defined?(const_name, false)
|
|
256
|
+
return false if module_parent.autoload?(const_name)
|
|
257
|
+
|
|
258
|
+
existing = module_parent.const_get(const_name, false)
|
|
259
|
+
existing.is_a?(Class) && existing <= Servus::Extensions::Async::Job
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @param const_name [String]
|
|
263
|
+
# @return [String] the constant's fully qualified name
|
|
264
|
+
# @api private
|
|
265
|
+
def qualified_const_name(const_name)
|
|
266
|
+
module_parent.equal?(Object) ? const_name : "#{module_parent.name}::#{const_name}"
|
|
267
|
+
end
|
|
181
268
|
end
|
|
182
269
|
end
|
|
183
270
|
end
|
|
@@ -22,6 +22,29 @@ module Servus
|
|
|
22
22
|
# Services::SendEmail::Service.call_async(user_id: 123)
|
|
23
23
|
# # => Servus::Extensions::Async::Errors::JobEnqueueError: Failed to enqueue async job
|
|
24
24
|
class JobEnqueueError < AsyncError; end
|
|
25
|
+
|
|
26
|
+
# Raised when +call_async+ is invoked on a service whose generated job was
|
|
27
|
+
# never published because the application already owns the constant.
|
|
28
|
+
#
|
|
29
|
+
# The generated job class exists but has no name, and ActiveJob serializes a
|
|
30
|
+
# job by its class name — enqueueing it would succeed and then fail on the
|
|
31
|
+
# worker at deserialization, far from the call site. Refusing here keeps the
|
|
32
|
+
# failure where it was caused.
|
|
33
|
+
#
|
|
34
|
+
# @see Servus::Extensions::Async::Call#call_async
|
|
35
|
+
class JobNameConflictError < AsyncError
|
|
36
|
+
# @param service [Class] the service whose job constant is taken
|
|
37
|
+
# @param const_name [String] the constant the application owns
|
|
38
|
+
# @return [JobNameConflictError]
|
|
39
|
+
def self.for(service, const_name)
|
|
40
|
+
new(
|
|
41
|
+
"Cannot enqueue #{service.name}: its generated job was not published because " \
|
|
42
|
+
"#{const_name} is already defined by the application. An unnamed job would " \
|
|
43
|
+
'enqueue but no worker could deserialize it. Rename the service or the ' \
|
|
44
|
+
"application's #{const_name} to stop the collision."
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
25
48
|
end
|
|
26
49
|
end
|
|
27
50
|
end
|
|
@@ -12,6 +12,7 @@ module Servus
|
|
|
12
12
|
module Async
|
|
13
13
|
require 'active_support/core_ext/module/introspection' # Module#module_parent
|
|
14
14
|
require 'active_support/core_ext/string/inflections' # String#demodulize
|
|
15
|
+
require 'active_support/core_ext/class/subclasses' # Class#descendants
|
|
15
16
|
|
|
16
17
|
require 'servus/extensions/async/errors'
|
|
17
18
|
require 'servus/extensions/async/job'
|
|
@@ -21,6 +22,33 @@ module Servus
|
|
|
21
22
|
#
|
|
22
23
|
# @api private
|
|
23
24
|
module Ext; end
|
|
25
|
+
|
|
26
|
+
# Installs async support on {Servus::Base} and backfills job classes for
|
|
27
|
+
# services that were already defined.
|
|
28
|
+
#
|
|
29
|
+
# The backfill exists because {Call#inherited} — which publishes a service's
|
|
30
|
+
# job constant — only exists once this module is installed, and installation
|
|
31
|
+
# is deferred to +on_load(:active_job)+. Servus's own railtie force-loads
|
|
32
|
+
# +app/events/*_event.rb+, and every service named by an +enqueue+ there loads
|
|
33
|
+
# with it, typically before anything has touched +ActiveJob::Base+. Those
|
|
34
|
+
# services would otherwise never publish a job constant, and a worker — which
|
|
35
|
+
# resolves jobs by name and never calls {Call#call_async} — would fail
|
|
36
|
+
# deserialization with +ActiveJob::UnknownJobClassError+.
|
|
37
|
+
#
|
|
38
|
+
# Only services still reachable under their own name are backfilled. An
|
|
39
|
+
# anonymous service has no constant to publish, and a class whose name no
|
|
40
|
+
# longer resolves to it — replaced by a reload, or defined under a namespace
|
|
41
|
+
# that has since gone — has nothing to publish it under either.
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @api private
|
|
45
|
+
def self.install!
|
|
46
|
+
Servus::Base.extend(Call)
|
|
47
|
+
|
|
48
|
+
Servus::Base.descendants.each do |service|
|
|
49
|
+
service.servus_job_class if service.name&.safe_constantize.equal?(service)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
24
52
|
end
|
|
25
53
|
end
|
|
26
54
|
end
|
data/lib/servus/railtie.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
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.
|
|
@@ -104,6 +107,22 @@ module Servus
|
|
|
104
107
|
logger.warn("Schema fragment #{key.inspect} was already registered with a different value; replacing it.")
|
|
105
108
|
end
|
|
106
109
|
|
|
110
|
+
# Logs that a service could not publish its generated job class because the
|
|
111
|
+
# constant is already taken by the application.
|
|
112
|
+
#
|
|
113
|
+
# The application's class is left alone. The service still runs, but its
|
|
114
|
+
# generated job has no name, so +call_async+ on it cannot be serialized.
|
|
115
|
+
#
|
|
116
|
+
# @param service_class [Class<Servus::Base>] The service whose job was not published
|
|
117
|
+
# @param const_name [String] The constant the application already owns
|
|
118
|
+
def self.log_job_class_conflict(service_class, const_name)
|
|
119
|
+
service_class.logger.warn(
|
|
120
|
+
"#{service_class.name} did not publish its generated job as #{const_name} — that constant " \
|
|
121
|
+
'is already defined by the application. The application class is unchanged; ' \
|
|
122
|
+
"#{service_class.name}.call_async cannot be enqueued until the service or the job is renamed."
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
107
126
|
# Filters parameters for logging based on the configured filter list.
|
|
108
127
|
#
|
|
109
128
|
# @param params [Hash] The parameters to filter
|
|
@@ -115,6 +134,18 @@ module Servus
|
|
|
115
134
|
Servus.config.parameter_filter.filter(params)
|
|
116
135
|
end
|
|
117
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
|
|
118
149
|
end
|
|
119
150
|
end
|
|
120
151
|
end
|
data/lib/servus/version.rb
CHANGED
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
|
|
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-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|