Class: A2A::Client::Middleware::LoggingInterceptor

Inherits:
Object
  • Object
show all
Defined in:
lib/a2a/client/middleware/logging_interceptor.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(logger: nil, log_level: :info, log_requests: true, log_responses: true, log_errors: true, mask_sensitive: true) ⇒ LoggingInterceptor

Initialize logging interceptor

Parameters:

  • logger (Logger, nil) (defaults to: nil)

    Logger instance (creates default if nil)

  • log_level (Symbol) (defaults to: :info)

    Log level (:debug, :info, :warn, :error)

  • log_requests (Boolean) (defaults to: true)

    Whether to log requests (default: true)

  • log_responses (Boolean) (defaults to: true)

    Whether to log responses (default: true)

  • log_errors (Boolean) (defaults to: true)

    Whether to log errors (default: true)

  • mask_sensitive (Boolean) (defaults to: true)

    Whether to mask sensitive data (default: true)



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 27

def initialize(logger: nil, log_level: :info, log_requests: true,
               log_responses: true, log_errors: true, mask_sensitive: true)
  @logger = logger || create_default_logger
  @log_level = log_level
  @log_requests = log_requests
  @log_responses = log_responses
  @log_errors = log_errors
  @mask_sensitive = mask_sensitive

  validate_configuration!
end

Instance Attribute Details

#log_errorsObject (readonly)

Returns the value of attribute log_errors.



16
17
18
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 16

def log_errors
  @log_errors
end

#log_levelObject (readonly)

Returns the value of attribute log_level.



16
17
18
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 16

def log_level
  @log_level
end

#log_requestsObject (readonly)

Returns the value of attribute log_requests.



16
17
18
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 16

def log_requests
  @log_requests
end

#log_responsesObject (readonly)

Returns the value of attribute log_responses.



16
17
18
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 16

def log_responses
  @log_responses
end

#loggerObject (readonly)

Returns the value of attribute logger.



16
17
18
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 16

def logger
  @logger
end

Instance Method Details

#call(request, context, next_middleware) ⇒ Object

Execute request with logging

Parameters:

  • request (Object)

    The request object

  • context (Hash)

    Request context

  • next_middleware (Proc)

    Next middleware in chain

Returns:

  • (Object)

    Response from next middleware



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 46

def call(request, context, next_middleware)
  request_id = context[:request_id] || generate_request_id
  context[:request_id] = request_id

  start_time = Time.now

  log_request(request, context) if @log_requests

  begin
    response = next_middleware.call(request, context)

    duration = Time.now - start_time
    log_response(response, context, duration) if @log_responses

    response
  rescue StandardError => e
    duration = Time.now - start_time
    log_error(e, context, duration) if @log_errors
    raise e
  end
end

#create_default_loggerLogger (private)

Create default logger

Returns:

  • (Logger)

    Default logger instance



138
139
140
141
142
143
144
145
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 138

def create_default_logger
  logger = Logger.new($stdout)
  logger.level = Logger::INFO
  logger.formatter = proc do |severity, datetime, _progname, msg|
    "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
  end
  logger
end

#extract_body(request) ⇒ String, ... (private)

Extract body from request

Parameters:

  • request (Object)

    The request object

Returns:

  • (String, Hash, nil)

    Request body



212
213
214
215
216
217
218
219
220
221
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 212

def extract_body(request)
  if request.respond_to?(:body)
    body = request.body
    return parse_json_body(body) if body.is_a?(String)

    body
  elsif request.respond_to?(:to_h)
    request.to_h
  end
end

#extract_headers(request) ⇒ Hash (private)

Extract headers from request

Parameters:

  • request (Object)

    The request object

Returns:

  • (Hash)

    Request headers



199
200
201
202
203
204
205
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 199

def extract_headers(request)
  if request.respond_to?(:headers)
    request.headers.to_h
  else
    {}
  end
end

#extract_method(request) ⇒ String? (private)

Extract method from request

Parameters:

  • request (Object)

    The request object

Returns:

  • (String, nil)

    HTTP method or JSON-RPC method



173
174
175
176
177
178
179
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 173

def extract_method(request)
  if request.respond_to?(:method)
    request.method
  elsif request.respond_to?(:[])
    request["method"] || request[:method]
  end
end

#extract_response_body(response) ⇒ String, ... (private)

Extract body from response

Parameters:

  • response (Object)

    The response object

Returns:

  • (String, Hash, nil)

    Response body



254
255
256
257
258
259
260
261
262
263
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 254

def extract_response_body(response)
  if response.respond_to?(:body)
    body = response.body
    return parse_json_body(body) if body.is_a?(String)

    body
  elsif response.respond_to?(:to_h)
    response.to_h
  end
end

#extract_response_headers(response) ⇒ Hash (private)

Extract headers from response

Parameters:

  • response (Object)

    The response object

Returns:

  • (Hash)

    Response headers



241
242
243
244
245
246
247
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 241

def extract_response_headers(response)
  if response.respond_to?(:headers)
    response.headers.to_h
  else
    {}
  end
end

#extract_status(response) ⇒ Integer, ... (private)

Extract status from response

Parameters:

  • response (Object)

    The response object

Returns:

  • (Integer, String, nil)

    Response status



228
229
230
231
232
233
234
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 228

def extract_status(response)
  if response.respond_to?(:status)
    response.status
  elsif response.respond_to?(:[])
    response["status"] || response[:status]
  end
end

#extract_url(request) ⇒ String? (private)

Extract URL from request

Parameters:

  • request (Object)

    The request object

Returns:

  • (String, nil)

    Request URL



186
187
188
189
190
191
192
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 186

def extract_url(request)
  if request.respond_to?(:url)
    request.url
  elsif request.respond_to?(:uri)
    request.uri.to_s
  end
