foam-otel 1.7.0 → 1.8.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: 48166d05b120dd8bdca15a9b45db5b4c16a6574fcb6890f4caff2c17c2ae9add
4
- data.tar.gz: 2dcd4b3b146a0221c231ba5efc057085418b74e21b24d8441396d47cfc0a8288
3
+ metadata.gz: 3e034361554ad48530554a1bdc62d75adf1899e3ef57104a367abd2c412d2f93
4
+ data.tar.gz: 40b6db9d7f11ed9105b00c9998b87da51e0822480d97da52be766ddea24435ca
5
5
  SHA512:
6
- metadata.gz: 4e92c7af8cc5cf7cc177b792f87a8107b918035efdce080e2a974750180ebbb9aac4a5460cb08372f3ded2724d136804bcb8f0574ee4685ebbdd94fde7afb3e8
7
- data.tar.gz: ddd7b6e431034663ec94dacd8b7b051ee63bfecdbeb2a38d383c58531854ba0580f34c9a04ed56473b654b89155fc9a3b7ce140b073ba346d90a5a801b9c6d4b
6
+ metadata.gz: 9c71df1b9ce568dabd996b659d13e0bc05e93a81e070376a77e8c09ccd6a6fc51dcaa028c19a904e78d38daae532c6bc75077e3fa5331047aa1c64e08a9609ee
7
+ data.tar.gz: 90623d934372eb908d8c9d13e5ddd4108faad54bea4c5a647e7934959562ff130c2cdfdc2f0c5c5cc00a1a86eb5fcc4ee03c745c69c8c258fced2caf6ceb2f31
data/README.md CHANGED
@@ -331,6 +331,7 @@ not captured — inbound only.
331
331
  | `additional_metric_readers:` | Array | no | `[]` | Tenant seam, metrics. |
332
332
  | `additional_instrumentations:` | Array | no | `[]` | Constructed tier-2 instrumentation instances to register (fault-isolated: one that throws is skipped with a `[foam]` warning). |
333
333
  | `ignored_outbound_hosts:` | Array<String> | no | `[]` | Hosts whose outbound calls produce no spans — EXTENDS the built-in export-loop guard. For a co-resident agent's intake or a tenant exporter's endpoint. Applies to the clients whose official instrumentation supports host suppression: **Net::HTTP and Excon**. Faraday/HTTP (httprb)/HTTPX have no upstream `untraced_hosts` option — foam warns loudly at init when one of those is bundled (GOTCHAS G13); foam's own export loop is guarded for every client regardless. |
334
+ | `before_send:` | Proc/Array | no | `[]` | Export hook (1.8.0): one callable — or an Array run as a pipeline — called with every **span and log record** at the export boundary, after batching and BEFORE foam's redaction pass and serialization, so nothing a hook drops ever leaves the process. Return the record to export it (mutating it in place is supported — `record.attributes` arrives unfrozen), or `nil` to drop it. A hook that raises (or returns anything else) drops **that record** loudly and the batch reports failure — fail-closed, never a raise into your threads. Runs BEFORE redaction, so the credential floor and your redact lists still apply to whatever the hook returns. Spans and logs only (metrics are aggregated state, not records). A non-callable raises at boot. Process-global: a second `init` never changes it. See "The `before_send` hook" below. |
334
335
  | `capture_payloads:` | Symbol/String | no | `:off` | Inbound HTTP **body** capture on the rack server span (see "Payload (body) capture"). `:off` = zero teeing, zero middleware (the default); `:errors` = tee per request, attach only on exception/5xx; `:always` = attach on every request. Equivalent strings accepted; anything else raises at boot. Overridden in both directions by the `FOAM_CAPTURE_PAYLOADS` env clamp (env table below). Textual payloads only, 8192-char cap + `…[truncated]`, true `.size` attributes; sizes-only for binary/compressed. |
335
336
  | `diagnostics:` | Boolean | no | false | Verbose `[foam]` self-reporting of init/health. Warnings and errors are always loud regardless. |
