console_kit 1.4.0 → 1.5.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/console_kit/configuration.rb +15 -21
  3. data/lib/console_kit/configuration_validator.rb +179 -0
  4. data/lib/console_kit/connections/base_connection_handler.rb +139 -25
  5. data/lib/console_kit/connections/connection_manager.rb +42 -8
  6. data/lib/console_kit/connections/dashboard.rb +3 -11
  7. data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
  8. data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
  9. data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
  10. data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
  11. data/lib/console_kit/connections/mongoid_support.rb +56 -0
  12. data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
  13. data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
  14. data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
  15. data/lib/console_kit/connections/sql_strategy.rb +248 -0
  16. data/lib/console_kit/connections/table_renderer.rb +30 -20
  17. data/lib/console_kit/console_helpers.rb +17 -31
  18. data/lib/console_kit/diagnostics.rb +143 -0
  19. data/lib/console_kit/errors.rb +93 -0
  20. data/lib/console_kit/instrumentation.rb +57 -0
  21. data/lib/console_kit/output.rb +5 -14
  22. data/lib/console_kit/prompt.rb +17 -29
  23. data/lib/console_kit/railtie.rb +2 -3
  24. data/lib/console_kit/setup.rb +66 -13
  25. data/lib/console_kit/setup_ui.rb +1 -4
  26. data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
  27. data/lib/console_kit/tenant_configurator.rb +40 -78
  28. data/lib/console_kit/tenant_orchestrator.rb +31 -27
  29. data/lib/console_kit/tenant_plan.rb +68 -0
  30. data/lib/console_kit/tenant_rollback.rb +77 -0
  31. data/lib/console_kit/tenant_selector.rb +3 -6
  32. data/lib/console_kit/tenant_state.rb +73 -0
  33. data/lib/console_kit/tenant_switch.rb +124 -0
  34. data/lib/console_kit/version.rb +1 -1
  35. data/lib/console_kit.rb +51 -24
  36. data/lib/generators/console_kit/install_generator.rb +1 -12
  37. data/lib/generators/console_kit/templates/console_kit.rb +4 -0
  38. metadata +15 -4
  39. data/lib/console_kit/connections/table_formatter.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aae8ac063c1d5f4ccdb652e8b450e629a04512ee5aa52e8cec26dc1ad6d93a3e
4
- data.tar.gz: 2c3a1726016122c152ef0c7a601abdd285748aab388a53b733cd4c3c5e2a146d
3
+ metadata.gz: 3f282d9d1153742f170546ec1add76c8028ff52e795b1ab73592268108185c1b
4
+ data.tar.gz: be533a3a7e11826dac60954c699ce82f1ca2f0f6b5c801a53c0a658a2f16a584
5
5
  SHA512:
6
- metadata.gz: d1c7a12a9c259b89ec988f606510412617856124e7d36428df2d7a9a35c74b4dadb364897111944efd2cc4de26c3e99fd15bab7b83f27804b1c7dd4c8f258d0d
7
- data.tar.gz: 824201bd35d0c9a8029c146cf4cff8170b5971d2768dafc96a0e26efb7545f0e072346781a490dd08559ede3d2b8e0781137e6aa7091b16e9584f3c3a0de0cbe
6
+ metadata.gz: c61113f26d1b1418e70bc3be9f89e5d85b64af56376e2cc08ed1f9abb07f3acb7f4811a30c01d783dd6783e679df6adfb64cb5f537a1f1e86c8f6569e28a3024
7
+ data.tar.gz: c3e5b3f5b405a68440f0cda638ab26485bcf3b54f0b7bc742a4a15a37bec31708abb97316b6fa2913f31dfd24a5269aa2a58b91d37939ede4562121ed12f5e56
@@ -1,39 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'configuration_validator'
4
+
3
5
  module ConsoleKit
4
- # Stores ConsoleKit configurations such as tenant map and context behavior
5
6
  class Configuration
6
- # Value object for storing configuration settings
7
- Settings = Struct.new(:pretty_output, :tenants, :context_class, :sql_base_class, :show_dashboard)
7
+ attr_accessor :pretty_output, :tenants, :sql_base_class, :show_dashboard
8
+ attr_writer :context_class
8
9
 
9
10
  def initialize
