Log analysis and anomaly detection for application, syslog and access logs. LogLynx reads a log file and tells you what happened in it: which errors repeat, when they clustered, which time windows deviate from the file's own baseline, and what all of it adds up to as a single health score.
No dependencies. Terminal output is in Turkish.
pip install loglynx
loglynx app.log
python -m loglynx does the same thing when the scripts directory is not on
your PATH, and from a checkout python main.py app.log works with nothing
installed at all.
loglynx app.log analyse one file
loglynx logs/ a directory, .gz included
cat app.log | loglynx - stdin
loglynx app.log --since 18:00 --until 20:00 narrow the window
loglynx app.log --grep timeout --level ERROR narrow the content
loglynx app.log --watch follow the file live
loglynx app.log --save record the run
loglynx --compare diff the last two runs
loglynx --serve dashboard on :8000
loglynx app.log --html report.html shareable report
loglynx app.log --json machine-readable output
loglynx app.log --notify "$SLACK_WEBHOOK" post the result
loglynx app.log --explain LLM commentary
Every mode is documented in docs/GUIDE.md. --help lists the
full set.
A database outage. You are handed yesterday's log and told "something happened around six".
loglynx app.log --quiet
The score comes back 44.8/100 (D), and the findings name the incident without
you reading a single line: 229 errors in the 18:00 hour against a baseline of
4, one signature accounting for 72% of them, the db module at a 59% error
rate, and — the part that is easy to miss by eye — a twelve minute stretch at
22:47 with no log output at all. Without --quiet it also prints the cluster:
DatabaseConnectionError, 275 occurrences, 18:15:00 to 19:19:57, with a
rule-based guess at the cause.
A deploy that made things worse. Record a run before and after:
loglynx app.log --save --label "before"
loglynx app.log --save --label "after"
Sağlık skoru 87.9 → 44.8 ▼ (-43.1) KÖTÜLEŞME
Yeni ortaya çıkan hatalar:
+ CircuitBreakerOpenError (20)
That is not a guess: error signatures are stored per run, so "new" means
literally absent from the previous set. As a CI gate:
loglynx app.log --fail-on health --min-health 80.
A spike of 5xx. Access logs have no level field, so the HTTP status supplies one — 5xx becomes ERROR, 4xx WARNING:
loglynx /var/log/nginx/access.log --since 18:00 --until 20:00 --level ERROR
Anomalies are scored against the file's own baseline rather than a fixed threshold, because "too many errors" only means something relative to what that system normally produces.
The default is a modified z-score, 0.6745 * (x - median) / MAD. The obvious
alternative, (x - mean) / stdev, has a flaw that matters here: the outlier
you are looking for inflates the standard deviation and therefore hides
itself. In the sample log there are two error spikes, at 18:00 and 19:00. The
classic score only reports the first; the larger spike raises stdev to 49.6
and the second one falls inside the noise. The median and MAD are unaffected
by outliers, so both are reported.
tests/test_anomalies.py::test_robust_beats_classic_zscore pins this down.
The classic method is still available with --method zscore.
Long silences are treated as anomalies too. A stopped service or a dead log agent produces nothing at all, which error counting never catches.
A single 0-100 number, computed by subtracting penalties from 100. Each factor has a ceiling, so one bad metric cannot flatten the score and make every bad log look identical.
| Factor | Ceiling | Full penalty at |
|---|---|---|
| Error rate | 35 | 25% of entries |
| Critical entries | 20 | 2% of entries |
| Anomalies | 20 | 4 windows |
| Bursts | 10 | 3 bursts |
| Log stream gaps | 10 | 10% of the time span |
| Unparsed lines | 5 | nothing parsed |
Grades: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below. The weights are a judgement
call, not a derived truth; WEIGHTS in loglynx/analyzer/health.py is the one
place to retune them.
| Name | Example |
|---|---|
standard |
2026-08-22 10:31:02 ERROR [db] Database connection failed |
bracket |
[2026-08-22 10:31:02] [ERROR] [db] Database down |
python_logging |
2026-08-22 10:31:02,123 - auth - ERROR - Bad token |
json |
{"timestamp":"...","level":"ERROR","message":"..."} |
syslog_rfc5424 |
<11>1 2026-08-22T18:15:00Z web-01 nginx 1234 ID - refused |
syslog |
Aug 22 18:15:00 web-01 nginx[1234]: connection refused |
access_log |
1.2.3.4 - - [22/Aug/2026:18:15:00 +0300] "GET / HTTP/1.1" 500 12 |
The format is detected per file, and lines that do not match the dominant format fall back to the other parsers, so mixed files still work. JSON field names follow OpenTelemetry, Elastic ECS or Python conventions; unrecognised keys are kept on the entry. Timestamps may be ISO strings or epoch numbers, and offset-aware values are normalised to UTC so a file mixing both does not break comparisons.
Two formats carry no level, so one is derived — syslog decodes severity from
the priority number, access logs from the HTTP status. Adding a format means
adding one class with an extract(line) method.
loglynx/cli.py argument parsing and wiring
loglynx/parser/ formats, timestamps, reading, filtering
loglynx/analyzer/ statistics, patterns, anomalies, health,
insights, optional LLM commentary
loglynx/database/ run history and comparison
loglynx/watcher.py live following and rolling window
loglynx/notify.py webhook notifications
loglynx/server.py dashboard HTTP server
loglynx/reports/ console, HTML, JSON, dashboard
tools/ sample data, log replay, README assets
Everything under loglynx/analyzer is pure: it returns data and never prints,
so the same code backs the terminal, the HTML report, the JSON output and the
dashboard.
pip install -e ".[dev]"
ruff check .
pytest
306 tests, no network access from any of them. The sample generator
(python tools/make_sample_logs.py) produces a day of traffic containing a
deliberate database outage between 18:15 and 19:20, which is what the pattern
and anomaly tests are written against.
- docs/GUIDE.md — every mode in detail
- CONTRIBUTING.md — module boundaries, adding a format
- docs/KARARLAR.md — why each design decision was made (Turkish)
MIT
