fluyenta-ruby 0.1.14

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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Pulse
5
+ class Instrumentation
6
+ class << self
7
+ def install!
8
+ return unless BrainzLab.configuration.pulse_enabled
9
+
10
+ install_active_record!
11
+ install_action_view!
12
+ install_active_support_cache!
13
+ install_action_controller!
14
+ install_http_clients!
15
+ install_active_job!
16
+ install_action_cable!
17
+ end
18
+
19
+ private
20
+
21
+ # Track SQL queries
22
+ def install_active_record!
23
+ return unless defined?(ActiveRecord)
24
+
25
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
26
+ event = ActiveSupport::Notifications::Event.new(*args)
27
+ next if skip_query?(event.payload)
28
+
29
+ sql = event.payload[:sql]
30
+ record_span(
31
+ name: event.payload[:name] || 'SQL',
32
+ kind: 'db',
33
+ started_at: event.time,
34
+ ended_at: event.end,
35
+ duration_ms: event.duration,
36
+ data: {
37
+ sql: truncate_sql(sql),
38
+ name: event.payload[:name],
39
+ cached: event.payload[:cached] || false,
40
+ table: extract_table(sql),
41
+ operation: extract_operation(sql)
42
+ }
43
+ )
44
+ end
45
+ end
46
+
47
+ # Track view rendering
48
+ def install_action_view!
49
+ return unless defined?(ActionView)
50
+
51
+ ActiveSupport::Notifications.subscribe('render_template.action_view') do |*args|
52
+ event = ActiveSupport::Notifications::Event.new(*args)
53
+
54
+ record_span(
55
+ name: short_path(event.payload[:identifier]),
56
+ kind: 'render',
57
+ started_at: event.time,
58
+ ended_at: event.end,
59
+ duration_ms: event.duration,
60
+ data: {
61
+ identifier: event.payload[:identifier],
62
+ layout: event.payload[:layout]
63
+ }
64
+ )
65
+ end
66
+
67
+ ActiveSupport::Notifications.subscribe('render_partial.action_view') do |*args|
68
+ event = ActiveSupport::Notifications::Event.new(*args)
69
+
70
+ record_span(
71
+ name: short_path(event.payload[:identifier]),
72
+ kind: 'render',
73
+ started_at: event.time,
74
+ ended_at: event.end,
75
+ duration_ms: event.duration,
76
+ data: {
77
+ identifier: event.payload[:identifier],
78
+ partial: true
79
+ }
80
+ )
81
+ end
82
+
83
+ ActiveSupport::Notifications.subscribe('render_collection.action_view') do |*args|
84
+ event = ActiveSupport::Notifications::Event.new(*args)
85
+
86
+ record_span(
87
+ name: short_path(event.payload[:identifier]),
88
+ kind: 'render',
89
+ started_at: event.time,
90
+ ended_at: event.end,
91
+ duration_ms: event.duration,
92
+ data: {
93
+ identifier: event.payload[:identifier],
94
+ count: event.payload[:count],
95
+ collection: true
96
+ }
97
+ )
98
+ end
99
+ end
100
+
101
+ # Track cache operations
102
+ def install_active_support_cache!
103
+ %w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
104
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
105
+ event = ActiveSupport::Notifications::Event.new(*args)
106
+ operation = event_name.split('.').first.sub('cache_', '')
107
+
108
+ record_span(
109
+ name: "Cache #{operation}",
110
+ kind: 'cache',
111
+ started_at: event.time,
112
+ ended_at: event.end,
113
+ duration_ms: event.duration,
114
+ data: {
115
+ key: truncate_key(event.payload[:key]),
116
+ hit: event.payload[:hit],
117
+ operation: operation
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
123
+
124
+ # Track controller processing for timing breakdown
125
+ def install_action_controller!
126
+ return unless defined?(ActionController)
127
+
128
+ ActiveSupport::Notifications.subscribe('process_action.action_controller') do |*args|
129
+ event = ActiveSupport::Notifications::Event.new(*args)
130
+ payload = event.payload
131
+
132
+ # Store timing breakdown in thread local for the middleware
133
+ Thread.current[:brainzlab_pulse_breakdown] = {
134
+ view_ms: payload[:view_runtime]&.round(2),
135
+ db_ms: payload[:db_runtime]&.round(2)
136
+ }
137
+ end
138
+ end
139
+
140
+ # Track external HTTP requests
141
+ def install_http_clients!
142
+ # Net::HTTP instrumentation
143
+ if defined?(Net::HTTP)
144
+ ActiveSupport::Notifications.subscribe('request.net_http') do |*args|
145
+ event = ActiveSupport::Notifications::Event.new(*args)
146
+
147
+ record_span(
148
+ name: "HTTP #{event.payload[:method]} #{event.payload[:host]}",
149
+ kind: 'http',
150
+ started_at: event.time,
151
+ ended_at: event.end,
152
+ duration_ms: event.duration,
153
+ data: {
154
+ method: event.payload[:method],
155
+ host: event.payload[:host],
156
+ path: event.payload[:path],
157
+ status: event.payload[:code]
158
+ }
159
+ )
160
+ end
161
+ end
162
+
163
+ # Faraday instrumentation
164
+ return unless defined?(Faraday)
165
+
166
+ ActiveSupport::Notifications.subscribe('request.faraday') do |*args|
167
+ event = ActiveSupport::Notifications::Event.new(*args)
168
+ env = event.payload[:env]
169
+ next unless env
170
+
171
+ record_span(
172
+ name: "HTTP #{env.method.to_s.upcase} #{env.url.host}",
173
+ kind: 'http',
174
+ started_at: event.time,
175
+ ended_at: event.end,
176
+ duration_ms: event.duration,
177
+ data: {
178
+ method: env.method.to_s.upcase,
179
+ host: env.url.host,
180
+ path: env.url.path,
181
+ status: env.status
182
+ }
183
+ )
184
+ end
185
+ end
186
+
187
+ # Track ActiveJob/SolidQueue
188
+ def install_active_job!
189
+ return unless defined?(ActiveJob)
190
+
191
+ # Track job enqueuing
192
+ ActiveSupport::Notifications.subscribe('enqueue.active_job') do |*args|
193
+ event = ActiveSupport::Notifications::Event.new(*args)
194
+ job = event.payload[:job]
195
+
196
+ record_span(
197
+ name: "Enqueue #{job.class.name}",
198
+ kind: 'job',
199
+ started_at: event.time,
200
+ ended_at: event.end,
201
+ duration_ms: event.duration,
202
+ data: {
203
+ job_class: job.class.name,
204
+ job_id: job.job_id,
205
+ queue: job.queue_name
206
+ }
207
+ )
208
+ end
209
+
210
+ # Track job retry
211
+ ActiveSupport::Notifications.subscribe('retry_stopped.active_job') do |*args|
212
+ event = ActiveSupport::Notifications::Event.new(*args)
213
+ job = event.payload[:job]
214
+ error = event.payload[:error]
215
+
216
+ record_span(
217
+ name: "Retry stopped #{job.class.name}",
218
+ kind: 'job',
219
+ started_at: event.time,
220
+ ended_at: event.end,
221
+ duration_ms: event.duration,
222
+ error: true,
223
+ error_class: error&.class&.name,
224
+ error_message: error&.message,
225
+ data: {
226
+ job_class: job.class.name,
227
+ job_id: job.job_id,
228
+ queue: job.queue_name,
229
+ executions: job.executions
230
+ }
231
+ )
232
+ end
233
+
234
+ # Track job discard
235
+ ActiveSupport::Notifications.subscribe('discard.active_job') do |*args|
236
+ event = ActiveSupport::Notifications::Event.new(*args)
237
+ job = event.payload[:job]
238
+ error = event.payload[:error]
239
+
240
+ record_span(
241
+ name: "Discarded #{job.class.name}",
242
+ kind: 'job',
243
+ started_at: event.time,
244
+ ended_at: event.end,
245
+ duration_ms: event.duration,
246
+ error: true,
247
+ error_class: error&.class&.name,
248
+ error_message: error&.message,
249
+ data: {
250
+ job_class: job.class.name,
251
+ job_id: job.job_id,
252
+ queue: job.queue_name,
253
+ executions: job.executions
254
+ }
255
+ )
256
+ end
257
+ end
258
+
259
+ # Track ActionCable/SolidCable
260
+ def install_action_cable!
261
+ return unless defined?(ActionCable)
262
+
263
+ ActiveSupport::Notifications.subscribe('perform_action.action_cable') do |*args|
264
+ event = ActiveSupport::Notifications::Event.new(*args)
265
+
266
+ record_span(
267
+ name: "Cable #{event.payload[:channel_class]}##{event.payload[:action]}",
268
+ kind: 'cable',
269
+ started_at: event.time,
270
+ ended_at: event.end,
271
+ duration_ms: event.duration,
272
+ data: {
273
+ channel: event.payload[:channel_class],
274
+ action: event.payload[:action]
275
+ }
276
+ )
277
+ end
278
+
279
+ ActiveSupport::Notifications.subscribe('transmit.action_cable') do |*args|
280
+ event = ActiveSupport::Notifications::Event.new(*args)
281
+
282
+ record_span(
283
+ name: "Cable transmit #{event.payload[:channel_class]}",
284
+ kind: 'cable',
285
+ started_at: event.time,
286
+ ended_at: event.end,
287
+ duration_ms: event.duration,
288
+ data: {
289
+ channel: event.payload[:channel_class],
290
+ via: event.payload[:via]
291
+ }
292
+ )
293
+ end
294
+
295
+ ActiveSupport::Notifications.subscribe('broadcast.action_cable') do |*args|
296
+ event = ActiveSupport::Notifications::Event.new(*args)
297
+
298
+ record_span(
299
+ name: "Cable broadcast #{event.payload[:broadcasting]}",
300
+ kind: 'cable',
301
+ started_at: event.time,
302
+ ended_at: event.end,
303
+ duration_ms: event.duration,
304
+ data: {
305
+ broadcasting: event.payload[:broadcasting],
306
+ coder: event.payload[:coder]
307
+ }
308
+ )
309
+ end
310
+ end
311
+
312
+ def record_span(name:, kind:, started_at:, ended_at:, duration_ms:, error: false, error_class: nil,
313
+ error_message: nil, data: {})
314
+ spans = Thread.current[:brainzlab_pulse_spans]
315
+ return unless spans
316
+
317
+ span = {
318
+ span_id: SecureRandom.uuid,
319
+ name: name,
320
+ kind: kind,
321
+ started_at: started_at,
322
+ ended_at: ended_at,
323
+ duration_ms: duration_ms.round(2),
324
+ data: data.compact
325
+ }
326
+
327
+ if error
328
+ span[:error] = true
329
+ span[:error_class] = error_class
330
+ span[:error_message] = error_message
331
+ end
332
+
333
+ spans << span
334
+ end
335
+
336
+ def skip_query?(payload)
337
+ # Skip SCHEMA queries and internal Rails queries
338
+ return true if payload[:name] == 'SCHEMA'
339
+ return true if payload[:name]&.start_with?('EXPLAIN')
340
+ return true if payload[:sql]&.include?('pg_')
341
+ return true if payload[:sql]&.include?('information_schema')
342
+ return true if payload[:cached] && !include_cached_queries?
343
+
344
+ false
345
+ end
346
+
347
+ def include_cached_queries?
348
+ false
349
+ end
350
+
351
+ def truncate_sql(sql)
352
+ return nil unless sql
353
+
354
+ sql.to_s[0, 1000]
355
+ end
356
+
357
+ def truncate_key(key)
358
+ return nil unless key
359
+
360
+ key.to_s[0, 200]
361
+ end
362
+
363
+ def short_path(path)
364
+ return nil unless path
365
+
366
+ path.to_s.split('/').last(2).join('/')
367
+ end
368
+
369
+ def extract_table(sql)
370
+ return nil unless sql
371
+
372
+ # Match FROM "table" or FROM table patterns
373
+ # Also handles INSERT INTO, UPDATE, DELETE FROM
374
+ case sql.to_s
375
+ when /\bFROM\s+["'`]?(\w+)["'`]?/i
376
+ Regexp.last_match(1)
377
+ when /\bINTO\s+["'`]?(\w+)["'`]?/i
378
+ Regexp.last_match(1)
379
+ when /\bUPDATE\s+["'`]?(\w+)["'`]?/i
380
+ Regexp.last_match(1)
381
+ when /\bJOIN\s+["'`]?(\w+)["'`]?/i
382
+ Regexp.last_match(1)
383
+ end
384
+ end
385
+
386
+ def extract_operation(sql)
387
+ return nil unless sql
388
+
389
+ case sql.to_s.strip.upcase
390
+ when /\ASELECT/i then 'SELECT'
391
+ when /\AINSERT/i then 'INSERT'
392
+ when /\AUPDATE/i then 'UPDATE'
393
+ when /\ADELETE/i then 'DELETE'
394
+ when /\ABEGIN/i, /\ACOMMIT/i, /\AROLLBACK/i then 'TRANSACTION'
395
+ else 'QUERY'
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Pulse
5
+ # Distributed tracing context propagation using W3C Trace Context format
6
+ # https://www.w3.org/TR/trace-context/
7
+ module Propagation
8
+ # W3C Trace Context header names
9
+ TRACEPARENT_HEADER = 'traceparent'
10
+ TRACESTATE_HEADER = 'tracestate'
11
+
12
+ # HTTP header versions (with HTTP_ prefix for Rack env)
13
+ HTTP_TRACEPARENT = 'HTTP_TRACEPARENT'
14
+ HTTP_TRACESTATE = 'HTTP_TRACESTATE'
15
+
16
+ # Also support B3 format for compatibility
17
+ B3_TRACE_ID = 'X-B3-TraceId'
18
+ B3_SPAN_ID = 'X-B3-SpanId'
19
+ B3_SAMPLED = 'X-B3-Sampled'
20
+ B3_PARENT_SPAN_ID = 'X-B3-ParentSpanId'
21
+
22
+ class Context
23
+ attr_accessor :trace_id, :span_id, :parent_span_id, :sampled, :tracestate
24
+
25
+ def initialize(trace_id: nil, span_id: nil, parent_span_id: nil, sampled: true, tracestate: nil)
26
+ @trace_id = trace_id || generate_trace_id
27
+ @span_id = span_id || generate_span_id
28
+ @parent_span_id = parent_span_id
29
+ @sampled = sampled
30
+ @tracestate = tracestate
31
+ end
32
+
33
+ def valid?
34
+ !trace_id.nil? && !span_id.nil?
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ trace_id: @trace_id,
40
+ span_id: @span_id,
41
+ parent_span_id: @parent_span_id,
42
+ sampled: @sampled,
43
+ tracestate: @tracestate
44
+ }.compact
45
+ end
46
+
47
+ private
48
+
49
+ def generate_trace_id
50
+ SecureRandom.hex(16) # 32 hex chars = 128 bits
51
+ end
52
+
53
+ def generate_span_id
54
+ SecureRandom.hex(8) # 16 hex chars = 64 bits
55
+ end
56
+ end
57
+
58
+ class << self
59
+ # Get current propagation context from thread local
60
+ def current
61
+ Thread.current[:brainzlab_propagation_context]
62
+ end
63
+
64
+ # Set current propagation context
65
+ def current=(context)
66
+ Thread.current[:brainzlab_propagation_context] = context
67
+ end
68
+
69
+ # Create new context and set as current
70
+ def start(trace_id: nil, parent_span_id: nil)
71
+ self.current = Context.new(
72
+ trace_id: trace_id,
73
+ parent_span_id: parent_span_id
74
+ )
75
+ end
76
+
77
+ # Clear current context
78
+ def clear!
79
+ Thread.current[:brainzlab_propagation_context] = nil
80
+ end
81
+
82
+ # Inject trace context into outgoing HTTP headers
83
+ # @param headers [Hash] the headers hash to inject into
84
+ # @param context [Context] optional context (defaults to current)
85
+ # @param format [Symbol] :w3c (default), :b3, or :all
86
+ def inject(headers, context: nil, format: :w3c)
87
+ ctx = context || current
88
+ return headers unless ctx&.valid?
89
+
90
+ case format
91
+ when :w3c
92
+ inject_w3c(headers, ctx)
93
+ when :b3
94
+ inject_b3(headers, ctx)
95
+ when :all
96
+ inject_w3c(headers, ctx)
97
+ inject_b3(headers, ctx)
98
+ end
99
+
100
+ headers
101
+ end
102
+
103
+ # Extract trace context from incoming HTTP headers (Rack env or plain headers)
104
+ # @param headers [Hash] the headers to extract from
105
+ # @return [Context, nil] the extracted context or nil
106
+ def extract(headers)
107
+ # Try W3C format first
108
+ ctx = extract_w3c(headers)
109
+ return ctx if ctx
110
+
111
+ # Fall back to B3 format
112
+ extract_b3(headers)
113
+ end
114
+
115
+ # Extract and set as current context
116
+ # Returns the context for chaining
117
+ def extract!(headers)
118
+ self.current = extract(headers)
119
+ end
120
+
121
+ # Create a child context for a new span
122
+ def child_context(parent: nil)
123
+ parent ||= current
124
+ return Context.new unless parent&.valid?
125
+
126
+ Context.new(
127
+ trace_id: parent.trace_id,
128
+ parent_span_id: parent.span_id,
129
+ sampled: parent.sampled,
130
+ tracestate: parent.tracestate
131
+ )
132
+ end
133
+
134
+ private
135
+
136
+ # W3C Trace Context format injection
137
+ # traceparent: version-traceid-spanid-flags
138
+ # Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
139
+ def inject_w3c(headers, ctx)
140
+ version = '00'
141
+ flags = ctx.sampled ? '01' : '00'
142
+ trace_id = normalize_trace_id(ctx.trace_id, 32)
143
+ span_id = normalize_trace_id(ctx.span_id, 16)
144
+
145
+ headers[TRACEPARENT_HEADER] = "#{version}-#{trace_id}-#{span_id}-#{flags}"
146
+ headers[TRACESTATE_HEADER] = ctx.tracestate if ctx.tracestate
147
+
148
+ headers
149
+ end
150
+
151
+ # W3C Trace Context format extraction
152
+ def extract_w3c(headers)
153
+ traceparent = headers[TRACEPARENT_HEADER] ||
154
+ headers[HTTP_TRACEPARENT] ||
155
+ headers['Traceparent']
156
+ return nil unless traceparent
157
+
158
+ # Parse: version-traceid-spanid-flags
159
+ parts = traceparent.to_s.split('-')
160
+ return nil if parts.length < 4
161
+
162
+ version, trace_id, span_id, flags = parts
163
+
164
+ # Validate version
165
+ return nil unless version == '00'
166
+
167
+ # Validate trace_id (32 hex chars, not all zeros)
168
+ return nil unless trace_id&.match?(/\A[a-f0-9]{32}\z/i)
169
+ return nil if trace_id == '0' * 32
170
+
171
+ # Validate span_id (16 hex chars, not all zeros)
172
+ return nil unless span_id&.match?(/\A[a-f0-9]{16}\z/i)
173
+ return nil if span_id == '0' * 16
174
+
175
+ sampled = flags.to_i(16) & 0x01 == 1
176
+
177
+ tracestate = headers[TRACESTATE_HEADER] ||
178
+ headers[HTTP_TRACESTATE] ||
179
+ headers['Tracestate']
180
+
181
+ Context.new(
182
+ trace_id: trace_id,
183
+ span_id: span_id,
184
+ sampled: sampled,
185
+ tracestate: tracestate
186
+ )
187
+ rescue StandardError
188
+ nil
189
+ end
190
+
191
+ # B3 format injection (Zipkin compatibility)
192
+ def inject_b3(headers, ctx)
193
+ headers[B3_TRACE_ID] = normalize_trace_id(ctx.trace_id, 32)
194
+ headers[B3_SPAN_ID] = normalize_trace_id(ctx.span_id, 16)
195
+ headers[B3_SAMPLED] = ctx.sampled ? '1' : '0'
196
+ headers[B3_PARENT_SPAN_ID] = ctx.parent_span_id if ctx.parent_span_id
197
+
198
+ headers
199
+ end
200
+
201
+ # B3 format extraction
202
+ def extract_b3(headers)
203
+ trace_id = headers[B3_TRACE_ID] ||
204
+ headers['HTTP_X_B3_TRACEID'] ||
205
+ headers['x-b3-traceid']
206
+ return nil unless trace_id
207
+
208
+ span_id = headers[B3_SPAN_ID] ||
209
+ headers['HTTP_X_B3_SPANID'] ||
210
+ headers['x-b3-spanid']
211
+ return nil unless span_id
212
+
213
+ sampled_header = headers[B3_SAMPLED] ||
214
+ headers['HTTP_X_B3_SAMPLED'] ||
215
+ headers['x-b3-sampled']
216
+ sampled = sampled_header != '0'
217
+
218
+ parent_span_id = headers[B3_PARENT_SPAN_ID] ||
219
+ headers['HTTP_X_B3_PARENTSPANID'] ||
220
+ headers['x-b3-parentspanid']
221
+
222
+ Context.new(
223
+ trace_id: trace_id,
224
+ span_id: span_id,
225
+ parent_span_id: parent_span_id,
226
+ sampled: sampled
227
+ )
228
+ rescue StandardError
229
+ nil
230
+ end
231
+
232
+ def normalize_trace_id(id, length)
233
+ return nil unless id
234
+
235
+ hex = id.to_s.gsub('-', '').downcase
236
+ hex.rjust(length, '0').slice(0, length)
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end