Metadata-Version: 2.4
Name: foam-otel
Version: 1.1.1
Summary: Foam's OpenTelemetry instrumentation package — the Python implementation of the fleet base-package spec.
Project-URL: Homepage, https://foam.ai
Author: Foam AI, Inc.
License: Foam Proprietary License
        
        Copyright (c) 2026 Foam AI, Inc. All rights reserved.
        
        This software is licensed, not sold. Use is permitted only by Foam and by
        customers with an active Foam agreement, solely to instrument their own services
        for the Foam platform. No other person or entity may use, copy, modify,
        redistribute, or sublicense this software.
        
        GRANT. Subject to an active Foam agreement, Foam grants the customer a
        non-exclusive, non-transferable, revocable license to install and run this
        software solely to instrument the customer's own services and send the resulting
        telemetry to the Foam platform.
        
        RESTRICTIONS. No redistribution, no sublicensing, no resale, and no use without
        an active Foam agreement. The software may not be copied or modified except as
        strictly necessary to run it for the purpose above.
        
        OWNERSHIP. The software is licensed, not sold; it remains the property of Foam
        AI, Inc. All rights not expressly granted are reserved.
        
        TERMINATION. This license terminates automatically when the Foam agreement ends,
        at which point the customer must stop using and remove the software.
        
        Third-party open-source components this software depends on are listed in
        THIRD-PARTY-NOTICES and remain under their own licenses; nothing in this file
        restricts the rights those licenses grant.
License-File: LICENSE
Keywords: foam,logs,metrics,observability,opentelemetry,tracing
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Requires-Dist: opentelemetry-api>=1.44
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44
Requires-Dist: opentelemetry-instrumentation>=0.65b0
Requires-Dist: opentelemetry-sdk>=1.44
Provides-Extra: fastapi
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.65b0; extra == 'fastapi'
Provides-Extra: httpx
Requires-Dist: opentelemetry-instrumentation-httpx>=0.65b0; extra == 'httpx'
Provides-Extra: llm
Requires-Dist: opentelemetry-instrumentation-openai-v2>=2.0b0; extra == 'llm'
Description-Content-Type: text/markdown

# foam-otel

Foam's Python base package: a thin, safe wrapper around the official
OpenTelemetry libraries that turns on automatic instrumentation, ships traces,
metrics, and logs to foam, and hands you a small set of bulletproof helper
functions. One `init()` at process start is the whole integration — everything
else is optional. Built on [OpenTelemetry](https://opentelemetry.io) (the
Python API/SDK, the OTLP-over-HTTP exporters, and the official
`opentelemetry-instrumentation-*` contrib packages); see `THIRD-PARTY-NOTICES`
for full credits.

This README is the FDE manual — everything you need to integrate, verify, and
troubleshoot lives here, not in foam's source.

## Install

```bash
pip install foam-otel
```

`foam-otel` depends only on the OpenTelemetry API/SDK, the OTLP-HTTP exporter,
and the instrumentation base. Framework and client instrumentations arrive as
**extras** — installing one pulls the official contrib package, and `init()`
auto-activates it (the entry-point sweep, below). No adapter code ships in
foam.

```bash
pip install "foam-otel[fastapi]"   # + opentelemetry-instrumentation-fastapi
pip install "foam-otel[httpx]"     # + opentelemetry-instrumentation-httpx
```

Any other official instrumentation you install (`pip install
opentelemetry-instrumentation-requests`, `-botocore`, `-psycopg`, …) is
activated the same way at `init()` — presence-checked, never blind-registered.

## The scenario matrix

One copy-paste-ready snippet per supported situation (spec rule 33). If your
situation is missing here, that is a docs bug — file it. All calls use the real
`init(...)` keyword arguments. **Load foam's `init()` before your app imports
the libraries you want instrumented** — instrumentation that registers after a
library is imported never applies to already-bound references (GOTCHAS G1).

```python
# 1 — CLEAN SERVICE (door 1, the common case) — run at the top of your entrypoint
import os
from foam_otel import init

env = os.environ.get("APP_ENV", "development")
init(
    name="checkout-api",                 # -> service.name
    environment=env,                     # -> deployment.environment.name (verbatim)
    enabled=env not in ("test", "ci"),   # the one export switch (idiomatic recipe)
    token=os.environ["FOAM_TOKEN"],      # Bearer auth; required only when enabled
    version=os.environ.get("GIT_SHA"),   # -> service.version (recommended)
)
```

