Module: Foam::Otel
- Defined in:
- lib/foam/otel/init.rb,
lib/foam/otel/rack.rb,
lib/foam/otel/config.rb,
lib/foam/otel/errors.rb,
lib/foam/otel/stamps.rb,
lib/foam/otel/railtie.rb,
lib/foam/otel/sidekiq.rb,
lib/foam/otel/version.rb,
lib/foam/otel/constants.rb,
lib/foam/otel/redaction.rb,
lib/foam/otel/logger_bridge.rb,
lib/foam/otel/vendor/datadog.rb,
lib/foam/otel/vendor/rollbar.rb
Defined Under Namespace
Modules: Errors, Rack, Redaction, STAMPS, Sidekiq, Stamping, Vendor Classes: Config, Instance, LoggerBridge, Railtie, StampLogRecordProcessor, StampSpanProcessor
Constant Summary collapse
- DEFAULT_CONFIG =
Config.new( service_name: "", endpoint: FOAM_OTEL_ENDPOINT, redacted_keys: DEFAULT_REDACTED_KEYS.to_set.freeze, health_routes: DEFAULT_HEALTH_ROUTES.to_set.freeze ).freeze
- VERSION =
The publish pipeline parses this literal (tools/publish-packages) — strict semver only; gem-style prereleases (0.1.0.alpha1) are unsupported.
"0.1.0"- FOAM_OTEL_ENDPOINT =
Fleet-wide defaults. The canonical source is contract/defaults.json; contract/verify/check-defaults.mjs extracts the literals below from THIS file on every conformance run — keep them flat quoted-string array literals (no %w, no computed values) or extraction breaks.
"https://otel.api.foam.ai"- BODY_TRUNCATION_LIMIT =
4096- MAX_REDACT_DEPTH =
10- DEFAULT_REDACTED_KEYS =
[ "password", "token", "authorization", "secret", "api_key", "apikey", "access_token", "accesstoken", "jwt", "x-api-key", "x-service-token", "cookie", "set-cookie", "creditcard", "credit_card", "card_number", "cardnumber", "cvv", "cvc", "dni", "ssn", ].freeze
- CONTACT_INFO_KEYS =
Opt-in contact-PII list. Enabled via
redact_contact_info: true. ["email", "phone", "telefono", "celular"].freeze
- DEFAULT_HEALTH_ROUTES =
Empty by default: health checks are captured like any other route (contract SPEC.md section 5); services exempt routes via health_routes.
[].freeze
- REQUEST_ID_HEADER =
"x-request-id"- INSTRUMENTATION_KEY =
"foam.instrumentation"- RID_KEY =
"foam.rid"
Class Method Summary collapse
-
.active_config ⇒ Object
The config of the most recently initialized service.
- .active_config=(config) ⇒ Object
-
.after_fork! ⇒ Object
Metrics readers do not self-heal across forks (unlike the batch span/log processors, which reset_on_fork automatically).
-
.check_fork! ⇒ Object
One authoritative process-level fork check the adapters call per request/job — re-arms the metric readers exactly once per fork.
- .clear_fallback_meter_provider_if_in(providers) ⇒ Object
-
.flush_foam_pipelines ⇒ Object
--- registries (used by Instance#shutdown and the pipelines) ---------.
- .forget_provider(provider) ⇒ Object
-
.init(service_name:, token: nil, endpoint: nil, force_export: false, redact_contact_info: false, redact_keys: nil, health_routes: nil, sidekiq: true, rollbar: true, rails: true) ⇒ Object
rubocop:disable Metrics/MethodLength.
- .logger(name = "foam") ⇒ Object
- .meter(name = "foam") ⇒ Object
-
.production? ⇒ Boolean
Production detection across common conventions (RAILS_ENV/RACK_ENV first — the Ruby idioms — then the fleet-shared names).
-
.rails_enabled? ⇒ Boolean
Whether Rails adapter hooks (middleware / exception notification / logger bridge) should activate.
-
.record_exception(error) ⇒ Object
Records an exception on the active span (status ERROR) when one exists.
-
.reset_for_tests! ⇒ Object
Test hook: full reset of module state (NOT for production use — OTel globals cannot be re-registered after a real shutdown).
- .resolve_config(service_name:, endpoint: nil, redact_contact_info: false, redact_keys: nil, health_routes: nil) ⇒ Object
- .resolve_endpoint(endpoint) ⇒ Object
-
.tracer(name = "foam") ⇒ Object
Signal passthrough: a tracer/meter/logger that exports to foam after init().
Class Method Details
.active_config ⇒ Object
The config of the most recently initialized service. Adapters read through this so they work (with safe defaults) even before init().
27 28 29 |
# File 'lib/foam/otel/config.rb', line 27 def active_config @active_config end |
.active_config=(config) ⇒ Object
31 32 33 |
# File 'lib/foam/otel/config.rb', line 31 def active_config=(config) @active_config = config end |
.after_fork! ⇒ Object
Metrics readers do not self-heal across forks (unlike the batch span/log processors, which reset_on_fork automatically). Call in Puma's on_worker_boot / Sidekiq lifecycle — the Rack and Sidekiq adapters also invoke it via check_fork! per call.
170 171 172 173 174 175 176 |
# File 'lib/foam/otel/init.rb', line 170 def after_fork! @metric_readers.each do |reader| reader.after_fork if reader.respond_to?(:after_fork) rescue StandardError nil end end |
.check_fork! ⇒ Object
One authoritative process-level fork check the adapters call per request/job — re-arms the metric readers exactly once per fork.
180 181 182 183 184 185 186 |
# File 'lib/foam/otel/init.rb', line 180 def check_fork! pid = Process.pid return if @pid == pid @pid = pid after_fork! end |
.clear_fallback_meter_provider_if_in(providers) ⇒ Object
214 215 216 217 218 |
# File 'lib/foam/otel/init.rb', line 214 def clear_fallback_meter_provider_if_in(providers) return unless @fallback_meter_provider @fallback_meter_provider = nil if providers.any? { |p| p.equal?(@fallback_meter_provider) } end |
.flush_foam_pipelines ⇒ Object
--- registries (used by Instance#shutdown and the pipelines) ---------
199 200 201 202 203 204 205 206 207 |
# File 'lib/foam/otel/init.rb', line 199 def flush_foam_pipelines flushables = @flushables.dup flushables << @fallback_meter_provider if @fallback_meter_provider flushables.each do |flushable| flushable.force_flush rescue StandardError nil end end |
.forget_provider(provider) ⇒ Object
209 210 211 212 |
# File 'lib/foam/otel/init.rb', line 209 def forget_provider(provider) @foam_providers.delete_if { |p| p.equal?(provider) } @flushables.delete_if { |p| p.equal?(provider) } end |
.init(service_name:, token: nil, endpoint: nil, force_export: false, redact_contact_info: false, redact_keys: nil, health_routes: nil, sidekiq: true, rollbar: true, rails: true) ⇒ Object
rubocop:disable Metrics/MethodLength
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/foam/otel/init.rb', line 73 def init(service_name:, token: nil, endpoint: nil, force_export: false, redact_contact_info: false, redact_keys: nil, health_routes: nil, sidekiq: true, rollbar: true, rails: true) existing = @instances[service_name] if existing warn "[foam] init() already called for \"#{service_name}\" — returning existing instance" return existing end unless @instances.empty? warn "[foam] init(\"#{service_name}\") called after #{@instances.keys.sort} — " \ "the active adapter config is process-global and last-write-wins" end # The resolved config becomes the active config on EVERY path — even # inert init: another SDK may own the global providers and the # adapters still emit real spans through it. config = resolve_config( service_name: service_name, endpoint: endpoint, redact_contact_info: redact_contact_info, redact_keys: redact_keys, health_routes: health_routes ) self.active_config = config # Zero-customer-effort: adapters and vendor bridges self-wire by # presence detection on every init path (inert included — a foreign # SDK may still export what the adapters produce). Opt-outs only. auto_wire(sidekiq: sidekiq, rollbar: , rails: rails) unless production? || force_export warn "[foam] skipping init for \"#{service_name}\" (not production, force_export false)" return remember(Instance.new(service_name, config)) end resolved_token = token || ENV.fetch("FOAM_OTEL_TOKEN", nil) if resolved_token.nil? || resolved_token.empty? warn "[foam] token is missing — telemetry will not be exported. " \ "Set token or the FOAM_OTEL_TOKEN env var." return remember(Instance.new(service_name, config)) end instance = Instance.new(service_name, config) headers = { "Authorization" => "Bearer #{resolved_token}" } resource = OpenTelemetry::SDK::Resources::Resource.create("service.name" => service_name) # We bypass OpenTelemetry::SDK.configure, so propagation must be set # explicitly: W3C TraceContext + Baggage. OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator .compose_propagators([ OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator, OpenTelemetry::Baggage::Propagation.text_map_propagator, ]) # Each signal is independent — a failure in one never blocks the rest. %i[setup_traces setup_logs setup_metrics].each do |setup| provider = send(setup, config.endpoint, headers, resource) instance.providers << provider if provider rescue StandardError => e warn "[foam] #{setup} failed: #{e.class}: #{e.}" end at_exit { instance.shutdown } warn "[foam] OTLP export enabled for #{service_name} -> #{config.endpoint}" remember(instance) end |
.logger(name = "foam") ⇒ Object
152 153 154 |
# File 'lib/foam/otel/init.rb', line 152 def logger(name = "foam") OpenTelemetry.logger_provider.logger(name: name) end |
.meter(name = "foam") ⇒ Object
146 147 148 149 150 |
# File 'lib/foam/otel/init.rb', line 146 def meter(name = "foam") return @fallback_meter_provider.meter(name) if @fallback_meter_provider OpenTelemetry.meter_provider.meter(name) end |
.production? ⇒ Boolean
Production detection across common conventions (RAILS_ENV/RACK_ENV first — the Ruby idioms — then the fleet-shared names).
59 60 61 62 63 |
# File 'lib/foam/otel/config.rb', line 59 def production? %w[RAILS_ENV RACK_ENV ENVIRONMENT ENV NODE_ENV].any? do |var| %w[production prod].include?(ENV.fetch(var, "").downcase) end end |
.rails_enabled? ⇒ Boolean
Whether Rails adapter hooks (middleware / exception notification / logger bridge) should activate. False until an init() with rails:true runs, so merely bundling the gem never installs global Rails hooks — the railtie checks this in after_initialize (opt-out symmetry with sidekiq:/rollbar:).
193 194 195 |
# File 'lib/foam/otel/init.rb', line 193 def rails_enabled? @rails_enabled == true end |
.record_exception(error) ⇒ Object
Records an exception on the active span (status ERROR) when one exists. Records to the foam span ONLY — never notifies any error tracker (SPEC section 12). Never raises.
159 160 161 162 163 164 |
# File 'lib/foam/otel/init.rb', line 159 def record_exception(error) Errors.record_once(OpenTelemetry::Trace.current_span, error) nil rescue StandardError nil end |
.reset_for_tests! ⇒ Object
Test hook: full reset of module state (NOT for production use — OTel globals cannot be re-registered after a real shutdown).
222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/foam/otel/init.rb', line 222 def reset_for_tests! @instances = {} @foam_providers = [] @attached_providers = [] @flushables = [] @fallback_meter_provider = nil @metric_readers = [] @pid = Process.pid @rails_enabled = false self.active_config = DEFAULT_CONFIG end |
.resolve_config(service_name:, endpoint: nil, redact_contact_info: false, redact_keys: nil, health_routes: nil) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/foam/otel/config.rb', line 40 def resolve_config(service_name:, endpoint: nil, redact_contact_info: false, redact_keys: nil, health_routes: nil) keys = DEFAULT_REDACTED_KEYS.to_set keys.merge(CONTACT_INFO_KEYS) if redact_contact_info (redact_keys || []).each { |key| keys.add(key.downcase) } routes = DEFAULT_HEALTH_ROUTES.to_set (health_routes || []).each { |route| routes.add(route) } Config.new( service_name: service_name, endpoint: resolve_endpoint(endpoint), redacted_keys: keys.freeze, health_routes: routes.freeze ).freeze end |
.resolve_endpoint(endpoint) ⇒ Object
35 36 37 38 |
# File 'lib/foam/otel/config.rb', line 35 def resolve_endpoint(endpoint) resolved = endpoint || ENV.fetch("OTEL_EXPORTER_OTLP_ENDPOINT", nil) || FOAM_OTEL_ENDPOINT resolved.sub(%r{/+\z}, "") end |
.tracer(name = "foam") ⇒ Object
Signal passthrough: a tracer/meter/logger that exports to foam after init(). meter() serves the fallback provider when another SDK owns the global MeterProvider (SPEC section 9).
142 143 144 |
# File 'lib/foam/otel/init.rb', line 142 def tracer(name = "foam") OpenTelemetry.tracer_provider.tracer(name) end |