Metadata-Version: 2.4
Name: loglynx
Version: 1.2.0
Summary: Log analysis and anomaly detection for application logs
Author: Cem
License: MIT
Project-URL: Homepage, https://github.com/LocalinTheEngineer/loglynx
Project-URL: Issues, https://github.com/LocalinTheEngineer/loglynx/issues
Keywords: logs,log-analysis,anomaly-detection,observability,sre
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Logging
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Provides-Extra: demo
Requires-Dist: pillow>=10; extra == "demo"
Dynamic: license-file

# LogLynx

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.

[![ci](https://github.com/LocalinTheEngineer/loglynx/actions/workflows/tests.yml/badge.svg)](https://github.com/LocalinTheEngineer/loglynx/actions/workflows/tests.yml)
[![PyPI](https://img.shields.io/pypi/v/loglynx)](https://pypi.org/project/loglynx/)
[![python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![coverage](https://img.shields.io/badge/coverage-87%25-brightgreen)](#development)
[![tests](https://img.shields.io/badge/tests-306-blue)](tests/)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)

<img src="docs/demo.gif" alt="LogLynx analysing a log file" width="540">

## Install

```
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.

## Usage

```
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](docs/GUIDE.md). `--help` lists the
full set.

## What it looks like in practice

**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
```

## Anomaly detection

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.

## Health score

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.

## Formats

| 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.

## Layout

```
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.

## Development

```
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](docs/GUIDE.md) — every mode in detail
- [CONTRIBUTING.md](CONTRIBUTING.md) — module boundaries, adding a format
- [docs/KARARLAR.md](docs/KARARLAR.md) — why each design decision was made (Turkish)

## License

MIT
