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 columns 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 column values.
  • 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 columns: extra field paths captured next to each value (for example a parent's name) so queries have human-readable context without extra lookups. Each one is a HistorianColumn with a field path and a column type, listed in the table's ContextColumns in the order the columns appear.
  • 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. Each context column listed in the table's ContextColumns is resolved at record time and stored with the value, using the column type you chose for it.

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).
ContextColumnsEntity listThe HistorianColumn entries to capture alongside the value, in column order. Only listed columns are recorded.
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.

HistorianColumn

One context column of a historian table. A table records a column only while the column is listed in its ContextColumns.

FieldTypePurpose
FieldPathStringOne field path, read from the recorded entity. Use -> to follow references (e.g. Name, Parent->Name, CurrentSeverity->Symbol).
ColumnTypeChoice (default: Text)How the captured value is stored and compared in filters: Text, Integer, Real, Boolean, Timestamp, or Blob.

A column's name in query results is its field path (Parent->Name). Two columns of one table cannot share a field path.

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 mapOne entry per context column, keyed by its field path (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

Create and edit tables in the Historian app's table editor. Fill in the name, source entity type and source field, then build the table's context under Context Columns: click Add Column, enter a field path such as Parent->Name, and pick its type. The arrow buttons reorder the columns and the remove button drops one. Saving writes the table and its columns together.

A table recording service CPU usage with two context columns looks like this:

SettingValue
NameService CPU Usage
Source Entity TypeService
Source Field TypeCPUUsage
Context column 1Parent->Name, Text
Context column 2Name, Text
MaxDbFileSizeMB / MaxFilesHot / MaxFilesCold50 / 3 / 10

Every context column has an explicit type; nothing is guessed from the captured values. Pick the type that matches what you will filter on: Real for a measurement you compare numerically, Text for names and symbols, even when a symbol looks numeric.

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 the recorded field by its name (for example CPUUsage), entity_id, timestamp, writer_name, and any context column. Refer to a context column by its field path with -> written as _, or quote the path.

CPUUsage > 50 AND entity_id = 12345
Parent_Name = 'Prod Server'
"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 Parent->Name and Name context columns 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 column paths are strict. A field path holds valid field names joined by ->. A path that does not resolve, or two columns with the same path, make the historian reject the change: the table keeps recording with its previous configuration until the column is fixed. Paths beyond two or three levels rarely work because of traversal-depth limits.
  • A column's type decides how it is stored. If a context column renders or filters incorrectly, change its type in the table editor (for example set RowBackgroundColor to Text so a color value is kept as written).
  • 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.