Skip to content

Trace Cache

The Zelos SDK Trace Cache API provides a caching layer for trace events that allows you to store and retrieve the last logged values for any field, at a small performance cost.

This is particularly useful for applications that need to reference previously logged values, implement conditional logic based on cached data, or integrate with testing frameworks.

Core Components

TraceSourceCacheLast

A wrapper around TraceSource that automatically caches the last logged value for every field in every event.

import zelos_sdk

zelos_sdk.init()

# Create a cached trace source
source = zelos_sdk.TraceSourceCacheLast("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.

# 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 along with metadata about the field's data type and units.

# 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}")       # DataType.Float64
print(f"Current value: {temp_field.get()}")       # 46.2

Access Patterns

Attribute Access

The most convenient way to access cached values is through attribute notation:

source = zelos_sdk.TraceSourceCacheLast("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
rpm_value = source["motor/rpm"]            # Returns the cached value directly (3500.0)
temp_value = source["motor/thermal/temperature"]  # Returns the cached value directly (75.5)

# Get field objects for metadata access
rpm_field = source["motor"].rpm            # Returns TraceSourceCacheLastField
temp_field = source["motor/thermal"].temperature  # Returns TraceSourceCacheLastField

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.TraceSourceCacheLast("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 cache supports dynamic event creation - you can log to events that haven't been explicitly defined:

source = zelos_sdk.TraceSourceCacheLast("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

The trace cache integrates seamlessly with the Zelos testing framework for automated verification:

Basic Checker Usage

import pytest
import zelos_sdk

def test_motor_control(check):
    source = zelos_sdk.TraceSourceCacheLast("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, "between", 10.0, 20.0)
    check.that(source.motor.temperature, "between", 30.0, 40.0)

Time-based Checker Assertions

def test_motor_warmup_sequence(check):
    import threading
    import time

    source = zelos_sdk.TraceSourceCacheLast("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, within_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, for_duration_s=1.0)

    warmup_thread.join()

Available Checker Operators

The checker supports various operators when working with cached fields:

def test_checker_operators(check):
    source = zelos_sdk.TraceSourceCacheLast("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, "between", 46.0, 50.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")

    # Type checks
    check.that(source.battery.voltage, "is instance of", float)

Error Handling

The cache provides clear error messages for common mistakes:

source = zelos_sdk.TraceSourceCacheLast("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:

# Good: Descriptive names
motor_controller = zelos_sdk.TraceSourceCacheLast("motor_controller")
motor_controller.add_event("thermal_monitoring", [...])
motor_controller.add_event("speed_control", [...])

# Less ideal: Generic names
source = zelos_sdk.TraceSourceCacheLast("app")
source.add_event("data", [...])

3. Handle None Values

Cached fields return None until first logged to:

source = zelos_sdk.TraceSourceCacheLast("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.TraceSourceCacheLast("vehicle_system")

# Organize by subsystem
source.add_event("powertrain/engine", [...])
source.add_event("powertrain/transmission", [...])
source.add_event("chassis/suspension", [...])
source.add_event("chassis/brakes", [...])

# Access hierarchically
engine_rpm = source.powertrain.engine.rpm.get()
brake_pressure = source.chassis.brakes.pressure.get()