How to Define Schemas¶
Schemas provide type safety, documentation, and validation for your trace events.
Dynamic vs Structured¶
Dynamic Events ¶
Python allows logging without pre-defined schemas:
import zelos_sdk
zelos_sdk.init()
source = zelos_sdk.TraceSource("dynamic")
# Types are inferred from values
source.log("sensor", {
"temperature": 25.5, # → Float64
"pressure": 1013, # → Int64
"status": "OK", # → String
"valid": True # → Boolean
})
Use for: Quick prototyping, exploration Avoid for: Production code, APIs, shared data
Structured Events ¶
Define schemas for type safety and documentation:
import zelos_sdk
zelos_sdk.init()
source = zelos_sdk.TraceSource("structured")
# Define schema with types and units
sensor_event = source.add_event("sensor", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float64, "°C"),
zelos_sdk.TraceEventFieldMetadata("pressure", zelos_sdk.DataType.Int32, "Pa"),
zelos_sdk.TraceEventFieldMetadata("status", zelos_sdk.DataType.String),
zelos_sdk.TraceEventFieldMetadata("valid", zelos_sdk.DataType.Boolean)
])
# Type-safe logging
sensor_event.log(
temperature=25.5,
pressure=1013,
status="OK",
valid=True
)
// Rust requires schema definition
let sensor_event = source
.build_event("sensor")
.add_f64_field("temperature", Some("°C".to_string()))
.add_i32_field("pressure", Some("Pa".to_string()))
.add_string_field("status", None)
.add_bool_field("valid", None)
.build()?;
// Type-safe logging
sensor_event.build()
.try_insert_f64("temperature", 25.5)?
.try_insert_i32("pressure", 1013)?
.try_insert_string("status", "OK".to_string())?
.try_insert_bool("valid", true)?
.emit()?;
// Go requires schema definition
unitC := "°C"
unitPa := "Pa"
sensorEvent, _ := source.BuildEvent("sensor").
AddFloat64Field("temperature", &unitC).
AddInt32Field("pressure", &unitPa).
AddStringField("status", nil).
AddBooleanField("valid", nil).
Build()
// Type-safe logging
builder, _ := sensorEvent.Build()
builder, _ = builder.TryInsertFloat64("temperature", 25.5)
builder, _ = builder.TryInsertInt32("pressure", 1013)
builder, _ = builder.TryInsertString("status", "OK")
builder, _ = builder.TryInsertBoolean("valid", true)
builder.Emit()
Data Types¶
Choosing the Right Type¶
Type | Range | Use For | Memory |
---|---|---|---|
Int8 | -128 to 127 | State codes, small counters | 1 byte |
Int16 | ±32K | Sensor ADC values | 2 bytes |
Int32 | ±2.1B | Timestamps (seconds), IDs | 4 bytes |
Int64 | ±9.2×10¹⁸ | Nanosecond timestamps | 8 bytes |
UInt8 | 0 to 255 | Percentages, byte values | 1 byte |
UInt16 | 0 to 65K | Port numbers, ADC values | 2 bytes |
UInt32 | 0 to 4.2B | Counters, CAN IDs | 4 bytes |
UInt64 | 0 to 1.8×10¹⁹ | Large counters, UUIDs | 8 bytes |
Float32 | ±3.4×10³⁸ | Sensor data (7 digits) | 4 bytes |
Float64 | ±1.7×10³⁰⁸ | Precise measurements (15 digits) | 8 bytes |
Boolean | true/false | Flags, states | 1 byte |
String | UTF-8 text | Text, identifiers | Variable |
Binary | Raw bytes | Payloads, images | Variable |
TimestampNs | Nanoseconds | Time points | 8 bytes |
Complete Example¶
# Comprehensive type usage
event = source.add_event("all_types", [
# Choose smallest type that fits your data
zelos_sdk.TraceEventFieldMetadata("state", zelos_sdk.DataType.UInt8), # 0-255
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float32, "°C"), # ±0.1°C precision
zelos_sdk.TraceEventFieldMetadata("timestamp", zelos_sdk.DataType.TimestampNs),
zelos_sdk.TraceEventFieldMetadata("message", zelos_sdk.DataType.String),
zelos_sdk.TraceEventFieldMetadata("payload", zelos_sdk.DataType.Binary),
])
import time
event.log(
state=3,
temperature=25.5,
timestamp=time.time_ns(),
message="Sensor reading",
payload=bytes([0xDE, 0xAD, 0xBE, 0xEF])
)
let event = source
.build_event("all_types")
.add_u8_field("state", None)
.add_f32_field("temperature", Some("°C".to_string()))
.add_timestamp_ns_field("timestamp", None)
.add_string_field("message", None)
.add_binary_field("payload", None)
.build()?;
use zelos_trace::time::now_time_ns;
event.build()
.try_insert_u8("state", 3)?
.try_insert_f32("temperature", 25.5)?
.try_insert_timestamp_ns("timestamp", now_time_ns())?
.try_insert_string("message", "Sensor reading".to_string())?
.try_insert_binary("payload", vec![0xDE, 0xAD, 0xBE, 0xEF])?
.emit()?;
unitC := "°C"
event, _ := source.BuildEvent("all_types").
AddUint8Field("state", nil).
AddFloat32Field("temperature", &unitC).
AddTimestampNsField("timestamp", nil).
AddStringField("message", nil).
AddBinaryField("payload", nil).
Build()
builder, _ := event.Build()
builder, _ = builder.TryInsertUint8("state", 3)
builder, _ = builder.TryInsertFloat32("temperature", 25.5)
builder, _ = builder.TryInsertTimestampNs("timestamp", time.Now().UnixNano())
builder, _ = builder.TryInsertString("message", "Sensor reading")
builder, _ = builder.TryInsertBinary("payload", []byte{0xDE, 0xAD, 0xBE, 0xEF})
builder.Emit()
Units¶
Always specify units for physical quantities:
# Common units by domain
telemetry = source.add_event("telemetry", [
# Mechanical
zelos_sdk.TraceEventFieldMetadata("speed", zelos_sdk.DataType.Float64, "m/s"),
zelos_sdk.TraceEventFieldMetadata("acceleration", zelos_sdk.DataType.Float64, "m/s²"),
zelos_sdk.TraceEventFieldMetadata("torque", zelos_sdk.DataType.Float64, "Nm"),
# Electrical
zelos_sdk.TraceEventFieldMetadata("voltage", zelos_sdk.DataType.Float32, "V"),
zelos_sdk.TraceEventFieldMetadata("current", zelos_sdk.DataType.Float32, "A"),
zelos_sdk.TraceEventFieldMetadata("power", zelos_sdk.DataType.Float32, "W"),
# Thermal
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float32, "°C"),
# Dimensionless
zelos_sdk.TraceEventFieldMetadata("efficiency", zelos_sdk.DataType.Float32, "%"),
zelos_sdk.TraceEventFieldMetadata("gear", zelos_sdk.DataType.Int8), # No unit
])
Enumerations¶
Map numeric values to readable strings using value tables:
from enum import IntEnum
class State(IntEnum):
IDLE = 0
RUNNING = 1
ERROR = 2
# Define event
status = source.add_event("status", [
zelos_sdk.TraceEventFieldMetadata("state", zelos_sdk.DataType.UInt8),
])
# Map values to strings for visualization
source.add_value_table("status", "state", {
0: "IDLE",
1: "RUNNING",
2: "ERROR"
})
# Log using enum
status.log(state=State.RUNNING)
use zelos_trace_types::Value;
#[repr(u8)]
enum State {
Idle = 0,
Running = 1,
Error = 2,
}
// Define event
let status = source
.build_event("status")
.add_u8_field("state", None)
.build()?;
// Add value table
source.add_value_table("status", "state", vec![
(Value::UInt8(0), "IDLE".to_string()),
(Value::UInt8(1), "RUNNING".to_string()),
(Value::UInt8(2), "ERROR".to_string()),
].into_iter())?;
// Log using enum
status.build()
.try_insert_u8("state", State::Running as u8)?
.emit()?;
const (
StateIdle = iota
StateRunning
StateError
)
// Define event
status, _ := source.BuildEvent("status").
AddUint8Field("state", nil).
Build()
// Add value table
source.AddValueTable("status", "state", map[*zelos.Value]string{
zelos.NewUint8Value(0): "IDLE",
zelos.NewUint8Value(1): "RUNNING",
zelos.NewUint8Value(2): "ERROR",
})
// Log using enum
builder, _ := status.Build()
builder, _ = builder.TryInsertUint8("state", StateRunning)
builder.Emit()
Nested Events¶
Organize related events using path notation:
# Parent event
source.add_event("motor", [
zelos_sdk.TraceEventFieldMetadata("rpm", zelos_sdk.DataType.Float64, "rpm"),
])
# Child events use "/" separator
source.add_event("motor/thermal", [
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float32, "°C"),
])
# Access via attributes (Python only)
source.motor.log(rpm=3500)
source.motor.thermal.log(temperature=85.5)
# Or via path
source.log("motor", {"rpm": 3500})
source.log("motor/thermal", {"temperature": 85.5})
Schema Evolution¶
Safe Changes¶
Adding new fields - Existing consumers ignore unknown fields
# Version 1
event_v1 = source.add_event("data", [
field("speed", Float64, "m/s"),
])
# Version 2 - Safe
event_v2 = source.add_event("data", [
field("speed", Float64, "m/s"),
field("altitude", Float64, "m"), # New field
])
Breaking Changes¶
Changing field types - Breaks existing consumers
# DON'T: Change type of existing field
# v1: field("count", Float64)
# v2: field("count", Int32) # BREAKS!
# DO: Add new field with different name
# v2: field("count_int", Int32) # Safe
Best Practices¶
1. Define Once¶
# Good
event = source.add_event("data", schema)
for _ in range(1000000):
event.log(value=read_value())
# Bad - Redefining schema
for _ in range(1000000):
source.add_event("data", schema) # Wasteful!
2. Choose Minimal Types¶
# Use smallest type that fits
field("percentage", UInt8) # 0-100 fits in UInt8
field("temperature", Float32) # 0.1°C precision is enough
# Don't oversize
field("percentage", Float64) # Wasteful for 0-100