10
- @settings = Settings.new(true, nil, nil, 'ApplicationRecord', false)
11
- end
12
-
13
- %i[pretty_output tenants context_class sql_base_class show_dashboard].each do |attr|
14
- define_method(attr) { @settings.send(attr) }
15
- define_method("#{attr}=") { |val| @settings.send("#{attr}=", val) }
11
+ @pretty_output = true
12
+ @context_class = nil
13
+ @tenants = nil
14
+ @sql_base_class = 'ApplicationRecord'
15
+ @show_dashboard = false
16
16
  end
17
17
 
18
18
  def context_class
19
- val = @settings.context_class
20
- case val
21
- when String, Symbol then resolve_context_class(val)
22
- else val
19
+ case @context_class
20
+ when String, Symbol then resolve_context_class(@context_class)
21
+ else @context_class
23
22
  end
24
23
  end
25
24
 
26
- def validate
27
- validate!
28
- true
29
- rescue Error
30
- false
31
- end
32
-
33
25
  def validate!
34
26
  raise Error, 'ConsoleKit: `tenants` is not configured.' if tenants.blank?
35
27
  raise Error, 'ConsoleKit: `tenants` must be a Hash.' unless tenants.is_a?(Hash)
36
- raise Error, 'ConsoleKit: `context_class` is not configured.' if @settings.context_class.blank?
28
+ raise Error, 'ConsoleKit: `context_class` is not configured.' if @context_class.blank?
29
+
30
+ ConfigurationValidator.new(self).validate!
37
31
  end
38
32
 
