Skip to main content

OpenTelemetry SpanProcessor that anchors selected GenAI spans to BSV via Satsignal.

Project description

satsignal-otel

OpenTelemetry SpanProcessor that anchors selected GenAI spans to BSV via Satsignal. One integration covers any observability stack already speaking OTel — Langfuse, LangSmith, Arize, Datadog, Honeycomb — and adds a tamper-evident receipt for the spans that matter.

Your observability stack shows the run. Satsignal proves the run record hasn't been edited since.

pip install satsignal-otel

What it does

Drop the processor into your TracerProvider. Only spans carrying the attribute satsignal.anchor=true are anchored — everything else flows through to your existing exporters untouched. Matching spans are batched and posted as a single manifest-mode anchor on BSV, binding all leaves under one Merkle root. The on-chain anchor is the receipt your auditor uses to prove the span record has not been edited since that block was mined.

By default the SDK is opt-in per span, batched (1-minute window), sha-only (your span bytes never leave the process), and fail-open (anchor failures log + drop; your app keeps running).

Failed-eval auto-anchor (headline)

Wire one line into your eval pipeline. When a scorer drops below threshold, mark the span — Satsignal anchors the failure with a per- span receipt so the timing claim ("we knew at 14:32 UTC") is provable.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from satsignal_otel import SatsignalSpanProcessor, auto_anchor_on_eval_fail

provider = TracerProvider()
provider.add_span_processor(SatsignalSpanProcessor(
    api_key=os.environ["SATSIGNAL_API_KEY"],
    matter_slug="otel-evals",
))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("eval.scorer") as span:
    score = run_scorer(prompt, response)
    span.set_attribute("gen_ai.eval.score", score)
    auto_anchor_on_eval_fail(span, threshold=0.7)

The helper sets satsignal.anchor=true only when score < threshold; under threshold spans are anchored individually (mode=single) so the receipt timing is per-event, not amortized across a batch.

Release-gate anchor (secondary)

At deploy time, mark the release-manifest span. One receipt per release — what shipped, with the eval evidence around it.

from satsignal_otel import mark_for_anchor

with tracer.start_as_current_span("release.gate") as span:
    span.set_attribute("gen_ai.system", "anthropic")
    span.set_attribute("gen_ai.model", "claude-opus-4-7")
    span.set_attribute("prompt.version", PROMPT_VERSION)
    span.set_attribute("eval.pass_rate", PASS_RATE)
    span.set_attribute("config.hash", CONFIG_HASH)
    mark_for_anchor(span, mode="single", label=f"release-{GIT_SHA}")

Configuration

SatsignalSpanProcessor(
    api_key,                    # required: SATSIGNAL_API_KEY
    matter_slug,                # required: workspace matter for receipts
    base_url="https://app.satsignal.cloud",
    flush_interval=60.0,        # seconds between manifest flushes
    max_batch_size=500,         # force-flush when queue hits this size
    daily_anchor_cap=1000,      # client-side guard against runaway anchoring
    fail_open=True,             # log + drop on anchor failure
    transport=None,             # inject a callable for tests (see below)
)

Attributes the processor reads

Attribute Type Effect
satsignal.anchor bool Required to anchor. True → span is queued.
satsignal.anchor.mode str "manifest" (default, batched) or "single" (per-span anchor).
satsignal.anchor.label str Optional display label on the receipt (truncated at 256 chars).
satsignal.anchor.force_new bool True bypasses server-side dedup (single mode only).

What gets anchored

The sha256 of a deterministic canonical-JSON encoding of the span's {name, kind, status, start_time, end_time, attributes, events, links, resource}. The bytes never leave your process — only the hash. The OTel trace_id and span_id ride along as an off-chain session_id so a verifier can correlate the receipt back to your existing trace data.

This is chain-of-custody for the span record, not the underlying prompt + completion bytes. Your trace store still owns the bytes; the anchor proves they have not been edited since.

Threat model

  • Span attributes are attacker-controllable when prompts include user-provided strings. The label is treated as untrusted display text server-side (length-capped, harness-string-rejected). The sha256 is bytes-are-bytes.
  • An anchor proves anchorer-knowledge, not world-existence. The on-chain receipt commits the anchorer's knowledge of the canonical bytes at the anchored time; it does NOT prove the span existed before then. For end-to-end provenance pair this with a commit- reveal flow over the underlying prompt + completion.
  • Semantic-convention churn. The GenAI OTel semantic conventions are still in development. auto_anchor_on_eval_fail reads gen_ai.eval.score, gen_ai.evaluation.score, and eval.score — pass score_attribute= if your stack names it something else, or pass score= directly.

Sister packages

Testing without the network

The processor accepts a transport= callable matching urllib's shape:

def fake(method, url, headers, body, timeout):
    return 200, b'{"bundle_id": "abc", "txid": "deadbeef", ...}'

processor = SatsignalSpanProcessor(
    api_key="sk_test", matter_slug="m", transport=fake,
)

The full test suite exercises the processor against a mock transport; see tests/test_processor.py for canned-response patterns.

License

MIT.

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

satsignal_otel-0.1.0.tar.gz (21.4 kB view details)

Uploaded Source

Built Distribution

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

satsignal_otel-0.1.0-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file satsignal_otel-0.1.0.tar.gz.

File metadata

  • Download URL: satsignal_otel-0.1.0.tar.gz
  • Upload date:
  • Size: 21.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for satsignal_otel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a3233e711d0c283a6b82ad3a4e7fc80045bfc01a2beeca3663dc0c971baf8e54
MD5 610c99ffcb8af14368ff3511ed83ae7a
BLAKE2b-256 6bbaad71198f67b589a289da0e0a68b03c2c113a8cdd6d5ccf76881f257971af

See more details on using hashes here.

File details

Details for the file satsignal_otel-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: satsignal_otel-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for satsignal_otel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 57e6f3769197122744dc1b870f0f9250a1f1884d35a6d738070bf30e1fb367ee
MD5 d990cf9bb7d66f9e9144288e258711f7
BLAKE2b-256 e49be30060b353f40586230ed98cd5411db2edbd6a523cb4c1911baf3f70ebdd

See more details on using hashes here.

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