336
337
  | `secret_heuristics:` | Boolean | no | true | The value-pattern secret layer's HEURISTIC tier (generic keyword+entropy detection, see "The value-pattern secret layer"). `false` disables ONLY the heuristics — the named provider patterns, the credential floor and the redaction-coverage contract have no off switch. Disabling logs one loud `[foam]` line at init (an explicit, audited decision for telemetry whose legitimate values collide with the heuristics). |
@@ -388,6 +389,24 @@ Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: EN
388
389
  ignored_outbound_hosts: ["ingest.eval-tool.example"]) # the tenant loop guard — required
389
390
  ```
390
391
 
392
+ ```ruby
393
+ # The before_send export hook (1.8.0): transform or drop spans/logs right
394
+ # before they are exported to foam — after batching, BEFORE redaction and
395
+ # serialization (a dropped record never leaves the process). Return the
396
+ # record (mutate in place freely) or nil to drop; a raising hook drops ONLY
397
+ # its record, loudly, and never raises into your threads (fail-closed).
398
+ Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: ENV.fetch("FOAM_OTEL_TOKEN"),
399
+ before_send: lambda { |record|
400
+ next nil if record.respond_to?(:name) && record.name == "GET /healthz" # drop noise
401
+
402
+ record.attributes.delete("internal.debug.blob") # trim before it ships
403
+ record
404
+ })
405
+ # Or an Array pipeline — hooks run in order, each receiving the previous
406
+ # hook's return; the first nil drops the record and short-circuits:
407
+ # Foam::Otel.init(..., before_send: [DropHealthchecks, TrimDebugAttrs, StampTeam])
408
+ ```
409
+
391
410
  ```ruby
392
411
  # Verbose self-reporting while wiring foam up (warnings are loud regardless):
