Module: Coolhand::BaseInterceptor
- Included in:
- NetHttpInterceptor
- Defined in:
- lib/coolhand/base_interceptor.rb
Overview
Base module with common functionality for all interceptors
Class Method Summary collapse
- .clean_request_headers(headers) ⇒ Object
- .clean_response_headers(headers) ⇒ Object
- .extract_response_data(response) ⇒ Object
- .extract_usage_metadata(usage) ⇒ Object
-
.normalize_header_value(value) ⇒ Object
Helper: convert arrays -> joined string, otherwise to_s.
- .parse_json(string) ⇒ Object
- .sanitize_headers(headers) ⇒ Object
- .sanitize_url(url) ⇒ Object
- .send_complete_request_log(request_id:, method:, url:, request_headers:, request_body:, response_headers:, response_body:, status_code:, start_time:, end_time:, duration_ms:, is_streaming:) ⇒ Object
Class Method Details
.clean_request_headers(headers) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/coolhand/base_interceptor.rb', line 78 def clean_request_headers(headers) cleaned = headers.dup # Remove sensitive headers cleaned.delete("Authorization") cleaned.delete("authorization") cleaned.delete("x-api-key") cleaned.delete("X-API-Key") cleaned end |
.clean_response_headers(headers) ⇒ Object
90 91 92 93 94 |
# File 'lib/coolhand/base_interceptor.rb', line 90 def clean_response_headers(headers) # Response headers typically don't contain sensitive data # but we can filter if needed headers.dup end |
.extract_response_data(response) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/coolhand/base_interceptor.rb', line 8 def extract_response_data(response) case response when Hash response when Struct response.to_h else # Handle streaming responses - these are often enumerator objects # that can't be serialized directly if response.class.name.include?("Stream") || response.respond_to?(:each) { response_type: "streaming", class: response.class.name, note: "Streaming response - content captured during enumeration" } elsif response.respond_to?(:to_h) begin response.to_h rescue StandardError => e { serialization_error: e., class: response.class.name, raw_response: response.to_s } end else # Extract content and token usage information response_data = {} # Get content response_data[:content] = response.content if response.respond_to?(:content) # Extract token usage information response_data[:usage] = (response.usage) if response.respond_to?(:usage) # Extract model information response_data[:model] = response.model if response.respond_to?(:model) # Extract role information response_data[:role] = response.role if response.respond_to?(:role) # Extract ID if available response_data[:id] = response.id if response.respond_to?(:id) # Extract stop reason if available response_data[:stop_reason] = response.stop_reason if response.respond_to?(:stop_reason) # Add class info for debugging response_data[:class] = response.class.name response_data.empty? ? { raw_response: response.to_s, class: response.class.name } : response_data end end end |
.extract_usage_metadata(usage) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/coolhand/base_interceptor.rb', line 63 def (usage) if usage.respond_to?(:to_h) usage.to_h elsif usage.is_a?(Hash) usage else # Extract individual usage fields usage_data = {} usage_data[:input_tokens] = usage.input_tokens if usage.respond_to?(:input_tokens) usage_data[:output_tokens] = usage.output_tokens if usage.respond_to?(:output_tokens) usage_data[:total_tokens] = usage_data[:input_tokens].to_i + usage_data[:output_tokens].to_i usage_data end end |
.normalize_header_value(value) ⇒ Object
Helper: convert arrays -> joined string, otherwise to_s
146 147 148 149 150 151 152 |
# File 'lib/coolhand/base_interceptor.rb', line 146 def normalize_header_value(value) if value.is_a?(Array) value.join(", ") else value.to_s end end |
.parse_json(string) ⇒ Object
207 208 209 210 211 |
# File 'lib/coolhand/base_interceptor.rb', line 207 def parse_json(string) JSON.parse(string) rescue JSON::ParserError, TypeError string end |
.sanitize_headers(headers) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/coolhand/base_interceptor.rb', line 96 def sanitize_headers(headers) return {} if headers.nil? # Normalize various header-like objects into a Hash{String => String} raw = if headers.is_a?(Hash) headers.transform_keys(&:to_s).transform_values { |v| normalize_header_value(v) } elsif headers.respond_to?(:to_hash) begin headers.to_hash.transform_keys(&:to_s).transform_values { |v| normalize_header_value(v) } rescue StandardError # fall through to other enumeration strategies nil end elsif headers.respond_to?(:each_header) h = {} headers.each_header { |k, v| h[k.to_s] = normalize_header_value(v) } h elsif headers.respond_to?(:each) h = {} headers.each { |k, v| h[k.to_s] = normalize_header_value(v) } h else { "raw" => headers.to_s } end raw ||= {} # in case to_hash raised and nothing was built sanitized = raw.dup sanitized_keys = %w[openai-api-key api-key x-api-key x-goog-api-key] sanitized.each do |k, v| next if v.nil? key_down = k.to_s.downcase if key_down == "authorization" sanitized[k] = if v.to_s.match?(/\ABearer\s+/i) v.to_s.gsub(/\ABearer\s+.+/i, "Bearer [REDACTED]") else "[REDACTED]" end elsif sanitized_keys.include?(key_down) sanitized[k] = "[REDACTED]" end end sanitized end |
.sanitize_url(url) ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/coolhand/base_interceptor.rb', line 154 def sanitize_url(url) uri = URI.parse(url) return url unless uri.query sensitive = %w[key api_key apikey token access_token secret] params = URI.decode_www_form(uri.query) redacted = false params.map! do |n, v| if sensitive.include?(n.downcase) redacted = true [n, "[REDACTED]"] else [n, v] end end if redacted uri.query = URI.encode_www_form(params) uri.to_s else url end rescue URI::InvalidURIError url end |
.send_complete_request_log(request_id:, method:, url:, request_headers:, request_body:, response_headers:, response_body:, status_code:, start_time:, end_time:, duration_ms:, is_streaming:) ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/coolhand/base_interceptor.rb', line 180 def send_complete_request_log(request_id:, method:, url:, request_headers:, request_body:, response_headers:, response_body:, status_code:, start_time:, end_time:, duration_ms:, is_streaming:) request_data = { raw_request: { id: request_id, timestamp: start_time.iso8601, method: method.to_s.downcase, url: sanitize_url(url), headers: request_headers, request_body: request_body, response_headers: response_headers, response_body: response_body, status_code: status_code, duration_ms: duration_ms, completed_at: end_time.iso8601, is_streaming: is_streaming } } api_service = Coolhand::ApiService.new api_service.send_llm_request_log(request_data) Coolhand.log "📤 Sent complete request/response log for #{request_id} (duration: #{duration_ms}ms)" rescue StandardError => e Coolhand.log "❌ Error sending complete request log: #{e.}" end |