Tracing#

egglog can emit OpenTelemetry spans from both the high-level Python wrapper and the Rust bindings. The Python package stays library-style: it only depends on opentelemetry-api, and it starts emitting Python spans once your application configures an OpenTelemetry tracer provider.

The Rust side uses the current Python trace context when one exists, so Rust spans can appear under the same parent trace. To export Rust spans, call egglog.bindings.setup_tracing(...) before the traced Rust calls:

  • exporter="console" writes Rust spans to stdout.

  • exporter="http" sends Rust spans to an OTLP/HTTP endpoint.

For the contributor-oriented pytest workflow, see Contributing.

Trace A Host Application#

This example configures Python tracing in an application that happens to call into egglog. The Python spans come from the configured tracer provider, and the Rust spans join the same trace because the bindings propagate the current traceparent and tracestate into the Rust tracing layer.

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

from egglog import EGraph, bindings, i64

provider = TracerProvider(resource=Resource.create({"service.name": "demo-app"}))
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
bindings.setup_tracing(exporter="console")

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("optimize"):
    EGraph().extract(i64(0))

bindings.shutdown_tracing()
provider.shutdown()

In that setup:

  • Python spans use the module tracer names such as egglog.egraph and egglog.egraph_state.

  • Python span names are the public method names such as create, push, pop, register, run, and extract, plus run_schedule_to_egg while schedules are lowered.

  • Rust spans use names such as bindings.run_program, bindings.serialize, and bindings.extractor.extract_best.

If you call the low-level bindings directly, pass traceparent= and tracestate= yourself on the traced methods. The high-level Python API does that automatically.

Send Traces To Jaeger#

The official Jaeger getting-started docs use this container:

docker run --rm \
  --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5778:5778 \
  -p 9411:9411 \
  cr.jaegertracing.io/jaegertracing/jaeger:2.16.0

Point both Python and Rust tracing at Jaeger over OTLP/HTTP:

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from egglog import bindings

provider = TracerProvider(resource=Resource.create({"service.name": "demo-app"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://127.0.0.1:4318/v1/traces")))
trace.set_tracer_provider(provider)
bindings.setup_tracing(exporter="http", endpoint="http://127.0.0.1:4318/v1/traces")

After that, open http://localhost:16686 and search for traces from demo-app and egglog.

Local Test Runs#

If you want the same tracing setup during pytest, use the built-in test flags documented in Contributing.

When using --otel-traces=console under pytest, pass -s so the console exporter output is shown as the test runs. Console mode is best for short, targeted runs because it is intentionally verbose. For longer or hotter tests, prefer OTLP/Jaeger tracing.