Skip to main content

Traces

Traces let you follow the lifecycle of an operation across your robot's software stack. Every trace is made up of spans — individual units of work with timing and attributes.

Your first trace

main.py
import botpulse
import time

pulse = botpulse.init(
service_name="navigation-node",
robot_id="robot-01",
)

# A simple trace
with pulse.trace("process_lidar_data") as span:
# Simulate processing
time.sleep(0.05)
span.set_attribute("points_received", 1024)
span.set_attribute("algorithm", "icp")

print("Done — trace sent to collector")

Running this creates a trace with a single span called process_lidar_data.

Nested spans

Real operations have sub-steps. Nest spans to see the breakdown:

main.py
import botpulse
import time

pulse = botpulse.init(
service_name="navigation-node",
robot_id="robot-01",
)

with pulse.trace("navigate_to_waypoint") as root:
root.set_attribute("waypoint.x", 12.5)
root.set_attribute("waypoint.y", 3.2)

with pulse.child_span("plan_path") as span:
time.sleep(0.02)
span.set_attribute("algorithm", "a_star")
span.set_attribute("path_length_m", 14.8)

with pulse.child_span("execute_trajectory") as span:
time.sleep(0.1)
span.set_attribute("duration_target_ms", 5000)
span.set_attribute("max_velocity", 0.5)

This produces a trace with three spans: the root and two children. You can see exactly how much time path planning vs. trajectory execution took.

Adding events

Events are timestamped markers within a span. Use them to record significant moments:

main.py
with pulse.trace("pick_and_place") as span:
span.add_event("gripper_opened")
# ... move to object ...
span.add_event("object_grasped", {"object_id": "block-A"})
# ... move to target ...
span.add_event("object_released", {"target_slot": 3})

Recording errors

Mark a span as an error when something goes wrong:

main.py
try:
with pulse.trace("call_motor_driver") as span:
result = motor_driver.set_speed(100)
if result.error:
span.set_status("error", result.error_message)
span.record_exception(result.error)
except Exception as e:
span.set_status("error", str(e))
span.record_exception(e)

Cross-service traces

When your robot has multiple nodes (e.g., a ROS2 system with separate processes), Bot Pulse propagates trace context automatically so you see the full picture:

perception_node.py
import botpulse

pulse = botpulse.init(service_name="perception", robot_id="robot-01")

with pulse.trace("detect_objects") as span:
# ... run detection ...
span.set_attribute("objects_found", 3)
planning_node.py
import botpulse

pulse = botpulse.init(service_name="planning", robot_id="robot-01")

# The trace context is propagated if both nodes share the same collector
with pulse.trace("plan_navigation") as span:
span.set_attribute("objects", 3)
# ... plan path ...

Attributes reference

Attributes are key-value pairs attached to spans. Use these types:

Value typeExample
str"a_star"
int1024
float0.5
boolTrue

Best practices

  • Name spans by operation, not by implementation ("navigate_to_waypoint" not "run_a_star_and_publish_cmd_vel")
  • Keep span count reasonable — one span per logical step, not per line of code
  • Set attributes early — they're most useful when they describe what the span is about
  • Always record exceptions — it makes debugging production issues much easier

Next steps

  • Logs — Correlate log entries with your traces
  • Metrics — Emit numeric data alongside traces