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.
- checksums.yaml +4 -4
- data/lib/console_kit/configuration.rb +15 -21
- data/lib/console_kit/configuration_validator.rb +179 -0
- data/lib/console_kit/connections/base_connection_handler.rb +139 -25
- data/lib/console_kit/connections/connection_manager.rb +42 -8
- data/lib/console_kit/connections/dashboard.rb +3 -11
- data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
- data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
- data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
- data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
- data/lib/console_kit/connections/mongoid_support.rb +56 -0
- data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
- data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
- data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
- data/lib/console_kit/connections/sql_strategy.rb +248 -0
- data/lib/console_kit/connections/table_renderer.rb +30 -20
- data/lib/console_kit/console_helpers.rb +17 -31
- data/lib/console_kit/diagnostics.rb +143 -0
- data/lib/console_kit/errors.rb +93 -0
- data/lib/console_kit/instrumentation.rb +57 -0
- data/lib/console_kit/output.rb +5 -14
- data/lib/console_kit/prompt.rb +17 -29
- data/lib/console_kit/railtie.rb +2 -3
- data/lib/console_kit/setup.rb +66 -13
- data/lib/console_kit/setup_ui.rb +1 -4
- data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
- data/lib/console_kit/tenant_configurator.rb +40 -78
- data/lib/console_kit/tenant_orchestrator.rb +31 -27
- data/lib/console_kit/tenant_plan.rb +68 -0
- data/lib/console_kit/tenant_rollback.rb +77 -0
- data/lib/console_kit/tenant_selector.rb +3 -6
- data/lib/console_kit/tenant_state.rb +73 -0
- data/lib/console_kit/tenant_switch.rb +124 -0
- data/lib/console_kit/version.rb +1 -1
- data/lib/console_kit.rb +51 -24
- data/lib/generators/console_kit/install_generator.rb +1 -12
- data/lib/generators/console_kit/templates/console_kit.rb +4 -0
- metadata +15 -4
- data/lib/console_kit/connections/table_formatter.rb +0 -37
|
@@ -1,69 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_connection_handler'
|
|
4
|
+
require_relative 'sql_strategy'
|
|
4
5
|
|
|
5
6
|
module ConsoleKit
|
|
6
7
|
module Connections
|
|
7
|
-
# Handles SQL connections
|
|
8
8
|
class SqlConnectionHandler < BaseConnectionHandler
|
|
9
|
+
backend :sql,
|
|
10
|
+
display_name: 'SQL',
|
|
11
|
+
context_attribute: :tenant_shard,
|
|
12
|
+
constants_key: :shard,
|
|
13
|
+
detail_label: 'Shard'
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_CLASS = 'ApplicationRecord'
|
|
16
|
+
MISSING_BASE_CLASS = 'the configured sql_base_class %<name>s could not be resolved. Check the class name and ' \
|
|
17
|
+
'that the class is loaded'
|
|
18
|
+
|
|
9
19
|
class << self
|
|
20
|
+
def target_error(value) = identifier_error(value)
|
|
21
|
+
|
|
10
22
|
def sql_version(conn)
|
|
11
23
|
conn.select_value('SELECT version()')
|
|
12
|
-
rescue StandardError
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
26
|
+
|
|
13
27
|
nil
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
def base_class_name = ConsoleKit.configuration.sql_base_class
|
|
17
31
|
end
|
|
18
32
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
def available? = resolved_base_class.present?
|
|
34
|
+
|
|
35
|
+
def unavailable_reason
|
|
36
|
+
name = self.class.base_class_name
|
|
37
|
+
return nil if name.to_s == DEFAULT_BASE_CLASS || resolved_base_class.present?
|
|
38
|
+
|
|
39
|
+
format(MISSING_BASE_CLASS, name: name.inspect)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prepare(target)
|
|
43
|
+
validate_target!(target)
|
|
44
|
+
unless strategy.switchable?
|
|
45
|
+
raise UnsupportedBackendError, "#{display_name} base class #{base_class} cannot switch connections."
|
|
46
|
+
end
|
|
47
|
+
return if strategy.resolvable?(normalize(target))
|
|
48
|
+
|
|
49
|
+
raise ConfigurationError,
|
|
50
|
+
"ConsoleKit: SQL shard #{scrub(target.inspect)} is not a registered shard or database configuration."
|
|
24
51
|
end
|
|
25
52
|
|
|
26
|
-
def
|
|
53
|
+
def snapshot = strategy.snapshot
|
|
27
54
|
|
|
28
|
-
def
|
|
29
|
-
|
|
55
|
+
def connect!(target)
|
|
56
|
+
shard = normalize(target)
|
|
57
|
+
Output.print_info("#{connection_message(shard)} via #{base_class}")
|
|
58
|
+
strategy.apply(shard)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def verify!(target)
|
|
62
|
+
expected, actual = strategy.identity(normalize(target))
|
|
63
|
+
return true if expected.to_s == actual.to_s
|
|
30
64
|
|
|
31
|
-
|
|
32
|
-
rescue StandardError => e
|
|
33
|
-
error_diagnostics('SQL', e)
|
|
65
|
+
raise verification_error(expected, actual)
|
|
34
66
|
end
|
|
35
67
|
|
|
68
|
+
def restore(state) = strategy.restore(state)
|
|
69
|
+
|
|
70
|
+
def diagnostic_identity = strategy.pool_details
|
|
71
|
+
|
|
36
72
|
private
|
|
37
73
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
build_sql_diagnostics(conn, latency)
|
|
74
|
+
def basic_diagnostics
|
|
75
|
+
details = strategy.pool_details
|
|
76
|
+
{ name: display_name, status: details.empty? ? :unknown : :connected, latency_ms: nil, details: details }
|
|
42
77
|
end
|
|
43
78
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
def full_diagnostics
|
|
80
|
+
conn = base_class.connection
|
|
81
|
+
latency = measure_latency { conn.execute('SELECT 1') }
|
|
82
|
+
{ name: display_name, status: :connected, latency_ms: latency, details: full_details(conn) }
|
|
47
83
|
end
|
|
48
84
|
|
|
49
|
-
def
|
|
85
|
+
def full_details(conn)
|
|
50
86
|
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
details: {
|
|
55
|
-
adapter: conn.adapter_name,
|
|
56
|
-
pool_size: base_class.connection_pool.size,
|
|
57
|
-
version: self.class.sql_version(conn).to_s.truncate(50)
|
|
58
|
-
}
|
|
87
|
+
adapter: conn.adapter_name,
|
|
88
|
+
pool_size: base_class.connection_pool.size,
|
|
89
|
+
version: self.class.sql_version(conn).to_s.truncate(50)
|
|
59
90
|
}
|
|
60
91
|
end
|
|
61
92
|
|
|
93
|
+
def strategy = @strategy ||= SqlStrategy.new(base_class)
|
|
94
|
+
def normalize(target) = target.presence&.to_sym
|
|
95
|
+
|
|
96
|
+
def resolved_base_class = self.class.base_class_name.to_s.safe_constantize
|
|
97
|
+
|
|
62
98
|
def base_class
|
|
63
99
|
@base_class ||= begin
|
|
64
100
|
name = self.class.base_class_name
|
|
65
101
|
klass = name.to_s.safe_constantize
|
|
66
|
-
klass || raise(
|
|
102
|
+
klass || raise(ConfigurationError, "ConsoleKit: sql_base_class '#{name}' could not be found.")
|
|
67
103
|
end
|
|
68
104
|
end
|
|
69
105
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
require_relative '../output'
|
|
5
|
+
require_relative '../instrumentation'
|
|
6
|
+
|
|
7
|
+
module ConsoleKit
|
|
8
|
+
module Connections
|
|
9
|
+
class ShardFrame
|
|
10
|
+
KEY = :console_kit_sql_connected_to_frame
|
|
11
|
+
STOLEN = 'ConsoleKit: a `connected_to` block removed the shard frame ConsoleKit had committed, so shard ' \
|
|
12
|
+
'%<shard>p was not live while that block was open. Re-applying it now.'
|
|
13
|
+
REASSERTED = 'console_kit.sql_frame_reasserted'
|
|
14
|
+
|
|
15
|
+
attr_reader :base_class
|
|
16
|
+
|
|
17
|
+
def initialize(base_class) = @base_class = base_class
|
|
18
|
+
|
|
19
|
+
def apply(shard)
|
|
20
|
+
base_class.connecting_to(shard: shard || base_class.default_shard, role: base_class.current_role)
|
|
21
|
+
place(shard)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def live_shard
|
|
25
|
+
reassert if taken?
|
|
26
|
+
base_class.current_shard
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def applied_shard
|
|
30
|
+
return nil unless on?
|
|
31
|
+
|
|
32
|
+
owned[:shard] || base_class.default_shard
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def forget_unless_on
|
|
36
|
+
Thread.current.thread_variable_set(KEY, nil) if owned && !on?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def on? = !index_in.nil?
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def stack = base_class.try(:connected_to_stack)
|
|
44
|
+
def taken? = !owned.nil? && !on?
|
|
45
|
+
|
|
46
|
+
def reassert
|
|
47
|
+
shard = owned[:shard]
|
|
48
|
+
Instrumentation.increment(REASSERTED)
|
|
49
|
+
Output.print_warning(format(STOLEN, shard: shard || base_class.default_shard))
|
|
50
|
+
apply(shard)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def place(shard)
|
|
54
|
+
current = stack
|
|
55
|
+
index = index_in
|
|
56
|
+
current[index] = current.pop if index
|
|
57
|
+
remember(current, index || (current.size - 1), shard)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def index_in
|
|
61
|
+
current = stack
|
|
62
|
+
entry = owned
|
|
63
|
+
return nil unless entry && current
|
|
64
|
+
|
|
65
|
+
index = entry[:index]
|
|
66
|
+
index if current[index].equal?(entry[:frame])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def owned
|
|
70
|
+
entry = Thread.current.thread_variable_get(KEY)
|
|
71
|
+
entry if entry && entry[:base].equal?(base_class)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remember(current, index, shard)
|
|
75
|
+
Thread.current.thread_variable_set(
|
|
76
|
+
KEY, { base: base_class, frame: current[index], index: index, shard: shard }
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class PoolSlot
|
|
82
|
+
attr_reader :base_class
|
|
83
|
+
|
|
84
|
+
def initialize(base_class) = @base_class = base_class
|
|
85
|
+
|
|
86
|
+
def absent? = lookup.nil?
|
|
87
|
+
|
|
88
|
+
def remove
|
|
89
|
+
return if absent?
|
|
90
|
+
return handler.remove_connection_pool(spec, role: role, shard: shard) if removable_handler?
|
|
91
|
+
|
|
92
|
+
base_class.remove_connection if base_class.respond_to?(:remove_connection)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def handler = base_class.try(:connection_handler)
|
|
98
|
+
def spec = base_class.try(:connection_specification_name)
|
|
99
|
+
def role = base_class.try(:current_role)
|
|
100
|
+
def shard = base_class.try(:current_shard)
|
|
101
|
+
def removable_handler? = handler.respond_to?(:remove_connection_pool) && spec
|
|
102
|
+
|
|
103
|
+
def lookup
|
|
104
|
+
return handler.retrieve_connection_pool(spec, role: role, shard: shard) if retrievable_handler?
|
|
105
|
+
|
|
106
|
+
base_class.try(:connection_pool)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def retrievable_handler? = handler.respond_to?(:retrieve_connection_pool) && spec
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class SqlStrategy
|
|
117
|
+
NATIVE_METHODS = %i[connecting_to connected_to_stack default_shard current_shard current_role].freeze
|
|
118
|
+
|
|
119
|
+
attr_reader :base_class
|
|
120
|
+
|
|
121
|
+
def initialize(base_class) = @base_class = base_class
|
|
122
|
+
|
|
123
|
+
def switchable? = base_class.respond_to?(:establish_connection) || native_capable?
|
|
124
|
+
|
|
125
|
+
def native?(shard)
|
|
126
|
+
return false unless native_capable?
|
|
127
|
+
return !connected_to_stack.to_a.empty? if shard.nil?
|
|
128
|
+
|
|
129
|
+
!shard_pool(shard).nil?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def resolvable?(shard)
|
|
133
|
+
return true if !shard || native?(shard)
|
|
134
|
+
|
|
135
|
+
configs = env_configs
|
|
136
|
+
configs.empty? || configs.any? { |cfg| config_name(cfg).to_s == shard.to_s }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def snapshot
|
|
140
|
+
name = current_db_config_name
|
|
141
|
+
{
|
|
142
|
+
shard: frame.applied_shard || base_class.try(:current_shard),
|
|
143
|
+
role: base_class.try(:current_role),
|
|
144
|
+
stack_depth: connected_to_stack&.size,
|
|
145
|
+
db_config_name: name,
|
|
146
|
+
pool_absent: !name && pool.absent?
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply(shard) = native?(shard) ? apply_native(shard) : apply_fallback(shard)
|
|
151
|
+
|
|
152
|
+
def restore(state)
|
|
153
|
+
return unless state
|
|
154
|
+
|
|
155
|
+
unwind_stack(state[:stack_depth])
|
|
156
|
+
restore_shard(state[:shard])
|
|
157
|
+
state[:pool_absent] ? pool.remove : reestablish(state[:db_config_name])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def identity(shard)
|
|
161
|
+
return [shard || base_class.default_shard, frame.live_shard] if native?(shard)
|
|
162
|
+
|
|
163
|
+
[expected_db_config_name(shard), current_db_config_name]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def pool_details
|
|
167
|
+
describe_pool(base_class.try(:connection_pool))
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
170
|
+
|
|
171
|
+
{}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def describe_pool(live_pool)
|
|
177
|
+
return {} unless live_pool
|
|
178
|
+
|
|
179
|
+
{ adapter: live_pool.try(:db_config).try(:adapter), pool_size: live_pool.try(:size),
|
|
180
|
+
config: current_db_config_name, shard: base_class.try(:current_shard) }.compact
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def native_capable? = NATIVE_METHODS.all? { |method| base_class.respond_to?(method) }
|
|
184
|
+
def connected_to_stack = base_class.try(:connected_to_stack)
|
|
185
|
+
|
|
186
|
+
def frame = @frame ||= ShardFrame.new(base_class)
|
|
187
|
+
def pool = @pool ||= PoolSlot.new(base_class)
|
|
188
|
+
def apply_native(shard) = frame.apply(shard)
|
|
189
|
+
|
|
190
|
+
def restore_shard(shard)
|
|
191
|
+
return if !shard || !native_capable? || (frame.applied_shard || base_class.current_shard) == shard
|
|
192
|
+
|
|
193
|
+
apply_native(shard)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def apply_fallback(shard)
|
|
197
|
+
desired = expected_db_config_name(shard)
|
|
198
|
+
return if desired && desired.to_s == current_db_config_name.to_s
|
|
199
|
+
|
|
200
|
+
shard ? base_class.establish_connection(shard.to_sym) : base_class.establish_connection
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def unwind_stack(depth)
|
|
204
|
+
stack = connected_to_stack
|
|
205
|
+
return unless stack && depth
|
|
206
|
+
|
|
207
|
+
stack.pop while stack.size > depth
|
|
208
|
+
frame.forget_unless_on
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def reestablish(name)
|
|
212
|
+
return if !name || name.to_s == current_db_config_name.to_s
|
|
213
|
+
|
|
214
|
+
base_class.establish_connection(name.to_sym)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def shard_pool(shard)
|
|
218
|
+
handler = base_class.try(:connection_handler)
|
|
219
|
+
spec_name = base_class.try(:connection_specification_name)
|
|
220
|
+
return nil unless spec_name && handler.respond_to?(:retrieve_connection_pool)
|
|
221
|
+
|
|
222
|
+
handler.retrieve_connection_pool(spec_name, role: base_class.current_role, shard: shard.to_sym)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def expected_db_config_name(shard) = shard ? shard.to_s : config_name(env_configs.first)
|
|
226
|
+
|
|
227
|
+
def env_configs
|
|
228
|
+
configs = base_class.try(:configurations)
|
|
229
|
+
env = current_db_config.try(:env_name)
|
|
230
|
+
return [] unless env && configs.respond_to?(:configs_for)
|
|
231
|
+
|
|
232
|
+
configs.configs_for(env_name: env)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def current_db_config
|
|
236
|
+
base_class.try(:connection_pool).try(:db_config)
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
239
|
+
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def current_db_config_name = config_name(current_db_config)
|
|
244
|
+
|
|
245
|
+
def config_name(config) = config&.name
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -1,43 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'table_formatter'
|
|
4
|
-
|
|
5
3
|
module ConsoleKit
|
|
6
4
|
module Connections
|
|
7
|
-
# Renders diagnostic data into a Unicode box-drawing table
|
|
8
5
|
module TableRenderer
|
|
6
|
+
HEADERS = %w[Service Status Latency Details].freeze
|
|
7
|
+
STATUS = {
|
|
8
|
+
connected: "\u2713 Connected",
|
|
9
|
+
error: "\u2717 Error",
|
|
10
|
+
unavailable: "\u2014 N/A"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
9
13
|
class << self
|
|
10
14
|
def render(rows)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
widths = calculate_widths(headers, table_rows)
|
|
15
|
+
table_rows = rows.map { |row| format_row(row) }
|
|
16
|
+
widths = ([HEADERS] + table_rows).transpose.map { |column| column.map(&:length).max }
|
|
14
17
|
|
|
15
|
-
build_table(
|
|
18
|
+
build_table(table_rows, widths)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
private
|
|
19
22
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
def format_row(diag)
|
|
24
|
+
latency = diag[:latency_ms]
|
|
25
|
+
[
|
|
26
|
+
diag[:name],
|
|
27
|
+
STATUS.fetch(diag[:status], '? Unknown'),
|
|
28
|
+
latency ? "#{latency}ms" : "\u2014",
|
|
29
|
+
format_details(diag[:details])
|
|
30
|
+
]
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
def
|
|
28
|
-
|
|
33
|
+
def format_details(details)
|
|
34
|
+
return '' unless details&.any?
|
|
35
|
+
|
|
36
|
+
details.compact.map { |key, value| "#{key}: #{value}" }.join(', ')
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
def build_table(
|
|
32
|
-
lines = [
|
|
39
|
+
def build_table(rows, widths)
|
|
40
|
+
lines = [rule(widths, "\u250C\u252C\u2510"), table_line(HEADERS, widths),
|
|
41
|
+
rule(widths, "\u251C\u253C\u2524")]
|
|
33
42
|
rows.each { |row| lines << table_line(row, widths) }
|
|
34
|
-
lines <<
|
|
43
|
+
lines << rule(widths, "\u2514\u2534\u2518")
|
|
35
44
|
lines.join("\n")
|
|
36
45
|
end
|
|
37
46
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
def rule(widths, corners)
|
|
48
|
+
left, join, right = corners.chars
|
|
49
|
+
"#{left}#{widths.map { |width| "\u2500" * (width + 2) }.join(join)}#{right}"
|
|
50
|
+
end
|
|
41
51
|
|
|
42
52
|
def table_line(cells, widths)
|
|
43
53
|
content = cells.each_with_index.map { |cell, index| " #{cell.ljust(widths[index])} " }.join("\u2502")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ConsoleKit
|
|
4
|
-
# Helper methods available in the Rails console
|
|
5
4
|
module ConsoleHelpers
|
|
6
5
|
def switch_tenant
|
|
7
6
|
ConsoleKit.reset_current_tenant
|
|
@@ -9,56 +8,43 @@ module ConsoleKit
|
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
def tenant_info
|
|
12
|
-
tenant = ConsoleKit::
|
|
13
|
-
return
|
|
11
|
+
tenant = ConsoleKit::StateStore.tenant_key
|
|
12
|
+
return ConsoleKit::Output.print_warning('No tenant is currently configured.') unless tenant
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
constants = ConsoleKit.configuration.tenants[tenant]&.[](:constants) || {}
|
|
15
|
+
ConsoleHelpers.print_tenant_details(tenant, constants)
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def dashboard
|
|
20
|
-
ConsoleKit::Connections::Dashboard.display
|
|
19
|
+
def dashboard(level: :basic)
|
|
20
|
+
ConsoleKit::Connections::Dashboard.display(level: level)
|
|
21
21
|
self
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def tenants
|
|
25
25
|
names = ConsoleKit.configuration.tenants&.keys || []
|
|
26
|
-
print_available_tenants(names)
|
|
27
|
-
names
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
DETAIL_LABELS = {
|
|
31
|
-
'Partner' => :partner_code, 'Shard' => :shard, 'Mongo DB' => :mongo_db,
|
|
32
|
-
'Redis DB' => :redis_db, 'ES Prefix' => :elasticsearch_prefix, 'Environment' => :environment
|
|
33
|
-
}.freeze
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def no_tenant_warning
|
|
38
|
-
ConsoleKit::Output.print_warning('No tenant is currently configured.')
|
|
39
|
-
self
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def display_tenant_info(tenant)
|
|
43
|
-
constants = ConsoleKit.configuration.tenants[tenant]&.[](:constants) || {}
|
|
44
|
-
ConsoleHelpers.print_tenant_details(tenant, constants)
|
|
45
|
-
self
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def print_available_tenants(names)
|
|
49
26
|
ConsoleKit::Output.print_list(names, header: 'Available Tenants')
|
|
50
|
-
|
|
27
|
+
names
|
|
51
28
|
end
|
|
52
29
|
|
|
53
30
|
class << self
|
|
54
31
|
def print_tenant_details(tenant, constants)
|
|
55
32
|
ConsoleKit::Output.print_header("Tenant: #{tenant}")
|
|
56
|
-
|
|
33
|
+
detail_labels.each do |label, key|
|
|
57
34
|
next unless constants.key?(key)
|
|
58
35
|
|
|
59
36
|
ConsoleKit::Output.print_info(" #{label.ljust(13)}#{constants[key]}")
|
|
60
37
|
end
|
|
61
38
|
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def detail_labels
|
|
43
|
+
backend_labels = ConsoleKit::Connections::BaseConnectionHandler.registry.to_h do |handler|
|
|
44
|
+
[handler.detail_label, handler.constants_key]
|
|
45
|
+
end
|
|
46
|
+
{ 'Partner' => :partner_code }.merge(backend_labels).merge('Environment' => :environment)
|
|
47
|
+
end
|
|
62
48
|
end
|
|
63
49
|
end
|
|
64
50
|
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'tenant_state'
|
|
5
|
+
require_relative 'instrumentation'
|
|
6
|
+
require_relative 'connections/diagnostic_helpers'
|
|
7
|
+
|
|
8
|
+
module ConsoleKit
|
|
9
|
+
module Diagnostics
|
|
10
|
+
LEVELS = %i[basic full].freeze
|
|
11
|
+
DEFAULT_TIMEOUT = 2
|
|
12
|
+
CACHE_TTL_SECONDS = 2.0
|
|
13
|
+
EVENT = 'console_kit.diagnostics'
|
|
14
|
+
TIMEOUT_COUNTER = 'console_kit.diagnostics_timeout'
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def run(level: :basic, timeout: DEFAULT_TIMEOUT)
|
|
18
|
+
validate_level!(level)
|
|
19
|
+
available_handlers.map { |handler| cached(handler, level, timeout) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear_cache! = Cache.clear!
|
|
23
|
+
|
|
24
|
+
def validate_level!(level)
|
|
25
|
+
return level if LEVELS.include?(level)
|
|
26
|
+
|
|
27
|
+
raise ConfigurationError,
|
|
28
|
+
"ConsoleKit: unknown diagnostics level #{level.inspect}. Expected one of #{LEVELS.inspect}."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def available_handlers
|
|
34
|
+
Connections::ConnectionManager.available_handlers(ConsoleKit.configuration.context_class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cached(handler, level, timeout)
|
|
38
|
+
Cache.fetch_row(handler, level) { handler.safe_diagnostics(timeout: timeout, level: level) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module Cache
|
|
43
|
+
STORE_KEY = :console_kit_diagnostics_cache
|
|
44
|
+
CACHED_LEVEL = :full
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def fetch_row(handler, level)
|
|
48
|
+
return yield unless level == CACHED_LEVEL
|
|
49
|
+
|
|
50
|
+
key = [StateStore.tenant_key, level, handler.backend_key]
|
|
51
|
+
identity = identity_of(handler)
|
|
52
|
+
read(key, identity) || write(key, identity, yield)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear!
|
|
56
|
+
Thread.current.thread_variable_set(STORE_KEY, nil)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def store
|
|
62
|
+
thread = Thread.current
|
|
63
|
+
thread.thread_variable_get(STORE_KEY) || thread.thread_variable_set(STORE_KEY, {})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read(key, identity)
|
|
67
|
+
entry = store[key]
|
|
68
|
+
return nil unless entry
|
|
69
|
+
|
|
70
|
+
unless current?(entry, identity)
|
|
71
|
+
store.delete(key)
|
|
72
|
+
return nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
entry[:row]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write(key, identity, row)
|
|
79
|
+
return row if row[:status] == :error
|
|
80
|
+
|
|
81
|
+
purge_expired!
|
|
82
|
+
store[key] = { row: row, expires_at: now + CACHE_TTL_SECONDS, state: state, identity: identity }
|
|
83
|
+
row
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def identity_of(handler)
|
|
87
|
+
handler.diagnostic_identity
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
90
|
+
|
|
91
|
+
Object.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def purge_expired!
|
|
95
|
+
store.delete_if { |_key, entry| !fresh?(entry) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fresh?(entry) = entry[:expires_at] > now && entry[:state].equal?(state)
|
|
99
|
+
|
|
100
|
+
def current?(entry, identity) = fresh?(entry) && entry[:identity] == identity
|
|
101
|
+
|
|
102
|
+
def state = StateStore.stored
|
|
103
|
+
def now = Connections::DiagnosticHelpers.clock_time
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module Runner
|
|
108
|
+
class << self
|
|
109
|
+
def call(handler, timeout: DEFAULT_TIMEOUT, level: :basic)
|
|
110
|
+
Diagnostics.validate_level!(level)
|
|
111
|
+
started = Connections::DiagnosticHelpers.clock_time
|
|
112
|
+
outcome = execute(handler, level)
|
|
113
|
+
report_overrun(started, timeout)
|
|
114
|
+
resolve(handler, outcome)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def execute(handler, level)
|
|
118
|
+
Instrumentation.instrument(EVENT, backend: handler.backend_key, level: level) do
|
|
119
|
+
handler.diagnostics(level: level)
|
|
120
|
+
end
|
|
121
|
+
rescue StandardError, ScriptError => e
|
|
122
|
+
e
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def resolve(handler, outcome) = outcome.is_a?(Hash) ? outcome : failed_row(handler, outcome)
|
|
128
|
+
|
|
129
|
+
def report_overrun(started, timeout)
|
|
130
|
+
return if Connections::DiagnosticHelpers.clock_time - started <= timeout
|
|
131
|
+
|
|
132
|
+
Instrumentation.increment(TIMEOUT_COUNTER)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def failed_row(handler, error)
|
|
136
|
+
raise error if ConsoleKit.programming_error?(error)
|
|
137
|
+
|
|
138
|
+
Connections::DiagnosticHelpers.error_diagnostics(handler.display_name, error)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|