Skip to content

4. Look inside

Use tatolab streams, graph, tap, logs and exchange to see what the two streams are doing while they run, and find each stream's JSON Lines log.

When a stream misbehaves, you want to see what it’s actually doing, not what the code says it should do. Tatolab gives you five commands for that, and each stream keeps a log you can read with any tool. Leave both tatolab dev commands running, and open another terminal in the motion-watcher folder.

Terminal window
tatolab streams
Output
NAME STATE NODES PROJECT
motion_watcher attached 3 /home/you/motion-watcher
motion_log attached 1 /home/you/motion-log

attached means a stream lives as long as the command that started it, here your two tatolab dev commands. In step 6 you’ll make them stay.

tatolab graph prints the live graph as JSON: every node and link, their states, and their counters. It’s long, so use jq to pick out what you need (install it with your package manager, or brew install jq). List the motion watcher’s nodes and their states:

Terminal window
tatolab graph --stream motion_watcher | jq -r '.nodes[] | "\(.name) \(.components.state)"'
Output
camera Running
detector Running
displaywindow Running

And its links:

Terminal window
tatolab graph --stream motion_watcher | jq -r '.links[] | "\(.source.node)/\(.source.port) -> \(.target.node)/\(.target.port) \(.state)"'

A healthy link reads wired. A link that reads error carries an error_reason saying why, and one that reads awaiting_remote is waiting for another stream, with an awaiting_remote_reason. If a reader drops bags because it fell behind, the count shows up under that node’s components.metrics, so a loss is never silent.

The window kept its default name, displaywindow, because it has no name=.

tatolab tap samples the bags an output sends, addressed as <stream>/<node>/<port>. Sample the motion reports motion_log reads:

Terminal window
tatolab tap motion_watcher/detector/motion --count 3

The answer is JSON describing the sample: how many bags were asked for and received, how many were dropped, and each bag’s length and bytes in hex. A tap never slows the stream down; on a quiet port, it returns what arrived within a short window rather than waiting. It can sample any output that a link reads. An output nothing reads has nothing flowing, so tapping it fails, saying so.

The hex is the bag exactly as it travelled: a 76-byte header (a 64-byte port key, the timestamp as 8 bytes, the length as 4), then the bag as msgpack. To read it, save this as decode_tap.py in your home folder, outside the projects: tatolab dev reloads a stream when any .py file in its project changes.

decode_tap.py
"""Print each bag of a `tatolab tap` sample: python decode_tap.py < sample.json"""
import json
import sys
import msgpack
for bag in json.load(sys.stdin)["bags"]:
raw = bytes.fromhex(bag["hex_preview"])
length = int.from_bytes(raw[72:76], "little")
print(msgpack.unpackb(raw[76 : 76 + length]))

and pipe a sample into it:

Terminal window
tatolab tap motion_watcher/detector/motion --count 3 | uv run --with msgpack python ~/decode_tap.py
Output
{'changed': 0.0016, 'moving': False, 'timestamp_ns': 893417260215}
{'changed': 0.0021, 'moving': False, 'timestamp_ns': 893517248113}
{'changed': 0.1391, 'moving': True, 'timestamp_ns': 893617265840}

That’s your detector’s output, exactly as motion_log receives it. Your numbers will differ; the timestamps are nanoseconds on the machine’s monotonic clock.

Terminal window
tatolab logs --stream motion_watcher -f

This follows the stream’s log as it’s written: the runtime’s records about the stream, the built-in nodes’, and your nodes’ log.info(...) calls, with anything a node prints. Filter it with --level warn for problems only, or --source python for your nodes alone. Press Ctrl-C to stop following.

Underneath, each stream writes its log to a file in its own project, one JSON object per line:

Terminal window
ls .streamlib/logs/
Output
Rk3x9q2m7v1c8b4n6p0w5z2ty-motion_watcher-1791727378117.jsonl

The name is the runtime’s ID, the stream’s name, and when the stream loaded, in milliseconds. Each line is one record, with fields such as level, source (python for your nodes, rust for the runtime and built-ins), message and attrs, which holds the fields you passed to log.info. Pick out your detector’s records with jq:

Terminal window
jq -c 'select(.source == "python") | {level, message, attrs}' .streamlib/logs/*-motion_watcher-*.jsonl
Output
{"level":"info","message":"motion started","attrs":{"changed":0.1391}}
{"level":"info","message":"motion stopped","attrs":{"changed":0.0018}}

Because it’s plain JSON Lines, any tool can read it: jq, a log shipper, or an OpenTelemetry Collector. Logs covers the fields and how to ship them.

A video bag doesn’t carry pixels; it names a frame held in GPU memory. tatolab exchange fetches the frame itself and saves it as a PNG. Its --channel option takes the same address as tap:

Terminal window
tatolab exchange --channel motion_watcher/camera/video --count 1 --out frames/

It prints the path of each file it writes. Open it: it’s the exact, full-resolution frame the camera produced. That works without any window, which makes it the way to see the picture on a machine with no screen.

The next step is optional: it puts the motion on the picture with a GPU effect. If your streams don’t need to process video, go straight to Keep it running. Otherwise, continue to Show motion on the picture.