Module: Buildkite::TestCollector

Defined in:
lib/buildkite/test_collector.rb,
lib/buildkite/test_collector.rb,
lib/buildkite/test_collector/otel.rb,
lib/buildkite/test_collector/error.rb,
lib/buildkite/test_collector/trace.rb,
lib/buildkite/test_collector/object.rb,
lib/buildkite/test_collector/tracer.rb,
lib/buildkite/test_collector/network.rb,
lib/buildkite/test_collector/session.rb,
lib/buildkite/test_collector/version.rb,
lib/buildkite/test_collector/uploader.rb,
lib/buildkite/test_collector/http_client.rb,
lib/buildkite/test_collector/otel/execution_child_forwarder.rb,
lib/buildkite/test_collector/otel/root_span_metrics_reporter.rb

Defined Under Namespace

Modules: CucumberPlugin, MinitestPlugin, OTel, RSpecPlugin Classes: CI, Error, HTTPClient, MinDurationSpanFilter, Network, Object, Session, TimeoutError, Trace, Tracer, UUID, Uploader

Constant Summary collapse

DEFAULT_URL =
"https://analytics-api.buildkite.com/v1/uploads"
DEFAULT_UPLOAD_BATCH_SIZE =
500
VERSION =
"2.15.0"
NAME =
"buildkite-test_collector"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.api_tokenObject

Returns the value of attribute api_token.



34
35
36
# File 'lib/buildkite/test_collector.rb', line 34

def api_token
  @api_token
end

.artifact_pathObject

Returns the value of attribute artifact_path.



39
40
41
# File 'lib/buildkite/test_collector.rb', line 39

def artifact_path
  @artifact_path
end

.batch_sizeObject

Returns the value of attribute batch_size.



45
46
47
# File 'lib/buildkite/test_collector.rb', line 45

def batch_size
  @batch_size
end

.envObject

Returns the value of attribute env.



42
43
44
# File 'lib/buildkite/test_collector.rb', line 42

def env
  @env
end

.location_prefixObject

Returns the value of attribute location_prefix.



40
41
42
# File 'lib/buildkite/test_collector.rb', line 40

def location_prefix
  @location_prefix
end

.otel_onlyObject

Returns the value of attribute otel_only.



44
45
46
# File 'lib/buildkite/test_collector.rb', line 44

def otel_only
  @otel_only
end

.sessionObject

Returns the value of attribute session.



37
38
39
# File 'lib/buildkite/test_collector.rb', line 37

def session
  @session
end

.span_filtersObject

Returns the value of attribute span_filters.



47
48
49
# File 'lib/buildkite/test_collector.rb', line 47

def span_filters
  @span_filters
end

.tagsObject

Returns the value of attribute tags.



43
44
45
# File 'lib/buildkite/test_collector.rb', line 43

def tags
  @tags
end

.test_runnerObject

Returns the value of attribute test_runner.



41
42
43
# File 'lib/buildkite/test_collector.rb', line 41

def test_runner
  @test_runner
end

.trace_min_durationObject

Returns the value of attribute trace_min_duration.



46
47
48
# File 'lib/buildkite/test_collector.rb', line 46

def trace_min_duration
  @trace_min_duration
end

.tracing_enabledObject

Returns the value of attribute tracing_enabled.



38
39
40
# File 'lib/buildkite/test_collector.rb', line 38

def tracing_enabled
  @tracing_enabled
end

.uploaderObject

Returns the value of attribute uploader.



36
37
38
# File 'lib/buildkite/test_collector.rb', line 36

def uploader
  @uploader
end

.urlObject

Returns the value of attribute url.



35
36
37
# File 'lib/buildkite/test_collector.rb', line 35

def url
  @url
end

Class Method Details

.annotate(content) ⇒ Object



164
165
166
167
168
169
170
171
172
173
# File 'lib/buildkite/test_collector.rb', line 164

def self.annotate(content)
  # Keep the OpenTelemetry span identical in both export modes. The
  # standard mode additionally records the annotation in its JSON trace.
  Buildkite::TestCollector::OTel.annotate(content)
  return if otel_only?

  tracer = Buildkite::TestCollector::Uploader.tracer
  tracer&.enter("annotation", **{ content: content })
  tracer&.leave
end

.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, location_prefix: nil, env: {}, tags: {}, otel_enabled: nil, otel_instrumentations: nil, otel_only: false) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/buildkite/test_collector.rb', line 50