393
412
  Foam::Otel.init(name: "checkout-api", environment: "development", enabled: true,
@@ -616,6 +635,54 @@ processor = Foam::Otel.create_ingest_span_processor(
616
635
 
617
636
  ---
618
637
 
638
+ ## The `before_send` hook
639
+
640
+ `before_send:` is the last word your code gets on every span and log record
641
+ before it is exported to foam. It runs at the **export boundary** — after the
642
+ batch processor hands foam the finished records, immediately **before**
643
+ foam's redaction pass and the OTLP serialization — so a record your hook
644
+ drops is never serialized and never leaves the process. (This is the same
645
+ seam redaction lives at, and for the same Ruby SDK reason: a span's
646
+ attributes freeze at finish before any `on_finish` processor runs, so a
647
+ `SpanProcessor` could neither mutate nor drop. The export boundary is the
648
+ first point where the record is a mutable struct.)
649
+
650
+ The pipeline order is pinned:
651
+
652
+ ```
653
+ BatchProcessor → before_send hooks → redaction (floor + your lists) → OTLP wire
654
+ ```
655
+
656
+ Your hooks run **first**, on the raw record (it is your own data, still
657
+ in-process), and redaction runs on whatever they return — so `before_send`
658
+ can never widen what ships: an attribute a hook adds is masked by the
659
+ credential floor and your redact lists exactly like one an instrumentation
660
+ set.
661
+
662
+ The per-record contract (Sentry `beforeSend` semantics):
663
+
664
+ * **return the record** → it continues to the next hook / to export.
665
+ Mutating it in place is supported — `record.attributes` arrives as an
666
+ unfrozen copy, so `record.attributes["k"] = v` and
667
+ `record.attributes.delete("k")` just work.
668
+ * **return `nil`** → the record is dropped, silently (that is the filter
669
+ mechanism, not an error).
670
+ * **raise, or return anything else** → that record is dropped **loudly**
671
+ (`[foam]` warning) and the batch reports failure — fail-closed: a faulted
672
+ hook never ships a record you may have meant to scrub, and never raises
673
+ into your application or export threads. Healthy records in the same batch
674
+ still export.
675
+
676
+ Scope: **spans and log records** (the two record-shaped signals — the hook
677
+ receives `SpanData` or `LogRecordData`; distinguish them with `is_a?` or
678
+ `respond_to?`). Metrics are aggregated state, not discrete records, and are
679
+ deliberately not routed through `before_send`. Door-2 ingest taps are
680
+ unchanged. Like `enabled:` and the endpoint, `before_send` is process-global:
681
+ the exporter chain is built at the first `init`, and a second `init` never
682
+ changes it.
683
+
684
+ ---
685
+
619
686
  ## The helpers
620
687
 
621
688
  All helpers never raise, and no-op silently before `init` and when disabled.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "diagnostics"
4
+
5
+ module Foam
6
+ module Otel
7
+ # The customer's `before_send:` hook(s) (1.8.0), run at the EXPORT
8
+ # BOUNDARY — after batching, immediately BEFORE foam's redaction pass and
9
+ # the OTLP serialization, so a record a hook drops is never serialized
10
+ # and never leaves the process. Why not a SpanProcessor: the same Ruby
11
+ # SDK constraint that forced redaction to the exporter boundary — a span
12
+ # freezes its attributes at finish BEFORE any on_finish processor runs
13
+ # (GOTCHAS F1), so a processor could neither mutate nor reliably drop.
14
+ # The mutable SpanData / LogRecordData Structs the batch processors hand
15
+ # the exporter are the first (and last) point where per-record transform
16
+ # + drop is possible before the wire.
17
+ #
18
+ # Composition order (pinned, pipelines.rb):
19
+ # BatchProcessor -> BeforeSend::*Exporter -> Redacting*Exporter -> OTLP
20
+ # The hooks run FIRST so the credential floor, the value-pattern secret
21
+ # layer and the customer's key lists still apply to whatever a hook
22
+ # returns — before_send can never widen what ships past redaction (an
23
+ # attribute a hook ADDS is masked exactly like one an instrumentation
24
+ # set).
25
+ #
26
+ # Contract per record (Sentry beforeSend semantics — the spec's named
27
+ # fail-closed precedent, BASE_PACKAGE_SPEC D6):
28
+ # * return the record (mutating it in place is fine) → next hook /
29
+ # export;
30
+ # * return nil → record DROPPED (the intentional filter mechanism —
31
+ # success, not a failure);
32
+ # * raise, or return a foreign object → record DROPPED, loudly, and
33
+ # the batch reports FAILURE (fail-closed: a hook fault must never
34
+ # ship a record the customer may have meant to scrub, and must never
35
+ # raise into the export thread — rules 9/14/15). SystemStackError is
36
+ # rescued explicitly (not a StandardError): a hook recursing on a
37
+ # poisoned payload kills neither the batch thread nor the caller.
38
+ # The record reaches the first hook as a shallow dup with an UNFROZEN
39
+ # attributes copy (span attributes freeze at finish — GOTCHAS F1), so
40
+ # `record.attributes["k"] = v` just works; the dup also keeps the
41
+ # customer's mutations off the struct any other consumer might hold.
42
+ #
43
+ # Scope: spans and logs (the two record-shaped signals). Metric data is
44
+ # aggregated state, not records — deliberately NOT routed through
45
+ # before_send (documented in the README).
46
+ module BeforeSend
47
+ # Internal sentinel distinguishing a FAULTED drop (counts toward batch
48
+ # FAILURE) from an intentional nil drop. A hook can never return it
49
+ # legitimately — any non-record return is itself classified a fault.
50
+ ERROR = Object.new.freeze
51
+
52
+ module_function
53
+
54
+ # Run the hook pipeline over one record: each hook receives the
55
+ # previous hook's return. Returns the surviving record, nil for an
56
+ # intentional drop, or ERROR for a faulted one.
57
+ def run(hooks, record, signal)
58
+ current = thaw(record)
59
+ hooks.each do |hook|
60
+ result = hook.call(current)
61
+ return nil if result.nil?
62
+
63
+ unless result.is_a?(record.class)
64
+ Diagnostics.warn("before_send returned a #{result.class} for a #{signal} record — record " \
65
+ "dropped (return the record, a replacement of the same type, or nil to drop; " \
66
+ "fail-closed, rule 14)")
67
+ return ERROR
68
+ end
69
+ current = result
70
+ end
71
+ current
72
+ rescue StandardError, SystemStackError => e
73
+ Diagnostics.warn("before_send raised for a #{signal} record — record dropped, never exported " \
74
+ "half-transformed (fail-closed, rule 14): #{e.class}: #{e.message}")
75
+ ERROR
76
+ end
77
+
78
+ # A shallow dup with an unfrozen attributes copy — the hook gets its
79
+ # own mutable record (span attributes arrive frozen; GOTCHAS F1).
80
+ def thaw(record)
81
+ copy = record.dup
82
+ if copy.respond_to?(:attributes) && copy.respond_to?(:attributes=)
83
+ attributes = copy.attributes
84
+ copy.attributes = attributes.nil? ? {} : attributes.dup
85
+ end
86
+ copy
87
+ end
88
+
89
+ # The span-side wrapper (composition over the redacting exporter, same
90
+ # wrap-never-subclass posture as redacting_exporter.rb: only the public
91
+ # export/force_flush/shutdown contract the batch processors call).
92
+ class SpanExporter
93
+ def initialize(inner, config)
94
+ @inner = inner
95
+ @hooks = config.before_send
96
+ end
97
+
98
+ def export(span_data, timeout: nil)
99
+ batch = span_data.to_a
100
+ faulted = 0
101
+ kept = batch.filter_map do |sd|
102
+ result = BeforeSend.run(@hooks, sd, :span)
103
+ if result.equal?(ERROR)
104
+ faulted += 1
105
+ nil
106
+ else
107
+ result
108
+ end
109
+ end
110
+ status = kept.empty? ? OpenTelemetry::SDK::Trace::Export::SUCCESS : @inner.export(kept, timeout: timeout)
111
+ faulted.positive? ? OpenTelemetry::SDK::Trace::Export::FAILURE : status
112
+ rescue StandardError, SystemStackError
113
+ # Fail closed past the per-record guards, loudly (rule 15) —
114
+ # never raise into the batch thread.
115
+ Diagnostics.warn("before_send pass raised past its guards — span batch dropped (fail-closed, rule 14/15)")
116
+ OpenTelemetry::SDK::Trace::Export::FAILURE
117
+ end
118
+
119
+ def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout)
120
+ def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout)
121
+ end
122
+
123
+ class LogRecordExporter
124
+ def initialize(inner, config)
125
+ @inner = inner
126
+ @hooks = config.before_send
127
+ end
128
+
129
+ def export(log_record_data, timeout: nil)
130
+ batch = log_record_data.to_a
131
+ faulted = 0
132
+ kept = batch.filter_map do |lrd|
133
+ result = BeforeSend.run(@hooks, lrd, :log)
134
+ if result.equal?(ERROR)
135
+ faulted += 1
136
+ nil
137
+ else
138
+ result
139
+ end
140
+ end
141
+ status = kept.empty? ? OpenTelemetry::SDK::Logs::Export::SUCCESS : @inner.export(kept, timeout: timeout)
142
+ faulted.positive? ? OpenTelemetry::SDK::Logs::Export::FAILURE : status
143
+ rescue StandardError, SystemStackError
144
+ Diagnostics.warn("before_send pass raised past its guards — log batch dropped (fail-closed, rule 14/15)")
145
+ OpenTelemetry::SDK::Logs::Export::FAILURE
146
+ end
147
+
148
+ def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout)
149
+ def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -13,6 +13,7 @@ module Foam
13
13
  :name, :environment, :version, :enabled,
