vector_mcp 0.2.0 → 0.3.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/CHANGELOG.md +281 -0
- data/README.md +302 -373
- data/lib/vector_mcp/definitions.rb +3 -1
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +132 -6
- data/lib/vector_mcp/logging/component.rb +131 -0
- data/lib/vector_mcp/logging/configuration.rb +156 -0
- data/lib/vector_mcp/logging/constants.rb +21 -0
- data/lib/vector_mcp/logging/core.rb +175 -0
- data/lib/vector_mcp/logging/filters/component.rb +69 -0
- data/lib/vector_mcp/logging/filters/level.rb +23 -0
- data/lib/vector_mcp/logging/formatters/base.rb +52 -0
- data/lib/vector_mcp/logging/formatters/json.rb +83 -0
- data/lib/vector_mcp/logging/formatters/text.rb +72 -0
- data/lib/vector_mcp/logging/outputs/base.rb +64 -0
- data/lib/vector_mcp/logging/outputs/console.rb +35 -0
- data/lib/vector_mcp/logging/outputs/file.rb +157 -0
- data/lib/vector_mcp/logging.rb +71 -0
- data/lib/vector_mcp/security/auth_manager.rb +79 -0
- data/lib/vector_mcp/security/authorization.rb +96 -0
- data/lib/vector_mcp/security/middleware.rb +172 -0
- data/lib/vector_mcp/security/session_context.rb +147 -0
- data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
- data/lib/vector_mcp/security/strategies/custom.rb +71 -0
- data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
- data/lib/vector_mcp/security.rb +46 -0
- data/lib/vector_mcp/server/registry.rb +24 -0
- data/lib/vector_mcp/server.rb +141 -1
- data/lib/vector_mcp/transport/sse/client_connection.rb +113 -0
- data/lib/vector_mcp/transport/sse/message_handler.rb +166 -0
- data/lib/vector_mcp/transport/sse/puma_config.rb +77 -0
- data/lib/vector_mcp/transport/sse/stream_manager.rb +92 -0
- data/lib/vector_mcp/transport/sse.rb +119 -460
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +63 -21
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
class Core
|
8
|
+
attr_reader :configuration, :components, :outputs
|
9
|
+
|
10
|
+
def initialize(configuration = nil)
|
11
|
+
@configuration = configuration || Configuration.new
|
12
|
+
@components = {}
|
13
|
+
@outputs = []
|
14
|
+
@mutex = Mutex.new
|
15
|
+
@legacy_logger = nil
|
16
|
+
|
17
|
+
setup_default_output
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger_for(component_name)
|
21
|
+
@mutex.synchronize do
|
22
|
+
@components[component_name] ||= Component.new(
|
23
|
+
component_name,
|
24
|
+
self,
|
25
|
+
@configuration.component_config(component_name)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def legacy_logger
|
31
|
+
@legacy_logger ||= LegacyAdapter.new(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def log(level, component, message, context = {})
|
35
|
+
return unless should_log?(level, component)
|
36
|
+
|
37
|
+
log_entry = Logging::LogEntry.new({
|
38
|
+
timestamp: Time.now,
|
39
|
+
level: level,
|
40
|
+
component: component,
|
41
|
+
message: message,
|
42
|
+
context: context,
|
43
|
+
thread_id: Thread.current.object_id
|
44
|
+
})
|
45
|
+
|
46
|
+
@outputs.each do |output|
|
47
|
+
output.write(log_entry)
|
48
|
+
rescue StandardError => e
|
49
|
+
warn "Failed to write to output #{output.class}: #{e.message}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_output(output)
|
54
|
+
@mutex.synchronize do
|
55
|
+
@outputs << output unless @outputs.include?(output)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def remove_output(output)
|
60
|
+
@mutex.synchronize do
|
61
|
+
@outputs.delete(output)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def configure(&)
|
66
|
+
@configuration.configure(&)
|
67
|
+
reconfigure_outputs
|
68
|
+
end
|
69
|
+
|
70
|
+
def shutdown
|
71
|
+
@outputs.each(&:close)
|
72
|
+
@outputs.clear
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def should_log?(level, component)
|
78
|
+
min_level = @configuration.level_for(component)
|
79
|
+
level >= min_level
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_default_output
|
83
|
+
console_output = Outputs::Console.new(@configuration.console_config)
|
84
|
+
add_output(console_output)
|
85
|
+
end
|
86
|
+
|
87
|
+
def reconfigure_outputs
|
88
|
+
@outputs.each(&:reconfigure)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class LegacyAdapter
|
93
|
+
def initialize(core)
|
94
|
+
@core = core
|
95
|
+
@component = "legacy"
|
96
|
+
@progname = "VectorMCP"
|
97
|
+
end
|
98
|
+
|
99
|
+
def debug(message = nil, &)
|
100
|
+
log_with_block(Logging::LEVELS[:DEBUG], message, &)
|
101
|
+
end
|
102
|
+
|
103
|
+
def info(message = nil, &)
|
104
|
+
log_with_block(Logging::LEVELS[:INFO], message, &)
|
105
|
+
end
|
106
|
+
|
107
|
+
def warn(message = nil, &)
|
108
|
+
log_with_block(Logging::LEVELS[:WARN], message, &)
|
109
|
+
end
|
110
|
+
|
111
|
+
def error(message = nil, &)
|
112
|
+
log_with_block(Logging::LEVELS[:ERROR], message, &)
|
113
|
+
end
|
114
|
+
|
115
|
+
def fatal(message = nil, &)
|
116
|
+
log_with_block(Logging::LEVELS[:FATAL], message, &)
|
117
|
+
end
|
118
|
+
|
119
|
+
def level
|
120
|
+
@core.configuration.level_for(@component)
|
121
|
+
end
|
122
|
+
|
123
|
+
def level=(new_level)
|
124
|
+
@core.configuration.set_component_level(@component, new_level)
|
125
|
+
end
|
126
|
+
|
127
|
+
attr_accessor :progname
|
128
|
+
|
129
|
+
def add(severity, message = nil, progname = nil, &block)
|
130
|
+
actual_message = message || block&.call || progname
|
131
|
+
@core.log(severity, @component, actual_message)
|
132
|
+
end
|
133
|
+
|
134
|
+
# For backward compatibility with Logger interface checks
|
135
|
+
def is_a?(klass)
|
136
|
+
return true if klass == Logger
|
137
|
+
|
138
|
+
super
|
139
|
+
end
|
140
|
+
|
141
|
+
def kind_of?(klass)
|
142
|
+
return true if klass == Logger
|
143
|
+
|
144
|
+
super
|
145
|
+
end
|
146
|
+
|
147
|
+
# Simulate Logger's logdev for compatibility
|
148
|
+
def instance_variable_get(var_name)
|
149
|
+
if var_name == :@logdev
|
150
|
+
# Return a mock object that simulates Logger's logdev
|
151
|
+
MockLogdev.new
|
152
|
+
else
|
153
|
+
super
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def log_with_block(level, message, &block)
|
160
|
+
if block_given?
|
161
|
+
return unless @core.configuration.level_for(@component) <= level
|
162
|
+
|
163
|
+
message = block.call
|
164
|
+
end
|
165
|
+
@core.log(level, @component, message)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class MockLogdev
|
170
|
+
def dev
|
171
|
+
$stderr
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Filters
|
8
|
+
class Component
|
9
|
+
def initialize(allowed_components = nil, blocked_components = nil)
|
10
|
+
@allowed_components = normalize_components(allowed_components)
|
11
|
+
@blocked_components = normalize_components(blocked_components)
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept?(log_entry)
|
15
|
+
return false if blocked?(log_entry.component)
|
16
|
+
return true if @allowed_components.nil?
|
17
|
+
|
18
|
+
allowed?(log_entry.component)
|
19
|
+
end
|
20
|
+
|
21
|
+
def allow_component(component)
|
22
|
+
@allowed_components ||= Set.new
|
23
|
+
@allowed_components.add(component.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def block_component(component)
|
27
|
+
@blocked_components ||= Set.new
|
28
|
+
@blocked_components.add(component.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_component_filter(component)
|
32
|
+
@allowed_components&.delete(component.to_s)
|
33
|
+
@blocked_components&.delete(component.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def allowed?(component)
|
39
|
+
return true if @allowed_components.nil?
|
40
|
+
|
41
|
+
@allowed_components.include?(component) ||
|
42
|
+
@allowed_components.any? { |pattern| component.start_with?(pattern) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def blocked?(component)
|
46
|
+
return false if @blocked_components.nil?
|
47
|
+
|
48
|
+
@blocked_components.include?(component) ||
|
49
|
+
@blocked_components.any? { |pattern| component.start_with?(pattern) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize_components(components)
|
53
|
+
case components
|
54
|
+
when nil
|
55
|
+
nil
|
56
|
+
when String
|
57
|
+
Set.new([components])
|
58
|
+
when Array
|
59
|
+
Set.new(components.map(&:to_s))
|
60
|
+
when Set
|
61
|
+
components
|
62
|
+
else
|
63
|
+
Set.new([components.to_s])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Filters
|
6
|
+
class Level
|
7
|
+
def initialize(min_level)
|
8
|
+
@min_level = min_level.is_a?(Integer) ? min_level : Logging.level_value(min_level)
|
9
|
+
end
|
10
|
+
|
11
|
+
def accept?(log_entry)
|
12
|
+
log_entry.level >= @min_level
|
13
|
+
end
|
14
|
+
|
15
|
+
def min_level=(new_level)
|
16
|
+
@min_level = new_level.is_a?(Integer) ? new_level : Logging.level_value(new_level)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :min_level
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Formatters
|
8
|
+
class Base
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(log_entry)
|
14
|
+
raise NotImplementedError, "Subclasses must implement #format"
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def format_timestamp(timestamp)
|
20
|
+
timestamp.strftime("%Y-%m-%d %H:%M:%S.%#{Constants::TIMESTAMP_PRECISION}N")
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_level(level_name, width = Constants::DEFAULT_LEVEL_WIDTH)
|
24
|
+
level_name.ljust(width)
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_component(component, width = Constants::DEFAULT_COMPONENT_WIDTH)
|
28
|
+
if component.length > width
|
29
|
+
"#{component[0..(width - Constants::TRUNCATION_SUFFIX_LENGTH)]}..."
|
30
|
+
else
|
31
|
+
component.ljust(width)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_context(context)
|
36
|
+
return "" if context.empty?
|
37
|
+
|
38
|
+
pairs = context.map do |key, value|
|
39
|
+
"#{key}=#{value}"
|
40
|
+
end
|
41
|
+
" (#{pairs.join(", ")})"
|
42
|
+
end
|
43
|
+
|
44
|
+
def truncate_message(message, max_length = Constants::DEFAULT_MAX_MESSAGE_LENGTH)
|
45
|
+
return message if message.length <= max_length
|
46
|
+
|
47
|
+
"#{message[0..(max_length - Constants::TRUNCATION_SUFFIX_LENGTH)]}..."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require_relative "../constants"
|
5
|
+
|
6
|
+
module VectorMCP
|
7
|
+
module Logging
|
8
|
+
module Formatters
|
9
|
+
class Json < Base
|
10
|
+
def initialize(options = {})
|
11
|
+
super
|
12
|
+
@pretty = options.fetch(:pretty, false)
|
13
|
+
@include_thread = options.fetch(:include_thread, false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def format(log_entry)
|
17
|
+
data = {
|
18
|
+
timestamp: log_entry.timestamp.iso8601(Constants::TIMESTAMP_PRECISION),
|
19
|
+
level: log_entry.level_name,
|
20
|
+
component: log_entry.component,
|
21
|
+
message: log_entry.message
|
22
|
+
}
|
23
|
+
|
24
|
+
data[:context] = log_entry.context unless log_entry.context.empty?
|
25
|
+
data[:thread_id] = log_entry.thread_id if @include_thread
|
26
|
+
|
27
|
+
if @pretty
|
28
|
+
"#{JSON.pretty_generate(data)}\n"
|
29
|
+
else
|
30
|
+
"#{JSON.generate(data)}\n"
|
31
|
+
end
|
32
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
33
|
+
fallback_format(log_entry, e)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def fallback_format(log_entry, error)
|
39
|
+
safe_data = {
|
40
|
+
timestamp: log_entry.timestamp.iso8601(Constants::TIMESTAMP_PRECISION),
|
41
|
+
level: log_entry.level_name,
|
42
|
+
component: log_entry.component,
|
43
|
+
message: "JSON serialization failed: #{error.message}",
|
44
|
+
original_message: log_entry.message.to_s,
|
45
|
+
context: sanitize_context(log_entry.context)
|
46
|
+
}
|
47
|
+
|
48
|
+
"#{JSON.generate(safe_data)}\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def sanitize_context(context, depth = 0)
|
52
|
+
return "<max_depth_reached>" if depth > Constants::MAX_SERIALIZATION_DEPTH
|
53
|
+
return {} unless context.is_a?(Hash)
|
54
|
+
|
55
|
+
context.each_with_object({}) do |(key, value), sanitized|
|
56
|
+
sanitized[key.to_s] = sanitize_value(value, depth + 1)
|
57
|
+
end
|
58
|
+
rescue StandardError
|
59
|
+
{ "<sanitization_error>" => true }
|
60
|
+
end
|
61
|
+
|
62
|
+
def sanitize_value(value, depth = 0)
|
63
|
+
return "<max_depth_reached>" if depth > Constants::MAX_SERIALIZATION_DEPTH
|
64
|
+
|
65
|
+
case value
|
66
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
67
|
+
value
|
68
|
+
when Array
|
69
|
+
return "<complex_array>" if depth > Constants::MAX_ARRAY_SERIALIZATION_DEPTH
|
70
|
+
|
71
|
+
value.first(Constants::MAX_ARRAY_ELEMENTS_TO_SERIALIZE).map { |v| sanitize_value(v, depth + 1) }
|
72
|
+
when Hash
|
73
|
+
sanitize_context(value, depth)
|
74
|
+
else
|
75
|
+
value.to_s
|
76
|
+
end
|
77
|
+
rescue StandardError
|
78
|
+
"<serialization_error>"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Formatters
|
8
|
+
class Text < Base
|
9
|
+
COLORS = {
|
10
|
+
TRACE: "\e[90m", # gray
|
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
|
+
SECURITY: "\e[1;31m" # bold red
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
RESET_COLOR = "\e[0m"
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
super
|
23
|
+
@colorize = options.fetch(:colorize, true)
|
24
|
+
@include_timestamp = options.fetch(:include_timestamp, true)
|
25
|
+
@include_thread = options.fetch(:include_thread, false)
|
26
|
+
@include_component = options.fetch(:include_component, true)
|
27
|
+
@max_message_length = options.fetch(:max_message_length, Constants::DEFAULT_MAX_MESSAGE_LENGTH)
|
28
|
+
end
|
29
|
+
|
30
|
+
def format(log_entry)
|
31
|
+
parts = []
|
32
|
+
|
33
|
+
parts << format_timestamp_part(log_entry.timestamp) if @include_timestamp
|
34
|
+
|
35
|
+
parts << format_level_part(log_entry.level_name)
|
36
|
+
|
37
|
+
parts << format_component_part(log_entry.component) if @include_component
|
38
|
+
|
39
|
+
parts << format_thread_part(log_entry.thread_id) if @include_thread
|
40
|
+
|
41
|
+
message = truncate_message(log_entry.message, @max_message_length)
|
42
|
+
context_str = format_context(log_entry.context)
|
43
|
+
|
44
|
+
"#{parts.join(" ")} #{message}#{context_str}\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def format_timestamp_part(timestamp)
|
50
|
+
"[#{format_timestamp(timestamp)}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_level_part(level_name)
|
54
|
+
level_str = format_level(level_name.to_s, Constants::DEFAULT_LEVEL_WIDTH)
|
55
|
+
if @colorize && COLORS[level_name.to_sym]
|
56
|
+
"#{COLORS[level_name.to_sym]}#{level_str}#{RESET_COLOR}"
|
57
|
+
else
|
58
|
+
level_str
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_component_part(component)
|
63
|
+
format_component(component, Constants::DEFAULT_COMPONENT_WIDTH)
|
64
|
+
end
|
65
|
+
|
66
|
+
def format_thread_part(thread_id)
|
67
|
+
"[#{thread_id}]"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Outputs
|
6
|
+
class Base
|
7
|
+
attr_reader :formatter, :config
|
8
|
+
|
9
|
+
def initialize(config = {})
|
10
|
+
@config = config
|
11
|
+
@formatter = create_formatter
|
12
|
+
@closed = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(log_entry)
|
16
|
+
return if @closed
|
17
|
+
|
18
|
+
formatted_message = @formatter.format(log_entry)
|
19
|
+
write_formatted(formatted_message)
|
20
|
+
rescue StandardError => e
|
21
|
+
fallback_write("Logging output error: #{e.message}\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
@closed = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def closed?
|
29
|
+
@closed
|
30
|
+
end
|
31
|
+
|
32
|
+
def reconfigure
|
33
|
+
@formatter = create_formatter
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def write_formatted(message)
|
39
|
+
raise NotImplementedError, "Subclasses must implement #write_formatted"
|
40
|
+
end
|
41
|
+
|
42
|
+
def fallback_write(message)
|
43
|
+
$stderr.write(message)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def create_formatter
|
49
|
+
format_type = @config[:format] || "text"
|
50
|
+
formatter_options = @config.except(:format)
|
51
|
+
|
52
|
+
case format_type.to_s.downcase
|
53
|
+
when "json"
|
54
|
+
Formatters::Json.new(formatter_options)
|
55
|
+
when "text"
|
56
|
+
Formatters::Text.new(formatter_options)
|
57
|
+
else
|
58
|
+
raise OutputError, "Unknown formatter type: #{format_type}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Outputs
|
6
|
+
class Console < Base
|
7
|
+
def initialize(config = {})
|
8
|
+
super
|
9
|
+
@stream = determine_stream
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def write_formatted(message)
|
16
|
+
@mutex.synchronize do
|
17
|
+
@stream.write(message)
|
18
|
+
@stream.flush if @stream.respond_to?(:flush)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def determine_stream
|
25
|
+
case @config[:stream]&.to_s&.downcase
|
26
|
+
when "stdout"
|
27
|
+
$stdout
|
28
|
+
else
|
29
|
+
$stderr # Default to stderr for logging
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|