Skip to main content

Observability framework for robotics research

Project description

😎 Goggles - Observability for Robotics Research

Python 3.10+ GitHub stars codecov Tests Code Style PyPI version License: MIT uv

A lightweight, flexible Python observability framework designed for robotics research. Goggles provides structured logging, experiment tracking, performance profiling, and device-resident temporal memory management for JAX-based pipelines.

✨ Features

  • 🤖 Multi-process (and multi-machines) logging - Synchronize logs across spawned processes reliably and efficiently (shared memory when available).
  • 🎯 Multi-output support - Log to console, files, and remote services simultaneously.
  • 📊 Experiment tracking - Native integration with Weights & Biases for metrics, images, and videos.
  • 🕒 Performance profiling - @goggles.timeit decorator for automatic runtime measurement.
  • 🐞 Error tracing - @goggles.trace_on_error auto-logs full stack traces on exceptions.
  • 🧠 Device-resident histories - JAX-based GPU memory management for efficient, long-running experiments metrics.
  • 🚦 Graceful shutdown - Automatic cleanup of resources and handlers.
  • ⚙️ Structured configuration - YAML-based config loading with validation.
  • 🔌 Extensible handlers - Plugin architecture for custom logging backends.

🏗️ Projects Built with Goggles

This framework has been battle-tested across multiple research projects:

FluidSControl FlowGym SynthPix Πnet Glitch

🚀 Quick Start

Installation

# Basic installation
uv add robo-goggles # or pip install robo-goggles

# With Weights & Biases support
uv add "robo-goggles[wandb]"

# With JAX device-resident histories
uv add "robo-goggles[jax]"

For the development installation, see our How to contribute page.

Basic usage

import goggles as gg
import logging

# Set up console logging
logger = gg.get_logger("my_experiment")
gg.attach(
    gg.ConsoleHandler(name="console", level=logging.INFO),
)

# Basic logging
logger.info("Experiment started")
logger.warning("This is a warning")
logger.error("An error occurred")

See also Example 1, which you can run after cloning the repo with

uv run examples/01_basic_run.py

Experiment tracking with W&B

import goggles as gg
import numpy as np

# Enable metrics logging
logger = gg.get_logger("experiment", with_metrics=True)
gg.attach(
    gg.WandBHandler(project="my_project", name="run_1"),
)

# Log metrics, images, and videos
for step in range(100):
    logger.scalar("loss", np.random.random(), step=step)
    logger.scalar("accuracy", 0.8 + 0.2 * np.random.random(), step=step)

# Log images and videos
image = np.random.randint(0, 255, (64, 64, 3), dtype=np.uint8)
logger.image(image, name="sample_image")

video = np.random.randint(0, 255, (30, 3, 64, 64), dtype=np.uint8)
logger.video(video, name="sample_video", fps=10)

# Ensure proper cleanup
gg.finish()

Performance profiling and error tracking

import goggles as gg
import logging

class Trainer:
    @gg.timeit(severity=logging.INFO)
    def train_step(self, batch):
        # Your training logic here
        return {"loss": 0.1}

    @gg.trace_on_error()
    def risky_operation(self, data):
        # This will log full traceback on any exception
        return data / 0  # Will trigger trace logging

trainer = Trainer()
trainer.train_step({"x": [1, 2, 3]})  # Logs execution time

try:
    trainer.risky_operation(10)
except ZeroDivisionError:
    pass  # Full traceback was automatically logged

Configuration Management

Load and validate YAML configurations:

import goggles

# Load configuration with automatic validation
config = goggles.load_configuration("config.yaml")
print(config) # Pretty print
print(config["learning_rate"])  # Access as dict

# Pretty-print configuration
goggles.save_configuration(config, "output.yaml")

Supported Platforms 💻

Platform Basic W&B JAX/GPU Development
Linux
macOS
Windows

GPU support requires CUDA-compatible hardware and drivers

🔥 Examples

Explore the examples/ directory for comprehensive usage patterns:

# Basic logging setup
uv run examples/01_basic_run.py

