Skip to content

Commit

Permalink
Add output to a file in catp
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed Jul 13, 2023
1 parent ba037d2 commit f3ac3b4
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 205 deletions.
Binary file modified cmd/catp.pgo
Binary file not shown.
51 changes: 50 additions & 1 deletion cmd/catp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,64 @@
go install github.com/bool64/progress/cmd/catp@latest
```

or download from [releases](https://github.com/bool64/progress/releases).

```
wget https://github.com/bool64/progress/releases/latest/download/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz
./catp -version
```

## Usage

```
Usage of catp:
-dbg-cpu-prof string
write first 10 seconds of CPU profile to file
-grep string
grep pattern, may contain multiple patterns separated by \|
-output string
output to file instead of STDOUT
-version
print version and exit
```

## Examples

Feed a file into `jq` field extractor.

```
catp get-key.log | jq .context.callback.Data.Nonce > get-key.jq
```

```
get-key.log: 4.0% bytes read, 39856 lines processed, 7968.7 l/s, 41.7 MB/s, elapsed 5s, remaining 1m59s
get-key.log: 8.1% bytes read, 79832 lines processed, 7979.9 l/s, 41.8 MB/s, elapsed 10s, remaining 1m54s
.....
get-key.log: 96.8% bytes read, 967819 lines processed, 8064.9 l/s, 41.8 MB/s, elapsed 2m0s, remaining 3s
get-key.log: 100.0% bytes read, 1000000 lines processed, 8065.7 l/s, 41.8 MB/s, elapsed 2m3.98s, remaining 0s
```
```

Run log filtering (lines containing `foo bar` or `baz`) on multiple files in background (with `screen`) and output to a
new file.

```
screen -dmS foo12 ./catp -output ~/foo-2023-07-12.log -grep "foo bar\|baz" /home/logs/server-2023-07-12*
```

```
# attaching to screen
screen -r foo12
....
all: 32.0% bytes read, /home/logs/server-2023-07-12-08-00.log_6.zst: 96.1% bytes read, 991578374 lines processed, 447112.5 l/s, 103.0 MB/s, elapsed 36m57.74s, remaining 1h18m36s, matches 6343
all: 32.0% bytes read, /home/logs/server-2023-07-12-08-00.log_6.zst: 97.9% bytes read, 993652260 lines processed, 447039.6 l/s, 102.9 MB/s, elapsed 37m2.74s, remaining 1h18m32s, matches 6357
all: 32.1% bytes read, /home/logs/server-2023-07-12-08-00.log_6.zst: 99.6% bytes read, 995708943 lines processed, 446959.5 l/s, 102.9 MB/s, elapsed 37m7.74s, remaining 1h18m29s, matches 6372
all: 32.1% bytes read, /home/logs/server-2023-07-12-08-00.log_6.zst: 100.0% bytes read, 996191417 lines processed, 446945.2 l/s, 102.9 MB/s, elapsed 37m8.89s, remaining 1h18m28s, matches 6376
all: 32.2% bytes read, /home/logs/server-2023-07-12-09-00.log_6.zst: 1.7% bytes read, 998243047 lines processed, 446595.4 l/s, 102.8 MB/s, elapsed 37m15.23s, remaining 1h18m27s, matches 6402
all: 32.3% bytes read, /home/logs/server-2023-07-12-09-00.log_6.zst: 3.5% bytes read, 1000516319 lines processed, 446613.3 l/s, 102.8 MB/s, elapsed 37m20.23s, remaining 1h18m22s, matches 6425
all: 32.3% bytes read, /home/logs/server-2023-07-12-09-00.log_6.zst: 5.1% bytes read, 1002520923 lines processed, 446510.7 l/s, 102.8 MB/s, elapsed 37m25.23s, remaining 1h18m18s, matches 6450
....
# detaching from screen with ctrl+a+d
```

237 changes: 237 additions & 0 deletions cmd/catp/app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Package app provides catp CLI tool as importable package.
package app

import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"log"
"os"
"runtime/pprof"
"strings"
"sync/atomic"
"time"

"github.com/bool64/dev/version"
"github.com/bool64/progress"
"github.com/klauspost/compress/zstd"
gzip "github.com/klauspost/pgzip"
)

type runner struct {
output io.Writer
pr *progress.Progress
sizes map[string]int64
readBytes int64
readLines int64
matches int64
totalBytes int64

grep [][]byte

currentFile *progress.CountingReader
currentTotal int64
lastErr error
}

