Class: OpenTrace::RequestBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/opentrace/request_buffer.rb

Constant Summary collapse

CAPTURE_LEVELS =

Capture levels control how much data is included in the final document. :minimal — request identity + timing only (no bodies, no captures) :standard — includes captures but strips request/response bodies :full — everything

%i[minimal standard full].freeze
CAPTURE_LEVEL_RANK =
{ none: -1, minimal: 0, standard: 1, full: 2 }.freeze
DOMAINS =

Domains that can have individual capture-level overrides

%i[request sql http email file audit log timeline].freeze
BASE_BYTE_ESTIMATE =

1KB base overhead

1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_buffer_bytes: 1_048_576, max_audit_events: 50) ⇒ RequestBuffer

Returns a new instance of RequestBuffer.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/opentrace/request_buffer.rb', line 43

def initialize(max_buffer_bytes: 1_048_576, max_audit_events: 50)
  @max_buffer_bytes = max_buffer_bytes
  @max_audit_events = max_audit_events

  # Pre-allocate arrays (reused across checkouts via reset!)
  @sql_captures   = []
  @http_captures  = []
  @email_captures = []
  @file_captures  = []
  @audit_captures = []
  @logs           = []
  @timeline       = []

  @audit_truncated = false

  # Identity — set on checkout
  @id         = nil
  @started_at = nil
  @event_type = nil

  # All other fields start nil
  reset_fields!
end

Instance Attribute Details

#actionObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def action
  @action
end

#audit_capturesObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def audit_captures
  @audit_captures
end

#content_typeObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def content_type
  @content_type
end

#contextObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def context
  @context
end

#controllerObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def controller
  @controller
end

#cookiesObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def cookies
  @cookies
end

#email_capturesObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def email_captures
  @email_captures
end

#event_typeObject

Returns the value of attribute event_type.



19
20
21
# File 'lib/opentrace/request_buffer.rb', line 19

def event_type
  @event_type
end

#file_capturesObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def file_captures
  @file_captures
end

#http_capturesObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def http_captures
  @http_captures
end

#idObject

Returns the value of attribute id.



19
20
21
# File 'lib/opentrace/request_buffer.rb', line 19

def id
  @id
end

#ip_addressObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def ip_address
  @ip_address
end

#job_argumentsObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_arguments
  @job_arguments
end

#job_classObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_class
  @job_class
end

#job_enqueued_atObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_enqueued_at
  @job_enqueued_at
end

#job_executionsObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_executions
  @job_executions
end

#job_idObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_id
  @job_id
end

#job_queueObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_queue
  @job_queue
end

#job_queue_latency_msObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def job_queue_latency_ms
  @job_queue_latency_ms
end

#logsObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def logs
  @logs
end

#memory_afterObject

Performance fields (set by middleware)



28
29
30
# File 'lib/opentrace/request_buffer.rb', line 28

def memory_after
  @memory_after
end

#memory_beforeObject

Performance fields (set by middleware)



28
29
30
# File 'lib/opentrace/request_buffer.rb', line 28

def memory_before
  @memory_before
end

#parent_span_idObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def parent_span_id
  @parent_span_id
end

#refererObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def referer
  @referer
end

#request_bodyObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_body
  @request_body
end

#request_headersObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_headers
  @request_headers
end

#request_idObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def request_id
  @request_id
end

#request_methodObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_method
  @request_method
end

#request_paramsObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_params
  @request_params
end

#request_pathObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_path
  @request_path
end

#request_sizeObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def request_size
  @request_size
end

#response_bodyObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def response_body
  @response_body
end

#response_headersObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def response_headers
  @response_headers
end

#response_sizeObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def response_size
  @response_size
end

#response_statusObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def response_status
  @response_status
end

#session_dataObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def session_data
  @session_data
end

#span_idObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def span_id
  @span_id
end

#sql_capturesObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def sql_captures
  @sql_captures
end

#started_atObject

Returns the value of attribute started_at.



19
20
21
# File 'lib/opentrace/request_buffer.rb', line 19

def started_at
  @started_at
end

#timelineObject (readonly)

Read-only access to internal capture arrays (useful for tests/inspection)



40
41
42
# File 'lib/opentrace/request_buffer.rb', line 40

def timeline
  @timeline
end

#trace_idObject

Context fields (set by middleware)



31
32
33
# File 'lib/opentrace/request_buffer.rb', line 31

def trace_id
  @trace_id
end

#triggered_by_job_idObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def triggered_by_job_id
  @triggered_by_job_id
end

#triggered_by_request_idObject

Job fields (for job.perform)



35
36
37
# File 'lib/opentrace/request_buffer.rb', line 35

def triggered_by_request_id
  @triggered_by_request_id
end