39
33
  private
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'output'
5
+ require_relative 'connections/diagnostic_helpers'
6
+
7
+ module ConsoleKit
8
+ class TenantEntry
9
+ EXTRA_CONSTANTS_KEYS = [:environment].freeze
10
+
11
+ attr_reader :key, :errors, :warnings
12
+
13
+ def initialize(key, entry)
14
+ @key = key
15
+ @entry = entry
16
+ @errors = []
17
+ @warnings = []
18
+ end
19
+
20
+ def constants
21
+ constants = @entry[:constants] if @entry.is_a?(Hash)
22
+ constants if constants.is_a?(Hash)
23
+ end
24
+
25
+ def validate
26
+ check_identifier
27
+ return @errors << "ConsoleKit: tenant #{@key.inspect} configuration must be a Hash, got #{@entry.class}." \
28
+ unless @entry.is_a?(Hash)
29
+
30
+ validate_constants
31
+ end
32
+
33
+ private
34
+
35
+ def validate_constants
36
+ declared = @entry[:constants]
37
+ tenant = @key.inspect
38
+ return @errors << "ConsoleKit: tenant #{tenant} is missing a `:constants` Hash." if declared.nil?
39
+ unless declared.is_a?(Hash)
40
+ return @errors << "ConsoleKit: tenant #{tenant} `:constants` must be a Hash, got #{declared.class}."
41
+ end
42
+
43
+ check_required_keys
44
+ check_constants_values
45
+ warn_unknown_keys
46
+ end
47
+
48
+ def check_identifier
49
+ return if (@key.is_a?(Symbol) || @key.is_a?(String)) && @key.to_s.strip.present?
50
+
51
+ @errors << "ConsoleKit: tenant identifier #{@key.inspect} must be a non-blank Symbol or String."
52
+ end
53
+
54
+ def check_required_keys
55
+ required = TenantPlan::REQUIRED_KEYS
56
+ missing = required - constants.keys
57
+ return if missing.empty?
58
+
59
+ @errors << "ConsoleKit: tenant #{@key.inspect} constants missing required keys: #{missing.join(', ')} " \
60
+ "(expected: #{required.join(', ')})."
61
+ end
62
+
63
+ def check_constants_values
64
+ values = constants
65
+ Connections::BaseConnectionHandler.registry.each do |handler_class|
66
+ field = handler_class.constants_key
67
+ next unless values.key?(field)
68
+
69
+ check_backend_value(field, values[field], handler_class)
70
+ end
71
+ end
72
+
73
+ def check_backend_value(field, value, handler_class)
74
+ reason = handler_class.target_error(value)
75
+ return unless reason
76
+
77
+ scrubbed = Connections::DiagnosticHelpers.scrub(value.inspect)
78
+ @errors << "ConsoleKit: tenant #{@key.inspect} #{field} #{scrubbed} is invalid: #{reason}"
79
+ end
80
+
81
+ def warn_unknown_keys
82
+ recognised = TenantConfigurator.context_mapping.values + EXTRA_CONSTANTS_KEYS
83
+ extra = constants.keys - recognised
84
+ return if extra.empty?
85
+
86
+ @warnings << "tenant #{@key.inspect} constants has unrecognised keys: #{extra.map(&:inspect).join(', ')} " \
87
+ "(recognised: #{recognised.map(&:inspect).join(', ')}). Check for typos."
88
+ end
89
+ end
90
+
91
+ class BackendCoverage
92
+ WARNING = 'tenant %<tenant>p does not name %<omitted>s, which other tenants do. Switching to it RESETS those ' \
93
+ 'backends to their defaults rather than leaving them on the tenant before it.'
94
+
95
+ def initialize(entries)
96
+ @entries = entries.select(&:constants)
97
+ end
98
+
99
+ def warnings
100
+ @entries.filter_map do |entry|
101
+ omitted = named - entry.constants.keys
102
+ next if omitted.empty?
103
+
104
+ format(WARNING, tenant: entry.key, omitted: omitted.map(&:inspect).join(', '))
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def named
111
+ @named ||= @entries.flat_map { |entry| entry.constants.keys }
112
+ .intersection(Connections::BaseConnectionHandler.registry.map(&:constants_key))
113
+ end
114
+ end
115
+
116
+ class ConfigurationValidator
117
+ def initialize(configuration)
118
+ @configuration = configuration
119
+ @errors = []
120
+ @warnings = []
121
+ end
122
+
123
+ def validate!
124
+ entries.each do |entry|
125
+ entry.validate
126
+ errors.concat(entry.errors)
127
+ warnings.concat(entry.warnings)
128
+ end
129
+ check_across_tenants
130
+ raise ConfigurationError, errors.join("\n") if errors.any?
131
+ end
132
+
133
+ private
134
+
135
+ attr_reader :configuration, :errors, :warnings
136
+
137
+ def entries = @entries ||= configuration.tenants.map { |key, entry| TenantEntry.new(key, entry) }
138
+
139
+ def check_across_tenants
140
+ check_duplicate_identifiers
141
+ warnings.concat(BackendCoverage.new(entries).warnings)
142
+ check_context_writers
143
+ warnings.each { |message| Output.print_warning("ConsoleKit: #{message}") }
144
+ end
145
+
146
+ def check_duplicate_identifiers
147
+ configuration.tenants.keys.group_by { |key| key.to_s.downcase }.each_value do |keys|
148
+ next if keys.size < 2
149
+
150
+ errors << "ConsoleKit: tenant identifiers #{keys.map(&:inspect).join(', ')} are duplicates once " \
151
+ 'normalized (differ only by type or case). Tenant lookup uses an exact match, so only one ' \
152
+ 'of these is ever reachable.'
153
+ end
154
+ end
155
+
156
+ def check_context_writers
157
+ klass = resolve_context_class
158
+ missing = klass && configured_attributes.reject { |attr| writer?(klass, :"#{attr}=") }
159
+ return if missing.blank?
160
+
161
+ warnings << "context_class #{klass} has no writer for: #{missing.join(', ')}. ConsoleKit will silently " \
162
+ 'never configure that backend during a tenant switch.'
163
+ end
164
+
165
+ def configured_attributes
166
+ named = entries.filter_map { |entry| entry.constants&.keys }.flatten.uniq
167
+ TenantConfigurator.context_mapping.select { |_attr, field| named.include?(field) }.keys
168
+ end
169
+
170
+ def writer?(klass, writer) = klass.respond_to?(writer) || klass.method_defined?(writer)
171
+
172
+ def resolve_context_class
173
+ configuration.context_class
174
+ rescue Error => e
175
+ errors << e.message
176
+ nil
177
+ end
178
+ end
179
+ end
@@ -1,50 +1,161 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/class/subclasses'
4
- require 'active_support/core_ext/string/inflections'
5
- require 'active_support/core_ext/string/filters'
6
3
  require_relative 'diagnostic_helpers'
4
+ require_relative '../errors'
5
+ require_relative '../output'
6
+ require_relative '../instrumentation'
7
+ require_relative '../diagnostics'
7
8
 
8
9
  module ConsoleKit
9
10
  module Connections
