Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ jobs:
path: npm/platforms/${{ matrix.platform }}/bin/
retention-days: 1

- name: Create tarball (Unix only — for Homebrew)
if: runner.os != 'Windows'
run: |
cd npm/platforms/${{ matrix.platform }}/bin
tar -czf tailflow-${{ matrix.platform }}.tar.gz tailflow tailflow-daemon

- uses: actions/upload-artifact@v4
if: runner.os != 'Windows'
with:
name: tarball-${{ matrix.platform }}
path: npm/platforms/${{ matrix.platform }}/bin/tailflow-${{ matrix.platform }}.tar.gz
retention-days: 1

# ── Step 3: publish all npm packages ──────────────────────────────────────
publish-npm:
name: Publish to npm
Expand Down Expand Up @@ -235,13 +248,90 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: cd npm/tailflow && npm publish

# Download tarballs so they can be attached to the GitHub Release
- uses: actions/download-artifact@v4
with:
name: tarball-darwin-arm64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-darwin-x64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-linux-x64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-linux-arm64
path: dist/

# Create a GitHub Release with the tag as the title
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
npm/platforms/*/bin/tailflow
npm/platforms/*/bin/tailflow-daemon
dist/tailflow-darwin-arm64.tar.gz
dist/tailflow-darwin-x64.tar.gz
dist/tailflow-linux-arm64.tar.gz
dist/tailflow-linux-x64.tar.gz
npm/platforms/win32-x64/bin/tailflow.exe
npm/platforms/win32-x64/bin/tailflow-daemon.exe

# ── Step 4: update Homebrew tap ───────────────────────────────────────────
publish-homebrew:
name: Update Homebrew tap
needs: build-binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: tarball-darwin-arm64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-darwin-x64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-linux-arm64
path: dist/
- uses: actions/download-artifact@v4
with:
name: tarball-linux-x64
path: dist/

- name: Compute SHA256s and render formula
run: |
VERSION="${GITHUB_REF_NAME#v}"
SHA_DARWIN_ARM64=$(sha256sum dist/tailflow-darwin-arm64.tar.gz | cut -d' ' -f1)
SHA_DARWIN_X64=$(sha256sum dist/tailflow-darwin-x64.tar.gz | cut -d' ' -f1)
SHA_LINUX_ARM64=$(sha256sum dist/tailflow-linux-arm64.tar.gz | cut -d' ' -f1)
SHA_LINUX_X64=$(sha256sum dist/tailflow-linux-x64.tar.gz | cut -d' ' -f1)

sed \
-e "s/{{VERSION}}/$VERSION/g" \
-e "s/{{SHA256_DARWIN_ARM64}}/$SHA_DARWIN_ARM64/g" \
-e "s/{{SHA256_DARWIN_X64}}/$SHA_DARWIN_X64/g" \
-e "s/{{SHA256_LINUX_ARM64}}/$SHA_LINUX_ARM64/g" \
-e "s/{{SHA256_LINUX_X64}}/$SHA_LINUX_X64/g" \
homebrew/tailflow.rb.tmpl > tailflow.rb

- name: Push formula to homebrew-tap
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
git clone \
https://x-access-token:$HOMEBREW_TAP_TOKEN@github.com/thinkgrid-labs/homebrew-tap.git \
tap
mkdir -p tap/Formula
cp tailflow.rb tap/Formula/tailflow.rb
cd tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/tailflow.rb
git diff --cached --quiet || git commit -m "tailflow: update to ${{ github.ref_name }}"
git push
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT"
authors = ["TailFlow Contributors"]
Expand Down
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ npm installs only the binary matching your OS and CPU via platform-specific opti
### Homebrew (macOS / Linux)

```bash
brew install your-org/tap/tailflow
brew tap thinkgrid-labs/tap
brew install tailflow
```

### From source — requires Rust 1.75+
Expand Down Expand Up @@ -424,17 +425,27 @@ tailflow/

## Roadmap

- [x] Rust core engine with broadcast bus
- [x] ratatui TUI — color-coded sources, regex filter, keyboard scroll
- [x] Docker, process, file, and stdin ingestion sources
- [x] `tailflow.toml` zero-config discovery
- [x] axum SSE daemon with ring buffer
- [x] Preact web dashboard embedded in the daemon binary
- [x] npm / npx distribution — no Rust toolchain required
- [ ] Homebrew formula for macOS and Linux
- [ ] Server-side `--grep` and `--source` filter flags for the daemon
- [ ] Process restart policy for crashed `[[sources.process]]` entries
- [ ] JSON log pretty-printing — detect structured payloads and expand inline
### Near-term

