buildkite-test_collector 2.14.0 → 2.15.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10535c306b5b29856f36335ba71e8f703b994605136545e9b1e3ed206ca4fe9d
4
- data.tar.gz: 3c8115481fb1cfd98a7cee11e7f05347cc3398bcc81f2f7494044a48cb811d27
3
+ metadata.gz: de04f3cd718a70ec62965dd2e491c5eca981e9781118b3ab7c6a7c05e6ad7655
4
+ data.tar.gz: c4a0fdbf14634c404ff6ade3db7d639b3f22fa4c5c6e21ba083ef92313ca6743
5
5
  SHA512:
6
- metadata.gz: d4188a10501a2bb5b32bde498ed360ac8bbbb733e199899c7bb51c40411f6c3ab96b6f3bef90fb79e4901168fad21f88f7e911765988df63ae47d03a120dbaef
7
- data.tar.gz: 7903643c37b1b65b9f0a38612253d6c8da084b56cab623f2e3782deacdb0ba3d631b6d17e2e40c796542226b9b00b996606ef38bd67181df6bdc9f99c7594d38
6
+ metadata.gz: 8682918b8784f2544a3da68e0a6dfd00ce8b2ab8decd9eb24bb0d458381c402697f3d20799bff9d38bd9ebec4a4f48e5ffe5b89d80ce88c89ffc7f54130e573a
7
+ data.tar.gz: 2c1316f5b43bc3ea14df5aeae2e48496755edfe1892594403df6af499efe5b2aecbef4b1c3f8c5289100b4cacaeffc63da917109611cc5b84d0122a39eb8052c
@@ -1,12 +1,48 @@
1
+ # Monorepo release pipeline for test-collector-ruby, uploaded by the
2
+ # release dispatcher (.buildkite/pipeline.release.yml) running on the
3
+ # separate bktest-release Buildkite pipeline — never by CI: as a rehearsal
4
+ # when this project changes (no tag), and for real on a
5
+ # test-collector-ruby/vX.Y.Z tag. Injected by `ctg gather` from the ctg
6
+ # repo's overlay/ directory during the dry-run era; post-cutover this file
7
+ # is maintained here, in the monorepo. It replaces the source repo's
8
+ # .buildkite/pipeline.release.yml, which assumed the repo root was the
9
+ # project root.
10
+ #
11
+ # Safety model (see docs/publishing.md in the ctg repo):
12
+ # - The build step is side-effect-free: it builds the gem, checks
13
+ # tag/version consistency, and queries RubyGems read-only. It runs on
14
+ # every rehearsal, so rehearsals exercise the release code. It never sees
15
+ # RELEASE_EXECUTE (deliberately not propagated into its container).
16
+ # - The publish steps mint a push token via the rubygems.org OIDC API key
17
+ # role, whose access policy trusts this pipeline (see the credential
18
+ # cut-over section of ctg docs/publishing.md).
19
+ # - The publish steps exist only on an exact test-collector-ruby/vX.Y.Z
20
+ # tag AND RELEASE_EXECUTE=true set on the build, behind a block step;
21
+ # the release script independently re-checks both before `gem push`.
22
+
1
23
  agents:
2
24
  queue: hosted
3
25
 
4
26
  steps:
5
- - block: "OK to release?"
27
+ - label: ":rubygems: test-collector-ruby: build + rehearse release"
28
+ key: ruby-build
29
+ command: test-collector-ruby/.buildkite/steps/release
30
+ plugins:
31
+ - docker#v5.12.0:
32
+ image: "ruby:3.4"
33
+ environment:
34
+ - BUILDKITE_TAG
6
35
 
7
- - command: ".buildkite/steps/release-gem"
8
- label: ":rubygems:"
9
- if: build.tag != null
36
+ - block: ":rocket: Publish test-collector-ruby to RubyGems?"
37
+ key: ruby-confirm
38
+ if: build.tag =~ /^test-collector-ruby\/v[0-9]+\.[0-9]+\.[0-9]+$/ && build.env("RELEASE_EXECUTE") == "true"
39
+
40
+ - label: ":rubygems: test-collector-ruby: publish to RubyGems"
41
+ if: build.tag =~ /^test-collector-ruby\/v[0-9]+\.[0-9]+\.[0-9]+$/ && build.env("RELEASE_EXECUTE") == "true"
42
+ depends_on:
43
+ - ruby-build
44
+ - ruby-confirm
45
+ command: test-collector-ruby/.buildkite/steps/release
10
46
  plugins:
