Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgieinsocks committed Aug 27, 2024
0 parents commit 13bd288
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: release

on:
push:
tags:
- "*"

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v5
with:
go-version: "1.22.6"
- run: |
go get .
- run: |
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o htp_linux_amd64
- run: |
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o htp_darwin_amd64
- run: |
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o htp_darwin_arm64
- uses: ncipollo/release-action@v1
with:
artifacts: "htp*"
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Artem Babii

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# htp

HTTP Tick Ping (htp) - a tool to send HTTP probe requests at regular intervals

The requests are sent at the exact scheduled time depending on the set interval, even if the previous requests have not completed yet. This might help determine how long the service exposed on the target URL stays unavailable from the user's perspective after e.g. k8s pod or web server restart.

## Usage

```
A tool to send HTTP probe requests at regular intervals
Usage:
htp URL [flags]
Flags:
-i, --interval int interval between requests in milliseconds (default 1000)
-l, --limit int number of requests to make (default unlimited)
-t, --tail int number of requests to tail (default 25)
-g, --get use GET method (default HEAD)
-k, --insecure allow insecure connections
-h, --help help for htp
-v, --version version for htp
```

## Example

```sh
❯ htp https://google.com -l 10
HEAD http://google.com every 1000ms

1: start=09:35:55.830, duration=262ms, end=09:35:56.092 [200] http://www.google.com/
2: start=09:35:56.831, duration=105ms, end=09:35:56.936 [200] http://www.google.com/
3: start=09:35:57.830, duration=103ms, end=09:35:57.934 [200] http://www.google.com/
4: start=09:35:58.831, duration=104ms, end=09:35:58.935 [200] http://www.google.com/
5: start=09:35:59.831, duration=106ms, end=09:35:59.937 [200] http://www.google.com/
6: start=09:36:00.831, duration=103ms, end=09:36:00.933 [200] http://www.google.com/
7: start=09:36:01.831, duration=105ms, end=09:36:01.936 [200] http://www.google.com/
8: start=09:36:02.830, duration=103ms, end=09:36:02.934 [200] http://www.google.com/
9: start=09:36:03.830, duration=103ms, end=09:36:03.933 [200] http://www.google.com/
10: start=09:36:04.831, duration=127ms, end=09:36:04.958 [200] http://www.google.com/
```
233 changes: 233 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package cmd

import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/spf13/cobra"
)

type options struct {
intervalMs int
requestLimit int
tailLines int
useGet bool
allowInsecure bool
}

type probe struct {
id int
start time.Time
duration time.Duration
end time.Time
status int
url string
err error
}

type model struct {
probes []probe
exit bool
}

type probeMsg probe

var opts options

var rootCmd = &cobra.Command{
Use: "htp URL",
Long: "A tool to send HTTP probe requests at regular intervals",
Version: "v0.0.1",
Args: cobra.ExactArgs(1),
Run: main,
}

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

func init() {
log.SetFlags(0)
log.SetPrefix("Error: ")
rootCmd.Flags().IntVarP(&opts.intervalMs, "interval", "i", 1000, "interval between requests in milliseconds")
rootCmd.Flags().IntVarP(&opts.requestLimit, "limit", "l", 0, "number of requests to make (default unlimited)")
rootCmd.Flags().IntVarP(&opts.tailLines, "tail", "t", 25, "number of requests to tail")
rootCmd.Flags().BoolVarP(&opts.useGet, "get", "g", false, "use GET method (default HEAD)")
rootCmd.Flags().BoolVarP(&opts.allowInsecure, "insecure", "k", false, "allow insecure connections")
rootCmd.Flags().SortFlags = false
}

func colorStatusCode(code int) string {
stringCode := strconv.Itoa(code)
switch {
case strings.HasPrefix(stringCode, "2"):
return color.GreenString(stringCode)
case strings.HasPrefix(stringCode, "4"):
return color.YellowString(stringCode)
case strings.HasPrefix(stringCode, "5"):
return color.RedString(stringCode)
default:
return stringCode
}
}

func setHttpMethod() string {
if opts.useGet {
return "GET"
} else {
return "HEAD"
}
}

func renderOutput(m model, offset int) string {
var output string
for _, probe := range m.probes[offset:] {
switch {
case probe.err != nil:
output += fmt.Sprintf("%d: start=%s, duration=%s, end=%s [%s] %v\n",
probe.id,
probe.start.Format("15:04:05.000"),
probe.duration.Round(time.Millisecond),
probe.end.Format("15:04:05.000"),
color.RedString("ERROR"),
probe.err,
)
case probe.status == 0:
output += fmt.Sprintf("%d:\n", probe.id)
default:
output += fmt.Sprintf("%d: start=%s, duration=%s, end=%s [%s] %s\n",
probe.id,
probe.start.Format("15:04:05.000"),
probe.duration.Round(time.Millisecond),
probe.end.Format("15:04:05.000"),
colorStatusCode(probe.status),
color.BlackString(probe.url),
)
}
}
return output
}

func probeUrl(c *http.Client, id int, target *url.URL, method string) tea.Msg {
req, err := http.NewRequest(method, target.String(), http.NoBody)
if err != nil {
log.Fatalf("%v\n", err)
}
start := time.Now()
resp, err := c.Do(req)
duration := time.Since(start)
end := start.Add(duration)
if err != nil {
return probeMsg{
id: id,
start: start,
duration: duration,
end: end,
err: err,
}
}
defer resp.Body.Close()
return probeMsg{
id: id,
start: start,
duration: duration,
end: end,
status: resp.StatusCode,
url: resp.Request.URL.String(),
}
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
m.exit = true
return m, tea.Quit
}
case probeMsg:
if i := slices.IndexFunc(m.probes, func(r probe) bool { return r.id == msg.id }); i >= 0 {
m.probes[i] = probe(msg)
} else {
m.probes = append(m.probes, probe(msg))
}
}
return m, nil
}

func (m model) View() string {
if m.exit {
return ""
}
offset := len(m.probes) - opts.tailLines
if offset < 0 {
offset = 0
}
return renderOutput(m, offset)
}

func main(cmd *cobra.Command, args []string) {
target, err := url.ParseRequestURI(args[0])
if err != nil {
log.Fatalf("%v\n", err)
}
method := setHttpMethod()

tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: opts.allowInsecure},
}
c := &http.Client{Transport: tr}
p := tea.NewProgram(model{})
t := time.NewTicker(time.Duration(opts.intervalMs) * time.Millisecond)

fmt.Printf("%s %s every %dms\n\n", method, target, opts.intervalMs)

go func() {
var wg sync.WaitGroup
defer t.Stop()
id := 1
for {
if opts.requestLimit != 0 && id > opts.requestLimit {
break
}
p.Send(probeMsg{id: id})
wg.Add(1)
go func(id int) {
defer wg.Done()
p.Send(probeUrl(c, id, target, method))
}(id)
id++
<-t.C
}
wg.Wait()
p.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}})
}()

result, err := p.Run()
if err != nil {
log.Fatalf("%v\n", err)
}

resultModel, ok := result.(model)
if !ok {
log.Fatalf("%v\n", err)
}

fmt.Printf(renderOutput(resultModel, 0))
}
34 changes: 34 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module htp

go 1.22.6

require (
github.com/charmbracelet/bubbletea v0.27.1
github.com/fatih/color v1.17.0
github.com/spf13/cobra v1.8.1
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/charmbracelet/x/input v0.1.0 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
Loading

0 comments on commit 13bd288

Please sign in to comment.