openpay/
compliance/ PCI-DSS readiness package (scope-map,
pci-zero-architecture, hsm-kms-guidance,
qsa-checklist), citing PCI-DSS v4.0.1
requirements. (#1)
examples/pci-zero/ Runnable example demonstrating a deployment
where merchant code never touches a PAN. (#1)
crates/op-core/ Tokenized<Card> typestate + NetworkToken
+ CardNetwork + Cryptogram. (#2)
crates/op-rails-card/network_token/
NetworkTokenProvider trait + VTS adapter
+ MDES adapter, with deterministic stubs
behind a 'live' feature flag. (#2)
crates/op-driver-sdk/conformance/network_token.rs
Conformance harness for third-party
NetworkTokenProvider implementations. (#2)
crates/op-cli/src/ledger/ 'openpay ledger as-of' subcommand for
bi-temporal queries. (#3)
crates/op-orchestrator/src/telemetry.rs
OpenTelemetry span emission per
Payment<S> transition with semantic-
conventions tags. (#3)
docs/observability/ Grafana dashboard JSON + wiring guide
for the bi-temporal ledger lens. (#3)
smart-byte/
smart-byte-rs/ Rust reference implementation: envelope
encode/decode (canonical CBOR + BLAKE3
content addressing), Ed25519 sign +
verify, lockstep BFT commit MVP, Iroh
transport scaffold, CLI. (#4)
spec/identity_and_key_rotation.md
Adopts KERI SAIDs + key-event log
format; explicitly does not adopt ACDC
chaining wholesale. (#5)
conformance/v1/ 21 test vectors covering canonicalisation,
SAID computation, signature verification,
lockstep commit, gossip propagation.
CI gate enforces spec/vector coupling. (#6)
eoc/
eoc-rs/ Rust + WASM reference implementation:
4-stage cascade (cache -> kv -> graph ->
neural), joule counters via RAPL / NVML /
powermetrics, CLI, wasm-bindgen surface.
18 tests pass. (#7)
eoc-bench/ Benchmark harness, MT-Bench and Router
case sets, 'joules per MT-Bench point'
metric, 'eoc-bench run | compare' CLI. (#8)
standards/sci-for-ai-submission.md
Submission proposal mapping EOC's
receipt schema to GSF SCI-for-AI. (#9)
cookbook/ HuggingFace-style cookbook (markdown +
runnable Python) demonstrating EOC's
cascade with HF AI Energy Score. (#9)
docs/bench-v0.md Reproducibility doc for eoc-bench.
Workspace state:
- openpay clippy -- -D warnings: clean across all touched crates
- smart-byte-rs cargo test --workspace + clippy: clean
- eoc-rs cargo test --workspace + clippy: clean (18 tests)
- eoc-bench cargo check --workspace + clippy + 15 tests: clean
Rust 1.95.0 edition 2024 pinned at the open-standards root via
rust-toolchain.toml; every workspace inherits.
Closes #1 #2 #3 #4 #5 #6 #7 #8 #9.
203 lines
7.9 KiB
Rust
203 lines
7.9 KiB
Rust
//! OpenTelemetry instrumentation for the typestate machine.
|
|
//!
|
|
//! This module is the **lens**, not the machine. It exposes:
|
|
//!
|
|
//! - [`instrument_transition`] — emit a `tracing::Span` tagged with
|
|
//! the semantic-conventions attributes operators expect on every
|
|
//! `Payment<S>` state transition: `payment.id`, `payment.state.from`,
|
|
//! `payment.state.to`, `payment.rail`, `payment.amount.minor`,
|
|
//! `payment.amount.currency`. Callers wrap their transition body in
|
|
//! `let _g = instrument_transition(...).entered();`.
|
|
//! - [`init_telemetry`] — opt-in initializer that wires the
|
|
//! `tracing` subscriber to an OTLP exporter when the `telemetry`
|
|
//! feature is enabled. Without the feature, it returns `Ok(())`
|
|
//! silently so the same call site compiles in both modes.
|
|
//!
|
|
//! ## Why a helper instead of inline `tracing::span!`?
|
|
//!
|
|
//! The orchestrator's state machine lives across multiple files
|
|
//! (`engine.rs`, `circuit_breaker.rs`, the rail adapters). Centralising
|
|
//! the attribute names here means:
|
|
//!
|
|
//! 1. Every transition span carries the same schema — Grafana
|
|
//! dashboards built on this schema don't break when a new
|
|
//! transition is added.
|
|
//! 2. Semantic-conventions are documented in one place and reviewed
|
|
//! once.
|
|
//! 3. The follow-up issue that wires this in to every state
|
|
//! transition has a single function to call.
|
|
//!
|
|
//! ## Append-only by issue contract
|
|
//!
|
|
//! Per the spec, this module **does not modify any existing
|
|
//! orchestrator code**. It is additive: a sibling module exposing a
|
|
//! helper. The follow-up issue threads the helper through
|
|
//! `engine::Orchestrator::run` and the typestate-transition methods.
|
|
//!
|
|
//! ## Semantic conventions
|
|
//!
|
|
//! The attribute namespace `payment.*` follows the OpenTelemetry
|
|
//! semantic-conventions pattern (`http.*`, `db.*`, `messaging.*`).
|
|
//! Amounts are recorded as **minor units + currency** rather than
|
|
//! a formatted string so dashboards can aggregate without parsing.
|
|
|
|
use op_core::Money;
|
|
use tracing::{Level, Span, span};
|
|
|
|
/// Emit a structured span describing a single `Payment<S>` state
|
|
/// transition.
|
|
///
|
|
/// The span uses [`Level::INFO`] (transitions are first-class events,
|
|
/// not debug noise) and carries the following attributes:
|
|
///
|
|
/// | Attribute | Type | Meaning |
|
|
/// | -------------------------- | --------- | -------------------------------------------- |
|
|
/// | `payment.id` | string | Idempotency key / opaque payment id |
|
|
/// | `payment.state.from` | string | Source typestate (e.g. `Authorized`) |
|
|
/// | `payment.state.to` | string | Destination typestate (e.g. `Captured`) |
|
|
/// | `payment.rail` | string | Rail name (`card`, `a2a`, `crypto`) |
|
|
/// | `payment.amount.minor` | i64 | Amount in minor units (cents for USD) |
|
|
/// | `payment.amount.currency` | string | ISO 4217 alpha-3 currency code |
|
|
///
|
|
/// Callers entered the span around the transition body:
|
|
///
|
|
/// ```ignore
|
|
/// use op_core::Money;
|
|
/// use op_orchestrator::telemetry::instrument_transition;
|
|
///
|
|
/// let amount = Money::from_minor(1_999, op_core::Currency::USD);
|
|
/// let span = instrument_transition("pay_123", "Authorized", "Captured", "card", &amount);
|
|
/// let _guard = span.enter();
|
|
/// // ... do the transition ...
|
|
/// ```
|
|
///
|
|
/// When no subscriber is registered the span is a no-op (zero cost
|
|
/// modulo a few atomic loads). When a subscriber is registered the
|
|
/// attributes appear on every emitted event inside the guard's scope.
|
|
#[must_use]
|
|
pub fn instrument_transition(
|
|
payment_id: &str,
|
|
from_state: &str,
|
|
to_state: &str,
|
|
rail: &str,
|
|
amount: &Money,
|
|
) -> Span {
|
|
span!(
|
|
Level::INFO,
|
|
"payment.transition",
|
|
payment.id = payment_id,
|
|
payment.state.from = from_state,
|
|
payment.state.to = to_state,
|
|
payment.rail = rail,
|
|
payment.amount.minor = amount.minor_units,
|
|
payment.amount.currency = amount.currency.code(),
|
|
)
|
|
}
|
|
|
|
/// Errors that [`init_telemetry`] can surface.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TelemetryError {
|
|
/// OTLP exporter construction failed (bad endpoint, missing
|
|
/// tonic features, etc.).
|
|
#[error("failed to build OTLP exporter: {0}")]
|
|
ExporterBuild(String),
|
|
/// Subscriber registration failed (another global subscriber is
|
|
/// already installed).
|
|
#[error("failed to install global tracing subscriber: {0}")]
|
|
SubscriberInstall(String),
|
|
}
|
|
|
|
/// Result alias for telemetry initialization.
|
|
pub type TelemetryResult<T> = std::result::Result<T, TelemetryError>;
|
|
|
|
// ============================================================
|
|
// `telemetry` feature ON — wire `tracing` to an OTLP exporter.
|
|
// ============================================================
|
|
|
|
/// Initialize the OpenTelemetry pipeline.
|
|
///
|
|
/// When the `telemetry` feature is **enabled**, this:
|
|
///
|
|
/// 1. Builds an OTLP gRPC exporter pointed at `otlp_endpoint`
|
|
/// (defaults to `http://127.0.0.1:4317` if `None`).
|
|
/// 2. Wraps it in a batch span processor.
|
|
/// 3. Installs a `tracing-subscriber` layer that forwards every
|
|
/// `tracing` span (including the ones emitted by
|
|
/// [`instrument_transition`]) to the exporter.
|
|
///
|
|
/// When the feature is **disabled**, this is a `Ok(())` no-op so
|
|
/// the same call site compiles in both modes.
|
|
///
|
|
/// # Errors
|
|
/// Returns [`TelemetryError`] if the exporter or subscriber install
|
|
/// fails.
|
|
#[cfg(feature = "telemetry")]
|
|
pub fn init_telemetry(otlp_endpoint: Option<&str>) -> TelemetryResult<()> {
|
|
use opentelemetry::trace::TracerProvider as _;
|
|
use opentelemetry_otlp::WithExportConfig;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
let endpoint = otlp_endpoint.unwrap_or("http://127.0.0.1:4317");
|
|
|
|
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(endpoint)
|
|
.build()
|
|
.map_err(|e| TelemetryError::ExporterBuild(e.to_string()))?;
|
|
|
|
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
|
.with_batch_exporter(exporter)
|
|
.build();
|
|
let tracer = provider.tracer("op-orchestrator");
|
|
|
|
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
|
|
tracing_subscriber::registry()
|
|
.with(otel_layer)
|
|
.try_init()
|
|
.map_err(|e| TelemetryError::SubscriberInstall(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================
|
|
// `telemetry` feature OFF — no-op stub.
|
|
// ============================================================
|
|
|
|
/// Initialize the OpenTelemetry pipeline.
|
|
///
|
|
/// See the feature-enabled version above for the real documentation;
|
|
/// this is the stub used when the `telemetry` feature is **off**.
|
|
#[cfg(not(feature = "telemetry"))]
|
|
#[allow(unused_variables)]
|
|
pub fn init_telemetry(otlp_endpoint: Option<&str>) -> TelemetryResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use op_core::{Currency, Money};
|
|
|
|
#[test]
|
|
fn instrument_transition_emits_named_span() {
|
|
let amount = Money::from_minor(1_999, Currency::USD);
|
|
let span = instrument_transition("pay_abc", "Pending", "Authorized", "card", &amount);
|
|
// The span has the canonical name; subscribers downstream
|
|
// will see the attributes. We only assert the name here
|
|
// because attribute introspection is not stable in
|
|
// `tracing::Span`.
|
|
assert_eq!(span.metadata().map(|m| m.name()), Some("payment.transition"));
|
|
}
|
|
|
|
#[test]
|
|
fn init_telemetry_no_feature_is_ok() {
|
|
// With the `telemetry` feature off, init is a no-op and
|
|
// never fails. With the feature on, this test would only
|
|
// succeed if an OTLP collector is reachable, so we just
|
|
// assert the no-op contract.
|
|
#[cfg(not(feature = "telemetry"))]
|
|
{
|
|
assert!(init_telemetry(None).is_ok());
|
|
}
|
|
}
|
|
}
|