Skip to content

Commit

Permalink
Expand usage, add hints, improve error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
lelandbatey committed Apr 25, 2022
1 parent 7894170 commit cbe7cb2
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 13 deletions.
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/lelandbatey/histogram_timestamps
go 1.16

require (
github.com/itchyny/timefmt-go v0.1.3 // indirect
github.com/kevinburke/go-bindata v3.22.0+incompatible // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/itchyny/timefmt-go v0.1.3
github.com/mattn/go-isatty v0.0.14
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
)
13 changes: 11 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
github.com/kevinburke/go-bindata v3.22.0+incompatible h1:/JmqEhIWQ7GRScV0WjX/0tqBrC5D21ALg0H0U/KZ/ts=
github.com/kevinburke/go-bindata v3.22.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
85 changes: 78 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var (
generateData = pflag.BoolP("generate-fake-data", "g", false, "If provided, all the program will do is generate a bunch of fake timestamps and print them on stdout. Useful as a way to feed known input to another histogram_timestamps")
unit = pflag.StringP("unit", "u", "auto", "The duration of each 'bin' to group timestamps into: https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases")
strptimefmt = pflag.StringP("strptime-fmt", "f", "", "A strptime-compatible date format specifier. Use if your data isn't formatted as integer milliseconds since epoch.")
helpFlag = pflag.BoolP("help", "h", false, "Print usage")
helpFlag = pflag.BoolP("help", "h", false, "Print usage and exit")
)

func smax(v interface{}, l int) string {
Expand All @@ -40,26 +40,57 @@ func smax(v interface{}, l int) string {
return s[:l]
}

func PrintUsage() {
// This copy-paste of the code from pflag.Usage() is done so we can
// wrap our usage messages automatically.
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
usages := pflag.CommandLine.FlagUsagesWrapped(90)
fmt.Fprintf(os.Stderr, usages)
fmt.Fprintf(os.Stderr, `
Examples:
# Generate a graph with some fake data
$ %s --generate-fake-data | %s
# Graph the data in 1-minute wide bins
$ %s --generate-fake-data | %s --unit 1minute
# Parse timestamps in a custom format
$ cat /tmp/file_with_timestamps | %s --strptime-fmt "%%Y-%%m-%%dT%%H:%%M:%%S.%%f"
`, os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0])
}

func main() {
pflag.Parse()
if *helpFlag {
pflag.Usage()
PrintUsage()
os.Exit(0)
}
if *generateData {
tss, err := tbin.SimpleRandomTimestamps(10000, 12)
tss, err := tbin.SimpleRandomTimestamps(10000, 36)
if err != nil {
fmt.Printf("cannot generate random timestamps: %q", err.Error())
os.Exit(2)
}
for _, ts := range tss {
fmt.Printf("%d\n", ts)
if *strptimefmt != "" {
fmt.Printf("%s\n", timefmt.Format(time.UnixMilli(ts).UTC(), *strptimefmt))
} else {
fmt.Printf("%d\n", ts)
}
}
os.Exit(0)
}
if isatty.IsTerminal(os.Stdin.Fd()) {
fmt.Printf("You must pipe the timestamps into this program on stdin; since stdin is a terminal, exiting.\n\n")
pflag.Usage()
fmt.Printf("You must pipe the timestamps into this program\non stdin; since stdin is a terminal, exiting.\n")
fmt.Printf(`
HINT: to see an example interactive graph, run the following command
%s --generate-fake-data | %s
`, os.Args[0], os.Args[0])
PrintUsage()
os.Exit(1)
}
// 1. read stdin for lines of text
Expand Down Expand Up @@ -150,6 +181,14 @@ func main() {
}
}

// read_lines_to_integers attempts to parse each non-empty lines in r as a time
// formatted according to `format`. The `format` parameter is a strptime (see
// manpage for strptime(3)) compatible string. Each integer in the output
// represents a count of milliseconds since UNIX epoch.
//
// If `format` is an empty string, then each line of `r` is assumed to be a
// string representing a moment in time as a count of milliseconds since UNIX
// epoch.
func read_lines_to_integers(r io.Reader, format string) ([]int64, error) {
tss := []int64{}
scnr := bufio.NewScanner(r)
Expand All @@ -166,14 +205,16 @@ func read_lines_to_integers(r io.Reader, format string) ([]int64, error) {
if format == "" {
ts, err = strconv.ParseInt(line, 10, 64)
if err != nil {
fmt.Fprint(os.Stderr, GuessTimestampFormat(line))
return nil, fmt.Errorf("cannot parse integer on line %d of stdin: %w\n", i, err)
}
} else {
t, err := timefmt.Parse(line, format)
if err != nil {
fmt.Fprint(os.Stderr, GuessTimestampFormat(line))
return nil, fmt.Errorf("cannot parse line %d of stdin to date: %w", i, err)
}
ts = t.UnixNano() / 1000000
ts = t.UnixMilli()
}
tss = append(tss, ts)
}
Expand All @@ -198,3 +239,33 @@ func openbrowser(url string) {
}

}

func GuessTimestampFormat(line string) string {
hinttempl := `
HINT: It looks like the timestamp '%s' is in format '%s'. To parse
all the incoming timestamps as format '%s', provide the following option:
--strptime-fmt '%s'
`
type tsfmt struct {
Name string
Fmt string
}
potenials := []tsfmt{
{"RFC3339", "%Y-%m-%dT%H:%M:%S.%f%z"},
{"RFC3339", "%Y-%m-%dT%H:%M:%S%z"},
{"YYYY-mm-dd HH:MM:SS.ms", "%Y-%m-%d %H:%M:%S.%f"},
{"YYYY-mm-dd HH:MM:SS.ms TZ", "%Y-%m-%d %H:%M:%S.%f %z"},
{"YYYY-mm-dd HH:MM:SS", "%Y-%m-%d %H:%M:%S"},
{"YYYY-mm-dd HH:MM:SS TZ", "%Y-%m-%d %H:%M:%S %z"},
{"YYYY-mm-dd", "%Y-%m-%d"},
}
for _, candidate := range potenials {
_, err := timefmt.Parse(line, candidate.Fmt)
if err == nil {
return fmt.Sprintf(hinttempl, line, candidate.Name, candidate.Name, candidate.Fmt)
}
}
return "HINT: Use the '--strptime-format' flag to indicate the format of the incoming timestamps\n\n"
}
6 changes: 6 additions & 0 deletions tbin/tbin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ func TestParseSpec(t *testing.T) {
ExpDelt: TD_1_min,
ExpErr: nil,
},
{
Spec: "1h",
ExpMult: 1,
ExpDelt: TD_1_hr,
ExpErr: nil,
},
{
Spec: "1",
ExpMult: 0,
Expand Down

0 comments on commit cbe7cb2

Please sign in to comment.