signalk-questdb 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1237 +1,206 @@
1
1
  # signalk-questdb
2
2
 
3
- QuestDB history provider for Signal K — a drop-in replacement for
4
- signalk-to-influxdb and signalk-to-influxdb2.
5
-
6
- Records every vessel's data in [QuestDB](https://questdb.com), running as a
7
- container managed for you by
8
- [signalk-container](https://github.com/dirkwa/signalk-container), and serves it
9
- back through Signal K's v2 History API and the legacy v1 playback API. Existing
10
- InfluxDB history can be imported.
11
-
12
- ## Features
13
-
14
- - **Managed QuestDB** — the container is pulled, created, updated and
15
- resource-capped for you; or point the plugin at an external QuestDB
16
- - **v2 History API** — `getValues`, `getPaths`, `getContexts` with every
17
- aggregate method, per-source reads (`path|sourceRef`) and per-source splitting
18
- (`sourcePolicy=all`)
19
- - **v1 playback API** — WebSocket replay at configurable speed
20
- - **Recording controls** — per-path sampling rates, include/exclude filters with
21
- globs, own vessel and AIS targets separately, retention by days, LZ4/ZSTD
22
- on-disk compression
23
- - **Archive past retention** — retention drops old days, but the own track, and
24
- any own-vessel paths you name such as depth, are kept, thinned, in an archive
25
- the History API reads seamlessly
26
- - **GPX track import** — a chartplotter's track export of any size goes into
27
- the archive; the file is read in the browser first, so you pick the own
28
- vessel's tracks before anything is uploaded
29
- - **Startup restore** — AIS targets back on the chart right after a server
30
- restart, in their ship-type colour
31
- - **WAL watchdog** — a table QuestDB has silently stopped applying is detected,
32
- alerted, and resumed
33
- - **InfluxDB migration** — detect InfluxDB 1.x/2.x, pick a bucket, a vessel and
34
- a range, import with original timestamps; resumable
35
- - **Export** — Parquet or CSV by date range, and a per-table full export for
36
- backup tooling
37
- - **Console webapp** — QuestDB's own SQL console inside the Signal K admin UI,
38
- admin only
39
- - **Config panel** — status, row counts, one-click QuestDB updates, and every
40
- setting in one place
3
+ Keeps the history of your boat's data. The plugin records what Signal K sees,
4
+ from your own instruments and from the AIS targets around you, in a
5
+ [QuestDB](https://questdb.com) database. Apps that draw tracks and graphs get it
6
+ back through Signal K's History API, and Grafana can read it too.
7
+
8
+ It replaces signalk-to-influxdb and signalk-to-influxdb2, and imports the
9
+ history they recorded.
10
+
11
+ - **Looks after itself.** QuestDB runs in a container that the plugin sets up,
12
+ updates and keeps healthy.
13
+ - **Small on disk.** A value that changes is written at most every 2 seconds, a
14
+ value that stays the same only every 5 minutes.
15
+ - **Keeps your track.** Old data can be deleted after a number of days, while
16
+ your own track is kept for good.
17
+ - **AIS back at once.** After a restart, the vessels around you are on the chart
18
+ right away instead of trickling in over minutes.
19
+ - **Imports** history from InfluxDB, and tracks from a chartplotter as GPX.
20
+ - **Exports** to Parquet or CSV.
21
+ - **SQL console**: QuestDB's own console inside the Signal K admin UI.
41
22
 
42
23
  ## Getting started
43
24
 
44
25
  ### Requirements
45
26
 
46
- - Node.js 24 or newer
47
- - QuestDB 8.1 or newer (resolution queries use `SAMPLE BY … FROM … TO`). This
48
- applies to an external QuestDB and to a pinned **QuestDB image version** in
49
- managed mode; `latest` always qualifies
50
- - Signal K server — 2.31 or newer to choose the default history provider in the
51
- admin UI
52
- - For managed mode: the
53
- [signalk-container](https://github.com/dirkwa/signalk-container) plugin, and
54
- Podman 5.4 or newer (what Debian 13 "trixie" ships) or Docker.
55
- signalk-container 1.29.0 or newer is recommended; the plugin runs with 1.14.0
56
- or newer, but older versions lose parts of it — below 1.19.0 the Danger zone
57
- cannot delete data kept on a named volume, below 1.26 the macOS open-files
58
- problem shows as a generic error, below 1.29.0 a container the runtime refuses
59
- to recreate is not reported (see Troubleshooting). On rootless Podman below
60
- 5.5 the plugin's open-files request is inherited from the podman service
61
- rather than granted per container
62
- ([containers/podman#25881](https://github.com/containers/podman/issues/25881));
63
- signalk-container accounts for this.
64
-
65
- ### Install
66
-
67
- 1. Install **signalk-container** from the App Store and enable it (skip this for
68
- an external QuestDB).
69
- 2. Install **signalk-questdb** and enable it. On the first start the plugin
70
- pulls `questdb/questdb` at the tag chosen under **QuestDB image version**
71
- (`latest` by default), creates the container — named `signalk-questdb` behind
72
- the runtime's prefix, `sk-` unless configured otherwise — with its ports
73
- bound to loopback, and starts recording. The data lives under Signal K's
74
- configuration directory, in `plugin-config-data/signalk-questdb` (see Data
75
- storage).
76
- 3. Open the plugin's config panel: the status card shows **Running** and
77
- **Recording**, the total row count and the paths active today.
78
-
79
- For an external QuestDB, switch **Manage QuestDB container** off and enter its
80
- host and ports.
81
-
82
- ### Make it the history provider
83
-
84
- Which registered provider answers `/signalk/v2/api/history/` is the operator's
85
- choice: pick it once under **Data → Preferences → Default History Provider**
86
- (server 2.31 or newer); the server persists it as `historyApi.defaultProvider`
87
- in `settings.json`, and it survives restarts and plugin load order. If no
88
- default is configured, the server uses whichever provider registers first.
89
- Versions 2.0.0 and earlier asked the server to make QuestDB the default on every
90
- start; that is gone as of 2.0.1 — it could silently override a default you had
91
- chosen, and on servers with security enabled it never worked at all.
92
-
93
- ## Configuration
94
-
95
- The config panel groups the settings as: the **status card** (running state,
96
- rows, active paths, restore outcome, WAL state), the **update check** and
97
- **image version** picker (latest, pre-releases and the last three stable
98
- releases), **Connection**, **Recording**, **History**, and the collapsible
99
- **Path filtering**, **Compression (on-disk)**, **InfluxDB Migration**, **Data
100
- Export** and **Danger zone** sections. The Danger zone removes the container and
101
- all of its data — including what Signal K's plugin uninstall cannot reach on
102
- rootless Podman.
103
-
104
- | Setting | Default | Description |
105
- | ----------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
106
- | QuestDB image version | `latest` | Image tag; the dropdown lists the latest release, pre-releases, and the last three stable releases |
107
- | Manage QuestDB container | `true` | Let signalk-container run QuestDB; off = connect to an external QuestDB |
108
- | QuestDB host | `127.0.0.1` | External mode only; in managed mode the address is resolved automatically |
109
- | HTTP port | `9000` | External mode, or the host binding when "Bind to 0.0.0.0" is on; otherwise signalk-container allocates it |
110
- | ILP port | `9009` | External mode, or the host binding when "Bind to 0.0.0.0" is on; otherwise signalk-container allocates it |
111
- | PostgreSQL port | `8812` | Host binding for Grafana/psql when "Bind to 0.0.0.0" is on |
112
- | QuestDB memory limit | `768m` | Hard cgroup cap on the container (empty = unlimited); the JVM heap gets half of it, so heap plus off-heap stays inside |
113
- | QuestDB CPU limit (cores) | `1.5` | Fractional cores (0 = unlimited) |
114
- | Default sampling rate (ms) | `2000` | Minimum ms between writes for any one path (0 = every update) |
115
- | Per-path sampling rates (ms) | _(empty)_ | Glob → ms overrides, one `pattern ms` per line in the panel (0 = wire speed, see Sampling) |
116
- | Unchanged-value heartbeat (s) | `300` | A value equal to the last one recorded is written again only this often; changes are written at the sampling rate (0 = every sample, see Change-only recording) |
117
- | Write batch interval (ms) | `5000` | How often buffered samples are committed — one WAL transaction per table per commit (see Performance) |
118
- | Record own vessel | `true` | Record the `self` context |
119
- | Record AIS targets | `true` | Record other vessels |
120
- | Record all sources | `false` | Record every source of a path, not only the one the source priorities prefer (see Source priorities) |
121
- | Restore vessels on startup | `false` | Replay each vessel's last recorded position after a restart (see Startup restore) |
122
- | Restore max age (minutes) | `9` | Only replay values recorded within this window |
123
- | Allow `sourcePolicy=all` | `false` | Let History API callers split a path into one column per recording source (see History API) |
124
- | QuestDB console webapp | `true` | Serve QuestDB's console inside the Signal K admin UI, admin only (see Console webapp) |
125
- | Retention (days) | `0` | Drop daily partitions older than this (0 = keep forever) |
126
- | Keep own track | `true` | Archive the own vessel's positions before retention drops them (see Retention and the archive) |
127
- | Archived own-vessel paths | _(empty)_ | Glob patterns of own-vessel numeric paths to archive the same way, e.g. `environment.depth.*` |
128
- | Archive resolution (s) | `10` | Archived rows are thinned to the first sample per interval (0 = every row) |
129
- | Filter mode | `exclude` | `exclude` the matching paths, or `include` only the matching paths |
130
- | Path patterns | _(empty)_ | Glob patterns, one per line, e.g. `environment.wind.*`; empty records everything |
131
- | Compression codec | `lz4` | On-disk WAL segments and Parquet exports: `none`, `lz4` (fast) or `zstd` (smaller) |
132
- | Compression level | `3` | ZSTD level 1–22, used only with `zstd` |
133
- | Container network | `sk-network` | Shared Podman/Docker network QuestDB joins in managed mode, so the companion signalk-grafana reaches it by container DNS; host-port publication is separate ("Bind to 0.0.0.0") |
134
- | Bind to 0.0.0.0 | `false` | Publish QuestDB's ports on all interfaces (see Connectivity) |
135
-
136
- ### QuestDB updates
137
-
138
- The panel's **update check** compares the running QuestDB with its latest stable
139
- release. **Update** pulls that release, recreates the container on it and saves
140
- it as the image version.
141
-
142
- After the plugin itself was updated, and at its very first start, the plugin
143
- also checks once on its own, in the background. It does so only in managed mode,
144
- and only where the image version is a tag that moves, such as the default
145
- `latest`. If QuestDB runs an older release than the latest stable one, the
146
- plugin pulls the tag again and recreates the container on it. The image version
147
- stays `latest`. Without this check, QuestDB on `latest` would stay on whatever
148
- release the first install pulled, because signalk-container pulls an image only
149
- when it is missing. An image version that names one release, such as `9.4.3`,
150
- stays as it is, and so does one that **Update** saved while the check was
151
- waiting. QuestDB is down while it starts again, as with **Update**. Progress
152
- goes to the debug log, and failures go to the server log. A recreate that fails
153
- half way and leaves QuestDB stopped or without a container is followed by a
154
- start, as the plugin's own start would do. Only if that fails too does the
155
- status line say so.
156
-
157
- With [signalk-internet](https://github.com/meri-imperiumi/signalk-internet)
158
- enabled, the check waits for a free link. On a metered, captive or offline link
159
- it asks again from time to time, for as long as the plugin runs. Without
160
- signalk-internet, the link counts as free. Telling the two apart needs
161
- signalk-container 1.38.0 or later; with an older one, the check does not run. A
162
- failure to read the running or the latest version is also tried again later.
163
-
164
- The check is done once QuestDB runs the latest release, or once its container
165
- runs the image the pull brought, even if QuestDB still reports an older release
166
- afterwards. It then waits for the next plugin update. The next start checks
167
- again wherever the container keeps its old image, or where the new one cannot
168
- be told from the old:
169
-
170
- - the plugin stopped first;
171
- - the download failed, or something else failed before the recreate;
172
- - an InfluxDB import, a GPX import or an export was running;
173
- - the string table was being rebuilt;
174
- - the container was stopped, which the check leaves as it is;
175
- - the container was wedged;
176
- - the tag on Docker Hub did not name the latest release yet.
177
-
178
- A tag that never reaches the latest release, such as `9` while the latest is
179
- `10.1.0`, therefore costs a pull and a look at GitHub at every start.
180
-
181
- ## Recording
182
-
183
- ### Schema
184
-
185
- Three tables, all with WAL mode, daily partitioning, and deduplication:
186
-
187
- | Table | Purpose | Columns |
188
- | ------------------ | -------------- | ---------------------------------------------------------------------------------------------------------- |
189
- | `signalk` | Numeric values | `ts`, `path` (SYMBOL), `context` (SYMBOL), `source` (SYMBOL), `value` (DOUBLE) |
190
- | `signalk_str` | String values | `ts`, `path` (SYMBOL), `context` (SYMBOL), `source` (SYMBOL), `value_str` (VARCHAR), `value_kind` (SYMBOL) |
191
- | `signalk_position` | Positions | `ts`, `context` (SYMBOL), `source` (SYMBOL), `lat` (DOUBLE), `lon` (DOUBLE) |
192
-
193
- What retention keeps of the own vessel lives in two more tables,
194
- `signalk_archive` and `signalk_position_archive`, with the same columns and
195
- monthly partitions (see Retention and the archive).
196
-
197
- The own vessel is stored as context `self`, whatever its identity; other vessels
198
- keep their `vessels.urn:…` context. The history API translates `self` back to
199
- `vessels.self`.
200
-
201
- `ts` is the **server receive time**, not the timestamp a source claims. Marine
202
- sources carry independent clocks, and storing their timestamps makes commits
203
- land out of order — QuestDB then rewrites partition tails on every merge
204
- (observed as >3000x write amplification). Receive time keeps ingestion
205
- append-only; the millisecond difference is far below the sampling resolution,
206
- and a device with a broken clock gets more accurate history, not less.
207
-
208
- `source` is the delta's sourceRef — which receiver produced the row. Two GPS
209
- units feeding the same server interleave in storage, and without the column a
210
- track drawn from history zigzags between them. Rows recorded before the column
211
- existed have `source` null; they replay unattributed and cannot be filtered.
212
-
213
- ### What is recorded
214
-
215
- Every value on the stream is routed by its type: numbers go to `signalk`,
216
- strings and booleans to `signalk_str` (booleans tagged `value_kind=boolean`),
217
- and `navigation.position` — that path only — to `signalk_position`. A vessel's
218
- name, which arrives as an empty-path `{name}` delta, is stored in `signalk_str`
219
- under the path `name` with `value_kind=identity`, and replayed in its original
220
- shape. An object value is recorded as its scalar leaves, one level deep:
221
- `navigation.attitude {roll, pitch, yaw}` becomes `navigation.attitude.roll` and
222
- so on, and another lat/lon object such as `navigation.anchor.position` becomes
223
- `.latitude` and `.longitude` leaves rather than rows in the track table. Nested
224
- objects — notifications, resource documents — are not descended into; a value no
225
- table can hold is skipped and reported in the status endpoint and the panel
226
- rather than dropped silently.
227
-
228
- A number that means "no reading" is skipped and reported the same way: `NaN`
229
- and `±Infinity`, and the NMEA 2000 "no data" marker on AC real, apparent and
230
- reactive power (PGNs 65007–65029). Those fields cannot encode anything below
231
- −2 000 000 000 W, but canboatjs decodes "no data" as −2 000 000 001 W, so real
232
- and reactive power below that floor are dropped; real negative power, such as
233
- export, is unaffected. Apparent power reaches the stream multiplied by the power
234
- factor, which can move the marker anywhere below zero — and apparent power is
235
- never negative, so any negative apparent power is dropped. Connections using the
236
- canboat WASM decoder do not produce the marker at all.
237
-
238
- **Sampling.** Each path is written at most once per **Default sampling rate**
239
- (2000 ms), with per-path overrides by glob for the few paths that need finer
240
- resolution and the many that need less. The first value after a window opens
241
- is written and the rest of the window is dropped, not averaged, so a gust
242
- between two samples is not in the history. A rate of 0, as default or per
243
- path, removes the window: `environment.wind.* 0` records every change of the
244
- wind at wire speed while everything else stays at the default. An unchanged
245
- value is still written only once per **Unchanged-value heartbeat** (see
246
- Change-only recording), except `navigation.position`, which is written even
247
- when unchanged; set the heartbeat to 0 as well to record every update. Wire
248
- speed costs rows: a wind sensor at 10 Hz writes about 1.7 million a day for
249
- angle and speed, and every path of a busy NMEA 2000 boat at wire speed can be
250
- a few hundred rows a second. **Record own vessel** and **Record AIS targets**
251
- switch the two kinds of context independently, and **Path patterns** exclude
252
- (or include only) paths by glob.
253
-
254
- **Source priorities.** When Signal K's source priorities rank the sources of a
255
- path, only the preferred source is recorded, the same value instruments show.
256
- Paths without a priority are recorded from every source. **Record all sources**
257
- records the sources a priority hides as well, for calibrating a sensor or
258
- comparing two GPS receivers. Each source is then sampled on its own, so a path
259
- with three sources writes up to three times the rows; with the option off, the
260
- sources of a path share one sampling window.
261
-
262
- **Change-only recording.** Most paths on a boat do not change from one sample
263
- to the next: a switch that flips twice a day, an energy counter at rest, a tank
264
- nobody is drawing from, a notification's state. Sampling alone would write each
265
- of them at the sampling rate forever, and those repeats make up most of the
266
- rows. So a value equal to the last one written for its path, vessel and source
267
- is skipped until the **Unchanged-value heartbeat** is due, and then
268
- written once more. Every change is written as it arrives, subject to the
269
- sampling rate.
270
-
271
- The heartbeat is what keeps the gaps readable. A path that is still being
272
- updated has a row at least once per heartbeat, so a longer silence means the
273
- data really stopped:
274
-
275
- - **History API (v2).** A resolution bucket with no row takes the last sample
276
- before it, for up to twice the heartbeat (see History API). A switch that has
277
- been on for an hour reads as on for the whole hour, not as one point and
278
- then nothing.
279
- - **Stale data.** When Signal K marks a path stale it sends `null` for it.
280
- Nothing is written for the `null`, so the history shows the last value for at
281
- most twice the heartbeat and then a gap. The next reading after that is
282
- always written, even if it equals the one before the gap.
283
- - **Direct SQL and Grafana** see the rows as stored. Add `FILL(PREV)` to a
284
- `SAMPLE BY` query to carry values across buckets.
285
- - **Averages** weight rows, not time. A bucket in which a value changes often
286
- and then holds steady averages mostly over the changes; `min`, `max`, `first`
287
- and `last` are exact.
288
- - **Startup restore** replays motion values recorded within **Restore max
289
- age**. Keep the heartbeat below that, or a vessel lying still may come back
290
- with its position but without its speed, course and heading.
291
-
292
- `navigation.position` is written even when unchanged. A heartbeat of 0 does the
293
- same for every path: each sample the sampling rate lets through is written. With
294
- a sampling rate of 0 as well, that is every update.
295
-
296
- **Batching.** Samples are buffered and committed every **Write batch interval**
297
- (5 s), one WAL transaction per table per commit. The buffer holds 100 000 lines;
298
- if QuestDB is unreachable for longer than that covers, the oldest lines are
299
- dropped and counted. A clean Signal K shutdown flushes first; a hard crash loses
300
- at most one interval.
301
-
302
- **Compression** applies LZ4 or ZSTD to the WAL segments on disk (and to
303
- Parquet exports).
304
-
305
- ### Retention and the archive
306
-
307
- Retention drops whole daily partitions once they lie entirely before the
308
- configured number of days; 0 keeps everything. QuestDB cannot delete single
309
- rows, so keeping part of a day means copying it out first. Before a day of
310
- `signalk_position` or `signalk` is dropped, the own vessel's rows that the
311
- settings ask to keep are copied into an archive table, thinned to one row per
312
- **Archive resolution** interval, and the day is dropped only once QuestDB has
313
- applied the copy. A copy that fails leaves the day in place for the next run.
314
-
315
- | Table | Holds |
316
- | -------------------------- | --------------------------------------------------------------- |
317
- | `signalk_position_archive` | Own-vessel positions (**Keep own track**, on by default) |
318
- | `signalk_archive` | Own-vessel numeric paths matching **Archived own-vessel paths** |
319
-
320
- Both have the columns and dedup keys of their base table and are partitioned by
321
- month. Retention never drops from them, other vessels are never archived, and
322
- strings are not archived. The History API reads the archive together with the
323
- base tables for any range it covers, so a track or a depth series continues
324
- past the retention boundary without a seam. An archived row is the first sample
325
- of its interval and is stamped on the interval's start.
27
+ - Signal K server 2.31 or newer, on Node.js 24 or newer
28
+ - The [signalk-container](https://github.com/dirkwa/signalk-container) plugin,
29
+ with Podman 5.4 or newer (what Debian 13 "trixie" ships) or Docker. Not needed
30
+ if you run QuestDB yourself.
326
31
 
327
- Size, measured: a position row takes 32 bytes, so a vessel recorded around the
328
- clock fills about 100 MB a year at a 10 s resolution, 34 MB at 30 s.
32
+ Which signalk-container version brings what is in
33
+ [Deployment](doc/deployment.md#requirements).
329
34
 
330
- ## History API
331
-
332
- ### v2 (REST — `/signalk/v2/api/history/`)
333
-
334
- Registered via `app.registerHistoryApiProvider()`. Supports all aggregate
335
- methods:
336
-
337
- | Method | QuestDB mapping |
338
- | -------------- | ------------------------------------------------------------------------------- |
339
- | `average` | `avg(value)` |
340
- | `min` | `min(value)` |
341
- | `max` | `max(value)` |
342
- | `first` | `first(value)` |
343
- | `last` | `last(value)` |
344
- | `mid` | `(min + max) / 2` |
345
- | `middle_index` | the middle row of each bucket, by time — a recorded value, not a computed one |
346
- | `sma` | the `average` bucket, then a moving window over N buckets (`sma:N`, default 5) |
347
- | `ema` | the `average` bucket, then an exponential moving average (`ema:α`, default 0.2) |
348
-
349
- A sample for `sma` and `ema` is one row of the series the window runs over: a
350
- resolution bucket (its average) when the request names a resolution, a raw row
351
- otherwise. So `sma:5` at `resolution=180` is a 15-minute moving average, on the
352
- same grid as every other column in the response. An empty bucket keeps its place
353
- in the window: the average there is of what the window still holds, and a value
354
- leaves it N buckets after it arrived. `ema` carries its last value across an
355
- empty bucket instead.
356
-
357
- Buckets start at the request's `from` and cover the whole range, empty ones
358
- included. A bucket with no row is not always missing data. Change-only recording (see
359
- Recording) writes an unchanged value only once per heartbeat, so an empty bucket
360
- takes the last sample before it — in every aggregate, since every aggregate of a
361
- steady value is that value — for up to twice the heartbeat after the sample was
362
- taken, and never into a bucket that has not started yet. At the start of a
363
- range the sample comes from just before it. Past that window, and with the
364
- heartbeat set to 0, an empty bucket is `null`. This happens before `sma` and
365
- `ema`, so a held value counts as a sample. `middle_index` and
366
- `navigation.position` return recorded rows only.
367
-
368
- A path recorded in radians — a heading, a course, a wind angle; `units: rad` in
369
- the server's metadata for the path — is averaged as a vector: `average`, `sma`
370
- and `ema` take the angle of the mean sine and cosine, so 359° and 1° average to
371
- 0°, not 180°. The result keeps the path's convention, `[0, 2π)` for a heading or
372
- `(−π, π]` for an apparent wind angle; samples that cancel, 0° and 180° in equal
373
- measure, have no mean direction and give `null`. `min`, `max` and `mid` stay
374
- arithmetic.
375
-
376
- `navigation.position` answers `first`, `last` and `middle_index` — each a point
377
- the vessel was at. Any other method runs `first`, and the column's `method` says
378
- so. A text path (rows in `signalk_str`) answers `first`, `last` and
379
- `middle_index`. With a resolution, `average`, `min`, `max` and `mid` run `last`,
380
- the state in force at the bucket's end, and the column's `method` says so.
381
- `sma` and `ema` read the numeric table only, so a text path has no values for
382
- them.
35
+ ### Install
383
36
 
384
- A method name outside the table above fails the request with an error naming
385
- the path and the method.
37
+ 1. Install **signalk-container** from the App Store and enable it.
38
+ 2. Install **signalk-questdb** and enable it. On its first start it downloads
39
+ QuestDB, which can take a few minutes on a slow connection, starts it and
40
+ begins recording.
41
+ 3. Open the plugin's config panel. The status card shows **Running** and
42
+ **Recording**.
386
43
 
387
- Query example:
44
+ ![The config panel with its status card](doc/screenshots/signalk-questdb-1.png)
388
45
 
389
- ```http
390
- GET /signalk/v2/api/history/values?paths=navigation.speedOverGround&duration=PT1H&resolution=60
391
- ```
46
+ To use a QuestDB you run yourself, switch **Manage QuestDB container** off and
47
+ enter its host and ports.
392
48
 
393
- A `navigation.position` value is a `[longitude, latitude]` pair — GeoJSON order,
394
- as the History API defines it and as signalk-to-influxdb2 and signalk-parquet
395
- return it — not the `{latitude, longitude}` object of the data model; a bucket
396
- without a fix is `null`. The v1 playback API is unaffected: it replays deltas,
397
- which carry the object.
398
-
399
- **Limits.** A request may produce at most 1 000 000 sample buckets across all of
400
- its columns (range ÷ resolution × columns); a finer one is rejected with an
401
- error that says so — use a coarser resolution or a shorter range. Without a
402
- resolution a column is its raw rows, capped at 10 000 (50 000 for
403
- `middle_index`).
49
+ ### Make it the history provider
404
50
 
405
- #### Reading one source
51
+ Apps ask Signal K for history, and Signal K asks its default history provider.
52
+ Choose this plugin once under **Data → Preferences → Default History
53
+ Provider**; Signal K remembers the choice.
406
54
 
407
- Append `|<sourceRef>` to a path to read one source's rows only (server 2.29+,
408
- [signalk-server#2737](https://github.com/SignalK/signalk-server/pull/2737)). The
409
- same path may appear once per source, giving one column per receiver:
55
+ ## Everyday settings
410
56
 
411
- ```http
412
- GET /signalk/v2/api/history/values?paths=navigation.position|gps.main,navigation.position|gps.backup&duration=PT1H
413
- ```
57
+ Every setting is in the plugin's config panel. These are the ones you are most
58
+ likely to change; all of them are in [Configuration](doc/configuration.md).
414
59
 
415
- Without a sourceRef a path returns all sources mixed. A source-bearing column
416
- reports its source as `$source` in its `values` entry — the key
417
- [signalk-server#2817](https://github.com/SignalK/signalk-server/pull/2817)
418
- settled on, and what v1 playback has always emitted — while the request side
419
- keeps the name `sourceRef`.
60
+ ### How often is data recorded?
420
61
 
421
- #### sourcePolicy=all
62
+ A value that changes is written at most once per **Default sampling rate**,
63
+ 2 seconds by default. A value that stays the same is written again only once
64
+ per **Unchanged-value heartbeat**, 5 minutes by default, which keeps the
65
+ database small. Your position is the exception: it is written every 2 seconds
66
+ even while the boat lies still.
422
67
 
423
- `sourcePolicy=all`
424
- ([signalk-server#2817](https://github.com/SignalK/signalk-server/pull/2817))
425
- asks for every source separated without naming them up front. Each path that
426
- does not already specify a source is expanded into one column per source that
427
- actually recorded it in range, with the source in the `$source` field of the
428
- column's `values` entry:
68
+ ### How do I record the wind at full speed?
429
69
 
430
- ```http
431
- GET /signalk/v2/api/history/values?paths=navigation.speedOverGround&duration=PT1H&sourcePolicy=all
432
- ```
70
+ Add a line to **Per-path sampling rates**: a path pattern, then the rate in
71
+ milliseconds. A rate of 0 records every change:
433
72
 
434
- ```jsonc
435
- "values": [
436
- { "path": "navigation.speedOverGround", "method": "average", "$source": "gps.aux" },
437
- { "path": "navigation.speedOverGround", "method": "average", "$source": "gps.main" },
438
- { "path": "navigation.speedOverGround", "method": "average" } // unattributed rows
439
- ]
73
+ ```text
74
+ environment.wind.* 0
440
75
  ```
441
76
 
442
- Named sources come first, sorted, so column order is stable between requests.
443
- Rows whose source is unset — recorded before the `source` column existed, or
444
- from a delta that carried none — form their own trailing column with no
445
- `$source` claim.
446
-
447
- **Off by default**, behind the plugin setting that allows the policy. Expansion
448
- multiplies the work one request can ask for: a path recorded by four receivers
449
- becomes four queries and four columns, which on a Pi-class host turns a cheap
450
- request into an expensive one. The sample-bucket cap counts expanded columns, so
451
- a resolution that is fine unexpanded may be rejected once expanded.
452
-
453
- Two ceilings bound the fan-out, and a request past either is rejected rather
454
- than truncated — returning some of the asked-for series without saying so would
455
- be worse than refusing a request that is too broad. A single path may expand
456
- into at most 16 columns: recorded by more sources in range, it fails the
457
- request, and the error names the count, the ceiling and the way out (name the
458
- sources with `path|sourceRef`, or ask for a shorter range). A whole request is
459
- bounded at 64 columns, with its own error naming the limit.
460
-
461
- An explicit `path|sourceRef` stays a filter and takes precedence over the
462
- policy, matching the upstream contract. Requests that name their sources are
463
- unaffected by the setting either way.
464
-
465
- ### v1 (WebSocket playback)
466
-
467
- Registered via `app.registerHistoryProvider()`. Supports playback at
468
- configurable speed multipliers using chunked reads from QuestDB. Replayed
469
- updates carry the recorded sourceRef as `$source`, one update per source, so
470
- consumers see the same attribution the live stream had.
471
-
472
- ## Console webapp
473
-
474
- QuestDB ships its own web console — a full SQL workbench with schema browsing,
475
- query history and CSV import. With **QuestDB console webapp** enabled (the
476
- default) the plugin appears in the Signal K webapp list and serves that console
477
- inside the admin UI, with Signal K's own navigation panel still in place. The
478
- **[Web Console user guide](doc/web-console.md)** walks through it with
479
- ready-to-paste queries: speed in knots, temperatures in °C, distance per day,
480
- track export, and more.
481
-
482
- **It is admin only.** The console is proxied at
483
- `/plugins/signalk-questdb/console/`, and Signal K gives plugin routes an
484
- admin-only default — the plugin deliberately does not relax that. A non-admin
485
- who opens the webapp sees the page frame and an authorization error, never the
486
- console.
487
-
488
- **It can modify data.** Unlike the read-only `/query` endpoint, which rejects
489
- anything that is not a `SELECT`, the console is a real SQL client: it can drop
490
- tables and delete recorded history. That is the point of having it, and it is
491
- what an administrator would use it for — but it is worth knowing before handing
492
- someone an admin account.
493
-
494
- If you would rather not serve it at all, switch the option off and the route is
495
- not registered.
496
-
497
- The console is also reachable directly on QuestDB's HTTP port. On a bare-metal
498
- Signal K with "Bind to 0.0.0.0" off, signalk-container binds it to loopback at
499
- the address the status card shows (the port is the one it bound, not necessarily
500
- 9000); from another machine, tunnel that port over SSH. It is 9000 on the host
501
- only with "Bind to 0.0.0.0" on. In managed mode a containerized Signal K reaches
502
- QuestDB over the container network, so there is no host port to open unless
503
- "Bind to 0.0.0.0" is on; in external mode the console is at the configured
504
- QuestDB host and HTTP port, whatever the deployment. The HTTP port carries
505
- QuestDB's REST API and ingestion too, with no authentication; enabling "Bind to
506
- 0.0.0.0" to reach it from the LAN publishes all of that, which is what the
507
- webapp route exists to avoid.
508
-
509
- ## Startup restore
510
-
511
- Signal K's data model lives in memory only. After a server restart every vessel
512
- is gone until it transmits again — for AIS that means roughly 30 seconds for a
513
- Class B target, up to 3 minutes for a Class A at anchor, and about 6 minutes
514
- before names arrive. The chart fills in gradually instead of showing the traffic
515
- that was there a moment ago.
516
-
517
- With **Restore vessels on startup** enabled, the plugin replays each vessel's
518
- last recorded position (plus course, speed, heading and identity — name,
519
- dimensions and AIS ship type) from QuestDB as soon as it connects, so the chart
520
- is populated immediately, with each target drawn in its ship-type colour rather
521
- than the default.
522
-
523
- **A restored position is where a vessel _was_, not where it is now.** Nothing is
524
- dead-reckoned forward. The plugin presents these values as the history they are
525
- rather than passing them off as live: each delta carries its original recorded
526
- timestamp, so a chart plotter ages it out under the same staleness rules it
527
- applies to any other target, and the deltas are tagged
528
- `$source: signalk-questdb.restore`.
529
-
530
- Values outside the **Restore max age** window are not replayed at all. The
531
- 9-minute default matches Freeboard's AIS expiry, so a restored target is one
532
- that would still have been on the chart had the server never stopped. Raising it
533
- puts progressively staler positions in front of you — treat the window as a
534
- collision-avoidance setting, not a convenience one.
535
-
536
- Only navigation and identity paths are replayed, so stale tank levels and engine
537
- temperatures are not resurrected as though they were current readings. A vessel
538
- with no position in the window is skipped. Nothing the Signal K model already
539
- holds is replaced: a vessel heard while QuestDB was still starting keeps its
540
- newer position, and after a plugin restart every vessel keeps the one it has.
541
- Restore honours the recording toggles: with **Record AIS targets** off, no AIS
542
- target is restored.
543
-
544
- The config panel reports the outcome once startup finishes: **Vessels Restored**
545
- with the count, or **Restore failed** if the replay could not run. Neither
546
- appears when the option is off.
547
-
548
- ## Migrating from InfluxDB
549
-
550
- The **InfluxDB Migration** section of the config panel copies history out of an
551
- existing InfluxDB into QuestDB. It supports InfluxDB 1.x (InfluxQL, what
552
- signalk-to-influxdb wrote) and 2.x (Flux, what signalk-to-influxdb2 writes).
553
-
554
- ### Running an import
555
-
556
- 1. **Detect** finds an InfluxDB on `localhost:8086`, or enter its URL —
557
- `localhost` or a loopback/private-network IP literal; anything else is
558
- refused.
559
- 2. Supply credentials — an API token and organisation for 2.x, or
560
- username/password for 1.x if authentication is enabled — and list the buckets
561
- (2.x) or databases (1.x). Credentials are used for the import only, are never
562
- written to the plugin's settings, and travel in the request body rather than
563
- the query string (Signal K logs full request URLs).
564
- 3. Pick a bucket. The panel then lists the vessels it holds (**Listing the
565
- vessels in the bucket…** — on a large bucket this takes a while) and shows
566
- **Which vessel in the source is this one**: that vessel's history is imported
567
- as `self`. The choice is made for you when the source says:
568
- signalk-to-influxdb2 tags its own vessel's points with `self=true`, so that
569
- vessel is preselected and marked "(recorded as the own vessel)" — as is the
570
- one matching this server's identity, marked "(this server)", or the only one
571
- there is. A signalk-to-influxdb 1.x source carries no such tag, so there the
572
- choice is yours whenever it holds several vessels and none is this server's
573
- identity — the usual case, since the recording server's identity is usually
574
- not this one's (a new install has a new one). Tick **Also import the other
575
- vessels (AIS targets), each under its own context** to bring those along; the
576
- history API serves them as it does live-recorded targets.
577
- 4. Pick a time range and **Start import**. Progress is polled while it runs;
578
- **Cancel** stops it, keeping the saved position (see Resuming).
579
-
580
- After the import, check the row counts on the status card or in the console
581
- (`SELECT count() FROM signalk WHERE source = 'influxdb-import'`), make sure this
582
- plugin is the default history provider (see Getting started), and disable the
583
- InfluxDB plugin once nothing else needs it — a consumer such as a track viewer
584
- reads whichever provider is the default, not both.
585
-
586
- ### How the data maps
587
-
588
- - A measurement with the conventional `value` field becomes the Signal K path of
589
- the same name. So do the typed fields signalk-to-influxdb 1.x writes
590
- (`stringValue`, `boolValue`, `jsonValue`). A measurement with any other named
591
- fields becomes `measurement.field` paths, so two fields cannot overwrite each
592
- other.
593
- - Numbers go to `signalk`, strings and booleans to `signalk_str` (booleans
594
- tagged `value_kind=boolean`). A `jsonValue` is decoded first: an object is
595
- recorded as its scalar leaves, the same as live data.
596
- - Positions go to `signalk_position`, whether stored as a `jsonValue`
597
- (signalk-to-influxdb 1.x) or as a `lat`/`lon` or `latitude`/`longitude` field
598
- pair (signalk-to-influxdb2).
599
- - Both writers tag every point with the **context** of the vessel it belongs to;
600
- the vessel you picked is imported as `self`, the others under their own
601
- contexts or left out.
602
- - Rows keep their **original timestamps**, to the microsecond (InfluxDB stores
603
- nanoseconds; QuestDB's `TIMESTAMP` holds microseconds), so imported history
604
- sorts and aggregates alongside live data.
605
- - History is **streamed**: read in batches and written before more is asked for,
606
- pausing while QuestDB catches up. Memory use stays flat however dense the
607
- source is, so a large import is safe on a small board.
608
- - Every imported row is tagged `source=influxdb-import`, which makes it
609
- distinguishable from live recording — and because the tables deduplicate on
610
- `(ts, path, context, source)`, **re-running the same range overwrites rather
611
- than duplicating**.
612
- - Anything that cannot be mapped (a gap, an unsupported value type, a
613
- `jsonValue` that is not valid JSON, a latitude with no matching longitude) is
614
- counted in the run's `skipped` total rather than being dropped silently.
615
-
616
- ### Resuming an interrupted import
617
-
618
- A large import runs for hours. If it stops part-way — cancelled, failed, or the
619
- Signal K server restarted — the panel shows what was interrupted and offers
620
- **Resume import**, which continues from the saved position instead of starting
621
- again at the first measurement. It needs only the credentials, if InfluxDB asks
622
- for any: the source and range are remembered, credentials never are. Starting
623
- the identical import again the ordinary way continues it too. **Discard saved
624
- position** starts over.
625
-
626
- A position is saved only once QuestDB has been asked for the newest row written
627
- before it and has it — ILP gives no acknowledgment, so the plugin reads the row
628
- back — and once the position is about a minute old. The first keeps a stalled or
629
- restarted QuestDB from letting the position past rows it never took, however
630
- long the stall. The minute covers what a committed row still has to survive: a
631
- crash of either process, and on a power cut the kernel's default write-back
632
- timing for a QuestDB running on its default `nosync` — an assumption, not a
633
- guarantee, which is why an external QuestDB should run with `sync` like the
634
- managed container does (see Durability on power loss). The position file is
635
- synced and renamed into place, so a power cut leaves the previous position or
636
- the new one, never a damaged one; should none survive, the import starts again,
637
- which is always safe. A position is a window boundary — the import reads each
638
- measurement a window at a time, a day of source data by default — so a resumed
639
- import repeats the window it was in plus up to a minute, which the deduplication
640
- above makes harmless. An import shorter than a window saves nothing, and is
641
- simply run again.
642
-
643
- ### Imports made before 2.1.5
644
-
645
- Imports from a signalk-to-influxdb 1.x database made before 2.1.5 filed every
646
- string, boolean and position under a path suffixed with the InfluxDB field name
647
- — `navigation.state.stringValue`, `steering.autopilot.engaged.boolValue`,
648
- `navigation.position.jsonValue` — which nothing reads. Running the import again
649
- writes them under their real paths but cannot remove the old rows: the path is
650
- part of the deduplication key, and QuestDB has no row delete. When such rows
651
- exist, the panel says how many and offers **Remove these rows**, which rebuilds
652
- `signalk_str` without them. Recording continues while it runs; only the swap of
653
- the rebuilt table for the old one holds the writer, for a few seconds, and rows
654
- recorded during the rebuild are carried across. Should recording be fast enough
655
- to fill the writer's buffer during those seconds, the samples it could not keep
656
- are counted and the panel says how many. Run the import again afterwards.
77
+ Everything else stays at the default. A value that repeats is still written
78
+ only once per heartbeat. To record repeats too, set the heartbeat to 0; that
79
+ applies to every path, each of which then writes at its sampling rate even when
80
+ nothing changes.
657
81
 
658
- ## Importing a GPX track
82
+ Full speed costs space and needs a capable machine: a wind sensor at 10 Hz
83
+ writes about 1.7 million rows a day for angle and speed. Use it for the few
84
+ paths that need it.
659
85
 
660
- A chartplotter's exported track goes into the position archive (see Retention
661
- and the archive), where retention never drops it, under the source
662
- `import.gpx`. An export can be gigabytes, a four-year TimeZero export is
663
- 1.5 GB, so the file is sent in slices and parsed as it arrives. Memory stays at
664
- one slice, whatever the file's size.
86
+ ### How long is data kept?
665
87
 
666
- In the configuration panel, **GPX track import** reads the chosen file in the
667
- browser first, which takes seconds even for a gigabyte, and lists its tracks by
668
- name with their points and period. It proposes the own vessel's tracks: the
669
- largest group whose name is not an AIS target's, and any other name that
670
- recurs. It flags a group that lay more than a kilometre from a larger one at
671
- the same time, since that was another vessel. Nothing is uploaded until
672
- **Import** is pressed; the import then shows its progress and can be aborted.
88
+ Forever, unless you set **Retention (days)**; older days are then deleted. Your
89
+ own track is kept anyway, thinned to one position every 10 seconds (**Keep own
90
+ track** and **Archive resolution**). That is about 100 MB for a year of
91
+ recording around the clock. To keep other paths of your own boat the same way,
92
+ list them under **Archived own-vessel paths**, for example
93
+ `environment.depth.*`.
673
94
 
674
- - **Which tracks.** An export mixes the own track with AIS targets and with
675
- passages imported into the plotter from elsewhere, and GPX does not say whose
676
- a track is. The import takes the names of the tracks to import (`tracks`);
677
- every other track is skipped and listed in the summary. In a TimeZero export
678
- the own track is named `Own Ship`, or the plotter's translation of it, such
679
- as `Eigenes Schiff`.
680
- - **Thinning.** Points are thinned to the **Archive resolution**: each is
681
- stamped on the start of its interval, and the archive keeps one row per
682
- interval. 11.5 million points over four years become 3.2 million rows,
683
- about 100 MB, at 10 s.
684
- - **Overlap.** With `fillGapsOnly` (the default), a point is left out where the
685
- vessel already has recorded positions in the same five minutes. Two
686
- receivers' fixes interleaved in one series draw a sawtooth.
687
- - **Doing it again.** Importing the same file again changes nothing, and a
688
- stopped import can simply be restarted: the archive's dedup keys absorb the
689
- rows already written.
690
- - **Positions only.** The import takes a position and a time from each point;
691
- depth, temperature or speed in a point's extensions are not imported. Depth
692
- along the track beyond retention comes from live recording: list
693
- `environment.depth.*` under **Archived own-vessel paths**, and retention keeps
694
- it from then on.
695
- - **Strict reading.** Coordinates must be decimals within range and times
696
- `xsd:dateTime`; anything else is counted as invalid. A longitude written just
697
- past ±180, as TimeZero does at the antimeridian, is wrapped. A file that is
698
- not well-formed XML stops the import at the error, keeping the rows before
699
- it.
95
+ ### AIS targets are gone after a restart
700
96
 
701
- ## Export and backup
97
+ Signal K keeps live data in memory only, so after a restart the chart stays
98
+ empty until each vessel transmits again, which can take minutes. Switch on
99
+ **Restore vessels on startup**: the vessels recorded in the last 9 minutes
100
+ (**Restore max age**) come back at once, with name and ship type. They keep the
101
+ time they were recorded at, so the chart plotter ages them out like any other
102
+ target.
702
103
 
703
- **Data Export** in the panel, and
704
- `GET /plugins/signalk-questdb/api/export?from=…&to=…&format=parquet|csv`,
705
- download the `signalk` numeric table for a date range (both bounds required) as
706
- Parquet — QuestDB's native export, compressed with the configured codec — or
707
- CSV.
104
+ ### Recording less
708
105
 
709
- `GET /plugins/signalk-questdb/api/full-export/:table?from=…&to=…` streams one
710
- whole table as Parquet, for snapshot and backup tooling that wants the full
711
- content sliced into dedup-friendly shards (kopia, for instance). Tables:
712
- `signalk`, `signalk_str`, `signalk_position`, `signalk_archive`, `signalk_position_archive`
713
- (`/full-export/tables` lists them). The archive tables hold the own vessel's
714
- history beyond retention and every imported track, so a backup that is meant to
715
- outlive retention needs them too.
106
+ **Record own vessel** and **Record AIS targets** switch the two off separately.
107
+ Under **Path filtering**, patterns such as `propulsion.*` leave out the paths
108
+ they match, or, in `include` mode, record only those.
716
109
 
717
- - `from` and `to` are optional but must be set together: omit both for the whole
718
- table, or pass both as ISO 8601 timestamps for a half-open `[from, to)` window
719
- — no row appears in two adjacent windows.
720
- - Repeated query parameters (`?from=A&from=B`) and empty strings (`?from=`) are
721
- rejected with HTTP 400; silently downgrading to a full-table export would hide
722
- bugs in the caller.
723
- - Format and compression follow the plugin's compression setting (LZ4_RAW or
724
- ZSTD), the same as `/export`.
110
+ ### Comparing two sensors
725
111
 
726
- ## REST endpoints
112
+ When Signal K's source priorities rank the sources of a path, only the preferred
113
+ one is recorded, the value your instruments show. Switch on **Record all
114
+ sources** to record the others too, for example to calibrate a sensor or to
115
+ compare two GPS receivers. Each source then writes its own rows.
727
116
 
728
- All under `/plugins/signalk-questdb/api/`; the console is mounted beside them at
729
- `/plugins/signalk-questdb/console/`. Plugin routes are admin-only by Signal K's
730
- default.
117
+ ## Common tasks
731
118
 
732
- | Method | Path | Description |
733
- | ------ | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
734
- | GET | `/status` | QuestDB health, row counts, active paths, restore outcome, WAL state, unstorable paths |
735
- | GET | `/query?sql=...` | Read-only SQL proxy (DDL/DML blocked) |
736
- | GET | `/paths` | All recorded paths with row counts and time range |
737
- | GET | `/versions` | QuestDB releases from GitHub (for the version picker) |
738
- | GET | `/update/check` | Compare the running version against the latest release |
739
- | POST | `/update/apply` | Pull the latest image, recreate the container, reconnect |
740
- | GET | `/wal-diagnosis` | Suspended tables with the engine's reason and a bounded skip plan (see WAL suspension) |
741
- | POST | `/resume-wal` | Lossless `RESUME WAL` of the suspended tables |
742
- | POST | `/resume-wal/skip` | Guided skip of the unreadable segment — explicit, quantified, and only on request |
743
- | POST | `/purge-data` | Remove the QuestDB container and delete all its data (rootless-Podman-safe) |
744
- | GET | `/migration/detect` | Detect InfluxDB (supports `?url=` for a remote one) |
745
- | POST | `/migration/buckets` | List buckets (2.x) or databases (1.x); credentials in the body, never the query string |
746
- | POST | `/migration/measurements` | List measurements and their field keys in a bucket/database |
747
- | POST | `/migration/contexts` | List the vessels a bucket/database holds, and which is this server's |
748
- | POST | `/migration/start` | Start an import, or continue a stopped one with `{"resume": true}`; returns immediately |
749
- | GET | `/migration/status` | Progress and state of the current/last import, and any stopped import that can resume |
750
- | POST | `/migration/cancel` | Cancel the running import; any saved position is kept |
751
- | POST | `/migration/discard` | Forget a stopped import's saved position, so the next start begins again |
752
- | GET | `/migration/legacy-rows` | Count rows an import made before 2.1.5 filed under suffixed paths |
753
- | POST | `/migration/legacy-rows/remove` | Rebuild the string table without those rows; recording continues meanwhile |
754
- | GET | `/export?from=...&to=...&format=parquet` | Parquet or CSV export of the `signalk` numeric table (date range required) |
755
- | GET | `/full-export/tables` | List the tables the full-export route serves |
756
- | GET | `/full-export/:table?from=...&to=...` | Stream a table as Parquet; optional half-open `[from, to)` window for slicing into shards |
757
- | GET | `/gpx-import` | The current or last GPX import |
758
- | POST | `/gpx-import` | Announce an import: `{filename, size, tracks, fillGapsOnly}`, see Importing a GPX track |
759
- | PUT | `/gpx-import/:id?offset=N` | One slice of the file, as `application/octet-stream`, at most 8 MiB |
760
- | POST | `/gpx-import/:id/finish` | End the import once every byte is in; answers when QuestDB has applied it |
761
- | DELETE | `/gpx-import/:id` | Abort the import; what it wrote stays |
119
+ ### Import history from InfluxDB
762
120
 
763
- ## Deployment
121
+ In the config panel, open **InfluxDB Migration**:
764
122
 
765
- ### Connectivity
123
+ 1. **Detect** finds an InfluxDB on this machine, or enter its address.
124
+ 2. Enter its credentials, if it needs any, and pick the bucket (InfluxDB 2.x)
125
+ or database (1.x).
126
+ 3. Check which vessel in it is your own boat. Tick the box below to import the
127
+ AIS targets as well.
128
+ 4. Pick a time range and **Start import**.
766
129
 
767
- In **managed mode** the plugin does not need `QuestDB host` to be correct for
768
- your deployment — signalk-container resolves the right address automatically,
769
- whether Signal K runs on bare metal or is itself containerized:
130
+ A large import runs for hours. If it stops, **Resume import** carries on where
131
+ it left off. Afterwards, make sure this plugin is the default history provider,
132
+ and switch the InfluxDB plugin off once nothing else needs it. More in
133
+ [Import and export](doc/import-export.md).
770
134
 
771
- - **Bind to 0.0.0.0 = off (default).** QuestDB stays private. signalk-container
772
- binds its ports to the host loopback (bare-metal Signal K) or attaches QuestDB
773
- to Signal K's own container network (containerized Signal K), and the plugin
774
- connects to whatever address it reports back. Nothing is exposed to the
775
- network. QuestDB is also attached to the shared `Container network` so the
776
- companion [signalk-grafana](https://github.com/dirkwa/signalk-grafana) plugin
777
- still reaches it by container DNS. This is the recommended setup and fixes
778
- connectivity for Signal K in a container.
779
- - **Bind to 0.0.0.0 = on.** QuestDB stays attached to the shared
780
- `Container network`; the setting publishes its HTTP/ILP/PostgreSQL ports on
781
- all host interfaces, using the configured port numbers. Enable this only to
782
- reach QuestDB from another machine or from a Grafana running in a separate
783
- Docker instance. When Signal K itself is containerized, the plugin probes
784
- which address reaches the published ports — its own loopback first, then the
785
- runtime's `host.containers.internal` gateway — and uses the one QuestDB
786
- answers on.
135
+ ### Import a track from a chartplotter
787
136
 
788
- In **external mode** (`Manage QuestDB container` off) the plugin connects to the
789
- QuestDB you point it at via `QuestDB host` and the HTTP/ILP ports.
137
+ Export the track as GPX on the plotter. In the config panel, **GPX track
138
+ import** reads the file and lists its tracks, with the ones that look like your
139
+ own boat already ticked. Check them and press **Import**. Imported tracks are
140
+ kept for good, whatever the retention.
790
141
 
791
- ### Data storage
142
+ ### Export data
792
143
 
793
- QuestDB's data root is the plugin's own data directory,
794
- `<config>/plugin-config-data/signalk-questdb`, whatever the deployment. Data
795
- survives container restarts, image upgrades, and plugin disable/enable cycles.
796
- How it reaches the container differs:
144
+ **Data Export** in the config panel downloads the numbers recorded in a date
145
+ range as Parquet or CSV. For backups of every table, see
146
+ [Export and backup](doc/import-export.md#export-and-backup).
797
147
 
798
- - **Bare-metal Signal K**: that directory, mounted at `/var/lib/questdb`.
799
- - **Signal K in a container with its config directory bind-mounted**: the exact
800
- host path of that directory, mounted at `/var/lib/questdb`.
801
- - **Signal K in a container with its config directory on a named volume**: the
802
- volume cannot be mounted from a subdirectory, so it is mounted whole at
803
- `/signalk-vols/<volume>` and QuestDB is pointed at the plugin's directory
804
- inside it (`QUESTDB_DATA_DIR`). A database found at the volume's root — where
805
- a whole-volume mount put it — is moved into the plugin's directory on start;
806
- both are the same volume, so nothing is copied. A purge deletes the directory
807
- from the Signal K process, since the runtime cannot mount the volume by Signal
808
- K's path; where QuestDB's user owns it and the Signal K user cannot delete it,
809
- the purge says so and the directory is deleted by hand.
148
+ ### Look at the data with SQL
810
149
 
811
- QuestDB's entrypoint makes its data root its own on start, so the plugin keeps
812
- the one file it writes alongside — the import checkpoint,
813
- `signalk-questdb.influx-import-checkpoint.json` — next to the directory rather
814
- than in it. A purge removes it, whether or not the data could be deleted.
815
-
816
- ### Durability on power loss
817
-
818
- The managed container runs QuestDB with `cairo.commit.mode=sync`.
819
-
820
- QuestDB's default is `nosync`: nothing on the ingest path is ever fsynced, and
821
- durability is left to the OS page cache. On a power cut an arbitrary subset of
822
- recent writes survives — including the case where a commit record reaches disk
823
- while the row data it describes does not. QuestDB then fails while _opening_
824
- that table's partition, which suspends the table permanently: neither a lossless
825
- `RESUME WAL` nor a bounded `RESUME WAL FROM TXN` skip can get past it, because
826
- the failure happens before any transaction is read. Repair means hand-patching
827
- the commit record.
828
-
829
- With `sync`, partition columns are fsynced before the commit record is written
830
- and WAL segments are fsynced on commit, so the stored state can never claim data
831
- that isn't durable. The cost is a few fsyncs per commit interval — writes are
832
- already batched (see **Write batch interval**), so at boat data rates it is
833
- negligible against losing a table.
834
-
835
- **Running an external QuestDB?** Set this yourself in `server.conf`:
836
-
837
- ```ini
838
- cairo.commit.mode=sync
839
- ```
840
-
841
- ### WAL suspension
842
-
843
- QuestDB ingestion is two-stage: the sequencer durably commits every batch (the
844
- writer sees success), then a background job applies the transactions to the
845
- partitions. When that job hits an error it **suspends the table** — the
846
- sequencer keeps accepting rows, nothing is applied, queries serve stale data,
847
- and no client-visible error is raised anywhere. Left alone, recording stalls
848
- silently for as long as nobody looks.
849
-
850
- The plugin watches for this. A suspended table of its own (in external mode,
851
- only the three tables above and the two archive tables — never someone else's)
852
- is logged loudly, raised as
853
- the Signal K notification `notifications.signalk-questdb.walSuspended`, and
854
- shown on the status card as **WAL suspended**. Two causes need two remedies:
855
-
856
- - **Transient** — disk full since cleared, out of memory, file-descriptor
857
- exhaustion: a plain `RESUME WAL` replays every pending transaction,
858
- losslessly. The plugin does this by itself, once per stall point; the panel's
859
- **Resume** button does the same on request, and the banner clears as the
860
- backlog drains.
861
- - **Unreadable segment** — an unclean container stop mid-write corrupts the
862
- in-flight segment's files, and every apply attempt fails on them within
863
- milliseconds, so a plain resume re-suspends at once, forever. The only way
864
- forward is `RESUME WAL FROM TXN` past the unreadable data, which loses what it
865
- skips. The plugin prepares that but never applies it on its own:
866
- **/wal-diagnosis** scrapes the engine log for the real reason (the
867
- `wal_tables()` error fields often carry none, and a container recreate wipes
868
- the engine's memory of it) and computes the minimal skip — the first
869
- transaction of the segment after the stuck one — with the number of
870
- transactions and the wall-clock window of ingestion it would drop. The panel
871
- shows those numbers and offers **Skip**; if the stuck segment is the newest
872
- one, skipping drops the entire pending backlog, and the panel says so.
873
-
874
- ### Out of memory or not answering
875
-
876
- QuestDB caps the native memory it tracks below the container's memory limit
877
- and fails any allocation past the cap with `global RSS memory limit exceeded`.
878
- When something holds memory at the cap, queries fail, the WAL apply job then
879
- suspends the tables, and in the end QuestDB stops accepting HTTP connections. The JVM stays up throughout, so the
880
- container's restart policy never acts.
881
-
882
- In managed mode the plugin checks for this every minute while it runs, and
883
- restarts the QuestDB container when:
884
-
885
- - QuestDB recently reported its global memory limit, and over consecutive
886
- checks its native memory is still held at that limit, it fails even the
887
- health check with it, or it has stopped answering. A single large query
888
- frees what it held when it ends, so it does not count, nor does a client that
889
- keeps asking for one.
890
- - the container is running but QuestDB has answered nothing for several
891
- minutes, over consecutive checks. A QuestDB busy with a long statement still
892
- answers it in the end, and no check counts while a parquet export streams
893
- from it.
894
-
895
- Before the restart the plugin writes QuestDB's message and its last error lines
896
- to the server log, shows the reason on the status card, and raises the Signal K
897
- notification `notifications.signalk-questdb.watchdog`. After the restart it
898
- logs the outcome. Once QuestDB answers again the notification returns to normal;
899
- when it answered right after the restart, the notification keeps the time and
900
- the reason. Restarts are rate limited, spaced apart and capped per few hours of
901
- the plugin running; past that the plugin logs that the restart is held back and
902
- keeps the alert up. A restart that fails, because stopping or starting the
903
- container failed, counts against none of these limits: the plugin tries again a
904
- few minutes later, since the container may be left stopped. It reports the
905
- first failure; while the retries keep failing, they go to the debug log only.
906
-
907
- The plugin leaves a container alone if someone else stopped it, and starts one
908
- that its own restart left stopped. One exception: a container stopped by hand
909
- while a stop of the plugin's is still pending, or right after a restart of the
910
- plugin's before the plugin has seen it running again, may be started again too.
911
- While a stop of the plugin's hangs, it sends no further stop. An external
912
- QuestDB is never restarted. A
913
- QuestDB that is already stuck when the plugin starts, as after a Signal K
914
- restart while it was stuck, is not covered: restarting the plugin stops the
915
- container and starts it afresh.
916
-
917
- The managed container runs with QuestDB's query caches off: history reads put
918
- their time range into the SQL, so the caches never got a hit, and on older
919
- QuestDB releases they held memory.
920
-
921
- ### Performance (Pi and other low-power hosts)
922
-
923
- - **Default sampling rate** of 2000 ms limits each path to one write per two
924
- seconds, keeping write volume modest on busy NMEA 2000 buses; per-path
925
- overrides allow faster rates, or wire speed with 0, for critical paths while
926
- slow-changing ones stay throttled.
927
- - **Change-only recording** skips a value that has not changed since it was last
928
- written until the heartbeat is due. Paths that hold steady — which is
929
- most of them — cost one row per heartbeat instead of one per sampling
930
- interval, which cuts both disk use and WAL apply work (see Recording).
931
- - **Resource caps** of 768 MB RAM and 1.5 CPU cores (cgroup limits via
932
- signalk-container) keep QuestDB from squeezing co-resident containers like
933
- Grafana, mayara, or signalk-backup. The JVM heap gets half of the memory
934
- cap, so total footprint (heap plus off-heap) is bounded, and a JVM that runs
935
- out of heap exits so the container restarts instead of hanging. Set the
936
- memory limit to empty or the CPU limit to `0` to lift the cap on roomier
937
- hosts.
938
- - **Worker tuning** cuts idle CPU. QuestDB sizes its worker pools from the host
939
- core count and busy-polls before sleeping — sensible on a server, wasteful on
940
- a mostly idle boat database. The plugin pins its worker pools down and quiets
941
- QuestDB's per-commit logging, keeping the messages that matter. One caveat
942
- when upgrading the QuestDB image: an unrecognised setting is ignored silently,
943
- so a property renamed between releases looks exactly like one that worked.
944
- - **Batching.** Each commit is one WAL transaction per table, and with
945
- deduplicated tables the apply cost of a transaction grows with partition size
946
- — frequent tiny commits eventually outpace what a Pi can apply and recording
947
- stalls. Bigger batches keep the WAL healthy; the cost is that at most one
948
- interval of buffered samples is lost on a hard crash.
949
-
950
- The console's Monitoring view flags any table whose 90th-percentile WAL
951
- transaction stays under QuestDB's recommended batch size (100 rows) as **"Small
952
- transactions — consider batching"**. `signalk_position` triggers this
953
- structurally: it holds exactly one path, so its share of every commit is just
954
- the handful of GPS fixes since the last one, while the other tables spread
955
- hundreds of paths across each transaction. At vessel data rates the alert is
956
- cosmetic; the numbers that matter are on the same page — **Write Amplification**
957
- near 1x, **Pending Rows** 0 and **Transaction Lag** 0 mean the WAL apply is
958
- healthy. Clearing the alert for the position table would need roughly 100 fixes
959
- per commit, a batch window in the minutes; the trade-off is up to one interval
960
- of buffered data lost on a hard crash, and history that is that much less live.
150
+ Open the admin UI, go to **Webapps** and pick **signalk-questdb**: that is
151
+ QuestDB's own console. Only admins can open it, and it can delete data, so
152
+ stick to `SELECT`. The [Web Console guide](doc/web-console.md) has queries to
153
+ paste, such as speed in knots or distance per day.
961
154
 
962
155
  ### Grafana
963
156
 
964
- Connect Grafana to QuestDB through the PostgreSQL data source (user `admin`,
965
- password `quest`, database `qdb`).
966
-
967
- - The companion [signalk-grafana](https://github.com/dirkwa/signalk-grafana)
968
- plugin wires this up for you: it runs Grafana as a managed container and
969
- reaches QuestDB by container DNS on the shared `sk-network`, so no port needs
970
- publishing.
971
- - A Grafana **on the Signal K host** can reach the PostgreSQL port without
972
- binding: with "Bind to 0.0.0.0" off, signalk-container binds it to loopback at
973
- a host port of its choosing — `podman port <container>` shows which (the
974
- container is `signalk-questdb` behind the `sk-` prefix). With "Bind to
975
- 0.0.0.0" on it is the configured port, 8812 by default.
976
- - A Grafana in a **separate Docker instance or on another machine** needs "Bind
977
- to 0.0.0.0" and the host's LAN address (`192.168.0.122:8812`, say). That
978
- publishes QuestDB's ports to your entire network; only enable it behind a
979
- firewall you trust.
980
-
981
- Example query:
982
-
983
- ```sql
984
- SELECT ts AS time, avg(value) AS sog
985
- FROM signalk
986
- WHERE path = 'navigation.speedOverGround'
987
- AND context = 'self'
988
- AND ts BETWEEN $__timeFrom() AND $__timeTo()
989
- SAMPLE BY $__interval
990
- ```
991
-
992
- ## Troubleshooting
993
-
994
- ### 'Module "signalk-questdb" is not available' right after install or update
995
-
996
- Right after installing or updating the plugin, the admin UI can show
997
- `Module "signalk-questdb" is not available. Make sure the webapp is installed.`
998
- It is installed — the browser tab is still running the admin UI it loaded before
999
- the update, which looks for the panel bundle it knew then. A hard refresh of the
1000
- tab (Shift-Reload) fixes it.
1001
-
1002
- ### "Cannot apply a change: the QuestDB container cannot be stopped or removed"
1003
-
1004
- The plugin needed to recreate the QuestDB container to apply a change — a new
1005
- image tag, a network or resource setting, a raised file-descriptor limit — but
1006
- the container runtime refused to stop or remove the running container. The
1007
- recreate is **deferred, not abandoned**: QuestDB keeps running and keeps
1008
- recording, on its **previous** configuration. The plugin reports the error
1009
- rather than a "Recording" status, because the change you asked for has not been
1010
- applied and reporting success would be a lie.
1011
-
1012
- The usual cause on rootless Podman is an orphaned user namespace left behind
1013
- when the Podman service restarted underneath a running container. The error text
1014
- carries the specific remedy; the general one is to clear the stale namespace and
1015
- remove the container so the plugin can recreate it:
1016
-
1017
- ```bash
1018
- podman system migrate # re-attaches orphaned user namespaces
1019
- podman ps -a | grep questdb # find the managed container's name
1020
- podman rm -f <name-from-above>
1021
- ```
1022
-
1023
- Run all three as the **same OS user that owns the container**. Rootless and
1024
- rootful Podman keep separate container stores, so running them as root (or as
1025
- another user) will not find, and will not remove, a rootless container.
1026
-
1027
- Look the name up rather than assuming it: the plugin's container is
1028
- `signalk-questdb` behind a prefix that defaults to `sk-` but is configurable
1029
- (`SIGNALK_CONTAINER_NAMESPACE`), so it is not always `sk-signalk-questdb`.
1030
-
1031
- Then disable and re-enable the plugin, or restart Signal K. The plugin recreates
1032
- the container with the settings that were pending. Recorded data lives under
1033
- Signal K's data directory, not inside the container (see Data storage), so
1034
- removing the container does not touch it.
1035
-
1036
- **Do not** mask `podman.socket` to work around this. That socket is how the
1037
- plugin reaches Podman at all, so masking it turns a deferred recreate into a
1038
- container the plugin cannot manage in any way. (`podman.service` is the unit the
1039
- socket activates, and masking that has the same effect.) If the socket itself is
1040
- unhealthy, restart it rather than masking it:
1041
-
1042
- ```bash
1043
- systemctl --user restart podman.socket
1044
- ```
1045
-
1046
- This error is reported with signalk-container 1.29.0 or later. On older versions
1047
- the wedge still happens, but appears only as a repeating drift message in the
1048
- Signal K server log with no plugin error to go with it.
1049
-
1050
- ### "QuestDB keeps dropping the write connection — the container may be unhealthy or out of memory"
1051
-
1052
- This status appears when the plugin's ILP writer connects to QuestDB, gets
1053
- dropped within a few seconds, and retries — repeatedly. It is QuestDB itself
1054
- being unhealthy (typically OOM-killed and restart-looping), not a problem with
1055
- the plugin or with a single table. The most common cause on a Raspberry Pi or
1056
- other low-RAM host is that QuestDB is hitting a memory ceiling. There are two
1057
- variants, and the cgroup check below tells them apart.
1058
-
1059
- **1. Is the cgroup `memory` controller delegated?**
1060
-
1061
- ```bash
1062
- cat /sys/fs/cgroup/cgroup.controllers
1063
- # ...memory... present -> memory delegation is available, so the cap can be enforced (see B)
1064
- # memory ABSENT -> the cap is silently dropped (see A)
1065
- ```
1066
-
1067
- When Signal K's containers run under rootless Podman, the kernel only enforces a
1068
- resource limit whose cgroup controller has been delegated to the user session.
1069
- Many distributions delegate `cpu`, `cpuset`, `io`, and `pids` by default but
1070
- **not** `memory`. signalk-container drops a limit whose controller is missing
1071
- rather than failing the container, so a configured cap can silently have no
1072
- effect.
1073
-
1074
- **A. `memory` not delegated — the cap is silently dropped.** QuestDB grows
1075
- without bound and the host kernel's OOM killer eventually kills it under
1076
- whole-system memory pressure; it restarts and the cycle repeats. Enable memory
1077
- delegation on the host (one-time, needs sudo), then **recreate** the QuestDB
1078
- container (a restart is not enough — the limit is set at create time):
1079
-
1080
- ```bash
1081
- sudo mkdir -p /etc/systemd/system/user@.service.d
1082
- sudo tee /etc/systemd/system/user@.service.d/delegate.conf >/dev/null <<'EOF'
1083
- [Service]
1084
- Delegate=cpu cpuset io memory pids
1085
- EOF
1086
- sudo systemctl daemon-reload
1087
- ```
1088
-
1089
- `daemon-reload` reloads the drop-in file but does not re-apply `Delegate=` to
1090
- the already-running user manager, so the new delegation only takes effect after
1091
- the **user session restarts**. On a headless box a reboot is simplest (and is
1092
- required anyway if you edit `cmdline.txt` below); otherwise restart the user
1093
- manager with `sudo systemctl restart user@$(id -u).service` (this stops all of
1094
- that user's containers, so let them come back before recreating QuestDB).
1095
-
1096
- ```bash
1097
- # On older Raspberry Pi kernels the memory controller is off at boot; add to
1098
- # /boot/cmdline.txt (one line) and reboot:
1099
- # cgroup_enable=memory cgroup_memory=1
1100
- ```
1101
-
1102
- After the session restart, recreate the QuestDB container so the cap applies.
1103
-
1104
- **B. `memory` is delegated — the 768 MB cap is enforced but too tight.** As the
1105
- database grows, QuestDB's peak memory (JVM heap plus off-heap memory-mapped
1106
- files, which spike during out-of-order merges) exceeds 768 MB and the cgroup
1107
- OOM-kills it. Raise **QuestDB memory limit** in the plugin config (e.g. `1g` or
1108
- `1.5g`, or empty to remove the cap on a roomier host), which recreates the
1109
- container.
1110
-
1111
- **Confirm the diagnosis** (substitute the container's name if the prefix is not
1112
- `sk-`):
1113
-
1114
- ```bash
1115
- podman inspect sk-signalk-questdb \
1116
- --format 'OOMKilled={{.State.OOMKilled}} RestartCount={{.RestartCount}} cap={{.HostConfig.Memory}}'
1117
- # cap=805306368 -> 768 MB cap is in place (variant B)
1118
- # cap=0 -> no cap applied (variant A, or cap intentionally removed)
1119
- podman events --since 2h --stream=false --filter container=sk-signalk-questdb \
1120
- --filter event=oom --filter event=died
1121
- ```
1122
-
1123
- ### History queries slow or timing out / "out-of-memory" errors with free RAM
1124
-
1125
- QuestDB memory-maps every partition column file and every pending WAL segment it
1126
- touches. The Linux kernel caps how many memory mappings one process may hold
1127
- (`vm.max_map_count`), and the stock value on Debian, Ubuntu ≤ 22.04, and RHEL
1128
- (65530) is far below what QuestDB recommends (1048576). A fresh database fits
1129
- easily; months of daily partitions across the three tables — plus a segment
1130
- backlog if a table's WAL is suspended — can exhaust the limit. When that happens
1131
- `mmap` fails with out-of-memory errors (errno 12) **even though plenty of RAM is
1132
- free**: queries error out or crawl, and the WAL apply job can suspend a table.
1133
-
1134
- The QuestDB Web Console shows the same warning
1135
- (`vm.max_map_count limit is too low`), the plugin's config panel shows a banner,
1136
- and the server log gets a warning at plugin startup. Fix it in a shell **on the
1137
- host machine itself** — not inside the QuestDB container via `podman exec`: the
1138
- limit is kernel-global, a container cannot change it, and the QuestDB image has
1139
- neither `sudo` nor `/etc/sysctl.d`. Being kernel-global also means this works no
1140
- matter how Signal K itself is deployed:
1141
-
1142
- ```bash
1143
- echo 'vm.max_map_count=1048576' | sudo tee /etc/sysctl.d/99-questdb.conf
1144
- sudo sysctl --system
1145
- ```
1146
-
1147
- This takes effect immediately — no container or host restart needed. See
1148
- [QuestDB capacity planning](https://questdb.com/docs/getting-started/capacity-planning/#max-virtual-memory-areas-limit)
1149
- for background.
1150
-
1151
- ### QuestDB container never starts on macOS (podman machine)
1152
-
1153
- On macOS the QuestDB container is created but may never start. What you see
1154
- depends on the signalk-container version: with signalk-container 1.26 or later,
1155
- the manager names the problem outright (an open-files limit the host refuses)
1156
- and starts the container on the runtime's default limits instead, with this
1157
- plugin's config panel showing a **"request rejected by the host"** banner — the
1158
- remediation below applies unchanged. Older versions never start the container at
1159
- all and show either a misleading **"Permission denied"** (the runtime's error
1160
- text contains "operation not permitted") or only a generic **"Unexpected error.
1161
- See logs for details."** — and the container log view stays empty, because a
1162
- container that never started has no logs.
1163
-
1164
- The cause is the plugin's open-files request. On macOS, Signal K runs on the Mac
1165
- while podman runs inside a Fedora CoreOS VM ("podman machine"). The plugin asks
1166
- for 1048576 open files, and signalk-container normally clamps that request to
1167
- what the host can grant — but from macOS it cannot read the VM's limits, so the
1168
- full request reaches the VM, exceeds its default hard limit (524288), and the
1169
- OCI runtime refuses to start the container. On current Fedora CoreOS the VM's
1170
- `vm.max_map_count` already meets QuestDB's 1048576, so usually only the
1171
- file-descriptor limit needs raising — but an older machine image may sit lower,
1172
- so check it first:
1173
-
1174
- ```bash
1175
- podman machine ssh -- sysctl -n vm.max_map_count
1176
- ```
1177
-
1178
- If that prints less than 1048576, raise it inside the VM before relying on the
1179
- file-descriptor fix alone (`podman machine ssh`, then
1180
- `sudo tee /etc/sysctl.d/99-signalk-questdb.conf <<< 'vm.max_map_count=1048576'`
1181
- and `sudo sysctl --system`).
1182
-
1183
- Raise the file-descriptor limit inside the VM. From a macOS terminal:
1184
-
1185
- ```bash
1186
- podman machine ssh
1187
- ```
1188
-
1189
- Inside the VM, add a systemd drop-in for every user manager (the `user@.service`
1190
- template covers each user instance, including the machine's `core` user, whose
1191
- session runs rootless containers) and the same drop-in for `podman.service` to
1192
- cover the rootful connection:
1193
-
1194
- ```bash
1195
- sudo mkdir -p /etc/systemd/system/user@.service.d /etc/systemd/system/podman.service.d
1196
- sudo tee /etc/systemd/system/user@.service.d/nofile.conf >/dev/null <<'EOF'
1197
- [Service]
1198
- LimitNOFILE=1048576
1199
- EOF
1200
- sudo cp /etc/systemd/system/user@.service.d/nofile.conf /etc/systemd/system/podman.service.d/nofile.conf
1201
- sudo systemctl daemon-reload
1202
- exit
1203
- ```
1204
-
1205
- Back on macOS, restart the VM:
1206
-
1207
- ```bash
1208
- podman machine stop && podman machine start
1209
- ```
1210
-
1211
- and verify the new limit is grantable:
1212
-
1213
- ```bash
1214
- podman run --rm --ulimit nofile=1048576:1048576 docker.io/library/alpine sh -c 'ulimit -n -H'
1215
- ```
1216
-
1217
- This should print `1048576`. Finally, remove the half-created container so it is
1218
- recreated with the full limit — the limit is set at create time, so a restart is
1219
- not enough:
1220
-
1221
- ```bash
1222
- podman rm -f sk-signalk-questdb
1223
- ```
1224
-
1225
- then restart Signal K — on the next plugin start the container is recreated with
1226
- the full limit. (The container manager's **Start** button only starts an
1227
- existing container, so it cannot replace this step.)
1228
-
1229
- The verify and `rm` commands run on podman's default connection — normally the
1230
- rootless one the plugin uses. Rootless and rootful connections keep separate
1231
- container stores, so on a setup with a non-default connection add the same
1232
- `--connection <name>` (list them with `podman system connection list`) to both
1233
- commands, so they hit the store where `sk-signalk-questdb` actually lives
1234
- (`podman ps -a` shows it).
157
+ The [signalk-grafana](https://github.com/dirkwa/signalk-grafana) plugin sets up
158
+ Grafana with QuestDB for you. To connect a Grafana of your own, see
159
+ [Grafana](doc/deployment.md#grafana).
160
+
161
+ ### Update QuestDB
162
+
163
+ **Check for updates** in the config panel tells you whether a newer QuestDB is
164
+ out, and **Update** installs it. With the default image version `latest`, the
165
+ plugin also checks by itself after each update of the plugin. See
166
+ [QuestDB updates](doc/configuration.md#questdb-updates).
167
+
168
+ ## When something goes wrong
169
+
170
+ - **"Module signalk-questdb is not available"** right after installing or
171
+ updating: reload the browser tab with Shift-Reload.
172
+ - **"QuestDB keeps dropping the write connection"**: QuestDB is most likely
173
+ running out of memory. [Troubleshooting](doc/troubleshooting.md) shows how to
174
+ tell whether to raise **QuestDB memory limit** or to change a setting of the
175
+ host.
176
+ - **History is slow, or "out-of-memory" errors although RAM is free**: the host
177
+ allows QuestDB too few memory mappings. Run this on the host, not inside the
178
+ container. It takes effect at once:
179
+
180
+ ```bash
181
+ echo 'vm.max_map_count=1048576' | sudo tee /etc/sysctl.d/99-questdb.conf
182
+ sudo sysctl --system
183
+ ```
184
+
185
+ - **WAL suspended** on the status card: QuestDB has stopped applying what it
186
+ receives. The plugin resumes it by itself where it can. Otherwise the panel
187
+ offers **Resume**, or **Skip** together with what skipping would lose.
188
+ - **"Cannot apply a change: the QuestDB container cannot be stopped or
189
+ removed"**, a **QuestDB that never starts on macOS**, and more:
190
+ [Troubleshooting](doc/troubleshooting.md).
191
+
192
+ ## More documentation
193
+
194
+ - [Configuration](doc/configuration.md): every setting, and QuestDB updates
195
+ - [Recording](doc/recording.md): what is stored and how, retention and the
196
+ archive, startup restore
197
+ - [History API](doc/history-api.md): the v1 and v2 APIs and the plugin's REST
198
+ endpoints
199
+ - [Import and export](doc/import-export.md): InfluxDB, GPX, export and backup
200
+ - [Web Console](doc/web-console.md): exploring the data with SQL
201
+ - [Deployment](doc/deployment.md): requirements, networking, storage,
202
+ durability, performance, Grafana
203
+ - [Troubleshooting](doc/troubleshooting.md)
1235
204
 
1236
205
  ## License
1237
206