frugal-iot-server 2.0.8 → 2.0.9
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/GRAFANA.md +37 -6
- package/INSTALL-OPENSEARCH.md +183 -97
- package/config.d/opensearch.yaml +18 -14
- package/frugal-iot-server.js +2 -2
- package/lib/forwarders.js +49 -10
- package/lib/opensearch-access.js +9 -2
- package/package.json +3 -3
- package/public/service-worker.js +1 -1
package/GRAFANA.md
CHANGED
|
@@ -17,7 +17,31 @@ Grafana reads the server with its free **Infinity** plugin, which can call any U
|
|
|
17
17
|
> Grafana's API), and the Grafana card on the dashboard clicked through by hand. Correct this page
|
|
18
18
|
> where they differ.
|
|
19
19
|
|
|
20
|
+
## Installing
|
|
21
|
+
|
|
22
|
+
#### On a Mac,
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
brew install grafana
|
|
26
|
+
brew services start grafana
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open <http://localhost:3000>. Grafana's default login is `admin` / `admin`, and it asks you to change it.
|
|
30
|
+
Then add the Infinity plugin - in Grafana, Administration → Plugins → "Infinity" - or from the shell:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
/opt/homebrew/opt/grafana/bin/grafana cli --homepath /opt/homebrew/opt/grafana/share/grafana \
|
|
34
|
+
--pluginsDir /opt/homebrew/var/lib/grafana/plugins plugins install yesoreyeram-infinity-datasource
|
|
35
|
+
brew services restart grafana
|
|
36
|
+
```
|
|
37
|
+
To stop it: `brew services stop grafana` (Homebrew restarts it at login until you do).
|
|
38
|
+
|
|
39
|
+
#### On Windows, Linux and other systems
|
|
40
|
+
|
|
41
|
+
For other systems, Grafana's own download page has installers for [Windows, Linux and others](https://grafana.com/grafana/download).
|
|
42
|
+
|
|
20
43
|
## Setting it up
|
|
44
|
+
Access Grafana from your browser at [localhost:3000](http://localhost:3000)
|
|
21
45
|
|
|
22
46
|
Anyone who can **read** the organization can make a token for themselves - you do not need to be an
|
|
23
47
|
administrator. A token is yours: Grafana sees what *you* can see, so a token made by someone who can
|
|
@@ -26,22 +50,29 @@ read only one project shows that project only.
|
|
|
26
50
|
1. **Install the Infinity plugin in Grafana.** In Grafana: Administration → Plugins, search for
|
|
27
51
|
"Infinity", install it, and restart Grafana if it asks. (Installing Grafana itself is in
|
|
28
52
|
[INSTALL-OPENSEARCH.md](INSTALL-OPENSEARCH.md).)
|
|
29
|
-
2. **Make a token.** In the dashboard, open the project's back, then the **Grafana** card. Type a name
|
|
53
|
+
2. **Make a token.** In the Frugal IoT dashboard, open the project's back, then the **Grafana** card. Type a name
|
|
30
54
|
for this Grafana (so it can be revoked on its own later) and press *Make a token*. The token is shown
|
|
31
55
|
**once** - only a hash is kept, so it cannot be looked up again. If you lose it, make one with the
|
|
32
56
|
same name, which replaces it.
|
|
33
|
-
3. **Give Grafana the data source.** Either
|
|
34
|
-
|
|
57
|
+
3. **Give Grafana the data source.** Either:
|
|
58
|
+
a: press *Download the data source file* in the same place and
|
|
59
|
+
put it in Grafana's `provisioning/datasources` folder (On a Mac typically in /opt/homebrew/Cellar/grafana/13.3.0/share/grafana/conf/provisioning),
|
|
60
|
+
then restart Grafana:
|
|
61
|
+
- On Mac: `brew services restart grafana`;
|
|
62
|
+
|
|
63
|
+
or add it by hand in
|
|
35
64
|
Grafana under Connections → Data sources → Add → **Infinity**:
|
|
36
65
|
- Authentication: **Bearer Token**, with the token
|
|
37
66
|
- Allowed hosts: the address you reached this dashboard at, e.g. `https://frugaliot.naturalinnovation.org`
|
|
38
|
-
4. **Import the dashboard.**
|
|
39
|
-
|
|
67
|
+
4. **Import the dashboard.**
|
|
68
|
+
* On FrugalIoT: In the Grafana card press *Download the dashboard*.
|
|
69
|
+
* Then in Grafana choose
|
|
70
|
+
Dashboards → New → Import, upload the file, and pick the data source from step 3 when it asks.
|
|
40
71
|
|
|
41
72
|
The dashboard has a list of every topic at the top to choose from, and the time range at the top right.
|
|
42
73
|
The file already has this server and organization filled in.
|
|
43
74
|
|
|
44
|
-
A token lets one Grafana read **one organization**, read-only
|
|
75
|
+
A token lets one Grafana read **one organization**, read-only - and only the
|
|
45
76
|
projects of it that the person who made the token may read. For another
|
|
46
77
|
organization, make its own token, data source and import - the dashboard file is separate for each, so
|
|
47
78
|
importing a second does not replace the first. *Revoke* on the card cuts off a Grafana at once.
|
package/INSTALL-OPENSEARCH.md
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
# Installing OpenSearch, Grafana and Dashboards
|
|
2
2
|
|
|
3
3
|
This is for **browsing and charting your readings**. All of it is optional - the server and logger work
|
|
4
|
-
without any of it, and the readings are always safe in the CSV files under `data
|
|
4
|
+
without any of it, and the readings are always safe in the CSV files under `data/`,
|
|
5
|
+
and the client has its own graphing.
|
|
5
6
|
|
|
6
7
|
## What the pieces are
|
|
7
8
|
|
|
8
|
-
OpenSearch is **two programs
|
|
9
|
+
OpenSearch is **two programs**:
|
|
9
10
|
|
|
10
11
|
- **OpenSearch** is the database. It stores and searches the readings, and listens on port 9200.
|
|
11
|
-
- **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch.
|
|
12
|
-
|
|
12
|
+
- **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch.
|
|
13
|
+
It access readings from OpenSearch at port 9200, using one login in its own
|
|
13
14
|
configuration file. (It also keeps its own saved dashboards *inside* OpenSearch, in an index.)
|
|
14
15
|
|
|
15
|
-
**Grafana** is a third, unrelated program that makes charts. It can read the server directly
|
|
16
|
-
([GRAFANA.md](GRAFANA.md)), needing no OpenSearch at all.
|
|
17
|
-
|
|
18
16
|
## The ways to use it
|
|
19
17
|
|
|
20
18
|
| | Readings are kept in | You look at them with | Set up |
|
|
21
19
|
|---|---|---|---|
|
|
22
|
-
| **Grafana, reading the server** *(start here)* | the server's CSV files, all of them | Grafana on your computer | [GRAFANA.md](GRAFANA.md); needs no OpenSearch |
|
|
23
20
|
| **(a) OpenSearch on the server** | OpenSearch on the production server | OpenSearch Dashboards on **your own computer** | sections 2, 3, 4, 5 |
|
|
24
21
|
| **(b) OpenSearch on your own machine** | OpenSearch on a machine you run, which the server sends readings to | whatever you like - you run it all | section 6 |
|
|
25
22
|
|
|
@@ -28,9 +25,8 @@ Either of (a) and (b) can be used with Grafana too.
|
|
|
28
25
|
**None of this is for the Raspberry Pi bridge.**
|
|
29
26
|
|
|
30
27
|
> **Status:** the Grafana and Mac steps were followed on macOS with Homebrew (OpenSearch 3.9.0,
|
|
31
|
-
> Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
|
|
32
|
-
>
|
|
33
|
-
> where they differ. [Open questions](#open-questions) lists what to check first.
|
|
28
|
+
> Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
|
|
29
|
+
> have been run on the production server at frugaliot.naturalinnovation.org
|
|
34
30
|
|
|
35
31
|
We use **OpenSearch**, not Elasticsearch: it is Apache-licensed, installs with Homebrew and apt, and
|
|
36
32
|
Grafana can read it. OpenSearch Dashboards is its fork of Kibana, so Kibana guides are close but not
|
|
@@ -38,117 +34,122 @@ identical. (The reasoning is also in a comment above the `OpenSearch` class in f
|
|
|
38
34
|
`index.js`, in case the decision is revisited.)
|
|
39
35
|
|
|
40
36
|
How heavy is each? OpenSearch is the heavy one: a Java program with its own 1 GB heap by default.
|
|
41
|
-
|
|
37
|
+
Dashboards are small by comparison (about 60 MB and 20 MB resident on a Mac, which
|
|
42
38
|
under-reports, so read that loosely).
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## 1. On your own computer: Grafana
|
|
40
|
+
## 1. (a) On the server: OpenSearch
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
**Debian or Ubuntu.**
|
|
49
43
|
|
|
44
|
+
**First, check there is room.** OpenSearch with its default 1 GB heap takes about 1.4 GB of RAM in
|
|
45
|
+
total (measured on a 2.4 GB server, where it became by far the biggest process). Run this *before*
|
|
46
|
+
installing:
|
|
50
47
|
```sh
|
|
51
|
-
|
|
52
|
-
brew services start grafana
|
|
48
|
+
free -h
|
|
53
49
|
```
|
|
50
|
+
The `available` column is what is left for OpenSearch; you want at least 1.5 GB. If you have less, either add
|
|
51
|
+
memory or give OpenSearch a smaller heap (the step before starting it, below). Run it again after
|
|
52
|
+
OpenSearch is started and `available` will, of course, be much lower; `top` (press `M` to sort by
|
|
53
|
+
memory, `q` to quit) shows what is using it.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
Then add the Infinity plugin - in Grafana, Administration → Plugins → "Infinity" - or from the shell:
|
|
57
|
-
|
|
55
|
+
Install the prerequisites
|
|
58
56
|
```sh
|
|
59
|
-
|
|
60
|
-
--pluginsDir /opt/homebrew/var/lib/grafana/plugins plugins install yesoreyeram-infinity-datasource
|
|
61
|
-
brew services restart grafana
|
|
57
|
+
sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
|
|
62
58
|
```
|
|
63
|
-
|
|
64
|
-
For other systems, Grafana's own download page has installers for [Windows, Linux and others](https://grafana.com/grafana/download).
|
|
65
|
-
|
|
66
|
-
Then continue at [GRAFANA.md](GRAFANA.md) to make a token and chart your readings.
|
|
67
|
-
|
|
68
|
-
To stop it: `brew services stop grafana` (Homebrew restarts it at login until you do).
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 2. (a) On the server: OpenSearch
|
|
75
|
-
|
|
76
|
-
Debian or Ubuntu.
|
|
77
|
-
|
|
78
|
-
`apt-get` does the installing. The `curl` line just downloads the signing key for OpenSearch's
|
|
79
|
-
repository, which apt needs in order to trust it:
|
|
80
|
-
|
|
59
|
+
Fetch the signing key for OpenSearch's repository
|
|
81
60
|
```sh
|
|
82
|
-
sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
|
|
83
61
|
sudo mkdir -p /etc/apt/keyrings
|
|
84
62
|
curl -fsSL https://artifacts.opensearch.org/publickeys/opensearch-release.pgp \
|
|
85
63
|
| sudo gpg --dearmor -o /etc/apt/keyrings/opensearch.gpg
|
|
86
64
|
```
|
|
65
|
+
Add OpenSearch's apt repository
|
|
66
|
+
```sh
|
|
67
|
+
echo "deb [signed-by=/etc/apt/keyrings/opensearch.gpg] https://artifacts.opensearch.org/releases/bundle/opensearch/3.x/apt stable main" \
|
|
68
|
+
| sudo tee /etc/apt/sources.list.d/opensearch-3.x.list
|
|
69
|
+
```
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
[their Debian guide](https://docs.opensearch.org/latest/install-and-configure/install-opensearch/debian/),
|
|
90
|
-
which is printed there for your architecture - and install, choosing an admin password now:
|
|
91
|
-
|
|
71
|
+
Choose a strong password to replace `<strong password>` with below. Installing does not start it yet.
|
|
92
72
|
```sh
|
|
93
73
|
sudo apt-get update
|
|
94
|
-
sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<
|
|
74
|
+
sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<strong password>' apt-get install opensearch
|
|
95
75
|
sudo systemctl enable opensearch
|
|
96
|
-
sudo systemctl start opensearch
|
|
97
|
-
curl https://localhost:9200 -u 'admin:<that password>' --insecure
|
|
98
76
|
```
|
|
99
77
|
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
**Set the memory before starting.** It is set in `/etc/opensearch/jvm.options`, which ships with `-Xms1g`
|
|
79
|
+
and `-Xmx1g` (a 1 GB heap). If `free -h` above showed less than 1.5 GB available, change both lines to
|
|
80
|
+
`512m`, which is enough for a little data: all 598,000 readings of the `dev` organization came to about
|
|
81
|
+
35 MB. More heap helps with a lot of history, but the machine needs the whole amount free *beyond* what
|
|
82
|
+
the server, broker, database and logger already use. You can change it later, then
|
|
83
|
+
`sudo systemctl restart opensearch`.
|
|
84
|
+
|
|
85
|
+
The other settings, in `/etc/opensearch/opensearch.yml`, need no change.
|
|
86
|
+
|
|
87
|
+
Now start it. This can take a couple of minutes.
|
|
88
|
+
```sh
|
|
89
|
+
sudo systemctl start opensearch
|
|
90
|
+
```
|
|
91
|
+
Check it is answering (`--insecure` means "do not check the certificate", because the one it ships with
|
|
92
|
+
is self-signed):
|
|
93
|
+
```sh
|
|
94
|
+
curl https://localhost:9200 -u 'admin:<strong password>' --insecure
|
|
95
|
+
```
|
|
102
96
|
|
|
103
97
|
This install **has the security plugin switched on**: HTTPS, a login, and that self-signed certificate.
|
|
104
|
-
The password is only used to set up that demo security configuration.
|
|
105
|
-
`/etc/opensearch/opensearch.yml` and the memory setting in `/etc/opensearch/jvm.options`.
|
|
98
|
+
The password is only used to set up that demo security configuration.
|
|
106
99
|
|
|
107
|
-
- **Memory:** the file ships with `-Xms1g` and `-Xmx1g` (a 1 GB heap). Give both lines the same value;
|
|
108
|
-
more helps with a lot of history, and the machine needs that much free *beyond* what the server,
|
|
109
|
-
broker and logger use. The data is small: all 598,000 readings of the `dev` organization came to
|
|
110
|
-
about 35 MB.
|
|
111
100
|
- **If it will not start**, look at `journalctl -u opensearch`. A common cause on Linux is the kernel
|
|
112
101
|
setting `vm.max_map_count` being below 262144; `sudo sysctl -w vm.max_map_count=262144` fixes it until
|
|
113
102
|
reboot, and a line in `/etc/sysctl.d/` makes it permanent.
|
|
114
103
|
- **Leave OpenSearch listening on the machine itself** (the default). Only Apache, below, is open to the
|
|
115
104
|
internet - never port 9200 directly. Check under [Keeping it safe](#keeping-it-safe).
|
|
116
105
|
|
|
117
|
-
### Tell the
|
|
106
|
+
### Tell the Frugal IoT Server about it
|
|
118
107
|
|
|
119
|
-
Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there),
|
|
120
|
-
the server:
|
|
108
|
+
Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there),
|
|
121
109
|
|
|
122
110
|
```yaml
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
public: https://search.example.org # the address people outside reach it at - section 3
|
|
111
|
+
node: https://localhost:9200 # where THIS server reaches OpenSearch: on this machine
|
|
112
|
+
username: admin # what the forwarder writes readings as
|
|
113
|
+
password: <the password you chose>
|
|
114
|
+
insecure: true # OpenSearch's demo certificate cannot be checked; fine on this machine
|
|
115
|
+
admin: # the account that makes the per-organization logins (section 4)
|
|
116
|
+
username: admin
|
|
117
|
+
password: <the same>
|
|
118
|
+
public: https://search.example.org # the address people outside reach it at - section 3
|
|
132
119
|
```
|
|
120
|
+
and restart the Frugal IoT server:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
sudo service frugaliot restart
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The lines go at the top level of the file - the file's name already says they are about `opensearch`.
|
|
133
127
|
|
|
134
|
-
This file is never sent to a browser.
|
|
128
|
+
This file is never sent to a browser. `username:` and `password:` at the top must be an account OpenSearch
|
|
129
|
+
knows - the `admin` one you chose when installing - or every send fails with "Response Error" and the
|
|
130
|
+
logger's journal says so. Using `admin` for everything is the simplest thing that works, not
|
|
135
131
|
the best: a role that can only write `frugaliot-*` for the forwarder would be better, and is not yet
|
|
136
132
|
written. Leave `admin:` and `public:` out until you have done sections 3 and 4.
|
|
137
133
|
|
|
138
134
|
### Send readings to it
|
|
139
135
|
|
|
140
|
-
In the dashboard, open the project's back, then the **Forwarders** card, and add **OpenSearch on this
|
|
136
|
+
In the dashboard, open the project's back, then the **Forwarders** card (not the **OpenSearch** card), and add **OpenSearch on this
|
|
141
137
|
server**. There is no address or password to type: they come from the file above. Optionally set *Only
|
|
142
138
|
these topics*, e.g. `dev/lotus/#`; leave it empty to send everything that is logged.
|
|
143
139
|
|
|
144
|
-
It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
|
|
140
|
+
Readings are sent in batches every few minutes, so the first ones take a few minutes to show up. It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
|
|
145
141
|
makes `value` a number) the first time it sends. An organization's administrator can add it for their own
|
|
146
142
|
organization, so only do this if you are happy for them to have their readings in OpenSearch.
|
|
147
143
|
|
|
148
144
|
### Loading the history you already have
|
|
149
145
|
|
|
150
|
-
A forwarder only sends readings from the moment it is switched on. To bring in
|
|
151
|
-
|
|
146
|
+
A forwarder only sends readings from the moment it is switched on. To bring in what is already recorded,
|
|
147
|
+
open the **Forwarders** card, and on the OpenSearch forwarder's row press **Send earlier readings...**.
|
|
148
|
+
Pick the day to start from (leave it empty for everything), optionally tick *Only count them* to see how
|
|
149
|
+
many there are first, and press **Start**. It runs in the background and the card shows how many it has
|
|
150
|
+
sent so far. It is safe to repeat, and safe while the server is running.
|
|
151
|
+
|
|
152
|
+
The same thing from a terminal, in the server's directory, if you prefer:
|
|
152
153
|
|
|
153
154
|
```sh
|
|
154
155
|
npx --no frugal-iot-backfill-opensearch dev dry-run # count what would be sent
|
|
@@ -158,12 +159,22 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
|
|
|
158
159
|
|
|
159
160
|
### Looking after it
|
|
160
161
|
|
|
162
|
+
Run these on the server, in a terminal. `admin:<strong password>` is the login and password you chose when
|
|
163
|
+
installing OpenSearch; `--insecure` is the same "don't check the certificate" as before.
|
|
164
|
+
|
|
165
|
+
- **See what is there.** One line per index, with its size:
|
|
166
|
+
```sh
|
|
167
|
+
curl -s 'https://localhost:9200/_cat/indices/frugaliot-*?v' -u 'admin:<strong password>' --insecure
|
|
168
|
+
```
|
|
161
169
|
- **Old months.** Readings go into one index per organization per month, named like
|
|
162
|
-
`frugaliot-dev-2026.09
|
|
163
|
-
|
|
170
|
+
`frugaliot-dev-2026.09` (that name is in the first column above). To drop one:
|
|
171
|
+
```sh
|
|
172
|
+
curl -X DELETE 'https://localhost:9200/frugaliot-dev-2026.02' -u 'admin:<strong password>' --insecure
|
|
173
|
+
```
|
|
174
|
+
The CSV files are untouched, so nothing is lost for good.
|
|
164
175
|
- **A gap while OpenSearch was down.** The forwarder does not retry a batch it could not send; the CSV
|
|
165
|
-
files have everything, and
|
|
166
|
-
- **Starting afresh.** Delete
|
|
176
|
+
files have everything, and *Send earlier readings* (above) fills the gap.
|
|
177
|
+
- **Starting afresh.** Delete each index listed above, as for an old month, and send the earlier readings again.
|
|
167
178
|
|
|
168
179
|
---
|
|
169
180
|
|
|
@@ -171,15 +182,25 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
|
|
|
171
182
|
|
|
172
183
|
## 3. (a) Apache: HTTPS in front of OpenSearch
|
|
173
184
|
|
|
174
|
-
Dashboards on someone's computer
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
of this server - add one more site to it, on its own name, e.g. `search.example.org`.
|
|
185
|
+
Dashboards on someone's computer reach OpenSearch across the internet, it needs HTTPS.
|
|
186
|
+
|
|
187
|
+
While opensearch ships with HTTP and some self-signed certificates, for most practical installs it will be behind an Apache proxy.
|
|
178
188
|
|
|
189
|
+
We'll add a domain name to it - e.g. `opensearch.naturalinnovation.org`
|
|
190
|
+
|
|
191
|
+
Add components needed for Apache
|
|
179
192
|
```sh
|
|
180
193
|
sudo a2enmod ssl proxy proxy_http headers
|
|
181
194
|
```
|
|
182
195
|
|
|
196
|
+
You need a DNS record pointing `search.example.org` (use your own name) at this server, and a
|
|
197
|
+
certificate for it, made the way you made the server's own, e.g. `sudo certbot certonly --apache -d search.example.org`.
|
|
198
|
+
|
|
199
|
+
Your certificate may be a wildcard one (e.g. `*.example.org`) you already have; then use its files in
|
|
200
|
+
the two `SSLCertificate...` lines below.
|
|
201
|
+
|
|
202
|
+
Make a new file for this site, `/etc/apache2/sites-available/opensearch.conf` (Debian and Ubuntu keep
|
|
203
|
+
every Apache site in that folder; it is separate from your existing site's file), with:
|
|
183
204
|
```apache
|
|
184
205
|
<VirtualHost *:443>
|
|
185
206
|
ServerName search.example.org
|
|
@@ -205,10 +226,17 @@ sudo a2enmod ssl proxy proxy_http headers
|
|
|
205
226
|
</LocationMatch>
|
|
206
227
|
</VirtualHost>
|
|
207
228
|
```
|
|
229
|
+
Switch the site on, then test and reload Apache:
|
|
230
|
+
```sh
|
|
231
|
+
sudo a2ensite opensearch
|
|
232
|
+
sudo apachectl configtest && sudo systemctl reload apache2
|
|
233
|
+
```
|
|
234
|
+
Check from your own computer:
|
|
235
|
+
`curl https://search.example.org` should answer 401 (it wants a login) - not a block of JSON, or HTML, and not a
|
|
236
|
+
timeout.
|
|
208
237
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
timeout. Put the address in `public:` in `config.d/opensearch.yaml`.
|
|
238
|
+
Put the address (with the `https://`) in `public:` in `config.d/opensearch.yaml`, with the `admin:` lines
|
|
239
|
+
from section 1, then restart the server so it reads the file: `sudo systemctl restart frugaliot`.
|
|
212
240
|
|
|
213
241
|
Port 9200 itself stays closed to the network (firewall) - only Apache's 443 is open.
|
|
214
242
|
|
|
@@ -256,17 +284,80 @@ on a Mac), start it (`brew services start opensearch-dashboards`) and open <http
|
|
|
256
284
|
|
|
257
285
|
```yaml
|
|
258
286
|
opensearch.hosts: [ "https://search.example.org" ]
|
|
259
|
-
opensearch.username: "
|
|
287
|
+
opensearch.username: "frugaliot_dev_mitra"
|
|
260
288
|
opensearch.password: "..."
|
|
261
289
|
opensearchDashboards.index: ".kibana_dev"
|
|
262
|
-
opensearch.requestHeadersAllowlist: [
|
|
290
|
+
opensearch.requestHeadersAllowlist: []
|
|
291
|
+
opensearch.customHeaders: { authorization: "Basic <the login, encoded - the card writes this line for you>" }
|
|
263
292
|
```
|
|
264
293
|
|
|
265
294
|
The `opensearchDashboards.index` line is what gives the organization its own place to keep saved
|
|
266
|
-
dashboards; without it Dashboards uses `.kibana`, which this login cannot touch.
|
|
267
|
-
|
|
295
|
+
dashboards; without it Dashboards uses `.kibana`, which this login cannot touch. The last two lines stop Dashboards asking your browser for a login: Dashboards uses the login above
|
|
296
|
+
only for its own housekeeping, so they add the same login to every request. Copy the whole block from
|
|
297
|
+
the card - the `Basic ...` line is the username and password encoded, which you cannot easily write by
|
|
298
|
+
hand. Nothing is typed when you open Dashboards. Keep Dashboards on your own computer: it listens on
|
|
299
|
+
`localhost` by default, and anyone who can reach its port acts as this login.
|
|
300
|
+
|
|
301
|
+
Open opensearch - by default it will be on `http://localhost:5601`
|
|
302
|
+
|
|
303
|
+
Then, in Dashboards:
|
|
304
|
+
**Dashboards Management → Index patterns → Create**, enter `frugaliot-<org>-*` (e.g. `frugaliot-dev-*`; your login can read only your own organization's), and choose `@timestamp` as the
|
|
268
305
|
time field.
|
|
269
306
|
|
|
307
|
+
If it says the pattern **"doesn't match any indices"**, OpenSearch has no readings for your organization yet:
|
|
308
|
+
the forwarder (section 2, "Send readings to it") is not added, or nothing has been sent. Readings only
|
|
309
|
+
arrive once the forwarder is on, so send the earlier ones as in "Loading the history you already have".
|
|
310
|
+
You can check from your own computer with the login that is in the Dashboards file - the generated one the card
|
|
311
|
+
showed (not the OpenSearch `admin` password). This reads it from the file on a Mac; use your own address
|
|
312
|
+
and organization:
|
|
313
|
+
```sh
|
|
314
|
+
curl -s -u "$(awk -F'"' '/^opensearch.username/{u=$2} /^opensearch.password/{p=$2} END{print u":"p}' /opt/homebrew/etc/opensearch-dashboards/opensearch_dashboards.yml)" 'https://search.example.org/frugaliot-dev-*/_count'
|
|
315
|
+
```
|
|
316
|
+
`"count":0` with `"total":0` shards means none are there; a number above 0 means they are.
|
|
317
|
+
|
|
318
|
+
If it says **"OpenSearch Dashboards server is not ready yet"**, look at its log
|
|
319
|
+
(`/opt/homebrew/var/log/opensearch-dashboards.log` on a Mac): it names what it could not do. A
|
|
320
|
+
`security_exception ... no permissions for [...]` line means the organization's role on the server is
|
|
321
|
+
missing that permission - press **Replace the password** on the OpenSearch card once (the server rewrites
|
|
322
|
+
the role), then restart Dashboards. A stalled "Another OpenSearch Dashboards instance appears to be
|
|
323
|
+
migrating" means a first start was interrupted; as the message says, delete the index it names
|
|
324
|
+
(`.kibana_<org>_1`, see "Looking after it" for the command) and restart it.
|
|
325
|
+
|
|
326
|
+
### Seeing your readings as a chart
|
|
327
|
+
|
|
328
|
+
Checked step by step against OpenSearch Dashboards 3.9 in a browser. Each reading has these fields:
|
|
329
|
+
`value` (the number), `@timestamp`, and `topic` split into `node` (the device), `module` and `leaf`
|
|
330
|
+
(e.g. `temperature`). First look at **Discover** (☰ menu, top left): with `frugaliot-<org>-*` chosen you
|
|
331
|
+
see a bar chart of how many readings arrived, and the readings below it.
|
|
332
|
+
|
|
333
|
+
Now a chart of one kind of reading, a line per device:
|
|
334
|
+
|
|
335
|
+
1. Go to **Visualize** (<http://localhost:5601/app/visualize>) and press **Create new visualization**.
|
|
336
|
+
Choose **Line**, then your `frugaliot-<org>-*` index pattern.
|
|
337
|
+
2. At the top right, click the calendar icon beside the time and choose **Last 24 hours**. The default
|
|
338
|
+
is the last 15 minutes, which is nearly always empty.
|
|
339
|
+
3. In the right-hand panel, under **Metrics**, open **Y-axis**. Set **Aggregation** to **Average**
|
|
340
|
+
(type "Average" and pick it). **Field** fills itself in with `value`.
|
|
341
|
+
4. Under **Buckets** press **Add**, choose **X-axis**, and set **Aggregation** to **Date Histogram**
|
|
342
|
+
(**Field** fills itself in with `@timestamp`). Press **Update** at the bottom right.
|
|
343
|
+
You now have one line, but it averages every kind of reading together (temperature, humidity,
|
|
344
|
+
wifi...), so the numbers mean nothing.
|
|
345
|
+
5. In the search bar above the chart type `leaf:temperature` and press Enter. Now only temperatures
|
|
346
|
+
are charted.
|
|
347
|
+
6. Under **Buckets** press **Add** again, choose **Split series**, set **Sub aggregation** to **Terms**
|
|
348
|
+
and **Field** to `node`. Press **Update**. You get one line per device, with its name in the legend.
|
|
349
|
+
7. Press **Save** (top right), give it a title such as `Temperature by device`, and **Save**.
|
|
350
|
+
|
|
351
|
+
Put it on a dashboard:
|
|
352
|
+
|
|
353
|
+
1. Go to **Dashboards** (<http://localhost:5601/app/dashboards>) and **Create new dashboard**.
|
|
354
|
+
2. Press **Add** (top right), click your chart in the list, and close the list with the **X**.
|
|
355
|
+
3. Press **Save**, give it a title, switch on **Store time with dashboard** (otherwise it opens on the
|
|
356
|
+
last 15 minutes and looks empty), and **Save**.
|
|
357
|
+
|
|
358
|
+
To chart something else, change `temperature` in step 5 to `humidity` or another `leaf`, or leave the
|
|
359
|
+
search empty and split by `topic` instead of `node`.
|
|
360
|
+
|
|
270
361
|
You can use the same login in **Grafana** instead, with its OpenSearch data source and the same address.
|
|
271
362
|
|
|
272
363
|
---
|
|
@@ -368,8 +459,7 @@ and only your own machine can reach it. Make `config.d/opensearch.yaml` in your
|
|
|
368
459
|
just:
|
|
369
460
|
|
|
370
461
|
```yaml
|
|
371
|
-
|
|
372
|
-
node: http://localhost:9200
|
|
462
|
+
node: http://localhost:9200
|
|
373
463
|
```
|
|
374
464
|
|
|
375
465
|
and restart the server. The Forwarders card then offers **OpenSearch on this server**. The Dashboards
|
|
@@ -397,11 +487,7 @@ To stop them: `brew services stop opensearch-dashboards opensearch`.
|
|
|
397
487
|
|
|
398
488
|
Please add what you find:
|
|
399
489
|
|
|
400
|
-
- **The Linux steps have not been run**: the exact `apt` repository line, and the Apache site above.
|
|
401
|
-
- **The role in section 4 is untried.** Its actions are the ones OpenSearch's documentation names for
|
|
402
|
-
Dashboards, and may be missing one - symptoms are Dashboards failing to start, or an index pattern that
|
|
403
|
-
cannot be created. If so, OpenSearch's log names the refused action; add it to `roleFor` in
|
|
404
|
-
`lib/opensearch-access.js`.
|
|
490
|
+
- **The Linux steps have not been run for the opensource-dashboards**: the exact `apt` repository line, and the Apache site above.
|
|
405
491
|
- **`opensearchDashboards.index`** is meant to give each organization its own saved-objects index. Check
|
|
406
492
|
that two organizations' Dashboards do not see each other's saved dashboards, and that neither can open
|
|
407
493
|
the other's `.kibana_<org>`.
|
package/config.d/opensearch.yaml
CHANGED
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
# login for OpenSearch Dashboards on the Dashboards access card. Neither shows an address or a password:
|
|
6
6
|
# they come from here, and never leave this machine.
|
|
7
7
|
#
|
|
8
|
-
# opensearch
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
8
|
+
# Write the lines at the top level, as below: the file's name already says they are about opensearch.
|
|
9
|
+
#
|
|
10
|
+
# node: https://localhost:9200 # where this server reaches OpenSearch - on this machine.
|
|
11
|
+
# # https on a Linux install (it has a login); http://localhost:9200
|
|
12
|
+
# # only for Homebrew's OpenSearch on a Mac, which has neither
|
|
13
|
+
# username: admin # what the forwarder writes readings as
|
|
14
|
+
# password: change-me
|
|
15
|
+
# insecure: true # OpenSearch's own certificate is self-signed, which can't be checked.
|
|
16
|
+
# # Needed with https://localhost; leave it out for http
|
|
13
17
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# # The account that makes and removes the per-organization logins on the Dashboards access card.
|
|
19
|
+
# # It must be allowed to change security settings, which is much more than the logger needs - so it
|
|
20
|
+
# # is separate, and you can leave it out if you would rather make those logins by hand.
|
|
21
|
+
# admin:
|
|
22
|
+
# username: admin
|
|
23
|
+
# password: change-me
|
|
20
24
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
25
|
+
# # The address Dashboards on someone's own computer uses to reach OpenSearch: https, through
|
|
26
|
+
# # Apache (INSTALL-OPENSEARCH.md), never the port 9200 above.
|
|
27
|
+
# public: https://search.example.org
|
package/frugal-iot-server.js
CHANGED
|
@@ -101,7 +101,7 @@ import morgan from 'morgan'; // https://www.npmjs.com/package/morgan - http requ
|
|
|
101
101
|
|
|
102
102
|
// If you are developing comment out the Production line, and uncomment the Development line
|
|
103
103
|
// Production
|
|
104
|
-
import { MqttLogger } from "frugal-iot-logger"; // https://github.com/mitra42/frugal-iot-logger
|
|
104
|
+
import { MqttLogger, backfillOpenSearch } from "frugal-iot-logger"; // https://github.com/mitra42/frugal-iot-logger
|
|
105
105
|
// Development of Logger
|
|
106
106
|
// import { MqttLogger } from "../frugal-iot-logger/index.js"; // https://github.com/mitra42/frugal-iot-logger
|
|
107
107
|
|
|
@@ -1247,7 +1247,7 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
1247
1247
|
app.use('/grafana_token', grafana.admin);
|
|
1248
1248
|
|
|
1249
1249
|
// Where each organization's readings are forwarded, edited from the admin tab - lib/forwarders.js
|
|
1250
|
-
app.use('/forwarders', createForwardersRouter(db, mqttLogger, () => config.organizations || {}, { loggedInOrFail, can_ADMIN }, { localOpenSearch: () => config.opensearch }));
|
|
1250
|
+
app.use('/forwarders', createForwardersRouter(db, mqttLogger, () => config.organizations || {}, { loggedInOrFail, can_ADMIN }, { localOpenSearch: () => config.opensearch, backfill: backfillOpenSearch, dataDir: config.server.datadir }));
|
|
1251
1251
|
app.use('/opensearch_access', createAccessRouter(() => config.organizations || {}, { loggedInOrFail, can_READ }, () => config.opensearch));
|
|
1252
1252
|
console.log("Mounted Grafana routes at /grafana");
|
|
1253
1253
|
|
package/lib/forwarders.js
CHANGED
|
@@ -212,15 +212,17 @@ export function typesFor(localOpenSearch) {
|
|
|
212
212
|
// configuration (config.d/opensearch.yaml), so those never pass through the browser or the database.
|
|
213
213
|
// A local row on a server that no longer has one is left out.
|
|
214
214
|
export async function enabledConfigs(db, org, localOpenSearch) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
return
|
|
215
|
+
return (await listRows(db, org)).filter((x) => x.enabled)
|
|
216
|
+
.map((r) => loggerConfig(r, localOpenSearch)).filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// One row as the logger takes it, or undefined for a local row on a server that no longer has one
|
|
220
|
+
export function loggerConfig(row, localOpenSearch) {
|
|
221
|
+
const config = JSON.parse(row.config);
|
|
222
|
+
if (row.type !== 'opensearch-local') return { type: row.type, ...config };
|
|
223
|
+
if (!localOpenSearch || !localOpenSearch.node) return undefined;
|
|
224
|
+
const { node, username, password, apiKey, insecure } = localOpenSearch;
|
|
225
|
+
return { type: 'opensearch', node, username, password, apiKey, insecure, ...config };
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
/**
|
|
@@ -229,8 +231,9 @@ export async function enabledConfigs(db, org, localOpenSearch) {
|
|
|
229
231
|
* @param orgs () => the organizations configuration, { org: {...} }
|
|
230
232
|
* @param auth { loggedInOrFail, can_ADMIN } from frugal-iot-server.js, passed in so tests can stand in
|
|
231
233
|
* @param localOpenSearch () => the server's own OpenSearch settings, or undefined (config.d/opensearch.yaml)
|
|
234
|
+
* @param backfill the logger's backfillOpenSearch, and dataDir where the readings are (tests stand in)
|
|
232
235
|
*/
|
|
233
|
-
export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, localOpenSearch = () => undefined } = {}) {
|
|
236
|
+
export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, localOpenSearch = () => undefined, backfill, dataDir } = {}) {
|
|
234
237
|
const router = Router();
|
|
235
238
|
// Whole-organization ADMIN: a forwarder sends every project's readings
|
|
236
239
|
const guard = [auth.loggedInOrFail, auth.can_ADMIN, orgWideAdmin];
|
|
@@ -308,5 +311,41 @@ export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, loc
|
|
|
308
311
|
} catch (err) { next(err); }
|
|
309
312
|
});
|
|
310
313
|
|
|
314
|
+
// Sending the readings already on disk to an OpenSearch forwarder. It can take minutes, so the request
|
|
315
|
+
// only starts it; the card asks how it is getting on. One at a time per forwarder, and the last
|
|
316
|
+
// outcome is kept (in memory - it is gone at a restart) for the card to show.
|
|
317
|
+
const jobs = new Map();
|
|
318
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
319
|
+
|
|
320
|
+
router.post('/:org/backfill', ...guard, orgExists, jsonOnly, express.json({ limit: '1kb' }), async (req, res, next) => {
|
|
321
|
+
try {
|
|
322
|
+
const org = req.params.org;
|
|
323
|
+
if (!backfill || !dataDir) return fail(res, 501, 'This server cannot send earlier readings');
|
|
324
|
+
const { id, since, dryRun } = req.body || {};
|
|
325
|
+
if (since !== undefined && since !== '' && (!DAY.test(since) || Number.isNaN(Date.parse(since)) || new Date(since).toISOString().slice(0, 10) !== since)) return fail(res, 400, 'Give the date as YYYY-MM-DD');
|
|
326
|
+
const row = await get(db, 'SELECT * FROM forwarders WHERE id = ? AND org = ?', [id, org]);
|
|
327
|
+
if (!row) return fail(res, 404, 'No such forwarder');
|
|
328
|
+
if (!['opensearch', 'opensearch-local'].includes(row.type)) return fail(res, 400, 'Only OpenSearch forwarders can be sent earlier readings');
|
|
329
|
+
if (!row.enabled) return fail(res, 400, 'Make the forwarder active first');
|
|
330
|
+
const config = loggerConfig(row, localOpenSearch());
|
|
331
|
+
if (!config) return fail(res, 400, 'This server has no OpenSearch of its own any more');
|
|
332
|
+
const key = `${org}:${row.id}`;
|
|
333
|
+
if (jobs.has(key) && jobs.get(key).running) return fail(res, 409, 'Already sending - wait for it to finish');
|
|
334
|
+
const job = { running: true, sent: 0, dryRun: !!dryRun, since: since || null, startedAt: Date.now() };
|
|
335
|
+
jobs.set(key, job);
|
|
336
|
+
backfill({ config, org, dataDir, since: since || undefined, dryRun: !!dryRun, progress: (n) => { job.sent = n; } })
|
|
337
|
+
.then((r) => { job.sent = r.sent; job.skippedFiles = r.skippedFiles; })
|
|
338
|
+
.catch((err) => { job.error = err.message; console.error(`Backfill for ${org} failed:`, err); })
|
|
339
|
+
.finally(() => { job.running = false; job.finishedAt = Date.now(); });
|
|
340
|
+
console.log('Backfill started by', req.user.username, '-', org, JSON.stringify(row.name), since || 'all', dryRun ? 'dry run' : '');
|
|
341
|
+
res.status(202).json({ started: true });
|
|
342
|
+
} catch (err) { next(err); }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
router.get('/:org/backfill/:id', ...guard, orgExists, (req, res) => {
|
|
346
|
+
res.set('Cache-Control', 'no-store');
|
|
347
|
+
res.json(jobs.get(`${req.params.org}:${req.params.id}`) || { running: false });
|
|
348
|
+
});
|
|
349
|
+
|
|
311
350
|
return router;
|
|
312
351
|
}
|
package/lib/opensearch-access.js
CHANGED
|
@@ -60,12 +60,14 @@ export const namesFor = (org, username) => ({
|
|
|
60
60
|
export function roleFor(org) {
|
|
61
61
|
const n = namesFor(org);
|
|
62
62
|
return {
|
|
63
|
-
cluster_permissions: ['cluster_composite_ops', 'cluster:monitor/main', 'cluster:monitor/health'],
|
|
63
|
+
cluster_permissions: ['cluster_composite_ops', 'cluster:monitor/main', 'cluster:monitor/health', 'cluster:monitor/nodes/info', 'cluster:monitor/state'],
|
|
64
64
|
index_permissions: [
|
|
65
65
|
{ index_patterns: [n.dataPattern],
|
|
66
66
|
allowed_actions: ['read', 'indices:admin/get', 'indices:admin/mappings/get',
|
|
67
67
|
'indices:admin/resolve/index', 'indices:monitor/settings/get'] },
|
|
68
68
|
{ index_patterns: [n.savedIndex, `${n.savedIndex}_*`], allowed_actions: ['indices_all'] },
|
|
69
|
+
// Dashboards looks an alias up before it makes one, and OpenSearch checks that against every index
|
|
70
|
+
{ index_patterns: ['*'], allowed_actions: ['indices:admin/aliases/get'] },
|
|
69
71
|
],
|
|
70
72
|
};
|
|
71
73
|
}
|
|
@@ -156,7 +158,11 @@ export function dashboardsConfig(cfg, org, username, password) {
|
|
|
156
158
|
`opensearch.username: ${JSON.stringify(n.user)}`,
|
|
157
159
|
`opensearch.password: ${JSON.stringify(password)}`,
|
|
158
160
|
`opensearchDashboards.index: ${JSON.stringify(n.savedIndex)}`,
|
|
159
|
-
|
|
161
|
+
// Dashboards uses the login above only for its own housekeeping, and leaves it off requests made for a
|
|
162
|
+
// browser - which would then ask for one. A header set here goes on every request and a browser cannot
|
|
163
|
+
// override it, so nobody is asked.
|
|
164
|
+
'opensearch.requestHeadersAllowlist: []',
|
|
165
|
+
`opensearch.customHeaders: { authorization: ${JSON.stringify('Basic ' + Buffer.from(`${n.user}:${password}`).toString('base64'))} }`,
|
|
160
166
|
].join('\n');
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -200,6 +206,7 @@ export function createAccessRouter(orgs, auth, localOpenSearch = () => undefined
|
|
|
200
206
|
all: has(req, 'ADMIN') ? all : undefined });
|
|
201
207
|
} catch (err) {
|
|
202
208
|
if (err instanceof AccessError) return res.json({ available: false, reason: err.message });
|
|
209
|
+
console.error('OpenSearch Dashboards access: could not reach', cfg.node, '-', err.code || '', err.message);
|
|
203
210
|
return res.json({ available: false, reason: 'OpenSearch did not answer' });
|
|
204
211
|
}
|
|
205
212
|
} catch (err) { next(err); }
|
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"cookie-parser": "^1.4.7",
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"express-session": "^1.19.0",
|
|
15
|
-
"frugal-iot-client": "^2.0.
|
|
16
|
-
"frugal-iot-logger": "^2.0.
|
|
15
|
+
"frugal-iot-client": "^2.0.9",
|
|
16
|
+
"frugal-iot-logger": "^2.0.9",
|
|
17
17
|
"hash-wasm": "^4.12.0",
|
|
18
18
|
"js-yaml": "^4.2.0",
|
|
19
19
|
"morgan": "^1.11.0",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"check-files": "node scripts/check-published-files.js"
|
|
94
94
|
},
|
|
95
95
|
"type": "module",
|
|
96
|
-
"version": "2.0.
|
|
96
|
+
"version": "2.0.9",
|
|
97
97
|
"overrides": {
|
|
98
98
|
"uuid": "^11.1.1"
|
|
99
99
|
}
|
package/public/service-worker.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// it, an installed browser keeps serving the files it already cached - and since the list now covers
|
|
7
7
|
// the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
|
|
8
8
|
// needs this bumped, or it does not reach anyone who already has the old one.
|
|
9
|
-
const CACHE_NAME = 'frugal-iot-cache-2.0.
|
|
9
|
+
const CACHE_NAME = 'frugal-iot-cache-2.0.9';
|
|
10
10
|
/*
|
|
11
11
|
* Everything the app needs, so that a phone on a poor or expensive link fetches it once.
|
|
12
12
|
*
|