- Posted on
- • Artificial Intelligence
Artificial Intelligence Log Analytics
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
From grep to guidance: AI-powered log analytics on Linux with Bash
Ever stared at a scrolling wall of logs at 3 a.m., knowing the answer is “in there somewhere,” but grep just isn’t cutting it? Today’s systems emit millions of log lines across services, containers, and hosts. The value is real—faster incident detection, clearer root cause analysis, and capacity planning—but the signal is buried in noise.
This article shows how to bolt simple, effective AI onto your existing Linux/Bash toolchain to turn raw logs into actionable insight. You’ll learn a minimal, reproducible pipeline to:
Normalize and stream logs from systemd-journald
Engineer features with Bash/jq
Train a quick anomaly detector (Isolation Forest) in Python
Score logs in near real time
Visualize or alert on anomalies
No vendor lock-in, no heavyweight stack required—just the Linux you already know.
Why AI for logs is worth it
Logs are structured(ish) time series. That’s perfect for anomaly detection, trend analysis, and clustering.
Rule-based alerts (grep/regex) miss unknown-unknowns. ML-based detectors catch “we’ve never seen this before” behavior.
Linux tools remain the backbone. Bash pipes handle plumbing; ML handles the pattern recognition.
Start small, iterate fast. A few numeric features + a classical model often beats waiting months to roll out a full platform.
Prerequisites: install the essentials
We’ll use jq for parsing, awk for aggregation, and Python for the model.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y jq gawk gnuplot rsyslog python3 python3-venv python3-pip build-essential
Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y jq gawk gnuplot rsyslog python3 python3-pip gcc gcc-c++
openSUSE/SLE (zypper):
sudo zypper install -y jq gawk gnuplot rsyslog python3 python3-pip python3-venv gcc gcc-c++
# If python3-venv isn’t found on your version, use:
sudo zypper install -y python3-virtualenv
Create a Python virtual environment and install ML libs:
python3 -m venv ~/.venvs/ailogs
source ~/.venvs/ailogs/bin/activate
pip install --upgrade pip
pip install numpy pandas scikit-learn joblib
Notes:
You might need root (or adm/journal group membership) to read system logs. For user-level logs, add
--userto journalctl commands.rsyslog is typically installed by default; commands above ensure it’s present if you rely on it elsewhere.
What we’ll build
We’ll go through four actionable steps:
1) Normalize and stream logs from journald to newline-delimited JSON-ish TSV
2) Turn log lines into per-minute numeric features
3) Train and save an anomaly detector
4) Wire it up for near-real-time scoring and simple visualization
Along the way, you’ll see a realistic example: flagging spikes in error-like messages.
1) Normalize and stream logs
Start with journald because it already captures kernel, system, and service logs with rich metadata.
Follow live logs, convert to a minimal schema, and make a “minute” bucket for later aggregation:
journalctl -o json -f \
| jq -rc '
. as $j
| { ts: ($j["__REALTIME_TIMESTAMP"]|tonumber/1000000|todateiso8601),
host: $j["_HOSTNAME"],
unit: $j["_SYSTEMD_UNIT"],
prio: ($j["PRIORITY"]|tonumber? // -1),
msg: $j["MESSAGE"] }
| { minute: (.ts | sub(":[0-9]{2}Z$"; ":00Z")),
is_error: (.msg|test("(?i)error|fail|exception|timeout|critical|panic")),
host: .host, unit: .unit }
| [.minute, (if .is_error then 1 else 0 end), .host, .unit]
| @tsv'
What this does:
Extracts timestamp, host, unit (systemd unit), and message
Buckets to minute precision (YYYY-MM-DDTHH:MM:00Z)
Emits a simple TSV: minute, is_error(0/1), host, unit
To build a historical dataset instead of following live logs, replace -f with a time range:
journalctl -o json --since "24 hours ago" \
| jq -rc '...same filter as above...'
Tip: Filter by unit to focus on a noisy service (e.g., nginx):
journalctl -o json -u nginx.service --since "24 hours ago" \
| jq -rc '...'
2) Engineer per-minute features with Bash + awk
Aggregate those TSV rows into per-minute features. We’ll compute:
total: total log lines that minute
error_count: lines matching error-like keywords
error_rate: error_count / total
Streaming aggregator (prints when the minute changes):
#!/usr/bin/env bash
set -euo pipefail
# Input: minute<TAB>is_error<TAB>host<TAB>unit
# Output: CSV header then: minute,total,error_count,error_rate
awk -F'\t' '
BEGIN {
OFS=",";
print "minute,total,error_count,error_rate";
}
{
m = $1; e = $2 + 0;
if (current_minute != "" && m != current_minute) {
er = (err_count / total_count);
printf "%s,%d,%d,%.6f\n", current_minute, total_count, err_count, er;
total_count = 0; err_count = 0;
}
current_minute = m;
total_count++;
if (e == 1) err_count++;
}
END {
if (total_count > 0) {
er = (err_count / total_count);
printf "%s,%d,%d,%.6f\n", current_minute, total_count, err_count, er;
}
}'
Example: build a 24-hour baseline file for training:
journalctl -o json --since "24 hours ago" \
| jq -rc '
. as $j
| { ts: ($j["__REALTIME_TIMESTAMP"]|tonumber/1000000|todateiso8601),
msg: $j["MESSAGE"], host: $j["_HOSTNAME"], unit: $j["_SYSTEMD_UNIT"] }
| { minute: (.ts | sub(":[0-9]{2}Z$"; ":00Z")),
is_error: (.msg|test("(?i)error|fail|exception|timeout|critical|panic")),
host: .host, unit: .unit }
| [.minute, (if .is_error then 1 else 0 end), .host, .unit]
| @tsv' \
| ./aggregate_per_minute.sh \
| sort -u > features_baseline.csv
Replace ./aggregate_per_minute.sh with the script above saved as a file and made executable:
chmod +x aggregate_per_minute.sh
You now have a tidy CSV with 1 row per minute—perfect ML input.
3) Train a quick anomaly detector (Isolation Forest)
Create a compact Python script to train, save, and stream-score anomalies.
Save as iforest_logs.py:
#!/usr/bin/env python3
import sys, argparse, io
import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from joblib import dump, load
def load_features_csv(path):
df = pd.read_csv(path)
# Ensure expected columns
for col in ["minute","total","error_count","error_rate"]:
if col not in df.columns:
raise ValueError(f"Missing column: {col}")
# Sort by time for stability
df = df.sort_values("minute")
X = df[["total","error_count","error_rate"]].astype(float)
return df, X
def train(args):
df, X = load_features_csv(args.input)
clf = IsolationForest(
n_estimators=200,
contamination=args.contamination,
random_state=42,
n_jobs=-1
)
clf.fit(X)
dump(clf, args.model)
print(f"Model saved to {args.model}. Trained on {len(df)} minutes.", file=sys.stderr)
def score_stream(args):
clf = load(args.model)
# Read CSV header from stdin or assume known order
header = sys.stdin.readline().strip()
cols = header.split(",")
idx_minute = cols.index("minute")
idx_total = cols.index("total")
idx_errc = cols.index("error_count")
idx_erate = cols.index("error_rate")
print("minute,total,error_count,error_rate,anomaly,score")
for line in sys.stdin:
line = line.strip()
if not line:
continue
parts = line.split(",")
try:
x = np.array([[
float(parts[idx_total]),
float(parts[idx_errc]),
float(parts[idx_erate])
]])
pred = clf.predict(x)[0] # -1 anomaly, 1 normal
score = clf.score_samples(x)[0] # lower = more anomalous
is_anom = 1 if pred == -1 else 0
print(f"{parts[idx_minute]},{parts[idx_total]},{parts[idx_errc]},{parts[idx_erate]},{is_anom},{score:.6f}")
sys.stdout.flush()
except Exception as e:
# Skip malformed lines
print(f"# skip: {e}", file=sys.stderr)
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
ap_train = sub.add_parser("train", help="Train model on a features CSV")
ap_train.add_argument("input", help="features_baseline.csv")
ap_train.add_argument("model", help="model.joblib")
ap_train.add_argument("--contamination", type=float, default=0.01, help="Expected anomaly fraction (0..0.5)")
ap_train.set_defaults(func=train)
ap_score = sub.add_parser("stream", help="Stream-score features from stdin")
ap_score.add_argument("model", help="model.joblib")
ap_score.set_defaults(func=score_stream)
args = ap.parse_args()
args.func(args)
if __name__ == "__main__":
main()
Make it executable:
chmod +x iforest_logs.py
Train the model:
./iforest_logs.py train features_baseline.csv model.joblib
Tip: Start with --contamination 0.01 (about 1% anomalies). Tweak based on your environment’s noisiness.
4) Wire up near real-time scoring
We’ll pipe live features to the model and print anomalies as they happen.
Option A: Simple pipe
journalctl -o json -f \
| jq -rc '
. as $j
| { ts: ($j["__REALTIME_TIMESTAMP"]|tonumber/1000000|todateiso8601),
msg: $j["MESSAGE"], host: $j["_HOSTNAME"], unit: $j["_SYSTEMD_UNIT"] }
| { minute: (.ts | sub(":[0-9]{2}Z$"; ":00Z")),
is_error: (.msg|test("(?i)error|fail|exception|timeout|critical|panic")),
host: .host, unit: .unit }
| [.minute, (if .is_error then 1 else 0 end), .host, .unit]
| @tsv' \
| ./aggregate_per_minute.sh \
| ./iforest_logs.py stream model.joblib \
| tee anomalies.csv
Option B: Use a FIFO to decouple producers/consumers
mkfifo /tmp/features.pipe
# Terminal 1: producer
journalctl -o json -f \
| jq -rc '...same filter...' \
| ./aggregate_per_minute.sh \
> /tmp/features.pipe
# Terminal 2: consumer
cat /tmp/features.pipe \
| ./iforest_logs.py stream model.joblib \
| tee anomalies.csv
Quick terminal view of recent anomalies:
grep -E ",1," anomalies.csv | tail
Optional terminal plot (requires gnuplot):
# Minute-wise error_rate and anomaly markers
gnuplot -persist <<'GP'
set datafile separator ","
set xdata time
set timefmt "%Y-%m-%dT%H:%M:%SZ"
set format x "%H:%M"
set grid
plot "anomalies.csv" using 1:4 with lines title "error_rate", \
"" using 1:(($5==1)?$4:1/0) with points pt 7 lc rgb "red" title "anomaly"
GP
Real-world-ish example: catching an nginx hiccup
Suppose nginx.service starts spitting 5xx errors after a deploy. Even if your log lines vary, the error-like keywords surge. The pipeline above sees:
total goes up modestly
error_count shoots up
error_rate spikes abruptly
Isolation Forest flags that minute as an outlier. While this doesn’t tell you “why” yet, it narrows the search window and lets you pivot to deep dives (e.g., unit-specific logs, upstream health checks, recent deploy diffs).
Tips to improve signal
Add domain features:
- Per-unit features (one-hot top N units)
- HTTP 5xx vs 4xx counts (if available)
- Latency percentiles if logs include timings
- Unique hosts per minute (if multi-host stream)
Maintain a rolling baseline:
- Retrain daily or weekly to adapt to seasonality
Alerting:
- Trigger on k consecutive anomalous minutes
- Send to Slack/email via a tiny webhook script
Reliability:
- Run producer/consumer under systemd with Restart=on-failure
- Persist features/anomalies to a small SQLite or CSV log rotated with logrotate
Conclusion and next steps (CTA)
You don’t need a massive platform to get useful AI in your log workflow. With journald, jq, Bash, and a compact Isolation Forest, you can surface outliers and shorten time-to-detection today.
Next steps:
Start by training on 24–72 hours of “mostly normal” traffic.
Tune contamination and add a couple of domain-specific features.
Wrap the pipeline in systemd units and ship anomaly summaries to your team chat.
When ready, explore richer models or embeddings-based clustering for semantic grouping.
If you’d like a follow-up post with systemd unit files, Slack alert scripts, or adding latency features from nginx logs, let me know what you’re running and I’ll tailor it!