Live streams

Collector: REST and webhooks

Poll a REST API (Canary, Proficy presets) or receive webhooks.

REST and webhooks

Two source types read from systems that speak HTTP and JSON: rest asks an API for new values on a schedule (a historian's REST interface, a vendor cloud, an MES), and webhook is a small receiver for systems that prefer to push. Both turn a JSON body into values with the same mapping options, so a payload you can describe once works either way. Neither needs more than read access, and the collector never writes to the system it reads.

Install the extras you use: pip install "kymoscope-collector[rest]" and "kymoscope-collector[webhook]".

REST poller

collector.yaml
sources:
  # Wide: one record per time, one column per tag
  - name: Energy portal
    type: rest
    url: https://portal.example.com/api/v1/readings
    auth: bearer
    token: env:PORTAL_TOKEN
    params: {from: "{since}", to: "{until}", limit: "1000"}
    poll_ms: 60000
    items_path: data[*]
    time_path: timestamp
    values: {Power: kw, Voltage: electrical.volts}

  # Long: one record per value, with the tag name in the record
  - name: MES
    type: rest
    url: https://mes.customer.local/api/samples
    auth: oauth2
    token_url: https://login.customer.local/oauth2/token
    client_id: kymoscope
    client_secret: file:mes-secret.txt
    scope: read
    ca: customer-ca.pem
    method: POST
    body: {from: "{since}", to: "{until}"}
    items_path: samples[*]
    tag_path: tag
    value_path: value
    time_path: ts
    time_format: epoch_ms
    cursor_path: next_cursor
    cursor_param: cursor

Every poll_ms the collector sends one request, follows the pages of the answer, and publishes the values it has not published before. The request is a template: in the URL, in params and in body, {since} is the start of the window, {until} its end, {now} the current time, {tags} the tag names (a list in a JSON body, joined by ; in a URL), and {api_key} / {token} the configured secrets. Times are written as request_time_format (iso, epoch_s or epoch_ms).

NameTypeDescription
urlrequiredstringhttps:// address of the API (a preset appends its own path).
methodGET | POST
Default: GET
For POST the body is sent as JSON.
params, headersmappingQuery parameters and extra headers. Header values accept env:NAME and file:path.
bodymappingJSON request body template.
poll_msnumber
Default: 5000
Interval between requests.
overlap_snumber
Default: 5
Each request starts this long before the last value already published, so a late-arriving value is not missed. Values seen twice are dropped.
lookback_snumber
Default: 0
On the very first request, how far back to start (otherwise one poll interval).
request_time_formatiso | epoch_s | epoch_ms
Default: iso
How {since}, {until} and {now} are written.
timeout_snumber
Default: 30
Per request.
capathA PEM file with the CA that signed the server certificate (a private customer CA).
insecurebool
Default: false
Do not verify the server certificate (a warning is logged at startup). Only for a test system: anyone on the network can then impersonate the server and read the credentials.
allow_httpbool
Default: false
Accept an http:// URL (and an http:// token_url). Data and credentials then travel unencrypted.
max_tagsnumber
Default: 5000
Without a tag list (long form, or every leaf of an answer), at most this many tags are created; values of further tags are dropped and counted in tags_dropped (one warning a minute). Set tags to collect exactly the ones you want.
history_chunk_snumber
Default: 86400
A backfill asks for the range in windows of this length, so an API that caps the points per answer cannot cut a long range short without notice.

Authentication

Set auth to one of the following; secrets accept env:NAME and file:path.

NameTypeDescription
auth: bearertokenSends Authorization: Bearer <token>.
auth: basicuser, passwordHTTP Basic.
auth: api_keyapi_key, api_key_header
Default: X-API-Key
The key in a header of that name.
auth: oauth2token_url, client_id, client_secret, scopeOAuth 2.0 client credentials. The access token is cached until shortly before it expires; a 401 triggers one fresh token.

Pages

Use at most one of these when the answer comes in pieces (up to max_pages, default 1000, per poll):

NameTypeDescription
next_pathpathA path in the body holding the address of the next page (absolute or relative). No value means the last page. An address on another scheme, host or port than url is not followed (paging stops, next_rejected counts it, and it is logged once) so the credentials never go to a third party.
next_link_headerboolFollow the rel="next" address of the Link header, under the same same-host rule.
page_param, page_startstring, number
Default: , 1
A page-number query parameter, increased until a page has no values.
cursor_path, cursor_parampath, stringA continuation token in the body that is sent back in the named query parameter (GET) or body field (POST).

Mapping the answer

NameTypeDescription
items_pathpath
Default: (the body)
Where the records are. Dotted names, [0] and [*]: result.rows[*]. Empty means the body itself (a list is read element by element). A path that names a list reads its elements.
time_pathpathThe record's timestamp. Without it values are stamped when received, and the poller cannot keep a watermark (it then asks for the last poll interval each time and cannot de-duplicate or backfill).
time_formatiso | epoch_s | epoch_ms
Default: iso
iso is ISO 8601 (a time without a zone is read as UTC).
tag_path, value_pathpath
Default: tag, value
Long form: one value per record, the tag name and the value read from each record.
values{tag: path}Wide form: one value per entry in each record, {Power: kw, Volts: electrical.volts}. When set, tag_path and value_path are not used, and these names are the channels.
quality_path, good_qualitypath, listWhen both are set, a value whose quality is not in the list becomes a gap (a Bad status) instead of a number.
tagslistOnly collect these tag names. Without it the long form collects every tag that appears (new ones are added as they show up) and the wide form collects the keys of values.

Inside a record a path starting with ^ reads from the enclosing element, which is how a nested answer such as {data: [{tagName, samples: [{t, v}]}]} is read: items_path: data[*].samples[*], tag_path: ^.tagName, time_path: t, value_path: v. Text values and records without a readable number are skipped.

Rate limits and errors

A 429 answer (or 503 with a Retry-After) is waited out for the time the server asks, up to five minutes, then retried. Any other failure is logged, counted in poll_errors and retried with a growing pause up to a minute. Nothing is lost meanwhile: the watermark does not move, so the next successful request covers the gap. The watermark and the list of recent values are kept in the collector's state folder, and are saved only once the values up to them have been sent (or are in the stream's disk buffer), so a restart continues where it stopped without duplicates and without a gap. The previous good copy is kept as a .bak file: if the state file is unreadable the collector logs an error and continues from the backup.

