tailr 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tailr-1.0.0/LICENSE +21 -0
- tailr-1.0.0/PKG-INFO +151 -0
- tailr-1.0.0/README.md +121 -0
- tailr-1.0.0/pyproject.toml +41 -0
- tailr-1.0.0/setup.cfg +4 -0
- tailr-1.0.0/tailr/__init__.py +3 -0
- tailr-1.0.0/tailr/__main__.py +4 -0
- tailr-1.0.0/tailr/cli.py +48 -0
- tailr-1.0.0/tailr/core.py +173 -0
- tailr-1.0.0/tailr.egg-info/PKG-INFO +151 -0
- tailr-1.0.0/tailr.egg-info/SOURCES.txt +13 -0
- tailr-1.0.0/tailr.egg-info/dependency_links.txt +1 -0
- tailr-1.0.0/tailr.egg-info/entry_points.txt +2 -0
- tailr-1.0.0/tailr.egg-info/top_level.txt +1 -0
- tailr-1.0.0/tests/test_tailr.py +137 -0
tailr-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsk-dev-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tailr-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tailr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Colorized live log viewer for the terminal. Zero dependencies.
|
|
5
|
+
Author: dsk-dev-ai
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dsk-dev-ai/tailr
|
|
8
|
+
Project-URL: Source, https://github.com/dsk-dev-ai/tailr
|
|
9
|
+
Project-URL: Issues, https://github.com/dsk-dev-ai/tailr/issues
|
|
10
|
+
Project-URL: Funding, https://github.com/sponsors/dsk-dev-ai
|
|
11
|
+
Keywords: log,tail,color,terminal,cli,devtools,tui
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Logging
|
|
24
|
+
Classifier: Topic :: Terminals
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
# 🎨 tailr
|
|
34
|
+
|
|
35
|
+
**Colorized live log viewer for your terminal.**
|
|
36
|
+
|
|
37
|
+
Tail files or piped stdin with automatic log-level and keyword highlighting.
|
|
38
|
+
Zero dependencies. Works in Python 3.8+.
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/tailr/)
|
|
41
|
+
[](#)
|
|
42
|
+
[](#)
|
|
43
|
+
[](https://github.com/dsk-dev-ai/tailr/pkgs/container/tailr)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Why tailr?
|
|
50
|
+
|
|
51
|
+
Logs are noisy. `tailr` makes them *readable* at a glance:
|
|
52
|
+
|
|
53
|
+
- **Level highlighting** — `INFO`, `WARN`, `ERROR`, `FATAL`, `DEBUG`, `TRACE` get distinct colors.
|
|
54
|
+
- **Keyword highlighting** — `error`, `failed`, `exception`, `traceback`, `panic` pop in bright red.
|
|
55
|
+
- **Timestamp coloring** — dates and times are dimmed so the signal stands out.
|
|
56
|
+
- **Live follow** — watch logs grow in real time, just like `tail -f`.
|
|
57
|
+
- **Filter on the fly** — regex grep, or filter by log level.
|
|
58
|
+
- **Zero dependencies** — pure Python stdlib. No install drama, no bloat.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install tailr
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or run without installing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx run tailr --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Via Docker:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
docker run --rm ghcr.io/dsk-dev-ai/tailr --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Colorize a log file (last 10 lines)
|
|
82
|
+
tailr app.log
|
|
83
|
+
|
|
84
|
+
# Follow a growing log live
|
|
85
|
+
tailr -f app.log
|
|
86
|
+
|
|
87
|
+
# Follow with more history
|
|
88
|
+
tailr -f -n 200 app.log
|
|
89
|
+
|
|
90
|
+
# Filter by log level
|
|
91
|
+
tailr -l ERROR app.log
|
|
92
|
+
|
|
93
|
+
# Grep with a regex, live
|
|
94
|
+
tailr -f -g "kafka|redis" app.log
|
|
95
|
+
|
|
96
|
+
# Pipe anything in
|
|
97
|
+
docker logs -f app | tailr
|
|
98
|
+
kubectl logs -f pod | tailr
|
|
99
|
+
journalctl -f | tailr -l WARN
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Options
|
|
103
|
+
|
|
104
|
+
| Flag | Description |
|
|
105
|
+
|------|-------------|
|
|
106
|
+
| `-f, --follow` | Keep tailing appended lines (like `tail -f`) |
|
|
107
|
+
| `-n, --lines N` | Show N lines from the end (default 10) |
|
|
108
|
+
| `-g, --grep REGEX` | Only show lines matching a regex |
|
|
109
|
+
| `-l, --level LVL` | Only show lines with this level (`ERROR`, `WARN`, …) |
|
|
110
|
+
| `--version` | Print version |
|
|
111
|
+
|
|
112
|
+
### Colors
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
INFO -> blue
|
|
116
|
+
DEBUG -> gray
|
|
117
|
+
WARN -> orange
|
|
118
|
+
ERROR -> red
|
|
119
|
+
FATAL -> magenta
|
|
120
|
+
TRACE -> gray
|
|
121
|
+
keywords -> bright red + bold
|
|
122
|
+
timestamps-> dim
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Set `NO_COLOR=1` to disable colorization, or `FORCE_COLOR=1` to force it.
|
|
126
|
+
|
|
127
|
+
## How it works
|
|
128
|
+
|
|
129
|
+
`tailr` streams lines and applies a lightweight ANSI colorizer powered by a few
|
|
130
|
+
small regexes — no external parsing libs, no heuristics beyond the log shape.
|
|
131
|
+
It respects `isatty()` to avoid spewing escape codes into pipelines.
|
|
132
|
+
|
|
133
|
+
## Tests
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pytest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Tested on Python 3.8–3.12 via GitHub Actions.
|
|
140
|
+
|
|
141
|
+
## Sponsor
|
|
142
|
+
|
|
143
|
+
Enjoy `tailr`? Keep it free and dependency-free by [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub 💜.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
<div align="center">
|
|
148
|
+
|
|
149
|
+
**tailr** · Colorized logs you can actually read.
|
|
150
|
+
|
|
151
|
+
</div>
|
tailr-1.0.0/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🎨 tailr
|
|
4
|
+
|
|
5
|
+
**Colorized live log viewer for your terminal.**
|
|
6
|
+
|
|
7
|
+
Tail files or piped stdin with automatic log-level and keyword highlighting.
|
|
8
|
+
Zero dependencies. Works in Python 3.8+.
|
|
9
|
+
|
|
10
|
+
[](https://pypi.org/project/tailr/)
|
|
11
|
+
[](#)
|
|
12
|
+
[](#)
|
|
13
|
+
[](https://github.com/dsk-dev-ai/tailr/pkgs/container/tailr)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why tailr?
|
|
20
|
+
|
|
21
|
+
Logs are noisy. `tailr` makes them *readable* at a glance:
|
|
22
|
+
|
|
23
|
+
- **Level highlighting** — `INFO`, `WARN`, `ERROR`, `FATAL`, `DEBUG`, `TRACE` get distinct colors.
|
|
24
|
+
- **Keyword highlighting** — `error`, `failed`, `exception`, `traceback`, `panic` pop in bright red.
|
|
25
|
+
- **Timestamp coloring** — dates and times are dimmed so the signal stands out.
|
|
26
|
+
- **Live follow** — watch logs grow in real time, just like `tail -f`.
|
|
27
|
+
- **Filter on the fly** — regex grep, or filter by log level.
|
|
28
|
+
- **Zero dependencies** — pure Python stdlib. No install drama, no bloat.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install tailr
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or run without installing:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx run tailr --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Via Docker:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
docker run --rm ghcr.io/dsk-dev-ai/tailr --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Colorize a log file (last 10 lines)
|
|
52
|
+
tailr app.log
|
|
53
|
+
|
|
54
|
+
# Follow a growing log live
|
|
55
|
+
tailr -f app.log
|
|
56
|
+
|
|
57
|
+
# Follow with more history
|
|
58
|
+
tailr -f -n 200 app.log
|
|
59
|
+
|
|
60
|
+
# Filter by log level
|
|
61
|
+
tailr -l ERROR app.log
|
|
62
|
+
|
|
63
|
+
# Grep with a regex, live
|
|
64
|
+
tailr -f -g "kafka|redis" app.log
|
|
65
|
+
|
|
66
|
+
# Pipe anything in
|
|
67
|
+
docker logs -f app | tailr
|
|
68
|
+
kubectl logs -f pod | tailr
|
|
69
|
+
journalctl -f | tailr -l WARN
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Options
|
|
73
|
+
|
|
74
|
+
| Flag | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `-f, --follow` | Keep tailing appended lines (like `tail -f`) |
|
|
77
|
+
| `-n, --lines N` | Show N lines from the end (default 10) |
|
|
78
|
+
| `-g, --grep REGEX` | Only show lines matching a regex |
|
|
79
|
+
| `-l, --level LVL` | Only show lines with this level (`ERROR`, `WARN`, …) |
|
|
80
|
+
| `--version` | Print version |
|
|
81
|
+
|
|
82
|
+
### Colors
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
INFO -> blue
|
|
86
|
+
DEBUG -> gray
|
|
87
|
+
WARN -> orange
|
|
88
|
+
ERROR -> red
|
|
89
|
+
FATAL -> magenta
|
|
90
|
+
TRACE -> gray
|
|
91
|
+
keywords -> bright red + bold
|
|
92
|
+
timestamps-> dim
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Set `NO_COLOR=1` to disable colorization, or `FORCE_COLOR=1` to force it.
|
|
96
|
+
|
|
97
|
+
## How it works
|
|
98
|
+
|
|
99
|
+
`tailr` streams lines and applies a lightweight ANSI colorizer powered by a few
|
|
100
|
+
small regexes — no external parsing libs, no heuristics beyond the log shape.
|
|
101
|
+
It respects `isatty()` to avoid spewing escape codes into pipelines.
|
|
102
|
+
|
|
103
|
+
## Tests
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m pytest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Tested on Python 3.8–3.12 via GitHub Actions.
|
|
110
|
+
|
|
111
|
+
## Sponsor
|
|
112
|
+
|
|
113
|
+
Enjoy `tailr`? Keep it free and dependency-free by [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub 💜.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
<div align="center">
|
|
118
|
+
|
|
119
|
+
**tailr** · Colorized logs you can actually read.
|
|
120
|
+
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tailr"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Colorized live log viewer for the terminal. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "dsk-dev-ai" }]
|
|
13
|
+
keywords = ["log", "tail", "color", "terminal", "cli", "devtools", "tui"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: System :: Logging",
|
|
27
|
+
"Topic :: Terminals",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
tailr = "tailr.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/dsk-dev-ai/tailr"
|
|
36
|
+
Source = "https://github.com/dsk-dev-ai/tailr"
|
|
37
|
+
Issues = "https://github.com/dsk-dev-ai/tailr/issues"
|
|
38
|
+
Funding = "https://github.com/sponsors/dsk-dev-ai"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
packages = ["tailr"]
|
tailr-1.0.0/setup.cfg
ADDED
tailr-1.0.0/tailr/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""tailr command-line interface."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from tailr import __version__
|
|
7
|
+
from tailr.core import follow_stdin, tail_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser():
|
|
11
|
+
p = argparse.ArgumentParser(
|
|
12
|
+
prog="tailr",
|
|
13
|
+
description="Colorized live log viewer. Tail files or piped stdin with "
|
|
14
|
+
"regex level and keyword highlighting. Zero dependencies.",
|
|
15
|
+
)
|
|
16
|
+
p.add_argument("path", nargs="*", default=["-"],
|
|
17
|
+
help="file to tail, or '-' for stdin (default)")
|
|
18
|
+
p.add_argument("-f", "--follow", action="store_true",
|
|
19
|
+
help="keep following appended lines (like tail -f)")
|
|
20
|
+
p.add_argument("-n", "--lines", type=int, default=10,
|
|
21
|
+
help="number of lines to show from the end (default 10)")
|
|
22
|
+
p.add_argument("-g", "--grep", help="only show lines matching this regex")
|
|
23
|
+
p.add_argument("-l", "--level", help="only show lines with this log level "
|
|
24
|
+
"(e.g. ERROR, INFO, WARN)")
|
|
25
|
+
p.add_argument("--version", action="version", version=f"tailr {__version__}")
|
|
26
|
+
return p
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv=None):
|
|
30
|
+
args = build_parser().parse_args(argv)
|
|
31
|
+
paths = args.path
|
|
32
|
+
if any(p == "-" for p in paths) and len(paths) > 1:
|
|
33
|
+
print("tailr: mixing stdin ('-') with files is not supported", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
if paths == ["-"]:
|
|
36
|
+
lines = follow_stdin(grep=args.grep, level=args.level)
|
|
37
|
+
else:
|
|
38
|
+
for path in paths:
|
|
39
|
+
try:
|
|
40
|
+
tail_file(path, follow=args.follow, lines=args.lines,
|
|
41
|
+
grep=args.grep, level=args.level)
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
print(f"tailr: no such file: {path}", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
except IsADirectoryError:
|
|
46
|
+
print(f"tailr: is a directory: {path}", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
return 0
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""tailr - colorized live log viewer.
|
|
2
|
+
|
|
3
|
+
Tail files (or stdin) with regex-driven level and keyword highlighting.
|
|
4
|
+
Zero dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
LEVELS = {
|
|
13
|
+
"TRACE": "\033[38;5;245m",
|
|
14
|
+
"DEBUG": "\033[38;5;244m",
|
|
15
|
+
"INFO": "\033[38;5;39m",
|
|
16
|
+
"WARN": "\033[38;5;214m",
|
|
17
|
+
"WARNING": "\033[38;5;214m",
|
|
18
|
+
"ERROR": "\033[38;5;196m",
|
|
19
|
+
"FATAL": "\033[38;5;201m",
|
|
20
|
+
"CRITICAL": "\033[38;5;201m",
|
|
21
|
+
}
|
|
22
|
+
RESET = "\033[0m"
|
|
23
|
+
|
|
24
|
+
_TIMESTAMP = re.compile(
|
|
25
|
+
r"(?P<ts>\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?)"
|
|
26
|
+
)
|
|
27
|
+
_ISO_TIME = re.compile(r"\d{2}:\d{2}:\d{2}(?:[.,]\d+)?")
|
|
28
|
+
_KEYWORDS_TO_HIGHLIGHT = ("error", "fail", "exception", "traceback", "panic", "fatal")
|
|
29
|
+
|
|
30
|
+
_LEVEL_MATCH = re.compile(
|
|
31
|
+
r"\b(" + "|".join(sorted(LEVELS.keys(), key=len, reverse=True)) + r")\b"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
NO_COLOR = os.environ.get("NO_COLOR") is not None
|
|
35
|
+
FORCE_COLOR = os.environ.get("FORCE_COLOR") not in (None, "", "0")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def use_color():
|
|
39
|
+
if NO_COLOR:
|
|
40
|
+
return False
|
|
41
|
+
if FORCE_COLOR:
|
|
42
|
+
return True
|
|
43
|
+
return sys.stdout.isatty()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _level_color(token):
|
|
47
|
+
key = token.upper()
|
|
48
|
+
return LEVELS.get(key, "\033[38;5;248m")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def colorize(line):
|
|
52
|
+
"""Apply ANSI color to a single log line (best-effort, safe)."""
|
|
53
|
+
if not line:
|
|
54
|
+
return line
|
|
55
|
+
work = line
|
|
56
|
+
m = _TIMESTAMP.search(work)
|
|
57
|
+
color = "\033[38;5;240m"
|
|
58
|
+
if m:
|
|
59
|
+
work = work[: m.start()] + color + work[m.start() : m.end()] + RESET + work[m.end():]
|
|
60
|
+
else:
|
|
61
|
+
time_m = _ISO_TIME.search(work)
|
|
62
|
+
if time_m:
|
|
63
|
+
work = work[: time_m.start()] + color + work[time_m.start() : time_m.end()] + RESET + work[time_m.end():]
|
|
64
|
+
work = _highlight_level_and_keywords(work)
|
|
65
|
+
if any(k in line.lower() for k in _KEYWORDS_TO_HIGHLIGHT):
|
|
66
|
+
work = _highlight_keywords(work)
|
|
67
|
+
return work
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _highlight_level_and_keywords(line):
|
|
71
|
+
out = []
|
|
72
|
+
pos = 0
|
|
73
|
+
for lm in _LEVEL_MATCH.finditer(line):
|
|
74
|
+
out.append(line[pos : lm.start()])
|
|
75
|
+
out.append(_level_color(lm.group(1)) + lm.group(1) + RESET)
|
|
76
|
+
pos = lm.end()
|
|
77
|
+
out.append(line[pos:])
|
|
78
|
+
return "".join(out)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _highlight_keywords(line):
|
|
82
|
+
def sub(m):
|
|
83
|
+
return "\033[1m\033[38;5;203m" + m.group(0) + RESET
|
|
84
|
+
|
|
85
|
+
return re.sub(r"\b(error|fail(?:ure|ed)?|exception|traceback|panic)\b",
|
|
86
|
+
sub, line, flags=re.IGNORECASE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tail_file(path, follow=False, lines=10, grep=None, level=None, stream=None):
|
|
90
|
+
"""Tail a file, emitting colorized lines to ``stream`` (default stdout).
|
|
91
|
+
|
|
92
|
+
Returns the list of emitted raw lines (uncolored) for testing.
|
|
93
|
+
"""
|
|
94
|
+
if stream is None:
|
|
95
|
+
stream = sys.stdout
|
|
96
|
+
use_colors = use_color()
|
|
97
|
+
grep_re = re.compile(grep, re.IGNORECASE) if grep else None
|
|
98
|
+
level_re = re.compile(r"\b" + re.escape(level) + r"\b", re.IGNORECASE) if level else None
|
|
99
|
+
|
|
100
|
+
with open(path, "r", errors="replace") as fh:
|
|
101
|
+
if not follow:
|
|
102
|
+
_seek_tail(fh, lines)
|
|
103
|
+
emitted = []
|
|
104
|
+
while True:
|
|
105
|
+
read_lines = fh.readlines()
|
|
106
|
+
for raw_line in read_lines:
|
|
107
|
+
line = raw_line.rstrip("\n")
|
|
108
|
+
if level_re and not level_re.search(line):
|
|
109
|
+
continue
|
|
110
|
+
if grep_re and not grep_re.search(line):
|
|
111
|
+
continue
|
|
112
|
+
out = colorize(line) if use_colors else line
|
|
113
|
+
stream.write(out + "\n")
|
|
114
|
+
emitted.append(line)
|
|
115
|
+
stream.flush()
|
|
116
|
+
if not follow:
|
|
117
|
+
return emitted
|
|
118
|
+
time.sleep(0.1)
|
|
119
|
+
if not fh.fp:
|
|
120
|
+
return emitted
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _seek_tail(fh, lines):
|
|
124
|
+
"""Position ``fh`` so that readlines() returns exactly the last ``lines`` lines."""
|
|
125
|
+
if lines <= 0:
|
|
126
|
+
fh.seek(0, 2)
|
|
127
|
+
return
|
|
128
|
+
fh.seek(0, 2)
|
|
129
|
+
size = fh.tell()
|
|
130
|
+
if size == 0:
|
|
131
|
+
fh.seek(0)
|
|
132
|
+
return
|
|
133
|
+
fh.seek(size - 1)
|
|
134
|
+
last = fh.read(1)
|
|
135
|
+
fh.seek(0)
|
|
136
|
+
newlines = 0
|
|
137
|
+
while True:
|
|
138
|
+
data = fh.read(64 * 1024)
|
|
139
|
+
if not data:
|
|
140
|
+
break
|
|
141
|
+
newlines += data.count("\n")
|
|
142
|
+
total_lines = newlines + (1 if last != "\n" else 0)
|
|
143
|
+
to_drop = total_lines - lines
|
|
144
|
+
if to_drop <= 0:
|
|
145
|
+
fh.seek(0)
|
|
146
|
+
return
|
|
147
|
+
fh.seek(0)
|
|
148
|
+
for _ in range(to_drop):
|
|
149
|
+
if not fh.readline():
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def follow_stdin(grep=None, level=None, stream=None):
|
|
154
|
+
"""Tail stdin (for piped input), emitting colorized lines."""
|
|
155
|
+
if stream is None:
|
|
156
|
+
stream = sys.stdout
|
|
157
|
+
use_colors = use_color()
|
|
158
|
+
grep_re = re.compile(grep, re.IGNORECASE) if grep else None
|
|
159
|
+
level_re = re.compile(r"\b" + re.escape(level) + r"\b", re.IGNORECASE) if level else None
|
|
160
|
+
lines = []
|
|
161
|
+
try:
|
|
162
|
+
for raw_line in sys.stdin:
|
|
163
|
+
line = raw_line.rstrip("\n")
|
|
164
|
+
if level_re and not level_re.search(line):
|
|
165
|
+
continue
|
|
166
|
+
if grep_re and not grep_re.search(line):
|
|
167
|
+
continue
|
|
168
|
+
out = colorize(line) if use_colors else line
|
|
169
|
+
stream.write(out + "\n")
|
|
170
|
+
lines.append(line)
|
|
171
|
+
except KeyboardInterrupt:
|
|
172
|
+
pass
|
|
173
|
+
return lines
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tailr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Colorized live log viewer for the terminal. Zero dependencies.
|
|
5
|
+
Author: dsk-dev-ai
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dsk-dev-ai/tailr
|
|
8
|
+
Project-URL: Source, https://github.com/dsk-dev-ai/tailr
|
|
9
|
+
Project-URL: Issues, https://github.com/dsk-dev-ai/tailr/issues
|
|
10
|
+
Project-URL: Funding, https://github.com/sponsors/dsk-dev-ai
|
|
11
|
+
Keywords: log,tail,color,terminal,cli,devtools,tui
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Logging
|
|
24
|
+
Classifier: Topic :: Terminals
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
# 🎨 tailr
|
|
34
|
+
|
|
35
|
+
**Colorized live log viewer for your terminal.**
|
|
36
|
+
|
|
37
|
+
Tail files or piped stdin with automatic log-level and keyword highlighting.
|
|
38
|
+
Zero dependencies. Works in Python 3.8+.
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/tailr/)
|
|
41
|
+
[](#)
|
|
42
|
+
[](#)
|
|
43
|
+
[](https://github.com/dsk-dev-ai/tailr/pkgs/container/tailr)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Why tailr?
|
|
50
|
+
|
|
51
|
+
Logs are noisy. `tailr` makes them *readable* at a glance:
|
|
52
|
+
|
|
53
|
+
- **Level highlighting** — `INFO`, `WARN`, `ERROR`, `FATAL`, `DEBUG`, `TRACE` get distinct colors.
|
|
54
|
+
- **Keyword highlighting** — `error`, `failed`, `exception`, `traceback`, `panic` pop in bright red.
|
|
55
|
+
- **Timestamp coloring** — dates and times are dimmed so the signal stands out.
|
|
56
|
+
- **Live follow** — watch logs grow in real time, just like `tail -f`.
|
|
57
|
+
- **Filter on the fly** — regex grep, or filter by log level.
|
|
58
|
+
- **Zero dependencies** — pure Python stdlib. No install drama, no bloat.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install tailr
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or run without installing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx run tailr --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Via Docker:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
docker run --rm ghcr.io/dsk-dev-ai/tailr --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Colorize a log file (last 10 lines)
|
|
82
|
+
tailr app.log
|
|
83
|
+
|
|
84
|
+
# Follow a growing log live
|
|
85
|
+
tailr -f app.log
|
|
86
|
+
|
|
87
|
+
# Follow with more history
|
|
88
|
+
tailr -f -n 200 app.log
|
|
89
|
+
|
|
90
|
+
# Filter by log level
|
|
91
|
+
tailr -l ERROR app.log
|
|
92
|
+
|
|
93
|
+
# Grep with a regex, live
|
|
94
|
+
tailr -f -g "kafka|redis" app.log
|
|
95
|
+
|
|
96
|
+
# Pipe anything in
|
|
97
|
+
docker logs -f app | tailr
|
|
98
|
+
kubectl logs -f pod | tailr
|
|
99
|
+
journalctl -f | tailr -l WARN
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Options
|
|
103
|
+
|
|
104
|
+
| Flag | Description |
|
|
105
|
+
|------|-------------|
|
|
106
|
+
| `-f, --follow` | Keep tailing appended lines (like `tail -f`) |
|
|
107
|
+
| `-n, --lines N` | Show N lines from the end (default 10) |
|
|
108
|
+
| `-g, --grep REGEX` | Only show lines matching a regex |
|
|
109
|
+
| `-l, --level LVL` | Only show lines with this level (`ERROR`, `WARN`, …) |
|
|
110
|
+
| `--version` | Print version |
|
|
111
|
+
|
|
112
|
+
### Colors
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
INFO -> blue
|
|
116
|
+
DEBUG -> gray
|
|
117
|
+
WARN -> orange
|
|
118
|
+
ERROR -> red
|
|
119
|
+
FATAL -> magenta
|
|
120
|
+
TRACE -> gray
|
|
121
|
+
keywords -> bright red + bold
|
|
122
|
+
timestamps-> dim
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Set `NO_COLOR=1` to disable colorization, or `FORCE_COLOR=1` to force it.
|
|
126
|
+
|
|
127
|
+
## How it works
|
|
128
|
+
|
|
129
|
+
`tailr` streams lines and applies a lightweight ANSI colorizer powered by a few
|
|
130
|
+
small regexes — no external parsing libs, no heuristics beyond the log shape.
|
|
131
|
+
It respects `isatty()` to avoid spewing escape codes into pipelines.
|
|
132
|
+
|
|
133
|
+
## Tests
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pytest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Tested on Python 3.8–3.12 via GitHub Actions.
|
|
140
|
+
|
|
141
|
+
## Sponsor
|
|
142
|
+
|
|
143
|
+
Enjoy `tailr`? Keep it free and dependency-free by [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub 💜.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
<div align="center">
|
|
148
|
+
|
|
149
|
+
**tailr** · Colorized logs you can actually read.
|
|
150
|
+
|
|
151
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tailr/__init__.py
|
|
5
|
+
tailr/__main__.py
|
|
6
|
+
tailr/cli.py
|
|
7
|
+
tailr/core.py
|
|
8
|
+
tailr.egg-info/PKG-INFO
|
|
9
|
+
tailr.egg-info/SOURCES.txt
|
|
10
|
+
tailr.egg-info/dependency_links.txt
|
|
11
|
+
tailr.egg-info/entry_points.txt
|
|
12
|
+
tailr.egg-info/top_level.txt
|
|
13
|
+
tests/test_tailr.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tailr
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import tailr.core as core
|
|
7
|
+
from tailr.core import (
|
|
8
|
+
_level_color,
|
|
9
|
+
_seek_tail,
|
|
10
|
+
colorize,
|
|
11
|
+
follow_stdin,
|
|
12
|
+
tail_file,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SAMPLE = [
|
|
17
|
+
"2024-01-02 03:04:05 INFO starting service",
|
|
18
|
+
"2024-01-02 03:04:06 DEBUG loading config",
|
|
19
|
+
"2024-01-02 03:04:07 ERROR failed to connect",
|
|
20
|
+
"2024-01-02 03:04:08 WARN retrying in 5s",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def no_color(monkeypatch):
|
|
26
|
+
core.FORCE_COLOR = False
|
|
27
|
+
core.NO_COLOR = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def log_file(tmp_path):
|
|
32
|
+
p = tmp_path / "sample.log"
|
|
33
|
+
p.write_text("\n".join(SAMPLE) + "\n")
|
|
34
|
+
return p
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_level_color_known_and_unknown():
|
|
38
|
+
assert _level_color("INFO") == "\033[38;5;39m"
|
|
39
|
+
assert _level_color("ERROR") == "\033[38;5;196m"
|
|
40
|
+
assert _level_color("NOPE") == "\033[38;5;248m"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_colorize_timestamp_highlighted():
|
|
44
|
+
out = colorize("2024-01-02 03:04:05 INFO hello")
|
|
45
|
+
assert "\033[38;5;240m" in out
|
|
46
|
+
assert "INFO" in out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_colorize_level_colored():
|
|
50
|
+
out = colorize("INFO hello")
|
|
51
|
+
assert _level_color("INFO") in out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_colorize_error_keyword_bold():
|
|
55
|
+
out = colorize("2024-01-02 03:04:05 ERROR failed to connect")
|
|
56
|
+
assert _level_color("ERROR") in out
|
|
57
|
+
assert "\033[1m" in out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_colorize_plain_message_kept():
|
|
61
|
+
out = colorize("just a plain message")
|
|
62
|
+
assert "just a plain message" in out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_colorize_empty():
|
|
66
|
+
assert colorize("") == ""
|
|
67
|
+
assert colorize("\n") == "\n"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_seek_tail_last_n_lines():
|
|
71
|
+
data = io.TextIOWrapper(io.BytesIO("\n".join(SAMPLE).encode("utf-8")))
|
|
72
|
+
_seek_tail(data, 2)
|
|
73
|
+
assert data.readlines() == [
|
|
74
|
+
"2024-01-02 03:04:07 ERROR failed to connect\n",
|
|
75
|
+
"2024-01-02 03:04:08 WARN retrying in 5s",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_tail_file_returns_last_lines(log_file):
|
|
80
|
+
stream = io.StringIO()
|
|
81
|
+
emitted = tail_file(str(log_file), follow=False, lines=2, stream=stream)
|
|
82
|
+
assert emitted == SAMPLE[-2:]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_tail_file_grep_filter(log_file):
|
|
86
|
+
stream = io.StringIO()
|
|
87
|
+
emitted = tail_file(str(log_file), follow=False, lines=10,
|
|
88
|
+
grep="ERROR|WARN", stream=stream)
|
|
89
|
+
assert emitted == [
|
|
90
|
+
"2024-01-02 03:04:07 ERROR failed to connect",
|
|
91
|
+
"2024-01-02 03:04:08 WARN retrying in 5s",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_tail_file_level_filter(log_file):
|
|
96
|
+
stream = io.StringIO()
|
|
97
|
+
emitted = tail_file(str(log_file), follow=False, lines=10,
|
|
98
|
+
level="ERROR", stream=stream)
|
|
99
|
+
assert emitted == ["2024-01-02 03:04:07 ERROR failed to connect"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_tail_file_no_color(log_file):
|
|
103
|
+
core.FORCE_COLOR = False
|
|
104
|
+
core.NO_COLOR = True
|
|
105
|
+
stream = io.StringIO()
|
|
106
|
+
tail_file(str(log_file), follow=False, lines=10, stream=stream)
|
|
107
|
+
assert "\033" not in stream.getvalue()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_follow_stdin(monkeypatch):
|
|
111
|
+
monkeypatch.setattr(core.sys, "stdin", io.StringIO("INFO a\nERROR boom\n"))
|
|
112
|
+
stream = io.StringIO()
|
|
113
|
+
lines = follow_stdin(stream=stream)
|
|
114
|
+
assert lines == ["INFO a", "ERROR boom"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_follow_stdin_grep(monkeypatch):
|
|
118
|
+
monkeypatch.setattr(core.sys, "stdin", io.StringIO("INFO a\nERROR boom\nWARN c\n"))
|
|
119
|
+
stream = io.StringIO()
|
|
120
|
+
lines = follow_stdin(grep="ERROR|WARN", stream=stream)
|
|
121
|
+
assert lines == ["ERROR boom", "WARN c"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_missing_file_returns_error(cli_main, capsys):
|
|
125
|
+
rc = cli_main(["-n", "10", "no_such_file.log"])
|
|
126
|
+
assert rc == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_mixing_stdin_and_files(cli_main):
|
|
130
|
+
rc = cli_main(["-", "somefile"])
|
|
131
|
+
assert rc == 1
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.fixture
|
|
135
|
+
def cli_main():
|
|
136
|
+
from tailr.cli import main
|
|
137
|
+
return main
|