Skip to content

Steps, scales & labels

A series is identified by (metric_name, scale, labels). Two points with the same name but a different scale or different labels go to different series, each with its own step axis. This page covers the arguments that shape series identity and the step axis.

Each series has its own step axis. The step argument to log controls it:

step value meaning
None (default) auto-increment from the series’ last step
a single int the step of the first point; further points in the call continue from it
a sequence/array an explicit step per point (length must match value)
metrana.log("loss", 0.5) # auto: next step
metrana.log("loss", [a, b, c], step=100) # steps 100, 101, 102
metrana.log("loss", [a, b, c], step=[10, 20, 30]) # explicit steps

Auto-increment is the common case for a single-writer series. Use explicit steps when you log out of order, resume a run, or want a metric pinned to your global training step (see Retrieving the last step).

timestamp works exactly like step, in Unix milliseconds:

  • None (default) — the server stamps each point on arrival.
  • a single int — applied to every point in the call.
  • a sequence/array — one timestamp per point (length must match value).
import time
now_ms = int(time.time() * 1000)
metrana.log("loss", loss, timestamp=now_ms)

These three arguments shape series identity:

The step scale — a StandardMetricScale value: "ML_STEP", "EPISODE", or "ENVIRONMENT_STEP". None defaults to ML_STEP. Only log / log_distributed take scale; the RL helpers fix their own scale (see RL metrics).

import metrana
metrana.log("custom_metric", v, scale=metrana.StandardMetricScale.ML_STEP)
metrana.log("custom_metric", v, scale="ML_STEP") # strings work too

A dict[str, str] that, together with the name and scale, identifies the series. Two points with the same name but different labels form different series — useful for splitting a metric by a dimension:

metrana.log("loss", per_layer_loss, labels={"layer": "3"})
metrana.log("reward", r, labels={"policy": "greedy"})

A shorthand that adds the evaluation label — True adds {"evaluation": "true"}, False adds {"evaluation": "false"} (unless you already set the key in labels), and None (the default) adds nothing. Evaluation points form a series distinct from otherwise identical training points; because both sides of the split must share the metric’s label key set (see One shape per metric), mark the training side with evaluation=False:

metrana.log("reward", train_reward, evaluation=False) # training series
metrana.log("reward", eval_reward, evaluation=True) # distinct eval series
metrana.log("reward_by_policy", r, labels={"policy": "greedy"}) # labelled series, own metric

Labels split a metric into series, but the metric itself — the same (metric_name, scale) — keeps one shape, fixed by the first point ever logged and enforced on every later call:

fixed per metric meaning
label key set every series of the metric uses the same label keys; only the values differ
value kind floats, counting/density histograms, or 2D/3D scatter plots — never a mix
logging mode log (single-writer) and log_distributed (multi-writer) cannot be mixed
episodic mode (RL) with episodes or without them, not both — pinned by the first call’s episode

A divergent call raises MetranaValidationError and sends nothing; conforming series keep logging:

metrana.log("loss", a, labels={"layer": "1"}) # pins loss to the key set {layer}
metrana.log("loss", b, labels={"layer": "2"}) # ok: same key, different value
metrana.log("loss", c, labels={"rank": "0"}) # rejected: different label key
metrana.log("loss", d) # rejected: no labels on a labeled metric

The shape survives restarts: a resumed run re-learns it from the server at init(), so a divergent call from a new process is rejected the same way. To record differently-shaped data, use a distinct metric name or a / prefix — see Naming rules.

metrana.get_last_step(metric_name, scale=None, labels=None) returns the last step logged for a series, or None if nothing has been logged yet. Pass the same scale / labels you logged it with.

This value is seeded from the server at init(), so after a restart or resume you can continue from where the run left off:

last = metrana.get_last_step("loss")
next_step = 0 if last is None else last + 1
metrana.log("loss", loss, step=next_step)

This is the building block for resuming a run with explicit steps — see Resuming & forking. The RL equivalent is get_env_last_rl_step_and_episode.