File-drop integrations fail silently. The sender writes an XML file to a share, the receiver picks it up — and when the receiver dies, nothing errors. Files just sit there, getting older. The best early-warning signal for this class of integration isn’t an error log; it’s file age: how long has the oldest file in the inbox been waiting?
This post shows how to turn file age into an OpenTelemetry metric using nothing but the Collector — no custom code — and how to export it to Sluicio so it becomes a health signal on your integration.
The filestats receiver
The OpenTelemetry Collector Contrib distribution ships a filestats receiver that scrapes file metadata on an interval and emits it as metrics — most usefully file.mtime (last-modified time as a Unix timestamp) and file.count.
receivers:
filestats:
include: /data/inbox/**/*.xml # ** recurses into child folders
collection_interval: 60s
metrics:
file.mtime:
enabled: true
file.count:
enabled: true
Including child folders
Recursion is just a glob: ** matches any number of nested directories, so /data/inbox/**/*.xml covers /data/inbox/a.xml, /data/inbox/customer-a/2026/a.xml, and everything in between. Use /data/inbox/**/* to match all file types, or list several patterns:
receivers:
filestats:
include:
- /data/inbox/**/*.xml
- /data/outbox/**/*.csv
collection_interval: 60s
Each matched file becomes its own resource with file.name and file.path attributes, so you can see exactly which file is stale.
Turning mtime into age — inside the Collector
file.mtime is a timestamp, not an age. You could compute now() − mtime at alert time in the backend, but it’s friendlier to ship an actual age metric. The transform processor does this with two OTTL statements — subtract the mtime from the current time on each datapoint, then rename the metric:
processors:
transform/file_age:
metric_statements:
- context: datapoint
statements:
- set(value_int, UnixSeconds(Now()) - value_int) where metric.name == "file.mtime"
- context: metric
statements:
- set(unit, "s") where name == "file.mtime"
- set(name, "file.age.seconds") where name == "file.mtime"
The datapoint statement runs first (the metric is still called file.mtime there), then the metric-level statements fix the unit and rename it. What leaves the Collector is a clean gauge: file.age.seconds, one series per file, recomputed every collection interval.
A note on cardinality: one series per file is fine for typical file-drop folders (tens to hundreds of files). If a backlog can grow into thousands of files, tighten the include pattern per pipeline, or raise the collection_interval — the alert-worthy signal is the oldest file, and that survives coarser scraping.
Exporting to Sluicio
Sluicio ingests standard OTLP, so no vendor-specific exporter is needed — point an otlphttp exporter at your cell’s ingest endpoint with your API key. The full Collector config, end to end:
The resource processor matters more than it looks: service.name is the join key. Set it to the same service name your traces use (here receive-file), and the metric attaches to the right service inside your Sluicio integration instead of floating around unassociated.
Alerting on it in Sluicio
Once file.age.seconds lands in the metric catalog, add a health check on the service — the same pattern Sluicio’s built-in system types use (e.g. RabbitMQ’s queue-backlog check):
- Metric:
file.age.seconds, aggregation:max, condition:> 3600, severity: warning — “a file has been waiting over an hour.” - Add a second, critical threshold (say 4 hours) for escalation.
- Pair it with
file.count(max > 500) to catch backlogs where files are arriving faster than they’re consumed, even if none are old yet.
Because every file carries its file.path, one check covers every child folder — a stale file in /data/inbox/customer-b trips the alert even while other folders flow normally, and the offending path is right there in the alert.
Wrapping up
File age is the heartbeat of file-based integrations, and the Collector handles the whole job: filestats watches the directory tree (child folders included via **), the transform processor turns timestamps into a proper age gauge, and one otlphttp exporter block ships it to Sluicio. Add a health check on file.age.seconds, and your quiet file drops can never fail silently again.