def self.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, location_prefix: nil, env: {}, tags: {}, otel_enabled: nil, otel_instrumentations: nil, otel_only: false)
  if otel_only && hook.to_sym != :rspec
    raise ArgumentError.new("otel_only is currently only supported with the rspec hook")
  end

  # They name one choice of upload mode, not two independent switches, so
  # any explicit otel_enabled (even false) contradicts otel_only. Its nil
  # default keeps unspecified distinct from an explicit value.
  if otel_only && !otel_enabled.nil?
    raise ArgumentError.new("otel_enabled and otel_only are mutually exclusive; pass at most one")
  end

  self.api_token = (token || ENV["BUILDKITE_ANALYTICS_TOKEN"])&.strip
  self.url = url || ENV["BUILDKITE_ANALYTICS_ENDPOINT"] || DEFAULT_URL
  self.tracing_enabled = tracing_enabled
  self.artifact_path = artifact_path
  self.location_prefix = location_prefix || ENV["BUILDKITE_ANALYTICS_LOCATION_PREFIX"]
  self.test_runner = hook.to_s
  self.env = env
  self.tags = worker_id_tag.merge(tags)
  self.otel_only = otel_only
  self.batch_size = ENV.fetch("BUILDKITE_ANALYTICS_UPLOAD_BATCH_SIZE") { DEFAULT_UPLOAD_BATCH_SIZE }.to_i

  trace_min_ms_string = ENV["BUILDKITE_ANALYTICS_TRACE_MIN_MS"]
  self.trace_min_duration = if trace_min_ms_string && !trace_min_ms_string.empty?
    Float(trace_min_ms_string) / 1000
  end

  self.span_filters = []
  unless self.trace_min_duration.nil?
    self.span_filters << MinDurationSpanFilter.new(self.trace_min_duration)
  end

  # Defer OTel setup until RSpec's before(:suite), after application and support files have loaded.
  # otel_only already guarantees the rspec hook (checked above), so both
  # modes use exactly the same OpenTelemetry setup.
  @otel_options = nil
  if otel_only || (otel_enabled && test_runner == "rspec")
    @otel_options = {
      # Undocumented, for development purposes.
      endpoint: ENV["BUILDKITE_ANALYTICS_OTLP_ENDPOINT"] || Buildkite::TestCollector::OTel::DEFAULT_ENDPOINT,
      api_token: api_token,
      run_env: Buildkite::TestCollector::CI.env,
      instrumentations: otel_instrumentations,
      # Tags describe the whole run, so they ride along as resource
      # attributes on every exported span. The merged self.tags, not the
      # raw argument, so the automatic ci.worker.id tag comes too.
      resource_attributes: self.tags,
    }
  end
  self.hook_into(hook)
  enable_tracing! if test_runner == "rspec" && !otel_only?
end

.enable_tracing!Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/buildkite/test_collector.rb', line 187

def self.enable_tracing!
  return unless self.tracing_enabled

  Buildkite::TestCollector::Network.configure
  Buildkite::TestCollector::Object.configure

  return unless defined?(ActiveSupport)

  require "active_support/notifications"
  return if @active_support_tracing_enabled

  ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
    Buildkite::TestCollector::Uploader.tracer&.backfill(:sql, finish - start, **{ query: payload[:sql] })
  end
  @active_support_tracing_enabled = true
end

.hook_into(hook) ⇒ Object



145
146
147
148
149
150
# File 'lib/buildkite/test_collector.rb', line 145

def self.hook_into(hook)
  file = "test_collector/library_hooks/#{hook}"
  require_relative file
rescue LoadError
  raise ArgumentError.new("#{hook.inspect} is not a supported Buildkite Analytics Test library hook.")
end

.otel_only?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'lib/buildkite/test_collector.rb', line 113

def self.otel_only?
  !!otel_only
end

.start_otelObject



104
105
106
107
108
109
110
111
# File 'lib/buildkite/test_collector.rb', line 104

def self.start_otel
  options = @otel_options
  @otel_options = nil
  return unless options

  Buildkite::TestCollector::OTel.configure!(**options)
  warn_otel_only_disabled if otel_only? && !Buildkite::TestCollector::OTel.enabled?
end

.tag_execution(key, value) ⇒ Object

Set a key=value tag on the current test execution.



176
177
178
179
180
181
182
183
184
185
# File 'lib/buildkite/test_collector.rb', line 176

def self.tag_execution(key, value)
  tags = Thread.current[:_buildkite_tags]
  raise "_buildkite_tags not available" unless tags

  unless key.is_a?(String) && value.is_a?(String)
    raise ArgumentError, "tag key and value expected string"
  end

  tags[key] = value
end

.warn_otel_only_disabledObject

In otel_only mode OTLP is the only upload method, so if OpenTelemetry could not be set up (see the warning OTel.configure! just printed) there is nothing to fall back to: the suite still runs, but no results are uploaded at all. That deserves more than one easily-missed line.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/buildkite/test_collector.rb', line 121

def self.warn_otel_only_disabled
  # Buildkite log output renders ANSI colour even though it isn't a TTY.
  red, reset = if $stderr.tty? || ENV["BUILDKITE"]
    ["\e[31;1m", "\e[0m"]
  else
    ["", ""]
  end

  warn <<~MESSAGE
    #{red}
    ############################################################
    ##                                                        ##
    ##  buildkite-test_collector: NO TEST RESULTS UPLOADED!   ##
    ##                                                        ##
    ##  otel_only is set, but OpenTelemetry could not be      ##
    ##  configured (see the warning above). This mode has no  ##
    ##  JSON fallback, so this run will upload NO results to  ##
    ##  Buildkite Test Engine.                                ##
    ##                                                        ##
    ############################################################
    #{reset}
  MESSAGE
end