Module: Foam::Otel
- Defined in:
- lib/foam/otel/api.rb,
lib/foam/otel/llm.rb,
lib/foam/otel/init.rb,
lib/foam/otel/config.rb,
lib/foam/otel/errors.rb,
lib/foam/otel/ingest.rb,
lib/foam/otel/metrics.rb,
lib/foam/otel/version.rb,
lib/foam/otel/instance.rb,
lib/foam/otel/resource.rb,
lib/foam/otel/constants.rb,
lib/foam/otel/pipelines.rb,
lib/foam/otel/redaction.rb,
lib/foam/otel/classifier.rb,
lib/foam/otel/fork_hooks.rb,
lib/foam/otel/diagnostics.rb,
lib/foam/otel/logger_bridge.rb,
lib/foam/otel/llm/gemini_shim.rb,
lib/foam/otel/llm/openai_shim.rb,
lib/foam/otel/runtime_metrics.rb,
lib/foam/otel/llm/ruby_llm_shim.rb,
lib/foam/otel/session_stitching.rb,
lib/foam/otel/llm/anthropic_shim.rb,
lib/foam/otel/redacting_exporter.rb,
lib/foam/otel/ingest_metric_reader.rb
Defined Under Namespace
Modules: Classifier, Diagnostics, Errors, ForkHooks, Ingest, LLM, LoggerBridge, Metrics, Redaction, Resource, RuntimeMetrics, SessionStitching Classes: Config, GuardedLogRecordProcessor, GuardedSpanProcessor, Instance, MaskedLogRecordView, MaskedSpanView, RedactingLogRecordExporter, RedactingMetricsExporter, RedactingSpanExporter, Verdict
Constant Summary collapse
- LEVELS =
---- shared ----------------------------------------------------------
{ trace: 1, debug: 5, info: 9, warn: 13, error: 17, fatal: 21 }.freeze
- LEVEL_TEXT =
{ trace: "TRACE", debug: "DEBUG", info: "INFO", warn: "WARN", error: "ERROR", fatal: "FATAL" }.freeze
- VERSION =
The publish pipeline parses this literal (tools/publish-packages) — strict semver only; gem-style prereleases (0.1.0.alpha1) are unsupported. 1.0.0: first pass aligned to docs/BASE_PACKAGE_SPEC.md (fleet convention, matching the JS core's alignment bump). Breaking: the stamped/rid/ request-context architecture is gone; the init surface changed. 1.0.1 (#80): HTTP-client instrumentation class resolved for the loop guard. 1.0.2: FOAM_OTEL_TOKEN doc convention (token comment source change); drop-in. 1.2.1: review-fleet fixes. DELIBERATE WIRE CHANGE (GenAI-uniformity ruling): Gemini shim gen_ai.operation.name + span name "chat" → "generate_content", matching the js/python Gemini emissions. ruby_llm shim now captures agent-loop tool activity (tool_calls id/name/ arguments + tool_call_id) in gen_ai input/output messages. Contrib -logger pinned disabled in the sweep (foam's LoggerBridge supersedes it — rule 12 double-bridge guard). Sidekiq + Faraday floor entries wire-proven (rule 8b); Sidekiq :link posture documented (GOTCHAS G16). 1.3.0: ALWAYS-ON CREDENTIAL FLOOR (fleet ruling 2026-07-26; MINOR per the ruling — security-motivated wire change, no API/config change, docs/decisions/credential-denylist-design.md §9). Foam now masks, by default and in every signal, the VALUES of the fixed credential/secret header + field NAME list in contract/credential-denylist.json (seven headers + 52 keys) to the literal [REDACTED]. Exact name-equality, case-insensitive, dash/underscore-insensitive — never substring:
authorization_url is untouched. Everything else still exports RAW; redact_keys/redact_pii_keys unchanged and additive; NO off switch. Tenant additional_* processors now receive a MASKED read-only view (closes the GOTCHAS F1 third-party-attribute divergence). If a dashboard keyed off a raw credential value (it should not have), it will now see [REDACTED]. "1.3.0"- FOAM_OTEL_ENDPOINT =
The pinned fleet endpoint (BASE_PACKAGE_SPEC rule 23). Never an init option; the one override is the OTEL_EXPORTER_OTLP_ENDPOINT env var (rule 19), which init() honors with a loud warning.
"https://otel.api.foam.ai"- DISTRO_NAME =
Resource self-identification (BASE_PACKAGE_SPEC section 0, resource identity table): every export carries telemetry.distro.name = "foam" and telemetry.distro.version = the package version.
"foam"- MAX_REDACT_DEPTH =
Deep-redaction depth guard (rule 14). Beyond this, a value redacts in full rather than recursing further. Pinned at 10 fleet-wide (contract SPEC §4; credential-denylist-design §4 cross-core table) — every core walks to the same depth so the floor's coverage is byte-identical.
10- KNOWN_ENVIRONMENTS =
deployment.environment.name values that do NOT warn (section 0: the value is exported verbatim regardless; anything outside this set warns but is never rewritten).
%w[production staging development test].freeze
- CREDENTIAL_HEADER_DENYLIST =
(a) The seven credential HEADERS, stored in wire-canonical dash form — matched as header names wherever headers are captured, including the semconv http.request,response.header.
span-attribute forms. %w[ authorization proxy-authorization cookie set-cookie x-api-key x-auth-token www-authenticate ].freeze
- CREDENTIAL_KEY_DENYLIST =
(b) The 52-entry KEY denylist, stored in canonical (lowercase, underscore) form, ASCII byte order — the deduplicated union of the seven headers, sentry-python's DEFAULT_DENYLIST + DEFAULT_PII_DENYLIST, and BASE_PACKAGE_SPEC rule 14's reference roots (design doc §1.2). Matched as attribute/field names at every depth the central engine walks. Every header above is also an entry here (subset invariant, design §1.1).
%w[ _csrf _csrf_token _session _xsrf aiohttp_session api_key apikey auth authorization bearer card connect.sid cookie credential credentials csrf csrf_token csrftoken cvc cvv ip_address jwt mfa mysql_pwd otp passphrase passwd password phpsessid private_key privatekey proxy_authorization pwd remote_addr secret session sessionid set_cookie sid signature ssn symfony token user_session www_authenticate x_api_key x_auth_token x_csrftoken x_forwarded_for x_real_ip xsrf xsrf_token ].freeze
- REDACTED =
The two mask shapes. A redact_keys match on a scalar keeps a debug tail; a redact_pii_keys match (and any non-scalar value under a redact_keys match) erases in full. MASK_BODY matches raven-ruby's STRING_MASK precedent.
"[REDACTED]"- MASK_BODY =
always exactly eight chars — never length-revealing
"********"- TAIL_MIN_LENGTH =
below this, no tail is safe
12
Class Attribute Summary collapse
Class Method Summary collapse
- .add_event(name, attributes: nil) ⇒ Object
- .add_up_down_counter(name, by:, attributes: nil) ⇒ Object
-
.after_fork! ⇒ Object
Restart foam's PeriodicMetricReader export threads after a fork (the trace/log batch processors self-heal on their own — GOTCHAS R1).
-
.blank_config ⇒ Object
The inert config the helpers read before init() runs (everything a no-op needs: empty redaction lists, export disabled).
-
.create_ingest_log_record_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 logs entry: a LogRecordProcessor duck for THEIR LoggerProvider.
-
.create_ingest_metric_reader(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 metrics entry: a PeriodicMetricReader subclass for THEIR MeterProvider#add_metric_reader (the back-filling attach point).
-
.create_ingest_span_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 traces entry: a SpanProcessor-duck instance for THEIR TracerProvider (their setup block / add_span_processor).
-
.flush ⇒ Object
Force-flush foam's buffered pipelines.
- .get_logger(name = "foam") ⇒ Object
- .get_meter(name = "foam") ⇒ Object
-
.get_trace_context ⇒ Object
The active trace/span ids as lowercase hex, or nil when no span is recording — never fabricates ids.
-
.get_tracer(name = "foam") ⇒ Object
---- passthroughs ---------------------------------------------------- The real OTel objects on foam's pipeline.
-
.increment_counter(name, by: 1, attributes: nil) ⇒ Object
---- metrics (one helper per synchronous instrument) -----------------.
-
.init(name:, environment:, enabled:, token: nil, version: nil, redact_keys: nil, redact_pii_keys: nil, additional_instrumentations: nil, additional_span_processors: nil, additional_log_record_processors: nil, additional_metric_readers: nil, ignored_outbound_hosts: nil, diagnostics: false) ⇒ Object
rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
-
.log(severity, body, attributes: nil) ⇒ Object
---- logs ------------------------------------------------------------ Emit a trace-correlated log record on foam's log pipeline.
-
.mark_forked! ⇒ Object
Called by the fork hook IN THE CHILD: clears the idempotency guard so the README's per-worker init is honored as a real re-init (the child then reclaims foam's inherited slots and mints a fresh service.instance.id — see classify_for_registration).
-
.record_exception(error) ⇒ Object
Records an exception on the active span (status ERROR) when one is recording.
- .record_histogram(name, value, attributes: nil) ⇒ Object
-
.redact(value) ⇒ Object
---- redaction ------------------------------------------------------- The same fail-closed engine foam uses internally: mask a value the caller knows is sensitive (scalar → tail mask, everything else → [REDACTED]); empty on hard failure, never the unredacted payload.
-
.reset_for_tests! ⇒ Object
Test hook: full reset of module state (NOT for production — OTel globals cannot be cleanly re-registered after a real shutdown).
- .resolve_config(name:, environment:, version:, enabled:, redact_keys:, redact_pii_keys:, ignored_outbound_hosts:, diagnostics:, endpoint:) ⇒ Object
- .set_attribute(key, value) ⇒ Object
- .set_attributes(attributes) ⇒ Object
-
.set_metric(name, value, attributes: nil) ⇒ Object
Gauge: the last-value instrument (set_metric in every core).
-
.shutdown ⇒ Object
Shut down foam's pipelines (flush + provider shutdown).
-
.span(name, attributes: nil, &block) ⇒ Object
A custom span around a block.
Class Attribute Details
.active_config ⇒ Object
31 32 33 |
# File 'lib/foam/otel/config.rb', line 31 def active_config @active_config ||= blank_config end |
Class Method Details
.add_event(name, attributes: nil) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/foam/otel/api.rb', line 136 def add_event(name, attributes: nil) return false if helpers_disabled? span = OpenTelemetry::Trace.current_span return false unless span.respond_to?(:recording?) && span.recording? span.add_event(name.to_s, attributes: stringify(attributes)) true rescue StandardError, SystemStackError false end |
.add_up_down_counter(name, by:, attributes: nil) ⇒ Object
169 170 171 |
# File 'lib/foam/otel/api.rb', line 169 def add_up_down_counter(name, by:, attributes: nil) Metrics.add_up_down_counter(name, by: by, attributes: attributes) end |
.after_fork! ⇒ Object
Restart foam's PeriodicMetricReader export threads after a fork (the trace/log batch processors self-heal on their own — GOTCHAS R1). Walks BOTH registries: foam's own door-1 readers AND the door-2 retires foam's pipelines never orphans a tap riding the CUSTOMER'S pipeline — GOTCHAS R7). Called automatically by foam's Process._fork hook; also safe to call from Puma on_worker_boot / Unicorn after_fork.
236 237 238 239 240 241 242 243 |
# File 'lib/foam/otel/api.rb', line 236 def after_fork! ((@metric_readers || []) + (@ingest_taps || [])).each do |reader| reader.after_fork if reader.respond_to?(:after_fork) rescue StandardError nil end nil end |
.blank_config ⇒ Object
The inert config the helpers read before init() runs (everything a no-op needs: empty redaction lists, export disabled).
22 23 24 25 26 27 28 29 |
# File 'lib/foam/otel/config.rb', line 22 def blank_config Config.new( name: "", environment: "", version: nil, enabled: false, redact_keys: [].freeze, redact_pii_keys: [].freeze, ignored_outbound_hosts: [].freeze, diagnostics: false, endpoint: FOAM_OTEL_ENDPOINT ).freeze end |
.create_ingest_log_record_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 logs entry: a LogRecordProcessor duck for THEIR LoggerProvider.
47 48 49 50 51 52 |
# File 'lib/foam/otel/ingest.rb', line 47 def create_ingest_log_record_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) Ingest.build(:logs, token: token, environment: environment, redact_keys: redact_keys, redact_pii_keys: redact_pii_keys, diagnostics: diagnostics) end |
.create_ingest_metric_reader(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 metrics entry: a PeriodicMetricReader subclass for THEIR MeterProvider#add_metric_reader (the back-filling attach point).
56 57 58 59 60 61 |
# File 'lib/foam/otel/ingest.rb', line 56 def create_ingest_metric_reader(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) Ingest.build(:metrics, token: token, environment: environment, redact_keys: redact_keys, redact_pii_keys: redact_pii_keys, diagnostics: diagnostics) end |
.create_ingest_span_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) ⇒ Object
Door-2 traces entry: a SpanProcessor-duck instance for THEIR TracerProvider (their setup block / add_span_processor). Foam's copy is stamped + masked at export; their spans are never touched.
39 40 41 42 43 44 |
# File 'lib/foam/otel/ingest.rb', line 39 def create_ingest_span_processor(token:, environment:, redact_keys: nil, redact_pii_keys: nil, diagnostics: false) Ingest.build(:traces, token: token, environment: environment, redact_keys: redact_keys, redact_pii_keys: redact_pii_keys, diagnostics: diagnostics) end |
.flush ⇒ Object
Force-flush foam's buffered pipelines. Call before a short-lived process exits (rake, cron, serverless handler end). Never raises.
207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/foam/otel/api.rb', line 207 def flush (@flushables || []).dup.each do |flushable| flushable.force_flush rescue StandardError, SystemStackError # SystemStackError is not a StandardError: a poisoned (e.g. cyclic) # payload deep in a pipeline must still never escape into the # customer's thread (rule 9). nil end nil end |
.get_logger(name = "foam") ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/foam/otel/api.rb', line 43 def get_logger(name = "foam") provider = OpenTelemetry.logger_provider warn_if_displaced(:logs, provider) # Never register into the PRE-INIT proxy registry (GOTCHAS L1): the # logs-api 0.4.1 ProxyLoggerProvider#delegate= upgrades cached proxy # loggers with a POSITIONAL provider.logger(name, version) call, but # the logs SDK's #logger is keyword-only — one cached proxy logger # makes init's logger_provider assignment raise and would leave the # logs pipeline dark for the process. Pre-init logs are a silent # no-op by contract, so hand back the API's no-op Logger directly. # (Same feature-detected Internal read as the classifier — rule 42, # GOTCHAS G14; when the constant is missing this check degrades to # the plain provider call below.) if defined?(OpenTelemetry::Internal::ProxyLoggerProvider) && provider.instance_of?(OpenTelemetry::Internal::ProxyLoggerProvider) return noop_logger end begin provider.logger(name: name) # the logs SDK's keyword-only signature rescue ArgumentError provider.logger(name) # a positional-signature (older/foreign) provider end end |
.get_meter(name = "foam") ⇒ Object
37 38 39 40 41 |
# File 'lib/foam/otel/api.rb', line 37 def get_meter(name = "foam") provider = OpenTelemetry.meter_provider warn_if_displaced(:metrics, provider) provider.meter(name) end |
.get_trace_context ⇒ Object
The active trace/span ids as lowercase hex, or nil when no span is recording — never fabricates ids.
150 151 152 153 154 155 156 157 158 |
# File 'lib/foam/otel/api.rb', line 150 def get_trace_context span = OpenTelemetry::Trace.current_span ctx = span.respond_to?(:context) ? span.context : nil return nil if ctx.nil? || !ctx.valid? { trace_id: ctx.hex_trace_id, span_id: ctx.hex_span_id } rescue StandardError, SystemStackError nil end |
.get_tracer(name = "foam") ⇒ Object
---- passthroughs ---------------------------------------------------- The real OTel objects on foam's pipeline. Everything unwrapped (links, span kinds, observable instruments, baggage) is reachable here — this keeps the helper set CLOSED. After init they export to foam; when a foreign SDK owns a signal, they emit through that provider (rule 18 B).
31 32 33 34 35 |
# File 'lib/foam/otel/api.rb', line 31 def get_tracer(name = "foam") provider = OpenTelemetry.tracer_provider warn_if_displaced(:traces, provider) provider.tracer(name) end |
.increment_counter(name, by: 1, attributes: nil) ⇒ Object
---- metrics (one helper per synchronous instrument) -----------------
161 162 163 |
# File 'lib/foam/otel/api.rb', line 161 def increment_counter(name, by: 1, attributes: nil) Metrics.increment_counter(name, by: by, attributes: attributes) end |
.init(name:, environment:, enabled:, token: nil, version: nil, redact_keys: nil, redact_pii_keys: nil, additional_instrumentations: nil, additional_span_processors: nil, additional_log_record_processors: nil, additional_metric_readers: nil, ignored_outbound_hosts: nil, diagnostics: false) ⇒ Object
rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/foam/otel/init.rb', line 51 def init(name:, environment:, enabled:, token: nil, version: nil, redact_keys: nil, redact_pii_keys: nil, additional_instrumentations: nil, additional_span_processors: nil, additional_log_record_processors: nil, additional_metric_readers: nil, ignored_outbound_hosts: nil, diagnostics: false) # Identity is validated at boot (rule 10): a blank required value is a # programming error the engineer must catch on their machine. validate_present!(:name, name) validate_present!(:environment, environment) # enabled must be a real boolean — a truthy non-true value (the string # "true", 1, "yes") would otherwise SILENTLY go inert. Complain at boot # (rule 10) rather than disable a service that looks configured. unless [true, false].include?(enabled) raise ArgumentError, "Foam::Otel.init requires enabled: true or false (got #{enabled.inspect})" end # OTEL_SDK_DISABLED=true is the operator kill switch and SUPERSEDES # enabled: true (rule 19). Foam bypasses SDK.configure, so it must honor # the var itself. Matched trimmed + case-insensitive so the fleet's one # emergency switch behaves identically across languages (JS/Python # trim + downcase; the OTel boolean-env convention is case-insensitive). killed = kill_switch_active? effective_enabled = enabled && !killed endpoint = resolve_endpoint warn_inert_per_signal_endpoints(endpoint) config = resolve_config( name: name, environment: environment, version: version, enabled: effective_enabled, redact_keys: redact_keys, redact_pii_keys: redact_pii_keys, ignored_outbound_hosts: ignored_outbound_hosts, diagnostics: diagnostics, endpoint: endpoint ) # Calling init twice warns and returns the first instance (rule: # lifecycle). The redaction lists (and diagnostics) refresh so the # helpers read the latest customer lists — but enabled/endpoint/identity # are process-global and are NEVER changed by a second init: overwriting # `enabled` here would leave foam HALF-dark (a second init(enabled:false) # would no-op the helpers while the already-registered providers keep # exporting Rack/Net::HTTP spans — the operator believes foam is off # while data still flows), and the reverse would "wake" a service booted # disabled without ever building a pipeline. Merge ONLY the redact lists. if @initialized self.active_config = refresh_post_init_config( active_config, redact_keys: redact_keys, redact_pii_keys: redact_pii_keys, diagnostics: diagnostics ) Diagnostics.warn("init already called — returning the existing instance (providers are process-global; " \ "enabled and endpoint cannot change after the first init — only the redaction lists refresh)") return @instance end self.active_config = config warn_environment(environment) warn_missing_version(version) unless effective_enabled if killed # The kill switch OVERRIDES the code's enabled: true — an operator # intervention is worth one loud line (rule 19 env table). Diagnostics.warn("OTEL_SDK_DISABLED=true — foam is fully off (no SDK, no export)") else # enabled: false is the DOCUMENTED tests/CI posture (README # scenario 6): an EXPECTED state stays silent (rule 10). The # posture line is still visible under diagnostics: true. Diagnostics.info("enabled: false — foam is inert (no SDK loaded, no providers, no export)") end @initialized = true return @instance = Instance.new(config) end # token is required WHEN enabled, wired explicitly — never an env # fallback (section 0). Validated only on the enabled path. if token.nil? || token.to_s.strip.empty? raise ArgumentError, "Foam::Otel.init requires a non-empty token when enabled" end require_sdk! configure_propagation # Providers foam registered in the PARENT process — non-empty only on # a post-fork re-init (the fork hook cleared @initialized, GOTCHAS # R1). A slot still holding one of these is foam's OWN to re-own # (retiring the inherited pipelines and minting a fresh resource — a # NEW service.instance.id per worker, section 0); a genuinely foreign # provider is never touched. inherited = @foam_providers.dup @inherited_retired = false resource = Resource.build(config) headers = { "Authorization" => "Bearer #{token}" } instance = Instance.new(config) # Each slot is classified IMMEDIATELY before its own setter — never # as an up-front classify-all pass. Ruby has no atomic set-once # attempt (rule 18 step 4's Ruby row: every setter assigns # unconditionally, so attempt-then-read would DISPLACE an incumbent — # RESEARCH.md §2 / GOTCHAS F7), so a pre-read is forced; classifying # per signal at the last moment keeps the residual pre-read→set race # window as narrow as the platform allows (GOTCHAS F8). traces_verdict = register_free_signal(:traces, instance, inherited) do setup_traces(endpoint, headers, resource, config, additional_span_processors) end register_free_signal(:logs, instance, inherited) do setup_logs(endpoint, headers, resource, config, additional_log_record_processors) end register_free_signal(:metrics, instance, inherited) do setup_metrics(endpoint, headers, resource, config, additional_metric_readers) end activate_instrumentations(config, additional_instrumentations, traces_verdict) activate_floor_extensions ForkHooks.install! at_exit { instance.shutdown } Diagnostics.info("init ok for #{name} (#{environment}) -> #{endpoint}") @initialized = true @instance = instance end |
.log(severity, body, attributes: nil) ⇒ Object
---- logs ------------------------------------------------------------
Emit a trace-correlated log record on foam's log pipeline. severity
is an OTel severity NUMBER (1..24) or a level symbol (:info, :warn,
:error, :debug, :fatal, :trace). Safe pre-init (no-op). Never raises.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/foam/otel/api.rb', line 182 def log(severity, body, attributes: nil) return true if helpers_disabled? # disabled means fully dark — accepted, silently dropped get_logger.on_emit( severity_number: severity_number(severity), severity_text: severity_text(severity), body: mask_log_body(body), attributes: stringify(attributes), context: OpenTelemetry::Context.current ) true rescue StandardError, SystemStackError false end |
.mark_forked! ⇒ Object
Called by the fork hook IN THE CHILD: clears the idempotency guard so the README's per-worker init is honored as a real re-init (the child then reclaims foam's inherited slots and mints a fresh service.instance.id — see classify_for_registration). Everything else is left running for workers that never re-init.
178 179 180 |
# File 'lib/foam/otel/init.rb', line 178 def mark_forked! @initialized = false end |
.record_exception(error) ⇒ Object
Records an exception on the active span (status ERROR) when one is recording. Accepts customer error classes; NEVER notifies a vendor tracker. Deduped per span. Never raises.
95 96 97 98 99 100 |
# File 'lib/foam/otel/api.rb', line 95 def record_exception(error) return nil if helpers_disabled? Errors.record_once(OpenTelemetry::Trace.current_span, error) nil end |
.record_histogram(name, value, attributes: nil) ⇒ Object
165 166 167 |
# File 'lib/foam/otel/api.rb', line 165 def record_histogram(name, value, attributes: nil) Metrics.record_histogram(name, value, attributes: attributes) end |
.redact(value) ⇒ Object
---- redaction ------------------------------------------------------- The same fail-closed engine foam uses internally: mask a value the caller knows is sensitive (scalar → tail mask, everything else → [REDACTED]); empty on hard failure, never the unredacted payload.
201 |
# File 'lib/foam/otel/api.rb', line 201 def redact(value) = Redaction.redact(value) |
.reset_for_tests! ⇒ Object
Test hook: full reset of module state (NOT for production — OTel globals cannot be cleanly re-registered after a real shutdown).
185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/foam/otel/init.rb', line 185 def reset_for_tests! @initialized = false @instance = nil @foam_providers = [] @flushables = [] @metric_readers = [] @ingest_taps = [] @foam_registered = {} @displacement_warned = {} @inherited_retired = false self.active_config = blank_config Metrics.reset_for_tests! RuntimeMetrics.reset_for_tests! end |
.resolve_config(name:, environment:, version:, enabled:, redact_keys:, redact_pii_keys:, ignored_outbound_hosts:, diagnostics:, endpoint:) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/foam/otel/config.rb', line 37 def resolve_config(name:, environment:, version:, enabled:, redact_keys:, redact_pii_keys:, ignored_outbound_hosts:, diagnostics:, endpoint:) Config.new( name: name, environment: environment, version: version, enabled: enabled, # Opt-in CUSTOMER redaction (holistic redesign), additive above the # always-on credential floor (which is a module constant, NOT config # state — no config shape can alter it). Empty (the default) → no # customer redaction, raw capture above the floor. Stored lowercased # for the engine's case-insensitive substring match. redact_keys: downcase_list(redact_keys), redact_pii_keys: downcase_list(redact_pii_keys), ignored_outbound_hosts: Array(ignored_outbound_hosts).map { |h| h.to_s.downcase }.freeze, diagnostics: diagnostics ? true : false, endpoint: endpoint ).freeze end |
.set_attribute(key, value) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/foam/otel/api.rb', line 102 def set_attribute(key, value) return false if helpers_disabled? span = OpenTelemetry::Trace.current_span return false unless span.respond_to?(:recording?) && span.recording? # Capture-time pass (rule 18 C.3): the always-on credential floor plus # any configured redact_keys/redact_pii_keys, so the value lands # ALREADY-MASKED — tenant processors and inert-mode providers only ever # see the redacted view of helper-set data. The exporter pass # re-applies (idempotent). Values under unmatched names are captured # RAW. scrub_utf8 is encoding hygiene, not masking: an invalid-UTF-8 # value would make the upstream OTLP encoder drop the WHOLE batch # (rule 15). span.set_attribute(key.to_s, Redaction.mask_one(key.to_s, Redaction.scrub_utf8(value), active_config)) true rescue StandardError, SystemStackError # SystemStackError is not a StandardError: a poisoned key/value (e.g. a # recursive #to_s/#hash) must never crash the caller's thread (rule 9). false end |
.set_attributes(attributes) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/foam/otel/api.rb', line 124 def set_attributes(attributes) return false if helpers_disabled? span = OpenTelemetry::Trace.current_span return false unless span.respond_to?(:recording?) && span.recording? span.add_attributes(stringify(attributes)) true rescue StandardError, SystemStackError false end |
.set_metric(name, value, attributes: nil) ⇒ Object
Gauge: the last-value instrument (set_metric in every core).
174 175 176 |
# File 'lib/foam/otel/api.rb', line 174 def set_metric(name, value, attributes: nil) Metrics.set_metric(name, value, attributes: attributes) end |
.shutdown ⇒ Object
Shut down foam's pipelines (flush + provider shutdown). Idempotent; normal process exit is covered by the at_exit hook init() installs. Never raises.
222 223 224 225 226 227 |
# File 'lib/foam/otel/api.rb', line 222 def shutdown @instance&.shutdown nil rescue StandardError, SystemStackError nil end |
.span(name, attributes: nil, &block) ⇒ Object
A custom span around a block. Re-raises the block's error identically, records the exception + ERROR status, and ALWAYS ends the span (rule 9 / never-ended-span leak). Safe pre-init (no-op span). Argument preparation NEVER throws (rule 9): garbage attributes degrade to nil, a poisoned name to "unnamed" — the block always runs and only ITS error re-raises. When foam is initialized disabled, the block runs on a non-recording no-op span: disabled beats everything, including inert-mode emission through a foreign provider (section 0).
78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/foam/otel/api.rb', line 78 def span(name, attributes: nil, &block) span_name = begin name.to_s rescue StandardError, SystemStackError # SystemStackError is not a StandardError: a name object whose #to_s # recurses unboundedly must degrade to "unnamed", never escape into # the customer's thread (rule 9). "unnamed" end return noop_tracer.in_span(span_name, &block) if helpers_disabled? get_tracer.in_span(span_name, attributes: stringify(attributes), &block) end |