Live streams

Collector: recording

Record each source always, by trigger or manually, mark moments, and keep every value for years as encrypted day files with 1-minute and 1-hour summaries.

Everything the collector stores from a stream is a recording. Each source says how it records with record:: always (the default), by trigger (from a start condition to a stop condition), or manual (only when someone presses Record in the app). Sources that record by themselves keep their values for years as encrypted day files with 1-minute and 1-hour summaries. This page is for the OT engineer who sets it up and the IT reviewer who approves the extra firewall host. Installation and the rest of collector.yaml are on the Site collector page.

How a source records

Set record: on each source, and the recording: block once for the whole collector:

collector.yaml
api_key: env:KYMOSCOPE_API_KEY
state_dir: state
health:
  stream: Customer A collector health
sources:
  - name: Customer A DCS
    type: opcua
    url: opc.tcp://dcs-gw:4840
    tags: tags.csv
    record: always               # the default: everything the source sends
    mark_tag: MarkButton         # optional: a rise of this channel marks the stream

  - name: Customer A test rig
    type: udp
    bind: 127.0.0.1:5005
    layout: rig_layout.yaml
    record:
      trigger:                   # only from start to stop
        start: Speed > 5
        stop: Speed < 1
        start_after: 5s          # optional: the start must hold this long
        stop_after: 2min         # optional: the stop must hold this long

  - name: Customer A lab bench
    type: modbus
    host: 192.168.1.20
    map: registers.csv
    record: manual               # stored only when someone presses Record in the app

recording:                       # for every source that records by itself (always or trigger)
  timezone: Europe/Madrid        # recordings and days are cut at its midnight (default UTC)
  split_gap: 30min               # no value for this long ends a recording (default 30min)
  # where: own                   # kymoscope | own; omit to follow the organization's choice
  # raw_years: 2                 # raw values kept this many years (your plan may allow fewer)
  # close_after_min: 15          # a day is uploaded this many minutes after its midnight
  # keep_local_days: 7           # the local copy stays this many days after the upload
  # max_local_gb: 50             # cap of each source's local day spool
  # today_every_min: 15          # today's summaries are uploaded this often (0: only at midnight)
recordWhat is stored
alwaysEverything the source sends, day and night. The default when record is left out.
triggerOnly from the moment the start condition is true until the stop condition is true. See Record by trigger.
manualNothing by the collector. The stream is recorded only while someone presses Record in the app, and those recordings are kept 90 days.

The live stream always carries every value, whatever record says: it only decides what is stored. Restart the collector after changing it.

Record by trigger

A condition is a channel, a comparison (> >= < <= == !=) and a number, for example Speed > 5. To combine several, write all: (every one true) or any: (at least one) with a list:

collector.yaml (one source)
record:
  trigger:
    start:
      all:                       # every condition true (any: at least one)
        - Speed > 5
        - Gear >= 1
    stop: Speed < 1
    stop_after: 90s
KeyMeaning and default
startRequired. Recording starts when it becomes true.
stopRequired. Recording stops when it becomes true.
start_afterOptional. The start must hold this long before recording starts; a shorter blip starts nothing. Values from the wait are not stored.
stop_afterOptional. The stop must hold this long before recording stops; a shorter dip keeps it going. Values from the wait are stored.

