next_station 0.1.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.
data/TODO.txt ADDED
@@ -0,0 +1,6 @@
1
+ DONE - Force return of state inside the defs
2
+ DONE - Custom Error class for state not set: spec/contract/basic_flows/success_should_return_exception_if_no_success_key_spec.rb
3
+ DONE - install rubocop?
4
+ TODO - Raise exception if a step does not have a "def",
5
+ TODO: - Should "state[:result]" be created by default when ".new"?
6
+ DONE - " config.messages.backend = :i18n" In the validation section of the README is no valid dry-validation code.
@@ -0,0 +1,102 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require_relative '../lib/next_station'
5
+
6
+ # --- The Plugin Definition ---
7
+ module HttpClientPlugin
8
+ module ClassMethods
9
+ def self.extended(base)
10
+ base.extend Dry::Configurable
11
+ base.instance_eval do
12
+ setting :http_client do
13
+ setting :base_url, default: "https://example.com"
14
+ setting :timeout, default: 5
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module InstanceMethods
21
+ def http_get(path)
22
+ uri = URI.parse(self.class.config.http_client.base_url)
23
+ uri = URI.join(uri, path)
24
+
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+ http.use_ssl = (uri.scheme == 'https')
27
+ http.read_timeout = self.class.config.http_client.timeout
28
+
29
+ request = Net::HTTP::Get.new(uri)
30
+ http.request(request)
31
+ rescue StandardError => e
32
+ # In a real plugin, we would use error definitions,
33
+ # but for this example, we'll return a minimal object
34
+ Struct.new(:code, :body).new("500", e.message)
35
+ end
36
+ end
37
+
38
+ module State
39
+ def response_received?
40
+ !self[:response].nil?
41
+ end
42
+
43
+ def last_response_code
44
+ self[:response]&.code
45
+ end
46
+ end
47
+ end
48
+
49
+ # Register the plugin manually for this standalone test
50
+ NextStation::Plugins.register(:http_client, HttpClientPlugin)
51
+
52
+ # --- The Operation Definition ---
53
+ class FetchExamplePage < NextStation::Operation
54
+ plugin :http_client
55
+
56
+ # Configure the plugin
57
+ config.http_client.base_url = "https://www.example.com"
58
+ config.http_client.timeout = 10
59
+
60
+ # Tell NextStation where to find the result in the state
61
+ result_at :content_length
62
+
63
+ process do
64
+ step :call_api
65
+ step :process_response
66
+ end
67
+
68
+ def call_api(state)
69
+ publish_log :info, "Calling API with: #{self.class.config.http_client.base_url}"
70
+ response = http_get("/")
71
+ state[:response] = response
72
+ state
73
+ end
74
+
75
+ def process_response(state)
76
+ # Using the State extension defined in the plugin
77
+ if state.response_received? && state.last_response_code == "200"
78
+ publish_log :info, "Success! Response code: #{state.last_response_code}"
79
+ publish_log :info, "Body length: #{state[:response].body.length}"
80
+ # In NextStation, you just return the state (or a hash that will be merged into state)
81
+ # To signal success with data, the operation's result_key must match what we return or what's in state.
82
+ state[:content_length] = state[:response].body.length
83
+ state
84
+ else
85
+ puts "Failed! Response code: #{state.last_response_code || 'N/A'}"
86
+ # If we want to fail explicitly, we can use error! or just return something that isn't state/success
87
+ error!(type: :api_error, details: { message: "Response was #{state.last_response_code}" })
88
+ end
89
+ end
90
+ end
91
+
92
+ # --- Execution ---
93
+ puts "Starting Operation..."
94
+ result = FetchExamplePage.call
95
+
96
+ puts "\nFinal Result: #{result.success? ? 'SUCCESS' : 'FAILURE'}"
97
+ if result.success?
98
+ puts "Value: #{result.value.inspect}"
99
+ else
100
+ puts "Error: #{result.error.inspect}"
101
+ puts "Details: #{result.error.details.inspect}" if result.error.respond_to?(:details)
102
+ end
@@ -0,0 +1,149 @@
1
+ en:
2
+ next_station_validations:
3
+ or: "or"
4
+ errors:
5
+ unexpected_key: "is not allowed"
6
+ array?: "must be an array"
7
+ empty?: "must be empty"
8
+ excludes?: "must not include %{value}"
9
+ excluded_from?:
10
+ arg:
11
+ default: "must not be one of: %{list}"
12
+ range: "must not be one of: %{list_left} - %{list_right}"
13
+ exclusion?: "must not be one of: %{list}"
14
+ eql?: "must be equal to %{left}"
15
+ not_eql?: "must not be equal to %{left}"
16
+ filled?: "must be filled"
17
+ format?: "is in invalid format"
18
+ number?: "must be a number"
19
+ odd?: "must be odd"
20
+ even?: "must be even"
21
+ gt?: "must be greater than %{num}"
22
+ gteq?: "must be greater than or equal to %{num}"
23
+ hash?: "must be a hash"
24
+ included_in?:
25
+ arg:
26
+ default: "must be one of: %{list}"
27
+ range: "must be one of: %{list_left} - %{list_right}"
28
+ inclusion?: "must be one of: %{list}"
29
+ includes?: "must include %{value}"
30
+ bool?: "must be boolean"
31
+ true?: "must be true"
32
+ false?: "must be false"
33
+ int?: "must be an integer"
34
+ float?: "must be a float"
35
+ decimal?: "must be a decimal"
36
+ date?: "must be a date"
37
+ date_time?: "must be a date time"
38
+ time?: "must be a time"
39
+ key?: "is missing"
40
+ attr?: "is missing"
41
+ lt?: "must be less than %{num}"
42
+ lteq?: "must be less than or equal to %{num}"
43
+ max_size?: "size cannot be greater than %{num}"
44
+ max_bytesize?: "bytesize cannot be greater than %{num}"
45
+ min_size?: "size cannot be less than %{num}"
46
+ min_bytesize?: "bytesize cannot be less than %{num}"
47
+ nil?: "cannot be defined"
48
+ str?: "must be a string"
49
+ type?: "must be %{type}"
50
+ respond_to?: "must respond to %{method}"
51
+ size?:
52
+ arg:
53
+ default: "size must be %{size}"
54
+ range: "size must be within %{size_left} - %{size_right}"
55
+ value:
56
+ string:
57
+ arg:
58
+ default: "length must be %{size}"
59
+ range: "length must be within %{size_left} - %{size_right}"
60
+ bytesize?:
61
+ arg:
62
+ default: "must be %{size} bytes long"
63
+ range: "must be within %{size_left} - %{size_right} bytes long"
64
+ uri?: "is not a valid URI"
65
+ uuid_v1?: "is not a valid UUID"
66
+ uuid_v2?: "is not a valid UUID"
67
+ uuid_v3?: "is not a valid UUID"
68
+ uuid_v4?: "is not a valid UUID"
69
+ uuid_v5?: "is not a valid UUID"
70
+ uuid_v6?: "is not a valid UUID"
71
+ uuid_v7?: "is not a valid UUID"
72
+ uuid_v8?: "is not a valid UUID"
73
+ not:
74
+ empty?: "cannot be empty"
75
+
76
+ sp:
77
+ next_station_validations:
78
+ or: "o"
79
+ errors:
80
+ unexpected_key: "no está permitido"
81
+ array?: "debe ser un arreglo"
82
+ empty?: "debe estar vacío"
83
+ excludes?: "no debe incluir %{value}"
84
+ excluded_from?:
85
+ arg:
86
+ default: "no debe ser uno de: %{list}"
87
+ range: "no debe ser uno de: %{list_left} - %{list_right}"
88
+ exclusion?: "no debe ser uno de: %{list}"
89
+ eql?: "debe ser igual a %{left}"
90
+ not_eql?: "no debe ser igual a %{left}"
91
+ filled?: "debe estar lleno"
92
+ format?: "tiene un formato inválido"
93
+ number?: "debe ser un número"
94
+ odd?: "debe ser impar"
95
+ even?: "debe ser par"
96
+ gt?: "debe ser mayor que %{num}"
97
+ gteq?: "debe ser mayor o igual que %{num}"
98
+ hash?: "debe ser un hash"
99
+ included_in?:
100
+ arg:
101
+ default: "debe ser uno de: %{list}"
102
+ range: "debe ser uno de: %{list_left} - %{list_right}"
103
+ inclusion?: "debe ser uno de: %{list}"
104
+ includes?: "debe incluir %{value}"
105
+ bool?: "debe ser booleano"
106
+ true?: "debe ser verdadero"
107
+ false?: "debe ser falso"
108
+ int?: "debe ser un número entero"
109
+ float?: "debe ser un número flotante"
110
+ decimal?: "debe ser un número decimal"
111
+ date?: "debe ser una fecha"
112
+ date_time?: "debe ser una fecha y hora"
113
+ time?: "debe ser una hora"
114
+ key?: "está ausente"
115
+ attr?: "está ausente"
116
+ lt?: "debe ser menor que %{num}"
117
+ lteq?: "debe ser menor o igual que %{num}"
118
+ max_size?: "el tamaño no puede ser mayor que %{num}"
119
+ max_bytesize?: "el tamaño en bytes no puede ser mayor que %{num}"
120
+ min_size?: "el tamaño no puede ser menor que %{num}"
121
+ min_bytesize?: "el tamaño en bytes no puede ser menor que %{num}"
122
+ nil?: "no puede estar definido"
123
+ str?: "debe ser una cadena de texto"
124
+ type?: "debe ser %{type}"
125
+ respond_to?: "debe responder al método %{method}"
126
+ size?:
127
+ arg:
128
+ default: "el tamaño debe ser %{size}"
129
+ range: "el tamaño debe estar entre %{size_left} y %{size_right}"
130
+ value:
131
+ string:
132
+ arg:
133
+ default: "la longitud debe ser %{size}"
134
+ range: "la longitud debe estar entre %{size_left} y %{size_right}"
135
+ bytesize?:
136
+ arg:
137
+ default: "debe tener %{size} bytes de longitud"
138
+ range: "debe tener entre %{size_left} y %{size_right} bytes de longitud"
139
+ uri?: "no es un URI válido"
140
+ uuid_v1?: "no es un UUID válido"
141
+ uuid_v2?: "no es un UUID válido"
142
+ uuid_v3?: "no es un UUID válido"
143
+ uuid_v4?: "no es un UUID válido"
144
+ uuid_v5?: "no es un UUID válido"
145
+ uuid_v6?: "no es un UUID válido"
146
+ uuid_v7?: "no es un UUID válido"
147
+ uuid_v8?: "no es un UUID válido"
148
+ not:
149
+ empty?: "no puede estar vacío"
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-configurable'
4
+ require 'dry-monitor'
5
+ require 'logger'
6
+ require_relative 'environment'
7
+ require_relative 'logging/formatters/json'
8
+ require_relative 'logging/formatters/console'
9
+
10
+ module NextStation
11
+ extend Dry::Configurable
12
+
13
+ # Define the environment
14
+ setting :environment, default: Environment.new, constructor: ->(v) {
15
+ if v.is_a?(String)
16
+ env = Environment.new
17
+ env.current = v
18
+ env
19
+ else
20
+ v
21
+ end
22
+ }
23
+
24
+ # Define the monitor
25
+ setting :monitor, default: (
26
+ monitor = Dry::Monitor::Notifications.new(:next_station)
27
+ monitor.register_event('operation.start')
28
+ monitor.register_event('operation.stop')
29
+ monitor.register_event('step.start')
30
+ monitor.register_event('step.stop')
31
+ monitor.register_event('step.retry')
32
+ monitor.register_event('log.custom')
33
+ monitor
34
+ )
35
+
36
+ # Define the default logger (STDOUT)
37
+ setting :logger, default: Logger.new($stdout)
38
+
39
+ # Enable/disable default logging subscribers
40
+ setting :logging_enabled, default: true
41
+
42
+ # Default logging level (:info, :debug)
43
+ setting :logging_level, default: :info
44
+ end
45
+
46
+ require_relative 'logging'
47
+
48
+ # Automatically setup logging if enabled
49
+ NextStation::Logging.setup! if NextStation.config.logging_enabled
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextStation
4
+ # Detects the current environment (e.g., development, production)
5
+ # based on a configurable set of environment variables.
6
+ class Environment
7
+ attr_accessor :env_vars, :production_names, :development_names
8
+ attr_writer :current
9
+
10
+ def initialize
11
+ # A list of common environment variables to check for the environment name.
12
+ @env_vars = %w[RAILS_ENV RACK_ENV APP_ENV RUBY_ENV]
13
+
14
+ # Names that are considered to be a "production" environment.
15
+ @production_names = %w[production prod prd]
16
+
17
+ # Names that are considered to be a "development" environment.
18
+ @development_names = %w[development dev]
19
+
20
+ # Manually set environment name.
21
+ @current = nil
22
+ end
23
+
24
+ # Returns the current environment name. Defaults to 'development' if none is found.
25
+ # @return [String]
26
+ def current
27
+ @current || env_vars.map { |var| ENV[var] }.compact.first || 'development'
28
+ end
29
+
30
+ # Checks if the current environment is production.
31
+ # @return [Boolean]
32
+ def production?
33
+ production_names.include?(current)
34
+ end
35
+
36
+ # Checks if the current environment is development.
37
+ # @return [Boolean]
38
+ def development?
39
+ development_names.include?(current)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextStation
4
+ class Errors
5
+ def self.inherited(subclass)
6
+ super
7
+ subclass.extend(SharedErrorsDSL)
8
+ end
9
+
10
+ module SharedErrorsDSL
11
+ def error_type(type, &block)
12
+ @dsl ||= NextStation::Operation::ErrorsDSL.new
13
+ @dsl.error_type(type, &block)
14
+ end
15
+
16
+ def definitions
17
+ @dsl&.definitions || {}
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module NextStation
6
+ module Logging
7
+ module Formatter
8
+ class Console < Logger::Formatter
9
+ # ANSI color codes
10
+ SEVERITY_COLORS = {
11
+ "DEBUG" => "\e[36m", # cyan
12
+ "INFO" => "\e[32m", # green
13
+ "WARN" => "\e[33m", # yellow
14
+ "ERROR" => "\e[31m", # red
15
+ "FATAL" => "\e[35m" # magenta
16
+ }.freeze
17
+
18
+ OPERATION_COLOR = "\e[34m" # blue
19
+ STEP_COLOR = "\e[90m" # gray
20
+ RESET_COLOR = "\e[0m"
21
+
22
+ def call(severity, datetime, _progname, msg)
23
+ msg = msg.is_a?(Hash) ? msg : { message: msg.to_s }
24
+
25
+ operation = msg[:operation]
26
+ step_name = msg[:step_name] ? "/#{msg[:step_name]}" : ""
27
+ payload = msg[:payload] unless msg[:payload].to_h.empty?
28
+
29
+ sev = "#{SEVERITY_COLORS[severity]}#{severity[0]}#{RESET_COLOR}"
30
+ op = "#{OPERATION_COLOR}#{operation}#{RESET_COLOR}"
31
+ step = step_name.empty? ? "" : "#{STEP_COLOR}#{step_name}#{RESET_COLOR}"
32
+
33
+ "[#{sev}][#{datetime.strftime('%Y-%m-%d %H:%M:%S')}][#{op}#{step}] -- #{msg[:message]} #{payload}\n"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'json'
5
+
6
+ module NextStation
7
+ module Logging
8
+ module Formatter
9
+ # A custom logger formatter that outputs log entries as JSON objects.
10
+ #
11
+ # This formatter is designed to work with the standard `Logger` class.
12
+ # It structures log messages into a JSON format that includes severity, timestamp,
13
+ # process ID, and structured data from the operation. It also automatically
14
+ # includes OpenTelemetry trace and span IDs if the `opentelemetry-sdk` is present.
15
+ class Json < Logger::Formatter
16
+ # Avoid repeated defined? calls in a hot path
17
+ OTEL_AVAILABLE = defined?(::OpenTelemetry::Trace)
18
+
19
+ # Formats the log entry into a JSON string.
20
+ #
21
+ # @param severity [String] The log severity (e.g., 'INFO', 'WARN').
22
+ # @param time [Time] The timestamp of the log event.
23
+ # @param _progname [String] The program name (unused).
24
+ # @param msg [String, Hash] The log message. If a Hash, it is treated as
25
+ # structured data with keys like `:message`, `:payload`, and `:operation`.
26
+ # If a String, it becomes the value of the `:message` key.
27
+ # @return [String] The formatted log entry as a JSON string, terminated
28
+ # with a newline character.
29
+ def call(severity, time, _progname, msg)
30
+ data = msg.is_a?(Hash) ? msg : { message: msg.to_s }
31
+
32
+ log_entry = {
33
+ level: severity,
34
+ time: time.utc.strftime('%Y-%m-%dT%H:%M:%S.%6N'),
35
+ pid: Process.pid,
36
+ origin: build_origin(data),
37
+ message: data[:message]
38
+ }
39
+
40
+ add_payload_to_log_entry(log_entry, data) if data[:payload]
41
+
42
+ add_otel_context(log_entry) if OTEL_AVAILABLE
43
+
44
+ # Compact the hash to remove nil values and ensure a newline
45
+ JSON.generate(log_entry.compact) << "\n"
46
+ end
47
+
48
+ private
49
+
50
+ # Adds the payload to the log entry if it is not empty.
51
+ def add_payload_to_log_entry(log_entry, data)
52
+ return unless data[:payload]
53
+
54
+ log_entry[:payload] = data[:payload] unless data[:payload].empty?
55
+ end
56
+
57
+ # Constructs the origin hash, returning nil if no origin data is present.
58
+ # This prevents an empty "origin": {} from appearing in the logs.
59
+ def build_origin(data)
60
+ origin = {}
61
+ origin[:operation] = data[:operation] if data[:operation]
62
+ origin[:event] = data[:event_kind] if data[:event_kind]
63
+ origin[:step_name] = data[:step_name] if data[:step_name]
64
+ origin[:step_attempt] = data[:step_attempt] if data[:step_attempt]
65
+
66
+ origin.empty? ? nil : origin
67
+ end
68
+
69
+ # Adds OpenTelemetry trace and span IDs to the log entry if available.
70
+ def add_otel_context(log_entry)
71
+ context = ::OpenTelemetry::Trace.current_span.context
72
+ if context.valid?
73
+ log_entry[:trace_id] = context.hex_trace_id
74
+ log_entry[:span_id] = context.hex_span_id
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextStation
4
+ module Logging
5
+ module Subscribers
6
+ # @api private
7
+ class Base
8
+ # Map levels to their numeric priority for comparison.
9
+ LEVELS = {
10
+ debug: 0,
11
+ info: 1,
12
+ warn: 2,
13
+ error: 3,
14
+ fatal: 4,
15
+ unknown: 5
16
+ }.freeze
17
+
18
+ # Subscribes a new instance to the monitor.
19
+ # @param monitor [Dry::Monitor::Notifications]
20
+ def self.subscribe(monitor)
21
+ new.subscribe(monitor)
22
+ end
23
+
24
+ # Subscribes to the event(s) in the monitor.
25
+ # @param monitor [Dry::Monitor::Notifications]
26
+ def subscribe(monitor)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ protected
31
+
32
+ # Default log level if none is provided in the event.
33
+ # @return [Symbol]
34
+ def default_level
35
+ :info
36
+ end
37
+
38
+ # Logs the event data to the configured logger.
39
+ # @param event [Dry::Monitor::Event]
40
+ # @param level [Symbol, nil] Explicit level, or derived from event/default.
41
+ # @param extra_data [Hash] Additional data to merge into the log entry.
42
+ def log_event(event, level: nil, extra_data: {})
43
+ # Add this check to respect `config.logging_enabled = false`
44
+ return unless NextStation.config.logging_enabled
45
+
46
+ event_data = event.to_h
47
+ log_level = level || event_data.delete(:level) || default_level
48
+
49
+ # Filter by logging_level
50
+ return unless level_sufficient?(log_level)
51
+
52
+ # Merge data while preserving the original event data
53
+ payload = event_data.merge(extra_data)
54
+
55
+ # We pass the whole hash. The Formatter will pick what it needs.
56
+ NextStation.config.logger.send(log_level, payload)
57
+ end
58
+
59
+ private
60
+
61
+ # @param log_level [Symbol] The level of the current log event.
62
+ # @return [Boolean] True if the log level is equal or higher than configured.
63
+ def level_sufficient?(log_level)
64
+ configured_level = NextStation.config.logging_level
65
+ LEVELS.fetch(log_level, 1) >= LEVELS.fetch(configured_level, 1)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module NextStation
6
+ module Logging
7
+ module Subscribers
8
+ # Subscriber for custom log events manually triggered.
9
+ # @api private
10
+ class Custom < Base
11
+ # @param monitor [Dry::Monitor::Notifications]
12
+ def subscribe(monitor)
13
+ monitor.subscribe('log.custom') { |event| on_custom(event) }
14
+ end
15
+
16
+ # @param event [Dry::Monitor::Event]
17
+ def on_custom(event)
18
+ log_event(event, extra_data: {
19
+ event_kind: 'log.custom'
20
+ })
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module NextStation
6
+ module Logging
7
+ module Subscribers
8
+ # Subscriber for operation lifecycle events.
9
+ # @api private
10
+ class Operation < Base
11
+ # @param monitor [Dry::Monitor::Notifications]
12
+ def subscribe(monitor)
13
+ monitor.subscribe('operation.start') { |event| on_start(event) }
14
+ monitor.subscribe('operation.stop') { |event| on_stop(event) }
15
+ end
16
+
17
+ # @param event [Dry::Monitor::Event]
18
+ def on_start(event)
19
+ log_event(event, extra_data: {
20
+ message: "Started operation: #{event[:operation]}",
21
+ event_kind: 'operation.start'
22
+ })
23
+ end
24
+
25
+ # @param event [Dry::Monitor::Event]
26
+ def on_stop(event)
27
+ result_status = event[:result].success? ? 'success' : 'failure'
28
+
29
+ log_event(event, extra_data: {
30
+ message: "completed operation: #{event[:operation]} with #{result_status}",
31
+ event_kind: 'operation.stop',
32
+ payload: {
33
+ duration: event[:duration],
34
+ result: result_status
35
+ }
36
+ })
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module NextStation
6
+ module Logging
7
+ module Subscribers
8
+ # Subscriber for step lifecycle events.
9
+ # @api private
10
+ class Step < Base
11
+ # @param monitor [Dry::Monitor::Notifications]
12
+ def subscribe(monitor)
13
+ monitor.subscribe('step.start') { |event| on_start(event) }
14
+ monitor.subscribe('step.stop') { |event| on_stop(event) }
15
+ monitor.subscribe('step.retry') { |event| on_retry(event) }
16
+ end
17
+
18
+ # @param event [Dry::Monitor::Event]
19
+ def on_start(event)
20
+ log_event(event, level: :debug, extra_data: {
21
+ message: "Started step: #{event[:step]} in #{event[:operation]}",
22
+ event_kind: 'step.start',
23
+ step_name: event[:step],
24
+ operation: event[:operation]
25
+ })
26
+ end
27
+
28
+ # @param event [Dry::Monitor::Event]
29
+ def on_stop(event)
30
+ log_event(event, level: :debug, extra_data: {
31
+ message: "Completed step: #{event[:step]} in #{event[:operation]}",
32
+ event_kind: 'step.stop',
33
+ step_name: event[:step],
34
+ operation: event[:operation],
35
+ payload: {
36
+ duration: event[:duration]
37
+ }
38
+ })
39
+ end
40
+
41
+ # @param event [Dry::Monitor::Event]
42
+ def on_retry(event)
43
+ log_event(event, level: :warn, extra_data: {
44
+ message: "Retrying step: #{event[:step]} (attempt #{event[:attempt]})",
45
+ event_kind: 'step.retry',
46
+ step_name: event[:step],
47
+ operation: event[:operation],
48
+ step_attempt: event[:attempt]
49
+ })
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end