bitfab 0.33.6 → 0.34.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: 1d72d477d9b3533bbba990387aeaf342774ec7e3271e11e51fc297a7de9534bb
4
- data.tar.gz: 624832fc2f8b1e526370a6c07b75fcb36dc5ffe62871ed888392e3f3d9eeb5ca
3
+ metadata.gz: 897fb65140b86333bb5012746e570c785560c7865cd79da1e872e4ec567fada2
4
+ data.tar.gz: 26b89a73668a0f86746a349c8f57fe39317f069dbab04dd8731ff91950d24a49
5
5
  SHA512:
6
- metadata.gz: d8e17a56b93c6ab2ab59e5ba5f5bb264f68329f19f7c1801c10e750609be96b3ae2be5dca4117aeae6e41f2d62ae5be757188c6a85bc98548290f1afe6b0aef1
7
- data.tar.gz: 7de5823eb5e244c4576723234d7ad6812fded976cd8f22837585a563ded544906d0f2c91114eb748d971056563fe2301a17ce937473538d8c3e507870a5e9929
6
+ metadata.gz: cb837f5a94d68391c0b25ac1bc073cba636816270abd623b825b59a2a3d8f387ac67a49aaef93aebcc3201ad24dbe4ef9f4d4738955d5db5387181eb1f94ee68
7
+ data.tar.gz: 0b609be82dca49043083bf966b52b01466f8252abe15b243e4d2ac6a3ea06d5238fb5fdf7b88e7a2c1be22fa4c36b06425baa2261a62ffed6967b5e8c86a8150
data/README.md CHANGED
@@ -20,7 +20,6 @@ gem install bitfab
20
20
 
21
21
  - Ruby >= 3.4
22
22
  - Runtime dependencies: `base64`, `opentelemetry-sdk` (`>= 1.2, < 2`, verified against 1.2 through 1.13, so an application already pinning its own OpenTelemetry version does not have to move)
23
- - Optional: `opentelemetry-exporter-otlp`, only to deliver through an OpenTelemetry Collector. Use a version compatible with your `opentelemetry-sdk`; if the gem cannot load, the SDK warns once and keeps delivering directly to Bitfab
24
23
 
25
24
  ## Quick Start
26
25
 
@@ -391,41 +390,17 @@ worker, export scheduling, flush, and shutdown lifecycle. Pipelines are created
391
390
  trace send and are never installed globally, so an unused or disabled client starts no OTel worker
392
391
  and the SDK does not replace an application's own OpenTelemetry setup.
393
392
 
394
- By default, batches are sent directly to Bitfab as OTLP/JSON. To send through a local OTel Collector
395
- instead, add the `opentelemetry-exporter-otlp` gem and set `BITFAB_OTEL_EXPORTER_ENDPOINT` to the
396
- Collector's OTLP/HTTP base URL (for example, `http://localhost:4318`). The SDK appends `/v1/traces`
397
- and sends protobuf through OTel's official exporter. Production and replay traffic share the same
398
- pipeline. Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API
399
- until every expected trace completion and span count is persisted, so Collector acceptance is never
400
- mistaken for Bitfab persistence.
401
-
402
- Direct requests carry at most eight carriers each and up to 32 run concurrently; set
403
- `BITFAB_OTEL_EXPORT_CONCURRENCY` to an integer from `1` through `64` to tune that. Direct JSON and
404
- Collector protobuf requests are partitioned at roughly 3 MB; set `BITFAB_OTEL_MAX_REQUEST_BYTES` to
405
- a positive integer no greater than `3000000` for a Collector or proxy with a stricter limit.
406
- Invalid values warn once and fall back to the defaults. If Bitfab accepts only part of a batch, it
407
- returns the standard OTLP `partialSuccess` response and the SDK warns with the rejected-span count
408
- and reason.
409
-
410
- ```yaml
411
- receivers:
412
- otlp:
413
- protocols:
414
- http:
415
-
416
- exporters:
417
- otlphttp/bitfab:
418
- endpoint: https://bitfab.ai/api/sdk/otel
419
- encoding: json
420
- headers:
421
- Authorization: Bearer ${env:BITFAB_API_KEY}
422
-
423
- service:
424
- pipelines:
425
- traces:
426
- receivers: [otlp]
427
- exporters: [otlphttp/bitfab]
428
- ```
393
+ Requests carry at most eight carriers each and up to 32 run concurrently; set
394
+ `BITFAB_OTEL_EXPORT_CONCURRENCY` to an integer from `1` through `64` to tune that. Requests are
395
+ partitioned at roughly 3 MB; set `BITFAB_OTEL_MAX_REQUEST_BYTES` to a positive integer no greater
396
+ than `3000000` for a proxy with a stricter limit. Invalid values warn once and fall back to the
397
+ defaults. Each carrier is encoded once and the request body is assembled from those encodings, so a
398
+ batch is never re-encoded to measure its size. If Bitfab accepts only part of a batch, it returns
399
+ the standard OTLP `partialSuccess` response and the SDK warns with the rejected-span count and
400
+ reason.
401
+
402
+ Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API until every
403
+ expected trace completion and span count is persisted.
429
404
 
