Skip to content

Commit

Permalink
Enhance ParseDuration with configurable parsing options
Browse files Browse the repository at this point in the history
Introduce ParseDuration() as the primary and simplest entrypoint for
parsing durations. By default it operates in "single-unit mode",
meaning only one unit type is allowed per input string (e.g., "1d" as
opposed to permitting "1d3h5m"). Assumes milliseconds as the default
unit when none is specified.
  • Loading branch information
frobware committed Aug 20, 2023
1 parent 298a36d commit f7141d0
Show file tree
Hide file tree
Showing 5 changed files with 945 additions and 285 deletions.
34 changes: 0 additions & 34 deletions cmd/haproxy-timeout-checker/haproxy-timeout-checker.go

This file was deleted.

197 changes: 197 additions & 0 deletions cmd/haproxytimeout/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package main

import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strconv"
"time"

"github.com/frobware/haproxytime"
)

func isInputInMilliseconds(input string) bool {
_, err := strconv.Atoi(input) // Check if the input is just a number (e.g., milliseconds)
return err == nil
}

func printErrorWithPosition(input string, err error, position int) {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, input)
fmt.Fprintf(os.Stderr, "%"+fmt.Sprint(position)+"s", "")
fmt.Fprintln(os.Stderr, "^")
}

func formatDuration(d time.Duration) string {
const (
Day = time.Hour * 24
Month = Day * 30
Year = Day * 365
Microsecond = time.Microsecond
)

years := d / Year
d -= years * Year

months := d / Month
d -= months * Month

days := d / Day
d -= days * Day

hours := d / time.Hour
d -= hours * time.Hour

minutes := d / time.Minute
d -= minutes * time.Minute

seconds := d / time.Second
d -= seconds * time.Second

milliseconds := d / time.Millisecond
d -= milliseconds * time.Millisecond

microseconds := d / Microsecond

var result string
if years > 0 {
result += fmt.Sprintf("%dy", years)
}
if months > 0 {
result += fmt.Sprintf("%dmo", months)
}
if days > 0 {
result += fmt.Sprintf("%dd", days)
}
if hours > 0 {
result += fmt.Sprintf("%dh", hours)
}
if minutes > 0 {
result += fmt.Sprintf("%dm", minutes)
}
if seconds > 0 {
result += fmt.Sprintf("%ds", seconds)
}
if milliseconds > 0 {
result += fmt.Sprintf("%dms", milliseconds)
}
if microseconds > 0 {
result += fmt.Sprintf("%dus", microseconds)
}

return result
}

func main() {
humanReadable := flag.Bool("h", false, "Print the duration in a human-readable format.")
max := flag.Bool("m", false, "Print the maximum duration HAProxy can tolerate and exit.")
jsonOutput := flag.Bool("j", false, "Print results in JSON format.")
flag.Parse()

args := flag.Args()

if len(args) == 0 && !*max {
fmt.Println("usage: haproxytimeout [-h] [-m] [-j] <duration>")
fmt.Println(" -h: Decode duration into human-readable format. Assumes input is in milliseconds unless units are provided.")
fmt.Println(" -m: Print the maximum duration HAProxy can tolerate and exit.")
fmt.Println(" -j: Print results in JSON format. (e.g., to extract the milliseconds value: haproxytimeout -j <duration> | jq .milliseconds)")

fmt.Println("\nInput Assumptions:")
fmt.Println(" If the input is a plain number (or a decimal without a unit), it's assumed to be in milliseconds.")
fmt.Println(" For example: 'haproxytimeout 1500' is assumed to be '1500ms'.")

fmt.Println("\nOutput Format:")
fmt.Println(" The output typically maps the human-readable format to the milliseconds format.")
fmt.Println(" For example: 'haproxytimeout 2h30m' will output '2h30m -> 9000000ms'.")
fmt.Println(" The right-hand side is always in milliseconds, making it suitable for direct use in a haproxy.cfg file.")

fmt.Println("\nAvailable units:")
fmt.Println(" d: days")
fmt.Println(" h: hours")
fmt.Println(" m: minutes")
fmt.Println(" s: seconds")
fmt.Println(" ms: milliseconds")
fmt.Println(" µs: microseconds")

fmt.Println("\nExample usage:")
fmt.Println(" haproxytimeout 2h30m -> Convert 2 hours 30 minutes to milliseconds.")
fmt.Println(" haproxytimeout 1500ms -> Convert 1500 milliseconds to a human-readable format.")
fmt.Println(" haproxytimeout -j 2h -> Get JSON output for 2 hours.")
fmt.Println(" haproxytimeout -m -> Show the maximum duration HAProxy can tolerate.")

os.Exit(1)
}

type outputResult struct {
HumanReadable string `json:"human_readable"`
Milliseconds int64 `json:"milliseconds"`
}

// Output function based on jsonOutput flag.
output := func(human string, millis int64) {
if human == "" {
human = formatDuration(time.Duration(millis) * time.Millisecond)
}

if *jsonOutput {
res := outputResult{
HumanReadable: human,
Milliseconds: millis,
}
jsonRes, err := json.Marshal(res)
if err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(6)
}
fmt.Println(string(jsonRes))
} else {
fmt.Printf("%s -> %dms\n", human, millis)
}
}

// Handle max duration request.
if *max {
maxDuration := haproxytime.MaxTimeout.Milliseconds()
// Always format max duration as human-readable.
output(formatDuration(haproxytime.MaxTimeout), maxDuration)
return
}

parserCfg, err := haproxytime.NewParserConfig(haproxytime.UnitMillisecond, haproxytime.MultiUnitMode)
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to create parser configuration: %v\n", err)
os.Exit(2)
}

duration, err := haproxytime.ParseDurationWithConfig(args[0], parserCfg)
if err != nil {
var se *haproxytime.SyntaxError
var oe *haproxytime.OverflowError
switch {
case errors.As(err, &se):
printErrorWithPosition(args[0], err, se.Position)
os.Exit(3)
case errors.As(err, &oe):
printErrorWithPosition(args[0], err, oe.Position)
os.Exit(4)
default:
panic(err)
}
}

if haproxytime.DurationExceedsMaxTimeout(duration) {
maxHumanFormat := formatDuration(haproxytime.MaxTimeout)
humanFormat := formatDuration(time.Duration(duration.Milliseconds()) * time.Millisecond)
fmt.Fprintf(os.Stderr, "%s exceeds HAProxy's maximum duration %s (%vms)\n",
humanFormat, maxHumanFormat, haproxytime.MaxTimeout.Milliseconds())
os.Exit(5)
}

if *humanReadable || !isInputInMilliseconds(args[0]) {
output(formatDuration(duration), duration.Milliseconds())
} else {
output("", duration.Milliseconds()) // no human-readable output if not requested
}
}
Loading

0 comments on commit f7141d0

Please sign in to comment.