Module: Foam::Otel
- Defined in:
- lib/foam/otel/api.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/redacting_exporter.rb,
lib/foam/otel/ingest_metric_reader.rb
Defined Under Namespace
Modules: Classifier, Diagnostics, Errors, ForkHooks, Ingest, Metrics, Redaction, Resource Classes: Config, GuardedLogRecordProcessor, GuardedSpanProcessor, Instance, 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.1.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.
12- 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
- BARE_SECRET_ROOTS =
---- Secrets floor (BASE_PACKAGE_SPEC rule 14) -------------------------- RESEARCHED, not invented (provenance cited in RESEARCH.md): substring roots curated from the consensus of OSS scrubbers — raven-ruby, sentry-python/js, Elastic APM sanitize_field_names, is-secret, @zapier/secret-scrubber, HyperDX, Instana, Rails filter_parameters. Matching is SUBSTRING, case-insensitive, over a key NORMALIZED by lowercasing and folding
-_.and space (soX-Api-Key≡api_key≡apiKey).BARE_SECRET_ROOTS are separator-free and matched against the normalized key. They are distinctive enough to never over-redact.
%w[ secret token password passwd pwd passphrase apikey secretkey accesskey privatekey credential credentials session phpsessid jsessionid connectsid aspnetsessionid cookie csrf xsrf jwt bearer signature mysqlpwd proxyauthorization crypt creditcard cardnumber ].freeze
- GUARDED_SECRET_ROOTS =
GUARDED roots are short/ambiguous — matched with word boundaries on the lowercased key (separators kept) so they never OVER-redact (lost observability, not extra safety).
key/sid/id/saltare REJECTED as roots entirely: barekeyeats idempotency_key/cache_key/public_key; baresideats consider/reside/president.authmust miss author;cardmust miss dashboard/wildcard/postcard. %w[ssn otp mfa 2fa sso saml cvv cvc dni].freeze
- GUARDED_SECRET_RE =
Precompiled once at load: one token-boundary regex matching ANY guarded root, so the hot path never recompiles an interpolated regex per key.
/(?<![a-z0-9])(?:#{GUARDED_SECRET_ROOTS.map { |r| Regexp.escape(r) }.join('|')})(?![a-z0-9])/- SECRET_VALUE_PATTERNS =
---- Value patterns (rule 14 second pass) ------------------------------ A pass over string VALUES/bodies so a secret under an UNLISTED key still masks. \b-bounded (usable both whole-value and embedded), prefix- anchored, low false-positive vendor key shapes catalogued by OSS secret scanners (detect-secrets Apache-2.0, secretlint MIT, gitleaks MIT). Bare high-entropy / plain 32-hex / base64 / credit-card-Luhn are deliberately OMITTED (opt-in only — they destroy observability without a nearby keyword). Cited in RESEARCH.md.
[ %r{\b(?:Bearer|Basic)\s+[A-Za-z0-9\-._~+/=]{8,}}i, # auth header values /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/, # JWT /\b(?:sk|pk|rk)[-_](?:ant-)?[A-Za-z0-9_-]{16,}\b/, # OpenAI/Anthropic/generic /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}\b/, # Stripe live /\bgh[pousr]_[A-Za-z0-9]{16,}\b/, # GitHub tokens /\bgithub_pat_[A-Za-z0-9_]{16,}\b/, # GitHub fine-grained PAT /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, # Slack /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/, # AWS access key id /\bAIza[0-9A-Za-z\-_]{20,}\b/, # Google API key /\bglpat-[A-Za-z0-9\-_]{16,}\b/, # GitLab PAT /\bnpm_[A-Za-z0-9]{16,}\b/, # npm /\bpypi-[A-Za-z0-9\-_]{16,}\b/, # PyPI /\bhf_[A-Za-z0-9]{16,}\b/, # HuggingFace /\bshp(?:ss|at|ca|pa|a)_[A-Za-z0-9]{16,}\b/, # Shopify /\bdapi[a-f0-9]{32}\b/i, # Databricks /-----BEGIN[A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END[A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----/, # PEM ].freeze
- SECRET_VALUE_RE =
One unioned regex (per-pattern flags preserved) so the value-pattern pass is a SINGLE scan per string instead of one gsub per pattern.
Regexp.union(SECRET_VALUE_PATTERNS)
- REDACTED =
The two mask shapes (rule 14). Secret-key matches keep a debug tail; PII matches and non-scalar values redact 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
132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/foam/otel/api.rb', line 132 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
165 166 167 |
# File 'lib/foam/otel/api.rb', line 165 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.
232 233 234 235 236 237 238 239 |
# File 'lib/foam/otel/api.rb', line 232 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.
203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/foam/otel/api.rb', line 203 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.
146 147 148 149 150 151 152 153 154 |
# File 'lib/foam/otel/api.rb', line 146 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) -----------------
157 158 159 |
# File 'lib/foam/otel/api.rb', line 157 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
48 49 50 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 |
# File 'lib/foam/otel/init.rb', line 48 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) 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.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/foam/otel/api.rb', line 178 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.
174 175 176 |
# File 'lib/foam/otel/init.rb', line 174 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
161 162 163 |
# File 'lib/foam/otel/api.rb', line 161 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.
197 |
# File 'lib/foam/otel/api.rb', line 197 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).
181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/foam/otel/init.rb', line 181 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! 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 |
# 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, # Customer lists EXTEND the always-on floor (rule 14) — 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 |
# 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 masking (rule 18 C.3): the value lands ALREADY-MASKED, # so tenant processors and inert-mode providers only ever see the # redacted view of helper-set data. The exporter floor re-applies # (idempotent) — the wire value is unchanged. span.set_attribute(key.to_s, Redaction.mask_one(key.to_s, 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
120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/foam/otel/api.rb', line 120 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).
170 171 172 |
# File 'lib/foam/otel/api.rb', line 170 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.
218 219 220 221 222 223 |
# File 'lib/foam/otel/api.rb', line 218 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 |