430
405
  See the [OpenTelemetry Transport Architecture](https://docs.bitfab.ai/otel-architecture) for the
431
406
  full component ownership, carrier format, replay barrier, batching, and lifecycle design.
@@ -51,6 +51,12 @@ module Bitfab
51
51
  # Make a POST request to the Bitfab API.
52
52
  # Returns parsed JSON response hash.
53
53
  def request(endpoint, payload, timeout: nil, max_retries: 1, retry_delay: 0.1)
54
+ send_encoded(endpoint, Serialize.safe_generate(payload), timeout:, max_retries:, retry_delay:)
55
+ end
56
+
57
+ # POST an already-encoded body. The span transport encodes its own batches,
58
+ # so routing them back through #request would encode the same data twice.
59
+ def send_encoded(endpoint, body, timeout: nil, max_retries: 1, retry_delay: 0.1)
54
60
  uri = URI("#{@service_url}#{endpoint}")
55
61
  request_timeout = timeout || @timeout
56
62
 
@@ -63,7 +69,7 @@ module Bitfab
63
69
  http.read_timeout = request_timeout
64
70
 
65
71
  req = Net::HTTP::Post.new(uri.path, headers)
66
- req.body = Serialize.safe_generate(payload)
72
+ req.body = body
67
73
 
68
74
  response = http.request(req)
69
75
 
@@ -260,14 +266,13 @@ module Bitfab
260
266
  end
261
267
 
262
268
  @transport ||= Transport.create_trace_transport(
263
- api_key: @api_key,
264
269
  direct_sender: method(:send_transport_request)
265
270
  )
266
271
  end
267
272
  end
268
273
 
269
- def send_transport_request(endpoint, payload, timeout)
270
- request(endpoint, payload, timeout:, max_retries: 1)
274
+ def send_transport_request(endpoint, body, timeout)
275
+ send_encoded(endpoint, body, timeout:, max_retries: 1)
271
276
  end
272
277
 
273
278
  # Normalize each entry to a hash with stable string keys, accepting either
data/lib/bitfab/otel.rb CHANGED
@@ -13,20 +13,17 @@ module Bitfab
13
13
  # OpenTelemetry delivery for Bitfab spans and trace completions.
14
14
  #
15
15
  # Bitfab payloads ride as OTLP span attributes (bitfab.operation +
16
- # bitfab.payload) so a batch can travel either straight to Bitfab as
17
- # OTLP/JSON or through a customer's OTel Collector as protobuf, without the
16
+ # bitfab.payload), so a batch travels to Bitfab as OTLP/JSON without the
18
17
  # payload itself changing shape.
19
18
  module Otel
20
19
  OPERATION_ATTRIBUTE = "bitfab.operation"
21
20
  PAYLOAD_ATTRIBUTE = "bitfab.payload"
22
21
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces"
23
- COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT"
24
22
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES"
25
23
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY"
26
24
  MAX_EXPORT_REQUEST_BYTES = 3_000_000
27
25
  MAX_QUEUE_SIZE = 8_192
28
26
  DIRECT_MAX_EXPORT_BATCH_SIZE = 512
29
- COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32
30
27
  DIRECT_MAX_REQUEST_BATCH_SIZE = 8
31
28
  DEFAULT_EXPORT_CONCURRENCY = 32
32
29
  MAX_EXPORT_CONCURRENCY = 64
@@ -35,9 +32,25 @@ module Bitfab
35
32
  EXPORT_RETRIES = 3
36
33
  RETRY_DELAY_SECONDS = 0.1
37
34
 
35
+ # The comma that joins adjacent spans in the request's span list.
36
+ SPAN_SEPARATOR_BYTES = 1
37
+
38
38
  SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
39
39
  FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
40
40
 
41
+ # A span encoded exactly as it goes on the wire, carrying its byte count.
42
+ # Encoding once and remembering the size is what keeps request packing
43
+ # linear: sizing a candidate batch by re-encoding the whole request
44
+ # re-escapes every carrier's bitfab.payload string on every span considered.
45
+ EncodedSpan = Struct.new(:encoded, :size)
46
+
47
+ # The invariant head and tail of an OTLP request for one export window. Key
48
+ # order matches what JSON.generate emits for the equivalent Hash, so a body
49
+ # assembled by concatenation is byte-identical to encoding that Hash.
50
+ RequestEnvelope = Struct.new(:head, :tail, :size)
51
+
52
+ RequestBatch = Struct.new(:spans, :size)
53
+
41
54
  SPAN_KINDS = {
42
55
  internal: 1,
43
56
  server: 2,
@@ -82,11 +95,9 @@ module Bitfab
82
95
  DEFAULT_EXPORT_CONCURRENCY
83
96
  end
84
97
 
85
- def create_transport(api_key:, direct_sender:)
98
+ def create_transport(direct_sender:)
86
99
  BatchTransport.new(
87
- api_key:,
88
100
  direct_sender:,
89
- collector_endpoint: collector_endpoint_from_env,
90
101
  export_concurrency: export_concurrency_from_env,
91
102
  max_request_bytes: max_request_bytes_from_env
92
103
  )
@@ -177,11 +188,6 @@ module Bitfab
177
188
 
178
189
  private
179
190
 
180
- def collector_endpoint_from_env
181
- endpoint = ENV[COLLECTOR_ENDPOINT_ENV]
182
- (endpoint.nil? || endpoint.empty?) ? nil : endpoint
183
- end
184
-
185
191
  def record_span_submission(source_trace_id, payload)
186
192
  raw_span = payload["rawSpan"]
187
193
  span_id = raw_span["id"] if raw_span.is_a?(Hash)
@@ -237,7 +243,8 @@ module Bitfab
237
243
  return SUCCESS if spans.empty?
238
244
 
239
245
  begin
240
- encoded = spans.map { |span| Encoder.span_to_otlp(span) }
246
+ encoded = spans.map { |span| Encoder.encode_span(span) }
247
+ envelope = Encoder.request_envelope(spans.first)
241
248
  rescue => e
242
249
  Bitfab.warn_once(
243
250
  "otel-encode-failed",
@@ -246,8 +253,8 @@ module Bitfab
246
253
  return FAILURE
247
254
  end
248
255
 
249
- batches = build_request_batches(spans.first, encoded)
250
- results = send_batches(spans.first, batches)
256
+ batches = build_request_batches(envelope, encoded)
257
+ results = send_batches(envelope, batches)
251
258
  results.all? ? SUCCESS : FAILURE
252
259
  end
253
260
 
@@ -261,7 +268,7 @@ module Bitfab
261
268
 
262
269
  private
263
270
 
264
- def send_batches(first, batches)
271
+ def send_batches(envelope, batches)
265
272
  results = Array.new(batches.size, false)
266
273
  next_index = 0
267
274
  index_mutex = Mutex.new
@@ -277,7 +284,7 @@ module Bitfab
277
284
  end
278
285
  break if index >= batches.size
279
286
 
280
- results[index] = send_batch(first, batches[index])
287
+ results[index] = send_batch(envelope, batches[index])
281
288
  end
282
289
  end
283
290
  end
@@ -285,40 +292,37 @@ module Bitfab
285
292
  results
286
293
  end
287
294
 
288
- def build_request_batches(first, encoded)
295
+ def build_request_batches(envelope, encoded)
289
296
  batches = []
290
297
  current = []
298
+ size = envelope.size
291
299
 
292
300
  encoded.each do |span|
293
- if current.size >= @max_request_batch_size
294
- batches << current
301
+ addition = span.size + (current.empty? ? 0 : SPAN_SEPARATOR_BYTES)
302
+ if !current.empty? && (current.size >= @max_request_batch_size || size + addition > @max_request_bytes)
303
+ batches << RequestBatch.new(current, size)
295
304
  current = []
305
+ size = envelope.size
306
+ addition = span.size
296
307
  end
297
-
298
- candidate = current + [span]
299
- if !current.empty? && Encoder.encoded_size(Encoder.build_request(first, candidate)) > @max_request_bytes
300
- batches << current
301
- current = [span]
302
- else
303
- current = candidate
304
- end
308
+ current << span
309
+ size += addition
305
310
  end
306
311
 
307
- batches << current unless current.empty?
312
+ batches << RequestBatch.new(current, size) unless current.empty?
308
313
  batches
309
314
  end
310
315
 
311
- def send_batch(first, spans)
312
- payload = Encoder.build_request(first, spans)
313
- if Encoder.encoded_size(payload) > @max_request_bytes
314
- warn_oversized(spans.size)
316
+ def send_batch(envelope, batch)
317
+ if batch.size > @max_request_bytes
318
+ warn_oversized(batch.spans.size)
315
319
  return false
316
320
  end
317
321
 
318
- send_with_retries(payload)
322
+ send_with_retries(Encoder.encode_request(envelope, batch.spans))
319
323
  true
320
324
  rescue PayloadTooLargeError
321
- warn_oversized(spans.size)
325
+ warn_oversized(batch.spans.size)
322
326
  false
323
327
  rescue PartialSuccessError
324
328
  false
@@ -339,11 +343,11 @@ module Bitfab
339
343
  )
340
344
  end
341
345
 
342
- def send_with_retries(payload)
346
+ def send_with_retries(body)
343
347
  attempt = 0
344
348
  begin
345
349
  attempt += 1
346
- response = @direct_sender.call(OTLP_TRACES_ENDPOINT, payload, EXPORT_TIMEOUT_MILLIS / 1000.0)
350
+ response = @direct_sender.call(OTLP_TRACES_ENDPOINT, body, EXPORT_TIMEOUT_MILLIS / 1000.0)
347
351
  check_partial_success(response)
348
352
  rescue PartialSuccessError
349
353
  raise
@@ -386,72 +390,6 @@ module Bitfab
386
390
  end
387
391
  end
388
392
 
389
- # Splits an export into requests no larger than the configured byte target
390
- # before handing them to the wrapped Collector exporter.
391
- class SizeLimitedExporter
392
- def initialize(exporter, max_request_bytes)
393
- @exporter = exporter
394
- @max_request_bytes = max_request_bytes
395
- # The OTLP gem exposes no public way to measure an encoded request, so
396
- # requests are partitioned by count alone when its encoder is missing.
397
- @measurable = exporter.respond_to?(:encode, true)
398
- end
399
-
400
- def export(spans, timeout: nil)
401
- batches = partition(spans.to_a)
402
- batches.reduce(SUCCESS) do |result, batch|
403
- exported = @exporter.export(batch, timeout:)
404
- (exported == SUCCESS) ? result : FAILURE
405
- end
406
- end
407
-
408
- def force_flush(timeout: nil)
409
- @exporter.force_flush(timeout:)
410
- end
411
-
412
- def shutdown(timeout: nil)
413
- @exporter.shutdown(timeout:)
414
- end
415
-
416
- private
417
-
418
- def partition(spans)
419
- return [spans] if !@measurable || spans.empty?
420
-
421
- batches = []
422
- current = []
423
- current_size = 0
424
-
425
- spans.each do |span|
426
- size = span_bytes(span)
427
- return [spans] if size.nil?
428
-
429
- if !current.empty? && current_size + size > @max_request_bytes
430
- batches << current
431
- current = []
432
- current_size = 0
433
- end
434
- current << span
435
- current_size += size
436
- end
437
-
438
- batches << current unless current.empty?
439
- batches
440
- end
441
-
442
- # The OTLP gem's encoder returns a serialized String on current versions
443
- # and a protobuf message on older ones, and returns nil when it cannot
444
- # encode a span at all. Anything unmeasurable falls back to count-based
445
- # partitioning rather than failing the export from here.
446
- def span_bytes(span)
447
- encoded = @exporter.send(:encode, [span])
448
- return encoded.bytesize if encoded.respond_to?(:bytesize)
449
- return encoded.to_proto.bytesize if encoded.respond_to?(:to_proto)
450
-
451
- nil
452
- end
453
- end
454
-
455
393
  # Records failed exports so a flush can report delivery, not just drain.
456
394
  class DeliveryTrackingExporter
457
395
  def initialize(exporter)
@@ -494,15 +432,13 @@ module Bitfab
494
432
  class BatchTransport
495
433
  attr_reader :owner_pid
496
434
 
497
- def initialize(api_key:, direct_sender:, collector_endpoint: nil, max_export_batch_size: nil,
435
+ def initialize(direct_sender:, max_export_batch_size: nil,
498
436
  max_request_batch_size: DIRECT_MAX_REQUEST_BATCH_SIZE, max_queue_size: MAX_QUEUE_SIZE,
499
437
  export_concurrency: DEFAULT_EXPORT_CONCURRENCY, max_request_bytes: nil)
500
438
  raise ArgumentError, "max_request_batch_size must be a positive integer" unless max_request_batch_size.positive?
501
439
 
502
- @api_key = api_key
503
440
  @direct_sender = direct_sender
504
- @collector_endpoint = collector_endpoint
505
- @max_export_batch_size = max_export_batch_size || default_export_batch_size
441
+ @max_export_batch_size = max_export_batch_size || DIRECT_MAX_EXPORT_BATCH_SIZE
506
442
  @max_request_batch_size = max_request_batch_size
507
443
  @max_queue_size = max_queue_size
508
444
  @export_concurrency = export_concurrency
@@ -620,10 +556,6 @@ module Bitfab
620
556
  nil
621
557
  end
622
558
 
623
- def default_export_batch_size
624
- @collector_endpoint.nil? ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE
625
- end
626
-
627
559
  # A forked child inherits the parent's provider and processor but none of
628
560
  # its threads, so the pipeline is rebuilt on the first submit after a fork.
629
561
  # Callers hold @state_mutex, so exactly one thread rebuilds.
@@ -667,27 +599,6 @@ module Bitfab
667
599
  end
668
600
 
669
601
  def create_exporter
670
- return direct_exporter if @collector_endpoint.nil?
671
-
672
- require_relative "otel_collector"
673
- SizeLimitedExporter.new(
674
- CollectorExporter.new(
675
- api_key: @api_key,
676
- endpoint: CollectorExporter.traces_endpoint(@collector_endpoint),
677
- timeout: EXPORT_TIMEOUT_MILLIS / 1000.0
678
- ),
679
- @max_request_bytes
680
- )
681
- rescue LoadError
682
- Bitfab.warn_once(
683
- "otel-collector-gem-missing",
684
- "#{COLLECTOR_ENDPOINT_ENV} is set but the opentelemetry-exporter-otlp " \
685
- "gem is not installed; sending spans directly to Bitfab instead"
686
- )
687
- direct_exporter
688
- end
689
-
690
- def direct_exporter
691
602
  DirectExporter.new(
692
603
  direct_sender: @direct_sender,
693
604
  max_request_bytes: @max_request_bytes,
@@ -700,21 +611,22 @@ module Bitfab
700
611
  # Turns finished carrier spans into the OTLP/JSON shapes Bitfab ingests.
701
612
  module Encoder
702
613
  class << self
703
- def build_request(first, spans)
614
+ def encode_span(span)
615
+ encoded = JSON.generate(span_to_otlp(span))
616
+ EncodedSpan.new(encoded, encoded.bytesize)
617
+ end
618
+
619
+ def request_envelope(first)
704
620
  scope = first.instrumentation_scope
705
- {
706
- "resourceSpans" => [{
707
- "resource" => {"attributes" => attributes(first.resource.attribute_enumerator.to_h)},
708
- "scopeSpans" => [{
709
- "scope" => {"name" => scope.name, "version" => scope.version || ""},
710
- "spans" => spans
711
- }]
712
- }]
713
- }
621
+ resource = JSON.generate({"attributes" => attributes(first.resource.attribute_enumerator.to_h)})
622
+ scope_json = JSON.generate({"name" => scope.name, "version" => scope.version || ""})
623
+ head = %({"resourceSpans":[{"resource":#{resource},"scopeSpans":[{"scope":#{scope_json},"spans":[)
624
+ tail = "]}]}]}"
625
+ RequestEnvelope.new(head, tail, (head + tail).bytesize)
714
626
  end
715
627
 
716
- def encoded_size(value)
717
- JSON.generate(value).bytesize
628
+ def encode_request(envelope, spans)
629
+ envelope.head + spans.map(&:encoded).join(",") + envelope.tail
718
630
  end
719
631
 
720
632
  def span_to_otlp(span)
@@ -7,8 +7,8 @@ module Bitfab
7
7
  # mechanism, so callers never depend on OpenTelemetry directly.
8
8
  module Transport
9
9
  class << self
10
- def create_trace_transport(api_key:, direct_sender:)
11
- Otel.create_transport(api_key:, direct_sender:)
10
+ def create_trace_transport(direct_sender:)
11
+ Otel.create_transport(direct_sender:)
12
12
  end
13
13
 
14
14
  def flush_trace_transports(timeout = 30.0)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.33.6"
4
+ VERSION = "0.34.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.6
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -43,20 +43,6 @@ dependencies:
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
45
  version: '2'
46
- - !ruby/object:Gem::Dependency
47
- name: opentelemetry-exporter-otlp
48
- requirement: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: '0.30'
53
- type: :development
54
- prerelease: false
55
- version_requirements: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: '0.30'
60
46
  - !ruby/object:Gem::Dependency
61
47
  name: rake
62
48
  requirement: !ruby/object:Gem::Requirement
@@ -157,7 +143,6 @@ files:
157
143
  - lib/bitfab/http_client.rb
158
144
  - lib/bitfab/mock_override.rb
159
145
  - lib/bitfab/otel.rb
160
- - lib/bitfab/otel_collector.rb
161
146
  - lib/bitfab/replay.rb
162
147
  - lib/bitfab/replay_branch.rb
163
148
  - lib/bitfab/serialize.rb
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "opentelemetry-exporter-otlp"
4
-
5
- module Bitfab
6
- module Otel
7
- # OTLP/HTTP protobuf delivery to a customer's Collector.
8
- class CollectorExporter < OpenTelemetry::Exporter::OTLP::Exporter
9
- def self.traces_endpoint(endpoint)
10
- base = endpoint.chomp("/")
11
- base.end_with?("/v1/traces") ? base : "#{base}/v1/traces"
12
- end
13
-
14
- def initialize(api_key:, **options)
15
- super(**options)
16
- @api_key = api_key
17
- end
18
-
19
- # The OTLP exporter reads headers once at construction, while a Bitfab key
20
- # is resolved at request time (an env var may load after the client is built).
21
- def export(span_data, timeout: nil)
22
- key = @api_key.respond_to?(:call) ? @api_key.call : @api_key
23
- @headers = @headers.merge("Authorization" => "Bearer #{key}")
24
- super
25
- end
26
- end
27
- end
28
- end