buda_api 1.0.0
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 +7 -0
- data/.gitignore +57 -0
- data/.rspec +3 -0
- data/.rubocop.yml +49 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +526 -0
- data/Rakefile +37 -0
- data/buda_api.gemspec +38 -0
- data/examples/.env.example +11 -0
- data/examples/authenticated_api_example.rb +213 -0
- data/examples/error_handling_example.rb +221 -0
- data/examples/public_api_example.rb +142 -0
- data/examples/trading_bot_example.rb +279 -0
- data/lib/buda_api/authenticated_client.rb +403 -0
- data/lib/buda_api/client.rb +248 -0
- data/lib/buda_api/constants.rb +163 -0
- data/lib/buda_api/errors.rb +60 -0
- data/lib/buda_api/logger.rb +99 -0
- data/lib/buda_api/models.rb +365 -0
- data/lib/buda_api/public_client.rb +231 -0
- data/lib/buda_api/version.rb +5 -0
- data/lib/buda_api.rb +81 -0
- metadata +194 -0
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "faraday/retry"
|
5
|
+
require "json"
|
6
|
+
require "openssl"
|
7
|
+
require "base64"
|
8
|
+
require "time"
|
9
|
+
|
10
|
+
module BudaApi
|
11
|
+
# Base client class providing core HTTP functionality and error handling
|
12
|
+
class Client
|
13
|
+
include Constants
|
14
|
+
|
15
|
+
attr_reader :base_url, :timeout, :retries, :debug_mode
|
16
|
+
|
17
|
+
DEFAULT_OPTIONS = {
|
18
|
+
base_url: "https://www.buda.com/api/v2/",
|
19
|
+
timeout: 30,
|
20
|
+
retries: 3,
|
21
|
+
debug_mode: false
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
26
|
+
@base_url = @options[:base_url]
|
27
|
+
@timeout = @options[:timeout]
|
28
|
+
@retries = @options[:retries]
|
29
|
+
@debug_mode = @options[:debug_mode]
|
30
|
+
|
31
|
+
setup_logger
|
32
|
+
setup_http_client
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Make a GET request
|
38
|
+
# @param path [String] API endpoint path
|
39
|
+
# @param params [Hash] query parameters
|
40
|
+
# @return [Hash] parsed response body
|
41
|
+
def get(path, params = {})
|
42
|
+
make_request(:get, path, params: params)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Make a POST request
|
46
|
+
# @param path [String] API endpoint path
|
47
|
+
# @param body [Hash] request body
|
48
|
+
# @param params [Hash] query parameters
|
49
|
+
# @return [Hash] parsed response body
|
50
|
+
def post(path, body: {}, params: {})
|
51
|
+
make_request(:post, path, body: body, params: params)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Make a PUT request
|
55
|
+
# @param path [String] API endpoint path
|
56
|
+
# @param body [Hash] request body
|
57
|
+
# @param params [Hash] query parameters
|
58
|
+
# @return [Hash] parsed response body
|
59
|
+
def put(path, body: {}, params: {})
|
60
|
+
make_request(:put, path, body: body, params: params)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Make a DELETE request
|
64
|
+
# @param path [String] API endpoint path
|
65
|
+
# @param params [Hash] query parameters
|
66
|
+
# @return [Hash] parsed response body
|
67
|
+
def delete(path, params = {})
|
68
|
+
make_request(:delete, path, params: params)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def setup_logger
|
74
|
+
log_level = @debug_mode ? :debug : BudaApi.configuration.logger_level
|
75
|
+
BudaApi::Logger.setup(level: log_level)
|
76
|
+
end
|
77
|
+
|
78
|
+
def setup_http_client
|
79
|
+
@http_client = Faraday.new(url: @base_url) do |f|
|
80
|
+
f.options.timeout = @timeout
|
81
|
+
f.options.open_timeout = @timeout / 2
|
82
|
+
|
83
|
+
f.request :json
|
84
|
+
f.request :retry,
|
85
|
+
max: @retries,
|
86
|
+
interval: 0.5,
|
87
|
+
backoff_factor: 2,
|
88
|
+
retry_statuses: [408, 429, 500, 502, 503, 504],
|
89
|
+
methods: [:get, :post, :put, :delete]
|
90
|
+
|
91
|
+
f.response :json, content_type: /\bjson$/
|
92
|
+
f.adapter :net_http
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_request(method, path, body: nil, params: {}, headers: {})
|
97
|
+
start_time = Time.now
|
98
|
+
full_url = build_url(path, params)
|
99
|
+
|
100
|
+
# Add authentication headers if this is an authenticated client
|
101
|
+
headers = add_authentication_headers(method, path, body, headers) if respond_to?(:add_authentication_headers, true)
|
102
|
+
|
103
|
+
BudaApi::Logger.log_request(method, full_url, headers: headers, body: body)
|
104
|
+
|
105
|
+
response = @http_client.public_send(method) do |req|
|
106
|
+
req.url path
|
107
|
+
req.params = params if params.any?
|
108
|
+
req.headers.merge!(headers)
|
109
|
+
req.body = body.to_json if body && !body.empty?
|
110
|
+
end
|
111
|
+
|
112
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
113
|
+
BudaApi::Logger.log_response(
|
114
|
+
response.status,
|
115
|
+
headers: response.headers,
|
116
|
+
body: response.body,
|
117
|
+
duration: duration
|
118
|
+
)
|
119
|
+
|
120
|
+
handle_response(response)
|
121
|
+
|
122
|
+
rescue Faraday::ConnectionFailed => e
|
123
|
+
error = ConnectionError.new("Connection failed: #{e.message}")
|
124
|
+
BudaApi::Logger.log_error(error, context: { method: method, path: path })
|
125
|
+
raise error
|
126
|
+
|
127
|
+
rescue Faraday::TimeoutError => e
|
128
|
+
error = TimeoutError.new("Request timed out: #{e.message}")
|
129
|
+
BudaApi::Logger.log_error(error, context: { method: method, path: path })
|
130
|
+
raise error
|
131
|
+
|
132
|
+
rescue StandardError => e
|
133
|
+
error = ApiError.new("Unexpected error: #{e.message}")
|
134
|
+
BudaApi::Logger.log_error(error, context: { method: method, path: path })
|
135
|
+
raise error
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_url(path, params)
|
139
|
+
url = @base_url + path.to_s
|
140
|
+
return url if params.empty?
|
141
|
+
|
142
|
+
query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
|
143
|
+
"#{url}?#{query_string}"
|
144
|
+
end
|
145
|
+
|
146
|
+
def handle_response(response)
|
147
|
+
case response.status
|
148
|
+
when HttpStatus::OK, HttpStatus::CREATED
|
149
|
+
validate_and_parse_response(response)
|
150
|
+
when HttpStatus::BAD_REQUEST
|
151
|
+
handle_error_response(BadRequestError, response, "Bad request")
|
152
|
+
when HttpStatus::UNAUTHORIZED
|
153
|
+
handle_error_response(AuthenticationError, response, "Authentication failed")
|
154
|
+
when HttpStatus::FORBIDDEN
|
155
|
+
handle_error_response(AuthorizationError, response, "Forbidden - insufficient permissions")
|
156
|
+
when HttpStatus::NOT_FOUND
|
157
|
+
handle_error_response(NotFoundError, response, "Resource not found")
|
158
|
+
when HttpStatus::UNPROCESSABLE_ENTITY
|
159
|
+
handle_error_response(ValidationError, response, "Validation failed")
|
160
|
+
when HttpStatus::RATE_LIMITED
|
161
|
+
handle_error_response(RateLimitError, response, "Rate limit exceeded")
|
162
|
+
when HttpStatus::INTERNAL_SERVER_ERROR..HttpStatus::GATEWAY_TIMEOUT
|
163
|
+
handle_error_response(ServerError, response, "Server error")
|
164
|
+
else
|
165
|
+
handle_error_response(ApiError, response, "Unknown error")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def validate_and_parse_response(response)
|
170
|
+
body = response.body
|
171
|
+
|
172
|
+
if body.nil? || body.empty?
|
173
|
+
raise InvalidResponseError.new("Empty response body", status_code: response.status)
|
174
|
+
end
|
175
|
+
|
176
|
+
unless body.is_a?(Hash)
|
177
|
+
raise InvalidResponseError.new("Invalid response format",
|
178
|
+
status_code: response.status,
|
179
|
+
response_body: body)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Check for API error in successful HTTP response
|
183
|
+
if body.key?("message") && body["message"]&.match?(/error/i)
|
184
|
+
raise ApiError.new(body["message"],
|
185
|
+
status_code: response.status,
|
186
|
+
response_body: body)
|
187
|
+
end
|
188
|
+
|
189
|
+
body
|
190
|
+
end
|
191
|
+
|
192
|
+
def handle_error_response(error_class, response, default_message)
|
193
|
+
error_message = extract_error_message(response.body) || default_message
|
194
|
+
|
195
|
+
raise error_class.new(
|
196
|
+
error_message,
|
197
|
+
status_code: response.status,
|
198
|
+
response_body: response.body,
|
199
|
+
response_headers: response.headers.to_h
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
def extract_error_message(body)
|
204
|
+
return nil unless body.is_a?(Hash)
|
205
|
+
|
206
|
+
# Try different common error message fields
|
207
|
+
%w[message error error_message detail details].each do |field|
|
208
|
+
return body[field] if body[field].is_a?(String) && !body[field].empty?
|
209
|
+
end
|
210
|
+
|
211
|
+
# Handle nested error structures
|
212
|
+
if body["errors"].is_a?(Array) && body["errors"].any?
|
213
|
+
return body["errors"].first if body["errors"].first.is_a?(String)
|
214
|
+
return body["errors"].first["message"] if body["errors"].first.is_a?(Hash)
|
215
|
+
end
|
216
|
+
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
|
220
|
+
# Validate required parameters
|
221
|
+
def validate_required_params(params, required_fields)
|
222
|
+
missing = required_fields.select { |field| params[field].nil? || params[field].to_s.empty? }
|
223
|
+
return if missing.empty?
|
224
|
+
|
225
|
+
raise ValidationError, "Missing required parameters: #{missing.join(', ')}"
|
226
|
+
end
|
227
|
+
|
228
|
+
# Validate parameter values against allowed options
|
229
|
+
def validate_param_values(params, validations)
|
230
|
+
validations.each do |param, allowed_values|
|
231
|
+
value = params[param]
|
232
|
+
next if value.nil?
|
233
|
+
|
234
|
+
unless allowed_values.include?(value)
|
235
|
+
raise ValidationError,
|
236
|
+
"Invalid #{param}: '#{value}'. Must be one of: #{allowed_values.join(', ')}"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Clean and normalize parameters
|
242
|
+
def normalize_params(params)
|
243
|
+
params.reject { |_, v| v.nil? || v.to_s.empty? }
|
244
|
+
.transform_keys(&:to_s)
|
245
|
+
.transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BudaApi
|
4
|
+
# Constants and enums for the Buda API
|
5
|
+
module Constants
|
6
|
+
# Supported currencies
|
7
|
+
module Currency
|
8
|
+
ARS = "ARS" # Argentine Peso
|
9
|
+
BCH = "BCH" # Bitcoin Cash
|
10
|
+
BTC = "BTC" # Bitcoin
|
11
|
+
CLP = "CLP" # Chilean Peso
|
12
|
+
COP = "COP" # Colombian Peso
|
13
|
+
ETH = "ETH" # Ethereum
|
14
|
+
LTC = "LTC" # Litecoin
|
15
|
+
PEN = "PEN" # Peruvian Sol
|
16
|
+
USDC = "USDC" # USD Coin
|
17
|
+
|
18
|
+
ALL = [ARS, BCH, BTC, CLP, COP, ETH, LTC, PEN, USDC].freeze
|
19
|
+
|
20
|
+
# Currency decimal places
|
21
|
+
DECIMALS = {
|
22
|
+
ARS => 2,
|
23
|
+
BCH => 8,
|
24
|
+
BTC => 8,
|
25
|
+
CLP => 2,
|
26
|
+
COP => 2,
|
27
|
+
ETH => 9,
|
28
|
+
LTC => 8,
|
29
|
+
PEN => 2,
|
30
|
+
USDC => 2
|
31
|
+
}.freeze
|
32
|
+
end
|
33
|
+
|
34
|
+
# Supported trading pairs
|
35
|
+
module Market
|
36
|
+
# Bitcoin pairs
|
37
|
+
BTC_ARS = "BTC-ARS"
|
38
|
+
BTC_CLP = "BTC-CLP"
|
39
|
+
BTC_COP = "BTC-COP"
|
40
|
+
BTC_PEN = "BTC-PEN"
|
41
|
+
BTC_USDC = "BTC-USDC"
|
42
|
+
|
43
|
+
# Ethereum pairs
|
44
|
+
ETH_ARS = "ETH-ARS"
|
45
|
+
ETH_BTC = "ETH-BTC"
|
46
|
+
ETH_CLP = "ETH-CLP"
|
47
|
+
ETH_COP = "ETH-COP"
|
48
|
+
ETH_PEN = "ETH-PEN"
|
49
|
+
|
50
|
+
# Bitcoin Cash pairs
|
51
|
+
BCH_ARS = "BCH-ARS"
|
52
|
+
BCH_BTC = "BCH-BTC"
|
53
|
+
BCH_CLP = "BCH-CLP"
|
54
|
+
BCH_COP = "BCH-COP"
|
55
|
+
BCH_PEN = "BCH-PEN"
|
56
|
+
|
57
|
+
# Litecoin pairs
|
58
|
+
LTC_ARS = "LTC-ARS"
|
59
|
+
LTC_BTC = "LTC-BTC"
|
60
|
+
LTC_CLP = "LTC-CLP"
|
61
|
+
LTC_COP = "LTC-COP"
|
62
|
+
LTC_PEN = "LTC-PEN"
|
63
|
+
|
64
|
+
# USDC pairs
|
65
|
+
USDC_ARS = "USDC-ARS"
|
66
|
+
USDC_CLP = "USDC-CLP"
|
67
|
+
USDC_COP = "USDC-COP"
|
68
|
+
USDC_PEN = "USDC-PEN"
|
69
|
+
|
70
|
+
ALL = [
|
71
|
+
BTC_ARS, BTC_CLP, BTC_COP, BTC_PEN, BTC_USDC,
|
72
|
+
ETH_ARS, ETH_BTC, ETH_CLP, ETH_COP, ETH_PEN,
|
73
|
+
BCH_ARS, BCH_BTC, BCH_CLP, BCH_COP, BCH_PEN,
|
74
|
+
LTC_ARS, LTC_BTC, LTC_CLP, LTC_COP, LTC_PEN,
|
75
|
+
USDC_ARS, USDC_CLP, USDC_COP, USDC_PEN
|
76
|
+
].freeze
|
77
|
+
end
|
78
|
+
|
79
|
+
# Order types
|
80
|
+
module OrderType
|
81
|
+
ASK = "Ask" # Sell order
|
82
|
+
BID = "Bid" # Buy order
|
83
|
+
|
84
|
+
ALL = [ASK, BID].freeze
|
85
|
+
end
|
86
|
+
|
87
|
+
# Price types for orders
|
88
|
+
module PriceType
|
89
|
+
MARKET = "market"
|
90
|
+
LIMIT = "limit"
|
91
|
+
|
92
|
+
ALL = [MARKET, LIMIT].freeze
|
93
|
+
end
|
94
|
+
|
95
|
+
# Order states
|
96
|
+
module OrderState
|
97
|
+
RECEIVED = "received"
|
98
|
+
PENDING = "pending"
|
99
|
+
TRADED = "traded"
|
100
|
+
CANCELING = "canceling"
|
101
|
+
CANCELED = "canceled"
|
102
|
+
|
103
|
+
ALL = [RECEIVED, PENDING, TRADED, CANCELING, CANCELED].freeze
|
104
|
+
end
|
105
|
+
|
106
|
+
# Quotation types
|
107
|
+
module QuotationType
|
108
|
+
BID_GIVEN_SIZE = "bid_given_size"
|
109
|
+
BID_GIVEN_EARNED_BASE = "bid_given_earned_base"
|
110
|
+
BID_GIVEN_SPENT_QUOTE = "bid_given_spent_quote"
|
111
|
+
ASK_GIVEN_SIZE = "ask_given_size"
|
112
|
+
ASK_GIVEN_EARNED_QUOTE = "ask_given_earned_quote"
|
113
|
+
ASK_GIVEN_SPENT_BASE = "ask_given_spent_base"
|
114
|
+
|
115
|
+
ALL = [
|
116
|
+
BID_GIVEN_SIZE, BID_GIVEN_EARNED_BASE, BID_GIVEN_SPENT_QUOTE,
|
117
|
+
ASK_GIVEN_SIZE, ASK_GIVEN_EARNED_QUOTE, ASK_GIVEN_SPENT_BASE
|
118
|
+
].freeze
|
119
|
+
end
|
120
|
+
|
121
|
+
# Balance event types
|
122
|
+
module BalanceEvent
|
123
|
+
DEPOSIT_CONFIRM = "deposit_confirm"
|
124
|
+
WITHDRAWAL_CONFIRM = "withdrawal_confirm"
|
125
|
+
TRANSACTION = "transaction"
|
126
|
+
TRANSFER_CONFIRMATION = "transfer_confirmation"
|
127
|
+
|
128
|
+
ALL = [DEPOSIT_CONFIRM, WITHDRAWAL_CONFIRM, TRANSACTION, TRANSFER_CONFIRMATION].freeze
|
129
|
+
end
|
130
|
+
|
131
|
+
# Report types
|
132
|
+
module ReportType
|
133
|
+
AVERAGE_PRICES = "average_prices"
|
134
|
+
CANDLESTICK = "candlestick"
|
135
|
+
|
136
|
+
ALL = [AVERAGE_PRICES, CANDLESTICK].freeze
|
137
|
+
end
|
138
|
+
|
139
|
+
# API limits
|
140
|
+
module Limits
|
141
|
+
ORDERS_PER_PAGE = 300
|
142
|
+
TRANSFERS_PER_PAGE = 300
|
143
|
+
DEFAULT_TIMEOUT = 30
|
144
|
+
MAX_RETRIES = 3
|
145
|
+
end
|
146
|
+
|
147
|
+
# HTTP status codes
|
148
|
+
module HttpStatus
|
149
|
+
OK = 200
|
150
|
+
CREATED = 201
|
151
|
+
BAD_REQUEST = 400
|
152
|
+
UNAUTHORIZED = 401
|
153
|
+
FORBIDDEN = 403
|
154
|
+
NOT_FOUND = 404
|
155
|
+
UNPROCESSABLE_ENTITY = 422
|
156
|
+
RATE_LIMITED = 429
|
157
|
+
INTERNAL_SERVER_ERROR = 500
|
158
|
+
BAD_GATEWAY = 502
|
159
|
+
SERVICE_UNAVAILABLE = 503
|
160
|
+
GATEWAY_TIMEOUT = 504
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BudaApi
|
4
|
+
# Custom error classes for the Buda API SDK
|
5
|
+
module Errors
|
6
|
+
# Base error class for all API errors
|
7
|
+
class ApiError < StandardError
|
8
|
+
attr_reader :status_code, :response_body, :response_headers
|
9
|
+
|
10
|
+
def initialize(message, status_code: nil, response_body: nil, response_headers: nil)
|
11
|
+
super(message)
|
12
|
+
@status_code = status_code
|
13
|
+
@response_body = response_body
|
14
|
+
@response_headers = response_headers
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
msg = super
|
19
|
+
msg += " (HTTP #{@status_code})" if @status_code
|
20
|
+
msg
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Authentication related errors
|
25
|
+
class AuthenticationError < ApiError; end
|
26
|
+
|
27
|
+
# Authorization/permission related errors
|
28
|
+
class AuthorizationError < ApiError; end
|
29
|
+
|
30
|
+
# Rate limiting errors
|
31
|
+
class RateLimitError < ApiError; end
|
32
|
+
|
33
|
+
# Invalid request errors (4xx)
|
34
|
+
class BadRequestError < ApiError; end
|
35
|
+
|
36
|
+
# Resource not found errors
|
37
|
+
class NotFoundError < ApiError; end
|
38
|
+
|
39
|
+
# Server errors (5xx)
|
40
|
+
class ServerError < ApiError; end
|
41
|
+
|
42
|
+
# Network/connection related errors
|
43
|
+
class ConnectionError < ApiError; end
|
44
|
+
|
45
|
+
# Request timeout errors
|
46
|
+
class TimeoutError < ApiError; end
|
47
|
+
|
48
|
+
# Invalid response format errors
|
49
|
+
class InvalidResponseError < ApiError; end
|
50
|
+
|
51
|
+
# Configuration errors
|
52
|
+
class ConfigurationError < StandardError; end
|
53
|
+
|
54
|
+
# Validation errors for request parameters
|
55
|
+
class ValidationError < StandardError; end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Include all error classes at the module level for convenience
|
59
|
+
include Errors
|
60
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module BudaApi
|
6
|
+
# Centralized logging functionality for the SDK
|
7
|
+
class Logger
|
8
|
+
LOG_LEVELS = {
|
9
|
+
debug: ::Logger::DEBUG,
|
10
|
+
info: ::Logger::INFO,
|
11
|
+
warn: ::Logger::WARN,
|
12
|
+
error: ::Logger::ERROR,
|
13
|
+
fatal: ::Logger::FATAL
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :logger
|
18
|
+
|
19
|
+
# Initialize the logger
|
20
|
+
def setup(level: :info, output: $stdout)
|
21
|
+
@logger = ::Logger.new(output)
|
22
|
+
@logger.level = LOG_LEVELS[level] || LOG_LEVELS[:info]
|
23
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
24
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity.ljust(5)} BudaApi: #{msg}\n"
|
25
|
+
end
|
26
|
+
@logger
|
27
|
+
end
|
28
|
+
|
29
|
+
# Log debug messages
|
30
|
+
def debug(message)
|
31
|
+
current_logger.debug(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Log info messages
|
35
|
+
def info(message)
|
36
|
+
current_logger.info(message)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Log warning messages
|
40
|
+
def warn(message)
|
41
|
+
current_logger.warn(message)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Log error messages
|
45
|
+
def error(message)
|
46
|
+
current_logger.error(message)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Log fatal messages
|
50
|
+
def fatal(message)
|
51
|
+
current_logger.fatal(message)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Log HTTP requests in debug mode
|
55
|
+
def log_request(method, url, headers: {}, body: nil)
|
56
|
+
return unless debug_enabled?
|
57
|
+
|
58
|
+
debug("→ #{method.upcase} #{url}")
|
59
|
+
debug("→ Headers: #{headers}") if headers&.any?
|
60
|
+
debug("→ Body: #{body}") if body
|
61
|
+
end
|
62
|
+
|
63
|
+
# Log HTTP responses in debug mode
|
64
|
+
def log_response(status, headers: {}, body: nil, duration: nil)
|
65
|
+
return unless debug_enabled?
|
66
|
+
|
67
|
+
debug("← #{status}")
|
68
|
+
debug("← Headers: #{headers}") if headers&.any?
|
69
|
+
debug("← Body: #{truncate_body(body)}") if body
|
70
|
+
debug("← Duration: #{duration}ms") if duration
|
71
|
+
end
|
72
|
+
|
73
|
+
# Log errors with full context
|
74
|
+
def log_error(error, context: {})
|
75
|
+
error_msg = "Error: #{error.class.name} - #{error.message}"
|
76
|
+
error_msg += "\nContext: #{context}" if context&.any?
|
77
|
+
error_msg += "\nBacktrace:\n #{error.backtrace.join("\n ")}" if error.backtrace
|
78
|
+
|
79
|
+
error(error_msg)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def current_logger
|
85
|
+
@logger || setup
|
86
|
+
end
|
87
|
+
|
88
|
+
def debug_enabled?
|
89
|
+
current_logger.level <= ::Logger::DEBUG
|
90
|
+
end
|
91
|
+
|
92
|
+
def truncate_body(body, limit: 1000)
|
93
|
+
return body unless body.is_a?(String) && body.length > limit
|
94
|
+
|
95
|
+
"#{body[0...limit]}... (truncated)"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|