netdoctor 0.1.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.
- netdoctor-0.1.0/.dockerignore +11 -0
- netdoctor-0.1.0/.github/workflows/ci.yml +28 -0
- netdoctor-0.1.0/.github/workflows/release.yml +90 -0
- netdoctor-0.1.0/.gitignore +22 -0
- netdoctor-0.1.0/Dockerfile +22 -0
- netdoctor-0.1.0/LICENSE +21 -0
- netdoctor-0.1.0/PKG-INFO +371 -0
- netdoctor-0.1.0/README.md +343 -0
- netdoctor-0.1.0/RELEASING.md +55 -0
- netdoctor-0.1.0/assets/hero.gif +0 -0
- netdoctor-0.1.0/assets/hero.tape +28 -0
- netdoctor-0.1.0/assets/mtr.gif +0 -0
- netdoctor-0.1.0/assets/mtr.tape +26 -0
- netdoctor-0.1.0/assets/scorecard.gif +0 -0
- netdoctor-0.1.0/assets/scorecard.tape +28 -0
- netdoctor-0.1.0/assets/watch.gif +0 -0
- netdoctor-0.1.0/assets/watch.tape +26 -0
- netdoctor-0.1.0/packaging/entry.py +7 -0
- netdoctor-0.1.0/pyproject.toml +42 -0
- netdoctor-0.1.0/scripts/update-brew-formula.sh +38 -0
- netdoctor-0.1.0/src/netdoctor/__init__.py +3 -0
- netdoctor-0.1.0/src/netdoctor/__main__.py +5 -0
- netdoctor-0.1.0/src/netdoctor/checks.py +358 -0
- netdoctor-0.1.0/src/netdoctor/cli.py +453 -0
- netdoctor-0.1.0/src/netdoctor/model.py +134 -0
- netdoctor-0.1.0/src/netdoctor/render.py +548 -0
- netdoctor-0.1.0/src/netdoctor/score.py +160 -0
- netdoctor-0.1.0/tests/test_checks.py +101 -0
- netdoctor-0.1.0/tests/test_diagnose.py +65 -0
- netdoctor-0.1.0/tests/test_parse.py +26 -0
- netdoctor-0.1.0/tests/test_score.py +54 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Unit tests
|
|
24
|
+
run: pytest -q
|
|
25
|
+
- name: CLI smoke test
|
|
26
|
+
run: |
|
|
27
|
+
netdoctor --version
|
|
28
|
+
netdoctor --help
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Cut a release by pushing a tag: git tag v0.1.0 && git push --tags
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# 1) Publish to PyPI via Trusted Publishing (OIDC) — no tokens/secrets needed.
|
|
10
|
+
# One-time: add this repo as a Trusted Publisher on pypi.org (see RELEASING.md).
|
|
11
|
+
pypi:
|
|
12
|
+
name: Publish to PyPI
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- run: pip install build
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
|
|
25
|
+
# 2) Build a standalone single-file binary for each OS and attach to the Release.
|
|
26
|
+
binaries:
|
|
27
|
+
name: Binary (${{ matrix.asset }})
|
|
28
|
+
permissions:
|
|
29
|
+
contents: write
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
include:
|
|
34
|
+
- os: ubuntu-latest
|
|
35
|
+
asset: netdoctor-linux-x86_64
|
|
36
|
+
- os: macos-latest
|
|
37
|
+
asset: netdoctor-macos-arm64
|
|
38
|
+
- os: macos-13
|
|
39
|
+
asset: netdoctor-macos-x86_64
|
|
40
|
+
- os: windows-latest
|
|
41
|
+
asset: netdoctor-windows-x86_64.exe
|
|
42
|
+
runs-on: ${{ matrix.os }}
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: "3.12"
|
|
48
|
+
- run: pip install pyinstaller .
|
|
49
|
+
- name: Build binary
|
|
50
|
+
run: pyinstaller --onefile --name netdoctor --collect-submodules rich packaging/entry.py
|
|
51
|
+
- name: Stage asset
|
|
52
|
+
shell: bash
|
|
53
|
+
run: |
|
|
54
|
+
mkdir -p out
|
|
55
|
+
if [ "${{ runner.os }}" = "Windows" ]; then
|
|
56
|
+
cp dist/netdoctor.exe "out/${{ matrix.asset }}"
|
|
57
|
+
else
|
|
58
|
+
cp dist/netdoctor "out/${{ matrix.asset }}"
|
|
59
|
+
fi
|
|
60
|
+
- uses: softprops/action-gh-release@v2
|
|
61
|
+
with:
|
|
62
|
+
files: out/${{ matrix.asset }}
|
|
63
|
+
|
|
64
|
+
# 3) Build and push the container image to GitHub Container Registry.
|
|
65
|
+
docker:
|
|
66
|
+
name: Docker image (GHCR)
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
permissions:
|
|
69
|
+
contents: read
|
|
70
|
+
packages: write
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- uses: docker/login-action@v3
|
|
74
|
+
with:
|
|
75
|
+
registry: ghcr.io
|
|
76
|
+
username: ${{ github.actor }}
|
|
77
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
78
|
+
- id: meta
|
|
79
|
+
uses: docker/metadata-action@v5
|
|
80
|
+
with:
|
|
81
|
+
images: ghcr.io/sahilll15/netdoctor
|
|
82
|
+
tags: |
|
|
83
|
+
type=semver,pattern={{version}}
|
|
84
|
+
type=raw,value=latest
|
|
85
|
+
- uses: docker/build-push-action@v6
|
|
86
|
+
with:
|
|
87
|
+
context: .
|
|
88
|
+
push: true
|
|
89
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
90
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtual envs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Tooling
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
|
|
19
|
+
# OS / editor
|
|
20
|
+
.DS_Store
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ---- build the wheel ----
|
|
2
|
+
FROM python:3.12-slim AS build
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN pip install --no-cache-dir build && python -m build --wheel
|
|
6
|
+
|
|
7
|
+
# ---- runtime ----
|
|
8
|
+
FROM python:3.12-slim
|
|
9
|
+
LABEL org.opencontainers.image.source="https://github.com/Sahilll15/netdoctor"
|
|
10
|
+
LABEL org.opencontainers.image.description="A network health-check CLI that walks the DevOps debugging ladder."
|
|
11
|
+
LABEL org.opencontainers.image.licenses="MIT"
|
|
12
|
+
|
|
13
|
+
# ping + traceroute power two of the rungs (need NET_RAW at run time to actually send)
|
|
14
|
+
RUN apt-get update \
|
|
15
|
+
&& apt-get install -y --no-install-recommends iputils-ping traceroute \
|
|
16
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
17
|
+
|
|
18
|
+
COPY --from=build /app/dist/*.whl /tmp/
|
|
19
|
+
RUN pip install --no-cache-dir /tmp/*.whl && rm -f /tmp/*.whl
|
|
20
|
+
|
|
21
|
+
ENTRYPOINT ["netdoctor"]
|
|
22
|
+
CMD ["--help"]
|
netdoctor-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sahil Chalke (github.com/Sahilll15)
|
|
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.
|
netdoctor-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netdoctor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A network health-check CLI that walks the DevOps debugging ladder and tells you which rung broke.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sahilll15/netdoctor
|
|
6
|
+
Project-URL: Repository, https://github.com/Sahilll15/netdoctor
|
|
7
|
+
Project-URL: Issues, https://github.com/Sahilll15/netdoctor/issues
|
|
8
|
+
Author: Sahil Chalke
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,devops,diagnostics,dns,http,networking,tcp,tls
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Provides-Extra: build
|
|
23
|
+
Requires-Dist: build; extra == 'build'
|
|
24
|
+
Requires-Dist: pyinstaller>=6.0; extra == 'build'
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# netdoctor
|
|
30
|
+
|
|
31
|
+
> Point it at a host. It walks the network debugging ladder rung by rung and tells you **exactly which one broke** — and what to do about it.
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="assets/hero.gif" alt="netdoctor walking the full network ladder against github.com: an animated ECG header, the signal-path rail filling in rung by rung, a plain-English diagnosis, a quality scorecard, and a hop-by-hop traceroute" width="760">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
`netdoctor` automates the sequence every engineer runs by hand when "the service is down": resolve the name, ping it, check the port, hit the app. Instead of five commands and five mental models, you get one live report, a plain-English diagnosis, and a **quality scorecard** that grades the host on reachability, performance, and latency.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
netdoctor github.com
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
╭──────────────────────────────────────────────────────────────────────────╮
|
|
45
|
+
│ ◉ netdoctor network vitals monitor api.example.com:443 │
|
|
46
|
+
│ ⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯ 2026-06-28 12:54:32 │
|
|
47
|
+
╰──────────────────────────────────────────────────────────────────────────╯
|
|
48
|
+
╭─ signal path · resolve → connect → respond ──────────────────────────────╮
|
|
49
|
+
│ │
|
|
50
|
+
│ ● 1 ✓ DNS resolution L7 api.example.com → 93.184.216.34 █······ 12 ms │
|
|
51
|
+
│ │ │
|
|
52
|
+
│ ● 2 ⚠ Ping (ICMP) L3 no reply (ICMP likely blocked) — │
|
|
53
|
+
│ │ │
|
|
54
|
+
│ ● 3 ✓ TCP port 443 L4 open █······ 41 ms │
|
|
55
|
+
│ │ │
|
|
56
|
+
│ ● 4 ✓ HTTPS / L7 200 OK · TLSv1.3 · cert 67d left ███████ 136 ms │
|
|
57
|
+
│ │
|
|
58
|
+
╰────────────────────────────────────────────────────────────────────────────╯
|
|
59
|
+
╭─ diagnosis ────────────────────────────────────────────────────────────────╮
|
|
60
|
+
│ │
|
|
61
|
+
│ ● HEALTHY api.example.com is reachable and responding. │
|
|
62
|
+
│ │
|
|
63
|
+
│ vitals ▰ ▰ ▰ ▰ 4 ok · 189 ms total │
|
|
64
|
+
│ │
|
|
65
|
+
│ → No ICMP reply — many hosts and firewalls block ping, so this alone does │
|
|
66
|
+
│ NOT mean the host is down. The TCP port and HTTP checks are authoritative.│
|
|
67
|
+
│ │
|
|
68
|
+
╰────────────────────────────────────────────────────────────────────────────╯
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> The header is a **live ECG line** that scrolls while checks run and freezes in
|
|
72
|
+
> the verdict colour when done. The **signal-path rail** fills in rung by rung as
|
|
73
|
+
> the packet descends the stack (each step tagged with its OSI layer). And note
|
|
74
|
+
> the diagnosis: the host **ignores ping** (normal for production hosts), yet it's
|
|
75
|
+
> still **healthy** — because the port and HTTP checks, the ones that actually
|
|
76
|
+
> matter, both passed. That judgement is the whole reason the tool exists.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Why this exists
|
|
81
|
+
|
|
82
|
+
When a service is unreachable, the fix is almost always finding **which layer** is broken. The classic move is to walk a ladder, top to bottom, until something fails:
|
|
83
|
+
|
|
84
|
+
| Question | The manual command | netdoctor's rung |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Does the name resolve to an IP? | `dig` / `nslookup` | **DNS resolution** |
|
|
87
|
+
| Is the host reachable at all? | `ping` | **Ping (ICMP)** — *advisory* |
|
|
88
|
+
| Is the specific port open? | `nc -zv host 443` | **TCP port** |
|
|
89
|
+
| Does the app actually respond? | `curl -v https://host` | **HTTP / TLS** |
|
|
90
|
+
| Where does the path break? | `traceroute` | **Traceroute** (`--trace`) |
|
|
91
|
+
|
|
92
|
+
netdoctor runs the whole ladder for you, stops reasoning about ICMP the way a human does (a blocked ping is **not** an outage), and points at the first rung that genuinely failed.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Install
|
|
97
|
+
|
|
98
|
+
**pipx** (recommended — isolated, always on your PATH):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pipx install netdoctor
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
…or with the modern, faster installer: `uv tool install netdoctor` — or plain `pip install netdoctor`.
|
|
105
|
+
|
|
106
|
+
**Homebrew** (macOS / Linux):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
brew install Sahilll15/tap/netdoctor
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Docker** (no local Python at all):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
docker run --rm ghcr.io/sahilll15/netdoctor github.com
|
|
116
|
+
# ping & traceroute need raw sockets — grant them when you want those rungs:
|
|
117
|
+
docker run --rm --cap-add=NET_RAW ghcr.io/sahilll15/netdoctor github.com
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Standalone binary** — grab the file for your OS from the [latest release](https://github.com/Sahilll15/netdoctor/releases), `chmod +x`, and run it. No Python required.
|
|
121
|
+
|
|
122
|
+
**From source** (for hacking on it):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/Sahilll15/netdoctor.git
|
|
126
|
+
cd netdoctor && python3 -m venv .venv && source .venv/bin/activate
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> Pre-built packages (PyPI, Homebrew, Docker, binaries) ship automatically with each tagged release — see [RELEASING.md](RELEASING.md).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
netdoctor github.com # full picture: ladder + traceroute + scorecard
|
|
138
|
+
netdoctor github.com --quick # fast: skip traceroute & extra probes
|
|
139
|
+
netdoctor db.internal:5432 # is the Postgres port reachable?
|
|
140
|
+
netdoctor github.com 1.1.1.1 db:5432 # a whole fleet at once (concurrent)
|
|
141
|
+
netdoctor github.com --watch # live vitals dashboard (ctrl-c to stop)
|
|
142
|
+
netdoctor github.com --mtr # live mtr-style path monitor
|
|
143
|
+
netdoctor github.com --json # machine-readable output for CI / cron
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The target accepts a **bare host**, **host:port**, or a **full URL** — the scheme, port, and path are parsed out automatically. Explicit flags (`--port`, `--scheme`, `--path`) always win. Pass **several targets** to check them concurrently as a fleet.
|
|
147
|
+
|
|
148
|
+
> **Full by default.** A single-host run does the whole picture — the ladder, a traceroute, and a latency scorecard — with no flags to remember. Use `--quick` (or `--no-trace`) when you just want a fast up/down answer.
|
|
149
|
+
|
|
150
|
+
### Traceroute (default) & `--mtr`
|
|
151
|
+
|
|
152
|
+
Every single-host run includes a **traceroute** that shows the path hop by hop, with per-hop latency, reverse-DNS names, and a marker for **exactly where the path goes dark**:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
╭─ route → 140.82.112.3 ───────────────────────────────────────────────╮
|
|
156
|
+
│ ● 1 router.local 192.168.1.1 1 ms │
|
|
157
|
+
│ ● 2 isp-gateway.net 10.20.0.1 6 ms │
|
|
158
|
+
│ ○ 3 * * * — │
|
|
159
|
+
│ ● 4 140.82.112.3 140.82.112.3 11 ms ◀ destination │
|
|
160
|
+
│ │
|
|
161
|
+
│ ✓ destination reached │
|
|
162
|
+
╰───────────────────────────────────────────────────────────────────────╯
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`--mtr` turns that into a live, **mtr-style** monitor that re-probes on an interval and tracks rolling **loss %**, last/avg/best/worst latency, and a sparkline **per hop**:
|
|
166
|
+
|
|
167
|
+
<p align="center">
|
|
168
|
+
<img src="assets/mtr.gif" alt="netdoctor --mtr live path monitor: per-hop loss %, last/avg/best/worst latency, sparklines, and the point where the route to github.com goes dark" width="820">
|
|
169
|
+
</p>
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
╭─────┬─────────────────┬──────┬──────┬─────┬──────┬───────┬────────────╮
|
|
173
|
+
│ HOP │ HOST │ LOSS │ LAST │ AVG │ BEST │ WORST │ TREND │
|
|
174
|
+
├─────┼─────────────────┼──────┼──────┼─────┼──────┼───────┼────────────┤
|
|
175
|
+
│ 1 │ router.local │ 0% │ 1 │ 1 │ 1 │ 2 │ ▁▂▁▁▂▁ │
|
|
176
|
+
│ 2 │ isp-gateway.net │ 0% │ 6 │ 7 │ 5 │ 12 │ ▂▃▂▅▂▃ │
|
|
177
|
+
│ 3 │ * * * │ 100% │ — │ — │ — │ — │ ×××××× │
|
|
178
|
+
╰─────┴─────────────────┴──────┴──────┴─────┴──────┴───────┴────────────╯
|
|
179
|
+
⟳ next probe in 3s · run #6 · ctrl-c to stop
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Live monitor (`--watch`)
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
netdoctor github.com api.internal:8080 --watch --interval 10
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Turns netdoctor into a persistent dashboard that re-checks on an interval and keeps a **latency-trend sparkline** and a rolling **uptime %** per host:
|
|
189
|
+
|
|
190
|
+
<p align="center">
|
|
191
|
+
<img src="assets/watch.gif" alt="netdoctor --watch live dashboard monitoring github.com and 1.1.1.1: per-host DNS/port/HTTP, latency-trend sparkline, grade, uptime %, and status" width="880">
|
|
192
|
+
</p>
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
╭────┬──────────────────┬────────┬─────────┬──────────┬───────────────┬──────┬───┬──────────╮
|
|
196
|
+
│ │ HOST │ DNS │ PORT │ HTTP │ LATENCY TREND │ UP │ # │ STATUS │
|
|
197
|
+
├────┼──────────────────┼────────┼─────────┼──────────┼───────────────┼──────┼───┼──────────┤
|
|
198
|
+
│ ● │ github.com:443 │ ✓ 2ms │ ✓ 41ms │ ✓ 151ms │ ▁▂▂▁▃▂▁▂ │ 100% │ 8 │ HEALTHY │
|
|
199
|
+
│ ● │ api.internal:8080│ ✓ 1ms │ ✗ 0ms │ · skip │ ▅▆█▅▄▅██ │ 62% │ 8 │ UNHEALTHY│
|
|
200
|
+
╰────┴──────────────────┴────────┴─────────┴──────────┴───────────────┴──────┴───┴──────────╯
|
|
201
|
+
⟳ next refresh in 7s · every 10s · run #8 · ctrl-c to stop
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Scorecard — *how good, how fast, how reliable*
|
|
205
|
+
|
|
206
|
+
Every run ends in a **scorecard** that grades the host **A+ → F**. It probes the
|
|
207
|
+
app a few times by default (`--samples 5`) for a real latency distribution, jitter,
|
|
208
|
+
and an estimated request rate — bump `--samples` higher for a fuller picture:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
netdoctor api.example.com --samples 30
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
<p align="center">
|
|
215
|
+
<img src="assets/scorecard.gif" alt="netdoctor scorecard from a 12-probe benchmark of github.com: letter grade, reachability/performance/stability gauges, latency min/avg/p95/max, jitter, throughput, and a per-sample distribution sparkline" width="620">
|
|
216
|
+
</p>
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
╭─ scorecard ────────────────────────────────────────────────────────────────╮
|
|
220
|
+
│ │
|
|
221
|
+
│ A- 92 / 100 api.example.com:443 │
|
|
222
|
+
│ │
|
|
223
|
+
│ reachability ██████████████████████ 100 30/30 probes ok │
|
|
224
|
+
│ performance ██████████████████░░░░ 83 │
|
|
225
|
+
│ stability ███████████████████░░░ 88 ±50 ms jitter │
|
|
226
|
+
│ │
|
|
227
|
+
│ latency min 38 · avg 112 · p95 240 · max 410 ms │
|
|
228
|
+
│ rate 100% reachable · ≈ 8.9 req/s (single connection) │
|
|
229
|
+
│ samples ▄▃▄▁▆█▁▃▁▂▅▁▃▂▄▁▂▅▃▁▂▄▆▃▁▂▄▁▃▂ (30) │
|
|
230
|
+
│ │
|
|
231
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Metric | What it measures |
|
|
235
|
+
|---|---|
|
|
236
|
+
| **Reachability** | % of probes that succeeded |
|
|
237
|
+
| **Performance** | 0–100 score derived from average latency |
|
|
238
|
+
| **Stability** | consistency — high when jitter is low (needs `--samples ≥ 2`) |
|
|
239
|
+
| **Latency** | min / avg / p95 / max, plus jitter (std-dev) |
|
|
240
|
+
| **Rate** | success rate + estimated throughput (`≈ req/s`, single connection) |
|
|
241
|
+
| **Grade** | a weighted A+→F summary of all of the above |
|
|
242
|
+
|
|
243
|
+
In fleet and `--watch` views, the grade shows as a compact **GRADE** column per host.
|
|
244
|
+
|
|
245
|
+
### Options
|
|
246
|
+
|
|
247
|
+
| Flag | Purpose |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `-p, --port` | TCP port to test (default: 443 for https, 80 for http) |
|
|
250
|
+
| `--scheme {http,https}` | Force the scheme for the app check |
|
|
251
|
+
| `--path` | HTTP path to request (default: `/`) |
|
|
252
|
+
| `-t, --timeout SECONDS` | Per-check timeout (default: 4.0) |
|
|
253
|
+
| `-w, --watch` | Live vitals dashboard, re-checking on an interval |
|
|
254
|
+
| `--mtr` | Live mtr-style path monitor (single host) |
|
|
255
|
+
| `-n, --interval SECONDS` | `--watch` / `--mtr` refresh interval (default: 5.0) |
|
|
256
|
+
| `--max-runs N` | With `--watch` / `--mtr`, stop after N refreshes (0 = until ctrl-c) |
|
|
257
|
+
| `-s, --samples N` | App probes for the latency scorecard (default: 5) |
|
|
258
|
+
| `--no-trace` | Skip the traceroute step (faster) |
|
|
259
|
+
| `--quick` | Fastest run: skip traceroute and extra probes |
|
|
260
|
+
| `--json` | Emit JSON instead of the live report |
|
|
261
|
+
| `--no-color` | Disable colour / styling |
|
|
262
|
+
|
|
263
|
+
### Exit codes
|
|
264
|
+
|
|
265
|
+
`netdoctor` is built to drop into scripts, cron jobs, and CI:
|
|
266
|
+
|
|
267
|
+
| Code | Verdict | Meaning |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `0` | healthy | all core checks passed |
|
|
270
|
+
| `1` | degraded | reachable, but a check returned a warning (e.g. HTTP 5xx, cert expiring) |
|
|
271
|
+
| `2` | unhealthy | a core check failed (the diagnosis names which rung) |
|
|
272
|
+
| `64` | — | usage error |
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
netdoctor api.example.com/health || echo "page someone!"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### JSON output
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
netdoctor github.com --json
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"target": "github.com",
|
|
287
|
+
"port": 443,
|
|
288
|
+
"scheme": "https",
|
|
289
|
+
"elapsed_ms": 191.4,
|
|
290
|
+
"diagnosis": { "verdict": "healthy", "broken_rung": null, "...": "..." },
|
|
291
|
+
"scorecard": {
|
|
292
|
+
"grade": "A-", "overall": 92.0, "reachability": 100.0, "performance": 83.0,
|
|
293
|
+
"stability": 88.0, "throughput_rps": 8.9,
|
|
294
|
+
"latency_ms": { "min": 38, "avg": 112, "p95": 240, "max": 410, "jitter": 50 }
|
|
295
|
+
},
|
|
296
|
+
"rungs": [
|
|
297
|
+
{ "key": "dns", "status": "ok", "detail": "github.com → 140.82.112.3", "latency_ms": 11.4 },
|
|
298
|
+
{ "key": "ping", "status": "warn", "detail": "no reply (ICMP likely blocked)", "core": false },
|
|
299
|
+
{ "key": "port", "status": "ok", "detail": "open", "latency_ms": 41.0 },
|
|
300
|
+
{ "key": "http", "status": "ok", "detail": "200 OK · TLSv1.3 · cert 67d left",
|
|
301
|
+
"data": { "tls": "TLSv1.3", "cert_days_left": 67,
|
|
302
|
+
"timing": { "connect_ms": 38.0, "ttfb_ms": 97.2 }, "status_code": 200 } }
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Pass several targets and you get a JSON **array** instead — ideal for piping into `jq`, dashboards, or a cron alert.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## How it works
|
|
312
|
+
|
|
313
|
+
A clean separation of concerns — the whole point is that the logic is testable without touching the network:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
src/netdoctor/
|
|
317
|
+
├── model.py # Rung / Status / Verdict + diagnose() — pure logic, no I/O
|
|
318
|
+
├── checks.py # one function per rung; returns a completed Rung, never prints
|
|
319
|
+
├── score.py # the grading model (reachability/performance/latency) — pure logic
|
|
320
|
+
├── render.py # all the Rich presentation (ECG header, signal rail, dashboard)
|
|
321
|
+
└── cli.py # arg parsing, run loop, watch/fleet orchestration, JSON, exit codes
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The **diagnosis engine** (`diagnose()`) is deliberately I/O-free, so the decision rules are unit-tested in isolation:
|
|
325
|
+
|
|
326
|
+
- any **core** rung that fails → **unhealthy** (named by the first failure)
|
|
327
|
+
- else any core rung warning → **degraded**
|
|
328
|
+
- else → **healthy**
|
|
329
|
+
- advisory rungs (ping, traceroute) never change the verdict — a blocked ping just adds a note.
|
|
330
|
+
|
|
331
|
+
## Tests
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
pip install -e ".[dev]"
|
|
335
|
+
pytest
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Tests are deterministic and need no internet: the verdict engine is pure, target
|
|
339
|
+
parsing is table-driven, and the TCP check runs against a socket the test opens
|
|
340
|
+
and closes itself. CI runs them on Python 3.9 / 3.11 / 3.13 via GitHub Actions.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Roadmap
|
|
345
|
+
|
|
346
|
+
- [x] `--watch` — live monitoring dashboard with latency-trend sparklines
|
|
347
|
+
- [x] Multiple targets in one run (concurrent fleet view)
|
|
348
|
+
- [x] HTTP timing breakdown (connect / TTFB)
|
|
349
|
+
- [x] Quality scorecard — reachability / performance / latency / grade (`--samples`)
|
|
350
|
+
- [x] Hop-by-hop traceroute with break detection + an `mtr`-style live monitor (`--mtr`)
|
|
351
|
+
- [ ] Read targets from a file / stdin
|
|
352
|
+
- [ ] Ship as a single-file binary (PyInstaller) and a container image
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Demos
|
|
357
|
+
|
|
358
|
+
The animated demos in this README are real `netdoctor` runs, recorded with
|
|
359
|
+
[vhs](https://github.com/charmbracelet/vhs). The scripts live in
|
|
360
|
+
[`assets/`](assets/) as `*.tape` files, so any demo is reproducible:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
brew install vhs
|
|
364
|
+
vhs assets/hero.tape # → assets/hero.gif
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT © Sahil Chalke
|