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.
Files changed (213) hide show
  1. {logtap-0.3.0 → logtap-0.4.0}/PKG-INFO +1 -1
  2. logtap-0.4.0/docs/plans/2026-02-02-gpu-cloud-pivot-design.md +334 -0
  3. {logtap-0.3.0 → logtap-0.4.0}/pyproject.toml +1 -1
  4. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/__init__.py +1 -1
  5. logtap-0.4.0/src/logtap/api/app.py +111 -0
  6. logtap-0.4.0/src/logtap/api/routes/health.py +41 -0
  7. logtap-0.4.0/src/logtap/api/routes/runs.py +351 -0
  8. logtap-0.4.0/src/logtap/cli/commands/collect.py +107 -0
  9. logtap-0.4.0/src/logtap/cli/commands/ingest.py +123 -0
  10. logtap-0.4.0/src/logtap/cli/commands/runs.py +116 -0
  11. logtap-0.4.0/src/logtap/cli/commands/tail.py +310 -0
  12. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/main.py +11 -5
  13. logtap-0.4.0/src/logtap/core/runs.py +393 -0
  14. logtap-0.4.0/src/logtap/models/responses.py +118 -0
  15. logtap-0.4.0/tests/unit/test_runs.py +173 -0
  16. {logtap-0.3.0 → logtap-0.4.0}/uv.lock +1 -1
  17. logtap-0.3.0/src/logtap/api/app.py +0 -45
  18. logtap-0.3.0/src/logtap/api/routes/health.py +0 -19
  19. logtap-0.3.0/src/logtap/cli/commands/tail.py +0 -121
  20. logtap-0.3.0/src/logtap/models/responses.py +0 -65
  21. {logtap-0.3.0 → logtap-0.4.0}/.dockerignore +0 -0
  22. {logtap-0.3.0 → logtap-0.4.0}/.env.example +0 -0
  23. {logtap-0.3.0 → logtap-0.4.0}/.github/FUNDING.yml +0 -0
  24. {logtap-0.3.0 → logtap-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {logtap-0.3.0 → logtap-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {logtap-0.3.0 → logtap-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {logtap-0.3.0 → logtap-0.4.0}/.github/workflows/publish.yml +0 -0
  28. {logtap-0.3.0 → logtap-0.4.0}/.github/workflows/tests.yml +0 -0
  29. {logtap-0.3.0 → logtap-0.4.0}/.gitignore +0 -0
  30. {logtap-0.3.0 → logtap-0.4.0}/.pre-commit-config.yaml +0 -0
  31. {logtap-0.3.0 → logtap-0.4.0}/.python-version +0 -0
  32. {logtap-0.3.0 → logtap-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  33. {logtap-0.3.0 → logtap-0.4.0}/CONTRIBUTING.md +0 -0
  34. {logtap-0.3.0 → logtap-0.4.0}/Dockerfile +0 -0
  35. {logtap-0.3.0 → logtap-0.4.0}/LICENSE +0 -0
  36. {logtap-0.3.0 → logtap-0.4.0}/README.md +0 -0
  37. {logtap-0.3.0 → logtap-0.4.0}/SECURITY.md +0 -0
  38. {logtap-0.3.0 → logtap-0.4.0}/docker-compose.yml +0 -0
  39. {logtap-0.3.0 → logtap-0.4.0}/docs/CHAOS_TEST_REPORT.md +0 -0
  40. {logtap-0.3.0 → logtap-0.4.0}/index.html +0 -0
  41. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/__main__.py +0 -0
  42. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/__init__.py +0 -0
  43. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/dependencies.py +0 -0
  44. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/__init__.py +0 -0
  45. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/files.py +0 -0
  46. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/logs.py +0 -0
  47. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/api/routes/parsed.py +0 -0
  48. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/__init__.py +0 -0
  49. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/__init__.py +0 -0
  50. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/files.py +0 -0
  51. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/query.py +0 -0
  52. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/cli/commands/serve.py +0 -0
  53. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/__init__.py +0 -0
  54. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/__init__.py +0 -0
  55. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/apache.py +0 -0
  56. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/auto.py +0 -0
  57. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/base.py +0 -0
  58. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/json_parser.py +0 -0
  59. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/nginx.py +0 -0
  60. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/parsers/syslog.py +0 -0
  61. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/reader.py +0 -0
  62. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/search.py +0 -0
  63. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/core/validation.py +0 -0
  64. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/models/__init__.py +0 -0
  65. {logtap-0.3.0 → logtap-0.4.0}/src/logtap/models/config.py +0 -0
  66. {logtap-0.3.0 → logtap-0.4.0}/tests/__init__.py +0 -0
  67. {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/__init__.py +0 -0
  68. {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_parsers.py +0 -0
  69. {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_robustness.py +0 -0
  70. {logtap-0.3.0 → logtap-0.4.0}/tests/chaos/test_security.py +0 -0
  71. {logtap-0.3.0 → logtap-0.4.0}/tests/conftest.py +0 -0
  72. {logtap-0.3.0 → logtap-0.4.0}/tests/fixtures/__init__.py +0 -0
  73. {logtap-0.3.0 → logtap-0.4.0}/tests/fixtures/log/.gitkeep +0 -0
  74. {logtap-0.3.0 → logtap-0.4.0}/tests/integration/__init__.py +0 -0
  75. {logtap-0.3.0 → logtap-0.4.0}/tests/integration/test_api.py +0 -0
  76. {logtap-0.3.0 → logtap-0.4.0}/tests/log/syslog +0 -0
  77. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp05x_q6nb +0 -0
  78. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp065rykpi +0 -0
  79. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0bm3cs8k +0 -0
  80. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0fivu3up +0 -0
  81. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0ijz83f3 +0 -0
  82. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp0rney_h_ +0 -0
  83. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp14lnvavq +0 -0
  84. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp1sps52p0 +0 -0
  85. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp23kjlxys +0 -0
  86. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp2dqr9age +0 -0
  87. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp34evzgj3 +0 -0
  88. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp36pg8nhr +0 -0
  89. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp3qua_9f7 +0 -0
  90. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp409e3kxw +0 -0
  91. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp40wj2d8h +0 -0
  92. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp4av7aq1x +0 -0
  93. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp4f2yhnse +0 -0
  94. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5_fts0ah +0 -0
  95. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5cz1fdvm +0 -0
  96. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5fy_6kqm +0 -0
  97. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5jkv8ly_ +0 -0
  98. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5pyk6rzf +0 -0
  99. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5pzzgnl_ +0 -0
  100. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp5w4es6gq +0 -0
  101. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp69zb3sz9 +0 -0
  102. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6ap9i9r0 +0 -0
  103. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6dhhwml0 +0 -0
  104. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp6syz7tnt +0 -0
  105. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp784bp8l5 +0 -0
  106. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp8hg6l0m8 +0 -0
  107. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp90rfsnyx +0 -0
  108. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp916cjvmi +0 -0
  109. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp91sd1e55 +0 -0
  110. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp94c0aoeb +0 -0
  111. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp98m7nh88 +0 -0
  112. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9c6k8nk6 +0 -0
  113. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9nwk10y6 +0 -0
  114. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp9vph0i97 +0 -0
  115. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_81gfiki +0 -0
  116. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_ct_338e +0 -0
  117. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_f0hoyd4 +0 -0
  118. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmp_y251lk8 +0 -0
  119. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpaacnlvjn +0 -0
  120. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpawrsvp35 +0 -0
  121. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpb1ntqz4a +0 -0
  122. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpb7djh3dt +0 -0
  123. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpbsuxncdv +0 -0
  124. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpc6mlcsdl +0 -0
  125. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpc8qb9l4k +0 -0
  126. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcc8h0x4q +0 -0
  127. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcdvslz5p +0 -0
  128. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcq19058a +0 -0
  129. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpcw2qflak +0 -0
  130. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe3pta4d3 +0 -0
  131. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe8493tiy +0 -0
  132. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpe946rhvt +0 -0
  133. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpef1f9h9m +0 -0
  134. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpeg2f7oov +0 -0
  135. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpepqoaana +0 -0
  136. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpf9uyvwel +0 -0
  137. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpfab61tmu +0 -0
  138. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpfu4d3omv +0 -0
  139. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpg93pkzoc +0 -0
  140. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmph17de7no +0 -0
  141. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmph59moiyt +0 -0
  142. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmphn0i0ngy +0 -0
  143. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpi25qtdum +0 -0
  144. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpiso7tb71 +0 -0
  145. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpju_l_8ur +0 -0
  146. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpkewrczka +0 -0
  147. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpkuob2ku5 +0 -0
  148. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpl1xj0zyo +0 -0
  149. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpl9rq9k_j +0 -0
  150. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpm2bmkbvd +0 -0
  151. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpm_xd1lzm +0 -0
  152. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmc_npe2u +0 -0
  153. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmq1l8ses +0 -0
  154. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmvz0bev1 +0 -0
  155. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpmywg8jr4 +0 -0
  156. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpn2ep2xoe +0 -0
  157. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpn_3mnuzy +0 -0
  158. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpnmn9ob75 +0 -0
  159. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpocsk9b52 +0 -0
  160. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpod_7ghhq +0 -0
  161. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpofk_ue9w +0 -0
  162. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpp3t0hk_v +0 -0
  163. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppas_s166 +0 -0
  164. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppn4p6_2h +0 -0
  165. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppv7dcstw +0 -0
  166. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmppwn77fw3 +0 -0
  167. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpq6ru59zb +0 -0
  168. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpq_k47l0a +0 -0
  169. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpqevemzw1 +0 -0
  170. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpqzd9zjft +0 -0
  171. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpr2bv_nlo +0 -0
  172. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpr4txnw42 +0 -0
  173. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprknp7bj3 +0 -0
  174. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprl6k40a_ +0 -0
  175. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprl8i4tj4 +0 -0
  176. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmproqzibwd +0 -0
  177. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmprs1hp8wh +0 -0
  178. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpstuzwlgi +0 -0
  179. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpt6c4i65h +0 -0
  180. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmptd48yi53 +0 -0
  181. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmptdqndikp +0 -0
  182. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu101z7gi +0 -0
  183. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu2te57pf +0 -0
  184. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu5atvpbl +0 -0
  185. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpu7ty_3jg +0 -0
  186. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpuk4xm9wu +0 -0
  187. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpupxqxpc_ +0 -0
  188. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpv318rejg +0 -0
  189. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpvcrh7utz +0 -0
  190. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpvsayrh3y +0 -0
  191. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpw_4wsps5 +0 -0
  192. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwbcafh_z +0 -0
  193. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwfacpdft +0 -0
  194. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpwg97gouq +0 -0
  195. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpxwt4w91l +0 -0
  196. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy21c1m2c +0 -0
  197. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy4qf8eco +0 -0
  198. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpy_qj2ih8 +0 -0
  199. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpya6d_rq0 +0 -0
  200. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyf_tdrun +0 -0
  201. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyiar72k_ +0 -0
  202. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpykep4ebm +0 -0
  203. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpyqgzw6nh +0 -0
  204. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpz4t5g3ee +0 -0
  205. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpz65snq8z +0 -0
  206. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzaxdh_3c +0 -0
  207. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzeplzl1z +0 -0
  208. {logtap-0.3.0 → logtap-0.4.0}/tests/log/tmpzkitocmv +0 -0
  209. {logtap-0.3.0 → logtap-0.4.0}/tests/unit/__init__.py +0 -0
  210. {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_parsers.py +0 -0
  211. {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_reader.py +0 -0
  212. {logtap-0.3.0 → logtap-0.4.0}/tests/unit/test_search.py +0 -0
  213. {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.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "logtap"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "A CLI-first log access tool for Unix systems. Remote log file access without SSH."
5
5
  authors = [{name = "cainky", email = "kylecain.me@gmail.com"}]
6
6
  readme = "README.md"
@@ -4,5 +4,5 @@ logtap - A CLI-first log access tool for Unix systems.
4
4
  Remote log file access without SSH. No database. No complex setup.
5
5
  """
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "0.4.0"
8
8
  __author__ = "Kyle Cain"
@@ -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
+ )