end

#format_log_message(title, data) ⇒ String (private)

Format log message

Parameters:

  • title (String)

    Log message title

  • data (Hash)

    Log data

Returns:

  • (String)

    Formatted log message



162
163
164
165
166
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 162

def format_log_message(title, data)
  "#{title}: #{JSON.pretty_generate(data)}"
rescue JSON::GeneratorError
  "#{title}: #{data.inspect}"
end

#generate_request_idString (private)

Generate a unique request ID

Returns:

  • (String)

    Request ID



151
152
153
154
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 151

def generate_request_id
  require "securerandom"
  SecureRandom.hex(8)
end

#log_error(error, context, duration) ⇒ Object

Log an error

Parameters:

  • error (Exception)

    The error that occurred

  • context (Hash)

    Request context

  • duration (Float)

    Request duration in seconds



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 116

def log_error(error, context, duration)
  log_data = {
    type: "error",
    request_id: context[:request_id],
    timestamp: Time.now.utc.iso8601,
    duration_ms: (duration * 1000).round(2),
    error_class: error.class.name,
    error_message: error.message,
    error_code: error.respond_to?(:code) ? error.code : nil,
    backtrace: error.backtrace&.first(10),
    retry_attempt: context[:retry_attempt]
  }

  @logger.error(format_log_message("A2A Error", log_data))
end

#log_request(request, context) ⇒ Object

Log a request

Parameters:

  • request (Object)

    The request object

  • context (Hash)

    Request context



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 73

def log_request(request, context)
  log_data = {
    type: "request",
    request_id: context[:request_id],
    timestamp: Time.now.utc.iso8601,
    method: extract_method(request),
    url: extract_url(request),
    headers: mask_headers(extract_headers(request)),
    body: mask_body(extract_body(request)),
    context: sanitize_context(context)
  }

  @logger.send(@log_level, format_log_message("A2A Request", log_data))
end

#log_response(response, context, duration) ⇒ Object

Log a response

Parameters:

  • response (Object)

    The response object

  • context (Hash)

    Request context

  • duration (Float)

    Request duration in seconds



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 94

def log_response(response, context, duration)
  log_data = {
    type: "response",
    request_id: context[:request_id],
    timestamp: Time.now.utc.iso8601,
    duration_ms: (duration * 1000).round(2),
    status: extract_status(response),
    headers: mask_headers(extract_response_headers(response)),
    body: mask_body(extract_response_body(response)),
    success: response_successful?(response)
  }

  level = response_successful?(response) ? @log_level : :warn
  @logger.send(level, format_log_message("A2A Response", log_data))
end

#mask_body(body) ⇒ Object (private)

Mask sensitive body content

Parameters:

  • body (Object)

    Body to mask

Returns:

  • (Object)

    Masked body



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 320

def mask_body(body)
  return body unless @mask_sensitive
  return body unless body.is_a?(Hash)

  masked = body.dup

  # Mask common sensitive fields
  %w[password secret token key credential].each do |field|
    masked.each do |k, v|
      masked[k] = mask_value(v) if k.to_s.downcase.include?(field)
    end
  end

  masked
end

#mask_headers(headers) ⇒ Hash (private)

Mask sensitive headers

Parameters:

  • headers (Hash)

    Headers to mask

Returns:

  • (Hash)

    Masked headers



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 298

def mask_headers(headers)
  return headers unless @mask_sensitive

  masked = headers.dup

  # Mask authorization headers
  masked.each do |key, value|
    next unless key.to_s.downcase.include?("authorization") ||
                key.to_s.downcase.include?("token") ||
                key.to_s.downcase.include?("key")

    masked[key] = mask_value(value)
  end

  masked
end

#mask_value(value) ⇒ String (private)

Mask a sensitive value

Parameters:

  • value (String)

    Value to mask

Returns:

  • (String)

    Masked value



341
342
343
344
345
346
347
348
349
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 341

def mask_value(value)
  return "[nil]" if value.nil?
  return "[empty]" if value.to_s.empty?

  str = value.to_s
  return str if str.length <= 8

  "#{str[0..3]}#{'*' * (str.length - 8)}#{str[-4..]}"
end

#parse_json_body(body) ⇒ Hash, String (private)

Parse JSON body

Parameters:

  • body (String)

    JSON body string

Returns:

  • (Hash, String)

    Parsed JSON or original string



287
288
289
290
291
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 287

def parse_json_body(body)
  JSON.parse(body)
rescue JSON::ParserError
  body
end

#response_successful?(response) ⇒ Boolean (private)

Check if response was successful

Parameters:

  • response (Object)

    The response object

Returns:

  • (Boolean)

    True if successful



270
271
272
273
274
275
276
277
278
279
280
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 270

def response_successful?(response)
  if response.respond_to?(:success?)
    response.success?
  elsif response.respond_to?(:status)
    (200..299).cover?(response.status)
  elsif response.respond_to?(:[])
    !response["error"] && !response[:error]
  else
    true # Assume success if we can't determine
  end
end

#sanitize_context(context) ⇒ Hash (private)

Sanitize context for logging

Parameters:

  • context (Hash)

    Context to sanitize

Returns:

  • (Hash)

    Sanitized context



356
357
358
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 356

def sanitize_context(context)
  context.reject { |k, _v| k.to_s.include?("password") || k.to_s.include?("secret") }
end

#validate_configuration!Object (private)

Validate configuration

Raises:

  • (ArgumentError)


362
363
364
365
366
367
# File 'lib/a2a/client/middleware/logging_interceptor.rb', line 362

def validate_configuration!
  valid_levels = i[debug info warn error]
  return if valid_levels.include?(@log_level)

  raise ArgumentError, "Invalid log level: #{@log_level}. Must be one of: #{valid_levels.join(', ')}"
end