# Advanced: Multi-scope logging
uv run examples/02_multi_scope.py

# File-based logging (local storage)
uv run examples/03_local_storage.py

# Weights & Biases integration
uv run examples/04_wandb.py

# Advanced: Weights & Biases multi-run setup
uv run examples/05_wandb_multiple_runs.py

# Advanced: Custom handler
uv run exacmples/06_custom_handler.py

# Graceful shutdown utils
uv run examples/100_interrupt.py

# Pretty and convenient utils for configuration laoding
uv run examples/101_config.py

# Advanced: Performance decorators
uv run examples/102_decorators.py

# Advanced: JAX device-resident histories
uv run examples/103_history.py

🧠 For Goggles power user

This section includes some cool functionalities of goggles. Enjoy!

Multi-scope logging

Goggles allow easily to set up different handlers for different scopes. That is, one can have an handler attached to multiple scopes, and a scope having multiple handlers. Each logger is associated to a single scope (by default: global), and logging with that logger will invoke all the loggers associated with the scope.

Why?

Within the same run, we may have logs that belong to different scopes. An example is training in Reinforcement Learning, where in a single training run there are multiple episodes. A complete example for this is provided in the multiple runs in WandB section.

Usage

# In this example, we set up a handlers associated
# to different scopes.
handler1 = gg.ConsoleHandler(name="examples.basic.console.1", level=logging.INFO)
gg.attach(handler1, scopes=["global", "scope1"])

handler2 = gg.ConsoleHandler(name="examples.basic.console.2", level=logging.INFO)
gg.attach(handler2, scopes=["global", "scope2"])

# We need to get separate loggers for each scope
logger_scope1 = gg.get_logger("examples.basic.scope1", scope="scope1")
logger_scope2 = gg.get_logger("examples.basic.scope2")
logger_scope2.bind(scope="scope2")  # You can also bind the scope after creation
logger_global = gg.get_logger("examples.basic.global", scope="global")

# Now we can log messages to different scopes, so that only the interested
# handlers will process them.
logger_scope1.info(f"This will be logged only by {handler1.name}")
logger_scope2.info(f"This will be logged only by {handler2.name}")
logger_global.info("This will be logged by both handlers.")

See also examples/02_multi_scope.py for a running example.

Multiple runs in WandB

An example of the benefit of scopes is given by the WandBHandler, which instantiate a different WandB run for each scope and groups them together:

import goggles as gg
from goggles import WandBHandler

# In this example, we set up multiple runs in Weights & Biases (W&B).
# All runs created by the handler will be grouped under
# the same project and group.
logger: gg.GogglesLogger = gg.get_logger("examples.basic", with_metrics=True)
handler = WandBHandler(
    project="goggles_example", reinit="create_new", group="multiple_runs"
)

# In particular, we set up multiple runs in an RL training loop, with each
# episode being a separate W&B run and a global run tracking all episodes.
num_episodes = 3
episode_length = 10
scopes = [f"episode_{episode}" for episode in range(num_episodes + 1)]
scopes.append("global")
gg.attach(handler, scopes=scopes)


def my_episode(index: int):
    episode_logger = gg.get_logger(scope=f"episode_{index}", with_metrics=True)
    for step in range(episode_length):
        # Supports scopes transparently
        # and has its own step counter
        episode_logger.scalar("env/reward", index * episode_length + step, step=step)


for i in range(num_episodes):
    my_episode(i)
    logger.scalar("total_reward", i, step=i)

# When using asynchronous logging (like wandb), make sure to finish
gg.finish()

Fully asynchronous logging

As in the WandB example, all the handlers work in the background. By default, the logging calls are blocking, but can be made not blocking by setting the environment variable GOGGLES_ASYNC to 1 or true. When you use the async mode, remember to call gg.finish() at the end from your host machine!

[!WARNING] This functionality still needs thorough tesing, as well as a better documentation. Help is appreciated! 🤗

Multi-machine logging

Goggles provides options to synchronize logging across machines, since there is always only a single server active. The relevant environment variables here are GOGGLES_HOST and GOGGLES_PORT.

