foam-otel 3.0.0.alpha.9 → 3.0.0.alpha.12

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: 240906547c665dd40513921db5ab9f00d65febef81d40848faa91a1f5ae02e15
4
- data.tar.gz: 9a2c4c5045ba080c21b165e1a87836cd9288408343b049c293352162e9be7083
3
+ metadata.gz: b67fa02fd12d317a0bceff3c596f63d928b0187995d02e95191f9ecb05b7383e
4
+ data.tar.gz: 6933cac173a8df6fa83fe474f06fd59569d0da61eb4a78f16919c695561b63af
5
5
  SHA512:
6
- metadata.gz: f891d7d8a749ce8a9042f08183581ec43924445d21137b863619a912cd0be0bb516e340d01e2ed6e238356a3b61f3cedb5739b769b04e38213d785712616cb9f
7
- data.tar.gz: dd19f649f254cff7a00da71b14f158cc3d3d370a3affcc587ec86156d5439aaecd956564e106c54f0c0b2d229195065f69c03ab3eae9058459265ea479b71aaf
6
+ metadata.gz: a7080090c2024fda65b0bf8d76edbe311961ae7c26814cd3154c11e9c7735e9730c10e100468d1046acc94e1cb2b2d1518b50cdc72b631201187b271b167c722
7
+ data.tar.gz: 184764049f1395f94f270cec7054138cfd78c8f6b40c93c3cafafdcb4cd7a3754bfa1eed0851e47d1f96258f10e1dea51889624862e07f86579e42d73a26353a
data/README.md CHANGED
@@ -227,7 +227,7 @@ Do not combine `init` and an ingest component for the same signal. The ingest co
227
227
 
228
228
  ### `inject_trace_context(carrier)`
229
229
 
230
- Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys.
230
+ Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys. A `tracestate` the carrier already holds is kept, since Foam adds no entry of its own and another SDK may have added one.
231
231
 
232
232
  Use it when the app sends a message itself (a Kafka record, a queue payload, a custom socket). The HTTP instrumentations already inject these headers; do not call it on ordinary HTTP requests. Without an active span it writes only `baggage`, and only when baggage is set.
233
233
 
@@ -252,7 +252,18 @@ end
252
252
 
253
253
  `inject_trace_context` and `extract_trace_context` always use the W3C `traceparent`, `tracestate`, and `baggage` headers, whatever propagator holds the global slot. Foam does not read `OTEL_PROPAGATORS`. An app that needs a different propagator registers it before `init`, and Foam leaves it in place.
254
254
 
255
- On outbound requests, Foam writes its propagation headers after any other SDK's. Another SDK's own propagation headers are left as written.
255
+ On outbound requests, Foam writes its propagation headers after any other SDK's. Another SDK's own propagation headers are left as written, and Foam's baggage entries are merged into the members it wrote to `baggage`. These writes go through the propagator in the global slot.
256
+
257
+ ### `propagator`
258
+
259
+ The propagator `inject_trace_context` and `extract_trace_context` use, for an app that sets its own composite in the global slot. List it last, so a caller's `traceparent` wins over another SDK's trace header:
260
+
261
+ ```ruby
262
+ OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
263
+ other_sdk_propagator,
264
+ Foam.propagator
265
+ ])
266
+ ```
256
267
 
257
268
  ### `set_baggage(key, value)`
258
269
 