```python
# 2 — BESIDE A PROPRIETARY AGENT (situation A): disjoint pipelines.
# Foam runs its own pipeline next to the agent's; you wire the agent's intake
# host into the loop guard so foam never traces the agent's own egress.
init(
    name="checkout-api", environment=env, enabled=True, token=FOAM_TOKEN,
    ignored_outbound_hosts=["agent-intake.vendor.example"],
)
```

```python
# 3 — TENANT RIDES FOAM (situation C): a scoped SDK (LLM eval / AI-obs) attaches
# a processor to foam's pipeline. Pass a CONSTRUCTED instance + the REQUIRED
# ignore entry for its exporter's host (or its export traffic becomes a loop).
from eval_tool_sdk import EvalToolSpanProcessor

init(
    name="checkout-api", environment=env, enabled=True, token=FOAM_TOKEN,
    additional_span_processors=[EvalToolSpanProcessor(project="prod")],
    ignored_outbound_hosts=["ingest.eval-tool.example"],
)
```

```python
# 4 — FOREIGN OTEL SDK OWNS TRACES (situation B): foam is DARK for that signal TODAY.
# Door 2's ingest entries are DEFERRED (rule 22a) — no create_foam_ingest_* exists.
# init() still runs: it warns honestly for traces and OWNS every free signal.
# Today's options for the claimed signal: remove the foreign SDK (scenario 1),
# feed foam server-side from their collector, or file the door-2 need — a real
# customer here IS the demand that ships the ingest entries via foam's update path.
# If any signal is free, DO NOT skip init() — compose (scenario 5).
init(name="checkout-api", environment=env, enabled=True, token=FOAM_TOKEN)
```

```python
# 5 — PER-SIGNAL COMPOSITION (traces claimed shown; symmetric over any subset).
# A foreign SDK owns traces only; metrics + logs are free.
init(name="checkout-api", environment=env, enabled=True, token=FOAM_TOKEN)
# -> warns ONCE: traces foreign-owned (foam DARK for traces; foam ingest for
#    traces not yet available). metrics + logs register to foam on the FULL
#    contract. Trace ids still correlate through the shared W3C context.
# (Python has per-signal globals, so the doors compose — unlike Java's single
#  composite global.)
```

```python
# 6 — TESTS / CI: fully inert. No token needed — token validates only when enabled.
init(name="checkout-api", environment="test", enabled=False)
# No SDK loads, no providers registered, no network, no warning. Helpers no-op.
```

```python
# 7 — SERVERLESS (door 1 + the flush recipe). The platform freezes the process
# the instant the handler returns; flush() before it does, or buffered telemetry
# is lost. flush() never raises and is safe even if init() never ran.
from foam_otel import init, flush

init(name="orders-fn", environment="production", enabled=True, token=FOAM_TOKEN)

def handler(event, context):
    try:
        return run(event)
    finally:
        flush()
```

**Scenario 8 (browser) does NOT apply to Python.** It exists only in the JS
core's matrix — the browser package is a separate companion, build-time gated
with a browser-scoped token, and this Python core has no browser surface.

## init options

`init()` is keyword-only. Required arguments raise at boot (rule 10) so a
misconfiguration fails on your machine, never silently in production.

