Trace Cache¶
The Trace Cache keeps the last value of every field while still emitting normal trace events. Read a cached value back through attribute or item access.
Use it to read back a previously logged value, branch on cached data, or assert against live state in a test.
The cache can also filter what it emits. See Log Conditions.
Alias
TraceSourceCacheLast is another name for TraceSourceCache. Both names refer to the same class.
Core Components¶
TraceSourceCache¶
A TraceSource-like API backed by the Rust trace cache. It automatically caches the last logged value for every field in every event.
TraceSourceCache(name, namespace=None) takes the same arguments as TraceSource. Pass a TraceNamespace to keep the source out of the global namespace.
import zelos_sdk
zelos_sdk.init()
# Create a cached trace source
source = zelos_sdk.TraceSourceCache("motor_controller")
# Define event schema
source.add_event("sensor_data", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "celsius"),
zelos_sdk.TraceEventFieldMetadata("voltage", zelos_sdk.DataType.Float64, "V"),
zelos_sdk.TraceEventFieldMetadata("current", zelos_sdk.DataType.Float64, "A")
])
# Log some data
source.sensor_data.log(temperature=45.5, voltage=48.2, current=12.5)
# Access cached values
print(f"Last temperature: {source.sensor_data.temperature.get()}") # 45.5
print(f"Last voltage: {source.sensor_data.voltage.get()}") # 48.2
TraceSourceCacheLastEvent¶
Represents a cached event that provides access to fields and nested submessages. Events are automatically created when you define them with add_event() or when you log to them dynamically.
event.time_ns is the timestamp of the last row logged to the event, in nanoseconds since the Unix epoch. It is None until the first log.
# Access event and log new values
event = source.sensor_data
event.log(temperature=46.2, voltage=47.8, current=13.1)
# The cache is automatically updated
assert source.sensor_data.temperature.get() == 46.2
TraceSourceCacheLastField¶
Represents a cached field that stores the last logged value and exposes its full path plus data type.
# Get field reference
temp_field = source.sensor_data.temperature
# Access cached value
current_temp = temp_field.get()
# Access field metadata
print(f"Field name: {temp_field.name}") # "sensor_data.temperature"
print(f"Data type: {temp_field.data_type}") # float64
print(f"Current value: {temp_field.get()}") # 46.2
Behavior to rely on¶
add_event()registers a schema immediately and returns a cached event wrapper.log()andlog_at()can also auto-register new events on first use.- The cache stores every logged value, even when a log condition skips the row.
TraceSourceCacheLastField.set()does nothing. The cache changes only when you log through the source or event API.get_source()raisesNotImplementedError. If you need a plainTraceSource, create one separately.
Other Methods¶
TraceSourceCache also has these TraceSource methods:
| Method | What it does |
|---|---|
add_value_table(event, field, {int: str}) |
Registers an enum mapping. Call add_event() first. |
get_event(name) |
Returns the cached event. Raises KeyError for an unknown event. |
log_dict(name, data) / log_dict_at(time_ns, name, data) |
Same as log() / log_at(). |
log_many([(time_ns, name, data), ...]) |
Logs the rows in order, as repeated log_at() calls. |
log_batch(name, batch) |
Logs a pyarrow.RecordBatch or Table. The cache keeps the last non-null value of each column. Log conditions do not apply. |
flush() |
Sends buffered rows now. |
Access Patterns¶
Attribute Access¶
The most convenient way to access cached values is through attribute notation:
source = zelos_sdk.TraceSourceCache("motor_controller")
# Define nested event structure
source.add_event("motor", [
zelos_sdk.TraceEventFieldMetadata("rpm", zelos_sdk.DataType.Float64),
zelos_sdk.TraceEventFieldMetadata("torque", zelos_sdk.DataType.Float64, "Nm")
])
source.add_event("motor/thermal", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "celsius")
])
# Log data
source.log("motor", {"rpm": 3500.0, "torque": 42.8})
source.log("motor/thermal", {"temperature": 75.5})
# Access via attributes
motor_rpm = source.motor.rpm.get() # 3500.0
motor_temp = source.motor.thermal.temperature.get() # 75.5
Dictionary Access¶
For dynamic access patterns or when field names contain special characters:
# Dictionary-style access for events
event = source["motor"] # Returns TraceSourceCacheLastEvent
temp_event = source["motor/thermal"] # Returns nested TraceSourceCacheLastEvent
# Dictionary-style access for fields returns the field object
rpm_field = source["motor/rpm"] # Returns TraceSourceCacheLastField
temp_field = source["motor/thermal/temperature"] # Returns TraceSourceCacheLastField
# Get cached values via .get()
rpm_value = rpm_field.get() # 3500.0
temp_value = temp_field.get() # 75.5
Handling Name Conflicts¶
When a submessage has the same name as a field in the parent event, use explicit methods to disambiguate:
source = zelos_sdk.TraceSourceCache("sensor_system")
# Create events with conflicting names
source.add_event("battery", [
zelos_sdk.TraceEventFieldMetadata("status", zelos_sdk.DataType.Int32)
])
source.add_event("battery/status", [
zelos_sdk.TraceEventFieldMetadata("code", zelos_sdk.DataType.Int32)
])
# Log data
source.log("battery", {"status": 1})
source.log("battery/status", {"code": 200})
# Access field explicitly
status_field_value = source.battery.get_field("status").get() # 1
# Access submessage explicitly
status_submessage = source.battery.get_submessage("status") # TraceSourceCacheLastEvent
status_code = status_submessage.code.get() # 200
# Or use attribute access (defaults to submessage if both exist)
status_event = source.battery.status # Returns TraceSourceCacheLastEvent
status_code = source.battery.status.code.get() # 200
Dynamic Event Creation¶
The Rust-backed cache supports dynamic event creation. You can log to events that have not been explicitly defined, and the Python wrapper becomes available after the first log:
source = zelos_sdk.TraceSourceCache("vehicle_system")
# Log to undefined events - they will be created dynamically
source.log("engine_stats", {"rpm": 2500, "load": 65.5})
source.log("engine_stats/emissions", {"nox": 45.2, "particulates": 12.1})
# Access the dynamically created cached values
rpm = source.engine_stats.rpm.get() # 2500
nox_level = source.engine_stats.emissions.nox.get() # 45.2
Time-based Logging¶
Cache supports logging with specific timestamps:
import time
# Log with current timestamp
source.log("sensor_data", {"temperature": 45.0})
# Log with specific timestamp
timestamp_ns = int(time.time_ns())
source.log_at(timestamp_ns, "sensor_data", {"temperature": 46.0})
# Log via event object with specific timestamp
source.sensor_data.log_at(timestamp_ns + 1000000000, temperature=47.0)
Checker Integration¶
Pass a cached field straight to the checker:
Basic Checker Usage¶
import pytest
import zelos_sdk
def test_motor_control(check):
source = zelos_sdk.TraceSourceCache("motor_test")
# Define motor control events
source.add_event("motor", [
zelos_sdk.TraceEventFieldMetadata("rpm", zelos_sdk.DataType.Float64),
zelos_sdk.TraceEventFieldMetadata("torque", zelos_sdk.DataType.Float64, "Nm"),
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "celsius")
])
# Log initial motor state
source.log("motor", {"rpm": 0.0, "torque": 0.0, "temperature": 20.0})
# Verify initial state
check.that(source.motor.rpm, "==", 0.0)
check.that(source.motor.temperature, "<", 25.0)
# Simulate motor startup
source.log("motor", {"rpm": 1500.0, "torque": 15.5, "temperature": 35.0})
# Verify running state
check.that(source.motor.rpm, ">", 1000.0)
check.that(source.motor.torque, ">", 10.0)
check.that(source.motor.torque, "<", 20.0)
check.that(source.motor.temperature, ">", 30.0)
check.that(source.motor.temperature, "<", 40.0)
Time-based Checker Assertions¶
def test_motor_warmup_sequence(check):
import threading
import time
source = zelos_sdk.TraceSourceCache("motor_warmup")
source.add_event("motor", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "celsius"),
zelos_sdk.TraceEventFieldMetadata("ready", zelos_sdk.DataType.Boolean)
])
# Start with cold motor
source.log("motor", {"temperature": 20.0, "ready": False})
# Simulate gradual warmup in background thread
def warmup_motor():
for temp in range(20, 61, 5): # Heat up from 20°C to 60°C
source.log("motor", {
"temperature": float(temp),
"ready": temp >= 50.0 # Ready when temperature reaches 50°C
})
time.sleep(0.2)
warmup_thread = threading.Thread(target=warmup_motor)
warmup_thread.start()
# Check that motor becomes ready within 3 seconds
check.that(source.motor.ready, "==", True,
temporal="within_duration", duration_s=3.0)
# Check that temperature stays above 45°C for at least 1 second once reached
check.that(source.motor.temperature, ">", 45.0,
temporal="for_duration", duration_s=1.0)
warmup_thread.join()
Available Checker Operators¶
Cached fields work with the full operator vocabulary:
def test_checker_operators(check):
source = zelos_sdk.TraceSourceCache("battery_monitor")
source.add_event("battery", [
zelos_sdk.TraceEventFieldMetadata("voltage", zelos_sdk.DataType.Float64, "V"),
zelos_sdk.TraceEventFieldMetadata("state", zelos_sdk.DataType.String),
zelos_sdk.TraceEventFieldMetadata("charging", zelos_sdk.DataType.Boolean)
])
source.log("battery", {"voltage": 48.2, "state": "nominal", "charging": True})
# Equality checks
check.that(source.battery.voltage, "==", 48.2)
check.that(source.battery.state, "==", "nominal")
# Comparison checks
check.that(source.battery.voltage, ">", 45.0)
check.that(source.battery.voltage, "<=", 50.0)
check.that(source.battery.voltage, ">", 46.0)
# Boolean checks
check.that(source.battery.charging, "is_true")
check.that(source.battery.charging, "==", True)
# String checks
check.that(source.battery.state, "contains", "nom")
check.that(source.battery.state, "starts_with", "nom")
Error Handling¶
The cache provides clear error messages for common mistakes:
source = zelos_sdk.TraceSourceCache("error_demo")
source.add_event("motor", [
zelos_sdk.TraceEventFieldMetadata("rpm", zelos_sdk.DataType.Float64)
])
try:
# This will raise AttributeError - field doesn't exist
value = source.motor.nonexistent_field.get()
except AttributeError as e:
print(f"Field access error: {e}")
try:
# This will raise KeyError - event doesn't exist
event = source["nonexistent_event"]
except KeyError as e:
print(f"Event access error: {e}")
try:
# This will raise KeyError - field path doesn't exist
value = source["motor/nonexistent_field"]
except KeyError as e:
print(f"Field path error: {e}")
Best Practices¶
1. Initialize Fields Early¶
Define your event schemas early in your application to get proper type checking and metadata:
# Good: Define schema upfront
source.add_event("sensor", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "celsius")
])
# Less ideal: Relying on dynamic creation
source.log("sensor", {"temperature": 25.0}) # Schema inferred from data
2. Use Meaningful Names¶
Choose descriptive names for your trace sources and events:
schema = [zelos_sdk.TraceEventFieldMetadata("value", zelos_sdk.DataType.Float64)]
# Good: Descriptive names
motor_controller = zelos_sdk.TraceSourceCache("motor_controller")
motor_controller.add_event("thermal_monitoring", schema)
motor_controller.add_event("speed_control", schema)
# Less ideal: Generic names
source = zelos_sdk.TraceSourceCache("app")
source.add_event("data", schema)
3. Handle None Values¶
Cached fields return None until first logged to:
source = zelos_sdk.TraceSourceCache("safety_check")
source.add_event("sensor", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64)
])
# Check for None before using cached values
temp = source.sensor.temperature.get()
if temp is not None:
if temp > 80.0:
print("Temperature warning!")
else:
print("No temperature data logged yet")
4. Organize Hierarchical Data¶
Use nested events to organize related data:
source = zelos_sdk.TraceSourceCache("vehicle_system")
# Organize by subsystem
source.add_event("powertrain/engine", [
zelos_sdk.TraceEventFieldMetadata("rpm", zelos_sdk.DataType.Float64)
])
source.add_event("chassis/brakes", [
zelos_sdk.TraceEventFieldMetadata("pressure", zelos_sdk.DataType.Float64, "bar")
])
source.log("powertrain/engine", {"rpm": 2200.0})
source.log("chassis/brakes", {"pressure": 12.5})
# Access by full event path
engine_rpm = source["powertrain/engine"].rpm.get() # 2200.0
brake_pressure = source["chassis/brakes/pressure"].get() # 12.5
Attribute chains such as source.powertrain.engine work only when every level is itself a registered event. Here powertrain is not an event, so source.powertrain raises AttributeError. Item access with the full path always works.