10
- # Parent class for connection handlers
11
+ module HandlerRegistry
12
+ COLLISION_WARNING = 'ConsoleKit: %<previous>s and %<current>s both claim the backend key %<key>p. Only ' \
13
+ '%<current>s will be switched, verified and rolled back.'
14
+ DUPLICATE_ATTRIBUTE = 'ConsoleKit: %<current>s claims the context attribute %<attribute>p, which %<previous>s ' \
15
+ 'already claims for the %<key>p backend. One attribute is one slot on the context ' \
16
+ 'object, so each backend would write the other values there and the context would ' \
17
+ 'disagree with the live connection. Give %<current>s a context attribute of its own.'
18
+
19
+ class << self
20
+ def all = entries.values.freeze
21
+
22
+ def add(handler_class)
23
+ key = handler_class.backend_key
24
+ reject_duplicate_attribute(handler_class, key)
25
+ drop_other_keys(handler_class, key)
26
+ report_collision(entries[key], handler_class)
27
+ entries[key] = handler_class
28
+ end
29
+
30
+ def remove(handler_class)
31
+ key = handler_class.backend_key
32
+ entries.delete(key) if entries[key].equal?(handler_class)
33
+ end
34
+
35
+ private
36
+
37
+ def entries = @entries ||= {}
38
+
39
+ def drop_other_keys(handler_class, key)
40
+ entries.delete_if { |existing, klass| existing != key && klass.equal?(handler_class) }
41
+ end
42
+
43
+ def reject_duplicate_attribute(handler_class, key)
44
+ attribute = handler_class.context_attribute
45
+ previous_key, previous_class =
46
+ entries.find { |other, klass| other != key && klass.context_attribute == attribute }
47
+ return if !previous_class || same_declaration?(previous_class, handler_class)
48
+
49
+ raise ConfigurationError, format(DUPLICATE_ATTRIBUTE, current: handler_class, previous: previous_class,
50
+ attribute: attribute, key: previous_key)
51
+ end
52
+
53
+ def same_declaration?(previous, current)
54
+ previous_name = previous.name
55
+ previous.equal?(current) || (previous_name && previous_name == current.name)
56
+ end
57
+
58
+ def report_collision(previous, current)
59
+ previous_name = previous&.name
60
+ current_name = current.name
61
+ return unless previous_name && current_name && previous_name != current_name
62
+
63
+ Instrumentation.increment('console_kit.handler_collision')
64
+ Output.print_warning(format(COLLISION_WARNING, previous: previous, current: current,
65
+ key: current.backend_key))
66
+ end
67
+ end
68
+ end
69
+
11
70
  class BaseConnectionHandler
12
71
  include DiagnosticHelpers
13
72
 
14
73
  class << self
15
- def registry = descendants
74
+ def backend_key = declaration[:backend_key] || superclass.try(:backend_key)
75
+ def display_name = declaration[:display_name] || superclass.try(:display_name)
76
+ def context_attribute = declaration[:context_attribute] || superclass.try(:context_attribute)
77
+ def constants_key = declaration[:constants_key] || superclass.try(:constants_key)
78
+ def detail_label = declaration[:detail_label] || superclass.try(:detail_label)
79
+
80
+ def backend(key, display_name:, context_attribute:, constants_key:, detail_label:)
81
+ @declaration = { backend_key: key, display_name: display_name, context_attribute: context_attribute,
82
+ constants_key: constants_key, detail_label: detail_label }
83
+ HandlerRegistry.add(self)
84
+ end
85
+
86
+ def declaration = @declaration ||= {}
87
+
88
+ def registry = HandlerRegistry.all
89
+ def unregister(handler_class) = HandlerRegistry.remove(handler_class)
90
+
91
+ def target_error(_value) = nil
92
+
93
+ def identifier_error(value)
94
+ return if value.nil? || ((value.is_a?(String) || value.is_a?(Symbol)) && value.to_s.strip.present?)
95
+
96
+ 'expected a non-blank String or Symbol'
97
+ end
16
98
  end
17
99
 
18
100
  attr_reader :context
19
101
 
20
102
  def initialize(context) = @context = context
21
- def connect = raise NotImplementedError, "#{self.class} must implement #connect"
103
+
104
+ def backend_key = self.class.backend_key
105
+ def display_name = self.class.display_name
106
+
107
+ def target
108
+ attr_name = self.class.context_attribute
109
+ attr_name && context_attribute(attr_name).presence
110
+ end
111
+
22
112
  def available? = raise NotImplementedError, "#{self.class} must implement #available?"
23
- def diagnostics = raise NotImplementedError, "#{self.class} must implement #diagnostics"
113
+ def snapshot = raise NotImplementedError, "#{self.class} must implement #snapshot"
114
+ def connect!(_target) = raise NotImplementedError, "#{self.class} must implement #connect!"
115
+ def restore(_snapshot) = raise NotImplementedError, "#{self.class} must implement #restore"
24
116
 