| Option | Type | Required | Default | What it changes on the wire / why |
| --- | --- | --- | --- | --- |
| `name` | `str` | YES | — | `service.name` on every export. Blank/non-string raises `ValueError` at init. |
| `environment` | `str` | YES | — | `deployment.environment.name`, exported VERBATIM. Values outside `{production, staging, development, test}` warn (typo guard) but export unchanged. Blank/non-string raises. |
| `enabled` | `bool` | YES | — | The ONE export switch. `False` = fully inert: no SDK import, no providers, no network, no warning, `token` untouched. `True` = export in that environment. Non-bool raises `TypeError`. No default because "on by accident in CI" and "off by accident in prod" are both incidents. |
| `token` | `str` | when `enabled=True` | `None` | `Authorization: Bearer <token>` on every OTLP export. Validated (and stripped) ONLY when `enabled=True` and the kill switch is unset; blank/missing there raises `ValueError`. Never read from the environment — you wire it from the service's real secret source. |
| `version` | `str` | no | `None` | `service.version`, verbatim (git SHA recommended). Omitting warns — foam never detects a deploy id at runtime. |
| `redact_keys` | `Sequence[str]` | no | `None` | EXTENDS the always-on secrets floor (never replaces it). Matched keys get the tail mask (`********cafe`); a matched key holding a dict/list masks in full. Normalized-token match: `["apiKey"]` also catches `api_key`, `x-api-key`. |
| `redact_pii_keys` | `Sequence[str]` | no | `None` | YOUR OWN PII field names. Always FULL `[REDACTED]`, never a tail (last-4 of an email is the domain). Foam ships no PII preset and infers nothing — PII posture is your call. |
| `additional_instrumentations` | `Sequence[object]` | no | `None` | Constructed instrumentor instances registered on foam's pipeline, fault-isolated: one throwing instance is skipped with a loud `[foam]` warning naming it, boot proceeds. |
| `additional_span_processors` | `Sequence[object]` | no | `None` | Tenant seam (traces). Your `SpanProcessor` joins foam's tracer provider AFTER the redaction stage — you see the masked view, byte-identical to what foam exports. A throwing instance is DISABLED and warned; foam's export is unaffected. |
| `additional_log_record_processors` | `Sequence[object]` | no | `None` | Same seam, logs (`LogRecordProcessor`). |
| `additional_metric_readers` | `Sequence[object]` | no | `None` | Same seam, metrics. NOTE: a `MetricReader` TRIGGERS collection on its own schedule/thread — that is your code on your thread; foam's never-throw guard covers only foam-invoked calls (flush/shutdown), not your reader's collect loop. |
| `ignored_outbound_hosts` | `Sequence[str]` | no | `None` | EXTENDS the outbound loop guard (rule 24), never replaces it. Explicit hostnames whose outbound calls produce no client spans. REQUIRED for any tenant/agent exporting over HTTP from inside your process (scenarios 2/3). |
| `diagnostics` | `bool` | no | `False` | Turns on `[foam]` self-narration at INFO (the init-ok line with per-signal ownership + instrumentation count, skipped-instrumentation notes). Off = the same messages log at DEBUG. Telemetry data never appears here. |

Unknown keyword arguments are ignored with a single `[foam]` warning naming
them (forward/backward-compat safety) — they never raise.

### Deliberately absent knobs (documented AS absent)

- **`endpoint`** — the fleet endpoint (`https://otel.api.foam.ai`) is pinned in
  code. The ONE override is the operator-level `OTEL_EXPORTER_OTLP_ENDPOINT` env
  var (warns loudly). Never an init option: a code change must not silently
  re-route telemetry.
- **`sampling` / any sampler knob** — none. Every span ships
  (`ParentBased(ALWAYS_ON)`); an unsampled inbound flag cannot drop foam's
  spans, and `OTEL_TRACES_SAMPLER` is inert. Cost control is server-side (rule
  4).
- **`cadence` / batch-delay knobs** — not init surface. Batch cadence is
  delegated to the standard `OTEL_BSP_SCHEDULE_DELAY` /
  `OTEL_BLRP_SCHEDULE_DELAY` / `OTEL_METRIC_EXPORT_INTERVAL` env vars (operator
  tuning), read by the SDK's own processors.
- **`disabled_environments`** — gone. `enabled` is an explicit boolean; compute
  it from your environment in one line (scenario 1).
- **exporter / processor injection** — the only pipeline seam is the three
  additive `additional_*` options. They can never redirect, drop, or displace
  foam's export.

### Environment variables

