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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'fileutils'
7
+
8
+ module BrainzLab
9
+ module Pulse
10
+ class Provisioner
11
+ CACHE_DIR = ENV.fetch('BRAINZLAB_CACHE_DIR') { File.join(Dir.home, '.brainzlab') }
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ end
16
+
17
+ def ensure_project!
18
+ return unless should_provision?
19
+
20
+ # Try cached credentials first
21
+ if (cached = load_cached_credentials)
22
+ apply_credentials(cached)
23
+ return cached
24
+ end
25
+
26
+ # Provision new project
27
+ project = provision_project
28
+ return unless project
29
+
30
+ # Cache and apply credentials
31
+ cache_credentials(project)
32
+ apply_credentials(project)
33
+
34
+ project
35
+ end
36
+
37
+ private
38
+
39
+ def should_provision?
40
+ return false unless @config.pulse_auto_provision
41
+ return false unless @config.app_name.to_s.strip.length.positive?
42
+ # Only skip if pulse_api_key is already set
43
+ return false if @config.pulse_api_key.to_s.strip.length.positive?
44
+ return false unless @config.pulse_master_key.to_s.strip.length.positive?
45
+
46
+ true
47
+ end
48
+
49
+ def provision_project
50
+ uri = URI.parse("#{@config.pulse_url}/api/v1/projects/provision")
51
+ request = Net::HTTP::Post.new(uri)
52
+ request['Content-Type'] = 'application/json'
53
+ request['X-Master-Key'] = @config.pulse_master_key
54
+ request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
55
+ request.body = JSON.generate({ name: @config.app_name })
56
+
57
+ response = execute(uri, request)
58
+ return nil unless response.is_a?(Net::HTTPSuccess)
59
+
60
+ JSON.parse(response.body, symbolize_names: true)
61
+ rescue StandardError => e
62
+ log_error("Failed to provision Pulse project: #{e.message}")
63
+ nil
64
+ end
65
+
66
+ def load_cached_credentials
67
+ path = cache_file_path
68
+ return nil unless File.exist?(path)
69
+
70
+ data = JSON.parse(File.read(path), symbolize_names: true)
71
+
72
+ # Validate cached data has required keys
73
+ return nil unless data[:api_key]
74
+
75
+ data
76
+ rescue StandardError => e
77
+ log_error("Failed to load cached Pulse credentials: #{e.message}")
78
+ nil
79
+ end
80
+
81
+ def cache_credentials(project)
82
+ FileUtils.mkdir_p(CACHE_DIR)
83
+ File.write(cache_file_path, JSON.generate(project))
84
+ rescue StandardError => e
85
+ log_error("Failed to cache Pulse credentials: #{e.message}")
86
+ end
87
+
88
+ def cache_file_path
89
+ File.join(CACHE_DIR, "#{@config.app_name}.pulse.json")
90
+ end
91
+
92
+ def apply_credentials(project)
93
+ @config.pulse_api_key = project[:api_key]
94
+
95
+ # Also set service name from app_name if not already set
96
+ @config.service ||= @config.app_name
97
+ end
98
+
99
+ def execute(uri, request)
100
+ http = Net::HTTP.new(uri.host, uri.port)
101
+ http.use_ssl = uri.scheme == 'https'
102
+ http.open_timeout = 5
103
+ http.read_timeout = 10
104
+ http.request(request)
105
+ end
106
+
107
+ def log_error(message)
108
+ return unless @config.logger
109
+
110
+ @config.logger.error("[BrainzLab::Pulse] #{message}")
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Pulse
5
+ class Tracer
6
+ def initialize(config, client)
7
+ @config = config
8
+ @client = client
9
+ end
10
+
11
+ def current_trace
12
+ Thread.current[:brainzlab_pulse_trace]
13
+ end
14
+
15
+ def current_spans
16
+ Thread.current[:brainzlab_pulse_spans] ||= []
17
+ end
18
+
19
+ def start_trace(name, kind: 'custom', **attributes)
20
+ trace = {
21
+ trace_id: SecureRandom.uuid,
22
+ name: name,
23
+ kind: kind,
24
+ started_at: Time.now.utc,
25
+ environment: @config.environment,
26
+ commit: @config.commit,
27
+ host: @config.host,
28
+ **attributes
29
+ }
30
+
31
+ Thread.current[:brainzlab_pulse_trace] = trace
32
+ Thread.current[:brainzlab_pulse_spans] = []
33
+
34
+ trace
35
+ end
36
+
37
+ def finish_trace(error: false, error_class: nil, error_message: nil)
38
+ trace = current_trace
39
+ return unless trace
40
+
41
+ ended_at = Time.now.utc
42
+ duration_ms = ((ended_at - trace[:started_at]) * 1000).round(2)
43
+
44
+ payload = trace.merge(
45
+ ended_at: ended_at.iso8601(3),
46
+ started_at: trace[:started_at].utc.iso8601(3),
47
+ duration_ms: duration_ms,
48
+ error: error,
49
+ error_class: error_class,
50
+ error_message: error_message,
51
+ spans: current_spans.map { |s| format_span(s, trace[:started_at]) }
52
+ ).compact
53
+
54
+ # Add request context if available
55
+ ctx = BrainzLab::Context.current
56
+ payload[:request_id] ||= ctx.request_id
57
+ payload[:user_id] ||= ctx.user&.dig(:id)&.to_s
58
+
59
+ @client.send_trace(payload)
60
+
61
+ Thread.current[:brainzlab_pulse_trace] = nil
62
+ Thread.current[:brainzlab_pulse_spans] = nil
63
+
64
+ payload
65
+ end
66
+
67
+ def span(name, kind: 'custom', **data)
68
+ span_data = {
69
+ span_id: SecureRandom.uuid,
70
+ name: name,
71
+ kind: kind,
72
+ started_at: Time.now.utc,
73
+ data: data
74
+ }
75
+
76
+ begin
77
+ result = yield
78
+ span_data[:error] = false
79
+ result
80
+ rescue StandardError => e
81
+ span_data[:error] = true
82
+ span_data[:error_class] = e.class.name
83
+ span_data[:error_message] = e.message
84
+ raise
85
+ ensure
86
+ span_data[:ended_at] = Time.now.utc
87
+ span_data[:duration_ms] = ((span_data[:ended_at] - span_data[:started_at]) * 1000).round(2)
88
+ current_spans << span_data
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def format_span(span, _trace_started_at)
95
+ {
96
+ span_id: span[:span_id],
97
+ parent_span_id: span[:parent_span_id],
98
+ name: span[:name],
99
+ kind: span[:kind],
100
+ started_at: span[:started_at].utc.iso8601(3),
101
+ ended_at: span[:ended_at].utc.iso8601(3),
102
+ duration_ms: span[:duration_ms],
103
+ error: span[:error],
104
+ error_class: span[:error_class],
105
+ error_message: span[:error_message],
106
+ data: span[:data]
107
+ }.compact
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pulse/client'
4
+ require_relative 'pulse/provisioner'
5
+ require_relative 'pulse/tracer'
6
+ require_relative 'pulse/instrumentation'
7
+ require_relative 'pulse/propagation'
8
+
9
+ module BrainzLab
10
+ module Pulse
11
+ class << self
12
+ # Start a new trace
13
+ # @param name [String] the trace name
14
+ # @param kind [String] trace kind (request, job, custom)
15
+ # @param parent_context [Propagation::Context] optional parent context for distributed tracing
16
+ def start_trace(name, kind: 'custom', parent_context: nil, **attributes)
17
+ return nil unless enabled?
18
+
19
+ ensure_provisioned!
20
+ return nil unless BrainzLab.configuration.pulse_valid?
21
+
22
+ # Use parent context trace_id if provided (distributed tracing)
23
+ if parent_context&.valid?
24
+ attributes[:parent_trace_id] = parent_context.trace_id
25
+ attributes[:parent_span_id] = parent_context.span_id
26
+ end
27
+
28
+ tracer.start_trace(name, kind: kind, **attributes)
29
+ end
30
+
31
+ # Finish current trace
32
+ def finish_trace(error: false, error_class: nil, error_message: nil)
33
+ return unless enabled?
34
+
35
+ tracer.finish_trace(error: error, error_class: error_class, error_message: error_message)
36
+ end
37
+
38
+ # Add a span to the current trace
39
+ def span(name, kind: 'custom', **data, &)
40
+ return yield unless enabled?
41
+ return yield unless tracer.current_trace
42
+
43
+ tracer.span(name, kind: kind, **data, &)
44
+ end
45
+
46
+ # Record a complete trace (for when you have all data upfront)
47
+ def record_trace(name, started_at:, ended_at:, kind: 'request', **attributes)
48
+ return unless enabled?
49
+
50
+ payload = build_trace_payload(name, kind, started_at, ended_at, attributes)
51
+
52
+ # In development mode, log locally instead of sending to server
53
+ if BrainzLab.configuration.development_mode?
54
+ Development.record(service: :pulse, event_type: 'trace', payload: payload)
55
+ return
56
+ end
57
+
58
+ ensure_provisioned!
59
+ return unless BrainzLab.configuration.pulse_valid?
60
+
61
+ client.send_trace(payload)
62
+ end
63
+
64
+ # Record a custom metric
65
+ def record_metric(name, value:, kind: 'gauge', tags: {})
66
+ return unless enabled?
67
+
68
+ payload = {
69
+ name: name,
70
+ value: value,
71
+ kind: kind,
72
+ timestamp: Time.now.utc.iso8601(3),
73
+ tags: tags
74
+ }
75
+
76
+ # In development mode, log locally instead of sending to server
77
+ if BrainzLab.configuration.development_mode?
78
+ Development.record(service: :pulse, event_type: 'metric', payload: payload)
79
+ return
80
+ end
81
+
82
+ ensure_provisioned!
83
+ return unless BrainzLab.configuration.pulse_valid?
84
+
85
+ if BrainzLab.instrumenting?
86
+ # During instrumentation, send in background thread to avoid
87
+ # blocking the host app with synchronous HTTP
88
+ Thread.new { client.send_metric(payload) }
89
+ else
90
+ client.send_metric(payload)
91
+ end
92
+ end
93
+
94
+ # Convenience methods for metrics
95
+ def gauge(name, value, tags: {})
96
+ record_metric(name, value: value, kind: 'gauge', tags: tags)
97
+ end
98
+
99
+ def counter(name, value = 1, tags: {})
100
+ record_metric(name, value: value, kind: 'counter', tags: tags)
101
+ end
102
+
103
+ def histogram(name, value, tags: {})
104
+ record_metric(name, value: value, kind: 'histogram', tags: tags)
105
+ end
106
+
107
+ # Record a standalone span (used by brainzlab-rails for Rails instrumentation)
108
+ # @param name [String] span name (e.g., "sql.SELECT", "cache.read")
109
+ # @param duration_ms [Float] span duration in milliseconds
110
+ # @param category [String] span category (e.g., "db.sql", "cache.read", "http.request")
111
+ # @param attributes [Hash] additional span attributes
112
+ # @param timestamp [String] ISO8601 timestamp
113
+ def record_span(name:, duration_ms:, category:, attributes: {}, timestamp: nil)
114
+ return unless enabled?
115
+
116
+ ensure_provisioned!
117
+ return unless BrainzLab.configuration.pulse_valid?
118
+
119
+ # Parse timestamp or use current time
120
+ started_at = if timestamp
121
+ Time.parse(timestamp) rescue Time.now.utc
122
+ else
123
+ Time.now.utc
124
+ end
125
+
126
+ span_data = {
127
+ span_id: SecureRandom.uuid,
128
+ name: name,
129
+ kind: category,
130
+ started_at: started_at,
131
+ ended_at: started_at, # Same as started_at since we only have duration
132
+ duration_ms: duration_ms,
133
+ error: false,
134
+ data: attributes
135
+ }
136
+
137
+ # If there's an active trace, add the span to it (will be sent with finish_trace)
138
+ # Otherwise, send it directly to the API as a standalone span
139
+ if tracer.current_trace
140
+ tracer.current_spans << span_data
141
+ else
142
+ # Send as standalone span (backward compatibility)
143
+ api_span_data = {
144
+ name: name,
145
+ category: category,
146
+ duration_ms: duration_ms,
147
+ timestamp: timestamp || Time.now.utc.iso8601(3),
148
+ attributes: attributes,
149
+ environment: BrainzLab.configuration.environment,
150
+ service: BrainzLab.configuration.service,
151
+ host: BrainzLab.configuration.host,
152
+ request_id: Context.current.request_id
153
+ }.compact
154
+ client.send_span(api_span_data)
155
+ end
156
+ end
157
+
158
+ def ensure_provisioned!
159
+ return if @provisioned
160
+
161
+ @provisioned = true
162
+ provisioner.ensure_project!
163
+ end
164
+
165
+ def provisioner
166
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
167
+ end
168
+
169
+ def tracer
170
+ @tracer ||= Tracer.new(BrainzLab.configuration, client)
171
+ end
172
+
173
+ def client
174
+ @client ||= Client.new(BrainzLab.configuration)
175
+ end
176
+
177
+ def reset!
178
+ @client = nil
179
+ @tracer = nil
180
+ @provisioner = nil
181
+ @provisioned = false
182
+ Propagation.clear!
183
+ end
184
+
185
+ # Distributed tracing: inject trace context into outgoing headers
186
+ # @param headers [Hash] the headers hash to inject into
187
+ # @param format [Symbol] :w3c (default), :b3, or :all
188
+ # @return [Hash] the headers with trace context added
189
+ def inject(headers, format: :w3c)
190
+ ctx = Propagation.current || create_propagation_context
191
+ Propagation.inject(headers, context: ctx, format: format)
192
+ end
193
+
194
+ # Distributed tracing: extract trace context from incoming headers
195
+ # @param headers [Hash] incoming headers (Rack env or plain headers)
196
+ # @return [Propagation::Context, nil] extracted context
197
+ def extract(headers)
198
+ Propagation.extract(headers)
199
+ end
200
+
201
+ # Distributed tracing: extract and set as current context
202
+ # @param headers [Hash] incoming headers
203
+ # @return [Propagation::Context, nil] extracted context
204
+ def extract!(headers)
205
+ Propagation.extract!(headers)
206
+ end
207
+
208
+ # Get current propagation context
209
+ def propagation_context
210
+ Propagation.current
211
+ end
212
+
213
+ # Create a child propagation context for a new span
214
+ def child_context
215
+ Propagation.child_context
216
+ end
217
+
218
+ private
219
+
220
+ def create_propagation_context
221
+ trace = tracer.current_trace
222
+ if trace
223
+ Propagation::Context.new(
224
+ trace_id: trace[:trace_id],
225
+ span_id: SecureRandom.hex(8)
226
+ )
227
+ else
228
+ Propagation::Context.new
229
+ end
230
+ end
231
+
232
+ def enabled?
233
+ BrainzLab.configuration.pulse_effectively_enabled?
234
+ end
235
+
236
+ def build_trace_payload(name, kind, started_at, ended_at, attributes)
237
+ config = BrainzLab.configuration
238
+ ctx = Context.current
239
+
240
+ duration_ms = ((ended_at - started_at) * 1000).round(2)
241
+
242
+ {
243
+ trace_id: attributes[:trace_id] || SecureRandom.uuid,
244
+ name: name,
245
+ kind: kind,
246
+ started_at: started_at.utc.iso8601(3),
247
+ ended_at: ended_at.utc.iso8601(3),
248
+ duration_ms: duration_ms,
249
+
250
+ # Distributed tracing - parent trace info
251
+ parent_trace_id: attributes[:parent_trace_id],
252
+ parent_span_id: attributes[:parent_span_id],
253
+
254
+ # Environment
255
+ environment: config.environment,
256
+ commit: config.commit,
257
+ host: config.host,
258
+
259
+ # Request context
260
+ request_id: ctx.request_id || attributes[:request_id],
261
+ request_method: attributes[:request_method],
262
+ request_path: attributes[:request_path],
263
+ controller: attributes[:controller],
264
+ action: attributes[:action],
265
+ status: attributes[:status],
266
+
267
+ # Timing breakdown
268
+ view_ms: attributes[:view_ms],
269
+ db_ms: attributes[:db_ms],
270
+ external_ms: attributes[:external_ms],
271
+ cache_ms: attributes[:cache_ms],
272
+
273
+ # Job context
274
+ job_class: attributes[:job_class],
275
+ job_id: attributes[:job_id],
276
+ queue: attributes[:queue],
277
+ queue_wait_ms: attributes[:queue_wait_ms],
278
+ executions: attributes[:executions],
279
+
280
+ # User
281
+ user_id: ctx.user&.dig(:id)&.to_s || attributes[:user_id],
282
+
283
+ # Error info
284
+ error: attributes[:error] || false,
285
+ error_class: attributes[:error_class],
286
+ error_message: attributes[:error_message],
287
+
288
+ # Spans
289
+ spans: attributes[:spans] || []
290
+ }.compact
291
+ end
292
+ end
293
+ end
294
+ end