Logging metrics and attributes
UpdateRuns (gRPC, or POST /api/ingest/v1/runs:update) writes to
existing runs. One request can carry updates for several runs, and each run’s update can mix attributes, series of
every kind, reinforcement-learning data and a close signal.
{ "sessionId": "8237561", "runUpdates": [ { "workspace": "my-workspace", "project": "vision", "runName": "resnet50-lr3e-4", "attributes": [ … ], "series": [ … ], "histogramSeries": [ … ], "scatterSeries": [ … ], "rlSeries": [ … ], "envAttrUpdates": [ … ], "closeRun": { … } } ]}sessionIdidentifies the writing process. It must be non-zero and should be fresh each time a process starts: Metrana uses it to recognize retried data from the same process. Processes writing to different series of the same run may share a value. A random 64-bit number chosen at startup works well.- The run is addressed by
workspace/project/runName(plusorganizationif the workspace name is ambiguous). It must already exist — create it withCreateRun. An unknown run isNOT_FOUND(404). - The whole request is validated before anything is applied: one invalid field rejects the request with
INVALID_ARGUMENT(400) and a message naming the problem.
The seriesDeclarations, streamingSeries and streamingRlSeries fields belong to the
streaming path and are rejected here.
Attributes
Section titled “Attributes”Attributes are typed key-value metadata — hyperparameters, configuration, notes. Each
AttributeUpdate sets one path to one value, replacing whatever
was there:
"attributes": [ {"path": "config/learning_rate", "floatValue": 0.0003}, {"path": "config/batch_size", "intValue": "256"}, {"path": "config/optimizer", "stringValue": "adamw"}, {"path": "config/mixed_precision", "boolValue": true}, {"path": "data/snapshot_time", "timestampValue": "2026-09-29T18:00:00Z"}, {"path": "data/splits", "stringSetValue": {"values": ["train", "val"]}}]| Value field | Type | Notes |
|---|---|---|
boolValue |
bool | |
intValue |
int64 | A string in JSON. |
floatValue |
double | |
stringValue |
string | Up to 16 KiB. |
timestampValue |
timestamp | RFC 3339 in JSON. |
stringSetValue |
set of strings | Replaces the whole set. Up to 32 KiB across values. |
stringSetDeltaValue |
set change | addedValues / removedValues applied to the current set; on a missing set, the additions become the set. |
Use / in paths to build a hierarchy: the portal and the Query API’s browse calls present it as folders.
Path characters follow the name rules, except that / is allowed.
System attributes. Paths under sys/ are maintained by Metrana (run name, status, creation time and so on) and
are read-only, with two exceptions you may write:
| Path | Type | Shown as |
|---|---|---|
sys/description |
stringValue |
The run’s description. |
sys/tags |
stringSetValue or stringSetDeltaValue |
The run’s tags. |
Series
Section titled “Series”A series is a sequence of numeric points on an x-axis. It is identified by three things; change any of them and you are writing a different series:
metricName— for exampletrain/loss. Up to 256 bytes;/builds a hierarchy.scale— what the x-axis counts. Leave it empty for the default,ML_STEP(training steps). The reinforcement-learning scalesEPISODEandENVIRONMENT_STEPare covered in Reinforcement learning data. Up to 64 bytes.labels— optional key/value pairs distinguishing variants of one metric, such as{"gpu": "0"}. At most 8 per series; keys and values up to 64 bytes each. Keys follow the name rules and may not contain&; values may contain anything except control characters,=and&.
A series also has a value kind — float, histogram, density histogram, 2D or 3D scatter — fixed by the first write. Writes of a different kind to the same identity are dropped, so give each kind its own metric name.
Float series
Section titled “Float series”A SeriesUpdate carries one or more
PointBatches:
"series": [{ "metricName": "train/loss", "labels": {"gpu": "0"}, "ordering": "ORDERING_MODE_ABSOLUTE_ORDERED", "pointBatches": [{ "firstStep": "1000", "values": [0.412, 0.409, 0.407], "timestampMillis": "1759233600000" }]}]A batch is columnar. Pick one form for each of its three columns:
| Column | Compact form | Explicit form |
|---|---|---|
| Values | values (double) |
valuesF32 (float — half the size when you do not need double precision) |
| Steps | firstStep: consecutive steps starting there, one per value |
steps: one step per value |
| Timestamps | timestampMillis: one timestamp for every point |
timestampsMillis: one per value |
Explicit steps take precedence over firstStep, and explicit timestampsMillis over timestampMillis. Parallel
arrays must have the same length. Steps are absolute, from 0 to 2^63 - 1. A batch holds at most 10,000 values;
send several batches in one update for more, or for runs of steps that are not contiguous. Timestamps are
milliseconds since the Unix epoch. They are optional, but the portal’s time-based x-axes and the Query API’s
timestamp bucketing use them, so send them.
The older points and floatPoints forms still work but are deprecated; an update may use only one of the three.
Ordering
Section titled “Ordering”ordering is required on every float series update:
| Mode | Meaning |
|---|---|
ORDERING_MODE_ABSOLUTE_ORDERED |
Each step is greater than the last one accepted for the series. A point at or below the last accepted step is dropped as a duplicate. Derived aggregations are computed. Use this unless you have a reason not to. |
ORDERING_MODE_ABSOLUTE_UNORDERED |
Points may arrive in any order, for example from asynchronous evaluators. Derived aggregations are skipped. |
ORDERING_MODE_RELATIVE_ORDERED is no longer supported and is rejected, as is leaving the field unset. Keep one
mode for the lifetime of a series.
Histograms
Section titled “Histograms”A HistogramSeriesUpdate logs a whole distribution per
step, in the shape numpy.histogram returns:
"histogramSeries": [{ "metricName": "weights/layer4", "points": [{ "step": "1000", "timestampMillis": "1759233600000", "binEdges": [-0.2, -0.1, 0.0, 0.1, 0.2], "counts": ["12", "340", "355", "9"], "stats": {"min": -0.193, "max": 0.187, "sum": 1.73, "sumOfSquares": 4.02} }]}]binEdgesare finite and strictly increasing: at least 2 and at most 513 (512 bins).- Set exactly one of
counts(a counting histogram) ordensities(a density histogram, as withnumpy.histogram(..., density=True)), with one entry per bin. The first point fixes which kind the series is, and every point in an update must match it. statsare optional exact moments of the raw values. Withcountsthey are raw sums; withdensitiesthey are normalized (sumis the mean,sumOfSquaresthe mean of squares). Without them Metrana approximates the moments from the bins.- Histograms have no ordering mode: points may arrive at any step in any order, and logging a step again replaces its histogram.
- At most 128 points per update.
Scatter plots
Section titled “Scatter plots”A ScatterSeriesUpdate logs a full point cloud per step:
"scatterSeries": [{ "metricName": "eval/embedding", "is3d": false, "metadataLabels": {"xt": "PC 1", "yt": "PC 2"}, "points": [{"step": "1000", "xs": [0.1, 0.4, -0.3], "ys": [1.2, 0.8, 0.5]}]}]xsandys(andzsfor a 3D scatter) are parallel arrays of finite coordinates. Setis3dto match; a series stays 2D or 3D for its lifetime.- A step holds up to 32,768 coordinates, and so does a whole update. An update holds at most 128 steps.
- An empty point set is valid and means “no points at this step”.
- Logging a step again replaces its point cloud.
metadataLabelsare display metadata, not part of the series identity. The keysxt,ytandztset axis titles. The latest non-empty set wins, so send it only when it changes.
Aggregation rules
Section titled “Aggregation rules”An aggregation rule makes Metrana maintain a derived series while it ingests, so a cross-series reduction is ready
to read instead of computed at query time. Rules are passed as aggregationRules when the run is created (with
CreateRun or InitializeRunLogging) and apply to that run’s float series.
"aggregationRules": [{ "metricName": "train/loss", "sourceScale": "ML_STEP", "outputScale": "ML_STEP", "fns": ["AGGREGATION_FN_MEAN", "AGGREGATION_FN_MAX"], "aggregateOverLabels": ["gpu"]}]This rule collapses every train/loss series that differs only in its gpu label into one aggregated series per
remaining label combination: train/loss{gpu=0, host=a} and train/loss{gpu=1, host=a} feed
train/loss{host=a}, with its mean and maximum at each step.
- A series is fed into a rule when its scale equals
sourceScale, it carries every label inaggregateOverLabels, and — ifmetricNameis set — its metric name matches. OmitmetricNameto apply the rule to every metric. - The output series is named
outputMetricName(only withmetricName; defaults to the metric name), or the source name plusoutputNameSuffix, and keeps the labels not aggregated over. fnsneeds at least one function (MEAN,MAX,MIN,SUM,STD_DEV,COUNT);aggregateOverLabelsneeds at least one label.- Only
ORDERING_MODE_ABSOLUTE_ORDEREDseries are aggregated.
Read aggregated series with the Query API’s aggregate=(keys=[...]) selectors — see
Series summary selectors.
Closing a run
Section titled “Closing a run”Add closeRun to a writer’s last update:
"closeRun": {"closeMessage": "finished"}It is processed after the rest of the update. Once every writer of the run has closed, the run’s status becomes closed; a run whose writers stop sending without closing is eventually marked crashed.
Retries and duplicates
Section titled “Retries and duplicates”Writes are at-least-once: when a request’s outcome is unknown, send it again. Duplicates are harmless:
- Ordered float series drop any point at or below the last accepted step.
- Unordered float series, histograms and scatter plots replace the value at a step with the latest write.
- Attributes replace their value; a replayed string-set delta has the same effect as the original.
Retry UNAVAILABLE (503) and INTERNAL (500) with exponential backoff. RESOURCE_EXHAUSTED (429) means the
request did not fit under your workspace’s or organization’s write rate: back off for longer before retrying, and
send less per second. Do not retry INVALID_ARGUMENT,
NOT_FOUND or PERMISSION_DENIED unchanged. See Errors and limits.
Batching
Section titled “Batching”Fewer, larger requests are more efficient than many small ones. Group points into batches, and batches for many series into one request, but keep each request under the transport’s size limit (2 MB for a REST body, 4 MiB for a gRPC message). For sustained high volumes, use the streaming path, which declares each series once instead of repeating its name and labels in every update.