logtap 0.3.0__tar.gz → 0.4.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.
- {logtap-0.3.0 → logtap-0.4.0}/PKG-INFO +1 -1
- logtap-0.4.0/docs/plans/2026-02-02-gpu-cloud-pivot-design.md +334 -0
- {logtap-0.3.0 → logtap-0.4.0}/pyproject.toml +1 -1
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/__init__.py +1 -1
- logtap-0.4.0/src/logtap/api/app.py +111 -0
- logtap-0.4.0/src/logtap/api/routes/health.py +41 -0
- logtap-0.4.0/src/logtap/api/routes/runs.py +351 -0
- logtap-0.4.0/src/logtap/cli/commands/collect.py +107 -0
- logtap-0.4.0/src/logtap/cli/commands/ingest.py +123 -0
- logtap-0.4.0/src/logtap/cli/commands/runs.py +116 -0
- logtap-0.4.0/src/logtap/cli/commands/tail.py +310 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/main.py +11 -5
- logtap-0.4.0/src/logtap/core/runs.py +393 -0
- logtap-0.4.0/src/logtap/models/responses.py +118 -0
- logtap-0.4.0/tests/unit/test_runs.py +173 -0
- {logtap-0.3.0 → logtap-0.4.0}/uv.lock +1 -1
- logtap-0.3.0/src/logtap/api/app.py +0 -45
- logtap-0.3.0/src/logtap/api/routes/health.py +0 -19
- logtap-0.3.0/src/logtap/cli/commands/tail.py +0 -121
- logtap-0.3.0/src/logtap/models/responses.py +0 -65
- {logtap-0.3.0 → logtap-0.4.0}/.dockerignore +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.env.example +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/FUNDING.yml +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/workflows/publish.yml +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.github/workflows/tests.yml +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.gitignore +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.pre-commit-config.yaml +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/.python-version +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/CONTRIBUTING.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/Dockerfile +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/LICENSE +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/README.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/SECURITY.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/docker-compose.yml +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/docs/CHAOS_TEST_REPORT.md +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/index.html +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/__main__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/dependencies.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/files.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/logs.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/parsed.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/files.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/query.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/serve.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/apache.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/auto.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/base.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/json_parser.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/nginx.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/syslog.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/reader.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/search.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/validation.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/models/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/src/logtap/models/config.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_parsers.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_robustness.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_security.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/conftest.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/fixtures/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/fixtures/log/.gitkeep +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/integration/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/integration/test_api.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/syslog +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp05x_q6nb +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp065rykpi +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0bm3cs8k +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0fivu3up +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0ijz83f3 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0rney_h_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp14lnvavq +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp1sps52p0 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp23kjlxys +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp2dqr9age +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp34evzgj3 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp36pg8nhr +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp3qua_9f7 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp409e3kxw +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp40wj2d8h +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp4av7aq1x +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp4f2yhnse +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5_fts0ah +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5cz1fdvm +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5fy_6kqm +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5jkv8ly_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5pyk6rzf +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5pzzgnl_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5w4es6gq +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp69zb3sz9 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6ap9i9r0 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6dhhwml0 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6syz7tnt +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp784bp8l5 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp8hg6l0m8 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp90rfsnyx +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp916cjvmi +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp91sd1e55 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp94c0aoeb +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp98m7nh88 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9c6k8nk6 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9nwk10y6 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9vph0i97 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_81gfiki +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_ct_338e +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_f0hoyd4 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_y251lk8 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpaacnlvjn +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpawrsvp35 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpb1ntqz4a +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpb7djh3dt +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpbsuxncdv +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpc6mlcsdl +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpc8qb9l4k +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcc8h0x4q +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcdvslz5p +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcq19058a +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcw2qflak +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe3pta4d3 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe8493tiy +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe946rhvt +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpef1f9h9m +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpeg2f7oov +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpepqoaana +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpf9uyvwel +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpfab61tmu +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpfu4d3omv +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpg93pkzoc +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmph17de7no +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmph59moiyt +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmphn0i0ngy +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpi25qtdum +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpiso7tb71 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpju_l_8ur +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpkewrczka +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpkuob2ku5 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpl1xj0zyo +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpl9rq9k_j +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpm2bmkbvd +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpm_xd1lzm +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmc_npe2u +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmq1l8ses +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmvz0bev1 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmywg8jr4 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpn2ep2xoe +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpn_3mnuzy +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpnmn9ob75 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpocsk9b52 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpod_7ghhq +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpofk_ue9w +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpp3t0hk_v +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppas_s166 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppn4p6_2h +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppv7dcstw +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppwn77fw3 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpq6ru59zb +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpq_k47l0a +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpqevemzw1 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpqzd9zjft +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpr2bv_nlo +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpr4txnw42 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprknp7bj3 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprl6k40a_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprl8i4tj4 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmproqzibwd +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprs1hp8wh +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpstuzwlgi +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpt6c4i65h +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmptd48yi53 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmptdqndikp +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu101z7gi +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu2te57pf +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu5atvpbl +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu7ty_3jg +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpuk4xm9wu +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpupxqxpc_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpv318rejg +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpvcrh7utz +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpvsayrh3y +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpw_4wsps5 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwbcafh_z +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwfacpdft +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwg97gouq +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpxwt4w91l +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy21c1m2c +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy4qf8eco +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy_qj2ih8 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpya6d_rq0 +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyf_tdrun +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyiar72k_ +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpykep4ebm +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyqgzw6nh +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpz4t5g3ee +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpz65snq8z +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzaxdh_3c +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzeplzl1z +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzkitocmv +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/unit/__init__.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_parsers.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_reader.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_search.py +0 -0
- {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logtap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A CLI-first log access tool for Unix systems. Remote log file access without SSH.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cainky/logtap
|
|
6
6
|
Project-URL: Repository, https://github.com/cainky/logtap
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# logtap GPU Cloud Pivot Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-02-02
|
|
4
|
+
**Status:** Approved
|
|
5
|
+
**Version:** 0.4.0 target
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Product Thesis
|
|
10
|
+
|
|
11
|
+
**One-line:** `tail -f` over HTTPS for ML training jobs — survives disconnects, supports multi-node aggregation, zero infra.
|
|
12
|
+
|
|
13
|
+
**Job-to-be-done:** When training on a cheap GPU box, watch training logs and troubleshoot without babysitting an SSH session, with logs preserved even if the instance is ephemeral.
|
|
14
|
+
|
|
15
|
+
**Primary user:** Solo ML engineers running training jobs (1-5 machines)
|
|
16
|
+
**Viral amplifier:** Indie hackers / hobbyists
|
|
17
|
+
**Explicitly not:** Teams/orgs (avoid MLOps platform gravity)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Core Features (v0.4.0)
|
|
22
|
+
|
|
23
|
+
1. **Ingest path** — `python train.py | logtap ingest run1`
|
|
24
|
+
2. **Resume cursors** — reconnect with `?since=<cursor>`, no gaps
|
|
25
|
+
3. **Collector mode** — `logtap collect` accepts ingested streams
|
|
26
|
+
4. **Tags + filtering** — `--tag node=gpu1` and `--tag node=gpu1` on tail
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## CLI Commands
|
|
31
|
+
|
|
32
|
+
### Existing (unchanged)
|
|
33
|
+
- `logtap serve` — serve static log directory (legacy)
|
|
34
|
+
- `logtap query` — query a file
|
|
35
|
+
- `logtap tail` — stream/tail a file or run
|
|
36
|
+
- `logtap files` — list available files
|
|
37
|
+
|
|
38
|
+
### New Commands
|
|
39
|
+
|
|
40
|
+
#### `logtap collect`
|
|
41
|
+
```
|
|
42
|
+
logtap collect [OPTIONS]
|
|
43
|
+
|
|
44
|
+
Start collector server to accept ingested runs.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-p, --port INT Port to listen on [default: 8000]
|
|
48
|
+
-H, --host TEXT Host to bind to [default: 0.0.0.0]
|
|
49
|
+
-k, --api-key TEXT API key for auth [env: LOGTAP_API_KEY]
|
|
50
|
+
-d, --data-dir PATH Directory for run storage [default: ~/.logtap/runs]
|
|
51
|
+
--buffer-lines INT In-memory cache size per run [default: 100000]
|
|
52
|
+
--max-disk-mb INT Max disk usage across all runs [default: 1000]
|
|
53
|
+
--retention-hours INT Hours to retain runs [default: 72]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### `logtap ingest`
|
|
57
|
+
```
|
|
58
|
+
logtap ingest [RUN_ID] [OPTIONS]
|
|
59
|
+
|
|
60
|
+
Pipe stdin to collector as a named run.
|
|
61
|
+
|
|
62
|
+
Arguments:
|
|
63
|
+
RUN_ID Run name [default: run-YYYYMMDD-HHMMSS]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
-s, --server TEXT Collector URL [default: http://localhost:8000] [env: LOGTAP_SERVER]
|
|
67
|
+
-k, --api-key TEXT API key [env: LOGTAP_API_KEY]
|
|
68
|
+
-t, --tag TEXT Tags as key=value (repeatable)
|
|
69
|
+
--quiet Suppress status messages
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### `logtap runs`
|
|
73
|
+
```
|
|
74
|
+
logtap runs [OPTIONS]
|
|
75
|
+
|
|
76
|
+
List runs on a collector.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
-s, --server TEXT Collector URL [env: LOGTAP_SERVER]
|
|
80
|
+
-k, --api-key TEXT API key [env: LOGTAP_API_KEY]
|
|
81
|
+
--since-hours INT Show runs active within N hours [default: 24]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### `logtap tail` (updated)
|
|
85
|
+
```
|
|
86
|
+
logtap tail TARGET [OPTIONS]
|
|
87
|
+
|
|
88
|
+
Tail a run or file.
|
|
89
|
+
|
|
90
|
+
Arguments:
|
|
91
|
+
TARGET Run name or file path
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
-s, --server TEXT Server URL [env: LOGTAP_SERVER]
|
|
95
|
+
-k, --api-key TEXT API key [env: LOGTAP_API_KEY]
|
|
96
|
+
-f, --follow Keep streaming new lines
|
|
97
|
+
-n, --lines INT Initial lines to show [default: 50]
|
|
98
|
+
--since INT Resume from cursor (exclusive)
|
|
99
|
+
-m, --mode TEXT auto|runs|files [default: auto]
|
|
100
|
+
-t, --tag TEXT Filter by tag (repeatable, AND semantics)
|
|
101
|
+
--output TEXT pretty|plain|jsonl [default: pretty]
|
|
102
|
+
|
|
103
|
+
Resolution (mode=auto):
|
|
104
|
+
1. If --server set: query server for run, fall back to file
|
|
105
|
+
2. If --server unset: treat as local file path
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## API Endpoints
|
|
111
|
+
|
|
112
|
+
### Collector Mode (`/runs/*`)
|
|
113
|
+
|
|
114
|
+
#### `POST /runs/{run_id}/ingest`
|
|
115
|
+
|
|
116
|
+
Chunked streaming ingest of log lines.
|
|
117
|
+
|
|
118
|
+
**Request:**
|
|
119
|
+
```http
|
|
120
|
+
POST /runs/run1/ingest HTTP/1.1
|
|
121
|
+
Content-Type: text/plain
|
|
122
|
+
Transfer-Encoding: chunked
|
|
123
|
+
X-API-Key: secret
|
|
124
|
+
X-Logtap-Tag: node=gpu1
|
|
125
|
+
X-Logtap-Tag: rank=0
|
|
126
|
+
|
|
127
|
+
line 1\n
|
|
128
|
+
line 2\n
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Behavior:**
|
|
132
|
+
- `\n` is record delimiter
|
|
133
|
+
- Partial line at close: flush as final line
|
|
134
|
+
- Auto-creates run if doesn't exist (201 Created)
|
|
135
|
+
- Tags: merge semantics, 409 on conflicting value for same key
|
|
136
|
+
|
|
137
|
+
**Response:**
|
|
138
|
+
```json
|
|
139
|
+
{"run_id": "run1", "lines_ingested": 1848, "cursor_end": 1847}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `GET /runs`
|
|
143
|
+
|
|
144
|
+
List runs.
|
|
145
|
+
|
|
146
|
+
**Query params:**
|
|
147
|
+
- `since_hours` — filter to runs active within N hours
|
|
148
|
+
|
|
149
|
+
**Response:**
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"runs": [{
|
|
153
|
+
"id": "run1",
|
|
154
|
+
"lines": 12847,
|
|
155
|
+
"cursor_earliest": 0,
|
|
156
|
+
"cursor_latest": 12846,
|
|
157
|
+
"tags": {"node": "gpu1"},
|
|
158
|
+
"created_at": "2026-02-02T18:30:00Z",
|
|
159
|
+
"last_activity": "2026-02-02T19:45:12Z",
|
|
160
|
+
"active": true
|
|
161
|
+
}]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `GET /runs/{run_id}`
|
|
166
|
+
|
|
167
|
+
Get run details.
|
|
168
|
+
|
|
169
|
+
**Response:**
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"id": "run1",
|
|
173
|
+
"lines": 12847,
|
|
174
|
+
"cursor_earliest": 0,
|
|
175
|
+
"cursor_latest": 12846,
|
|
176
|
+
"tags": {"node": "gpu1"},
|
|
177
|
+
"created_at": "2026-02-02T18:30:00Z",
|
|
178
|
+
"last_activity": "2026-02-02T19:45:12Z",
|
|
179
|
+
"active": true,
|
|
180
|
+
"bytes_on_disk": 524288
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `GET /runs/{run_id}/stream`
|
|
185
|
+
|
|
186
|
+
SSE stream for tailing.
|
|
187
|
+
|
|
188
|
+
**Query params:**
|
|
189
|
+
- `since` — cursor to resume from (exclusive)
|
|
190
|
+
- `tail` — last N lines if since omitted [default: 50]
|
|
191
|
+
- `follow` — keep connection open [default: false]
|
|
192
|
+
- `tag` — filter by tag (repeatable, AND semantics)
|
|
193
|
+
|
|
194
|
+
**Headers:**
|
|
195
|
+
- `Last-Event-ID` — alternative to `since` param
|
|
196
|
+
|
|
197
|
+
**Response:**
|
|
198
|
+
```
|
|
199
|
+
HTTP/1.1 200 OK
|
|
200
|
+
Content-Type: text/event-stream
|
|
201
|
+
X-Logtap-Earliest-Cursor: 0
|
|
202
|
+
X-Logtap-Latest-Cursor: 12846
|
|
203
|
+
|
|
204
|
+
event: meta
|
|
205
|
+
data: {"cursor_earliest": 0, "cursor_latest": 12846, "gap": false}
|
|
206
|
+
|
|
207
|
+
id: 5001
|
|
208
|
+
event: line
|
|
209
|
+
data: {"cursor": 5001, "line": "Epoch 5: loss=0.089", "ts": "2026-02-02T19:30:01Z"}
|
|
210
|
+
|
|
211
|
+
: heartbeat
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Gap detection:**
|
|
216
|
+
If `since < cursor_earliest`, first meta event includes:
|
|
217
|
+
```json
|
|
218
|
+
{"cursor_earliest": 1000, "cursor_latest": 12846, "gap": true, "missed": 500}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `GET /runs/{run_id}/query`
|
|
222
|
+
|
|
223
|
+
Non-streaming query for a range.
|
|
224
|
+
|
|
225
|
+
**Query params:**
|
|
226
|
+
- `from` / `to` — cursor range (inclusive)
|
|
227
|
+
- `tail` — last N lines (alternative)
|
|
228
|
+
- `limit` — max lines [default: 1000, max: 10000]
|
|
229
|
+
- `search` — substring filter
|
|
230
|
+
- `regex` — regex filter (mutually exclusive with search)
|
|
231
|
+
- `output` — jsonl|plain [default: jsonl]
|
|
232
|
+
|
|
233
|
+
**Response (jsonl):**
|
|
234
|
+
```
|
|
235
|
+
{"cursor": 1000, "line": "Epoch 1: loss=0.523", "ts": "..."}
|
|
236
|
+
{"cursor": 1001, "line": "Epoch 2: loss=0.312", "ts": "..."}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `GET /health`
|
|
240
|
+
|
|
241
|
+
**Response:**
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"status": "ok",
|
|
245
|
+
"mode": "collect",
|
|
246
|
+
"features": ["runs"],
|
|
247
|
+
"runs": 5,
|
|
248
|
+
"uptime_seconds": 3600
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Error Responses
|
|
253
|
+
|
|
254
|
+
| Code | Error | When |
|
|
255
|
+
|------|-------|------|
|
|
256
|
+
| 400 | `invalid_tag` | Malformed tag |
|
|
257
|
+
| 400 | `invalid_cursor` | Non-numeric cursor |
|
|
258
|
+
| 400 | `invalid_query` | Both search and regex provided |
|
|
259
|
+
| 401 | `unauthorized` | Missing/invalid API key |
|
|
260
|
+
| 404 | `run_not_found` | Run doesn't exist |
|
|
261
|
+
| 409 | `tag_conflict` | Conflicting tag value on ingest |
|
|
262
|
+
| 507 | `insufficient_storage` | Disk cap exceeded |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Storage Model
|
|
267
|
+
|
|
268
|
+
**Append-only file per run + in-memory tail cache.**
|
|
269
|
+
|
|
270
|
+
- Each run: `{data_dir}/{run_id}/log.txt` (append-only)
|
|
271
|
+
- Cursor = line number (0-indexed)
|
|
272
|
+
- In-memory ring buffer caches last N lines for fast tail
|
|
273
|
+
- Retention enforced by `--retention-hours`
|
|
274
|
+
- Disk cap enforced by `--max-disk-mb` with oldest-run eviction
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Protocol Details
|
|
279
|
+
|
|
280
|
+
### Ingest Flow
|
|
281
|
+
1. Client does preflight `GET /runs/{id}` to check auth (optional, fast-fail)
|
|
282
|
+
2. Client opens chunked `POST /runs/{id}/ingest`
|
|
283
|
+
3. Server assigns cursor per line (monotonic int64)
|
|
284
|
+
4. On connection close: flush partial line, return summary
|
|
285
|
+
|
|
286
|
+
### Resume Flow
|
|
287
|
+
1. Client connects `GET /runs/{id}/stream?since=5000&follow=true`
|
|
288
|
+
2. Server returns `X-Logtap-Earliest-Cursor` / `X-Logtap-Latest-Cursor`
|
|
289
|
+
3. If `since < earliest`: meta event includes `gap: true, missed: N`
|
|
290
|
+
4. Client prints: `reconnected (missed N lines)` or continues seamlessly
|
|
291
|
+
|
|
292
|
+
### Tag Semantics
|
|
293
|
+
- Keys: `[a-zA-Z0-9_.-]+`
|
|
294
|
+
- Values: any string, max 256 chars
|
|
295
|
+
- Ingest: merge, error on conflict
|
|
296
|
+
- Filter: AND semantics, exact match
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Migration Strategy
|
|
301
|
+
|
|
302
|
+
**Soft migration (backward compatible):**
|
|
303
|
+
- Keep `/logs/*`, `/files/*` endpoints unchanged
|
|
304
|
+
- Add `/runs/*` endpoints for new workflow
|
|
305
|
+
- Docs lead with ML use case, legacy in appendix
|
|
306
|
+
- Stay on `0.x` until features stabilize
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Build Order
|
|
311
|
+
|
|
312
|
+
1. **Ingest path** — stdin to collector
|
|
313
|
+
2. **Resume cursors** — gap detection, seamless reconnect
|
|
314
|
+
3. **Collector mode** — `logtap collect` with file storage
|
|
315
|
+
4. **Tags + filtering** — multi-node support
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## What NOT to Build
|
|
320
|
+
|
|
321
|
+
- Dashboards
|
|
322
|
+
- Metrics/graphs
|
|
323
|
+
- Persistence backends (S3/R2) — defer to v0.5+
|
|
324
|
+
- RBAC / SSO
|
|
325
|
+
- Experiment comparison
|
|
326
|
+
- Anything that smells like "MLOps platform"
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Success Criteria
|
|
331
|
+
|
|
332
|
+
- User can `pip install logtap` and stream training logs in <60 seconds
|
|
333
|
+
- SSH disconnect + reconnect shows "missed 0 lines"
|
|
334
|
+
- README demo is a 20-second GIF showing the magic moment
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""FastAPI application factory for logtap."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
|
|
10
|
+
from logtap import __version__
|
|
11
|
+
from logtap.api.routes import files, health, logs, parsed, runs
|
|
12
|
+
from logtap.core.runs import RunStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_app() -> FastAPI:
|
|
16
|
+
"""
|
|
17
|
+
Create and configure the FastAPI application for serve mode.
|
|
18
|
+
|
|
19
|
+
Serves static log files from a directory (legacy mode).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Configured FastAPI application instance.
|
|
23
|
+
"""
|
|
24
|
+
app = FastAPI(
|
|
25
|
+
title="logtap",
|
|
26
|
+
description="A CLI-first log access tool for Unix systems.",
|
|
27
|
+
version=__version__,
|
|
28
|
+
docs_url="/docs",
|
|
29
|
+
redoc_url="/redoc",
|
|
30
|
+
openapi_url="/openapi.json",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Store mode info
|
|
34
|
+
app.state.mode = "serve"
|
|
35
|
+
app.state.features = ["files"]
|
|
36
|
+
|
|
37
|
+
# Configure CORS
|
|
38
|
+
app.add_middleware(
|
|
39
|
+
CORSMiddleware,
|
|
40
|
+
allow_origins=["*"],
|
|
41
|
+
allow_credentials=True,
|
|
42
|
+
allow_methods=["*"],
|
|
43
|
+
allow_headers=["*"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Include routers
|
|
47
|
+
app.include_router(health.router, tags=["health"])
|
|
48
|
+
app.include_router(logs.router, prefix="/logs", tags=["logs"])
|
|
49
|
+
app.include_router(files.router, prefix="/files", tags=["files"])
|
|
50
|
+
app.include_router(parsed.router, prefix="/parsed", tags=["parsed"])
|
|
51
|
+
|
|
52
|
+
return app
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_collector_app() -> FastAPI:
|
|
56
|
+
"""
|
|
57
|
+
Create and configure the FastAPI application for collector mode.
|
|
58
|
+
|
|
59
|
+
Accepts ingested log streams and serves them for tailing.
|
|
60
|
+
This is the recommended mode for ML training logs.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Configured FastAPI application instance.
|
|
64
|
+
"""
|
|
65
|
+
app = FastAPI(
|
|
66
|
+
title="logtap",
|
|
67
|
+
description="tail -f for GPU clouds. Survives disconnects, aggregates multi-node.",
|
|
68
|
+
version=__version__,
|
|
69
|
+
docs_url="/docs",
|
|
70
|
+
redoc_url="/redoc",
|
|
71
|
+
openapi_url="/openapi.json",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Store mode info and start time
|
|
75
|
+
app.state.mode = "collect"
|
|
76
|
+
app.state.features = ["runs"]
|
|
77
|
+
app.state.start_time = time.time()
|
|
78
|
+
|
|
79
|
+
# Configure CORS
|
|
80
|
+
app.add_middleware(
|
|
81
|
+
CORSMiddleware,
|
|
82
|
+
allow_origins=["*"],
|
|
83
|
+
allow_credentials=True,
|
|
84
|
+
allow_methods=["*"],
|
|
85
|
+
allow_headers=["*"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Initialize run store from environment
|
|
89
|
+
data_dir = Path(os.environ.get("LOGTAP_DATA_DIR", "~/.logtap/runs")).expanduser()
|
|
90
|
+
buffer_lines = int(os.environ.get("LOGTAP_BUFFER_LINES", "100000"))
|
|
91
|
+
max_disk_mb = int(os.environ.get("LOGTAP_MAX_DISK_MB", "1000"))
|
|
92
|
+
retention_hours = int(os.environ.get("LOGTAP_RETENTION_HOURS", "72"))
|
|
93
|
+
|
|
94
|
+
run_store = RunStore(
|
|
95
|
+
data_dir=data_dir,
|
|
96
|
+
buffer_lines=buffer_lines,
|
|
97
|
+
max_disk_mb=max_disk_mb,
|
|
98
|
+
retention_hours=retention_hours,
|
|
99
|
+
)
|
|
100
|
+
runs.set_run_store(run_store)
|
|
101
|
+
app.state.run_store = run_store
|
|
102
|
+
|
|
103
|
+
# Include routers
|
|
104
|
+
app.include_router(health.router, tags=["health"])
|
|
105
|
+
app.include_router(runs.router, prefix="/runs", tags=["runs"])
|
|
106
|
+
|
|
107
|
+
return app
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Create default app instance for uvicorn (serve mode)
|
|
111
|
+
app = create_app()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Health check endpoint for logtap."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request
|
|
6
|
+
|
|
7
|
+
from logtap import __version__
|
|
8
|
+
from logtap.models.responses import HealthResponse
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/health", response_model=HealthResponse)
|
|
14
|
+
async def health_check(request: Request) -> HealthResponse:
|
|
15
|
+
"""
|
|
16
|
+
Check the health of the logtap service.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Health status, version, mode, and capability information.
|
|
20
|
+
"""
|
|
21
|
+
mode = getattr(request.app.state, "mode", "serve")
|
|
22
|
+
features = getattr(request.app.state, "features", ["files"])
|
|
23
|
+
|
|
24
|
+
# Get run count if in collect mode
|
|
25
|
+
runs_count = None
|
|
26
|
+
if hasattr(request.app.state, "run_store"):
|
|
27
|
+
runs_count = len(request.app.state.run_store.list_runs())
|
|
28
|
+
|
|
29
|
+
# Get uptime if start_time is set
|
|
30
|
+
uptime = None
|
|
31
|
+
if hasattr(request.app.state, "start_time"):
|
|
32
|
+
uptime = int(time.time() - request.app.state.start_time)
|
|
33
|
+
|
|
34
|
+
return HealthResponse(
|
|
35
|
+
status="healthy",
|
|
36
|
+
version=__version__,
|
|
37
|
+
mode=mode,
|
|
38
|
+
features=features,
|
|
39
|
+
runs=runs_count,
|
|
40
|
+
uptime_seconds=uptime,
|
|
41
|
+
)
|