rototo
DocsLearn
Learn

Using Rototo

This page assumes you've been through the quickstart and understand Rototo's concepts.

To run Rototo as your runtime configuration control plane, there are really four decisions to make. Everything else builds on top of them:

We'll walk through them one at a time.

Where the package lives

We've already said Rototo packages live in git. You can keep the package right alongside your app's code, or give it its own dedicated repo - both work fine, so pick whichever fits how your team already works.

Once you've picked the repo, it's worth running rototo setup. It wires up:

Making sure configuration does what you mean

A little effort here pays off enormously in misconfigurations you never ship. Concretely:

Getting the package out to your fleet

Rototo can load packages from a bunch of sources and protocols, but two of them are the ones you'll actually reach for:

To distribute through a CDN or object store, the easiest path is:

Keep both the object-store cache lifetime and your app's refresh period short (~5 seconds), and configuration changes will propagate to the fleet quickly.

Watching it in production

Two things are worth watching in production:

Tracking package refresh matters so you can be sure your config changes actually rolled out. The easiest way is to have your app subscribe to refresh events from the SDK and log them through your normal telemetry stack.

let mut events = package.subscribe_refresh_events();
tokio::spawn(async move {
    while let Ok(event) = events.recv().await {
        tracing::info!("rototo refresh: {event:?}");
    }
});
async for event in package.refresh_events():
    logging.info("rototo refresh: %s", event)
for await (const event of pkg.refreshEvents()) {
  console.log("rototo refresh:", event);
}
pkg.addRefreshListener(event -> {
    System.out.println("rototo refresh: " + event);
});
events, err := pkg.RefreshEvents(ctx)
if err != nil {
    return err
}
go func() {
    for event := range events {
        log.Printf("rototo refresh: %+v", event)
    }
}()

For resolution traces, the question is which resolutions to capture - and there are two ways to decide.

The powerful one is to let the package decide. You add a [[trace]] policy to rototo-package.toml, so you can turn tracing on for exactly the case you're chasing through a reviewed change - no app deploy:

[[trace]]
when = 'env.resolving.variable == "checkout-redesign" && context.user.id == "tester-123"'

The other way is for the app to ask for a trace on a specific call, when the app itself knows the request is interesting (a ?debug=1 flag, a support session, a sampled request):

use rototo::ResolveOptions;

let options = ResolveOptions { trace: true, ..ResolveOptions::default() }; let resolution = package.resolve_variable_with_options("checkout-redesign", &context, options)?;

resolution = package.resolve_variable("checkout-redesign", context, trace=True)
const resolution = pkg.resolveVariable("checkout-redesign", context, { trace: true });
VariableResolution resolution = pkg.resolveVariable(
    "checkout-redesign", context, ResolveOptions.trace(true));
resolution, err := pkg.ResolveVariable("checkout-redesign", context, &rototo.ResolveOptions{Trace: true})

Either way, the traces come out in one place: the trace stream. Your app subscribes and forwards them to its logs or debugger, off the resolve path:

let mut traces = package.subscribe_trace_events();
tokio::spawn(async move {
    while let Some(item) = traces.recv().await {
        match item {
            rototo::TraceStreamItem::Trace(trace) => tracing::info!("trace: {trace:?}"),
            rototo::TraceStreamItem::Dropped { count } => {
                tracing::warn!(count, "rototo traces dropped")
            }
        }
    }
});
async for item in package.trace_events():
    if item["kind"] == "trace":
        logging.info("trace: %s", item["trace"])
    else:  # {"kind": "dropped", "count": n}
        logging.warning("rototo traces dropped: %s", item["count"])
for await (const item of pkg.traceEvents()) {
  if (item.kind === "trace") {
    console.log("trace:", item.trace);
  } else {
    console.warn("rototo traces dropped:", item.count);
  }
}
pkg.addTraceListener(trace -> {
    System.out.println("trace: " + trace);
});
traces, err := pkg.TraceEvents(ctx)
if err != nil {
    return err
}
go func() {
    for item := range traces {
        log.Printf("trace: %+v", item)
    }
}()

Wrapping up

As we said in the motivation, Rototo is about bringing the engineering rigor of code to runtime configuration without dragging along code's operational constraints. What we've walked through here is the whole lifecycle of a configuration package - where it can be reviewed, versioned, tested, released, and observed just like code, while still shipping on its own separate path.

For the exact details, the reference docs are the place to go (and they're a good thing to point your agent at, too).