11
47
  - rubygems-oidc#v0.2.0:
12
48
  role: "rg_oidc_akr_fy1x4px4yjwd1rdhkkda"
@@ -15,4 +51,4 @@ steps:
15
51
  environment:
16
52
  - GEM_HOST_API_KEY
17
53
  - BUILDKITE_TAG
18
-
54
+ - RELEASE_EXECUTE
@@ -0,0 +1,30 @@
1
+ # Monorepo pipeline for test-collector-ruby, uploaded by the top-level
2
+ # dispatcher when this project changes. Injected by `ctg gather` from the
3
+ # ctg repo's overlay/ directory during the dry-run era; post-cutover this
4
+ # file is maintained here, in the monorepo.
5
+ #
6
+ # Dry-run-safe: pure test steps only, derived from the source repo's
7
+ # buildkite.yaml with credentialed plugins (OIDC role assumption, ECR)
8
+ # removed, Docker Hub images, and a trimmed matrix.
9
+ #
10
+ # Jobs check out the monorepo root, so commands cd into this project.
11
+
12
+ agents:
13
+ queue: hosted
14
+
15
+ steps:
16
+ - group: ":ruby: test-collector-ruby"
17
+ steps:
18
+ - label: ":rspec: Ruby {{matrix}}"
19
+ command:
20
+ - "cd test-collector-ruby"
21
+ - "gem install bundler:2.3.25"
22
+ - "bundle"
23
+ - "bundle exec rake"
24
+ - "bundle exec cucumber"
25
+ plugins:
26
+ - docker#v3.7.0:
27
+ image: "ruby:{{matrix}}"
28
+ matrix:
29
+ - "latest"
30
+ - "3.3"
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Monorepo release script for test-collector-ruby: build the gem, check
3
+ # tag/version consistency and RubyGems state, then publish — but the publish
4
+ # side effect only happens when RELEASE_EXECUTE=true (set explicitly on the
5
+ # release build; see pipeline.release.yml). Everything before the push runs
6
+ # in rehearsals too, so dry-runs exercise this code.
7
+ #
8
+ # Injected by `ctg gather` from the ctg repo's overlay/ directory during the
9
+ # dry-run era; post-cutover this file is maintained here, in the monorepo.
10
+
11
+ set -euo pipefail
12
+
13
+ cd "$(dirname "$0")/../.." # this project's root, wherever the checkout cwd is
14
+
15
+ PROJECT="test-collector-ruby"
16
+ GEM_NAME="buildkite-test_collector"
17
+ TAG_PATTERN="^${PROJECT}/v([0-9]+\.[0-9]+\.[0-9]+)$"
18
+
19
+ echo "--- Determining version"
20
+ if [ -n "${BUILDKITE_TAG:-}" ]; then
21
+ if [[ ! "${BUILDKITE_TAG}" =~ ${TAG_PATTERN} ]]; then
22
+ echo "BUILDKITE_TAG '${BUILDKITE_TAG}' does not match ${PROJECT}/vX.Y.Z; refusing" >&2
23
+ exit 1
24
+ fi
25
+ VERSION="${BASH_REMATCH[1]}"
26
+ echo "version ${VERSION} (from tag ${BUILDKITE_TAG})"
27
+ else
28
+ VERSION="$(ruby -Ilib -rbuildkite/test_collector/version -e 'puts Buildkite::TestCollector::VERSION')"
29
+ echo "version ${VERSION} (from lib/buildkite/test_collector/version.rb; no tag)"
30
+ fi
31
+
32
+ GEM_FILENAME="${GEM_NAME}-${VERSION}.gem"
33
+
34
+ echo "--- Building gem"
35
+ gem build "${GEM_NAME}.gemspec"
36
+ if [ ! -f "${GEM_FILENAME}" ]; then
37
+ echo "ERROR: expected ${GEM_FILENAME}; does the gemspec/version.rb say ${VERSION}?" >&2
38
+ echo "gem files found:" >&2
39
+ ls -- *.gem >&2 || true
40
+ exit 1
41
+ fi
42
+
43
+ echo "--- Checking rubygems.org for existing version ${VERSION} (read-only)"
44
+ STATUS="$(curl -s -o /dev/null -w "%{http_code}" "https://rubygems.org/api/v2/rubygems/${GEM_NAME}/versions/${VERSION}.json")"
45
+ if [ "${STATUS}" = "200" ]; then
46
+ ALREADY_PUBLISHED=true
47
+ echo "${GEM_NAME} ${VERSION} is already on rubygems.org"
48
+ else
49
+ ALREADY_PUBLISHED=false
50
+ echo "${GEM_NAME} ${VERSION} is not on rubygems.org (HTTP ${STATUS})"
51
+ fi
52
+
53
+ if [ "${RELEASE_EXECUTE:-}" != "true" ]; then
54
+ echo "--- Rehearsal only"
55
+ echo "RELEASE_EXECUTE is not 'true': stopping before 'gem push ${GEM_FILENAME}'."
56
+ exit 0
57
+ fi
58
+
59
+ echo "--- Publishing ${GEM_FILENAME} to RubyGems"
60
+ if [ -z "${BUILDKITE_TAG:-}" ]; then
61
+ echo "RELEASE_EXECUTE=true without a ${PROJECT}/vX.Y.Z tag; refusing" >&2
62
+ exit 1
63
+ fi
64
+ if [ "${ALREADY_PUBLISHED}" = "true" ]; then
65
+ echo "version ${VERSION} already published; refusing" >&2
66
+ exit 1
67
+ fi
68
+ if [ -z "${GEM_HOST_API_KEY:-}" ]; then
69
+ echo "GEM_HOST_API_KEY not set (rubygems-oidc plugin missing or failed)" >&2
70
+ exit 1
71
+ fi
72
+ gem push "${GEM_FILENAME}"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v2.15.0
4
+
5
+ * Add [experimental, opt-in OpenTelemetry trace export](docs/opentelemetry.md)
6
+ for RSpec suites. Both modes share execution spans and run resources while
7
+ preserving execution roots; `otel_only` skips legacy trace capture and JSON
8
+ uploads. Standard OTLP header environment variables are supported for export
9
+ authentication. Export requires Ruby 3.3 or newer and the OpenTelemetry SDK
10
+ and OTLP exporter gems.
11
+
12
+ * Automatically tag executions with `ci.worker.id` from `BUILDKITE_AGENT_ID`,
13
+ so failures can be grouped by the agent that ran them. An explicit
14
+ caller-supplied `ci.worker.id` tag still takes precedence.
15
+
16
+ * Drop support for Ruby 2.x. Ruby 3.0 or newer is now required.
17
+
3
18
  ## v2.14.0
