Add Metrics to Your Batch
In this tutorial you'll add Metrics to the drone flight demo from Run Your First Test Batch. By the end you'll have replaced the legacy metrics build with direct data emission, and you'll be able to see speed, altitude, and flight state visualizations in the dashboard — plus a batch-level summary comparing all three flights.
Time: about 30 minutes
Before you start:
- You've completed Run Your First Test Batch and have the
drone-demoproject set up with thedrone-flightsystem and three experiences registered. - You have Docker installed and access to a container registry (AWS ECR, Docker Hub, or similar).
- You have Python 3.8+ installed.
What you're changing
The first tutorial used a pre-built metrics build image — a separate Docker container that ran after each test and computed metrics using the legacy protobuf-based SDK. In this tutorial you'll:
- Clone the demo repo and modify the experience build to emit flight data directly using the
Emitterclass. - Write a
config.resim.yamlthat defines the data schema and SQL metrics. - Create a new test suite that uses the Metrics framework instead of a metrics build image.
- Run a batch and see the results.
Step 1: Clone the repo
git clone https://github.com/resim-ai/getting-started-demo.git
cd getting-started-demo
The experience build lives in experience-build/. The key file is sim_run.py — it reads the flight log JSON for each experience and writes processed_flight_log.json to /tmp/resim/outputs/. You'll add emission calls alongside that existing logic.
Step 2: Install the Emitter library
pip install resim-open-core
The resim-open-core package provides the Emitter class used to write data to the ReSim data lake.
Step 3: Define your data schema
Create the directory and config file:
mkdir -p .resim/metrics
touch .resim/metrics/config.resim.yaml
The flight log has one sample per second with speed, position (x, y, z in meters), state (Idle, Takeoff, Hovering, Moving, Landing), and status (OK, WARNING, Error). Define two topics to capture this:
version: 1
topics:
flight_data:
schema:
speed: float
x: float
y: float
altitude: float
flight_state:
schema:
state: string
status: string
flight_data captures the numeric values you'll plot over time. flight_state captures the categorical state and status strings you'll use for the state timeline and status checks.
Step 4: Add emission to the experience build
Open experience-build/sim_run.py. The file reads the flight log and writes the processed output. You'll add emission calls that run alongside that logic.
Add the import near the top of the file:
from datetime import datetime
from resim.metrics.python.emissions import Emitter
Then, after the flight log is loaded into flight_data (the dict parsed from flight_log.json), add the emission block:
with Emitter(config_path=".resim/metrics/config.resim.yaml") as emitter:
first_ts = None
for sample in flight_data["samples"]:
ts = datetime.fromisoformat(sample["timestamp"])
if first_ts is None:
first_ts = ts
# Elapsed time in nanoseconds from the start of the flight
elapsed_ns = int((ts - first_ts).total_seconds() * 1e9)
emitter.emit("flight_data", {
"speed": sample["speed"],
"x": sample["position"]["x"],
"y": sample["position"]["y"],
"altitude": sample["position"]["z"],
}, timestamp=elapsed_ns)
emitter.emit("flight_state", {
"state": sample["state"],
"status": sample["status"],
}, timestamp=elapsed_ns)
The emitter writes to /tmp/resim/outputs/emissions.resim.jsonl by default — ReSim picks this file up automatically at the end of each test.
Step 5: Write your metrics
Add a metrics and metrics sets section to config.resim.yaml. Start with four test-level metrics and two batch-level metrics.
Speed over time
A line chart of drone speed for each test. Timestamps are in nanoseconds, so divide by 1E9 to show seconds on the x-axis.
metrics:
Speed Over Time:
type: test
description: Drone speed over the course of the flight.
query_string: |
SELECT
'Speed',
timestamp / 1E9 AS "Time (s)",
speed AS "Speed (m/s)"
FROM flight_data;
template_type: system
template: line
Maximum speed — with a status check
A scalar metric that also flags flights that exceed speed thresholds. The warning_drone_flight experience reaches ~89 m/s and the fast_drone_flight reaches ~45 m/s, so thresholds of 50 m/s (block) and 20 m/s (warn) will produce the expected pass/warn/block split across the three experiences.
Maximum Speed:
type: test
description: Highest speed recorded during the flight.
query_string: |
SELECT MAX(speed) FROM flight_data;
template_type: system
template: scalar
units: "m/s"
status:
query_string: SELECT '1' FROM flight_data HAVING MAX(speed) > ?
block: 50.0
warn: 20.0
Altitude over time
Altitude Over Time:
type: test
description: Drone altitude over the course of the flight.
query_string: |
SELECT
'Altitude',
timestamp / 1E9 AS "Time (s)",
altitude AS "Altitude (m)"
FROM flight_data;
template_type: system
template: line
Flight state timeline
The state timeline template expects three columns: [system_name, timestamp, state_name]. Consecutive rows with the same state are merged into segments, making it easy to see when the drone was hovering vs. moving vs. landing.
Flight States:
type: test
description: Drone state over the course of the flight.
query_string: |
SELECT
'Drone' AS "System",
timestamp,
state
FROM flight_state;
template_type: system
template: state_timeline
Batch metrics
Batch metrics aggregate across all jobs in a batch. Add these to compare the three flights at the batch level.
Average Max Speed:
type: batch
description: Average of the highest speed recorded across all flights in the batch.
query_string: |
SELECT AVG(max_speed_ms)
FROM (
SELECT job_id, MAX(speed) AS max_speed_ms
FROM flight_data
GROUP BY job_id
);
template_type: system
template: scalar
units: "m/s"
Peak Altitude by Experience:
type: batch
description: Highest altitude reached in each experience, side by side.
query_string: |
SELECT
'Peak Altitude',
m.experience_name AS "Experience",
MAX(f.altitude) AS "Peak Altitude (m)"
FROM flight_data f
JOIN metadata m ON f.job_id = m.job_id
GROUP BY m.experience_name
ORDER BY m.experience_name;
template_type: system
template: bar
Wire up a metrics set
A metrics set groups metrics together and is what you attach to a test suite. Add this at the end of the config:
metrics sets:
Drone Flight Metrics:
metrics:
- Speed Over Time
- Maximum Speed
- Altitude Over Time
- Flight States
- Average Max Speed
- Peak Altitude by Experience
Complete config
Your finished config.resim.yaml should look like this:
version: 1
topics:
flight_data:
schema:
speed: float
x: float
y: float
altitude: float
flight_state:
schema:
state: string
status: string
metrics:
Speed Over Time:
type: test
description: Drone speed over the course of the flight.
query_string: |
SELECT
'Speed',
timestamp / 1E9 AS "Time (s)",
speed AS "Speed (m/s)"
FROM flight_data;
template_type: system
template: line
Maximum Speed:
type: test
description: Highest speed recorded during the flight.
query_string: |
SELECT MAX(speed) FROM flight_data;
template_type: system
template: scalar
units: "m/s"
status:
query_string: SELECT '1' FROM flight_data HAVING MAX(speed) > ?
block: 50.0
warn: 20.0
Altitude Over Time:
type: test
description: Drone altitude over the course of the flight.
query_string: |
SELECT
'Altitude',
timestamp / 1E9 AS "Time (s)",
altitude AS "Altitude (m)"
FROM flight_data;
template_type: system
template: line
Flight States:
type: test
description: Drone state over the course of the flight.
query_string: |
SELECT
'Drone' AS "System",
timestamp,
state
FROM flight_state;
template_type: system
template: state_timeline
Average Max Speed:
type: batch
description: Average of the highest speed recorded across all flights in the batch.
query_string: |
SELECT AVG(max_speed_ms)
FROM (
SELECT job_id, MAX(speed) AS max_speed_ms
FROM flight_data
GROUP BY job_id
);
template_type: system
template: scalar
units: "m/s"
Peak Altitude by Experience:
type: batch
description: Highest altitude reached in each experience, side by side.
query_string: |
SELECT
'Peak Altitude',
m.experience_name AS "Experience",
MAX(f.altitude) AS "Peak Altitude (m)"
FROM flight_data f
JOIN metadata m ON f.job_id = m.job_id
GROUP BY m.experience_name
ORDER BY m.experience_name;
template_type: system
template: bar
metrics sets:
Drone Flight Metrics:
metrics:
- Speed Over Time
- Maximum Speed
- Altitude Over Time
- Flight States
- Average Max Speed
- Peak Altitude by Experience
Step 6: Add the dependency to the Dockerfile
Open experience-build/Dockerfile and add resim-open-core to the pip install step. It will look something like:
RUN pip install resim-open-core
Also ensure the config file gets copied into the image:
COPY .resim/metrics/config.resim.yaml .resim/metrics/config.resim.yaml
Step 7: Build and push your image
Build the modified experience image and push it to your container registry:
docker build -f experience-build/Dockerfile -t <YOUR_REGISTRY>/drone-demo:metrics-v1 .
docker push <YOUR_REGISTRY>/drone-demo:metrics-v1
Step 8: Register the new build
Register your updated image with ReSim. Make sure your project is still selected:
resim projects select "drone-demo"
resim builds create \
--name "Demo Build — Metrics" \
--description "Drone demo build with metrics instrumentation" \
--version "metrics-v1" \
--image "<YOUR_REGISTRY>/drone-demo:metrics-v1" \
--branch "main" \
--system "drone-flight" \
--auto-create-branch
Note the build UUID from the output.
Step 9: Sync your metrics config
Push your config.resim.yaml to the ReSim platform so it knows about your topics and metrics:
resim metrics sync \
--project "drone-demo" \
--branch "main"
Step 10: Create a test suite with your metrics set
Create a new test suite that uses your metrics set. This suite doesn't need a --metrics-build-id — the Metrics framework handles results directly from the emitted data.
resim suites create \
--name "Drone Metrics Suite" \
--description "Test suite for drone flight metrics" \
--system "drone-flight" \
--metrics-set "Drone Flight Metrics" \
--experiences "Maiden Flight Voyage" "Drone Flight Fast" "Drone Flight with Warning"
Step 11: Run the suite
resim suites run \
--test-suite "Drone Metrics Suite" \
--build-id "<UUID from Step 8>" \
--sync-metrics-config
The --sync-metrics-config flag automatically syncs your config before triggering the batch, so you don't have to run resim metrics sync separately on future runs.
Step 12: View results in the dashboard
Once the batch completes, open app.resim.ai, navigate to drone-demo, and click into the batch.
For each test you'll see:
- Speed Over Time — a line chart of drone speed in m/s
- Maximum Speed — a scalar with a pass/warn/block status indicator
- Altitude Over Time — a line chart of altitude in meters
- Flight States — a state timeline showing Idle → Takeoff → Hovering → Moving → Landing segments
At the batch level:
- Average Max Speed — a single number aggregated across all three flights
- Peak Altitude by Experience — a bar chart comparing the three experiences side by side
The status checks will produce the expected split:
| Experience | Max speed | Status |
|---|---|---|
| Maiden Flight Voyage | ~2 m/s | PASSED |
| Drone Flight Fast | ~45 m/s | WARNING |
| Drone Flight with Warning | ~89 m/s | BLOCKER |
What's next
You've now completed the full Metrics workflow: define a schema, emit data from your system, write SQL metrics, and view the results in the dashboard.
From here you can:
- Add more metric templates — tables, images, histograms via custom templates
- Iterate with the debug dashboard — develop metrics locally without running a full batch
- Bring your own system — apply the same pattern to your real robot or simulator
- Set up CI — trigger batches automatically on pull requests