| Variable | Posture |
| --- | --- |
| `OTEL_SDK_DISABLED=true` | HONORED — the emergency kill switch (config-only, no redeploy). SUPERSEDES `enabled=True`: when set, foam is fully off and warns loudly. Parsed exactly like the SDK: literal `true`, case-insensitive, trimmed. |
| `OTEL_EXPORTER_OTLP_ENDPOINT` (+ `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT`) | HONORED — the one operator-level override of the pinned fleet endpoint. Per-signal variant wins over the base (OTLP-spec precedence). Active override = one loud `[foam]` warning per signal naming the destination. |
| `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BLRP_SCHEDULE_DELAY`, `OTEL_METRIC_EXPORT_INTERVAL` | HONORED — batch/export cadence, delegated to the SDK's own batch processors and periodic reader. Positive integers only; anything else is ignored. Never customer (init) surface. |
| `OTEL_PROPAGATORS=none` | HONORED — the propagation kill lever: injection stops AND inbound extraction stops (the service becomes its own trace root), telemetry keeps flowing. Foam implements `none` itself and warns loudly. Any OTHER value warns "set but ignored" — foam's propagator set is fixed (W3C tracecontext + baggage). |
| `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` | HONORED — a comma-separated entry-point-name list; those instrumentations are skipped at activation (the same var the upstream `opentelemetry-instrument` CLI reads). |
| `TRACEPARENT` / `TRACESTATE` | HONORED (inbound) — a launcher that sets these parents the whole process to its trace (spawn is an inbound call whose carrier is the environment). No-op when unset. |
| Every other `OTEL_*` (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_TRACES_SAMPLER`, `OTEL_EXPORTER_OTLP_HEADERS`/`_TIMEOUT`/`_COMPRESSION`, `OTEL_SEMCONV_STABILITY_OPT_IN`, `OTEL_*_EXPORTER`, attribute limits, …) | INERT — and inert BY CONSTRUCTION. Foam reads every honored var itself, then builds the whole pipeline inside a synchronous window that strips every `OTEL_*` var from `os.environ` (restored before init returns), so no un-consumed var reaches an exporter/provider/instrumentation constructor. Identity and auth come from `init` options only. |

## API — every public function

Every function below **never throws** and **no-ops silently before `init()`,
when `enabled=False`, or when the kill switch is active** — the one exception is
`init()` itself, which raises on developer error (missing/blank required
options) so a misconfigured boot fails in CI, not dark in production. Import
everything from the top-level `foam_otel`.

### `init(*, name, environment, enabled, token=None, version=None, redact_keys=None, redact_pii_keys=None, additional_instrumentations=None, additional_span_processors=None, additional_log_record_processors=None, additional_metric_readers=None, ignored_outbound_hosts=None, diagnostics=False) -> bool`

Wire up foam telemetry (door 1). Validates arguments, classifies the three
global provider slots BEFORE any registration, registers foam's providers into
every FREE slot, activates installed instrumentations (traces only), wires the
loop guard, and installs the stdlib-logging root export. Idempotent: a second
call warns and returns. Returns `True` when foam is exporting at least one
signal, `False` when disabled / killed / every signal is foreign-owned or
failed. See the options table for every parameter.

```python
from foam_otel import init

owned = init(
    name="checkout-api",
    environment="production",
    enabled=True,
    token=os.environ["FOAM_TOKEN"],
    version=os.environ.get("GIT_SHA"),
)
# owned is True when >= 1 signal reaches foam
```

### `flush() -> None`

Force-flush every foam-owned signal without tearing anything down. Never raises;
safe before `init()` (silent no-op). The serverless and pre-crash lifeline —
call it before the process can freeze (scenario 7).

```python
from foam_otel import flush

flush()  # batched spans/logs/metrics are on the wire when this returns
```

### `shutdown() -> None`

Flush and stop export for every foam-owned signal, unregister the globals foam
set (providers, the `TRACEPARENT` context token, the stdlib root handler), and
reset to a pre-init state — so a later `init()` (e.g. a post-fork re-init) can
run. Fail-open; safe without init; never raises. Helpers no-op afterward.

```python
import signal, sys
from foam_otel import shutdown

def _graceful(*_):
    shutdown()
    sys.exit(0)

signal.signal(signal.SIGTERM, _graceful)
```

### `span(name, attributes=None)`

A context manager: run a block inside a new ACTIVE span. On exception it records
the exception, sets status `ERROR`, re-raises the IDENTICAL error, and ALWAYS
ends the span. When foam is not active it yields `None` and runs the block
uninstrumented — your code never behaves differently because telemetry is off.
`attributes`, if given, are set on the span (masked).

```python
from foam_otel import span

with span("price-cart", {"cart.items": len(cart)}) as s:
    total = price_cart(cart)   # if this raises, the error is recorded and re-raised
```

### `set_attribute(key, value) -> None`

Set one attribute on the currently active span; no-op when none is active (never
fabricates a span). The value rides the redaction engine, so a secret-named key
is masked before it lands on the span.

```python
from foam_otel import set_attribute

set_attribute("order.id", order.id)
set_attribute("api_key", key)   # -> "********" + last 4, masked before it exists on the span
```

### `set_attributes(attributes) -> None`

Set many attributes on the active span at once; each value is masked by key. No-op
without an active span.

```python
from foam_otel import set_attributes

set_attributes({"customer.tier": "gold", "password": "sup3rs3cr3tvalue"})
# -> password: "********alue"
```

### `add_event(name, attributes=None) -> None`

Add a timestamped event to the active span; attributes masked. No-op without an
active span.

```python
from foam_otel import add_event

add_event("cache.miss", {"key": cache_key})
```

### `record_exception(error, attributes=None) -> None`

Record a standard OTel exception event (type/message/stacktrace) on the active
span and set status `ERROR`. Accepts any `BaseException`, including your own
error classes. NEVER notifies a vendor tracker — the bridge direction is tracker
-> foam only. No-op without an active span. Attributes masked.

```python
from foam_otel import record_exception

try:
    charge(card)
except PaymentError as err:
    record_exception(err)
    raise
```

### `get_trace_context() -> dict`

The active span's ids for correlating foam traces with your OWN logs and
outbound payloads: `{"trace_id": <32 hex>, "span_id": <16 hex>}`. Returns an
EMPTY dict when no valid span is active — never fabricates ids.

```python
from foam_otel import get_trace_context

ctx = get_trace_context()
if ctx:
    my_logger.info("charging card", extra={"trace_id": ctx["trace_id"]})
```

### `increment_counter(name, n=1, attributes=None) -> None`

Add to a monotonic counter (`create_counter`). Instruments are lazily created
and cached per name (rebound once if the provider swaps). Names pass through
verbatim.

```python
from foam_otel import increment_counter

increment_counter("orders_placed", 1, {"plan": "pro"})
```

### `record_histogram(name, value, attributes=None) -> None`

Record a distribution observation (durations, sizes) via `create_histogram`.

```python
import time
from foam_otel import record_histogram

record_histogram("checkout_duration_ms", (time.monotonic() - started) * 1000)
```

### `add_up_down_counter(name, n, attributes=None) -> None`

Add to a counter that can decrease (in-flight counts, pool sizes), via
`create_up_down_counter`. Pass a negative `n` to subtract.

```python
from foam_otel import add_up_down_counter

add_up_down_counter("jobs_in_flight", +1)
# ... work ...
add_up_down_counter("jobs_in_flight", -1)
```

### `set_metric(name, value, attributes=None) -> None`

Set a synchronous gauge — last value wins (queue depth, temperature-style
readings), via `create_gauge`.

```python
from foam_otel import set_metric

set_metric("queue_depth", queue.qsize())
```

**Metrics — attribute discipline.** Every distinct attribute combination is a
live metric stream. `increment_counter("requests", 1, {"user_id": uid})` is a
memory leak and a flat dashboard (the SDK caps streams protectively). Keep
attribute VALUES low-cardinality: plans, regions, status classes — never ids,
raw paths, or emails.

### `log(severity, body, attributes=None) -> None`

Emit a log record through foam's pipeline, trace-correlated to the active span.
`severity` is one of `trace|debug|info|warn|warning|error|fatal|critical`
(case-insensitive; anything unknown lands as INFO — never throws). The body and
attributes ride the redaction engine (string bodies get the value-pattern pass;
dict/list bodies deep-redact).

```python
from foam_otel import log

log("warn", "payment retry scheduled", {"attempt": 2, "gateway": "stripe"})
```

Most apps don't call `log()` directly — they use stdlib `logging`, which foam
bridges automatically (see `install_root_export` below).

### `redact(value) -> str`

Foam's masking on demand, for your OWN sinks (your logger, an audit trail). Same
fail-closed engine foam uses internally, with no key context: a scalar
string/number gets the tail mask, everything else masks in full. Empty string on
hard failure — never the unredacted payload. Never raises.

```python
from foam_otel import redact

my_logger.info(f"token used: {redact(api_token)}")
# redact("sk-live-0123456789abcdef") -> "********cdef"   (len >= 12, tail kept)
# redact("short")                    -> "********"        (len < 12, full mask)
# redact({"nested": "secret"})       -> "********"        (non-scalar, full mask)
```

`DEFAULT_REDACTED_KEYS` is also exported (read-only tuple) — the always-on
secrets floor `redact_keys`/`redact_pii_keys` extend. It is informational; you
cannot disable it.

### `get_tracer(name=None)` / `get_meter(name=None)` / `get_logger(name=None)`

The raw OTel `Tracer` / `Meter` / `Logger` on whatever pipeline owns the process
globals (foam's when foam owns the signal; a foreign SDK's in inert mode).
Everything the helper set doesn't wrap — span links, explicit span kinds,
observable/async instruments, batch log emission — is reachable here; this is
what keeps the helper set small and CLOSED. Default scope is `app.custom`. Before
init they return safe no-op objects. NOTE: a span you `start_span()` here is
yours to `end()` — a never-ended span leaks; the `span()` helper cannot leak by
construction.

```python
from foam_otel import get_tracer
from opentelemetry.trace import SpanKind

tracer = get_tracer("worker")
with tracer.start_as_current_span("drain-batch", kind=SpanKind.CLIENT, links=links) as s:
    drain()   # explicit kind + links — not wrapped by span(), reached via the passthrough
```

```python
from foam_otel import get_meter
from opentelemetry.metrics import Observation

meter = get_meter("inventory")
def observe(options):
    return [Observation(current_stock())]
meter.create_observable_gauge("stock_level", callbacks=[observe])  # async instrument
```

### `install_root_export() -> bool`

Attach a plain OTel `LoggingHandler` to the ROOT logger so every stdlib logger
that propagates (the normal case) exports to foam, trace-correlated. `init()`
calls this automatically when foam owns the logs signal — you rarely call it
yourself. Idempotent: it stands down if any OTel export handler is already on
root, and is RE-callable after `logging.basicConfig(force=True)` wipes root
handlers. Returns `True` when a foam (or existing OTel) handler covers root,
`False` when foam isn't active or owning logs. Fail-open.

```python
import logging
from foam_otel import install_root_export

logging.basicConfig(force=True)   # your app resets root handlers, wiping foam's
install_root_export()             # re-attach foam's root export
```

### `export_handler(record_filter=None, transform=None)`

Build a policy-drivable OTel export handler for loggers you attach it to
YOURSELF — the path for libraries that set `propagate=False` and never reach the
root handler. `record_filter(record) -> bool` gates each record;
`transform(copy)` may scrub a COPY of the record (return `None` to drop it, so
other handlers on that logger still see the original). Returns `None` when foam
isn't active or doesn't own logs.

```python
import logging
from foam_otel import export_handler

def only_warnings(record):
    return record.levelno >= logging.WARNING

handler = export_handler(record_filter=only_warnings)
```

### `attach_to_non_propagating(handler, *, skip_prefixes=("foam_otel", "opentelemetry")) -> list`

Attach `handler` to every ALREADY-REGISTERED logger with `propagate=False`
(libraries that detach from root, so the root export never sees them). Idempotent
per logger by a marker attribute (not class identity), so a `--reload` re-import
can't double-attach. Propagating loggers are never touched. Returns the list of
logger names it attached to. Re-call after importing libraries that configure
their loggers late.

```python
from foam_otel import export_handler, attach_to_non_propagating

handler = export_handler()
if handler is not None:
    attached = attach_to_non_propagating(handler)   # e.g. ["browser_use", "some.detached.logger"]
```

## Failure modes — what happens, and where the warning appears

All foam warnings are single-line, prefixed `[foam]`, emitted on the
`foam_otel` stdlib logger (visible on the root handler / your logging config).

| Situation | Behavior |
| --- | --- |
| Missing/blank `token` while `enabled=True` (kill switch unset) | `init()` raises `ValueError` at boot — fail in CI, not dark in prod. |
| Blank/non-string `name` or `environment` | `init()` raises `ValueError`, naming the option. |
| Non-bool `enabled` | `init()` raises `TypeError`. |
| `enabled=False` | Fully inert and SILENT. No SDK import, no providers, no network, `token` never touched. Helpers no-op. |
| `OTEL_SDK_DISABLED=true` | Same inert posture, but LOUD: one `[foam]` warning that the kill switch superseded `enabled=True`. |
| `environment` outside `{production, staging, development, test}` | Exports verbatim, plus one `[foam]` warning (typo guard). |
| `version` omitted | Exports without `service.version`, plus one `[foam]` warning. |
| A foreign OTel SDK owns a signal's global | `init()` registers nothing there and warns ONCE per claimed signal naming the owner (see the coexistence guide); free signals still register to foam. |
| Export endpoint overridden via `OTEL_EXPORTER_OTLP_ENDPOINT` | One `[foam]` warning per signal naming the destination. |
| A broken exporter / instrumentation at runtime | That signal is disabled with a `[foam]` warning; the host app never crashes (fail-open per signal). Runtime detail is visible under `diagnostics=True`. |
| `init()` called twice | Second call warns `[foam] init called twice …` and returns the current ownership; the first init stands. |

## Coexistence guide — when foam is not alone

`init()` classifies all three global provider slots (traces, metrics, logs)
BEFORE registering anything, then registers into every FREE slot and warns once
per CLAIMED slot. Three situations (rule 18):

- **A — a proprietary APM agent beside foam** (a vendor agent running its own
  private pipeline, no OTel globals). Disjoint pipelines; both run. Foam never
  touches the agent. Your job: add the agent's intake host to
  `ignored_outbound_hosts` (scenario 2) so foam never traces the agent's own
  export traffic. (If a modern agent ALSO registers an OTel global, that signal
  becomes situation B — the classifier decides per signal.)

- **B — a foreign OTel SDK owns a signal's global.** Foam is **DARK** for that
  signal — *not degraded*: NOTHING for that signal reaches foam. For each claimed
  signal, `init()` emits exactly one warning:

  > `[foam] traces: the global provider is already owned by <Owner> — foam
  > registered nothing for this signal and is DARK for it (not degraded: nothing
  > for traces reaches foam). Foam ingest for traces is not yet available. Helper
  > telemetry for traces rides the foreign provider and lands wherever <Owner>
  > exports — typically a third-party vendor's dashboards and bill — carrying
  > that SDK's resource identity.`

  What it means TODAY: **foam ingest for the claimed signal is not yet available**
  — door 2 (the `create_foam_ingest_*` entries) is a DEFERRED, frozen design
  (rule 22a); it does not exist yet and arrives via foam's update path by
  explicit opt-in when a real customer needs it. Your options now: remove the
  foreign SDK, feed foam server-side from their collector, or file the door-2
  need. Your helpers keep working, but that signal's telemetry rides the foreign
  provider and carries ITS resource identity, not foam's. (The owner name is read
  from the provider's class; a registered provider doesn't announce its vendor,
  so "an unknown OTel SDK" is the honest fallback.)

