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 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
+ [![PyPI](https://img.shields.io/pypi/v/tailr?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/tailr/)
41
+ [![Python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](#)
42
+ [![License](https://img.shields.io/badge/license-MIT-green)](#)
43
+ [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fdsk--dev--ai%2Ftailr-blue?logo=docker)](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
+ [![PyPI](https://img.shields.io/pypi/v/tailr?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/tailr/)
11
+ [![Python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](#)
12
+ [![License](https://img.shields.io/badge/license-MIT-green)](#)
13
+ [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fdsk--dev--ai%2Ftailr-blue?logo=docker)](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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """tailr - colorized live log viewer."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from tailr.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/tailr?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/tailr/)
41
+ [![Python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](#)
42
+ [![License](https://img.shields.io/badge/license-MIT-green)](#)
43
+ [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fdsk--dev--ai%2Ftailr-blue?logo=docker)](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,2 @@
1
+ [console_scripts]
2
+ tailr = tailr.cli:main
@@ -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