Historian

Store and query time-series history for any entity field.

Overview

The Historian is ControlBird's time-series recording and query engine. It watches field changes across entities and captures them into rotating time-series storage, so you can answer questions about the past: what a value was at a given moment, how a metric trended over a week, or which events fired overnight. Recording rules are declared as HistorianTable configuration entities. Each one names a source entity type (or a specific list of entities), the field to record, how much data to retain, and which related context to capture alongside every value.

The Historian app in the Platform UI provides a split-pane interface: a list of configured tables on the left, and a query view on the right. Select a table, pick a time range, optionally add a SQL WHERE filter, and run the query to get a sortable results table. Reach for the Historian whenever you need durable, queryable history of field values: performance metrics, alarm event logs, or any other field whose changes you want to keep beyond the live Store.

Prefer a guided tutorial?

New to this? Follow the View Historical Data walkthrough for a step-by-step tour, then come back here for the full reference.

Key Concepts

  • Historian table: a HistorianTable entity that defines one recording rule: which source entity type or entities to monitor, which field to record, storage limits, and the context fields to capture. The Historian app lists every configured table.
  • Record: a single timestamped value capture. Each record carries the moment of capture, the entity ID, the field path, the recorded value, the writer that triggered the change, and a map of context fields.
  • Trigger mode: controlled by TriggerOnChange. When true (the default), only changed values are recorded; when false, every notification is recorded, which is required to capture repeated event logs.
  • Context fields: extra field paths captured next to each value (for example a parent's name) so queries have human-readable context without extra lookups.
  • Retention: each table bounds how much history it keeps. Recent data is retained at full fidelity and older data is compressed for long-term storage, with limits you configure per table.

How Recording Works

Each HistorianTable watches its configured source field and captures values as they change. Recording stays efficient under sustained load, and history is bounded by the retention limits you set per table: when storage grows past your configured size, older data rotates out and is eventually compressed or removed according to your retention settings.

Each record automatically shows the human-readable name of the entity or user that triggered the change. Context fields are captured per the table's ContextFields specification, resolving the related values at record time.

HistorianTable Configuration

HistorianTable

The configuration entity that defines a time-series recording rule: which source entity type or field to monitor, storage limits, and the context to capture.

FieldTypePurpose
NameStringDisplay name of the historian table (e.g. Alarm Events, Service CPU Usage).
SourceEntityTypeStringEntity type whose fields are being recorded (e.g. Alarm, Service); empty if recording specific SourceEntities.
SourceEntitiesEntity listSpecific entities to record from; if non-empty, overrides SourceEntityType-based recording.
SourceFieldTypeStringField to record (e.g. Log, CPUUsage, MemoryUsage); must match a field on the source type or entities.
TriggerOnChangeBoolean (default: true)If true, record only when the source field changes; if false, record all notifications (e.g. for log event captures).
ContextFieldsString (semicolon-delimited)Field paths to capture alongside the value (e.g. Parent->Name;Name;State) for context in queries; supports a :TypeOverride suffix per field.
MaxDbFileSizeMBIntegerTarget size in MB before history rotates to a new storage segment; smaller values keep queries fast, larger values suit high-volume tables.
MaxFilesHotInteger (default: 1)Number of recent full-fidelity files to keep before moving to compressed long-term storage.
MaxFilesColdInteger (default: 0)Number of cold compressed files to retain; older files are deleted. 0 disables cold storage entirely.
DescriptionString (optional)User description of what is being recorded and why.

HistoryRecord

The result returned from a historian query. Each record represents one value-capture event.

FieldTypePurpose
timestampInteger (Unix seconds)Exact moment the field value was recorded, as epoch seconds.
entity_idIntegerNumeric ID of the entity whose field was recorded.
field_pathString (semicolon-delimited for multi-level)Path to the field within its entity; matches SourceFieldType.
valueNumber, text, boolean, or timestampThe actual recorded value at that moment.
writer_nameString (optional)Name of the entity or user that triggered the change.
contextKey-value mapContext fields extracted per the ContextFields spec (e.g. Parent->Name => MyService).

The Historian App

The Historian app requires the app.historian permission. It opens in a horizontal split-pane layout: the table list on the left, the query view on the right. The default deployment ships three pre-configured tables out of the box:

  • Alarm Events: the Log field of Alarm entities.
  • Service CPU Usage: a float metric on Service entities.
  • Service Memory Usage: a float metric on Service entities.

The query flow is: select a table from the list, which opens the query view with a time-range picker (presets or custom epoch seconds); set a row limit (default 1000 in the UI); add an optional SQL WHERE filter; then click Query to see a results table with sortable, resizable columns and virtual scrolling for large datasets. Numeric columns are auto-detected and right-aligned. The results table also renders special values: CSS colors appear as swatches, SVG markup is rendered inline (sanitized for safety), and epoch-zero timestamps display as -.

Time ranges

The time-range picker offers presets and a custom option, all converted to Unix epoch seconds before querying. Presets compute an offset from now:

PresetOffset from now
24hnow() - 86400s
7dnow() - 604800s
30d30 days of seconds before now
CustomExplicit start and end epoch seconds

Saved queries

The app stores saved queries in browser localStorage together with their metadata: the chosen preset, time range, SQL filter, and row limit.

Configuration Examples

Define a recording rule as a HistorianTable entity in your topology configuration.

{
  "entityType": "HistorianTable",
  "Name": "Service CPU Usage",
  "SourceEntityType": "Service",
  "SourceFieldType": "CPUUsage",
  "ContextFields": "Parent->Name;Name",
  "MaxDbFileSizeMB": 50,
  "MaxFilesHot": 3,
  "MaxFilesCold": 10
}

Context fields normally infer their SQL type, but you can force a type with a :TYPE suffix per field when inference is ambiguous:

ContextFields = "Parent->Name:TEXT;CurrentSeverity->Symbol:TEXT;CPUUsage:REAL"

Querying

A query targets one historian table and optionally narrows the results by time range, row limit, and a filter expression. In the Historian app you choose the table, pick a time range, set a row limit (the picker defaults to 1000), and add an optional filter before running the query.

Filter expressions

Filters use standard SQL WHERE syntax and are parsed and validated before running. Supported operators include =, !=, <, >, AND, and OR. You can filter on value, entity_id, timestamp, and captured context fields.

value > 50 AND entity_id = 12345
context['Parent->Name'] = 'Prod Server'
timestamp >= 1704067200

Common Patterns

  • Recording a metric across a type: set SourceEntityType to the type (e.g. Service) and SourceFieldType to the metric field, leave TriggerOnChange at its default true, and add a Parent->Name;Name context so each row is identifiable.
  • Capturing an event log: point the table at the Log field and set TriggerOnChange = false so every write is recorded, even when the logged text is identical to the previous one.
  • Recording from a hand-picked set: populate SourceEntities with specific entities instead of a type when you only care about a few instances.
  • Tuning retention: keep a small MaxDbFileSizeMB and a modest MaxFilesHot for fast queries, and use MaxFilesCold to bound long-term compressed history.

SourceEntityType and SourceEntities are mutually exclusive

If SourceEntities is non-empty it overrides SourceEntityType, which is then ignored. If both are empty, nothing is recorded at all.

Use TriggerOnChange=false for event logs

TriggerOnChange = true skips duplicate writes on unchanged fields. To capture every entry in a Log field (including repeated identical messages) set TriggerOnChange = false.

Troubleshooting & Limitations

  • Context field syntax is strict. Field paths must use valid field names and the -> indirection syntax exactly; invalid paths cause config load errors. Paths beyond 2–3 levels rarely work because of traversal-depth limits.
  • Type inference can guess wrong. If a context field renders incorrectly in the UI, add a :TYPE override (e.g. RowBackgroundColor:TEXT to prevent numeric coercion).
  • Retention bounds your history. Older data is compressed and eventually removed according to your per-table limits. Set MaxFilesCold = 0 to disable long-term compressed storage entirely.
  • Query cost grows with retention depth. Larger tables and longer retention make queries scan more data. Keep MaxDbFileSizeMB and MaxFilesHot conservative for fast queries.
  • SQL parsing is strict. Operators must match standard SQL syntax; custom functions are not supported.
  • Saved queries are local. They live in your browser and are not synced to the server; clearing browser data loses them.