- **C — a scoped tenant rides foam's pipeline** (an LLM-eval / AI-observability
  SDK that attaches a processor/reader). Pass its CONSTRUCTED instance via
  `additional_span_processors` / `additional_log_record_processors` /
  `additional_metric_readers` (scenario 3). Guarantees: strictly ADDITIVE (foam's
  export is byte-identical with or without it), FAULT-ISOLATED (a throwing span/log
  instance is DISABLED loudly, foam unaffected), and ALREADY-MASKED (the redaction
  stage runs first, so the tenant only ever sees the masked view). **REQUIRED**:
  the tenant exporter's host in `ignored_outbound_hosts` — otherwise foam traces
  the tenant's export calls, which the tenant re-exports (a loop in the tenant's
  pipeline). A metric READER is different in kind: it collects on its own
  schedule/thread, so its collect loop is your code, not covered by foam's guard.

**Claim severity is not uniform.** A claimed MeterProvider costs foam little —
RED metrics derive server-side from foam-owned spans; only custom metric-helper
data diverts. A claimed TracerProvider costs the spans AND everything derived
from them. Prioritize freeing traces first.

**Late arrival.** A foreign SDK can register AFTER foam boots. The same
once-per-signal warning fires on the next helper use when the cached provider
identity no longer matches the global — a coexistence warning is not only a
boot-time event.

