Class: Binocs::Request

Inherits:
ApplicationRecord show all
Defined in:
app/models/binocs/request.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.average_durationObject



207
208
209
# File 'app/models/binocs/request.rb', line 207

def self.average_duration
  average(:duration_ms)&.round(2)
end

.client_identifiersObject

Class methods for statistics



200
201
202
203
204
205
# File 'app/models/binocs/request.rb', line 200

def self.client_identifiers
  where.not(client_identifier: nil)
    .group(:client_identifier)
    .order(Arel.sql("MAX(created_at) DESC"))
    .pluck(:client_identifier)
end

.controllers_listObject



225
226
227
# File 'app/models/binocs/request.rb', line 225

def self.controllers_list
  distinct.pluck(:controller_name).compact.sort
end

.error_rateObject



211
212
213
214
215
# File 'app/models/binocs/request.rb', line 211

def self.error_rate
  return 0 if count.zero?

  ((with_exception.count + by_status_range("5xx").count).to_f / count * 100).round(2)
end

.methods_breakdownObject



217
218
219
# File 'app/models/binocs/request.rb', line 217

def self.methods_breakdown
  group(:method).count
end

.status_breakdownObject



221
222
223
# File 'app/models/binocs/request.rb', line 221

def self.status_breakdown
  group(:status_code).count
end

Instance Method Details

#client_error?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'app/models/binocs/request.rb', line 71

def client_error?
  status_code.present? && status_code >= 400 && status_code < 500
end

#client_labelObject



187
188
189
190
191
192
193
194
195
196
197
# File 'app/models/binocs/request.rb', line 187

def client_label
  return "Unknown" if client_identifier.blank?

  prefix, value = client_identifier.split(":", 2)
  case prefix
  when "session" then "Session #{value.to_s[0, 8]}"
  when "auth" then "Auth #{value.to_s[0, 8]}"
  when "ip" then "IP #{value}"
  else client_identifier
  end
end

#controller_actionObject



138
139
140
141
142
# File 'app/models/binocs/request.rb', line 138

def controller_action
  return nil unless controller_name && action_name

  "#{controller_name}##{action_name}"
end

#default_tagObject



179
180
181
182
183
184
185
# File 'app/models/binocs/request.rb', line 179

def default_tag
  # Fallback: derive tag from controller name
  # V1::CompaniesController -> "Companies"
  return "default" unless controller_name

  controller_name.demodulize.sub(/Controller$/, "").titleize
end

#find_swagger_path_and_tagObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/models/binocs/request.rb', line 155

def find_swagger_path_and_tag
  spec_path = Rails.root.join("swagger/v1/swagger.yaml")
  return [path, default_tag] unless File.exist?(spec_path)

  spec = YAML.load_file(spec_path)

  # Find matching swagger path by converting UUIDs to param placeholders
  spec["paths"]&.each do |swagger_path, methods|
    # Create regex from swagger path: /v1/channels/{channel_uuid} -> /v1/channels/[^/]+
    pattern = swagger_path.gsub(/\{[^}]+\}/, "[^/]+")
    regex = /\A#{pattern}\z/

    if path.match?(regex) && methods[method.downcase]
      tag = methods.dig(method.downcase, "tags")&.first || default_tag
      return [swagger_path, tag]
    end
  end

  [path, default_tag]
rescue => e
  Rails.logger.debug "[Binocs] Failed to find swagger path: #{e.message}"
  [path, default_tag]
end

#formatted_durationObject



101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/binocs/request.rb', line 101

def formatted_duration
  return "N/A" unless duration_ms

  if duration_ms < 1
    "< 1ms"
  elsif duration_ms < 1000
    "#{duration_ms.round(1)}ms"
  else
    "#{(duration_ms / 1000).round(2)}s"
  end
end

#formatted_memory_deltaObject



113
114
115
116
117
118
119
120
121
122
123
# File 'app/models/binocs/request.rb', line 113

def formatted_memory_delta
  return "N/A" unless memory_delta

  if memory_delta.abs < 1024
    "#{memory_delta} B"
  elsif memory_delta.abs < 1024 * 1024
    "#{(memory_delta / 1024.0).round(2)} KB"
  else
    "#{(memory_delta / (1024.0 * 1024)).round(2)} MB"
  end
end

#full_urlObject



131
132
133
134
135
136
# File 'app/models/binocs/request.rb', line 131

def full_url
  # Construct URL from host header if available
  host = request_headers&.dig('Host') || request_headers&.dig('host') || 'localhost'
  scheme = request_headers&.dig('X-Forwarded-Proto') || 'http'
  "#{scheme}://#{host}#{path}"
end

#has_exception?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'app/models/binocs/request.rb', line 79

def has_exception?
  exception.present?
end

#http_methodObject

Alias for ‘method’ to avoid conflict with Object#method



59
60
61
# File 'app/models/binocs/request.rb', line 59

def http_method
  read_attribute(:method)
end

#method_classObject



91
92
93
94
95
96
97
98
99
# File 'app/models/binocs/request.rb', line 91

def method_class
  case method.upcase
  when "GET" then "method-get"
  when "POST" then "method-post"
  when "PUT", "PATCH" then "method-put"
  when "DELETE" then "method-delete"
  else "method-other"
  end
end

#redirect?Boolean

Returns:

  • (Boolean)


67
68
69
# File 'app/models/binocs/request.rb', line 67

def redirect?
  status_code.present? && status_code >= 300 && status_code < 400
end

#server_error?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'app/models/binocs/request.rb', line 75

def server_error?
  status_code.present? && status_code >= 500
end

#short_pathObject



125
126
127
128
129
# File 'app/models/binocs/request.rb', line 125

def short_path
  return path if path.length <= 50

  "#{path[0, 47]}..."
end

#status_classObject



83
84
85
86
87
88
89
# File 'app/models/binocs/request.rb', line 83

def status_class
  return "error" if server_error? || has_exception?
  return "warning" if client_error?
  return "redirect" if redirect?

  "success"
end

#success?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'app/models/binocs/request.rb', line 63

def success?
  status_code.present? && status_code >= 200 && status_code < 300
end

#swagger_urlObject



144
145
146
147
148
149
150
151
152
153
# File 'app/models/binocs/request.rb', line 144

def swagger_url
  # Convert path to Swagger UI deep link format: #/{tagName}/{operationId}
  swagger_path, tag = find_swagger_path_and_tag

  # Generate operationId: method + path with non-alphanumeric chars replaced
  # /v1/companies/{company_uuid}/invitations -> v1_companies__company_uuid__invitations
  operation_id = "#{method.downcase}#{swagger_path.gsub(/[^a-zA-Z0-9]/, '_')}"

  "/api-docs/index.html#/#{ERB::Util.url_encode(tag)}/#{operation_id}"
end