Server configuration
weavster server reads an optional YAML configuration file:
weavster server --config /etc/weavster/weavster.yaml
Without --config, the server uses the defaults below. A positional address overrides
listen.address, for example weavster server --config weavster.yaml 127.0.0.1:9090. Put
--config before the address. Anything after the address is rejected with
Error: unexpected arguments.
The file is checked strictly at startup. It must contain exactly one YAML document. An unknown
key, a value of the wrong type, an address that isn't host:port, or an invalid combination (for
example a password policy that forbids every character class) stops the server with exit code 1 and a message like:
Error: config: /etc/weavster/weavster.yaml: yaml: unmarshal errors:
line 3: field dialekt not found in type serverconfig.Store
Example
A single-node server with a durable PostgreSQL store and HTTPS:
listen:
address: 127.0.0.1:8080 # cleartext listener; "" disables it
tlsAddress: 0.0.0.0:8443 # HTTPS listener; "" disables it
requireMarkerHeader: true
tls:
certFile: /etc/weavster/tls/cert.pem
keyFile: /etc/weavster/tls/key.pem
minVersion: "1.2"
store:
dialect: postgres
dsn: postgres://weavster@db.internal:5432/weavster?sslmode=verify-full
maxRetry: 3
retryWaitMs: 1000
auth:
passwordPolicy:
minLength: 12
minUpper: 1
minLower: 1
minNumeric: 1
minSpecial: 1
lockout:
retryLimit: 5
lockoutPeriodSeconds: 300
Startup order
weavster server starts in this order:
- Checks its arguments (a bad flag or an extra argument exits
2) and refuses to run as root unlessWEAVSTER_ALLOW_ROOT=1is set. - Reads and checks the configuration file (a problem exits
1). - Opens the store and applies any pending schema migrations (see Upgrades).
With PostgreSQL, a failed connection or migration is retried per
store.maxRetryandstore.retryWaitMs. - Loads users and creates the first
adminaccount when there are none (see Authentication). - With
flows.deployOnStartup(and a store), deploys every flow that isenabledandundeployedinto itsinitialState(see Flow lifecycle). Flows keep the status they had otherwise. - Binds the API ports (
listen.address,listen.tlsAddress). A port that is taken exits1here, before anything below starts, so no source has taken a file or a message. - Starts the background work, side by side: the recovery pass that resumes messages stored
before the last stop (queued deliveries, and messages that were still being processed) for
flows that are running, then retries every
delivery.retryIntervalMs; and the running flows' sources. New messages can therefore be processed while older ones are still being recovered. - Answers API requests.
Messages of a flow that is not running are not recovered at start; they are finished once the flow is started. The server keeps no jobs or leases of its own to reconcile: everything it must resume is a stored message, which the recovery pass picks up.
Upgrades
A new release upgrades the database schema when it starts (step 3 above), before it accepts any traffic. The data is kept. Each schema change runs in its own transaction, so a failed upgrade leaves the database at the last completed version, and the next start carries on from there. Servers that start together on one database take turns: only the first applies the upgrade. The database records which weavster release applied each schema version.
Some upgrades rebuild a table's indexes, and PostgreSQL locks the table while they run: other servers using the same database wait for its messages until the upgrade ends (on a large store, minutes). Upgrading to this release does this for the message tables on PostgreSQL (message ids now compare byte by byte, the order searches use). Stop the other servers, or upgrade at a quiet time.
A database that a newer release has already upgraded is refused, and nothing in it changes.
The server exits 1 at once (this is not retried) with:
Error: store: state: the database schema is at version 10 (written by weavster 0.3.0), newer than this release supports (9): run weavster 0.3.0 or later, or restore a backup taken before the upgrade
So you can't roll back to an older release by just starting it again. Back up the database before upgrading, and to go back, restore that backup and start the older release.
Keys
Keys you leave out keep their default.
listen
| Key | Default | Description |
|---|---|---|
address |
127.0.0.1:8080 |
Cleartext HTTP host:port. The port is a number from 1 to 65535 or a service name such as http; 0 (a random port) is rejected. Set to "" to serve HTTPS only. |
tlsAddress |
"" (off) |
HTTPS host:port, with the same port rules as address. Requires tls.certFile and tls.keyFile. |
requireMarkerHeader |
true |
Require X-Weavster-CSRF: 1 on every /api/v1 request. Requests without it get 400. |
shutdownTimeoutMs |
10000 |
On SIGINT/SIGTERM, how long to wait for in-flight requests and the retry worker before closing connections and exiting (1–600000). |
contextPath |
"" |
Serve everything under this path, for example /weavster behind a proxy that routes by path: the API is then /weavster/api/v1/…, the OpenAPI document /weavster/api/openapi.yaml, and metrics /weavster/metrics. Other paths answer 404. It starts with / and does not end with one. A flow's own HTTP source listens on its own address and is not affected. |
At least one of address and tlsAddress must be set.
tls
| Key | Default | Description |
|---|---|---|
certFile |
"" |
PEM certificate (chain) for the HTTPS listener. |
keyFile |
"" |
PEM private key matching certFile. |
minVersion |
"1.2" |
Lowest accepted TLS version: "1.2" or "1.3". Quote the value so YAML reads it as a string. |
With TLS 1.2, only ECDHE AES-GCM cipher suites are offered. The certificate and key are loaded
before any listener starts. If they can't be read, the server exits 1 with Error: tls: …
and serves nothing.
store
| Key | Default | Description |
|---|---|---|
dialect |
memory |
memory, postgres, or disabled. |
dsn |
"" |
For postgres, a URL such as postgres://weavster@db:5432/weavster?sslmode=verify-full. |
dsnEnv |
"" |
For postgres, instead of dsn: the name of a secret holding the URL, so its password stays out of this file. Letters, digits, and _, not starting with WEAVSTER_DB_ or WEAVSTER_SOURCE_ (flows may read those), for example WEAVSTER_STORE_DSN. |
maxConnections |
10 |
Maximum open PostgreSQL connections. |
maxRetry |
3 |
PostgreSQL only: extra connection attempts after the first failure. |
retryWaitMs |
1000 |
PostgreSQL only: wait between attempts, in milliseconds. |
memory: messages, flow definitions, and users live in process memory and are lost on restart.postgres:dsnordsnEnvis required (not both), for examplepostgres://weavster@db.internal:5432/weavster?sslmode=verify-full(tested with PostgreSQL 16). The server creates and migrates its tables on the first start; the account needs to create tables in the database (or in the schema named bysearch_pathin the URL, for example…&search_path=weavster). Keep the password out of the file: put the whole URL in a secret and name it withdsnEnv, or put the password in~/.pgpassof the server's account, on a line for the database's host only. A missing secret counts as a failed connection attempt (maxRetry), and the server exits1naming it when the attempts run out.disabled: runs with no message store.GET /api/v1/messagesreturns503 messages unavailable. Flow definitions and users are kept in memory.sqliteis no longer a server store. The server exits1withError: config: store.dialect sqlite is no longer supported: use postgres for a durable store, or memory. Thepathssection (paths.dataDirnamed the SQLite file's directory) was removed with it: delete it from the file, or the server exits1withfield paths not found. See Move from SQLite to PostgreSQL.
If every PostgreSQL attempt fails, the server exits 1. SIGINT/SIGTERM during the retries
stops the server immediately with exit code 0. When every attempt fails:
Error: store: postgres: giving up after 4 attempts: ...
The store holds messages, flow definitions, and users. With postgres, flows and
users (including password changes and lockouts) survive a restart.
Move from SQLite to PostgreSQL
To move a server from SQLite to PostgreSQL, save its setup while the old release is still
running (the new one does not start with sqlite):
- Export the configuration with the config map:
GET /api/v1/config/export?includeConfigMap=true(see Export and import). - Save every lookup group.
GET /api/v1/lookupslists the groups with their number of entries, andGET /api/v1/lookups/{group}?limit=10000returns up to 10,000 entries of one as{key: value}(see Dynamic lookups). A group with 10,000 entries or more does not fit in one response: save it in parts withprefix, one part per first character of the keys (for exampleprefix=A,prefix=B, …), and split any part that returns exactly 10,000 entries by a longer prefix (prefix=AB, …). Check that the saved entries of each group add up to its number of entries before you upgrade. - Upgrade, set
store.dialect: postgresandstore.dsn, removepaths, and start the server. - Import the configuration with
POST /api/v1/config/import?overwriteConfigMap=true, and each group withPOST /api/v1/lookups/{group}/import.
Messages and users are not moved: create the users again (see Authentication).
delivery
| Key | Default | Description |
|---|---|---|
maxAttempts |
5 |
Attempts per destination, including the first, before a message is dead-lettered. |
backoffBaseMs |
1000 |
Delay before the first retry; it doubles for each further retry, up to one minute. |
retryIntervalMs |
1000 |
How often the server checks for retries that are due. |
maxAttempts must be 1–1000; the two intervals must be 1–3,600,000 ms (one hour). See Processing messages.
processing
Bounds how many messages the server receives and processes at once, across the API and every flow source, so a burst queues at the senders instead of piling up in the server.
| Key | Default | Description |
|---|---|---|
maxConcurrent |
32 |
Messages received and processed at the same time (1–10000). |
waitMs |
5000 |
How long a message that arrives while all are busy waits for its turn (0–60000; 0 refuses it at once). |
processing: {maxConcurrent: 64, waitMs: 2000}
A message still waiting after waitMs is refused as busy, and the sender tries again:
- the API and http sources answer
503withRetry-After: 1and{"error":{"code":"SERVICE_UNAVAILABLE","message":"the server is busy: too many messages are being processed; retry shortly"}}; - an mllp source answers
AEwithserver busy; - a file or database source keeps the file or row and tries it again a moment later (this is
throttling, not a failure: no
source.database.failedevent).
The server logs processing limit reached: messages refused as busy at most every 10 seconds
while it refuses messages, with how many it refused; a message waiting for a slot also stops
waiting when the server shuts down. A flow's own lock is taken first, so a flow being stopped or
changed holds up only its own messages, and a message for an unknown or stopped flow is answered
at once (404/409) without waiting.
A message a flow destination hands to another flow is processed in the sender's turn, so a chain
of flows never waits for itself. An http or mllp destination that sends to a source of the same
server needs a second turn while the sender holds its own: with every turn taken, such
deliveries are refused and retried. Use a flow destination to pass messages between flows of
one server. Retries of queued deliveries run one message at a time outside
this limit. Raise maxConcurrent when senders often see 503/AE and the destinations can take
more parallel traffic; lower it to protect slow destinations or a small database.
flows
| Key | Default | Description |
|---|---|---|
deployOnStartup |
true |
At startup, deploy and start every flow that is enabled and undeployed. See Flow lifecycle. |
prune
Removes old messages from the store (see Prune old messages). Off by default.
| Key | Default | Description |
|---|---|---|
maxAgeHours |
0 (off) |
Remove messages received more than this many hours ago (1–876000). |
maxMessages |
0 (off) |
Keep at most this many messages in the store; the oldest finished ones are removed first. Unfinished messages count toward the limit but are never removed. |
intervalMinutes |
60 |
How often a pass runs (1–10080, one week). The first pass runs one interval after the server starts. |
auditMaxAgeDays |
0 (keep) |
Remove stored audit entries older than this many days (1–36500). |
eventMaxAgeDays |
0 (keep) |
Remove stored events older than this many days (1–36500). |
Pruning needs a message store: with store.dialect: disabled a prune section stops the server at startup.
prune: {maxAgeHours: 720, intervalMinutes: 60} # keep 30 days
Only messages that are done are removed: sent, filtered, errored, and dead-lettered.
Messages that are received, transformed, or queued are never pruned.
stats
| Key | Default | Description |
|---|---|---|
sampleIntervalMs |
60000 |
How often every flow's lifetime statistics are sampled for statistics over time. |
retentionHours |
24 |
How long samples are kept. |
sampleIntervalMs must be 100–3,600,000 ms (one hour) and retentionHours 1–8760 (one year),
and together they may keep at most 100,000 samples per flow (for example, a 1-second interval
allows up to 27 hours). Samples are held in memory, at most 1,000,000 for all flows together; past
that the oldest are dropped. With store.dialect: postgres they are also stored, together with
each flow's current and lifetime statistics, and loaded again at start; stored samples older than
retentionHours are removed at every sample.
stats:
sampleIntervalMs: 10000 # every 10 seconds
retentionHours: 48
secrets
| Key | Default | Description |
|---|---|---|
dir |
/run/secrets |
Where secret files are read from. An absolute path. |
A secret named NAME (a flow's dsnEnv or passwordEnv, or store.dsnEnv) is the server's
environment variable NAME or, when that is not set, the file NAME in secrets.dir. One
trailing newline in the file is ignored. /run/secrets is where Docker and Kubernetes mount
secrets, so a secret mounted there with the name a flow uses needs no other setup:
store:
dialect: postgres
dsnEnv: WEAVSTER_STORE_DSN # the file /etc/weavster/secrets/WEAVSTER_STORE_DSN
secrets:
dir: /etc/weavster/secrets
install -m 0600 /dev/null /etc/weavster/secrets/WEAVSTER_STORE_DSN
echo 'postgres://weavster:the-password@db.internal:5432/weavster?sslmode=verify-full' > /etc/weavster/secrets/WEAVSTER_STORE_DSN
- An empty variable or file counts as not set; an empty variable does not hide the file. A missing
secret is reported as
environment variable NAME is not set, and there is no file /etc/weavster/secrets/NAME, and a file the server's account cannot read assecret NAME: … permission denied. - Database connection strings are read for every message and poll, so a changed secret applies at once. An http source reads its password when its port opens: after changing it, stop and start the flow.
- If
secrets.diris set to something other than/run/secretsand is not a directory, the server logs a warning at start and reads secrets from environment variables only. - Keep the directory readable by the server's account only (
chmod 0700), and each file0600. - A secret file may be a link to another file in the directory (as Kubernetes mounts secrets); a
link to anywhere outside it is refused with
secret NAME: … links to outside the secrets directory. - Only one trailing line ending (
\nor\r\n) is removed.
auth
| Key | Default | Description |
|---|---|---|
passwordPolicy.minLength |
8 |
Minimum password length in characters (0 = none). |
passwordPolicy.minUpper / passwordPolicy.minLower / passwordPolicy.minNumeric / passwordPolicy.minSpecial |
1 / 1 / 1 / 0 |
Required characters of each class. -1 forbids the class. A special character is anything that is not a letter (of any script) or digit, spaces included. GET /api/v1/system/password-requirements shows the rules in words. |
lockout.retryLimit |
5 |
Failed logins before lockout (0 = never lock). |
lockout.lockoutPeriodSeconds |
300 |
Lockout duration in seconds. |
These settings apply to the first-run admin password and to every login. See
Authentication.
Root accounts
The server refuses to run as root. Set the environment variable WEAVSTER_ALLOW_ROOT=1 to
override this.