Skip to content

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": { … }
}
]
}
  • sessionId identifies 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 (plus organization if the workspace name is ambiguous). It must already exist — create it with CreateRun. An unknown run is NOT_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 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.

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 example train/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 scales EPISODE and ENVIRONMENT_STEP are 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.

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 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.

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}
}]
}]
  • binEdges are finite and strictly increasing: at least 2 and at most 513 (512 bins).
  • Set exactly one of counts (a counting histogram) or densities (a density histogram, as with numpy.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.
  • stats are optional exact moments of the raw values. With counts they are raw sums; with densities they are normalized (sum is the mean, sumOfSquares the 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.

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]}]
}]
  • xs and ys (and zs for a 3D scatter) are parallel arrays of finite coordinates. Set is3d to 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.
  • metadataLabels are display metadata, not part of the series identity. The keys xt, yt and zt set axis titles. The latest non-empty set wins, so send it only when it changes.

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 in aggregateOverLabels, and — if metricName is set — its metric name matches. Omit metricName to apply the rule to every metric.
  • The output series is named outputMetricName (only with metricName; defaults to the metric name), or the source name plus outputNameSuffix, and keeps the labels not aggregated over.
  • fns needs at least one function (MEAN, MAX, MIN, SUM, STD_DEV, COUNT); aggregateOverLabels needs at least one label.
  • Only ORDERING_MODE_ABSOLUTE_ORDERED series are aggregated.

Read aggregated series with the Query API’s aggregate=(keys=[...]) selectors — see Series summary selectors.

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.

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.

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.