Info
License: Free — part of the universal collection tier.
File Tail Probe¶
The filetail probe tails flat log files — application logs, web
server access logs, anything line-oriented — and ships each record
as a structured OTel log record through the
OTLP storage. It handles glob patterns, log rotation,
multiline records (stacktraces), and optional structured parsing
(regex, JSON, logfmt) with timestamp extraction.
Works on Linux, Windows and macOS; files are opened in shared-read mode so the producing application is never blocked.
Quick start¶
# probes.d/10-filetail.yaml — each file under probes.d/ is a YAML array of probes
- name: app-logs
type: filetail
params:
paths:
- /var/log/myapp/*.log
bookmark_path: /var/lib/senhub-agent/filetail.bookmark
Parameters¶
| Parameter | Must set | Default | Description |
|---|---|---|---|
paths |
Yes | - | File paths or glob patterns, re-expanded every 15 seconds. Example: /var/log/app/*.log |
bookmark_path |
No | - | File persisting read offsets across restarts; use a distinct one per instance. Example: /var/lib/senhub-agent/filetail-app.json |
from_beginning |
No | false |
Read existing content the first time a file is seen |
max_bytes_per_line |
No | 1048576 |
Cap on one record after multiline folding, in bytes |
multiline |
No | - | Fold continuation lines into one record |
multiline.pattern |
No | - | Regular expression tested against each line. Example: ^\d{4}-\d{2}-\d{2} |
multiline.negate |
No | false |
Invert the pattern match |
multiline.match |
No | after |
Whether a matching line starts a record or flushes the previous one. One of after, before |
parser |
No | - | Structured parsing of each record |
parser.type |
No | raw |
Record format. One of raw, regex, json, logfmt |
parser.pattern |
No | - | Regular expression with named groups; required for the regex type |
parser.timestamp_field |
No | - | Field carrying the record timestamp |
parser.timestamp_format |
No | - | Go reference-time layout of that field. Example: 2006-01-02 15:04:05 |
Without bookmark_path the probe tails from the end of each file on every
start. from_beginning only applies to a file no bookmark knows yet.
A path that does not exist yet, or whose directory is not there yet (a
mount that comes up after the agent), is picked up by the rescan once it
appears. Such a file is read from its first line, whatever
from_beginning says, since all of it was written after the probe
started watching for it.
Multiline folding¶
Java stacktraces, Python tracebacks and pretty-printed payloads span
several physical lines. The multiline block folds them into one
record:
params:
paths: [/var/log/myapp/server.log]
multiline:
pattern: '^\d{4}-\d{2}-\d{2}' # a timestamp starts a new record
negate: false
match: after
With match: after, a matching line starts a new record and non-matching
lines are continuations. With match: before, a matching line ends the
record: it is added to it, then the record is sent. negate: true inverts
the test, so a line that does not match the pattern is the one that
starts (or ends) a record.
The pattern describes the line that starts a record, which is not how
Filebeat reads the same keys. A Filebeat configuration for Java logs,
pattern: '^\[' with negate: true and match: after, describes the
continuation lines; here the same record is written with negate: false:
multiline:
pattern: '^\[' # "[2026-09-29T18:21:51,024+02:00] ..." starts a record
negate: false
match: after
Copied as is from Filebeat, negate: true takes every stack-trace line
for the start of a new record.
Structured parsing¶
params:
paths: [/var/log/nginx/access.log]
parser:
type: regex
pattern: '^(?P<remote_addr>\S+) \S+ \S+ \[(?P<time_local>[^\]]+)\] "(?P<request>[^"]*)" (?P<status>\d+)'
timestamp_field: time_local
timestamp_format: "02/Jan/2006:15:04:05 -0700"
With regex, each named capture group becomes a log attribute. With
json and logfmt, every key becomes a log attribute. With raw, the
line is the record body, unparsed. timestamp_field names a capture group
or a JSON/logfmt key.
Operational notes¶
- Rotation-safe. Each file is fingerprinted (hash of its first bytes), so copytruncate, daily rotation and file replacement are detected and the probe restarts from offset 0 of the new file — no silent gaps, no duplicates.
- At-least-once delivery. Bookmarks are flushed every 2 seconds; after a crash, up to 2 seconds of records may be re-read. Plan deduplication downstream if exact-once matters.
- Event-driven. No polling cycle: records ship as lines are written.
- Start position. Default is tail-from-now. Set
from_beginning: truefor files whose full history matters on first ingestion (combine withbookmark_pathso it only happens once).
Detecting a tail that no longer reads¶
For every file it follows, the probe reports two gauges carrying the
file's absolute path in the log.file.path attribute (log_file_path
label in Prometheus): senhub.filetail.read_offset, the byte offset the
tail has read up to, and senhub.filetail.file_size, the size of the file
at collection time. A healthy tail has an offset equal to the size, give
or take the lines being written at that instant: on an active file a
non-zero gap is normal, because the tail reads while the writer writes
(several kilobytes on a large busy log). What is abnormal is a tail whose
offset stops moving while the file grows, a condition the number of
records emitted cannot show, since a quiet file and a stuck tail both
leave it flat. A path that does not exist yet or cannot be read reports neither
metric (it is reported as a probe error instead); when the file cannot be
sized, only the offset is reported.
In Prometheus the metrics are senhub_filetail_read_offset_bytes and
senhub_filetail_file_size_bytes. This expression
fires when, over 15 minutes, the file grew and the offset did not move:
changes(senhub_filetail_read_offset_bytes[15m]) == 0
and on (instance, probe_name, log_file_path)
delta(senhub_filetail_file_size_bytes[15m]) > 0
The labels on each series are log_file_path, probe_name and
probe_type; instance is added by your Prometheus when it scrapes the
agent (use the label your backend attaches to identify the host, for
example host_id when the metrics arrive through OTLP). Both gauges
carry the same labels; compare the series count of each side before
relying on the rule.
Both are gauges, hence changes() and delta() rather than
increase(), which would treat any drop as a counter reset. Across a
rotation the size drops, so the window containing the switch stays
silent; a real freeze lasts hours and is caught on the next window. Do
not alert on file_size - read_offset > 0: that gap is rarely zero on a
busy file.
Give the rule a for: 15m. The two gauges go stale together, but a
restart can leave one a sample behind the other for a moment; the hold
absorbs that, and a real freeze lasts far longer. The rule also stays
quiet across an agent upgrade, even where version labels are attached to
every series: the old series stops with both its gauges frozen, so
delta(size) is zero and the and keeps it silent.
Metric reference¶
Every metric this probe can emit. Metric is the OpenTelemetry name the
OTLP, Prometheus and Zabbix outputs derive theirs from. Name is what a
Nagios check and the API metrics= filter match.
PRTG channel is the label PRTG shows, placeholders filled from the
series' tags.
| Metric | Name | PRTG channel | Unit | Description |
|---|---|---|---|---|
senhub.filetail.records_emitted |
senhub.filetail.records_emitted |
File Tail Records Emitted | # | Cumulative count of log records this file-tail probe has published to the log rail |
senhub.filetail.read_offset |
senhub.filetail.read_offset |
File Tail Read Offset | bytes | Byte offset the tail of a followed file has read up to |
senhub.filetail.file_size |
senhub.filetail.file_size |
File Tail File Size | bytes | Current size in bytes of a followed file; a healthy tail has a read offset equal to it |