jnav 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.
- jnav-1.0.0/PKG-INFO +186 -0
- jnav-1.0.0/README.md +159 -0
- jnav-1.0.0/pyproject.toml +69 -0
- jnav-1.0.0/src/jnav/__init__.py +0 -0
- jnav-1.0.0/src/jnav/__main__.py +108 -0
- jnav-1.0.0/src/jnav/app.py +398 -0
- jnav-1.0.0/src/jnav/buffer.py +48 -0
- jnav-1.0.0/src/jnav/detail_tree.py +242 -0
- jnav-1.0.0/src/jnav/field_mapping.py +136 -0
- jnav-1.0.0/src/jnav/filter_manager_screen.py +100 -0
- jnav-1.0.0/src/jnav/filter_provider.py +103 -0
- jnav-1.0.0/src/jnav/filter_tree.py +344 -0
- jnav-1.0.0/src/jnav/filtering.py +191 -0
- jnav-1.0.0/src/jnav/header.py +48 -0
- jnav-1.0.0/src/jnav/help_screen.py +79 -0
- jnav-1.0.0/src/jnav/inline_tree.py +75 -0
- jnav-1.0.0/src/jnav/key_sequences.py +211 -0
- jnav-1.0.0/src/jnav/log_entry_item.py +96 -0
- jnav-1.0.0/src/jnav/log_entry_renderer.py +90 -0
- jnav-1.0.0/src/jnav/log_list_view.py +292 -0
- jnav-1.0.0/src/jnav/log_model.py +114 -0
- jnav-1.0.0/src/jnav/logging.py +52 -0
- jnav-1.0.0/src/jnav/manager_screen_common.py +27 -0
- jnav-1.0.0/src/jnav/model.py +13 -0
- jnav-1.0.0/src/jnav/offset_group.py +30 -0
- jnav-1.0.0/src/jnav/parsing.py +101 -0
- jnav-1.0.0/src/jnav/reading.py +52 -0
- jnav-1.0.0/src/jnav/role_mapper.py +53 -0
- jnav-1.0.0/src/jnav/scrollbar_overlay.py +79 -0
- jnav-1.0.0/src/jnav/search_engine.py +87 -0
- jnav-1.0.0/src/jnav/selector_manager_screen.py +187 -0
- jnav-1.0.0/src/jnav/selector_provider.py +64 -0
- jnav-1.0.0/src/jnav/store.py +37 -0
- jnav-1.0.0/src/jnav/text_input_screen.py +63 -0
- jnav-1.0.0/src/jnav/tree_rendering.py +194 -0
- jnav-1.0.0/src/jnav/virtual_list_view.py +383 -0
jnav-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jnav
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Interactive JSON log viewer with jq filtering for the terminal
|
|
5
|
+
Keywords: json,log,viewer,jq,tui,terminal
|
|
6
|
+
Author: Tibo Clausen
|
|
7
|
+
License-Expression: GPL-3.0-only
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: System :: Logging
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Dist: textual>=1.0.0
|
|
17
|
+
Requires-Dist: click>=8.1.0
|
|
18
|
+
Requires-Dist: jq>=1.8.0
|
|
19
|
+
Requires-Dist: python-json-logger>=4.0.0
|
|
20
|
+
Requires-Dist: aioreactive>=0.20.0
|
|
21
|
+
Requires-Dist: orjson>=3.11.8
|
|
22
|
+
Requires-Dist: pydantic>=2.12.5
|
|
23
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
24
|
+
Requires-Python: >=3.14
|
|
25
|
+
Project-URL: Repository, https://github.com/XenotriX/jnav
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<p>
|
|
29
|
+
<picture>
|
|
30
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
|
|
31
|
+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_light.svg">
|
|
32
|
+
<img alt="jnav" src="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
|
|
33
|
+
</picture>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
Interactive JSON log viewer with [jq](https://jqlang.github.io/jq/) filtering. Navigate, search, filter, and inspect structured logs in the terminal.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install jnav
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
or
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pipx install jnav
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Open a log file
|
|
56
|
+
$ jnav app.log
|
|
57
|
+
|
|
58
|
+
# Follow the file for new entries (like tail -f)
|
|
59
|
+
$ jnav --follow app.log
|
|
60
|
+
|
|
61
|
+
# Pipe JSONL from stdin
|
|
62
|
+
$ cat logs.jsonl | jnav
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
### Filtering
|
|
68
|
+
|
|
69
|
+
Filters use jq expressions. Clauses are organized in a tree of AND/OR groups, which gets compiled into a single jq expression used to evaluate each entry.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
.level == "error" # exact match
|
|
73
|
+
.data.user_id | IN("abc123", "def456", "ghi789") # match any of
|
|
74
|
+
.message | contains("timeout") # substring
|
|
75
|
+
.message | test("connection (refused|reset)") # regex
|
|
76
|
+
.status >= 500 # numeric comparison
|
|
77
|
+
.items | any(.name == "widget") # list predicate
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
https://github.com/user-attachments/assets/4c5fa668-7c37-4b59-97f5-4962aeb12504
|
|
81
|
+
|
|
82
|
+
### Selected Fields
|
|
83
|
+
|
|
84
|
+
Selectors are jq expressions evaluated per entry, rendered inline next to the summary. They can index arrays, aggregate, or reshape strings.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
.status # plain field
|
|
88
|
+
.items[0].name # array index
|
|
89
|
+
.items | length # computed
|
|
90
|
+
.items | map("\(.name): \(.qty)") # string interpolation over a list
|
|
91
|
+
.build | split("-")[1] | tonumber # extract a number from a string
|
|
92
|
+
.request.headers.user_agent # deep path
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Add from the detail panel with `s`, or open the selectors manager with `S` to add, edit, reorder, and toggle them.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
https://github.com/user-attachments/assets/a9e93048-c3e8-44e6-9f82-727b8e9f8d10
|
|
99
|
+
|
|
100
|
+
### JSON String Expansion
|
|
101
|
+
|
|
102
|
+
Values that are JSON-encoded strings (e.g. `"data": "{\"key\": \"value\"}"`) are automatically parsed and displayed as nested objects. The tree view shows `"{}"` in italic to distinguish them from real objects. Filters and selectors work through expanded JSON strings transparently.
|
|
103
|
+
|
|
104
|
+
### Live Tailing
|
|
105
|
+
|
|
106
|
+
With `--follow`, jnav watches the file for new lines (like `tail -f`). New entries are parsed, filtered, and appended to the view. If you're at the bottom of the list, it auto-scrolls to show new entries.
|
|
107
|
+
|
|
108
|
+
### Session Persistence
|
|
109
|
+
|
|
110
|
+
Filters, selected fields, scroll position, panel state, and search terms are saved when you quit and restored when reopening the same file. State is stored in `~/.local/share/jnav/`.
|
|
111
|
+
|
|
112
|
+
## Keybindings
|
|
113
|
+
|
|
114
|
+
### Navigation
|
|
115
|
+
|
|
116
|
+
| Key | Action |
|
|
117
|
+
| ------------------- | --------------------------- |
|
|
118
|
+
| `j` / `k` | Move up/down |
|
|
119
|
+
| `gg` / `G` | Jump to first/last entry |
|
|
120
|
+
| `Ctrl+D` / `Ctrl+U` | Half-page scroll |
|
|
121
|
+
| `h` / `l` | Focus list / detail panel |
|
|
122
|
+
| `Enter` | Inspect entry (open detail) |
|
|
123
|
+
| `Escape` | Back |
|
|
124
|
+
|
|
125
|
+
### Search and Filter
|
|
126
|
+
|
|
127
|
+
| Key | Action |
|
|
128
|
+
| --------- | -------------------------------------------------- |
|
|
129
|
+
| `/` | Search (highlights matches, does not hide entries) |
|
|
130
|
+
| `n` / `N` | Next / previous search match |
|
|
131
|
+
| `F` | Filter manager (jq expressions) |
|
|
132
|
+
| `ft` | Quick text filter |
|
|
133
|
+
| `fp` | Pause / resume filters |
|
|
134
|
+
| `Escape` | Clear search |
|
|
135
|
+
| `r` | Reset filters, fields, and search |
|
|
136
|
+
|
|
137
|
+
### Display
|
|
138
|
+
|
|
139
|
+
| Key | Action |
|
|
140
|
+
| ---- | ---------------------------------------- |
|
|
141
|
+
| `S` | Selected fields manager |
|
|
142
|
+
| `vi` | Toggle inline expanded tree in list rows |
|
|
143
|
+
| `d` | Toggle detail panel |
|
|
144
|
+
| `?` | Help |
|
|
145
|
+
| `q` | Quit |
|
|
146
|
+
|
|
147
|
+
### Detail Panel
|
|
148
|
+
|
|
149
|
+
| Key | Action |
|
|
150
|
+
| ---- | ---------------------------------------- |
|
|
151
|
+
| `s` | Select (or deselect) the field |
|
|
152
|
+
| `ff` | Filter by value of the highlighted field |
|
|
153
|
+
| `fn` | Filter: entries that have this field |
|
|
154
|
+
| `ve` | View the value in `$EDITOR` |
|
|
155
|
+
| `vo` | Show only selected fields |
|
|
156
|
+
|
|
157
|
+
### Filter Manager
|
|
158
|
+
|
|
159
|
+
| Key | Action |
|
|
160
|
+
| --------------- | -------------------------- |
|
|
161
|
+
| `a` | Add filter clause |
|
|
162
|
+
| `g` | Add group |
|
|
163
|
+
| `e` | Edit clause |
|
|
164
|
+
| `d` / `y` / `p` | Cut / yank / paste |
|
|
165
|
+
| `t` | Enable / disable |
|
|
166
|
+
| `o` | Toggle AND / OR on a group |
|
|
167
|
+
| `n` | Negate |
|
|
168
|
+
| `c` | Flatten group |
|
|
169
|
+
| `r` | Rename |
|
|
170
|
+
| `Escape` | Close |
|
|
171
|
+
|
|
172
|
+
### Selected Fields Manager
|
|
173
|
+
|
|
174
|
+
| Key | Action |
|
|
175
|
+
| --------------- | ------------------ |
|
|
176
|
+
| `a` | Add selector |
|
|
177
|
+
| `e` | Edit selector |
|
|
178
|
+
| `d` / `y` / `p` | Cut / yank / paste |
|
|
179
|
+
| `t` | Enable / disable |
|
|
180
|
+
| `Escape` | Close |
|
|
181
|
+
|
|
182
|
+
## Credits
|
|
183
|
+
|
|
184
|
+
- [lnav](https://github.com/tstack/lnav): the main inspiration: an interactive log viewer with structured querying
|
|
185
|
+
- [hl](https://github.com/pamburus/hl): shaped several design decisions around how JSON log entries should look
|
|
186
|
+
- [Open Color](https://yeun.github.io/open-color/): the palette the theme is built on
|
jnav-1.0.0/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_light.svg">
|
|
5
|
+
<img alt="jnav" src="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
Interactive JSON log viewer with [jq](https://jqlang.github.io/jq/) filtering. Navigate, search, filter, and inspect structured logs in the terminal.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv tool install jnav
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pipx install jnav
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Open a log file
|
|
29
|
+
$ jnav app.log
|
|
30
|
+
|
|
31
|
+
# Follow the file for new entries (like tail -f)
|
|
32
|
+
$ jnav --follow app.log
|
|
33
|
+
|
|
34
|
+
# Pipe JSONL from stdin
|
|
35
|
+
$ cat logs.jsonl | jnav
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
### Filtering
|
|
41
|
+
|
|
42
|
+
Filters use jq expressions. Clauses are organized in a tree of AND/OR groups, which gets compiled into a single jq expression used to evaluate each entry.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
.level == "error" # exact match
|
|
46
|
+
.data.user_id | IN("abc123", "def456", "ghi789") # match any of
|
|
47
|
+
.message | contains("timeout") # substring
|
|
48
|
+
.message | test("connection (refused|reset)") # regex
|
|
49
|
+
.status >= 500 # numeric comparison
|
|
50
|
+
.items | any(.name == "widget") # list predicate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
https://github.com/user-attachments/assets/4c5fa668-7c37-4b59-97f5-4962aeb12504
|
|
54
|
+
|
|
55
|
+
### Selected Fields
|
|
56
|
+
|
|
57
|
+
Selectors are jq expressions evaluated per entry, rendered inline next to the summary. They can index arrays, aggregate, or reshape strings.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
.status # plain field
|
|
61
|
+
.items[0].name # array index
|
|
62
|
+
.items | length # computed
|
|
63
|
+
.items | map("\(.name): \(.qty)") # string interpolation over a list
|
|
64
|
+
.build | split("-")[1] | tonumber # extract a number from a string
|
|
65
|
+
.request.headers.user_agent # deep path
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add from the detail panel with `s`, or open the selectors manager with `S` to add, edit, reorder, and toggle them.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
https://github.com/user-attachments/assets/a9e93048-c3e8-44e6-9f82-727b8e9f8d10
|
|
72
|
+
|
|
73
|
+
### JSON String Expansion
|
|
74
|
+
|
|
75
|
+
Values that are JSON-encoded strings (e.g. `"data": "{\"key\": \"value\"}"`) are automatically parsed and displayed as nested objects. The tree view shows `"{}"` in italic to distinguish them from real objects. Filters and selectors work through expanded JSON strings transparently.
|
|
76
|
+
|
|
77
|
+
### Live Tailing
|
|
78
|
+
|
|
79
|
+
With `--follow`, jnav watches the file for new lines (like `tail -f`). New entries are parsed, filtered, and appended to the view. If you're at the bottom of the list, it auto-scrolls to show new entries.
|
|
80
|
+
|
|
81
|
+
### Session Persistence
|
|
82
|
+
|
|
83
|
+
Filters, selected fields, scroll position, panel state, and search terms are saved when you quit and restored when reopening the same file. State is stored in `~/.local/share/jnav/`.
|
|
84
|
+
|
|
85
|
+
## Keybindings
|
|
86
|
+
|
|
87
|
+
### Navigation
|
|
88
|
+
|
|
89
|
+
| Key | Action |
|
|
90
|
+
| ------------------- | --------------------------- |
|
|
91
|
+
| `j` / `k` | Move up/down |
|
|
92
|
+
| `gg` / `G` | Jump to first/last entry |
|
|
93
|
+
| `Ctrl+D` / `Ctrl+U` | Half-page scroll |
|
|
94
|
+
| `h` / `l` | Focus list / detail panel |
|
|
95
|
+
| `Enter` | Inspect entry (open detail) |
|
|
96
|
+
| `Escape` | Back |
|
|
97
|
+
|
|
98
|
+
### Search and Filter
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
| --------- | -------------------------------------------------- |
|
|
102
|
+
| `/` | Search (highlights matches, does not hide entries) |
|
|
103
|
+
| `n` / `N` | Next / previous search match |
|
|
104
|
+
| `F` | Filter manager (jq expressions) |
|
|
105
|
+
| `ft` | Quick text filter |
|
|
106
|
+
| `fp` | Pause / resume filters |
|
|
107
|
+
| `Escape` | Clear search |
|
|
108
|
+
| `r` | Reset filters, fields, and search |
|
|
109
|
+
|
|
110
|
+
### Display
|
|
111
|
+
|
|
112
|
+
| Key | Action |
|
|
113
|
+
| ---- | ---------------------------------------- |
|
|
114
|
+
| `S` | Selected fields manager |
|
|
115
|
+
| `vi` | Toggle inline expanded tree in list rows |
|
|
116
|
+
| `d` | Toggle detail panel |
|
|
117
|
+
| `?` | Help |
|
|
118
|
+
| `q` | Quit |
|
|
119
|
+
|
|
120
|
+
### Detail Panel
|
|
121
|
+
|
|
122
|
+
| Key | Action |
|
|
123
|
+
| ---- | ---------------------------------------- |
|
|
124
|
+
| `s` | Select (or deselect) the field |
|
|
125
|
+
| `ff` | Filter by value of the highlighted field |
|
|
126
|
+
| `fn` | Filter: entries that have this field |
|
|
127
|
+
| `ve` | View the value in `$EDITOR` |
|
|
128
|
+
| `vo` | Show only selected fields |
|
|
129
|
+
|
|
130
|
+
### Filter Manager
|
|
131
|
+
|
|
132
|
+
| Key | Action |
|
|
133
|
+
| --------------- | -------------------------- |
|
|
134
|
+
| `a` | Add filter clause |
|
|
135
|
+
| `g` | Add group |
|
|
136
|
+
| `e` | Edit clause |
|
|
137
|
+
| `d` / `y` / `p` | Cut / yank / paste |
|
|
138
|
+
| `t` | Enable / disable |
|
|
139
|
+
| `o` | Toggle AND / OR on a group |
|
|
140
|
+
| `n` | Negate |
|
|
141
|
+
| `c` | Flatten group |
|
|
142
|
+
| `r` | Rename |
|
|
143
|
+
| `Escape` | Close |
|
|
144
|
+
|
|
145
|
+
### Selected Fields Manager
|
|
146
|
+
|
|
147
|
+
| Key | Action |
|
|
148
|
+
| --------------- | ------------------ |
|
|
149
|
+
| `a` | Add selector |
|
|
150
|
+
| `e` | Edit selector |
|
|
151
|
+
| `d` / `y` / `p` | Cut / yank / paste |
|
|
152
|
+
| `t` | Enable / disable |
|
|
153
|
+
| `Escape` | Close |
|
|
154
|
+
|
|
155
|
+
## Credits
|
|
156
|
+
|
|
157
|
+
- [lnav](https://github.com/tstack/lnav): the main inspiration: an interactive log viewer with structured querying
|
|
158
|
+
- [hl](https://github.com/pamburus/hl): shaped several design decisions around how JSON log entries should look
|
|
159
|
+
- [Open Color](https://yeun.github.io/open-color/): the palette the theme is built on
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jnav"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Interactive JSON log viewer with jq filtering for the terminal"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "GPL-3.0-only"
|
|
8
|
+
authors = [{ name = "Tibo Clausen" }]
|
|
9
|
+
urls = { Repository = "https://github.com/XenotriX/jnav" }
|
|
10
|
+
keywords = ["json", "log", "viewer", "jq", "tui", "terminal"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: System Administrators",
|
|
16
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Topic :: System :: Logging",
|
|
19
|
+
"Topic :: Utilities",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"textual>=1.0.0",
|
|
24
|
+
"click>=8.1.0",
|
|
25
|
+
"jq>=1.8.0",
|
|
26
|
+
"python-json-logger>=4.0.0",
|
|
27
|
+
"aioreactive>=0.20.0",
|
|
28
|
+
"orjson>=3.11.8",
|
|
29
|
+
"pydantic>=2.12.5",
|
|
30
|
+
"platformdirs>=4.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
jnav = "jnav.__main__:main"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.8.0,<0.9"]
|
|
38
|
+
build-backend = "uv_build"
|
|
39
|
+
|
|
40
|
+
[tool.basedpyright]
|
|
41
|
+
reportAny = false
|
|
42
|
+
reportExplicitAny = false
|
|
43
|
+
reportUnannotatedClassAttribute = false
|
|
44
|
+
reportIncompatibleUnannotatedOverride = true
|
|
45
|
+
reportUnusedCallResult = false
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
asyncio_mode = "strict"
|
|
49
|
+
filterwarnings = [
|
|
50
|
+
"ignore::DeprecationWarning:aioreactive",
|
|
51
|
+
"ignore::DeprecationWarning:expression",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
preview = true
|
|
56
|
+
|
|
57
|
+
[tool.coverage.run]
|
|
58
|
+
source = ["src/jnav"]
|
|
59
|
+
|
|
60
|
+
[dependency-groups]
|
|
61
|
+
dev = [
|
|
62
|
+
"basedpyright>=1.39.0",
|
|
63
|
+
"hypothesis>=6.152.1",
|
|
64
|
+
"lefthook>=2.1.4",
|
|
65
|
+
"pytest>=9.0.2",
|
|
66
|
+
"pytest-asyncio>=1.3.0",
|
|
67
|
+
"pytest-cov>=7.1.0",
|
|
68
|
+
"ruff>=0.15.10",
|
|
69
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import aioreactive as rx
|
|
10
|
+
import click
|
|
11
|
+
from platformdirs import user_data_dir
|
|
12
|
+
|
|
13
|
+
from jnav.filter_provider import FilterProvider
|
|
14
|
+
from jnav.log_model import LogModel
|
|
15
|
+
from jnav.role_mapper import RoleMapper
|
|
16
|
+
from jnav.search_engine import SearchEngine
|
|
17
|
+
from jnav.selector_provider import SelectorProvider
|
|
18
|
+
from jnav.store import Store
|
|
19
|
+
|
|
20
|
+
from .app import JnavApp
|
|
21
|
+
from .buffer import buffer_time_or_count
|
|
22
|
+
from .logging import init_logging
|
|
23
|
+
from .parsing import ParsedEntry, parse_entry
|
|
24
|
+
from .reading import read_file, read_pipe, setup_stdin_pipe
|
|
25
|
+
|
|
26
|
+
logging.getLogger("aioreactive").setLevel(logging.WARNING)
|
|
27
|
+
|
|
28
|
+
_STATE_DIR = Path(user_data_dir("jnav"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _state_file_for(file_path: str) -> Path:
|
|
32
|
+
key = hashlib.sha256(str(Path(file_path).resolve()).encode()).hexdigest()[:16]
|
|
33
|
+
return _STATE_DIR / f"{key}.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_input_iterator(file: str | None) -> AsyncIterator[str] | None:
|
|
37
|
+
if file:
|
|
38
|
+
return read_file(file)
|
|
39
|
+
elif not sys.stdin.isatty():
|
|
40
|
+
pipe = setup_stdin_pipe()
|
|
41
|
+
return read_pipe(pipe)
|
|
42
|
+
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _run(file: str | None, follow: bool) -> None:
|
|
47
|
+
lines = _get_input_iterator(file)
|
|
48
|
+
|
|
49
|
+
if lines is None:
|
|
50
|
+
click.echo("Usage: jnav [FILE] or pipe JSONL via stdin", err=True)
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
|
|
53
|
+
filter_provider = FilterProvider()
|
|
54
|
+
role_mapper = RoleMapper()
|
|
55
|
+
selectors = SelectorProvider()
|
|
56
|
+
store = Store()
|
|
57
|
+
model = LogModel(
|
|
58
|
+
store=store,
|
|
59
|
+
filter_provider=filter_provider,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
search = SearchEngine(model)
|
|
63
|
+
|
|
64
|
+
await model.start()
|
|
65
|
+
await search.start()
|
|
66
|
+
|
|
67
|
+
entry_stream = rx.from_async_iterable(
|
|
68
|
+
buffer_time_or_count(
|
|
69
|
+
rx.pipe(
|
|
70
|
+
rx.from_async_iterable(lines),
|
|
71
|
+
rx.map(lambda line: parse_entry(line)),
|
|
72
|
+
rx.filter(lambda result: result is not None),
|
|
73
|
+
rx.map(lambda entry: cast(ParsedEntry, entry)),
|
|
74
|
+
),
|
|
75
|
+
max_count=10000,
|
|
76
|
+
timeout=0.1,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
await entry_stream.subscribe_async(store.append_entries)
|
|
81
|
+
|
|
82
|
+
state_file = _state_file_for(file) if file else None
|
|
83
|
+
app = JnavApp(
|
|
84
|
+
model=model,
|
|
85
|
+
filter_provider=filter_provider,
|
|
86
|
+
role_mapper=role_mapper,
|
|
87
|
+
selectors=selectors,
|
|
88
|
+
search=search,
|
|
89
|
+
state_file=state_file,
|
|
90
|
+
follow=follow,
|
|
91
|
+
file_name=file if file else "<stdin>",
|
|
92
|
+
)
|
|
93
|
+
app.title = "jnav"
|
|
94
|
+
await app.run_async()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.command()
|
|
98
|
+
@click.argument("file", required=False, type=click.Path(exists=True))
|
|
99
|
+
@click.option("--follow", "-f", is_flag=True, help="Follow the file for new entries")
|
|
100
|
+
@click.version_option()
|
|
101
|
+
def main(file: str | None, follow: bool) -> None:
|
|
102
|
+
"""Interactive JSON log viewer with jq filtering."""
|
|
103
|
+
init_logging()
|
|
104
|
+
asyncio.run(_run(file, follow=follow))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|