14 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. It is the source of the
built-in pipe pipeline, and of every spec pipeline given no --source.
[[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.
Lines:
- Each line is one entry, its terminator (
\nor\r\n) removed; the formatter writes one back. An unterminated last line is kept. - Blank lines are skipped.
- A line over 1 MiB continues in the next entry; no byte is lost.
- The level is inferred from the line text, and
Sourceis set toconsole.
Delivery and lifetime:
- Nothing is dropped: the source waits for its pipeline, so a slow pipeline slows the reading of stdin.
- One reader serves the process. A reload's new source continues where the old one stopped; no line is read twice. Hence at most one console source in the whole configuration, checked at validation.
- At the end of input the source ends, and with it a pipeline whose sources
have all ended; lw exits once every pipeline has
(CLI). Under a supervisor whose stdin is
/dev/nullthat is at once.
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.acl(table):allowanddeny, addresses or CIDRs admitted and refused before TLS, andproxy_protocolwithproxy_fromfor the client a PROXY header names; 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, — under auth —auth_methodandauth_identity, and behind a PROXY header the proxy aspeer_addr. - 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; with acl rules or proxy_from also acl,
acl_denied and acl_proxy_headers.
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.acl(table):allowanddeny, addresses or CIDRs admitted and refused before TLS, andproxy_protocolwithproxy_fromfor the client a PROXY header names; 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; with acl rules or
proxy_from also acl, acl_denied and acl_proxy_headers.
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.