#user_agentObject

Request/response fields (set by middleware)



22
23
24
# File 'lib/opentrace/request_buffer.rb', line 22

def user_agent
  @user_agent
end

Instance Method Details

#audit_truncated?Boolean

Returns:

  • (Boolean)


221
222
223
# File 'lib/opentrace/request_buffer.rb', line 221

def audit_truncated?
  @audit_truncated
end

#byte_sizeObject

── Output ──



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/opentrace/request_buffer.rb', line 190

def byte_size
  size = BASE_BYTE_ESTIMATE

  size += @request_body.bytesize  if @request_body.is_a?(String)
  size += @response_body.bytesize if @response_body.is_a?(String)

  @sql_captures.each do |c|
    size += c[:raw_sql].bytesize if c[:raw_sql].is_a?(String)
  end

  @http_captures.each do |c|
    size += c[:request_body].bytesize  if c[:request_body].is_a?(String)
    size += c[:response_body].bytesize if c[:response_body].is_a?(String)
  end

  @email_captures.each do |c|
    size += c[:body_html].bytesize if c[:body_html].is_a?(String)
    size += c[:body_text].bytesize if c[:body_text].is_a?(String)
  end

  @logs.each do |c|
    size += c[:message].bytesize if c[:message].is_a?(String)
  end

  size
end

#exceeded_buffer?Boolean

Returns:

  • (Boolean)


217
218
219
# File 'lib/opentrace/request_buffer.rb', line 217

def exceeded_buffer?
  byte_size > @max_buffer_bytes
end

#record_audit(action:, record_type:, record_id: nil, actor_id: nil, actor_type: nil, changed_fields: nil, full_before: nil, full_after: nil) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/opentrace/request_buffer.rb', line 142

def record_audit(action:, record_type:, record_id: nil, actor_id: nil,
                 actor_type: nil, changed_fields: nil, full_before: nil,
                 full_after: nil)
  if @audit_captures.size >= @max_audit_events
    @audit_truncated = true
    return
  end

  @audit_captures << {
    action: action,
    record_type: record_type,
    record_id: record_id,
    actor_id: actor_id,
    actor_type: actor_type,
    changed_fields: changed_fields,
    full_before: full_before,
    full_after: full_after
  }
end

#record_email(mailer_class:, mailer_action:, from: nil, to: nil, subject: nil, body_html: nil, body_text: nil, template: nil, variables: nil, attachments: nil, delivery_status: nil, smtp_response: nil, duration_ms: nil) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/opentrace/request_buffer.rb', line 108

def record_email(mailer_class:, mailer_action:, from: nil, to: nil, subject: nil,
                 body_html: nil, body_text: nil, template: nil, variables: nil,
                 attachments: nil, delivery_status: nil, smtp_response: nil,
                 duration_ms: nil)
  @email_captures << {
    mailer_class: mailer_class,
    mailer_action: mailer_action,
    from: from,
    to: to,
    subject: subject,
    body_html: body_html,
    body_text: body_text,
    template: template,
    variables: variables,
    attachments: attachments,
    delivery_status: delivery_status,
    smtp_response: smtp_response,
    duration_ms: duration_ms
  }
end

#record_file(action:, filename:, size_bytes: nil, content_type: nil, service: nil, key: nil, duration_ms: nil) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/opentrace/request_buffer.rb', line 129

def record_file(action:, filename:, size_bytes: nil, content_type: nil,
                service: nil, key: nil, duration_ms: nil)
  @file_captures << {
    action: action,
    filename: filename,
    size_bytes: size_bytes,
    content_type: content_type,
    service: service,
    key: key,
    duration_ms: duration_ms
  }
end

#record_http(method:, url:, host: nil, vendor: nil, status: nil, duration_ms:, request_headers: nil, request_body: nil, response_headers: nil, response_body: nil, response_size: nil, retry_attempt: nil, error_class: nil) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/opentrace/request_buffer.rb', line 87

def record_http(method:, url:, host: nil, vendor: nil, status: nil, duration_ms:,
                request_headers: nil, request_body: nil, response_headers: nil,
                response_body: nil, response_size: nil, retry_attempt: nil,
                error_class: nil)
  @http_captures << {
    method: method,
    url: url,
    host: host,
    vendor: vendor,
    status: status,
    duration_ms: duration_ms,
    request_headers: request_headers,
    request_body: request_body,
    response_headers: response_headers,
    response_body: response_body,
    response_size: response_size,
    retry_attempt: retry_attempt,
    error_class: error_class
  }
end

#record_log(level:, message:, metadata: nil) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/opentrace/request_buffer.rb', line 162

def record_log(level:, message:, metadata: nil)
  @logs << {
    level: level,
    message: message,
    metadata: 
  }
end