## Recipes

- **Per-environment enable (the one-liner):**
  `enabled=os.environ.get("APP_ENV") not in ("test", "ci")` — scenario 1. Other
  valid shapes: `enabled=os.environ.get("APP_ENV") in ("production", "staging")`
  (explicit allowlist), `enabled=bool(os.environ.get("FOAM_ENABLED"))` (your own
  flag), `enabled=True` (export everywhere, honestly labeled).

- **Serverless flush:** scenario 7 — `try: … finally: flush()`. The platform
  freezes the process before any batch timer fires; `flush()` is the fix. Import
  cost is lazy: importing `foam_otel` costs only the OTel API; the SDK and
  exporters load inside `init()` (and never with `enabled=False`).

- **Gunicorn / uvicorn / any pre-fork server — foam re-inits automatically.**
  Foam registers a post-fork hook (`os.register_at_fork`), so under
  `gunicorn --preload` or a `uvicorn` worker fork each child **re-inits
  automatically and re-mints a fresh `service.instance.id`** — N workers report
  as N processes, not one. You do NOT need a manual `post_fork` hook or a
  `shutdown()`+`init()` dance. (A *bare* `init()` inside a hand-written
  `post_fork` would be a no-op — one init per process — but the automatic hook
  makes that unnecessary. On platforms without `os.fork`, the hook is skipped.)
  See GOTCHAS "Fork / pre-fork servers".