@@ -432,7 +443,7 @@ Traces, metrics, and logs are flushed before the handler returns (including fail
432
443
 
433
444
  ## Pre-forking servers and threads
434
445
 
435
- TODO(jhartquist): pre-forking servers (Puma in cluster mode, Unicorn, Pitchfork, Resque): whether to call `init` before or after the fork, and what the SDK's batch processors and the metric reader do in the child. Foam registers no fork hook today.
446
+ Call `init` once, in the process that forks: the master of Puma in cluster mode with `preload_app!` (the default since Puma 7), the master of Unicorn with `preload_app true`, or a worker that forks a child per job. Each child exports its own traces, logs, and metrics, carrying its own `process.pid`.
436
447
 
437
448
  The current span and baggage are stored per fiber. A `Thread.new` or `Fiber.new` starts with none, so spans started inside it begin a new trace. To continue the trace, capture the context first and make it current inside the thread:
438
449
 
@@ -485,6 +496,50 @@ Foam.init(
485
496
  )
486
497
  ```
487
498
 
499
+ ### Sentry
500
+
501
+ Sentry sends its events to the DSN's host over HTTP, which Foam would otherwise record as spans. Pass that address and Sentry's request header to `init`. This works whether Sentry sends inline or from its background worker:
502
+
503
+ ```ruby
504
+ dsn = Sentry.configuration.dsn
505
+
506
+ Foam.init(
507
+ name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
508
+ ignored_outbound_hosts: ["#{dsn.host}:#{dsn.port}"],
509
+ ignored_outbound_headers: ["X-Sentry-Auth"]
510
+ )
511
+ ```
512
+
513
+ If the app puts Sentry's propagator in the global slot, through `sentry-opentelemetry`'s `config.otlp.setup_propagator` or `OpenTelemetry.propagation = Sentry::OpenTelemetry::Propagator.new`, outbound requests carry `sentry-trace` but no `traceparent`, so Foam's traces break between services. Once both `init` and that setting have run, wrap Sentry's propagator with Foam's:
514
+
515
+ ```ruby
516
+ OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
517
+ OpenTelemetry.propagation,
518
+ Foam.propagator
519
+ ])
520
+ ```
521
+
522
+ ### New Relic
523
+
524
+ New Relic's agent sends its data to its collector over HTTP, and probes the AWS, Azure and GCP metadata endpoints when it connects, which Foam would otherwise record as spans. Pass the collector's address and the metadata hosts to `init`. The agent's requests carry no header of its own, so there is none to pass:
525
+
526
+ ```ruby
527
+ collector = "#{NewRelic::Agent.config[:host]}:#{NewRelic::Agent.config[:port]}"
528
+
529
+ Foam.init(
530
+ name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
531
+ ignored_outbound_hosts: [
532
+ collector,
533
+ "169.254.169.254", # AWS and Azure metadata
534
+ "metadata.google.internal" # GCP metadata
535
+ ]
536
+ )
537
+ ```
538
+
539
+ Start New Relic before `init`, as Rails does through the agent's Railtie. If the agent instruments Net::HTTP in chain mode and starts after `init`, every Net::HTTP request raises `SystemStackError`. The agent uses chain mode when `instrumentation.net_http` is `chain`, or on its own next to ScoutApm, Rack::MiniProfiler, or Airbrake before 10.0.2. Where the order cannot change, set `instrumentation.net_http` to `prepend`.
540
+
541
+ If the app turns on New Relic's OpenTelemetry bridge (`opentelemetry.enabled`), New Relic takes the tracer provider and propagator, and Foam records no traces. Logs and metrics still reach Foam.
542
+
488
543
  ## Redaction
489
544
 
490
545
  Foam masks the values of sensitive keys in everything it exports: span, span event, and span link attributes; log bodies and attributes; metric data point attributes; and resource attributes. A built-in list of credential and personal-data keys always applies, matched by exact name or by a sensitive segment of the key (`legacy_api_key_2`, `stripeToken`). Keys match after normalization: case, `-`, `.`, and camelCase boundaries are folded, so `apiKey`, `api-key`, and `API_KEY` are one key. Header attributes match on the header name. SQL statements arrive from the database instrumentations with every literal replaced with `?` (see Instrumentation) and are redacted like any other string. Captured HTTP bodies are masked by content type with the same keys. When masking a body fails, the whole body is replaced with `********`.
@@ -22,7 +22,7 @@ module Foam
22
22
  }.freeze
23
23
 
24
24
  def self.install
25
- return unless Instrumentations.instance(INSTRUMENTATION).installed?
25
+ return unless Instrumentations.installed?(INSTRUMENTATION)
26
26
 
27
27
  ::OpenTelemetry::Instrumentation::AwsLambda::Wrap.prepend(Wrap)
28
28
  end
@@ -17,7 +17,7 @@ module Foam
17
17
  DECOMPRESSED = %w[gzip x-gzip deflate].freeze
18
18
 
19
19
  def self.install
20
- return unless Instrumentations.instance(INSTRUMENTATION).installed?
20
+ return unless Instrumentations.installed?(INSTRUMENTATION)
21
21
 
22
22
  stack = ::Excon.defaults[:middlewares]
23
23
  return if stack.include?(self)
@@ -28,7 +28,7 @@ module Foam
28
28
  end
29
29
 
30
30
  def self.install
31
- return unless Instrumentations.instance(INSTRUMENTATION).installed?
31
+ return unless Instrumentations.installed?(INSTRUMENTATION)
32
32
 
33
33
  ::Faraday::Connection.prepend(Connection)
34
34
  instrument_default_connection
@@ -13,7 +13,7 @@ module Foam
13
13
  INFLATED_ENCODINGS = %w[gzip x-gzip deflate].freeze
14
14
 
15
15
  def self.install
16
- return unless Instrumentations.instance(INSTRUMENTATION).installed?
16
+ return unless Instrumentations.installed?(INSTRUMENTATION)
17
17
 
18
18
  ::HTTPClient::Session.prepend(self)
19
19
  end
@@ -12,7 +12,7 @@ module Foam
12
12
  attr_reader :current
13
13
 
14
14
  def set(value)
15
- if State.current[:initialized]
15
+ if State.initialized?
16
16
  Diagnostics.warn("set_endpoint after init: exporters keep #{@current}; only later reports use #{value}")
17
17
  end
18
18
  @current = value
@@ -12,12 +12,16 @@ module Foam
12
12
  # A copy has the before_send hook's edits, Foam's identity merged over its resource, and every
13
13
  # sensitive value masked.
14
14
  module ExportCopies
15
- # Explicit path and headers so the SDK does not derive them from OTEL_* env.
15
+ # Explicit path, headers, and compression so the SDK does not derive them from OTEL_* env.
16
16
  def initialize(token:, redactor:, before_send: BeforeSend::NONE, stamp: {})
17
17
  @redactor = redactor
18
18
  @before_send = before_send
19
19
  @stamp = OpenTelemetry::SDK::Resources::Resource.create(stamp)
20
- super(endpoint: Endpoint.url(self.class::PATH), headers: Constants.bearer_headers(token))
20
+ super(
21
+ endpoint: Endpoint.url(self.class::PATH),
22
+ headers: Constants.bearer_headers(token),
23
+ compression: "gzip"
24
+ )
21
25
  end
22
26
 
23
27
  # The encoder groups items by resource object, so items sharing one must share its
@@ -50,12 +54,6 @@ module Foam
50
54
  def redacted_attributes(attributes)
51
55
  attributes && @redactor.redact_attributes(attributes)
52
56
  end
53
-
54
- # The encoder derives a dropped count as the total minus what is present, so the copy's total
55
- # must follow its size to keep the SDK's dropped count.
56
- def total_recorded(original_total, original_list, list)
57
- original_total - original_list&.size.to_i + list.size
58
- end
59
57
  end
60
58
 
61
59
  class TraceExporter < OpenTelemetry::Exporter::OTLP::Exporter
@@ -70,8 +68,31 @@ module Foam
70
68
  }.freeze
71
69
  SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
72
70
 
71
+ # The upstream span encoder, with a hash the hook added exported as a nested map, as it is in
72
+ # a log attribute. The upstream encoder has no case for a hash and sends an empty value.
73
+ module Encoder
74
+ include OpenTelemetry::Exporter::OTLP::Common
75
+ extend self
76
+
77
+ private
78
+
79
+ def as_otlp_any_value(value)
80
+ return super unless value.is_a?(Hash)
81
+
82
+ values = value.map { |key, item| as_otlp_key_value(key.to_s, item) }
83
+ Opentelemetry::Proto::Common::V1::AnyValue.new(
84
+ kvlist_value: Opentelemetry::Proto::Common::V1::KeyValueList.new(values: values)
85
+ )
86
+ end
87
+ end
88
+
73
89
  private
74
90
 
91
+ # The inherited method calls the upstream encoder directly.
92
+ def encode(span_data)
93
+ Encoder.as_encoded_etsr(span_data)
94
+ end
95
+
75
96
  # The span as the hook left it, with its own, event, and link attributes and its status redacted; nil when
76
97
  # the hook dropped it.
77
98
  def redacted_copy(span, resource, &on_raise)
@@ -142,15 +163,10 @@ module Foam
142
163
  )
143
164
  end
144
165
 
145
- # A hash the hook added exports as a nested map, as it does in a log attribute; the span
146
- # encoder this class inherits has no case for one and would send an empty value.
147
- def as_otlp_any_value(value)
148
- return super unless value.is_a?(Hash)
149
-
150
- values = value.map { |key, item| as_otlp_key_value(key.to_s, item) }
151
- Opentelemetry::Proto::Common::V1::AnyValue.new(
152
- kvlist_value: Opentelemetry::Proto::Common::V1::KeyValueList.new(values: values)
153
- )
166
+ # The encoder derives a dropped count as the total minus what is present, so the copy's total
167
+ # must follow its size to keep the SDK's dropped count.
168
+ def total_recorded(original_total, original_list, list)
169
+ original_total - original_list&.size.to_i + list.size
154
170
  end
155
171
  end
156
172
 
@@ -172,8 +188,6 @@ module Foam
172
188
  copy.severity_text = event[:severity_text]
173
189
  copy.severity_number = event[:severity_number]
174
190
  copy.attributes = redacted_attributes(event[:attributes])
175
- copy.total_recorded_attributes =
176
- total_recorded(record.total_recorded_attributes, record.attributes, copy.attributes)
177
191
  copy.resource = resource
178
192
  end
179
193
  end
@@ -36,7 +36,7 @@ module Foam
36
36
  end
37
37
 
38
38
  # A second configure would install a second pipeline next to the first.
39
- if State.current[:initialized]
39
+ if State.initialized?
40
40
  Report.call(severity: :warn, message: "SDK already initialized", **identity)
41
41
  return
42
42
  end
@@ -90,7 +90,7 @@ module Foam
90
90
  # there while it boots, which would make every app log line raise. LoggerBridge is
91
91
  # prepended after the instrumentation's patch, so it sits in front of it and drops just
92
92
  # the record whose emit failed.
93
- ::Logger.prepend(LoggerBridge) if Instrumentations.instance(Instrumentations::LOGGER).installed?
93
+ ::Logger.prepend(LoggerBridge) if Instrumentations.installed?(Instrumentations::LOGGER)
94
94
 
95
95
  # The client patches set the safe headers on every client span and, in advanced mode,
96
96
  # capture bodies too. Bodies are redacted by the same rules as attributes, so only
@@ -102,6 +102,10 @@ module Foam
102
102
  # Foam's logger provider where it flushes the tracer and meter providers.
103
103
  Safely.call("aws_lambda") { AwsLambda.install }
104
104
 
105
+ # Leaves out a propagation header Que would refuse as a job tag, so the app's enqueue
106
+ # does not raise on a long baggage.
107
+ Safely.call("que") { Que.install }
108
+
105
109
  # A Rails app cannot add the middleware in config.ru the way a plain Rack app does, so
106
110
  # Foam adds it through Rails itself.
107
111
  Safely.call("rails middleware") { RailsMiddleware.install } unless options[:network_capture] == :off
@@ -104,7 +104,7 @@ module Foam
104
104
 
105
105
  # Labels of the instrumentations that installed, in table order, then the caller's entries.
106
106
  def installed(config_map)
107
- config_map.filter_map { |name, _config| label(short_name(name)) if instance(name).installed? }
107
+ config_map.filter_map { |name, _config| label(short_name(name)) if installed?(name) }
108
108
  end
109
109
 
110
110
  # Labels of the instrumentations whose library arrived after init: present, enabled, and
@@ -113,8 +113,10 @@ module Foam
113
113
  config_map.filter_map { |name, config| label(short_name(name)) if late?(instance(name), config) }
114
114
  end
115
115
 
116
+ # An entry another SDK deleted from the registry after init (see installed?) has no instance to
117
+ # ask, so it counts as neither installed nor late.
116
118
  def late?(instance, config)
117
- !instance.installed? && instance.present? && instance.enabled?(config) && compatible?(instance)
119
+ !instance.nil? && !instance.installed? && instance.present? && instance.enabled?(config) && compatible?(instance)
118
120
  end
119
121
 
120
122
  # A compatibility block reads the library's version constant, which can raise on an odd install.
@@ -128,6 +130,12 @@ module Foam
128
130
  OpenTelemetry::Instrumentation.registry.lookup(name)
129
131
  end
130
132
 
133
+ # Another SDK can remove entries from the registry, as New Relic's OpenTelemetry bridge does
134
+ # for the libraries it covers itself, so a missing entry reads as not installed.
135
+ def installed?(name)
136
+ instance(name)&.installed? || false
137
+ end
138
+
131
139
  # The table's short name, or one derived the same way for an instrumentation outside the bundle.
132
140
  def short_name(name)
133
141
  TABLE.fetch(name) do
@@ -4,10 +4,86 @@ module Foam
4
4
  module Otel
5
5
  module Internal
6
6
  module Propagation
7
+ # Writes W3C trace context, as OpenTelemetry's propagator does, but does not replace a
8
+ # tracestate header that is already on the outgoing request. Foam has no tracestate entry of
9
+ # its own. Another SDK, such as New Relic or Datadog, can write tracestate first and put its
10
+ # entry (@nr, dd) before the caller's entries. If Foam wrote tracestate again, that entry
11
+ # would be lost. Foam always writes traceparent.
12
+ module KeptTracestate
13
+ UPSTREAM = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator
14
+
15
+ # Passes every header through but tracestate.
16
+ SkipTracestate = Struct.new(:setter) do
17
+ def set(carrier, key, value)
18
+ setter.set(carrier, key, value) unless key == "tracestate"
19
+ end
20
+ end
21
+
22
+ module_function
23
+
24
+ def inject(carrier, context:, setter:)
25
+ setter = SkipTracestate.new(setter) if header(carrier)
26
+ UPSTREAM.inject(carrier, context: context, setter: setter)
27
+ end
28
+
29
+ def extract(carrier, **) = UPSTREAM.extract(carrier, **)
30
+
31
+ def fields = UPSTREAM.fields
32
+
33
+ # Reads the carrier's tracestate header as one string, or nil when it has none. Carriers
34
+ # differ: HTTPClient returns an array of header values, the others a string. A list carrier,
35
+ # such as Que's job tags, cannot be read by header name, so there is nothing to keep.
36
+ def header(carrier)
37
+ return if carrier.is_a?(Array)
38
+
39
+ value = Array(carrier["tracestate"]).join(",")
40
+ value unless value.empty?
41
+ end
42
+ end
43
+
44
+ # The W3C baggage header is shared between SDKs: Sentry, for one, writes its sampling context
45
+ # there as sentry-* members. OpenTelemetry's baggage propagator overwrites the whole header
46
+ # with Foam's entries, so whichever SDK wrote first loses its members. This wraps it to merge
47
+ # instead, keeping the other SDK's members and letting Foam's value win a shared key.
48
+ module MergingBaggage
49
+ UPSTREAM = OpenTelemetry::Baggage::Propagation.text_map_propagator
50
+
51
+ module_function
52
+
53
+ def inject(carrier, context:, setter:)
54
+ theirs = header(carrier)
55
+ UPSTREAM.inject(carrier, context: context, setter: setter)
56
+ ours = header(carrier)
57
+ return if theirs.nil? || ours == theirs
58
+
59
+ keys = ours.split(",").map { |member| key(member) }
60
+ kept = theirs.split(",").map(&:strip).reject { |member| member.empty? || keys.include?(key(member)) }
61
+ setter.set(carrier, "baggage", [ours, *kept].join(","))
62
+ end
63
+
64
+ def extract(carrier, **) = UPSTREAM.extract(carrier, **)
65
+
66
+ def fields = UPSTREAM.fields
67
+
68
+ # Reads the carrier's baggage header as one string, or nil when it has none. inject reads it
69
+ # before and after the upstream write, to tell the other SDK's members from Foam's. Carriers
70
+ # differ: HTTPClient returns an array of header values, the others a string. A list carrier,
71
+ # such as Que's job tags, is written through its setter and cannot be read by header name,
72
+ # so there is nothing to merge.
73
+ def header(carrier)
74
+ return if carrier.is_a?(Array)
75
+
76
+ value = Array(carrier["baggage"]).join(",")
77
+ value unless value.empty?
78
+ end
79
+
80
+ def key(member) = member.split("=", 2).first.strip
81
+ end
82
+
7
83
  # Foam's own W3C propagator, so the helpers work the same whoever configured the global one.
8
84
  PROPAGATOR = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
9
- OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator,
10
- OpenTelemetry::Baggage::Propagation.text_map_propagator
85
+ KeptTracestate,
86
+ MergingBaggage
11
87
  ])
12
88
 
13
89
  module_function
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foam
4
+ module Otel
5
+ module Internal
6
+ # Foam's addition to the que instrumentation, prepended on its TagSetter by init after
7
+ # installation. The instrumentation writes each propagation header into the job's tags as
8
+ # "key:value", and Que refuses to enqueue a job with a tag longer than MAXIMUM_TAG_LENGTH or
9
+ # more than MAXIMUM_TAGS_COUNT tags, so a long baggage, the app's or another SDK's, would make
10
+ # the app's enqueue raise. The setter leaves out a header Que would refuse, warning once, and
11
+ # the job enqueues without it.
12
+ module Que
13
+ INSTRUMENTATION = "OpenTelemetry::Instrumentation::Que"
14
+
15
+ def self.install
16
+ return unless Instrumentations.installed?(INSTRUMENTATION)
17
+
18
+ ::OpenTelemetry::Instrumentation::Que::TagSetter.singleton_class.prepend(TagSetter)
19
+ end
20
+
21
+ def self.fits?(tags, tag)
22
+ tag.length <= ::Que::Job::MAXIMUM_TAG_LENGTH && tags.length < ::Que::Job::MAXIMUM_TAGS_COUNT
23
+ end
24
+
25
+ def self.warn_left_out(key)
26
+ return if @warned
27
+
28
+ @warned = true
29
+ Diagnostics.warn(
30
+ "propagation header #{key} left out of a Que job's tags; Que allows at most " \
31
+ "#{::Que::Job::MAXIMUM_TAGS_COUNT} tags of #{::Que::Job::MAXIMUM_TAG_LENGTH} characters"
32
+ )
33
+ end
34
+
35
+ module TagSetter
36
+ def set(carrier, key, value)
37
+ return super if Que.fits?(carrier, "#{key}:#{value}")
38
+
39
+ Que.warn_left_out(key)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -46,7 +46,7 @@ module Foam
46
46
  # it builds the real stack late in initialize!. Anchored on the OpenTelemetry class, not
47
47
  # position 0, so another SDK inserting at the top cannot come between the two.
48
48
  def insert(app)
49
- return unless Instrumentations.instance(ACTION_PACK).installed?
49
+ return unless Instrumentations.installed?(ACTION_PACK)
50
50
 
51
51
  otel = Instrumentations.instance(RACK).middleware_args.first
52
52
  app.middleware.insert_after(otel, Foam::Rack::Middleware)
@@ -11,8 +11,9 @@ module Foam
11
11
  module Reinject
12
12
  module_function
13
13
 
14
- # The global propagator, the one the instrumentation used. Without a span there is nothing
15
- # the instrumentation wrote, so nothing is written here either.
14
+ # The global propagator, the one the instrumentation used: whoever holds the slot, before or
15
+ # after init, decides the headers. Without a span there is nothing the instrumentation
16
+ # wrote, so nothing is written here either.
16
17
  def call(carrier, context: OpenTelemetry::Context.current)
17
18
  return if OwnRequest.current? || !OpenTelemetry::Trace.current_span(context).context.valid?
18
19
 
@@ -108,7 +109,7 @@ module Foam
108
109
 
109
110
  def install
110
111
  HOOKS.each do |hook, (klass, instrumentation)|
111
- next unless instrumentation.nil? || Instrumentations.instance(instrumentation).installed?
112
+ next unless instrumentation.nil? || Instrumentations.installed?(instrumentation)
112
113
 
113
114
  Object.const_get(klass).prepend(hook)
114
115
  end
@@ -35,6 +35,12 @@ module Foam
35
35
  end
36
36
  end
37
37
 
38
+ # Whether init has completed, without reading the live slots as current does, which warns
39
+ # about a slot another SDK took since.
40
+ def initialized?
41
+ @mutex.synchronize { @initialized }
42
+ end
43
+
38
44
  # Gate for the helpers: the hash is replaced whole, never mutated, so no lock.
39
45
  def active?(signal)
40
46
  @signals[signal] != :none
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Foam
4
4
  module Otel
5
- VERSION = "3.0.0.alpha.9"
5
+ VERSION = "3.0.0.alpha.12"
6
6
  end
7
7
  end
data/lib/foam/otel.rb CHANGED
@@ -23,6 +23,7 @@ require_relative "otel/internal/clients/excon"
23
23
  require_relative "otel/internal/clients/http_client"
24
24
  require_relative "otel/internal/clients/faraday"
25
25
  require_relative "otel/internal/aws_lambda"
26
+ require_relative "otel/internal/que"
26
27
  require_relative "otel/internal/rails_middleware"
27
28
  require_relative "otel/internal/before_send"
28
29
  require_relative "otel/internal/instrumentations"
@@ -129,6 +130,11 @@ module Foam
129
130
  end
130
131
  end
131
132
 
133
+ # The propagator the two helpers above use, for an app that composes its own.
134
+ def propagator
135
+ Internal::Propagation::PROPAGATOR
136
+ end
137
+
132
138
  def set_baggage(key, value)
133
139
  Internal::Safely.call("set_baggage") do
134
140
  Internal::Propagation.set_baggage(key, value)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foam-otel
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.alpha.9
4
+ version: 3.0.0.alpha.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-05 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-exporter-otlp
@@ -16,56 +16,56 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.34.1
19
+ version: 0.37.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.34.1
26
+ version: 0.37.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: opentelemetry-exporter-otlp-logs
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.5.1
33
+ version: 0.6.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.5.1
40
+ version: 0.6.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: opentelemetry-exporter-otlp-metrics
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.10.0
47
+ version: 0.13.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.10.0
54
+ version: 0.13.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: opentelemetry-instrumentation-all
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.96.0
61
+ version: 0.97.0
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.96.0
68
+ version: 0.97.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: opentelemetry-instrumentation-logger
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,56 +86,56 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.6.1
89
+ version: 0.8.0
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.6.1
96
+ version: 0.8.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: opentelemetry-logs-api
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.5.0
103
+ version: 0.6.0
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.5.0
110
+ version: 0.6.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: opentelemetry-metrics-api
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 0.7.0
117
+ version: 0.9.0
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 0.7.0
124
+ version: 0.9.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: opentelemetry-metrics-sdk
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.16.0
131
+ version: 0.20.0
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.16.0
138
+ version: 0.20.0
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: opentelemetry-sdk
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -204,6 +204,7 @@ files:
204
204
  - lib/foam/otel/internal/own_request.rb
205
205
  - lib/foam/otel/internal/patch_resolver.rb
206
206
  - lib/foam/otel/internal/propagation.rb
207
+ - lib/foam/otel/internal/que.rb
207
208
  - lib/foam/otel/internal/rack/body_tee.rb
208
209
  - lib/foam/otel/internal/rack/input_tee.rb
209
210
  - lib/foam/otel/internal/rails_middleware.rb