Real-Time Monitoring
The Activities tab in the Web Admin Console shows live per-user bandwidth, transfer events, and session counts. This chapter covers how the monitoring subsystem is wired, how to configure it, how to read the charts, and how to disable it if it ever needs to be ruled out as a contributor to a performance incident.
Overview
Three charts share one toolbar (range combo, user combo, refresh button) and auto-refresh every 10 seconds:
- Upload & Download Activity — count of completed file transfers per time bucket. Two lines: uploads (green), downloads (blue).
-
Session Activity —
count of successful logins per time bucket. One line (orange).
Sourced from
AUTH_SUCCESSevents in the audit log (which carry usernames), not fromSESSION_CONNECT(which fire before authentication and have a NULL username). - Bandwidth — bytes per second per time bucket, measured by sampling the live byte counter rather than dividing transfer totals by duration. Two lines: upload throughput (green), download throughput (blue). Y-axis auto-scales between B/s, KB/s, and MB/s based on the displayed range.
All three honour the range combo (Last hour, Last 24 hours, Last 7 days, Last 30 days) and the user combo (All users plus every user with measurable activity).
Architecture
Bandwidth measurement is a four-stage pipeline:
-
Counting stream wrappers tap every read and write at
the
StorageBackendinterface. Both the local filesystem backend and the JDBC (database) backend wrap their streams withCountingInputStream/CountingOutputStream. SFTP, FTP, WebDAV, and the browser Portal all route through one of these backends, so a single hook covers every protocol. -
Bandwidth meter — a process-global
ConcurrentHashMapof per-userAtomicLongpairs (bytes up, bytes down). The hook callback resolves the username to auser_idthrough a Caffeine cache (10k entries, 1h TTL) and increments the matching counter. Lock-free; about ten nanoseconds per chunk write on modern hardware. -
Sampler — a daemon thread on a fixed schedule
(5 seconds by default). On each tick it atomically snapshots every
non-zero counter pair (resetting them to zero in the same call),
computes the wall-clock
dt_mssince the previous tick, and inserts one row per active user intosftp_throughput_samplesin the audit database. The drain-and-reset semantics mean each row represents a delta, not a cumulative — there is no cumulative state to lose on restart and no negative-delta edge case. -
REST query path — the Web Admin's Bandwidth
chart calls
GET /api/throughput/timeseries, which sumsbytes_up/bytes_downper requested bucket (with optionaluserfilter) and divides bySUM(dt_ms) / 1000to return a true average rate for each bucket.
dt_ms matters.
Storing the actual elapsed time per sample (not the nominal 5 s)
keeps the rate calculation accurate when scheduler jitter or GC pauses
stretch a tick to 5.1 s or 6 s. Without it, those samples would inflate
the apparent rate during the busy hours when accuracy matters most.
Configuration
The monitoring subsystem is configured via the
<bandwidth-sampler> element in
sftp-server.xml. Defaults: enabled, 5-second
sampling. The element is shipped commented-out in the
default configuration; uncomment to customise.
<sftp-server>
...
<bandwidth-sampler>
<enabled>true</enabled>
<interval-seconds>5</interval-seconds>
</bandwidth-sampler>
...
</sftp-server>
enabled
When false, the sampler thread does not start and the
retention worker does not run. The byte-counter hooks at the storage
backends are still installed in the I/O pipeline but their callback
short-circuits at a volatile-null check before any map mutation,
so the cost when disabled is a few nanoseconds per stream call —
below measurement noise.
interval-seconds
How often the sampler drains the counters and writes one row per active user. Larger values lower DB write rate and produce a coarser bandwidth chart. Reasonable range: 1 to 60. The default of 5 seconds gives ~17 280 rows per active user per day before the daily compactor folds them into hour aggregates.
JVM Kill Switch
Set the JVM system property
stylus.sftpserver.bandwidth.disabled=true and the entire
bandwidth subsystem is forced off, overriding the XML
<enabled> setting. Add the flag to the service
command-line, or for a one-off check pass it on the start script:
-Dstylus.sftpserver.bandwidth.disabled=true
When set, the server logs the choice at startup:
INFO Bandwidth sampler disabled by -Dstylus.sftpserver.bandwidth.disabled=true
Intended as an operator escape hatch during incident response — no config file edit required, and the override is reversible by removing the property and restarting. The Activities tab still loads in the Web Admin; it just shows empty charts.
Retention & Compaction
A daily compactor task runs the following two passes in one transaction:
-
Rollup. Every row in
sftp_throughput_samplesolder than 24 hours is grouped by(hour_bucket, user_id), summed (bytes_up,bytes_down, anddt_msall accumulate), then the source rows are deleted and one aggregate row per (hour, user) is inserted. A typical 5-second sample rate produces ~720 raw rows per active user per hour, so the rollup collapses that to a single row — a 720× reduction in long-tail storage. - Retention. Every row older than 30 days is deleted. Rolled-up hour rows count against retention too; they age out on the same calendar.
The rollup is idempotent — running it twice on the same data
produces the same result, because a row already at an hour boundary
groups to itself. The rate math is preserved exactly: a chart query
against rolled-up rows returns the same MB/s value it would have
returned against the raw 5-second rows, because the aggregate row
carries both SUM(bytes) and SUM(dt_ms).
Every compactor run logs at INFO, including the heartbeat case with both counters at zero so operators can see the schedule is alive:
INFO Bandwidth compactor: rollup wrote 24 hour-bucket(s) for rows older than PT24H;
retention deleted 0 row(s) older than P30D.
Storage Footprint
Storage is small even on busy installations. Rough capacity planning at the default 5-second sampling rate:
- Within the last 24 hours: ~17 280 raw rows per active user per day. At ~60 bytes per row in most relational engines, that's ~1 MB per active user per day.
- Day 2 onward: raw rows collapse to 24 hour-rollup rows per user per day. ~1.5 KB per active user per day.
- 30-day steady state: 1 day raw (1 MB/user) + 29 days rollup (~45 KB/user) ≈ ~1 MB per active user, total.
Users with no transfer activity in a window do not appear in
sftp_throughput_samples for that window — the drain
skips zero-counter entries. A 200-user install where only 5 users are
active at any moment carries ~5 MB of monitoring data in the audit
database at steady state.
Database Schema
The sampler writes to one table in the audit database:
CREATE TABLE sftp_throughput_samples (
sampled_at TIMESTAMP NOT NULL,
user_id BIGINT NOT NULL,
dt_ms INTEGER NOT NULL,
bytes_up BIGINT NOT NULL,
bytes_down BIGINT NOT NULL,
CONSTRAINT pk_sftp_throughput_samples PRIMARY KEY (sampled_at, user_id)
);
CREATE INDEX ix_sftp_throughput_user_time
ON sftp_throughput_samples (user_id, sampled_at);
sftp_users.
Same policy as sftp_audit: historical metrics survive a
user deletion, and the table works in deployments where the user
database and the audit database are physically separate. The
user_id column carries the integer surrogate key, which is
unambiguous over time even if usernames are renamed.
The table is auto-created on first server boot via the same
ensureSchema path that creates sftp_audit
and sftp_notification_log. No manual migration step is
needed when upgrading from an earlier release.
REST API
The Web Admin charts are powered by two endpoints:
GET /api/throughput/timeseries
GET /api/throughput/timeseries?bucket=<bucket>&range=<range>[&user=<username>]
Returns one point per bucket as
[{ ts, bytesPerSecUp, bytesPerSecDown }, ...].
Parameters:
bucket—5min,hour, orday. Other values (1min,30min, ...) are accepted; the backend reads the suffix and computes minute buckets accordingly.range—1h,6h,24h,7d,30d. Numeric suffixes accepted.user— optional. When set, only that user's rows are aggregated. Unknown usernames return a zero-filled timeseries so the chart still has a shape.
GET /api/audit/timeseries
GET /api/audit/timeseries?bucket=<bucket>&range=<range>[&user=<username>]
Returns counts and byte totals per bucket as
[{ ts, uploads, downloads, bytesUp, bytesDown, sessions }, ...].
Same parameter semantics as the throughput endpoint. Powers the Upload &
Download Activity and Session Activity charts.
Troubleshooting
The Activities tab loads but every chart is empty.
-
Check the JVM system property is not set:
stylus.sftpserver.bandwidth.disabled=truein the service command-line forces the subsystem off. -
Check the configuration:
<bandwidth-sampler><enabled>may be set tofalse. -
Check the audit database is reachable. The sampler writes to the
same DB as
sftp_audit; if that DB is offline, the sampler logs a WARN line every tick.
The Bandwidth chart shows data but values look low compared to actual transfer speed.
- The chart shows the average rate within each bucket. A 10-second burst at 100 MB/s, in a 5-minute bucket, reads as ~3 MB/s average (10 s of 100 MB/s averaged over 300 s). Drop to a shorter range (Last hour, 5-minute buckets) to see the burst.
- The byte counter is on the application side. Bytes consumed by the SSH or TLS layer (encryption overhead, headers) are not counted. Wire-level bytes will be slightly higher than what the chart shows.
How do I disable monitoring without editing config?
Add -Dstylus.sftpserver.bandwidth.disabled=true to the
JVM command-line in the service definition and restart. The startup
log will confirm the override:
Bandwidth sampler disabled by -Dstylus.sftpserver.bandwidth.disabled=true.