Skip to main content

Foam's OpenTelemetry instrumentation package — the Python implementation of the fleet base-package spec.

Project description

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 (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

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.

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).

# 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)
)
# 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"],
)
# 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"],
)
# 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)
# 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.)
# 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.
# 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.

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).

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.

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).

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.

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.

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.

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.

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.

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.

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.

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.

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.

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).

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.

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.

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
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.

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.

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.

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 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).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

foam_otel-1.1.1.tar.gz (96.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

foam_otel-1.1.1-py3-none-any.whl (51.9 kB view details)

Uploaded Python 3

File details

Details for the file foam_otel-1.1.1.tar.gz.

File metadata

  • Download URL: foam_otel-1.1.1.tar.gz
  • Upload date:
  • Size: 96.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for foam_otel-1.1.1.tar.gz
Algorithm Hash digest
SHA256 1230cb641d62b013e8f1460c0d692e2893d4f59275e7180885377ad5d4f1be9b
MD5 a44d6dede7eded07c1b0085098b174f9
BLAKE2b-256 a1a164ad1ffd5f4e9ce06be4e748f07c48a5678bd89508cdc3cf4e450f76a7c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for foam_otel-1.1.1.tar.gz:

Publisher: release.yml on foam-ai/packages

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file foam_otel-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: foam_otel-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 51.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for foam_otel-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d7230dd0d4d274491f9a649cb021aca7e0d9bf508dd6c423940c9ffc22913e2d
MD5 3324addea25ce5eaa35d47e5be92fa64
BLAKE2b-256 a959781be2a6ded18332a9c890b43695a049adc68c56c21a805c36ed7c8e09fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for foam_otel-1.1.1-py3-none-any.whl:

Publisher: release.yml on foam-ai/packages

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page