- [ ] **Web dashboard search bar** — live `?grep=` filter input in the UI so users don't need to hand-craft query params
- [ ] **Log export** — download filtered records as `.ndjson` or `.txt` from the web dashboard
- [ ] **Graceful shutdown** — SIGTERM drains in-flight records and flushes the ring buffer before exit
- [ ] **`--follow` flag for files** — tail from the end by default; `--no-follow` reads the whole file and exits (like `tail -f` vs `cat`)
- [ ] **Docker Compose integration** — auto-discover services from a `docker-compose.yml` in the project root without listing them manually

### High-impact

- [ ] **Log level filter toggles in TUI** — press `e`/`w`/`i`/`d` to show/hide Error, Warn, Info, Debug levels; currently only regex filter exists
- [ ] **Persistent log buffer to disk** — optional SQLite ring buffer so logs survive daemon restarts and can be queried historically
- [ ] **`[[sources.http]]` webhook receiver** — accept POST payloads from external services (Vercel, Render, Fly.io log drains) and ingest them as a named source
- [ ] **Web dashboard dark/light theme toggle** — currently hardcoded dark; one `prefers-color-scheme` CSS variable swap would cover both
- [ ] **OpenTelemetry / OTLP exporter** — forward collected logs to a collector (Grafana Cloud, Honeycomb, Datadog) for teams who want cloud retention without changing their local workflow

### Speculative / community interest

- [ ] **TUI split-pane view** — side-by-side panes showing two sources simultaneously; useful when debugging a frontend + backend at the same time
- [ ] **Plugin system for custom sources** — WASM or subprocess-based source plugins so users can add sources (Kafka, Redis pub/sub, AWS CloudWatch) without forking
- [ ] **AI log summarisation** — `s` key in TUI calls a local LLM (Ollama) or cloud API to summarise the last N error records into a plain-English diagnosis

---

Expand Down
54 changes: 53 additions & 1 deletion crates/tailflow-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,32 @@ pub struct FileEntry {
pub label: Option<String>,
}

#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum RestartPolicy {
/// Never restart — default behaviour.
#[default]
Never,
/// Restart after every exit, zero or non-zero.
Always,
/// Restart only when the process exits with a non-zero status.
OnFailure,
}

fn default_restart_delay_ms() -> u64 {
1_000
}

#[derive(Debug, Deserialize)]
pub struct ProcessEntry {
pub cmd: String,
pub label: String,
/// Restart policy on process exit. Defaults to `never`.
#[serde(default)]
pub restart: RestartPolicy,
/// Initial restart delay in milliseconds. Doubles on each attempt, capped at 30 s.
#[serde(default = "default_restart_delay_ms")]
pub restart_delay_ms: u64,
}

impl Config {
Expand Down Expand Up @@ -84,7 +106,9 @@ impl Config {
}

for entry in self.sources.process {
sources.push(Box::new(ProcessSource::new(entry.label, entry.cmd)));
let src = ProcessSource::new(entry.label, entry.cmd)
.with_restart(entry.restart, entry.restart_delay_ms);
sources.push(Box::new(src));
}

if let Some(label) = self.sources.stdin {
Expand Down Expand Up @@ -163,6 +187,34 @@ label = "app"
assert!(cfg.sources.file[0].label.is_none());
}

#[test]
fn process_restart_defaults_to_never() {
let cfg = parse("[[sources.process]]\nlabel = \"api\"\ncmd = \"go run .\"");
assert_eq!(cfg.sources.process[0].restart, RestartPolicy::Never);
assert_eq!(cfg.sources.process[0].restart_delay_ms, 1_000);
}

#[test]
fn process_restart_on_failure_parsed() {
let cfg = parse(
r#"
[[sources.process]]
label = "api"
cmd = "go run ."
restart = "on-failure"
restart_delay_ms = 2000
"#,
);
assert_eq!(cfg.sources.process[0].restart, RestartPolicy::OnFailure);
assert_eq!(cfg.sources.process[0].restart_delay_ms, 2_000);
}

#[test]
fn process_restart_always_parsed() {
let cfg = parse("[[sources.process]]\nlabel = \"w\"\ncmd = \"x\"\nrestart = \"always\"");
assert_eq!(cfg.sources.process[0].restart, RestartPolicy::Always);
}

#[test]
fn invalid_toml_returns_error() {
let result: Result<Config, _> = toml::from_str("[[[[invalid toml");
Expand Down
Loading
Loading