[!WARNING] This functionality still needs thorough tesing, as well as a better documentation. Help is appreciated! 🤗

Adding a custom handler

[!NOTE] Ideally, you should open a PR: We would love to integrate your work!

Adding a custom handler is straightforward:

import goggles as gg
import logging


class CustomConsoleHandler(gg.ConsoleHandler):
    """A custom console handler that adds a prefix to each log message."""

    def handle(self, event: gg.Event) -> None:
        dict = event.to_dict()

        dict["payload"] = f"[CUSTOM PREFIX] {dict['payload']}"

        event = gg.Event.from_dict(dict)
        super().handle(event)


# Register the custom handler so it can be serialized/deserialized
gg.register_handler(CustomConsoleHandler)

# In this basic example, we set up a logger that outputs to the console.
logger = gg.get_logger("examples.custom_handler")


gg.attach(
    CustomConsoleHandler(name="examples.custom.console", level=logging.INFO),
    scopes=["global"],
)
# Because the logging level is set to INFO, the debug message will not be shown.
logger.info("Hello, world!")
logger.debug("you won't see this at INFO")

See also examples/05_custom_handler.py for a complete example.

Device-resident histories

For long-running GPU experiments that need efficient temporal memory management:

Why?

During development of fluid control experiments and reinforcement learning pipelines, we needed to:

  • Track detailed metrics during GPU-accelerated training
  • Avoid expensive device-to-host transfers
  • Maintain temporal state across episodes
  • Support JIT compilation for maximum performance

Features

  • Pure functional and JIT-safe buffer updates
  • Per-field history lengths with episodic reset support
  • Batch-first convention: (B, T, *shape) for all tensors
  • Zero host-device synchronization during updates
  • Integrated with FlowGym's EstimatorState for temporal RL memory

Usage

from goggles.history import HistorySpec, create_history, update_history
import jax.numpy as jnp

# Define what to track over time
spec = HistorySpec.from_config({
    "states": {"length": 100, "shape": (64, 64, 2), "dtype": jnp.float32},
    "actions": {"length": 50, "shape": (8,), "dtype": jnp.float32},
    "rewards": {"length": 100, "shape": (), "dtype": jnp.float32},
})

# Create GPU-resident history buffers
history = create_history(spec, batch_size=32)
print(history["states"].shape)  # (32, 100, 64, 64, 2)

# Update buffers during training (JIT-compiled)
new_state = jnp.ones((32, 64, 64, 2))
history = update_history(history, {"states": new_state})

See also examples/103_history.py for a running example.

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for detailed information on:

• Development workflow and environment setup • Code style requirements and automated checks • Testing standards and coverage expectations • PR preparation and commit message conventions

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.


Ready to enhance your robotics research with structured observability? Get started with Goggles today! 🚀gles

A lightweight, flexible Python logging and monitoring library designed to simplify and enhance experiment tracking, performance profiling, and error tracing. Integrates with terminal, file-based logs, and W&B (Weights & Biases). It is thought primarily for research projects in robotics.

pip install "goggles @ git+ssh://git@github.com/antonioterpin/goggles.git"

Features

  • 🤖 Multi-process, single-thread compatible Synchronize logs from all spawned processes via shared memory.

  • 🎯 Multi-output logging Log to terminal and/or file.

  • 🕒 Performance profiling @goggles.timeit decorator measures and logs runtime.

  • 🐞 Error tracing @goggles.trace_on_error auto-logs full stack on exceptions.

  • 📊 Metrics tracking goggles.scalar, goggles.vector, goggles.image, goggles.video → Weights & Biases.

  • 🚦 Graceful shutdown Call goggles.cleanup() (or hook into your own signal handler).

  • ⚙️ Asynchronous scheduling Offload heavy logging tasks via goggles.schedule_log(...).

  • 📁 Pretty configuration loading goggles.load_configuration(...) loads YAML with validation.

Quickstart

  1. TODO: update

  2. Ready to log:

    import goggles
    
    goggles.debug("Debugging details…")
    goggles.info("Experiment started")
    goggles.warning("This is a warning")
    goggles.error("An error occurred")
    