14
14
  :redact_keys, :redact_pii_keys, :redact_detect, :ignored_outbound_hosts,
15
15
  :diagnostics, :endpoint, :secret_heuristics, :capture_payloads,
16
+ :before_send,
16
17
  keyword_init: true
17
18
  )
18
19
 
@@ -40,7 +41,8 @@ module Foam
40
41
  redact_detect: [].freeze,
41
42
  ignored_outbound_hosts: [].freeze,
42
43
  diagnostics: false, endpoint: FOAM_OTEL_ENDPOINT,
43
- secret_heuristics: true, capture_payloads: :off
44
+ secret_heuristics: true, capture_payloads: :off,
45
+ before_send: [].freeze
44
46
  ).freeze
45
47
  end
46
48
 
@@ -53,7 +55,7 @@ module Foam
53
55
  def resolve_config(name:, environment:, version:, enabled:,
54
56
  redact_keys:, redact_pii_keys:, ignored_outbound_hosts:,
55
57
  diagnostics:, endpoint:, secret_heuristics: true, redact: nil,
56
- capture_payloads: :off)
58
+ capture_payloads: :off, before_send: nil)
57
59
  keys, pii, detect = merged_redact_lists(redact_keys, redact_pii_keys, redact)
58
60
  # capture_payloads is validated LOUDLY at boot (rule 10, exactly like
