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"],
    folder_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

Vocabulary: folder_slug is the canonical name; matter_slug is a deprecated legacy alias that is still accepted silently. The constructor accepts either; at least one is required (legacy callers that pass matter_slug= are unaffected). If both are set to different values the constructor raises ValueError.

Compatibility note (v0.3.0, vocabulary sunset): requests now send the canonical folder_slug wire key, and the live Satsignal API emits canonical response keys only (proof_id, proof_url, folder_slug). Reading still falls back to the legacy response keys (bundle_id, receipt_url, matter_slug) for older self-hosted servers, but a self-hosted server too old to accept the folder_slug request key needs v0.2.x of this package. .folder_slug / .matter_slug read accessors and AnchorResult.proof_id / .proof_url / .folder_slug accessors are available; new code should use the canonical names.

SatsignalSpanProcessor(
    api_key,                    # required: SATSIGNAL_API_KEY
    folder_slug,                # canonical: workspace folder for proofs
    # matter_slug,              # deprecated legacy alias of folder_slug
    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'{"proof_id": "abc", "txid": "deadbeef", ...}'

processor = SatsignalSpanProcessor(
    api_key="sk_test", folder_slug="f", 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.3.0.tar.gz (27.7 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.3.0-py3-none-any.whl (19.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: satsignal_otel-0.3.0.tar.gz
  • Upload date:
  • Size: 27.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for satsignal_otel-0.3.0.tar.gz
Algorithm Hash digest
SHA256 af5f977e07bdf23f08cc67837b987b59eb43f74c0d16f1894457c53935c9fd62
MD5 4f8348604a3559532cd7944ad57cad67
BLAKE2b-256 7021b0680536b670ff814edbe66cc21a14344fbf3f9e7e64f8eb409f4a9f978a

See more details on using hashes here.

Provenance

The following attestation bundles were made for satsignal_otel-0.3.0.tar.gz:

Publisher: publish.yml on Steleet/satsignal-otel

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

File details

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

File metadata

  • Download URL: satsignal_otel-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 19.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for satsignal_otel-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55a0376e0065932abb9e39c5f777e152d444e9f62b78f69d02e9d11c8cf41c60
MD5 5f68774e34b6af086cd0572977e6a573
BLAKE2b-256 2fa8bc3f75018f56b5249f9f7de9ce87f940f655c90a387f63536eaabd16ce9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for satsignal_otel-0.3.0-py3-none-any.whl:

Publisher: publish.yml on Steleet/satsignal-otel

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