25
- def safe_diagnostics(timeout: 2)
26
- handler_name = self.class.name.demodulize.delete_suffix('ConnectionHandler')
27
- thread, result_wrapper = spawn_diagnostic_thread(handler_name)
117
+ def unavailable_reason = nil
28
118
 
29
- if thread.join(timeout)
30
- result_wrapper[:value] || error_diagnostics(handler_name, StandardError.new('Unknown error'))
31
- else
32
- timeout_diagnostics(handler_name, timeout)
33
- end
119
+ def prepare(_target) = nil
120
+
121
+ def verify!(_target) = nil
122
+
123
+ def diagnostics(level: :basic)
124
+ return unavailable_diagnostics unless available?
125
+
126
+ diagnostics_at(level)
127
+ rescue StandardError => e
128
+ raise e if ConsoleKit.programming_error?(e)
129
+
130
+ error_diagnostics(display_name, e)
34
131
  end
35
132
 
36
- private
133
+ def diagnostic_identity = nil
37
134
 
38
- def spawn_diagnostic_thread(handler_name)
39
- wrapper = { value: nil }
40
- thread = Thread.new { wrapper[:value] = run_diagnostics_safely(handler_name) { diagnostics } }
41
- [thread, wrapper]
135
+ def safe_diagnostics(timeout: Diagnostics::DEFAULT_TIMEOUT, level: :basic)
136
+ Diagnostics::Runner.call(self, timeout: timeout, level: level)
42
137
  end
43
138
 
44
- def run_diagnostics_safely(name)
45
- yield
46
- rescue StandardError => e
47
- error_diagnostics(name, e)
139
+ def verification_error(expected, actual)
140
+ ConnectionVerificationError.new(
141
+ nil, backend: display_name, tenant: nil, expected: expected, actual: actual
142
+ )
143
+ end
144
+
145
+ private
146
+
147
+ def diagnostics_at(level) = level == :full ? full_diagnostics : basic_diagnostics
148
+
149
+ def basic_diagnostics = raise NotImplementedError, "#{self.class} must implement #basic_diagnostics"
150
+ def full_diagnostics = raise NotImplementedError, "#{self.class} must implement #full_diagnostics"
151
+
152
+ def validate_target!(target)
153
+ handler_class = self.class
154
+ reason = handler_class.target_error(target)
155
+ return unless reason
156
+
157
+ raise ConfigurationError,
158
+ "ConsoleKit: #{handler_class.constants_key} #{scrub(target.inspect)} is invalid: #{reason}."
48
159
  end
49
160
 
50
161
  def measure_latency
@@ -54,7 +165,10 @@ module ConsoleKit
54
165
  end
55
166
 
56
167
  def context_attribute(name) = @context.try(name)
57
- def unavailable_diagnostics(name) = { name: name, status: :unavailable, latency_ms: nil, details: {} }
168
+
169
+ def unavailable_diagnostics
170
+ { name: display_name, status: :unavailable, latency_ms: nil, details: {} }
171
+ end
58
172
  end
59
173
  end
60
174
  end
@@ -4,23 +4,57 @@ require_relative 'sql_connection_handler'
4
4
  require_relative 'mongo_connection_handler'
5
5
  require_relative 'redis_connection_handler'
6
6
  require_relative 'elasticsearch_connection_handler'
7
+ require_relative '../output'
8
+ require_relative '../instrumentation'
7
9
 
8
10
  module ConsoleKit
9
11
  module Connections
10
- # Manages available connection handlers
11
12
  class ConnectionManager
13
+ DROPPED_WARNING = 'was skipped because %<reason>s. That backend will NOT be switched, verified or rolled ' \
14
+ 'back; it is recorded on the tenant state so the switch cannot report itself as fully ' \
15
+ 'verified.'
16
+ HALF_IMPLEMENTED = 'it is only half-implemented (%<message>s)'
17
+ REPORTED_KEY = :console_kit_dropped_reported
18
+
12
19
  class << self
13
- def available_handlers(context)
14
- handler_classes.filter_map do |klass|
15
- handler = klass.new(context)
16
- handler if handler.available?
17
- rescue NotImplementedError
18
- nil
19
- end
20
+ def available_handlers(context, dropped = nil)
21
+ handler_classes.filter_map { |klass| resolve(klass, context, dropped) }
20
22
  end
21
23
 
22
24
  private
23
25
 