59
61
  # the redact: object): a value outside CAPTURE_PAYLOAD_MODES (or its
@@ -89,12 +91,39 @@ module Foam
89
91
  # — zero teeing, zero middleware) | :errors | :always, canonical
90
92
  # symbol form. init.rb applies the FOAM_CAPTURE_PAYLOADS operator
91
93
  # env clamp BEFORE this resolves.
92
- capture_payloads: capture_mode
94
+ capture_payloads: capture_mode,
95
+ # The customer's export hook(s) (1.8.0): one callable or an Array
96
+ # pipeline, validated LOUDLY at boot (rule 10 — a non-callable is a
97
+ # programming error). Stored as a frozen Array; empty (the default)
98
+ # means the exporter chain gains no before_send stage at all.
99
+ before_send: validate_before_send!(before_send)
93
100
  ).freeze
94
101
  end
95
102
 
96
103
  private
97
104
 
105
+ # ---- the before_send option (1.8.0) ------------------------------------
106
+ # Loud at boot (rule 10, same posture as capture_payloads and the
107
+ # redact: object): before_send must be one callable (responds to
108
+ # #call — a lambda, a Proc, a Method, or any hook object) or an Array
109
+ # of callables run as a pipeline. Anything else — including a nil or
110
+ # non-callable INSIDE the array — raises here, never a silent no-op
111
+ # that leaves the customer believing their filter runs.
112
+ def validate_before_send!(value, context: "Foam::Otel.init")
113
+ hooks = if value.nil?
114
+ []
115
+ else
116
+ value.is_a?(Array) ? value : [value]
117
+ end
118
+ hooks.each_with_index do |hook, index|
119
+ next if hook.respond_to?(:call)
120
+
121
+ raise ArgumentError, "#{context} before_send: must be a callable (responds to #call) or an " \
122
+ "Array of callables, got #{hook.class} at position #{index}"
123
+ end
124
+ hooks.freeze
125
+ end
126
+
98
127
  # ---- the capture_payloads option (payload-capture mandate 2026-07-28)
99
128
  # Canonicalize a mode value: the three symbols, or their string
100
129
  # spellings trimmed + case-insensitive, map to the canonical symbol;
@@ -212,7 +241,11 @@ module Foam
212
241
  # the first init: middleware insertion already happened (or
213
242
  # deliberately did not — an :off boot shipped none), so a second
214
243
  # init cannot meaningfully flip it. Carried forward unchanged.
215
- capture_payloads: existing.capture_payloads
244
+ capture_payloads: existing.capture_payloads,
245
+ # before_send is process-global too: the exporter chain was built
246
+ # (with or without its before_send stage) at the first init and a
247
+ # second init never rebuilds pipelines. Carried forward unchanged.
248
+ before_send: existing.before_send
216
249
  ).freeze
217
250
  end
218
251
  end
@@ -59,7 +59,8 @@ module Foam
59
59
  ignored_outbound_hosts: nil,
60
60
  diagnostics: false,
61
61
  secret_heuristics: true,
62
- capture_payloads: :off)
62
+ capture_payloads: :off,
63
+ before_send: nil)
63
64
  # Identity is validated at boot (rule 10): a blank required value is a
64
65
  # programming error the engineer must catch on their machine.
65
66
  validate_present!(:name, name)
@@ -96,7 +97,10 @@ module Foam
96
97
  # FOAM_CAPTURE_PAYLOADS operator env clamp applied first (a valid
97
98
  # env value overrides the option in BOTH directions; an invalid
98
99
  # one warns + falls back — never a crashed boot).
