Skip to content
Closed
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
17 changes: 16 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
version: 2

builds:
- main: ./cmd/tusk
- id: tusk
main: ./cmd/tusk
binary: tusk
env:
- CGO_ENABLED=0
Expand All @@ -14,6 +15,20 @@ builds:
ldflags:
- -s -w

- id: tuskd
main: ./cmd/tuskd
binary: tuskd
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s -w

archives:
- formats:
- tar.gz
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[![CI](https://github.com/Fraser-Isbester/tusk/actions/workflows/ci.yml/badge.svg)](https://github.com/Fraser-Isbester/tusk/actions/workflows/ci.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/fraser-isbester/tusk)](https://goreportcard.com/report/github.com/fraser-isbester/tusk)

# Tusk

A k9s-style terminal UI for real-time PostgreSQL monitoring and management. Not a database client — a tool for observing and acting on live database activity.
Expand Down
19 changes: 18 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,29 @@ tasks:
generates:
- ./tusk

build:daemon:
desc: Build the tuskd daemon binary
cmds:
- go build -o tuskd ./cmd/tuskd
sources:
- ./**/*.go
- go.mod
- go.sum
generates:
- ./tuskd

run:
desc: Build and run tusk against the local dev database
deps: [build]
cmds:
- ./tusk -P dev

daemon:
desc: Build and run the tuskd daemon against the local dev database
deps: [build:daemon]
cmds:
- ./tuskd -P dev

dev:
desc: Build + run (alias)
cmds:
Expand Down Expand Up @@ -75,7 +92,7 @@ tasks:
clean:
desc: Clean build artifacts
cmds:
- rm -f tusk
- rm -f tusk tuskd

test:
desc: Run all tests with Ginkgo
Expand Down
103 changes: 103 additions & 0 deletions cmd/tuskd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/spf13/cobra"

"github.com/fraser-isbester/tusk/internal/config"
"github.com/fraser-isbester/tusk/internal/db"
"github.com/fraser-isbester/tusk/internal/rules"
)

func main() {
var profileFlag string
var intervalFlag time.Duration

rootCmd := &cobra.Command{
Use: "tuskd",
Short: "Tusk daemon — continuously monitors PostgreSQL and enforces rules",
Long: "A lightweight daemon that polls PostgreSQL and evaluates rules without any TUI dependencies.",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}

profile, err := cfg.ResolveProfile(profileFlag)
if err != nil {
return fmt.Errorf("resolving profile: %w", err)
}

profileName := profileFlag
if profileName == "" {
profileName = cfg.DefaultProfile
}

if len(profile.Rules) == 0 {
return fmt.Errorf("no rules configured in profile %q", profileName)
}

connStr := profile.ConnectionString()
database, err := db.New(connStr)
if err != nil {
return fmt.Errorf("connecting to database: %w", err)
}
defer database.Close()

compiled, err := rules.BuildRules(profile.Rules, profile.Readonly)
if err != nil {
return fmt.Errorf("compiling rules: %w", err)
}

engine := rules.NewEngine(compiled, database, 5*time.Minute, 1000)

log.Printf("tuskd: profile=%s rules=%d interval=%s", profileName, engine.RuleCount(), intervalFlag)
return runDaemon(engine, database, intervalFlag)
},
}

rootCmd.Flags().StringVarP(&profileFlag, "profile", "P", "", "connection profile name from config file")
rootCmd.Flags().DurationVarP(&intervalFlag, "interval", "i", 2*time.Second, "polling interval")

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

func runDaemon(engine *rules.Engine, database *db.DB, interval time.Duration) error {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

ticker := time.NewTicker(interval)
defer ticker.Stop()

evaluate(ctx, engine, database)

for {
select {
case <-ctx.Done():
log.Println("tuskd: shutting down")
return nil
case <-ticker.C:
evaluate(ctx, engine, database)
}
}
}

func evaluate(ctx context.Context, engine *rules.Engine, database *db.DB) {
queries, _ := database.GetActiveQueries(ctx)
txns, _ := database.GetTransactions(ctx)
locks, _ := database.GetLocks(ctx)
engine.Evaluate(ctx, rules.Snapshot{
Queries: queries,
Transactions: txns,
Locks: locks,
})
}
19 changes: 10 additions & 9 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,15 +375,16 @@ func (a *App) updateHeader() {
y := "[#FFD700]"
r := "[#FF5F5F]"

// TUSK in big open letters (figlet "big" style)
o := "[#D78700]"
logo := []string{
o + tview.Escape(" _______ _ _ _____ _ __") + "[-]",
o + tview.Escape(" |__ __|| | | | / ____|| |/ /") + "[-]",
o + tview.Escape(" | | | | | || (___ | ' / ") + "[-]",
o + tview.Escape(" | | | | | | \\___ \\ | < ") + "[-]",
o + tview.Escape(" | | | |__| | ____) || . \\ ") + "[-]",
o + tview.Escape(" |_| \\____/ |_____/ |_|\\_\\") + "[-]",
"",
"",
"",
c + ` _______ _ _ _______ _ _` + "[-]",
c + ` | | | |______ |____/ ` + "[-]",
c + ` | |_____| ______| | \_` + "[-]",
"",
"",
"",
}

uptimeStr := "--"
Expand Down Expand Up @@ -436,7 +437,7 @@ func (a *App) updateHeader() {
if headerWidth <= 0 {
headerWidth = 120
}
logoWidth := 33 // visual width of the big letters
logoWidth := 34 // visual width of cyberlarge logo

var lines []string
for i := 0; i < len(infoLines) || i < len(logo); i++ {
Expand Down