26
+ def resolve(klass, context, dropped)
27
+ handler = klass.new(context)
28
+ return handler if handler.available?
29
+
30
+ drop(klass, dropped, handler.try(:unavailable_reason))
31
+ rescue NotImplementedError => e
32
+ drop(klass, dropped, format(HALF_IMPLEMENTED, message: e.message))
33
+ end
34
+
35
+ def drop(klass, dropped, reason)
36
+ return nil unless reason
37
+
38
+ report_dropped(klass, reason)
39
+ dropped << klass.backend_key if dropped
40
+ nil
41
+ end
42
+
43
+ def report_dropped(klass, reason)
44
+ Instrumentation.increment('console_kit.handler_dropped')
45
+ return unless unreported?(klass.backend_key, reason)
46
+
47
+ Output.print_warning("#{klass} #{format(DROPPED_WARNING, reason: reason)}")
48
+ end
49
+
50
+ def unreported?(key, reason)
51
+ reported = Thread.current[REPORTED_KEY] ||= {}
52
+ return false if reported[key] == reason
53
+
54
+ reported[key] = reason
55
+ true
56
+ end
57
+
24
58
  def handler_classes = BaseConnectionHandler.registry
25
59
  end
26
60
  end
@@ -1,27 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'table_renderer'
4
+ require_relative '../diagnostics'
4
5
 
5
6
  module ConsoleKit
6
7
  module Connections
7
- # Displays connection diagnostics as a Unicode table
8
8
  module Dashboard
9
9
  class << self
10
- def display
11
- rows = fetch_diagnostics
10
+ def display(level: :basic)
11
+ rows = ConsoleKit::Diagnostics.run(level: level)
12
12
  return Output.print_warning('No connections available') if rows.empty?
13
13
 
14
14
  Output.print_header('Connection Dashboard')
15
15
  Output.print_raw(TableRenderer.render(rows))
16
16
  end
17
-
18
- private
19
-
20
- def fetch_diagnostics
21
- ctx = ConsoleKit.configuration.context_class
22
- handlers = ConnectionManager.available_handlers(ctx)
23
- handlers.map(&:safe_diagnostics)
24
- end
25
17
  end
26
18
  end
27
19
  end
@@ -1,21 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/string/filters'
4
+
3
5
  module ConsoleKit
4
6
  module Connections
5
- # Shared helper methods for connection diagnostics
6
7
  module DiagnosticHelpers
8
+ CREDENTIAL_PATTERN = begin
9
+ url = %r{\b[a-z][a-z0-9+.-]*://\S*}i
10
+ secret_key = /password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|auth(?:orization)?/i
11
+ auth_scheme = /(?:Bearer|Basic|Token|Digest|ApiKey)\s+/i
12
+ assignment = /["']?\b#{secret_key}\b["']?\s*(?:=>|=|:(?!:))\s*["']?#{auth_scheme}?\S+/
13
+ noise = /
14
+ authentication|auth|is|was|for|error|required|mismatch|incorrect|invalid|failed|missing|expired|
15
+ limit|type|count|name|header|value|must|cannot|not|and|or|in|of|to|exceeded|denied|unsupported|
16
+ supported|rejected|refused|revoked|length|format|scheme|provider|store|file|path|key
17
+ /xi
18
+ phrase = /\b#{secret_key}\s+(?!#{noise}\b)["']?\S+/i
19
+ principal = /\b(?:for user|username|user name)\b\s+["']?\S+/i
20
+ principal_at = /\buser\s+["']?\S+@\S+/i
21
+ Regexp.union(url, assignment, phrase, principal, principal_at).freeze
22
+ end
23
+
24
+ REDACTED = '[redacted]'
25
+
7
26
  module_function
8
27
 
9
28
  def clock_time
10
29
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
30
  end
12
31
 
13
- def error_diagnostics(name, error)
14
- { name: name, status: :error, latency_ms: nil, details: { error: error.message.truncate(60) } }
32
+ def scrub(message)
33
+ message.to_s.scrub.gsub(CREDENTIAL_PATTERN, REDACTED)
15
34
  end
16
35
 
17
- def timeout_diagnostics(name, timeout)
18
- { name: name, status: :timeout, latency_ms: nil, details: { error: "Timed out after #{timeout}s" } }
36
+ def error_diagnostics(name, error)
37
+ { name: name, status: :error, latency_ms: nil, details: { error: scrub(error.message).truncate(60) } }
19
38
  end
20
39
  end
21
40
  end