12 KiB
Input Sources
Sources produce core.LogEntry values for a pipeline. Every source is declared
as a [[pipelines.plugin_sources]] entry with an id, a type, and a
type-specific config table.
[[pipelines.plugin_sources]]
id = "app_logs"
type = "file"
[pipelines.plugin_sources.config]
directory = "/var/log/myapp"
Registered types: file, console, random, null, tcp_chain,
http_chain.
Publication from any source is non-blocking. When a subscriber channel is full
the entry is dropped and counted in dropped_entries.
file
Tails every file in a directory whose name matches a glob.
[[pipelines.plugin_sources]]
id = "app_logs"
type = "file"
[pipelines.plugin_sources.config]
directory = "/var/log/myapp"
pattern = "*.log"
check_interval_ms = 100
raw = false
from = "end"
Options, each as type and default:
directory(string, required): directory to scan; not recursive.pattern(string,*): glob over filenames;*and?only.check_interval_ms(int,100): directory rescan interval; minimum10.raw(bool,false): never parse a line: the whole line is the message.from(string,end): where a new watcher starts,endorstartof the file.
Behaviour
check_interval_msgoverns how often the directory is rescanned for new or removed files. Tailing an already-open file polls on a fixed 100 ms interval that this option does not change.- Each matched file gets its own watcher. Watchers for files that disappear are stopped and removed on the next scan.
- A new watcher seeks to end-of-file. Positions live in memory only, so a
restart resumes from the current end of each file and content written while
LogWisp was down is not read.
from = "start"reads each file whole when its watcher is created instead — what a process writing beside LogWisp needs, at the cost of replaying a file already on disk at every restart. - Rotation is detected from size decrease, modification-time reset, a position beyond end-of-file, or an inode change. An inode change where the new file is already larger than the recorded position is treated as an atomic save, not a rotation, and the position is preserved.
- A rotation that renames in place — what a size-capped writer does — puts the
same inode back under a name
patternalso matches. Its watcher resumes at the position the original reached, sofrom = "start"reads the tail an unfinished read left behind rather than the whole archive a second time. - A line is parsed as JSON only when it is an object whose top-level keys are
all drawn from
time,level,msgandfields— the four an entry can carry.timeis read as RFC3339Nano. Any other key, and any non-object line, is kept whole as text with the level inferred from common markers ([ERROR],WARN:, and so on), because parsing it would drop the rest. raw = trueskips the JSON branch entirely. The line, plus its newline, becomes the message;fieldsstays empty, the time is the read time, and the level is inferred from the text as for any unparsed line. Paired withformat.type = "raw"this is byte-exact transport for records LogWisp's envelope cannot hold — see Formatters.Sourceis set to the file's base name.
Statistics: per-watcher size, position, entries read, rotation count, and
last read time, plus active_watchers.
console
Reads newline-delimited entries from standard input.
[[pipelines.plugin_sources]]
id = "stdin"
type = "console"
[pipelines.plugin_sources.config]
buffer_size = 1000
Option, as type and default:
buffer_size(int,1000): subscriber channel depth.
At most one instance per pipeline: the type is registered with
MaxInstances: 1, and a second instance is rejected at pipeline construction.
The level is inferred from the line text, and Source is set to console.
random
Synthetic entry generator for development, smoke tests, and sanitizer testing.
[[pipelines.plugin_sources]]
id = "generator"
type = "random"
[pipelines.plugin_sources.config]
interval_ms = 500
jitter_ms = 0
format = "txt"
length = 20
special = false
Options, each as type and default:
interval_ms(int,500): emission period.jitter_ms(int,0): symmetric jitter; must be non-negative, clamped tointerval_ms.format(string,txt):raw(message only),txt(bracketed line) orjson(JSON object as the message).length(int,20): message length in characters.special(bool,false): inject control and non-ASCII characters.
special = true is the intended way to exercise sanitizer policies: it inserts
control bytes and multi-byte Unicode into otherwise ordinary messages. Levels
are chosen at random from DEBUG, INFO, WARN, ERROR.
null
Produces nothing. Useful as a placeholder so a sink-only pipeline satisfies the "at least one source" requirement.
[[pipelines.plugin_sources]]
id = "void"
type = "null"
No options.
tcp_chain
Listens for persistent NDJSON streams from upstream LogWisp tcp_chain sinks.
See Chaining for the protocol.
[[pipelines.plugin_sources]]
id = "ingest_tcp"
type = "tcp_chain"
[pipelines.plugin_sources.config]
host = "0.0.0.0"
port = 15801
buffer_size = 1000
max_connections = 0
read_timeout_ms = 0
hello_timeout_ms = 10000
trust_node = true
[pipelines.plugin_sources.config.tls]
enabled = true
cert_file = "/etc/logwisp/tls/server.crt"
key_file = "/etc/logwisp/tls/server.key"
client_auth = true
client_ca_file = "/etc/logwisp/tls/client-ca.crt"
min_version = "1.3"
[pipelines.plugin_sources.config.auth]
type = "mtls"
allow = ["edge-01", "edge-02"]
node_binding = "force"
Options, each as type and default:
host(string,0.0.0.0): bind address, IPv4 or IPv6 (::); the listener keeps to its family (Networking).port(int, required): listen port, 1–65535.buffer_size(int,1000): subscriber channel depth.max_connections(int,0): concurrent connection cap;0= unlimited.read_timeout_ms(int,0): per-connection idle read deadline;0= none.hello_timeout_ms(int,10000): deadline for the hello preamble.trust_node(bool,true):falseoverrides the sender's node label with its remote address.auth.node_bindingassertorforcetakes over the connection label; onlyforcealso overrides the per-entry labels;noneleaves both totrust_node.tls(table): listener TLS; see Security.auth(table): peer authentication (mtlsorscram) and node binding; see Security.
For passwords instead of certificates, the auth block takes type = "scram"
and a credentials_file, and client_auth becomes optional; see
Password Authentication.
Behaviour
- TLS handshakes run explicitly with a 10 s bound before the preamble is read,
after the
max_connectionsadmission check. - A connection is rejected if the first line is not a valid hello with a matching protocol version.
- Under
mtlsthe certificate is checked before the hello is read, so an unauthorized peer never gets a preamble parsed on its behalf. Underscramthe hello opens the login: challenge, proof and final follow on the same connection, the whole exchange withinhello_timeout_ms, and the final is sent only once node binding agrees. A source withoutscramanswers a hello offering credentials withauthentication not enabled. A rejection is logged at WARN and counted inrejected_conns. - The node label is then resolved: under
auth.node_bindingit comes from the peer's certificate identity or username, otherwisetrust_nodegoverns.forcealso overrides thenodefield on every individual entry;assertleaves per-entry labels totrust_node, so a relay can forward other nodes' entries while proving its own identity. - Each accepted connection gets a session recording the remote address, node
label, — under TLS —
tlsandtls_peer_cn, and — under auth —auth_methodandauth_identity. - A malformed entry line increments
parse_errorsand is skipped; the connection survives. A line over 1 MiB is a protocol violation and terminates the connection.
Statistics: active_connections, rejected_conns, parse_errors,
tls_handshake_errors, trust_node, auth, auth_allowed, auth_rejected,
node_binding; under scram also auth_users, auth_throttled, auth_busy
and auth_binding_mismatch.
http_chain
Accepts NDJSON batches POSTed by upstream LogWisp http_chain sinks.
[[pipelines.plugin_sources]]
id = "ingest_http"
type = "http_chain"
[pipelines.plugin_sources.config]
host = "0.0.0.0"
port = 15802
ingest_path = "/ingest"
buffer_size = 1000
max_body_bytes = 8388608
read_timeout_ms = 30000
trust_node = true
[pipelines.plugin_sources.config.tls]
enabled = true
cert_file = "/etc/logwisp/tls/server.crt"
key_file = "/etc/logwisp/tls/server.key"
client_auth = true
client_ca_file = "/etc/logwisp/tls/client-ca.crt"
[pipelines.plugin_sources.config.auth]
type = "mtls"
allow = ["edge-01", "edge-02"]
node_binding = "force"
Options, each as type and default:
host(string,0.0.0.0): bind address, IPv4 or IPv6 (::); the listener keeps to its family (Networking).port(int, required): listen port, 1–65535.ingest_path(string,/ingest): endpoint path; must start with/, and/authis reserved for the login.buffer_size(int,1000): subscriber channel depth.max_body_bytes(int,8388608): per-request body cap (8 MiB).read_timeout_ms(int,30000): full request read deadline.trust_node(bool,true):falseoverrides the sender's node label with its remote address.auth.node_bindingassertorforcetakes over the connection label; onlyforcealso overrides the per-entry labels;noneleaves both totrust_node.tls(table): listener TLS; see Security.auth(table): peer authentication (mtlsorscram) and node binding; see Security.
Behaviour
- Only
POSTtoingest_pathand to/authis routed; other methods get405with anAllowheader, and other paths get404./authis the SCRAM login and answers404unlessauth.type = "scram". - Authorization runs before the body is read, so an unauthorized sender does not
get to stream
max_body_bytesinto the process. A missing, invalid or expired bearer token (scram) answers401, so the sender logs in again; a refused certificate, a certificate that does not match the token's user, and a node-binding failure answer403. Both are distinct from the400used for protocol errors, so a sender can tell "not allowed" from "malformed batch". - A missing or mismatched
X-Logwisp-Protocolheader is rejected with400. - Batch acceptance is atomic: entries are published only after the body reads
cleanly end to end. A transfer error rejects the whole batch (
400, or413when the body cap is hit) so the sender retries it. A malformed line inside an otherwise clean transfer is skipped and counted inparse_errors. - Success is
204 No ContentwithX-Logwisp-Acceptedset to the number of entries ingested. - Sessions are cached per remote host + node + authenticated identity, and recreated after idle expiry. Including the identity in the key means two peers sharing a remote address never share a session.
Statistics: total_requests, rejected_requests, parse_errors,
cached_sessions, trust_node, auth, auth_allowed, auth_rejected,
node_binding; under scram also auth_users, auth_throttled, auth_busy,
auth_binding_mismatch and auth_token_lifetime_ms.
Source Statistics
Every source reports: id, type, total_entries, dropped_entries,
start_time, last_entry_time, and a type-specific details map. The DEBUG
status report spreads details into each plugin's line.