discover lists the configured tags (or the keys of values). backfill sends the same request for the window you give it ({since} and {until} are its start and end) and needs a time_path. The collector's proxy setting applies to these requests.

Presets for Canary and GE Proficy Historian

preset fills in the request and the mapping for a historian's REST API; you give the server url, credentials and the tags to read. Any option you set yourself overrides the preset.

collector.yaml
sources:
  - name: Canary
    type: rest
    preset: canary
    url: https://canary.customer.local:55236
    api_key: env:CANARY_TOKEN
    tags: ["Customer.Reactor1.Temp", "Customer.Reactor1.Pressure"]
    ca: customer-ca.pem

  - name: Proficy
    type: rest
    preset: proficy
    url: https://historian.customer.local:8443
    auth: oauth2
    token_url: https://historian.customer.local:8443/uaa/oauth/token
    client_id: kymoscope
    client_secret: env:PROFICY_SECRET
    tags: ["Reactor1.Temp"]

canary posts to the Web API's /api/v2/getTagData2 with the API token in the body and follows its continuation token. proficy reads /historian-rest-api/v1/datapoints/raw/<tags>/<start>/<end>/0/0, with the OAuth 2 token of the Proficy authorization service.

Webhook receiver

collector.yaml
sources:
  - name: Vendor push
    type: webhook
    bind: 127.0.0.1:8099        # inbound port, see the warning below
    path: /ingest
    hmac_secret: env:WEBHOOK_HMAC
    hmac_header: X-Signature
    items_path: events[*]
    tag_path: sensor
    value_path: reading
    time_path: at
    time_format: epoch_s
NameTypeDescription
bindhost:port
Default: 127.0.0.1:8099
Address and port to listen on.
pathstring
Default: /
The URL path that accepts POST requests. Anything else answers 404.
secret, secret_headerstring, string
Default: , X-Webhook-Secret
A shared secret the sender puts in that header.
hmac_secret, hmac_headerstring, string
Default: , X-Signature
The sender signs the raw body with HMAC-SHA256 and sends the hex digest (optionally prefixed sha256=). Preferred: the secret never travels and a changed body is detected. Add hmac_timestamp_header so a captured request cannot be replayed.
tls_cert, tls_keypathPEM files; the receiver then speaks HTTPS (TLS 1.2 or later).
max_body_kbnumber
Default: 1024
Larger bodies are refused with 413 without being read in full.
hmac_timestamp_header, max_skew_sstring, number
Default: , 300
Recommended with hmac_secret (replay protection). The sender puts the Unix time in seconds in that header (for example X-Kymoscope-Timestamp) and signs timestamp + "." + body; a request whose time is more than max_skew_s from the collector's clock, or without it, is refused with 401. Keep both clocks in sync (NTP).
allow_httpbool
Default: false
Required to listen on a network address without tls_cert: the secret and the data would then travel unencrypted. Without it such a configuration is refused.
max_tagsnumber
Default: 5000
Same as for rest: tags beyond this many are dropped and counted in tags_dropped.

One of secret or hmac_secret is required; a receiver without authentication is not accepted by the configuration check. The mapping options above (items_path, tag_path, value_path, time_path, values, quality_path, tags) read the JSON body. A request is answered right after it is checked and parsed: 202 accepted, 400 not JSON, 401 failed authentication, 413 too large, 503 with Retry-After if the queue (up to 100,000 values, counted in values, not requests) is full: the sender should retry.202 means accepted, not durable: the values are published from a queue, so they are lost if the collector stops before it is drained. A sender that must not lose data keeps its own copy until it can verify the data arrived.

Signing a request
# The sender signs the raw body: hex( HMAC-SHA256(secret, body) ), optionally prefixed with "sha256="
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST http://127.0.0.1:8099/ingest -H "X-Signature: sha256=$SIG" -d "$BODY"

# With hmac_timestamp_header: X-Kymoscope-Timestamp, sign "<timestamp>.<body>" (recommended)
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST http://127.0.0.1:8099/ingest -H "X-Kymoscope-Timestamp: $TS" -H "X-Signature: sha256=$SIG" -d "$BODY"

Health

Health stream
rest:     requests, http_2xx, http_3xx, http_4xx, http_429, http_5xx, token_requests, rate_limited, next_rejected, tags_dropped,
          polls, poll_errors, overruns, last_poll_s, values_per_s, last_watermark_age_s
webhook:  requests, rejected (failed authentication), bad_requests (too large or not JSON),
          queue_full, queue (values waiting), tags_dropped, values_per_s

last_watermark_age_s is how old the newest published value is; a number that keeps growing while poll_errors stays at 0 means the API answers but has no new data.

Ask the site for

  • A read-only API account, token or OAuth client for the system (the collector never writes).
  • REST: outbound HTTPS from the collector to the API host and port, and the CA certificate if the server uses a private CA.
  • Webhook: the sender's address and an approved firewall rule for the receiver's port; see the warning above.