Durations are written 500ms, 30s, 2min, 1.5h, or a plain number of seconds. Each start to stop is one recording, however short the pause before the next start (collector 0.6.1 or later; the pauses are kept in trigger-runs.jsonl beside the source's days for 31 days). In the app's Cloud window the stream shows Trigger.

Marks

Every stream of a source has a channel named mark. It is 0 from the stream's start and 1 for a single sample when the stream is marked, so each mark is a spike you can chart, alert on or start a cycle on. A mark flags a moment, not a stretch: look before and after it.

To mark from the process, set mark_tag on the source to one of its channels, for example an operator push-button wired to the DCS or PLC. Each rise of that channel (from 0 or below to above 0) marks the stream.

Turn it on

Sources that record always or by trigger need three things:

  1. The Historian on your organization. Enterprise includes it (5 years of raw data, 5,000 channels, and the collector's cycle rules); on other plans ask Kymoscope to turn it on. We set the years of raw data and the number of channels your contract includes. Without it, a source with no record line records manually (the log says so once, the health stream shows history_not_in_plan). A source with record: always or a trigger written keeps its days on the collector's disk until the Historian is on.
  2. An end-to-end encrypted project. The streams must belong to a project with end-to-end encryption. For any other project the collector uploads nothing, the log says why, and the health stream shows history_needs_e2ee = 1.
  3. The history extra. Install pip install "kymoscope-collector[history]" (the Docker image already includes it). Without it, a source with no record key records manually and the log says so; a source with record: always or trigger written out stops the start with a sentence naming the extra.

The recording block

Every key is optional and applies to every source that records by itself. Leave the block out to take the defaults.

KeyMeaning and default
recording.timezoneIANA zone of the site, for example Europe/Madrid. Default UTC. Recordings and days are cut at its midnight. It cannot change once a day of that source is stored.
recording.split_gapA duration. A span with no value at all this long ends a recording, and the next value starts a new one; the span is stored as a gap. Default 30min; 0 never splits.
recording.wherekymoscope or own. Leave it out to follow the organization: its own bucket when an admin set one up, else Kymoscope storage. See Your own storage.
recording.raw_yearsYears the raw day files are kept, 1 to 30. Default 1. The server keeps the lower of this and what your plan allows.
recording.close_after_minMinutes after midnight before a day is built and uploaded. Default 15.
recording.keep_local_daysDays the local copy of a day stays after it was uploaded. Default 7, at least 1.
recording.max_local_gbCap of each source’s local day spool in GB. Default 50. See Disk on the collector host.
recording.today_every_minMinutes between uploads of today’s 1-minute and 1-hour summaries, so today can be opened in the app before midnight. Default 15; 0 uploads them only when the day closes. Any other value must be at least 5.

Coming from history:

The history: section of earlier collectors is gone, and a configuration that still has it stops the start. Move its keys under recording:. gap_s becomes split_gap; a source left out of history.sources gets record: manual, and so does every source where enabled: false was set. Stored days stay as they are.

collector.yaml
# before (refused now):
#   history:
#     timezone: Europe/Madrid
#     gap_s: 600
#     sources: [Customer A DCS]
recording:
  timezone: Europe/Madrid
  split_gap: 10min               # was gap_s: 600
sources:
  - name: Customer A DCS         # records always (the default)
    ...
  - name: Customer A lab bench   # was not in history.sources
    ...
    record: manual

What it keeps

  • Every value at the rate the source delivers it, exactly as the live stream carries it (for trigger, while a recording is open). Nothing is resampled or deadbanded.
  • Per source and per site-local day (midnight to midnight in recording.timezone): one raw day file with every value, and two rollup files with a summary per channel for every minute and every hour (min, max, mean, count and last value).
  • Spans with no value at all longer than split_gap are stored as gaps, so a missing hour reads as missing, not as a flat line.
  • Everything is encrypted on the collector before it is uploaded. Kymoscope stores files it cannot read.

The rollups make long ranges fast: a year of a channel is a few thousand hourly points, and the raw file is only needed when you zoom in.

How long it is kept

  • Raw day files: raw_years, up to what your plan allows.
  • Rollups: 10 years, whatever the raw setting.
  • Raising raw_years extends the days already stored. Lowering it applies to new days only: stored days keep the expiry they had.
  • If the contract ends or is reduced, no stored day is deleted early. Each day stays until the expiry it got when it was uploaded; only new uploads stop.

Keep windows

A keep window holds raw data past its normal expiry, for example around an incident or a batch you may be asked about. Any member of the project can mark one while the Historian is on. A window keeps every whole raw day it overlaps, for as long as the window exists, and covers at most 7 days.

Each organization has 10 keep windows per calendar year included. Beyond that, windows are still created (marking an incident is never blocked) and count as overage on your plan.

Today's data

A day's raw file is built and uploaded after the site's midnight. Until then, the collector uploads today's 1-minute and 1-hour summaries every today_every_min minutes (15 by default), so today can already be read in the app at summary resolution; every value of today arrives once the day closes. How members open, zoom, keep and export a history is on Open history in the app.

Cycles and alerts

A cycles: section next to recording: lets the collector cut batches, shifts and laps, with their phases and limits, 24/7 with no browser open, on sources that record always or by trigger, and alerts: emails when one misses a limit. Both, and how members compare runs against a reference, are on Cycles. The older segments: section still works in this release.

Late data and outages

Every value a source records also goes into a local day spool on the collector host (state_dir/history/<source>/days), one folder per local day. The day is built from that copy, so a network outage only delays the upload: the collector retries every minute, and an interrupted upload resumes where it stopped.

  • Values that arrive after a day was uploaded (a backfill, a buffer catching up, a slow source) make a new version of that day, which replaces the previous one. A day is never shortened by late data.
  • The local copy stays keep_local_days after the upload, so late values within that time merge from the local copy. After that, the collector downloads the stored day and merges into it.
  • A day older than its retention is not accepted any more and is skipped (day_expired).

Disk on the collector host

As a rough guide, 1,000 tags at 1 value per second take about 0.4 GB per day in the local spool. With the default keep_local_days: 7, plan for about 9 days of that (the current day, the day being uploaded and the 7 kept), so about 3.5 GB per 1,000 tags at 1 Hz. Tags that report only on change take much less.

The spool has a cap, recording.max_local_gb (default 50 GB). The collector protects the disk and your data in this order:

  • Near the cap, it deletes days that are already uploaded first, oldest first, even before keep_local_days is over.
  • It never deletes a day that is not uploaded yet.
  • Over the cap, or with less than buffer.min_free_mb free on the disk, it refuses new data for the spool (history_spool_refused rises). The live stream is not affected.
  • It skips building a day when the disk does not have room for it, and tries again later (history_disk_low is 1).

Days that cannot be uploaded (the Historian is off, the network is down) stay on disk until they are, so a long outage can fill the cap: values refused meanwhile are missing from the recording, not from the live stream. Watch disk_free_gb, history_days_pending and history_spool_mb in the health stream.

Security

  • Local spool: encrypted with a key made on the collector host. On Windows that key is protected by DPAPI; on Linux and Docker it is a file only the collector account can read, inside state_dir.
  • Day files: sealed with a random history key per source. The collector seals that key under the project key, so only the browsers of project members and the collector itself can open the files. Kymoscope cannot.
  • Key rotation: rotating the project key keeps every stored day readable. If the project key is regenerated, the collector re-registers its history key on its next hourly check.
  • A new collector sending to a project waits for a project member to approve it before it can upload history, just as for the live stream.

Network

An organization can keep its day files in its own S3-compatible bucket instead, in the cloud or on site. See Your own storage.

Day files go straight from the collector to Kymoscope storage, through short-lived upload links. This adds one outbound host to the two on the Site collector page:

Outbound host (port 443)Used for
<account>.eu.r2.cloudflarestorage.comKymoscope storage (Cloudflare R2, EU jurisdiction): uploading day files, and downloading a stored day to merge late data or to verify it. Allow *.eu.r2.cloudflarestorage.com if your firewall or proxy takes wildcards; Kymoscope support gives you the exact host otherwise.

HTTPS only, outbound only. When proxy is set, these requests go through it like the others.

Commands

Shell
# every source that records by itself and the state of each local day
kymoscope-collector history status --config collector.yaml

# upload a day now instead of waiting for midnight (the running collector does it within a minute)
kymoscope-collector history upload --config collector.yaml --source "Customer A DCS" --day 2026-10-20

# download the uploaded day, open it and count the values per channel next to the local copy
kymoscope-collector history verify --config collector.yaml --source "Customer A DCS" --day 2026-10-20

The commands and the health channels below keep the name history. --source is the source's name and --day is the site-local day. history upload only marks the day: the running collector (service, Docker or console) picks it up. history status prints one line per local day:

history status
Customer A DCS:
  2026-10-19: uploaded v1, local copy deleted
  2026-10-20: uploaded v2
  2026-10-21: pending: 2 file(s) left
  2026-10-22: waiting (open)

history verify exits with 1 when the day is not uploaded or does not open, and with 0 otherwise, so a scheduled task can alert on it. The counts it prints let you compare the stored day with the local copy while that copy still exists.

verify-history.ps1 (Windows)
# verify-history.ps1: run daily from Task Scheduler, after the upload (for example at 02:00)
$day = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
& kymoscope-collector history verify --config C:\kymoscope\collector.yaml --source "Customer A DCS" --day $day
if ($LASTEXITCODE -ne 0) {
  Write-EventLog -LogName Application -Source "Kymoscope" -EventId 1001 -EntryType Error `
    -Message "Kymoscope recording day $day is missing or does not open"
  exit 1
}
verify-history.sh (Linux)
#!/bin/sh
# /etc/cron.d/kymoscope-history: 0 2 * * * kymoscope /opt/kymoscope/verify-history.sh
day=$(date -d yesterday +%F)
if ! kymoscope-collector history verify --config /opt/kymoscope/collector.yaml \
     --source "Customer A DCS" --day "$day"; then
  logger -p user.err "Kymoscope recording day $day is missing or does not open"
  exit 1
fi

Health channels

With health.stream set, each source that records by itself adds these channels to the health stream:

ChannelMeaning
<source>.history_days_pendingClosed days not uploaded yet. 0 most of the time; above 1 for long means uploads are failing.
<source>.history_errorsFailed upload attempts since the collector started. The log says why.
<source>.history_needs_e2ee1 while the project is not end-to-end encrypted, so nothing is uploaded.
<source>.history_spool_errorsBlocks that could not be written to the local day spool (a full disk, for example). The live stream is not affected.
<source>.history_spool_mbSize of the local day spool in MB. Compare with recording.max_local_gb.
<source>.history_spool_refusedBlocks refused for the spool because it was over its cap or the disk was below buffer.min_free_mb. The live stream is not affected.
<source>.history_spool_key_resetsSpool keys set aside as unreadable (renamed to spool.key.bad) since start. Days spooled under the old key can no longer be read.
<source>.history_disk_low1 while a day could not be built for lack of free disk space; it is retried later.
<source>.history_storage_waiting1 while new days wait for your organization’s own bucket: not tested yet, or the own-storage contract ended. See Own storage.

Deleting a history

A project lead can delete a source's history in the app. If its collector is still running with that source recording always or by trigger, it creates the history again within an hour and uploads its local days once more. To delete one for good, first set record: manual on the source, restart the collector, then delete the history in the app.

Troubleshooting

The collector log names the source and the day, then the reason. The first two stop the start:

MessageWhat to do
'history' was replaced: every source records always by defaultThe configuration still has a history: section. Move its keys under recording: (see Coming from history:) and start again.
record: always needs pyarrowInstall the history extra: pip install "kymoscope-collector[history]", or set record: manual on that source.
needs_e2eeThe project is not end-to-end encrypted. Move the stream to an encrypted project. Until then each local day is deleted keep_local_days after it ends.
history_not_in_planThe Historian is off for your organization (it comes with Enterprise), or its end date has passed. Ask Kymoscope. Days stay on the local disk and upload once it is on.
channels_quotaThe channels of all your sources that record by themselves exceed what your plan includes. Set record: manual on some, narrow their tag lists, or ask for more channels.
day_expiredThe day is older than its retention, so it is skipped for good. history status shows it as skipped.
tz_lockedrecording.timezone was changed after days were stored. Set it back; a source keeps its zone for life.
sealed with a history key this collector doesn’t holdhistory verify ran on a collector that did not upload that day (a reinstall or another host). The day is fine: run verify on the collector that uploaded it, or open it in the app.
waiting for a project member to approve this senderApprove the collector in the project, as for its live stream. Uploads start at the next check.