Monitoring file age with OpenTelemetry — and shipping it to Sluicio

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.