Transaction Science open standards monorepo. Each subdirectory is a self-contained reference protocol with its own README and licence. - openpay/ Apache-2.0 payment-acceptance stack (Rust, 26 crates) - smart-byte/ CC-BY-4.0 content-addressed value carrier (spec) - eoc/ CC-BY-4.0 energy-optimised compute (spec) The wire format and the right to fork are public. The optional hosted services are the offer; the protocols are not.
160 lines
5.8 KiB
Markdown
160 lines
5.8 KiB
Markdown
# OpenPay — observability
|
|
|
|
`op-server` uses `tracing` end-to-end. Every interesting operation
|
|
is wrapped in an instrumented span; spans carry structured fields
|
|
(amount, currency, driver, idempotency key, batch id, ...). What
|
|
you do with that output is a configuration choice.
|
|
|
|
## Log levels
|
|
|
|
Set via `RUST_LOG`, parsed by `tracing-subscriber::EnvFilter` in
|
|
`crates/op-server/src/main.rs`. Default when unset:
|
|
|
|
```
|
|
RUST_LOG=info,op_server=debug
|
|
```
|
|
|
|
Per-crate overrides work as you'd expect:
|
|
|
|
```
|
|
RUST_LOG=warn,op_server=debug,op_orchestrator=trace,op_webhook=debug
|
|
```
|
|
|
|
## JSON output for ingestion
|
|
|
|
For journald → loki / Datadog / Honeycomb / OpenTelemetry
|
|
collectors, you want newline-delimited JSON. The shipped `main.rs`
|
|
uses the default pretty subscriber and **does not** branch on
|
|
`OP_LOG_FORMAT`. Patch `main.rs` to read it:
|
|
|
|
```rust
|
|
// crates/op-server/src/main.rs
|
|
let want_json = std::env::var("OP_LOG_FORMAT")
|
|
.map(|v| v.eq_ignore_ascii_case("json"))
|
|
.unwrap_or(false);
|
|
|
|
let base = tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| EnvFilter::new("info,op_server=debug")),
|
|
);
|
|
|
|
if want_json {
|
|
base.json().init();
|
|
} else {
|
|
base.init();
|
|
}
|
|
```
|
|
|
|
(Add `tracing-subscriber` with the `json` feature in
|
|
`crates/op-server/Cargo.toml` if it isn't already pulled in.)
|
|
|
|
With `OP_LOG_FORMAT=json`, every line on stdout is a parseable JSON
|
|
object: timestamp, level, target, span path, fields. journald
|
|
preserves these; promtail / vector / fluent-bit picks them up
|
|
unchanged.
|
|
|
|
## Spans worth dashboarding
|
|
|
|
These are the `tracing::instrument` spans the workspace currently
|
|
emits. Grep `crates/op-*/src/` for `tracing::instrument` to find
|
|
them in source.
|
|
|
|
| Span | Where | Key fields |
|
|
|---|---|---|
|
|
| `orchestrator.run` | `op-orchestrator/src/engine.rs` | `idempotency_key`, `amount_minor`, `currency` |
|
|
| `orchestrator.resume` | `op-orchestrator/src/engine.rs` | `idempotency_key`, `driver`, `rail` |
|
|
| `webhook.dispatch` | `op-webhook/src/dispatcher.rs` | `event_id`, `event_type`, `payload_bytes` |
|
|
| `ledger.post_transaction` | `op-ledger/src/store.rs` | `tx_id`, `ledger_id`, `external_id` |
|
|
| `settlement.open_batch` | `op-settlement/src/engine.rs` | `currency`, `rail` |
|
|
| `settlement.add_entry` | `op-settlement/src/engine.rs` | `batch_id` |
|
|
| `settlement.close_batch` | `op-settlement/src/engine.rs` | `batch_id`, `dispute_adjustment_bps` |
|
|
| `reconciliation.reconcile` | `op-reconciliation/src/engine.rs` | `window_start`, `window_end`, `tx_count` |
|
|
|
|
Useful dashboards to build:
|
|
|
|
- **Orchestrator latency** — p50/p95/p99 duration of `orchestrator.run`, broken out by `currency`. Spikes correlate with rail issues.
|
|
- **Webhook delivery health** — rate of `webhook.dispatch` events grouped by `event_type` and outcome. Failures (visible at `WARN` or `ERROR` level inside the span) feed dunning on receiver outages.
|
|
- **Ledger throughput** — count of `ledger.post_transaction` per minute. The system-of-record write rate. A sudden drop while `orchestrator.run` keeps rolling means writes are stuck.
|
|
- **Settlement cycle time** — gap between `settlement.open_batch` and the matching `settlement.close_batch` (correlate on `batch_id`).
|
|
- **Reconciliation lag** — frequency of `reconciliation.reconcile` and the `tx_count` per run.
|
|
|
|
## Request tracing
|
|
|
|
`tower_http::trace::TraceLayer` is layered in `main.rs`. Every
|
|
HTTP request gets a span with `method`, `uri`, `status`, `latency`.
|
|
If you're running behind Caddy, correlate Caddy's access-log
|
|
`request_id` field with the request-scope span fields by adding a
|
|
header-extracting middleware — Axum's `tower-http::request_id`
|
|
will do it in a few lines.
|
|
|
|
## journald + Loki pipeline
|
|
|
|
If you're on systemd (the recommended deploy mode in
|
|
`openpay.service`):
|
|
|
|
```bash
|
|
journalctl -u openpay -f # tail live
|
|
journalctl -u openpay --since "1h ago" # last hour
|
|
journalctl -u openpay -p err # errors only
|
|
```
|
|
|
|
To ship to Loki, run `promtail` reading the systemd journal:
|
|
|
|
```yaml
|
|
# /etc/promtail/config.yml (excerpt)
|
|
scrape_configs:
|
|
- job_name: openpay
|
|
journal:
|
|
max_age: 12h
|
|
labels:
|
|
job: openpay
|
|
relabel_configs:
|
|
- source_labels: ['__journal__systemd_unit']
|
|
target_label: 'unit'
|
|
pipeline_stages:
|
|
- json:
|
|
expressions:
|
|
level: level
|
|
target: target
|
|
span: 'spans[0].name'
|
|
```
|
|
|
|
With `OP_LOG_FORMAT=json` set, the `json` pipeline stage parses
|
|
each line and exposes `level`, `target`, span names as Loki
|
|
labels — query by `{job="openpay", level="error"}` etc.
|
|
|
|
## Datadog / Honeycomb / generic OTLP
|
|
|
|
For OpenTelemetry export, drop `tracing-opentelemetry` into your
|
|
patched `main.rs`. The instrumented spans translate directly into
|
|
OTLP spans with attributes; correlation IDs (`idempotency_key`,
|
|
`batch_id`, `tx_id`) ride through as span attributes for free.
|
|
This is operator-side glue; the workspace deliberately doesn't
|
|
ship an opinionated OTLP setup.
|
|
|
|
## Health and readiness
|
|
|
|
Two endpoints, both ungated by auth (or behind `with_bypass_paths`
|
|
if you turned on `ApiKeyAuthLayer`):
|
|
|
|
```
|
|
GET /health → 200 always (liveness; "is the process up?")
|
|
GET /readiness → 200 if stores answer; 503 otherwise
|
|
```
|
|
|
|
Wire both into your load balancer / orchestrator. `/health` for
|
|
restart decisions, `/readiness` for "should we route traffic
|
|
here?" Caddy's `health_uri /health` in `Caddyfile.sample` uses the
|
|
first.
|
|
|
|
## Metrics?
|
|
|
|
There is no Prometheus / StatsD endpoint shipped today. The
|
|
`tracing` event stream is the source of truth — derive metrics
|
|
from it via `tracing-subscriber`'s metrics layer, or scrape your
|
|
log aggregator. If you need a `/metrics` endpoint badly enough to
|
|
add it, the right place is a new route in
|
|
`crates/op-server/src/routes.rs` backed by `metrics-exporter-prometheus`
|
|
and a few `metrics::counter!` calls sprinkled where the
|
|
`tracing::instrument` spans already sit.
|