// st renders Status as a string.
func (r *runner) st(s progress.Status) string {
var res string

if len(r.sizes) > 1 {
fileDonePercent := 100 * float64(r.currentFile.Bytes()) / float64(r.currentTotal)
res = fmt.Sprintf("all: %.1f%% bytes read, %s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s",
s.DonePercent, s.Task, fileDonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS,
s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String())
} else {
res = fmt.Sprintf("%s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s",
s.Task, s.DonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS,
s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String())
}

if r.grep != nil {
res += fmt.Sprintf(", matches %d", atomic.LoadInt64(&r.matches))
}

return res
}

func (r *runner) readFile(rd io.Reader) {
b := bufio.NewReaderSize(rd, 64*1024)

_, err := io.Copy(r.output, b)
if err != nil {
log.Fatal(err)
}
}

func (r *runner) scanFile(rd io.Reader) {
s := bufio.NewScanner(rd)
s.Buffer(make([]byte, 64*1024), 10*1024*1024)

for s.Scan() {
for _, g := range r.grep {
if bytes.Contains(s.Bytes(), g) {
if _, err := r.output.Write(append(s.Bytes(), '\n')); err != nil {
r.lastErr = err

return
}

atomic.AddInt64(&r.matches, 1)

break
}
}
}

if err := s.Err(); err != nil {
r.lastErr = err
}
}

func (r *runner) cat(filename string) (err error) {
file, err := os.Open(filename) //nolint:gosec
if err != nil {
return err
}

defer func() {
if clErr := file.Close(); clErr != nil && err == nil {
err = clErr
}
}()

r.currentFile = &progress.CountingReader{Reader: file}
r.currentTotal = r.sizes[filename]
rd := io.Reader(r.currentFile)

switch {
case strings.HasSuffix(filename, ".gz"):
if rd, err = gzip.NewReader(rd); err != nil {
return fmt.Errorf("failed to init gzip reader: %w", err)
}
case strings.HasSuffix(filename, ".zst"):
if rd, err = zstd.NewReader(rd); err != nil {
return fmt.Errorf("failed to init zst reader: %w", err)
}
}

r.pr.Start(func(t *progress.Task) {
t.TotalBytes = func() int64 {
return r.totalBytes
}
t.CurrentBytes = func() int64 {
return r.readBytes + r.currentFile.Bytes()
}
t.CurrentLines = func() int64 {
return r.readLines + r.currentFile.Lines()
}
t.Task = filename
t.Continue = true
})

if len(r.grep) > 0 {
r.scanFile(rd)
} else {
r.readFile(rd)
}

r.pr.Stop()
r.readBytes += r.currentFile.Bytes()
r.readLines += r.currentFile.Lines()

return r.lastErr
}

func startProfiling(cpuProfile string) {
f, err := os.Create(cpuProfile) //nolint:gosec
if err != nil {
log.Fatal(err)
}

if err = pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}

go func() {
time.Sleep(10 * time.Second)
pprof.StopCPUProfile()
println("CPU profile written to", cpuProfile)
}()
}

// Main is the entry point for catp CLI tool.
func Main() error { //nolint:funlen,cyclop
grep := flag.String("grep", "", "grep pattern, may contain multiple patterns separated by \\|")
cpuProfile := flag.String("dbg-cpu-prof", "", "write first 10 seconds of CPU profile to file")
output := flag.String("output", "", "output to file instead of STDOUT")
ver := flag.Bool("version", false, "print version and exit")

flag.Parse()

if *ver {
fmt.Println(version.Module("github.com/bool64/progress").Version)

return nil
}

if *cpuProfile != "" {
startProfiling(*cpuProfile)
}

r := &runner{}

r.output = os.Stdout

if *output != "" {
out, err := os.Create(*output)
if err != nil {
return fmt.Errorf("failed to create output file %s: %w", *output, err)
}

r.output = out

defer func() {
if err := out.Close(); err != nil {
log.Fatalf("failed to close output file %s: %s", *output, err)
}
}()
}

if *grep != "" {
for _, s := range strings.Split(*grep, "\\|") {
r.grep = append(r.grep, []byte(s))
}
}

r.sizes = make(map[string]int64)
r.pr = &progress.Progress{
Interval: 5 * time.Second,
Print: func(status progress.Status) {
println(r.st(status))
},
}

for i := 0; i < flag.NArg(); i++ {
fn := flag.Arg(i)

st, err := os.Stat(fn)
if err != nil {
return fmt.Errorf("failed to read file stats %s: %w", fn, err)
}

r.totalBytes += st.Size()
r.sizes[fn] = st.Size()
}

for i := 0; i < flag.NArg(); i++ {
if err := r.cat(flag.Arg(i)); err != nil {
return err
}
}

return nil
}
Loading

0 comments on commit f3ac3b4

Please sign in to comment.