---
title: "4. Look inside"
description: "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."
type: "docs-page"
url: "https://tatolab.com/docs/tutorial/look-inside"
site: "Tatolab"
section: "Tutorial"
---

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

## What's running: `tatolab streams`

```sh
tatolab streams
```

```txt title="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.

## Its shape and health: `tatolab graph`

`tatolab graph` prints the live graph as JSON: every node and link, their states, and their counters. It's long, so use [`jq`](https://jqlang.org) 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:

```sh
tatolab graph --stream motion_watcher | jq -r '.nodes[] | "\(.name)  \(.components.state)"'
```

```txt title="Output"
camera  Running
detector  Running
displaywindow  Running
```

And its links:

```sh
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=`.

## What a port carries: `tatolab tap`

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

```sh
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](https://msgpack.org). 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.

```python title="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:

```sh
tatolab tap motion_watcher/detector/motion --count 3 | uv run --with msgpack python ~/decode_tap.py
```

```txt title="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.

## What it logged: `tatolab logs`

```sh
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:

```sh
ls .streamlib/logs/
```

```txt title="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`:

```sh
jq -c 'select(.source == "python") | {level, message, attrs}' .streamlib/logs/*-motion_watcher-*.jsonl
```

```txt title="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](https://tatolab.com/docs/run/logs) covers the fields and how to ship them.

## What a frame looks like: `tatolab exchange`

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`:

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

## Next

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](https://tatolab.com/docs/tutorial/keep-running). Otherwise, continue to [Show motion on the picture](https://tatolab.com/docs/tutorial/gpu-tint).
