vector_mcp 0.3.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 +292 -501
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +89 -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.rb +141 -1
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +55 -3
data/lib/vector_mcp/errors.rb
CHANGED
@@ -174,4 +174,28 @@ module VectorMCP
|
|
174
174
|
# super(message, code: client_code, details: client_details, request_id: request_id)
|
175
175
|
# end
|
176
176
|
# end
|
177
|
+
|
178
|
+
# --- Security Specific Errors ---
|
179
|
+
|
180
|
+
# Represents an authentication required error (-32401).
|
181
|
+
# Indicates the request requires authentication.
|
182
|
+
class UnauthorizedError < ProtocolError
|
183
|
+
# @param message [String] The error message.
|
184
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
185
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
186
|
+
def initialize(message = "Authentication required", details: nil, request_id: nil)
|
187
|
+
super(message, code: -32_401, details: details, request_id: request_id)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Represents an authorization failed error (-32403).
|
192
|
+
# Indicates the authenticated user does not have permission to perform the requested action.
|
193
|
+
class ForbiddenError < ProtocolError
|
194
|
+
# @param message [String] The error message.
|
195
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
196
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
197
|
+
def initialize(message = "Access denied", details: nil, request_id: nil)
|
198
|
+
super(message, code: -32_403, details: details, request_id: request_id)
|
199
|
+
end
|
200
|
+
end
|
177
201
|
end
|
@@ -42,24 +42,35 @@ module VectorMCP
|
|
42
42
|
#
|
43
43
|
# @param params [Hash] The request parameters.
|
44
44
|
# Expected keys: "name" (String), "arguments" (Hash, optional).
|
45
|
-
# @param
|
45
|
+
# @param session [VectorMCP::Session] The current session.
|
46
46
|
# @param server [VectorMCP::Server] The server instance.
|
47
47
|
# @return [Hash] A hash containing the tool call result or an error indication.
|
48
48
|
# Example success: `{ isError: false, content: [{ type: "text", ... }] }`
|
49
49
|
# @raise [VectorMCP::NotFoundError] if the requested tool is not found.
|
50
50
|
# @raise [VectorMCP::InvalidParamsError] if arguments validation fails.
|
51
|
-
|
51
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
52
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
53
|
+
def self.call_tool(params, session, server)
|
52
54
|
tool_name = params["name"]
|
53
55
|
arguments = params["arguments"] || {}
|
54
56
|
|
55
57
|
tool = server.tools[tool_name]
|
56
58
|
raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
|
57
59
|
|
60
|
+
# Security check: authenticate and authorize the request
|
61
|
+
security_result = check_tool_security(session, tool, server)
|
62
|
+
handle_security_failure(security_result) unless security_result[:success]
|
63
|
+
|
58
64
|
# Validate arguments against the tool's input schema
|
59
65
|
validate_input_arguments!(tool_name, tool, arguments)
|
60
66
|
|
61
67
|
# Let StandardError propagate to Server#handle_request
|
62
|
-
|
68
|
+
# Pass session_context only if the handler supports it (for backward compatibility)
|
69
|
+
result = if [1, -1].include?(tool.handler.arity)
|
70
|
+
tool.handler.call(arguments)
|
71
|
+
else
|
72
|
+
tool.handler.call(arguments, security_result[:session_context])
|
73
|
+
end
|
63
74
|
{
|
64
75
|
isError: false,
|
65
76
|
content: VectorMCP::Util.convert_to_mcp_content(result)
|
@@ -83,18 +94,30 @@ module VectorMCP
|
|
83
94
|
#
|
84
95
|
# @param params [Hash] The request parameters.
|
85
96
|
# Expected key: "uri" (String).
|
86
|
-
# @param
|
97
|
+
# @param session [VectorMCP::Session] The current session.
|
87
98
|
# @param server [VectorMCP::Server] The server instance.
|
88
99
|
# @return [Hash] A hash containing an array of content items from the resource.
|
89
100
|
# Example: `{ contents: [{ type: "text", text: "...", uri: "memory://data" }] }`
|
90
101
|
# @raise [VectorMCP::NotFoundError] if the requested resource URI is not found.
|
91
|
-
|
102
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
103
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
104
|
+
def self.read_resource(params, session, server)
|
92
105
|
uri_s = params["uri"]
|
93
106
|
raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
|
94
107
|
|
95
108
|
resource = server.resources[uri_s]
|
109
|
+
|
110
|
+
# Security check: authenticate and authorize the request
|
111
|
+
security_result = check_resource_security(session, resource, server)
|
112
|
+
handle_security_failure(security_result) unless security_result[:success]
|
113
|
+
|
96
114
|
# Let StandardError propagate to Server#handle_request
|
97
|
-
|
115
|
+
# Pass session_context only if the handler supports it (for backward compatibility)
|
116
|
+
content_raw = if [1, -1].include?(resource.handler.arity)
|
117
|
+
resource.handler.call(params)
|
118
|
+
else
|
119
|
+
resource.handler.call(params, security_result[:session_context])
|
120
|
+
end
|
98
121
|
contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
|
99
122
|
contents.each do |item|
|
100
123
|
# Add URI to each content item if not already present
|
@@ -344,6 +367,66 @@ module VectorMCP
|
|
344
367
|
end
|
345
368
|
private_class_method :validate_input_arguments!
|
346
369
|
private_class_method :raise_tool_validation_error
|
370
|
+
|
371
|
+
# Security helper methods
|
372
|
+
|
373
|
+
# Check security for tool access
|
374
|
+
# @api private
|
375
|
+
# @param session [VectorMCP::Session] The current session
|
376
|
+
# @param tool [VectorMCP::Definitions::Tool] The tool being accessed
|
377
|
+
# @param server [VectorMCP::Server] The server instance
|
378
|
+
# @return [Hash] Security check result
|
379
|
+
def self.check_tool_security(session, tool, server)
|
380
|
+
# Extract request context from session for security middleware
|
381
|
+
request = extract_request_from_session(session)
|
382
|
+
server.security_middleware.process_request(request, action: :call, resource: tool)
|
383
|
+
end
|
384
|
+
private_class_method :check_tool_security
|
385
|
+
|
386
|
+
# Check security for resource access
|
387
|
+
# @api private
|
388
|
+
# @param session [VectorMCP::Session] The current session
|
389
|
+
# @param resource [VectorMCP::Definitions::Resource] The resource being accessed
|
390
|
+
# @param server [VectorMCP::Server] The server instance
|
391
|
+
# @return [Hash] Security check result
|
392
|
+
def self.check_resource_security(session, resource, server)
|
393
|
+
request = extract_request_from_session(session)
|
394
|
+
server.security_middleware.process_request(request, action: :read, resource: resource)
|
395
|
+
end
|
396
|
+
private_class_method :check_resource_security
|
397
|
+
|
398
|
+
# Extract request context from session for security processing
|
399
|
+
# @api private
|
400
|
+
# @param session [VectorMCP::Session] The current session
|
401
|
+
# @return [Hash] Request context for security middleware
|
402
|
+
def self.extract_request_from_session(session)
|
403
|
+
# Extract security context from session
|
404
|
+
# This will be enhanced as we integrate with transport layers
|
405
|
+
{
|
406
|
+
headers: session.instance_variable_get(:@request_headers) || {},
|
407
|
+
params: session.instance_variable_get(:@request_params) || {},
|
408
|
+
session_id: session.respond_to?(:id) ? session.id : "test-session"
|
409
|
+
}
|
410
|
+
end
|
411
|
+
private_class_method :extract_request_from_session
|
412
|
+
|
413
|
+
# Handle security failure by raising appropriate error
|
414
|
+
# @api private
|
415
|
+
# @param security_result [Hash] The security check result
|
416
|
+
# @return [void]
|
417
|
+
# @raise [VectorMCP::UnauthorizedError, VectorMCP::ForbiddenError]
|
418
|
+
def self.handle_security_failure(security_result)
|
419
|
+
case security_result[:error_code]
|
420
|
+
when "AUTHENTICATION_REQUIRED"
|
421
|
+
raise VectorMCP::UnauthorizedError, security_result[:error]
|
422
|
+
when "AUTHORIZATION_FAILED"
|
423
|
+
raise VectorMCP::ForbiddenError, security_result[:error]
|
424
|
+
else
|
425
|
+
# Fallback to generic unauthorized error
|
426
|
+
raise VectorMCP::UnauthorizedError, security_result[:error] || "Security check failed"
|
427
|
+
end
|
428
|
+
end
|
429
|
+
private_class_method :handle_security_failure
|
347
430
|
end
|
348
431
|
end
|
349
432
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
class Component
|
6
|
+
attr_reader :name, :core, :config
|
7
|
+
|
8
|
+
def initialize(name, core, config = {})
|
9
|
+
@name = name.to_s
|
10
|
+
@core = core
|
11
|
+
@config = config
|
12
|
+
@context = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_context(context)
|
16
|
+
old_context = @context
|
17
|
+
@context = @context.merge(context)
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
@context = old_context
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_context(context)
|
24
|
+
@context = @context.merge(context)
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear_context
|
28
|
+
@context = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def trace(message = nil, context: {}, &block)
|
32
|
+
log_with_block(Logging::LEVELS[:TRACE], message, context, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def debug(message = nil, context: {}, &block)
|
36
|
+
log_with_block(Logging::LEVELS[:DEBUG], message, context, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def info(message = nil, context: {}, &block)
|
40
|
+
log_with_block(Logging::LEVELS[:INFO], message, context, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def warn(message = nil, context: {}, &block)
|
44
|
+
log_with_block(Logging::LEVELS[:WARN], message, context, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def error(message = nil, context: {}, &block)
|
48
|
+
log_with_block(Logging::LEVELS[:ERROR], message, context, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fatal(message = nil, context: {}, &block)
|
52
|
+
log_with_block(Logging::LEVELS[:FATAL], message, context, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def security(message = nil, context: {}, &block)
|
56
|
+
log_with_block(Logging::LEVELS[:SECURITY], message, context, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def level
|
60
|
+
@core.configuration.level_for(@name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def level_enabled?(level)
|
64
|
+
level >= self.level
|
65
|
+
end
|
66
|
+
|
67
|
+
def trace?
|
68
|
+
level_enabled?(Logging::LEVELS[:TRACE])
|
69
|
+
end
|
70
|
+
|
71
|
+
def debug?
|
72
|
+
level_enabled?(Logging::LEVELS[:DEBUG])
|
73
|
+
end
|
74
|
+
|
75
|
+
def info?
|
76
|
+
level_enabled?(Logging::LEVELS[:INFO])
|
77
|
+
end
|
78
|
+
|
79
|
+
def warn?
|
80
|
+
level_enabled?(Logging::LEVELS[:WARN])
|
81
|
+
end
|
82
|
+
|
83
|
+
def error?
|
84
|
+
level_enabled?(Logging::LEVELS[:ERROR])
|
85
|
+
end
|
86
|
+
|
87
|
+
def fatal?
|
88
|
+
level_enabled?(Logging::LEVELS[:FATAL])
|
89
|
+
end
|
90
|
+
|
91
|
+
def security?
|
92
|
+
level_enabled?(Logging::LEVELS[:SECURITY])
|
93
|
+
end
|
94
|
+
|
95
|
+
def measure(message, context: {}, level: :info, &block)
|
96
|
+
start_time = Time.now
|
97
|
+
result = nil
|
98
|
+
error = nil
|
99
|
+
|
100
|
+
begin
|
101
|
+
result = block.call
|
102
|
+
rescue StandardError => e
|
103
|
+
error = e
|
104
|
+
raise
|
105
|
+
ensure
|
106
|
+
duration = Time.now - start_time
|
107
|
+
measure_context = context.merge(
|
108
|
+
duration_ms: (duration * 1000).round(2),
|
109
|
+
success: error.nil?
|
110
|
+
)
|
111
|
+
measure_context[:error] = error.class.name if error
|
112
|
+
|
113
|
+
send(level, "#{message} completed", context: measure_context)
|
114
|
+
end
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def log_with_block(level, message, context, &block)
|
122
|
+
return unless level_enabled?(level)
|
123
|
+
|
124
|
+
message = block.call if block_given?
|
125
|
+
|
126
|
+
full_context = @context.merge(context)
|
127
|
+
@core.log(level, @name, message, full_context)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
class Configuration
|
8
|
+
DEFAULT_CONFIG = {
|
9
|
+
level: "INFO",
|
10
|
+
format: "text",
|
11
|
+
output: "console",
|
12
|
+
components: {},
|
13
|
+
console: {
|
14
|
+
colorize: true,
|
15
|
+
include_timestamp: true,
|
16
|
+
include_thread: false
|
17
|
+
},
|
18
|
+
file: {
|
19
|
+
path: nil,
|
20
|
+
rotation: "daily",
|
21
|
+
max_size: "100MB",
|
22
|
+
max_files: 7
|
23
|
+
}
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :config
|
27
|
+
|
28
|
+
def initialize(config = {})
|
29
|
+
@config = deep_merge(DEFAULT_CONFIG, normalize_config(config))
|
30
|
+
validate_config!
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_file(path)
|
34
|
+
config = YAML.load_file(path)
|
35
|
+
new(config["logging"] || config)
|
36
|
+
rescue StandardError => e
|
37
|
+
raise ConfigurationError, "Failed to load configuration from #{path}: #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.from_env
|
41
|
+
config = {}
|
42
|
+
|
43
|
+
config[:level] = ENV["VECTORMCP_LOG_LEVEL"] if ENV["VECTORMCP_LOG_LEVEL"]
|
44
|
+
config[:format] = ENV["VECTORMCP_LOG_FORMAT"] if ENV["VECTORMCP_LOG_FORMAT"]
|
45
|
+
config[:output] = ENV["VECTORMCP_LOG_OUTPUT"] if ENV["VECTORMCP_LOG_OUTPUT"]
|
46
|
+
|
47
|
+
config[:file] = { path: ENV["VECTORMCP_LOG_FILE_PATH"] } if ENV["VECTORMCP_LOG_FILE_PATH"]
|
48
|
+
|
49
|
+
new(config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def level_for(component)
|
53
|
+
component_level = @config[:components][component.to_s]
|
54
|
+
level_value = component_level || @config[:level]
|
55
|
+
Logging.level_value(level_value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_component_level(component, level)
|
59
|
+
@config[:components][component.to_s] = if level.is_a?(Integer)
|
60
|
+
Logging.level_name(level)
|
61
|
+
else
|
62
|
+
level.to_s.upcase
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def component_config(component_name)
|
67
|
+
@config[:components][component_name.to_s] || {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def console_config
|
71
|
+
@config[:console]
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_config
|
75
|
+
@config[:file]
|
76
|
+
end
|
77
|
+
|
78
|
+
def format
|
79
|
+
@config[:format]
|
80
|
+
end
|
81
|
+
|
82
|
+
def output
|
83
|
+
@config[:output]
|
84
|
+
end
|
85
|
+
|
86
|
+
def configure(&)
|
87
|
+
instance_eval(&) if block_given?
|
88
|
+
validate_config!
|
89
|
+
end
|
90
|
+
|
91
|
+
def level(new_level)
|
92
|
+
@config[:level] = new_level.to_s.upcase
|
93
|
+
end
|
94
|
+
|
95
|
+
def component(name, level:)
|
96
|
+
@config[:components][name.to_s] = level.to_s.upcase
|
97
|
+
end
|
98
|
+
|
99
|
+
def console(options = {})
|
100
|
+
@config[:console].merge!(options)
|
101
|
+
end
|
102
|
+
|
103
|
+
def file(options = {})
|
104
|
+
@config[:file].merge!(options)
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_h
|
108
|
+
@config.dup
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def normalize_config(config)
|
114
|
+
case config
|
115
|
+
when Hash
|
116
|
+
config.transform_keys(&:to_sym)
|
117
|
+
when String
|
118
|
+
{ level: config }
|
119
|
+
else
|
120
|
+
{}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def deep_merge(hash1, hash2)
|
125
|
+
result = hash1.dup
|
126
|
+
hash2.each do |key, value|
|
127
|
+
result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
|
128
|
+
deep_merge(result[key], value)
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_config!
|
137
|
+
validate_level!(@config[:level])
|
138
|
+
@config[:components].each_value do |level|
|
139
|
+
validate_level!(level)
|
140
|
+
end
|
141
|
+
|
142
|
+
raise ConfigurationError, "Invalid format: #{@config[:format]}" unless %w[text json].include?(@config[:format])
|
143
|
+
|
144
|
+
return if %w[console file both].include?(@config[:output])
|
145
|
+
|
146
|
+
raise ConfigurationError, "Invalid output: #{@config[:output]}"
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_level!(level)
|
150
|
+
return if Logging::LEVELS.key?(level.to_s.upcase.to_sym)
|
151
|
+
|
152
|
+
raise ConfigurationError, "Invalid log level: #{level}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Constants
|
6
|
+
# JSON serialization limits
|
7
|
+
MAX_SERIALIZATION_DEPTH = 5
|
8
|
+
MAX_ARRAY_SERIALIZATION_DEPTH = 3
|
9
|
+
MAX_ARRAY_ELEMENTS_TO_SERIALIZE = 10
|
10
|
+
|
11
|
+
# Text formatting limits
|
12
|
+
DEFAULT_MAX_MESSAGE_LENGTH = 1000
|
13
|
+
DEFAULT_COMPONENT_WIDTH = 20
|
14
|
+
DEFAULT_LEVEL_WIDTH = 8
|
15
|
+
TRUNCATION_SUFFIX_LENGTH = 4 # for "..."
|
16
|
+
|
17
|
+
# ISO timestamp precision
|
18
|
+
TIMESTAMP_PRECISION = 3 # milliseconds
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -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
|