metrics live in the terminal, no Grafana required
Not every team has Grafana or an ELK stack available when they need to know fast how many errors a service is producing right now. A Bash terminal dashboard built from watch, tput, and ANSI color codes shows error rates, request rates, and log levels live, right inside the SSH session, without a browser and without additional infrastructure.
Table of Contents
- 1. When a terminal dashboard makes more sense than Grafana
- 2. Basics: watch as the simplest live refresh
- 3. Aggregating log lines: error rate per minute with awk
- 4. Layout with tput: cursor positioning without flicker
- 5. ANSI colors for thresholds and status indicators
- 6. Drawing sparklines and mini trends in the terminal
- 7. A custom refresh loop instead of watch for more control
- 8. Keeping performance in check with growing log files
- 9. Terminal dashboard compared to Grafana and ELK
- 10. Summary
- 11. FAQ
1. When a terminal dashboard makes more sense than Grafana
A Bash terminal dashboard solves a very specific problem: you are on a server over SSH, a service shows anomalies, and you want to see within seconds how the error rate or request rate is currently developing, without first opening a Grafana dashboard, setting up a VPN tunnel, or building a Kibana query. For exactly that moment, a Bash terminal dashboard that aggregates directly from the local log files is often ready to use faster than any browser based tool.
The second use case for a Bash terminal dashboard is the environment without central monitoring: small projects, staging servers, or client systems where no ELK stack runs and none should run, because the operational overhead is disproportionate to the number of servers. A script combining tail -f, awk, and a bit of ANSI formatting delivers, in such cases, a usable substitute for a full observability setup, without a single additional process running permanently.
This article shows how a Bash terminal dashboard grows from the simplest form using watch to a full live dashboard with color coding, sparklines, and its own refresh loop, specifically for analyzing log files.
2. Basics: watch as the simplest live refresh
The simplest entry point for a Bash terminal dashboard is the watch command. watch -n 5 './metrics.sh' runs a script every five seconds and completely replaces the screen output, creating the impression of a live update. For most log analytics use cases, this simple approach is already sufficient, because the update frequency rarely needs to be below one second.
watch also offers difference highlighting with -d, which colors changed values between two runs. For a Bash terminal dashboard showing error counters, this makes it immediately visible at a glance which metric changed since the last refresh, without you having to program a delta calculation yourself.
#!/usr/bin/env bash
# metrics.sh — collect and print current error rate metrics
set -euo pipefail
readonly LOG_FILE="/var/log/app/access.log"
readonly WINDOW_MINUTES=5
cutoff="$(date -d "-${WINDOW_MINUTES} minutes" '+%Y-%m-%dT%H:%M')"
total_requests=$(awk -v cutoff="$cutoff" '$1 >= cutoff' "$LOG_FILE" | wc -l)
error_requests=$(awk -v cutoff="$cutoff" '$1 >= cutoff && $2 >= 500' "$LOG_FILE" | wc -l)
error_rate="0.0"
if (( total_requests > 0 )); then
error_rate=$(awk -v e="$error_requests" -v t="$total_requests" 'BEGIN { printf "%.2f", (e/t)*100 }')
fi
echo "=== Log Dashboard — last ${WINDOW_MINUTES} min ==="
echo "Total requests: $total_requests"
echo "5xx errors: $error_requests"
echo "Error rate: ${error_rate}%"
Run with watch -n 5 -d ./metrics.sh, this already produces a working Bash terminal dashboard that refreshes every five seconds and highlights changes. The next logical step is refining the aggregation logic so it produces not just a single total, but a breakdown over time.
3. Aggregating log lines: error rate per minute with awk
A Bash terminal dashboard that only shows a single overall count wastes information. A breakdown by time window is more useful, so a viewer immediately sees whether an error spike just started or has been ongoing for ten minutes already. awk is excellent for this kind of aggregation, because it can group and count log lines in a single pass without needing an external database.
The technique is an associative array in awk that keeps a counter per minute (or per status code, per endpoint). At the end of the pass, awk iterates over all collected keys and prints a sorted table. For a Bash terminal dashboard meant to show several metrics at once, it pays off to extract this aggregation into a reusable function.
#!/usr/bin/env bash
set -euo pipefail
readonly LOG_FILE="/var/log/app/access.log"
# Aggregate error rate per minute using an associative array in awk
awk '
{
minute = substr($1, 1, 16) # YYYY-MM-DDTHH:MM
total[minute]++
if ($2 >= 500) errors[minute]++
}
END {
for (m in total) {
rate = (errors[m] > 0) ? (errors[m] / total[m]) * 100 : 0
printf "%s total=%-5d errors=%-4d rate=%.1f%%\n", m, total[m], errors[m]+0, rate
}
}' "$LOG_FILE" | sort | tail -n 10
This aggregation pattern is the building block that turns a Bash terminal dashboard from a static number into a real time series. Combined with watch, it already produces a rolling view of the last ten minutes that updates automatically on every refresh.
4. Layout with tput: cursor positioning without flicker
The downside of watch is that it clears and redraws the entire screen on every run, which causes visible flicker on larger dashboards. For a calmer Bash terminal dashboard, tput takes more targeted control over cursor position and screen regions. With tput cup ROW COLUMN, the cursor jumps to a specific position without clearing the rest of the screen, allowing only the values that actually changed to be updated.
tput clear clears the entire screen once at startup, tput civis hides the blinking cursor during runtime, and tput cnorm restores it on exit. These three commands together with a custom refresh loop (see section seven) produce a Bash terminal dashboard that looks considerably more professional than a plain watch solution, because individual numbers are updated instead of clearing the whole screen.
#!/usr/bin/env bash
set -euo pipefail
readonly ROW_TOTAL=3
readonly ROW_ERRORS=4
readonly ROW_RATE=5
draw_static_layout() {
tput clear
tput cup 1 2; echo "=== Log Analytics Dashboard ==="
tput cup "$ROW_TOTAL" 2; echo "Total requests:"
tput cup "$ROW_ERRORS" 2; echo "5xx errors:"
tput cup "$ROW_RATE" 2; echo "Error rate:"
}
update_value() {
local row="$1" col="$2" value="$3"
tput cup "$row" "$col"
printf "%-20s" "$value" # pad to overwrite any leftover characters
}
draw_static_layout
while true; do
total=$(( RANDOM % 500 + 100 ))
errors=$(( RANDOM % 20 ))
rate=$(awk -v e="$errors" -v t="$total" 'BEGIN { printf "%.1f", (e/t)*100 }')
update_value "$ROW_TOTAL" 20 "$total"
update_value "$ROW_ERRORS" 20 "$errors"
update_value "$ROW_RATE" 20 "${rate}%"
sleep 2
done
5. ANSI colors for thresholds and status indicators
Plain numbers in a Bash terminal dashboard force the viewer to mentally check every value against a threshold. Color takes over that assessment visually and instantly recognizable. ANSI escape codes such as \033[31m for red, \033[33m for yellow, and \033[32m for green, followed by \033[0m to reset, are enough to color every value in the Bash terminal dashboard by threshold.
What matters is bundling this color logic in a function instead of scattering escape codes throughout the script. A colorize function that takes a value and two thresholds and returns the matching color makes a Bash terminal dashboard maintainable and consistent, because the threshold logic is defined in a single place.
#!/usr/bin/env bash
set -euo pipefail
readonly RED=$'\033[31m'
readonly YELLOW=$'\033[33m'
readonly GREEN=$'\033[32m'
readonly RESET=$'\033[0m'
colorize_rate() {
local rate="$1"
local color="$GREEN"
awk -v r="$rate" 'BEGIN { exit !(r >= 10) }' && color="$RED"
awk -v r="$rate" 'BEGIN { exit !(r >= 3 && r < 10) }' && color="$YELLOW"
printf "%s%s%%%s" "$color" "$rate" "$RESET"
}
error_rate="7.4"
echo "Error rate: $(colorize_rate "$error_rate")"
This color coding, applied consistently to every metric in the Bash terminal dashboard, turns a column of numbers into an instantly readable traffic light system, without a browser or a graphics library.
6. Drawing sparklines and mini trends in the terminal
A single current value reveals nothing about the trend. A sparkline, a tiny trend line built from Unicode block characters, shows the course of the last measurements in a single line of text. For a Bash terminal dashboard, this is the most compact way to display a time series without drawing a full chart.
The technique: an array of eight Unicode block characters of increasing height (▁▂▃▄▅▆▇█) is selected by index based on the normalized value. For a series of measurements, each value is mapped to an index between zero and seven, and the corresponding characters strung together form the sparkline.
#!/usr/bin/env bash
set -euo pipefail
readonly BLOCKS=("▁" "▂" "▃" "▄" "▅" "▆" "▇" "█")
render_sparkline() {
local values=("$@")
local max=0
for v in "${values[@]}"; do (( v > max )) && max=$v; done
(( max == 0 )) && max=1
local sparkline=""
for v in "${values[@]}"; do
local index=$(( v * 7 / max ))
sparkline+="${BLOCKS[$index]}"
done
echo "$sparkline"
}
# Sample: error counts for the last 10 monitoring windows
error_history=(2 3 1 5 8 12 9 4 2 1)
echo "Error trend: $(render_sparkline "${error_history[@]}")"
A sparkline in a Bash terminal dashboard combines well with the aggregation pattern from section three: instead of just printing the last ten minutes as a table, the same data series is additionally rendered as a sparkline next to the metric, making the trend recognizable at a glance.
7. A custom refresh loop instead of watch for more control
Once a Bash terminal dashboard combines several data sources, needs to handle keyboard input (for example pausing with the space bar), or requires a variable refresh rate depending on system load, watch reaches its limits. A custom infinite loop with trap for clean termination and read -t for non-blocking keyboard input gives full control over the dashboard's behavior.
The cursor is hidden at startup with tput civis and reliably restored on exit via a trap on EXIT with tput cnorm, even if the script is interrupted with Ctrl+C. This matters especially for a Bash terminal dashboard, because a vanished cursor after an aborted script is a common and annoying nuisance for the user.
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
tput cnorm # restore cursor visibility
tput sgr0 # reset all text attributes
echo
}
trap cleanup EXIT INT TERM
tput civis
tput clear
refresh_interval=2
paused=false
while true; do
if [[ "$paused" == false ]]; then
tput cup 0 0
echo "Dashboard running — press 'p' to pause, 'q' to quit"
# ... render metrics here ...
fi
# Non-blocking read: wait up to $refresh_interval seconds for a keypress
if read -r -t "$refresh_interval" -n 1 key; then
case "$key" in
p|P) paused=true ;;
r|R) paused=false ;;
q|Q) break ;;
esac
fi
done
8. Keeping performance in check with growing log files
A Bash terminal dashboard that rereads the entire log file on every refresh gets noticeably slower for large files, the longer the service runs. For files in the gigabyte range, a full scan every few seconds is impractical. The solution is reading only the part of the file that has been newly added since the last run, similar to how tail -f works internally, but with custom byte offset tracking.
The byte position is saved to a state file after every run, and on the next run, dd or tail -c +OFFSET reads only the new section. For a Bash terminal dashboard that runs continuously, this incremental approach is the difference between constant response time and a slowdown that grows linearly with the size of the log file.
#!/usr/bin/env bash
set -euo pipefail
readonly LOG_FILE="/var/log/app/access.log"
readonly STATE_FILE="/tmp/dashboard-offset.state"
last_offset=0
[[ -f "$STATE_FILE" ]] && last_offset="$(cat "$STATE_FILE")"
current_size="$(stat -c%s "$LOG_FILE")"
# Log rotated (file shrank) — restart from the beginning
(( current_size < last_offset )) && last_offset=0
new_lines="$(tail -c +$((last_offset + 1)) "$LOG_FILE")"
echo "$current_size" > "$STATE_FILE"
new_error_count="$(echo "$new_lines" | awk '$2 >= 500' | wc -l)"
echo "New errors since last check: $new_error_count"
9. Terminal dashboard compared to Grafana and ELK
A Bash terminal dashboard does not compete directly with a full observability stack, but covers a different niche: fast, local diagnostics with no setup overhead. The following table compares the key differences.
| Criterion | Bash terminal dashboard | Grafana | ELK stack |
|---|---|---|---|
| Setup time | Seconds | Hours | Days |
| SSH access sufficient | Yes | No, browser required | No, browser required |
| Historical data, retention | Not intended | Fully supported | Fully supported |
| Multiple servers at once | Only one host per session | Central view | Central view |
| Operational overhead | None | Medium | High |
For fast diagnostics on a single server during an incident, a Bash terminal dashboard is practically unbeatable in terms of readiness. For long term trends across many servers, a central observability tool remains the right choice, and both approaches do not exclude each other.
Mironsoft
Shell automation, monitoring, and deployment infrastructure
Log diagnostics in seconds instead of through a browser?
We build custom terminal dashboards for your log analytics, with color coding, sparklines, and efficient processing even of large log files, ready to use in any SSH session.
Dashboard design
tput layouts with color coding and sparklines for your metrics
Efficient log processing
Incremental reading of large files, stable even across log rotation
No additional operations
Pure Bash scripts with no permanently running extra services
10. Summary
A Bash terminal dashboard for log analytics starts with the simple watch command, which repeats a script at fixed intervals. For calmer, more professional presentations, tput takes over targeted cursor positioning, so only changed values get redrawn instead of the entire screen. ANSI color codes turn bare numbers into an instantly readable traffic light system, sparklines built from Unicode block characters show trends in a single line of text.
For more demanding requirements like keyboard input or variable refresh rates, a custom infinite loop with trap and read -t replaces the rigid watch command. With growing log files, incremental reading with byte offset tracking prevents performance from degrading linearly with file size. A Bash terminal dashboard does not replace a central observability tool for long term trends across many servers, but is often the most pragmatic solution for fast diagnostics on a single system.
Terminal Dashboards for Log Analytics: the essentials at a glance
Getting started
watch -n 5 -d ./script.sh already delivers a working live dashboard with delta highlighting.
Layout without flicker
tput cup positions the cursor precisely, only changed values get rewritten.
Visualization
ANSI colors for thresholds, sparklines built from Unicode blocks for trends in one line.
Scaling
Byte offset tracking for incremental reads, so large log files don't slow things down.