Skip to content

Observability & Logging

Sayiir uses tracing for structured logging and spans across the entire Rust stack — runtime, persistence, and database layers. The Python and Node.js bindings include built-in support for exporting traces to any OpenTelemetry-compatible backend (Jaeger, Grafana Tempo, Datadog, etc.).

import sayiir
# Initialize logging (call once at startup)
sayiir.init_tracing()
# Your workflow code...

All logging and tracing behavior is controlled through environment variables.

Controls which log messages are emitted. Sayiir uses the standard tracing_subscriber::EnvFilter filter syntax.

Terminal window
# Set the global log level
RUST_LOG=info # Default — show info, warn, and error
RUST_LOG=debug # Verbose — includes task claim details, snapshot saves
RUST_LOG=warn # Quiet — only warnings and errors
RUST_LOG=error # Minimal — only errors
RUST_LOG=off # Silent — no logs at all

You can also set levels per module to fine-tune output:

Terminal window
# Silence Sayiir internals, keep your app logs
RUST_LOG=info,sayiir_runtime=off,sayiir_postgres=off
# Debug only the database layer
RUST_LOG=warn,sayiir_postgres=debug
# Debug only the worker/runtime layer
RUST_LOG=warn,sayiir_runtime=debug
# Show everything from Sayiir, silence dependencies
RUST_LOG=warn,sayiir_runtime=trace,sayiir_postgres=trace,sayiir_persistence=trace

OTEL_EXPORTER_OTLP_ENDPOINT — OpenTelemetry Endpoint

Section titled “OTEL_EXPORTER_OTLP_ENDPOINT — OpenTelemetry Endpoint”

When set, Sayiir exports traces via gRPC to the specified OTLP endpoint. When unset, only console logging is active (no OTel overhead).

Terminal window
# Local Jaeger (default OTLP gRPC port)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Grafana Tempo
OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo.monitoring:4317
# Datadog Agent
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Identifies your service in the tracing backend. Defaults to sayiir-py (Python) or sayiir-node (Node.js) if not set.

Terminal window
OTEL_SERVICE_NAME=order-service
OTEL_SERVICE_NAME=payment-worker

Sayiir instruments every significant operation with structured spans and fields:

Every database call is wrapped in a span with db.system = "postgresql":

Span NameDescription
db.save_snapshotPersist workflow checkpoint
db.load_snapshotLoad workflow state
db.save_task_resultSave completed task output
db.claim_taskAcquire task claim
db.release_task_claimRelease task claim
db.find_available_tasksPoll for available work
db.store_signalStore cancel/pause signal
db.check_and_cancelCheck for cancellation
db.check_and_pauseCheck for pause request
Span NameDescription
workflowFull workflow execution
taskIndividual task execution with heartbeat
settle_resultPost-task result handling (retry/save/advance)
forkParallel branch execution
loopLoop iteration
lifecycle.prepare_runInitialize new workflow
lifecycle.prepare_resumeResume from checkpoint
lifecycle.finalizeFinalize workflow (complete/fail/cancel)

All spans include structured fields for filtering and correlation:

  • instance_id — Workflow instance identifier
  • task_id — Current task being executed
  • worker_id — Worker processing the task
  • definition_hash — Workflow definition version

Run Jaeger with Docker:

Terminal window
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/jaeger:latest

Then start your workflow with OTel enabled:

Terminal window
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_SERVICE_NAME=my-workflow \
RUST_LOG=info \
python my_workflow.py

Open http://localhost:16686 to view traces.

docker-compose.yml
services:
tempo:
image: grafana/tempo:latest
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
ports:
- "4317:4317" # OTLP gRPC
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin

For Rust users who want full control over the tracing subscriber:

use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::SpanExporter;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let exporter = SpanExporter::builder()
.with_tonic()
.with_endpoint("http://localhost:4317")
.build()?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
opentelemetry_sdk::Resource::builder()
.with_service_name("my-service")
.build(),
)
.build();
let tracer = provider.tracer("sayiir");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.with(otel_layer)
.init();
// ... run workflows ...
// Flush before exit
provider.shutdown()?;
Terminal window
RUST_LOG=off python my_workflow.py

Or silence only Sayiir while keeping your app’s logs:

Terminal window
RUST_LOG=info,sayiir_runtime=off,sayiir_postgres=off,sayiir_persistence=off,sayiir_core=off

Console logs only, minimal noise:

Terminal window
RUST_LOG=warn python my_workflow.py
Terminal window
OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 \
OTEL_SERVICE_NAME=order-worker \
RUST_LOG=info \
python worker.py

Temporarily increase verbosity for the runtime:

Terminal window
RUST_LOG=warn,sayiir_runtime=debug,sayiir_postgres=debug python my_workflow.py