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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- 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
|