4
19
 
5
20
  * Send `location_prefix` in `run_env`
data/Gemfile CHANGED
@@ -8,3 +8,8 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+
12
+ if Gem.ruby_version >= Gem::Version.new("3.3")
13
+ gem "opentelemetry-exporter-otlp", "~> 0.34"
14
+ gem "opentelemetry-sdk", "~> 1.13"
15
+ end
data/README.md CHANGED
@@ -83,6 +83,102 @@ BUILDKITE_ANALYTICS_TOKEN=xyz cucumber
83
83
 
84
84
  Add the `BUILDKITE_ANALYTICS_TOKEN` secret to your CI, push your changes to a branch, and open a pull request 🎉
85
85
 
86
+ ### OpenTelemetry export (experimental)
87
+
88
+ RSpec suites can also send an OpenTelemetry trace per test execution to Buildkite,
89
+ showing what each test did and where it spent its time. Each trace is rooted in a
90
+ `test.execution` span carrying its name, location, result, and any failure detail.
91
+ Tags passed to `configure` appear as resource attributes, while `tag_execution`
92
+ adds attributes to the current test's root span.
93
+
94
+ This feature is still under development and may change. This first release is
95
+ intended for suites that do not already configure OpenTelemetry. It may work
96
+ with an existing OpenTelemetry setup, but that configuration is not yet
97
+ supported or guaranteed to work.
98
+
99
+ OpenTelemetry export is off by default. Opt in when you configure the collector:
100
+
101
+ ```ruby
102
+ Buildkite::TestCollector.configure(hook: :rspec, otel_enabled: true)
103
+ ```
104
+
105
+ Execution roots use a private AlwaysOn provider so a suite's sampling policy
106
+ cannot remove them.
107
+
108
+ The collector configures a global provider for child spans and installs all
109
+ applicable instrumentation registered when the suite starts. Because export is
110
+ optional and its dependencies require Ruby 3.3+, the collector does not install
111
+ them automatically. Add the OpenTelemetry SDK and OTLP exporter, plus any
112
+ instrumentation you want to use:
113
+
114
+ ```ruby
115
+ # Gemfile
116
+ gem "opentelemetry-exporter-otlp", "~> 0.34", require: false
117
+ gem "opentelemetry-sdk", "~> 1.13", require: false
118
+ gem "opentelemetry-instrumentation-pg", require: false
119
+
120
+ # spec/spec_helper.rb
121
+ require "opentelemetry-instrumentation-pg"
122
+ require "buildkite/test_collector"
123
+
124
+ Buildkite::TestCollector.configure(hook: :rspec, otel_enabled: true)
125
+ ```
126
+
127
+ Adding a gem to the Gemfile may auto-require it in applications that call
128
+ `Bundler.require`, but that is not guaranteed. An explicit `require` is the
129
+ recommended setup. To disable instrumentations and export only root
130
+ `test.execution` spans, set `otel_instrumentations: []`. Any other value is
131
+ reserved for a future release and disables span export with a warning. See the
132
+ [OpenTelemetry guide](docs/opentelemetry.md#choosing-instrumentation) for more.
133
+
134
+ Export needs Ruby 3.3 or newer, which is what the OpenTelemetry gems require. If
135
+ those gems are unavailable, the option is accepted and export remains disabled.
136
+
137
+ The collector honors standard `OTEL_EXPORTER_OTLP_TRACES_HEADERS` (or the
138
+ generic `OTEL_EXPORTER_OTLP_HEADERS`) and gives them precedence over its own
139
+ headers, including `Authorization`. bktec's OTLP relay uses this to provide its
140
+ local credential without changing `BUILDKITE_ANALYTICS_TOKEN`, which remains
141
+ available for normal JSON uploads in `otel_enabled` mode. Without an OTLP
142
+ Authorization header, spans use `BUILDKITE_ANALYTICS_TOKEN`, which must be an
143
+ agent OIDC token with the `write_uploads` scope; a suite API token still uploads
144
+ executions, but its spans are rejected.
145
+
146
+ Export failures never fail a test or block the normal Test Engine upload. See the
147
+ [OpenTelemetry guide](docs/opentelemetry.md) for setup details and current
148
+ limitations.
149
+
150
+ ### OTLP-only submission (experimental)
151
+
152
+ RSpec suites can go one step further and submit results *only* over OTLP, with
153
+ no JSON upload at all. It exports the same spans as `otel_enabled` and adds
154
+ `buildkite.execution.via=otlp`, which tells Buildkite to synthesize each test
155
+ execution from its span server-side:
156
+
157
+ ```ruby
158
+ Buildkite::TestCollector.configure(hook: :rspec, otel_only: true)
159
+ ```
160
+
161
+ In this mode the collector's legacy machinery is switched off: nothing is
162
+ uploaded to `/v1/uploads`, and `Net::HTTP` and `Object` are left unpatched. The
163
+ gem's whole job is to configure OpenTelemetry so each test gets a suitable span:
164
+
165
+ - `Buildkite::TestCollector.annotate` adds a `test.annotation` event to the
166
+ current span.
167
+ - `Buildkite::TestCollector.tag_execution` sets attributes on the test span.
168
+ - `tags:` given to `configure` become resource attributes on every span.
169
+ - Instrumentation works exactly as it does with `otel_enabled`: everything you
170
+ require and register installs, and `otel_instrumentations: []` exports only
171
+ the `test.execution` spans. See
172
+ [choosing instrumentation](docs/opentelemetry.md#choosing-instrumentation).
173
+ - Your code can also talk to OpenTelemetry directly — the collector configures
174
+ the global tracer provider, so
175
+ `OpenTelemetry::Trace.current_span.set_attribute(...)` works during a test,
176
+ and any instrumentation joins the test's trace.
177
+
178
+ `otel_only` is currently RSpec-only and has the same Ruby 3.3+ and OpenTelemetry
179
+ gem requirements as `otel_enabled`. It's an alternative to `otel_enabled`; the
180
+ two are mutually exclusive, and passing both (either value) raises
181
+ `ArgumentError`.
86
182
 
87
183
  ## More information
88
184
 
@@ -112,16 +208,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/buildk
112
208
 
113
209
  ## 🚀 Releasing
114
210
 
115
- 1. Bump the version in `version.rb` and run `bundle` to update the `Gemfile.lock`.
116
- 2. Update the CHANGELOG.md with your new version and a description of your changes.
117
- 3. Once your PR is merged to `main` git tag the merge commit and push:
118
-
119
- ```
120
- git tag vX.X.X
121
- git push origin vX.X.X
122
- ```
123
- 4. Visit the [release pipeline](https://buildkite.com/buildkite/test-collector-ruby-release) to unblock it and confirm the new version is pushed to rubygems.org
124
- 5. Create a [new release in github](https://github.com/buildkite/test-collector-ruby/releases).
211
+ See the monorepo's [collector release guide](../RELEASING.md#ruby-rubygems).
125
212
 
126
213
  ## 📜 MIT License
127
214
 
@@ -22,17 +22,13 @@ Gem::Specification.new do |spec|
22
22
  end
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
25
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
26
26
 
27
27
  spec.add_dependency "concurrent-ruby"
28
28
 
29
29
  spec.add_development_dependency "activesupport", ">= 4.2"
30
+ spec.add_development_dependency "cucumber", '~> 9.0'
30
31
  spec.add_development_dependency "ostruct"
31
32
  spec.add_development_dependency "rspec-core", '~> 3.10'
32
33
  spec.add_development_dependency "rspec-expectations", '~> 3.10'
33
-
34
- # When running the legacy CI builds against versions of Ruby pre 2.7 we cannot include cucumber 9 as it's not supported.
35
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
36
- spec.add_development_dependency "cucumber", '~> 9.0'
37
- end
38
34
  end
data/buildkite.yaml CHANGED
@@ -27,36 +27,29 @@ steps:
27
27
  - "latest"
28
28
  - "3.4"
29
29
  - "3.3"
30
+
31
+ - label: ":rspec: Tests (EOL Ruby) :ruby: {{matrix}}"
32
+ command:
33
+ - "gem install bundler:2.3.25"
34
+ - "bundle"
35
+ - "bundle exec rake"
36
+ - "bundle exec cucumber"
37
+ plugins:
38
+ - aws-assume-role-with-web-identity#v1.2.0:
39
+ role-arn: "${OIDC_ASSUME_ROLE_ARN:-arn:aws:iam::445615400570:role/pipeline-buildkite-test-collector-ruby-build}"
40
+ session-tags:
41
+ - organization_slug
42
+ - organization_id
43
+ - pipeline_slug
44
+ - build_branch
45
+
46
+ - ecr#v2.9.0:
47
+ login: true
48
+ account_ids: "public.ecr.aws"
49
+ region: "us-east-1"
50
+ - docker#v3.7.0:
51
+ image: "public.ecr.aws/docker/library/ruby:{{matrix}}"
52
+ matrix:
30
53
  - "3.2"
31
54
  - "3.1"
32
55
  - "3.0"
33
- - "2.7"
34
-
35
- - group: ":rspec: Legacy Ruby :ruby:"
36
- steps:
37
- - label: ":rspec: Tests :ruby: {{matrix}}"
38
- command:
39
- - "gem install bundler:2.3.25"
40
- - "bundle"
41
- - "bundle exec rake"
42
- plugins:
43
- - aws-assume-role-with-web-identity#v1.2.0:
44
- role-arn: "${OIDC_ASSUME_ROLE_ARN:-arn:aws:iam::445615400570:role/pipeline-buildkite-test-collector-ruby-build}"
45
- session-tags:
46
- - organization_slug
47
- - organization_id
48
- - pipeline_slug
49
- - build_branch
50
- - ecr#v2.9.0:
51
- login: true
52
- account_ids: "public.ecr.aws"
53
- region: "us-east-1"
54
- - docker#v3.7.0:
55
- # Images for older Ruby versions aren't available on AWS ECR
56
- # so fall back to Docker Hub
57
- image: "ruby:{{matrix}}"
58
- matrix:
59
- - "2.6"
60
- - "2.5"
61
- - "2.4"
62
- - "2.3"
@@ -0,0 +1,247 @@
1
+ # OpenTelemetry export (experimental)
2
+
3
+ > **This feature is still under development and may change.**
4
+ > This first release is intended for suites that do not already configure
5
+ > OpenTelemetry. Existing OpenTelemetry setups may work, but are not yet
6
+ > supported or guaranteed to work.
7
+
8
+ This page describes `otel_enabled`, where spans are exported *alongside* the
9
+ normal JSON upload and linked to it by trace ID. There is also an OTLP-only
10
+ mode (`otel_only`) that exports the same spans but adds
11
+ `buildkite.execution.via=otlp`. That marker makes the span itself the
12
+ submission: Buildkite synthesizes the execution from it server-side, with
13
+ nothing sent to `/v1/uploads`. In both modes the run's details (run key, branch,
14
+ commit, and any `tags:` you configure) travel as OpenTelemetry resource
15
+ attributes on the spans the collector exports. See the
16
+ [README](../README.md#otlp-only-submission-experimental) for how to turn it on,
17
+ and [OTLP-only attributes](#otlp-only-attributes) below for what's sent.
18
+
19
+ Every RSpec example gets an OpenTelemetry `test.execution` root. The collector
20
+ can configure a provider and export instrumented child spans showing what the
21
+ test did and where its time went. The traces are sent to Buildkite and shown
22
+ against the test's execution.
23
+
24
+ It is off by default. See the [README](../README.md#opentelemetry-export-experimental)
25
+ for how to turn it on.
26
+
27
+ Export requires Ruby 3.3 or newer and the `opentelemetry-sdk` and
28
+ `opentelemetry-exporter-otlp` gems. These optional dependencies are not installed
29
+ with `buildkite-test_collector`; add them to your bundle as shown in
30
+ [Choosing instrumentation](#choosing-instrumentation). This requirement applies
31
+ to both `otel_enabled` and `otel_only`.
32
+
33
+ ## What a trace looks like
34
+
35
+ Each example gets a `test.execution` span of its own, with the instrumented work
36
+ it did underneath:
37
+
38
+ ```text
39
+ test.execution "Buildkite::Pipeline creates a build" 12.4ms
40
+ ├── GET api.example.com 8.1ms
41
+ └── SELECT pipelines 1.2ms
42
+ ```
43
+
44
+ One example is one trace. The span is never nested under anything else, so a
45
+ trace always belongs to exactly one test. On Buildkite Agent v3.110 or newer,
46
+ the root span links to the Agent's propagated job trace when tracing is enabled,
47
+ letting you navigate between them without combining every test into one trace.
48
+
49
+ ## What's on the span
50
+
51
+ Each `tags:` entry passed to `configure` is attached to the root as a
52
+ `buildkite.tag.<key>` resource attribute. Per-execution tags use the same prefix
53
+ as span attributes.
54
+
55
+ | Attribute | Value |
56
+ | --- | --- |
57
+ | `buildkite.test.scope` | the example group |
58
+ | `buildkite.test.name` | the example's description |
59
+ | `test.case.name` | the example's full description |
60
+ | `test.suite.name` | the example group |
61
+ | `code.file.path` | the file the test is in |
62
+ | `code.line.number` | the line, or the call site for a shared example |
63
+ | `test.case.result.status` | `pass`, `fail` or `skipped` |
64
+ | `buildkite.test.execution.external_id` | the ID of the matching Test Engine execution |
65
+ | `buildkite.tag.<key>` | each `tag_execution` tag |
66
+
67
+ A failed test also sets the span's status to error with the failure summary as
68
+ its description. Each failure is recorded as a semconv `exception` event with
69
+ `exception.message` and `exception.stacktrace` attributes.
70
+
71
+ Two things worth knowing:
72
+
73
+ - An example skipped with `skip` produces no span at all. RSpec doesn't run its
74
+ hooks, so there is nothing to time. `skipped` on a span means a `pending`
75
+ example that failed as expected.
76
+ - The execution's duration is whatever the span timed, so the two always agree.
77
+ With the export off, the collector times the example itself as it always has.
78
+ - The result is RSpec's final verdict, read after every `around` hook has
79
+ unwound: an `around` hook that raises after the example ran counts as a
80
+ failure, and an example a hook marks `pending` before deliberately raising
81
+ stays skipped.
82
+
83
+ ## OTLP-only attributes
84
+
85
+ Both modes use the same resource and span attributes. With `otel_only`, the
86
+ `buildkite.execution.via=otlp` marker opts the span into execution synthesis.
87
+ Buildkite attributes are flat (`buildkite.run_key`, `buildkite.build_id`, ...),
88
+ matching the agent's own OpenTelemetry attributes; everything else follows
89
+ OpenTelemetry semantic conventions.
90
+
91
+ Run-level details travel once, as resource attributes on every span:
92
+
93
+ | Resource attribute | Value | Execution field |
94
+ | --- | --- | --- |
95
+ | `buildkite.run_key` | the run key (required) | run key |
96
+ | `buildkite.run_url` | the build URL | URL |
97
+ | `vcs.ref.head.name` | the branch (or tag) name | branch |
98
+ | `vcs.ref.head.revision` | the commit SHA | commit |
99
+ | `vcs.ref.head.type` | `branch` or `tag` | — |
100
+ | `buildkite.build_number` | the build number | number |
101
+ | `buildkite.build_id` | the build's UUID | build ID |
102
+ | `buildkite.job_id` | the job's UUID | job ID |
103
+ | `buildkite.step_id` | the step's UUID | step ID |
104
+ | `buildkite.message` | the commit message | message |
105
+ | `buildkite.collector.name` | this gem's name | collector |
106
+ | `buildkite.collector.version` | this gem's version | version |
107
+ | `buildkite.tag.<key>` | each `tags:` entry from `configure` | run tag `<key>` |
108
+
109
+ The resource also names the suite for any other OpenTelemetry backend looking
110
+ at the same spans: `service.name` (the suite slug), `service.namespace` (the
111
+ organization slug), `service.instance.id` (the job UUID), and
112
+ `buildkite.test.framework.name`/`.version`. Buildkite doesn't use these.
113
+
114
+ Each test's span carries the execution itself:
115
+
116
+ | Span attribute | Value | Execution field |
117
+ | --- | --- | --- |
118
+ | `buildkite.execution.via` | `otlp` in OTLP-only mode — opts this span in to synthesis | — |
119
+ | `buildkite.test.scope` | the example group | scope |
120
+ | `buildkite.test.name` | the example's description | name |
121
+ | `test.suite.name` | the example group | — |
122
+ | `test.case.name` | the example's full description | — |
123
+ | `code.file.path` | the file the test is in | file name, location |
124
+ | `code.line.number` | the line number | location |
125
+ | `test.case.result.status` | `pass`, `fail`, `skipped` | result |
126
+ | `buildkite.test.execution.external_id` | the execution's collector-generated ID | external ID |
127
+ | `buildkite.tag.<key>` | each `tag_execution` tag | execution tag `<key>` |
128
+
129
+ In OTLP-only mode the server maps the span's failure status and exception events
130
+ back to the execution's failure reason and expanded failure detail.
131
+
132
+ ## Finding a test's trace
133
+
134
+ With `otel_enabled`, the trace's ID is sent in the JSON execution so Buildkite
135
+ can show the two together. With `otel_only`, the span is the execution. Child
136
+ spans share the root's trace ID through normal context propagation, so one trace
137
+ holds everything the test did.
138
+
139
+ ## Recommended setup: suites without OpenTelemetry
140
+
141
+ The collector configures a global SDK provider for child spans and installs the
142
+ applicable instrumentation registered when the suite starts. The private
143
+ provider still owns `test.execution`; instrumented spans use the
144
+ collector-created provider's normal sampling. The same forwarding filter
145
+ excludes setup, teardown, detached traces, and other spans outside an active
146
+ execution.
147
+
148
+ ## Choosing instrumentation
149
+
150
+ Instrumentation selection applies only when the collector configures the global
151
+ provider, and works the same with `otel_enabled` and `otel_only`.
152
+ Add the OpenTelemetry SDK and OTLP exporter, plus the instrumentation you want,
153
+ to your bundle. Require each instrumentation explicitly:
154
+
155
+ ```ruby
156
+ # Gemfile
157
+ group :test do
158
+ gem "opentelemetry-exporter-otlp", "~> 0.34", require: false
159
+ gem "opentelemetry-sdk", "~> 1.13", require: false
160
+ gem "opentelemetry-instrumentation-pg", require: false
161
+ gem "opentelemetry-instrumentation-redis", require: false
162
+ end
163
+ ```
164
+
165
+ ```ruby
166
+ # spec/spec_helper.rb
167
+ require "opentelemetry-instrumentation-pg"
168
+ require "opentelemetry-instrumentation-redis"
169
+ require "buildkite/test_collector"
170
+
171
+ Buildkite::TestCollector.configure(
172
+ hook: :rspec,
173
+ otel_enabled: true,
174
+ )
175
+ ```
176
+
177
+ A Gemfile entry makes the gem available but does not always load it. Some
178
+ applications call `Bundler.require` and auto-require their gems, but that is
179
+ host-dependent and can be disabled with `require: false`. Explicitly requiring
180
+ each instrumentation is the recommended setup.
181
+
182
+ Requiring an instrumentation gem registers its definition; it does not install
183
+ the instrumentation immediately. The collector defers OpenTelemetry setup until
184
+ RSpec's `before(:suite)` hooks and asks the SDK to install all registered
185
+ instrumentation. The SDK skips instrumentation whose target library is absent
186
+ or incompatible and reports individual installation failures without stopping
187
+ the remaining installations.
188
+
189
+ To prevent the collector from installing registered instrumentation, pass an
190
+ empty list. Manually created spans under an execution are still forwarded:
191
+
192
+ ```ruby
193
+ Buildkite::TestCollector.configure(
194
+ hook: :rspec,
195
+ otel_enabled: true,
196
+ otel_instrumentations: [],
197
+ )
198
+ ```
199
+
200
+ For this release, omitting `otel_instrumentations` and setting it to `[]` are the
201
+ only supported choices. Any other value is reserved for a future release and
202
+ disables span export with a warning, in every path. The collector does not
203
+ inspect instrumentation patches, so compatibility between customer-selected
204
+ instrumentation and other APM or test-library patches remains the customer's
205
+ responsibility.
206
+
207
+ ## What gets sent
208
+
209
+ The collector merges standard `OTEL_EXPORTER_OTLP_TRACES_HEADERS` (or, when it
210
+ is absent, `OTEL_EXPORTER_OTLP_HEADERS`) over its own OTLP headers. Header names
211
+ are matched case-insensitively, so a standard `authorization` entry takes
212
+ precedence over the credential sourced from `BUILDKITE_ANALYTICS_TOKEN`. Empty
213
+ header environment variables are treated as unset.
214
+
215
+ bktec's OTLP relay uses the trace-specific header variable to provide its local
216
+ credential. bktec forwards spans to Buildkite with its OIDC credential while
217
+ `BUILDKITE_ANALYTICS_TOKEN` remains available for normal JSON uploads in
218
+ `otel_enabled` mode. Without an OTLP Authorization header, spans go directly to
219
+ Buildkite using `BUILDKITE_ANALYTICS_TOKEN`, which must be an agent OIDC token
220
+ with the `write_uploads` scope; a suite API token uploads test results as normal
221
+ but its spans are rejected.
222
+
223
+ OpenTelemetry's SDK owns batching, retries, and transport. `test.execution`
224
+ spans have a reserved, faster-draining queue and exporter. Forwarded children
225
+ use a separate queue and exporter, so a child flood or invalid child request
226
+ cannot displace or poison execution roots. When the suite finishes, both queues
227
+ share one 30-second flush budget, roots first. A hard exit or sustained endpoint
228
+ failure can still lose spans because the queues live in process memory.
229
+
230
+ One process reports one run. Export survives repeated suite runs in the same
231
+ process (warm workers), and a refreshed token is picked up when the collector
232
+ is reconfigured, but run identity is fixed when export starts: reconfiguring
233
+ with a different run key warns and keeps attributing results to the original
234
+ run. Reporting a new run requires a new process.
235
+
236
+ ## When something goes wrong
237
+
238
+ Export never fails a test. If root setup fails, the collector warns and the
239
+ normal test result upload continues without spans. If optional child setup or
240
+ attachment fails, the collector warns, cleans up that path, and continues
241
+ exporting roots. The suite-end flush and the process-exit shutdown each give
242
+ the OpenTelemetry SDK a 30-second budget to export buffered spans; the SDK's
243
+ own retry backoff can run past it when the endpoint keeps failing.
244
+
245
+ Export failures are reported through OpenTelemetry's own logger. The collector
246
+ also warns if its reserved root queue drops any `test.execution` spans; normal
247
+ child-span queue overflow is not logged by the OpenTelemetry SDK.
@@ -67,10 +67,14 @@ class Buildkite::TestCollector::CI
67
67
  end
68
68
 
69
69
  def github_actions
70
+ repository = ENV["GITHUB_REPOSITORY"]
71
+ run_id = ENV["GITHUB_RUN_ID"]
72
+ url = File.join("https://github.com", repository, "actions/runs", run_id) if repository && run_id
73
+
70
74
  {
71
75
  "CI" => "github_actions",
72
76
  "key" => "#{ENV["GITHUB_ACTION"]}-#{ENV["GITHUB_RUN_NUMBER"]}-#{ENV["GITHUB_RUN_ATTEMPT"]}",
73
- "url" => File.join("https://github.com", ENV["GITHUB_REPOSITORY"], "actions/runs", ENV["GITHUB_RUN_ID"]),
77
+ "url" => url,
74
78
  "branch" => ENV["GITHUB_REF_NAME"],
75
79
  "commit_sha" => ENV["GITHUB_SHA"],
76
80
  "number" => ENV["GITHUB_RUN_NUMBER"],