Keygrabber#
The keygrabber is a libby daemon that polls keywords from other peers on a configurable cadence and writes them to a time-series database, so Grafana can dashboard an instrument without every daemon growing its own database code.
One process serves the whole fleet. Cadence lives in this daemon’s config rather than in each hardware daemon, adding a metric never restarts a daemon that owns moving hardware, and the database credential lives in one place.
InfluxDB 2.x is the only backend today. It needs the optional extra:
pip install libby[influxdb]
Running it#
keygrabber -c /etc/hispec/keygrabber.yaml
It is an ordinary LibbyDaemon, so SIGTERM stops it cleanly and so does
writing its shutdown keyword. On the way out it gives its retry queue a
bounded chance to drain, so a graceful stop does not lose the last tick.
Config#
A keygrabber config is an ordinary daemon config (peer_id, group_id,
transport, and the transport’s own settings) plus three sections of its own.
peer_id: keygrabber
group_id: hispec
transport: rabbitmq
rabbitmq_url: amqp://localhost
sink:
type: influxdb
url: http://influx.hispec:8086
org: hispec
bucket: telemetry
token_env: HISPEC_INFLUX_TOKEN
workers: 4
defaults:
interval_s: 10.0
timeout_s: 2.0
refresh_s: 300.0
collections:
adc:
peer: hsfei.adc
interval_s: 5.0
keywords: ["positionvalue%", "ismoving", "isconnected"]
pressure:
peer: hsfei.atcpress
interval_s: 60.0
keywords: ["%"]
exclude: ["units_code"]
sink#
type selects the backend; influxdb is the only one implemented.
The token is never written in the config. token_env names an environment
variable to read it from, and a token key in the file is rejected outright.
timeout_ms is optional.
collections#
One entry per peer and cadence; several entries may target the same peer at
different cadences. A collection name must match [a-z0-9_]+, because it
becomes the prefix of that collection’s control keywords.
peeris<group>.<daemon>, the address of one peerkeywordsis a list of names or%patterns to recordexcluderemoves names the includes matchedinterval_s,timeout_sandrefresh_sfall back todefaults
uptime and lasterror are excluded by default: uptime changes every second
and says nothing a timestamp does not, and lasterror is null most of the
time. Naming either one in keywords explicitly opts it back in.
Patterns are resolved against the live peer at startup and again every
refresh_s, so keywords added by a restarted daemon get picked up without
restarting the keygrabber.
Cadence and timeouts#
Config load rejects an interval_s at or below 1.5 x timeout_s. bamboo waits
timeout_s for the acknowledgement and a further timeout_s / 2 for the
reply, so one unanswered read can occupy a worker for one and a half timeouts,
and a tighter interval would be overrun by a single slow peer.
A tick whose predecessor is still running is skipped rather than queued behind it, so a wedged peer cannot accumulate overlapping reads.
A collection that keeps failing reads less often. After three consecutive
failures its interval doubles each time, up to 32x, until a read succeeds.
Without that, a daemon that is down would be retried on its configured cadence
indefinitely, logging an error every time and rewriting lasterror with it.
Logging follows the same shape: the first failures are reported, then the
backoff is announced once, then it stays quiet until the peer answers again.
The cap means a peer that comes back is picked up within a bounded time.
Control keywords#
The keygrabber is itself a peer, so its cadence and health are reachable with
the ordinary libby verbs, and it can feed the same dashboards it fills.
Keyword |
Type |
Access |
Meaning |
|---|---|---|---|
|
bool |
R/W |
Collect on the configured cadences; false pauses without exiting |
|
bool |
R/W |
Last sink write succeeded; write true to request a reconnect |
|
int |
R |
Samples the sink has stored since start |
|
int |
R |
Keyword reads that failed |
|
int |
R |
Sink writes that failed |
|
int |
R |
Batches waiting in the retry queue |
|
int |
R |
Ticks skipped because the previous read was still in flight |
|
int |
R |
Batches discarded because a queue was full |
|
trigger |
W |
Re-read the config file and apply it |
|
trigger |
W |
Gracefully stop the daemon |
|
bool |
R/W |
Collect this one collection |
|
float |
R/W |
Cadence in seconds |
|
string |
R |
UTC time of the last successful tick |
|
float |
R |
Seconds the last tick ran past its due time |
libby show hispec.keygrabber.%
libby modify hispec.keygrabber.adc.interval=30
libby modify hispec.keygrabber.enabled=false
Per-collection keywords contain a dot, and % matches within a single segment
only, so libby list hispec.keygrabber.% will not show adc.enabled. Use
hispec.keygrabber.%.% for those.
isconnected reports whether the last write to the sink succeeded; it does not
ping the database, because these getters are answered on the transport’s
receive thread and a blocking one would time out every read in flight. Writing
true asks the writer thread to reconnect and returns immediately, so poll the
keyword for the outcome. There is no manual disconnect.
A failing sink is visible from three keywords together: isconnected goes
false, queuedepth climbs as batches wait, and pointswritten stops moving.
writeerrors counts every failed attempt including retries.
reload#
reload re-reads the config file and applies it to the running collections,
so keyword selections and cadences can change without a restart. A file that
fails to parse leaves the running collections untouched and reports why, both
to the caller and on lasterror.
It will not add or remove collections, and says so rather than half-applying:
libby has no way to withdraw a keyword, so a new collection’s control keywords
could not appear without a restart. Changing sink or workers also needs a
restart.
How it reads#
A tick is one keys.read request per peer, not one per keyword. This matters
more than the smaller number suggests: a daemon dispatches requests inline on
its receive thread, so reading twenty keywords individually does not overlap
anything on that daemon. It serializes exactly as a batch would, while paying
twenty dispatch cycles instead of one. The saving is contention on a control
daemon’s only dispatch thread.
A peer running a libby without keys.read is read one keyword at a time
instead. That is detected from the services field of keys.list, not by
trying keys.read and seeing what happens: an unknown key is dropped without
an acknowledgement, so a probe cannot tell an old peer from a dead one.
Every value in a tick carries one timestamp, taken by the keygrabber rather than by each daemon, which keeps clock skew between daemon hosts out of the data.
Storage#
Samples reach the backend through a Sink, and nothing in a Sample is
Influx-shaped, so a second backend is a new sink rather than a change to the
collector.
The InfluxDB schema is one measurement per keyword name, tagged with group,
peer and units, with a single value field. A keyword name carries one
type across peers, so field types stay consistent, and a Grafana query is a
measurement plus a peer tag filter.
Three details follow from what Influx can store:
Integers are written as floats, so one peer reporting
0and another0.5for the same keyword cannot collide as int against float and be rejected.A null value is skipped, since Influx has no null field. It is not an error.
A keyword with no units gets a
units=nonetag, because Influx drops an empty tag value and the keyword would otherwise split into two series.
A failed write goes to a bounded retry queue with exponential backoff, which drops its oldest batch when full, so a database that stays down cannot grow the daemon’s memory without limit.
See libby.keygrabber.sink in the API reference for
the sink contract.