We cleanup all the resources automatically at exit.

Configuration

Pretty logging of configuration files.

import goggles

# Load from examples/example_config.yaml
config = goggles.load_configuration("examples/example_config.yaml")
print(config)

# Access as dict
print(f"time_per_experiment = {config['time_per_experiment']}")

Decorators: @goggles.timeit and @goggles.trace_on_error

Measure execution time of methods or functions:

import goggles

class Worker:
    @goggles.timeit(severity=logging.DEBUG)
    def compute_heavy(self, n):
        return sum(range(n))

    @goggles.trace_on_error()
    def risky_division(self, x, y):
        return x / y

g = Worker()
g.compute_heavy(1_000_000)

try:
    g.risky_division(1, 0)
except ZeroDivisionError:
    pass  # Full traceback was logged

W&B Integration

Log scalars, vectors, images, and videos directly to Weights & Biases:

import goggles
from PIL import Image
import numpy as np

# Start or switch a W&B run
goggles.new_wandb_run(name="exp-run", config={"lr":1e-3, "batch":32})

# Scalars & histograms
goggles.scalar("accuracy", 0.92)
goggles.vector("loss_curve", [0.5,0.4,0.3])

# Images
img = Image.fromarray((np.random.rand(64,64,3)*255).astype(np.uint8))
goggles.image("random_image", img)

Graceful Shutdown

Cleanly handle interrupts (e.g., Ctrl-C) and perform cleanup:

import goggles
from PIL import Image
import numpy as np

# Start or switch a W&B run
goggles.new_wandb_run(name="exp-run", config={"lr":1e-3, "batch":32})

# Scalars & histograms
goggles.scalar("accuracy", 0.92)
goggles.vector("loss_curve", [0.5,0.4,0.3])

# Images
img = Image.fromarray((np.random.rand(64,64,3)*255).astype(np.uint8))
goggles.image("random_image", img)

Asynchronous Logging & Video

Offload heavy logging tasks to worker threads and log video sequences:

import goggles, numpy as np, time
from PIL import Image

goggles.new_wandb_run("video_demo", {})
goggles.init_scheduler(num_workers=4)

def save_and_log_frame(frame, idx):v
    path = f"/tmp/frame_{idx}.png"
    frame.save(path)
    goggles.image(f"frame_{idx}", frame)

for i in range(100):
    arr = (np.random.rand(64,64,3)*255).astype(np.uint8)
    img = Image.fromarray(arr)
    goggles.schedule_log(save_and_log_frame, img, i)
    goggles.scalar("queue_size", goggles._task_queue.qsize())

goggles.stop_workers()

Full list of running examples

We prepared an examples/ folder with scripts covering:

TODO

Contributing

PRs, issues, and feature requests are welcome! Open an issue or submit a PR on GitHub. See our contributing guide for more information.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

robo_goggles-0.1.6.tar.gz (46.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

robo_goggles-0.1.6-py3-none-any.whl (42.9 kB view details)

Uploaded Python 3

File details

Details for the file robo_goggles-0.1.6.tar.gz.

File metadata

  • Download URL: robo_goggles-0.1.6.tar.gz
  • Upload date:
  • Size: 46.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for robo_goggles-0.1.6.tar.gz
Algorithm Hash digest
SHA256 162aa3a6aa2990799d10ec6875ea4a04943845722383feccbbf2f7fac1e2446e
MD5 800b73f96c2f56f4b6b5b9dda2761b78
BLAKE2b-256 abfbb07561f5500a2ad190985d9e0fba265bb314251fba4bd17438b75d103030

See more details on using hashes here.

File details

Details for the file robo_goggles-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: robo_goggles-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 42.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for robo_goggles-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 d41351e60f7ba82906174cb3f65afd4bc4a04cd82775f9234f2f0e48084880d6
MD5 1d11fe3d7788fe987ca468dd38399e1d
BLAKE2b-256 927cd6db4831ac8d89b7e58fe332c1b4571b478b7f40a32a75140e54416e3017

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page