#record_sql(raw_sql:, normalized_sql: nil, binds: nil, duration_ms:, name: nil, cached: false, row_count: nil, in_transaction: false, fingerprint: nil, table: nil, caller_location: nil) ⇒ Object

── Accumulation methods (called by subscribers during request) ──



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/opentrace/request_buffer.rb', line 69

def record_sql(raw_sql:, normalized_sql: nil, binds: nil, duration_ms:, name: nil,
               cached: false, row_count: nil, in_transaction: false,
               fingerprint: nil, table: nil, caller_location: nil)
  @sql_captures << {
    raw_sql: raw_sql,
    normalized_sql: normalized_sql,
    binds: binds,
    duration_ms: duration_ms,
    name: name,
    cached: cached,
    row_count: row_count,
    in_transaction: in_transaction,
    fingerprint: fingerprint,
    table: table,
    caller_location: caller_location
  }
end

#record_timeline(type:, name:, duration_ms: nil, extra: {}) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/opentrace/request_buffer.rb', line 170

def record_timeline(type:, name:, duration_ms: nil, extra: {})
  offset = if @started_at
             ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at) * 1000).round(1)
           else
             0.0
           end

  entry = {
    type: type,
    name: name,
    offset_ms: offset
  }
  entry[:duration_ms] = duration_ms if duration_ms
  entry.merge!(extra) unless extra.empty?

  @timeline << entry
end

#reset!Object

── Lifecycle ──



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/opentrace/request_buffer.rb', line 296

def reset!
  @id         = nil
  @started_at = nil
  @event_type = nil

  # Clear arrays but keep allocated memory
  @sql_captures.clear
  @http_captures.clear
  @email_captures.clear
  @file_captures.clear
  @audit_captures.clear
  @logs.clear
  @timeline.clear

  @audit_truncated = false

  reset_fields!
end

#to_document(capture_level:, domain_overrides: {}) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/opentrace/request_buffer.rb', line 225

def to_document(capture_level:, domain_overrides: {})
  base_rank = CAPTURE_LEVEL_RANK.fetch(capture_level, 1)

  doc = {}

  # Identity
  doc[:id]         = @id
  doc[:event_type] = @event_type
  doc[:started_at] = @started_at

  # Request identity (always included)
  doc[:request] = build_request_section(base_rank, domain_overrides)

  # Response (always included at minimum level for status)
  doc[:response] = build_response_section(base_rank, domain_overrides)

  # Context
  doc[:context] = @context if @context

  # Controller/action
  doc[:controller] = @controller if @controller
  doc[:action]     = @action     if @action

  # Tracing
  doc[:request_id]      = @request_id      if @request_id
  doc[:trace_id]        = @trace_id        if @trace_id
  doc[:span_id]         = @span_id         if @span_id
  doc[:parent_span_id]  = @parent_span_id  if @parent_span_id

  # Performance
  if @memory_before || @memory_after
    doc[:performance] = {
      memory_before: @memory_before,
      memory_after: @memory_after
    }.compact
  end

  # Job fields
  if @job_class
    doc[:job] = {
      class: @job_class,
      id: @job_id,
      queue: @job_queue,
      arguments: @job_arguments,
      executions: @job_executions,
      queue_latency_ms: @job_queue_latency_ms,
      enqueued_at: @job_enqueued_at,
      triggered_by_request_id: @triggered_by_request_id,
      triggered_by_job_id: @triggered_by_job_id
    }.compact
  end

  # Domain captures — only include if above :minimal
  unless base_rank == 0 && !has_domain_override?(domain_overrides)
    doc[:sql]    = filter_captures(@sql_captures, :sql, base_rank, domain_overrides)       unless @sql_captures.empty? && !domain_included?(:sql, base_rank, domain_overrides)
    doc[:http]   = filter_captures(@http_captures, :http, base_rank, domain_overrides)     unless @http_captures.empty? && !domain_included?(:http, base_rank, domain_overrides)
    doc[:email]  = filter_captures(@email_captures, :email, base_rank, domain_overrides)   unless @email_captures.empty? && !domain_included?(:email, base_rank, domain_overrides)
    doc[:file]   = filter_captures(@file_captures, :file, base_rank, domain_overrides)     unless @file_captures.empty? && !domain_included?(:file, base_rank, domain_overrides)
    doc[:audit]  = filter_captures(@audit_captures, :audit, base_rank, domain_overrides)   unless @audit_captures.empty? && !domain_included?(:audit, base_rank, domain_overrides)
    doc[:logs]   = filter_captures(@logs, :log, base_rank, domain_overrides)               unless @logs.empty? && !domain_included?(:log, base_rank, domain_overrides)
    doc[:timeline] = @timeline.dup unless @timeline.empty?
  end

  doc[:audit_truncated] = true if @audit_truncated
  doc[:buffer_exceeded] = true if exceeded_buffer?

  doc.compact
end