- **gevent / eventlet workers:** call `monkey.patch_all()` as line 1 of your
  entrypoint, BEFORE `import foam_otel` — patching after `threading`/`ssl`/`socket`
  are imported breaks context propagation and the export threads. See GOTCHAS.

- **What foam deliberately does NOT automate:** reading your git SHA (pass
  `version` yourself), choosing your PII fields (pass `redact_pii_keys`
  yourself), sampling (none exists), named logger bridges beyond the generic
  stdlib bridge (structlog/loguru arrive via the update path — `log()` and the
  root-export bridge cover the mechanism today), and LLM instrumentation (v1
  ships no LLM surface — install the official instrumentor and foam auto-activates
  it, or write tier-3 spans with the helpers/passthroughs).

## Supported versions

| Surface | Supported | Reason |
| --- | --- | --- |
| Python | `>= 3.10` | `init()` uses `importlib.metadata.entry_points(group=...)`, whose keyword form landed in 3.10. |
| `opentelemetry-api` / `opentelemetry-sdk` | `>= 1.44` | The logs SDK's processor contract is `on_emit(ReadWriteLogRecord)` at this line (the older `emit(LogData)` shape is a different, incompatible signature), and foam's redaction rides that hook. API and SDK move in lockstep. |
| `opentelemetry-exporter-otlp-proto-http` | `>= 1.44` | The OTLP-over-HTTP transport foam exports on. |
| `opentelemetry-instrumentation` | `>= 0.65b0` | The entry-point activation base foam sweeps. |
| Instrumentations (`opentelemetry-instrumentation-fastapi`, `-httpx`, …) | via extras / installed | Auto-activated at `init()`, presence-checked. FastAPI is proven end-to-end by the conformance app. |

Foam builds on the [OpenTelemetry](https://opentelemetry.io) project — the
Python API/SDK, the OTLP exporters, and the contrib instrumentations — all
Apache-2.0. Full credits in `THIRD-PARTY-NOTICES`.

## License

Proprietary — see `LICENSE`. Use is permitted only by Foam and customers with an
active Foam agreement. Third-party components are credited in
`THIRD-PARTY-NOTICES` (OpenTelemetry is Apache-2.0).