99
- capture_payloads: resolve_capture_payloads(capture_payloads)
100
+ capture_payloads: resolve_capture_payloads(capture_payloads),
101
+ # The customer's export hook(s): a non-callable raises INSIDE
102
+ # resolve_config, loudly, at boot (rule 10).
103
+ before_send: before_send
100
104
  )
101
105
  # The heuristic-tier opt-out is an explicit, audited customer decision
102
106
  # (security-fixes-design §V.8) — always one loud line, never silent.
@@ -8,6 +8,7 @@ require "delegate"
8
8
  require "openssl"
9
9
 
10
10
  require_relative "redacting_exporter"
11
+ require_relative "before_send"
11
12
  require_relative "session_stitching"
12
13
 
13
14
  module Foam
@@ -33,6 +34,11 @@ module Foam
33
34
  ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER
34
35
  )
35
36
  exporter = RedactingSpanExporter.new(inner, config)
37
+ # The customer's before_send hooks (1.8.0) wrap OUTSIDE redaction:
38
+ # batch → before_send → redaction → OTLP, so the hooks see the record
39
+ # first and the credential floor still masks whatever they return
40
+ # (before_send.rb). No hooks configured → no stage at all.
41
+ exporter = BeforeSend::SpanExporter.new(exporter, config) unless config.before_send.empty?
36
42
  # Pin the sampler explicitly: the TracerProvider constructor otherwise
37
43
  # reads OTEL_TRACES_SAMPLER, which an operator (or a stale fleet-wide
38
44
  # var) could set to always_off and silently drop EVERY foam span
@@ -87,6 +93,8 @@ module Foam
87
93
  ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER # F-RB1 pin — see setup_traces
88
94
  )
89
95
  exporter = RedactingLogRecordExporter.new(inner, config)
96
+ # before_send stage, same shape and ordering as setup_traces above.
97
+ exporter = BeforeSend::LogRecordExporter.new(exporter, config) unless config.before_send.empty?
90
98
  # Pin the log-record limits at the SDK defaults — LogRecordLimits
91
99
  # otherwise reads OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT /
92
100
  # OTEL_(LOGRECORD_)ATTRIBUTE_VALUE_LENGTH_LIMIT (same env family as
@@ -109,6 +109,21 @@ module Foam
109
109
  # customer redact lists reach inside the body JSON). Rails auto-wiring
110
110
  # (Railtie + init fallback) inserts the middleware ONLY when the mode
111
111
  # is not :off; plain Rack adds one `use` line.
112
- VERSION = "1.7.0"
112
+ # 1.8.0: `before_send:` EXPORT HOOK (MINOR — one additive init option,
113
+ # default absent, zero default-path behavior change). One callable or
114
+ # an Array pipeline, run per record at the EXPORT BOUNDARY — after
115
+ # batching, BEFORE foam's redaction pass and the OTLP serialization —
116
+ # so a dropped record never leaves the process, and whatever a hook
117
+ # returns still rides the full credential floor / value layer /
118
+ # customer-key pass (before_send can never widen what ships). Spans
119
+ # and logs only (metrics are aggregated state, not records). Return
120
+ # the record (in-place mutation supported — attributes arrive
121
+ # unfrozen) to keep it, nil to drop it; a hook that raises or returns
122
+ # a foreign object drops the record LOUDLY and the batch reports
123
+ # FAILURE (fail-closed, Sentry beforeSend precedent, spec D6). A
124
+ # non-callable raises at boot; process-global like enabled/endpoint
125
+ # (a second init never changes it). Door-1 only — the door-2 ingest
126
+ # taps are unchanged.
127
+ VERSION = "1.8.0"
113
128
  end
114
129
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foam-otel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foam
@@ -378,6 +378,7 @@ files:
378
378
  - lib/foam-otel.rb
379
379
  - lib/foam/otel.rb
380
380
  - lib/foam/otel/api.rb
381
+ - lib/foam/otel/before_send.rb
381
382
  - lib/foam/otel/classifier.rb
382
383
  - lib/foam/otel/config.rb
383
384
  - lib/foam/otel/constants.rb