diff --git a/README.md b/README.md index 4889c47..d4ddfe5 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ Cryptarch has **"modes"** that determine what type of query should be provided. **Query mode** is the default and is for running shell commands. -![Demo of query mode](https://raw.githubusercontent.com/spacez320/cryptarch/master/media/query-mode.gif) +![Demo of query mode](https://raw.githubusercontent.com/spacez320/cryptarch/master/assets/query-mode.gif) **Profile mode** is like Query mode except specialized for inspecting systems or processes. -![Demo of profile mode](https://raw.githubusercontent.com/spacez320/cryptarch/master/media/profile-mode.gif) +![Demo of profile mode](https://raw.githubusercontent.com/spacez320/cryptarch/master/assets/profile-mode.gif) ### Displays @@ -42,11 +42,11 @@ Cryptarch's interactive window. The examples above use stream displays. **Table display** will parse results into a table. -![Demo of table display](https://raw.githubusercontent.com/spacez320/cryptarch/master/media/table-display.gif) +![Demo of table display](https://raw.githubusercontent.com/spacez320/cryptarch/master/assets/table-display.gif) **Graph display** will target a specific field in a result and graph it. -![Demo of graph display](https://raw.githubusercontent.com/spacez320/cryptarch/master/media/graph-display.gif) +![Demo of graph display](https://raw.githubusercontent.com/spacez320/cryptarch/master/assets/graph-display.gif) ### Persistence diff --git a/media/graph-display.gif b/assets/graph-display.gif similarity index 100% rename from media/graph-display.gif rename to assets/graph-display.gif diff --git a/media/profile-mode.gif b/assets/profile-mode.gif similarity index 100% rename from media/profile-mode.gif rename to assets/profile-mode.gif diff --git a/media/query-mode.gif b/assets/query-mode.gif similarity index 100% rename from media/query-mode.gif rename to assets/query-mode.gif diff --git a/media/table-display.gif b/assets/table-display.gif similarity index 100% rename from media/table-display.gif rename to assets/table-display.gif diff --git a/go.mod b/go.mod index 9630400..421d4de 100644 --- a/go.mod +++ b/go.mod @@ -9,17 +9,24 @@ require internal/lib v0.0.0 require pkg/storage v0.0.0 // indirect require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mum4k/termdash v0.18.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rivo/tview v0.0.0-20231206124440-5f078138442e // indirect github.com/rivo/uniseg v0.4.3 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/term v0.9.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) replace internal/lib => ./internal/lib diff --git a/go.sum b/go.sum index 221c1b7..2361f0d 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,29 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 h1:SeDV6ZUSVlTAUUPdMzPXgMyj96z+whQJRRUff8dIeic= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73/go.mod h1:pwzJMyH4Hd0AZMJkWQ+/g01dDvYWEvmJuaiRU71Xl8k= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mum4k/termdash v0.18.0 h1:wpy3FKcVV5s2TOoMTKzqQXwL5VClZIlNrRqZDpeIzBA= github.com/mum4k/termdash v0.18.0/go.mod h1:VWL18wLZDKVKF/f4TkMRiKZb9Eg8Ax99PtNuGuRAguw= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e h1:mPy47VW9tkqImnSPgcjnEHJuG3XHDBtXj2hDb1qBrRs= @@ -39,8 +53,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -50,10 +64,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/lib/client.go b/internal/lib/client.go index c52820e..1aa5eb2 100644 --- a/internal/lib/client.go +++ b/internal/lib/client.go @@ -27,7 +27,6 @@ func initClient(port string) { err = client.Call("Results.GetAllRPC", storage.ArgsRPC{}, &reply) e(err) - // TODO For now, just print results until we define actions that - // may be done upon them. + // TODO For now, just print results until we define actions that may be done upon them. fmt.Printf("Got: %v\n", reply.Results) } diff --git a/internal/lib/config.go b/internal/lib/config.go index 3c7c4e2..2c10c13 100644 --- a/internal/lib/config.go +++ b/internal/lib/config.go @@ -16,7 +16,8 @@ var ( // Shareable configuration. type Config struct { - LogLevel string + LogLevel string // Log level. + PushgatewayAddr string // Pushgateway address. } // Retrieves an Slog level from a human-readable level string. diff --git a/internal/lib/display.go b/internal/lib/display.go index c558d0b..e32c6ab 100644 --- a/internal/lib/display.go +++ b/internal/lib/display.go @@ -14,12 +14,6 @@ import ( "golang.org/x/exp/slog" ) -// Represents the display driver. -type DisplayDriver int - -// Represents the display mode. -type DisplayMode int - // Display driver constants. Each display mode uses a specific display driver. const ( DISPLAY_RAW DisplayDriver = iota + 1 // Used for direct output. @@ -57,6 +51,12 @@ var ( interruptChan = make(chan bool) // Channel for interrupting displays. ) +// Represents the display driver. +type DisplayDriver int + +// Represents the display mode. +type DisplayMode int + // Starts the display. Applies contextual logic depending on the provided display driver. Expects a // function to execute within a goroutine to update the display. func display(driver DisplayDriver, displayUpdateFunc func()) { diff --git a/internal/lib/display_termdash.go b/internal/lib/display_termdash.go index 4fd5556..b431337 100644 --- a/internal/lib/display_termdash.go +++ b/internal/lib/display_termdash.go @@ -70,15 +70,14 @@ func keyboardTermdashHandler(key *terminalapi.Keyboard) { // Error management for termdash. func errorTermdashHandler(e error) { - // If we hit an error from termdash, just log it and try to continue. Cases - // of errors seen so far make sense to ignore: + // If we hit an error from termdash, just log it and try to continue. Cases of errors seen so far + // make sense to ignore: // // - Unimplemented key-strokes. slog.Error(e.Error()) } -// Sets-up the termdash container, which defines the overall layout, and begins -// running the display. +// Sets-up the termdash container, which defines the overall layout, and begins running the display. func initDisplayTermdash(resultsWidget, helpWidget, logsWidget widgetapi.Widget) { var ( ctx context.Context // Termdash specific context. diff --git a/internal/lib/display_tview.go b/internal/lib/display_tview.go index 8ae2cab..d2399ac 100644 --- a/internal/lib/display_tview.go +++ b/internal/lib/display_tview.go @@ -96,14 +96,12 @@ func initDisplayTviewText(helpText string) (resultsView, helpView, logsView *tvi return } -// Sets-up the tview flex box, which defines the overall layout. Meant to -// encapsulate the common things needed regardless of what from the results -// view takes (assuming it fits into flex box). +// Sets-up the tview flex box, which defines the overall layout. Meant to encapsulate the common +// things needed regardless of what from the results view takes (assuming it fits into flex box). // -// Note that the app needs to be run separately from initialization in the -// coroutine display function. Note also that direct manipulation of the tview -// Primitives as subclasses (like tview.Box) needs to happen outside this -// function, as well. +// Note that the app needs to be run separately from initialization in the coroutine display +// function. Note also that direct manipulation of the tview Primitives as subclasses (like +// tview.Box) needs to happen outside this function, as well. func initDisplayTview( resultsView tview.Primitive, helpView, logsView *tview.TextView, diff --git a/internal/lib/go.mod b/internal/lib/go.mod index 760f006..3b5cd50 100644 --- a/internal/lib/go.mod +++ b/internal/lib/go.mod @@ -15,13 +15,20 @@ require ( ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/term v0.9.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) replace pkg/storage => ../../pkg/storage diff --git a/internal/lib/go.sum b/internal/lib/go.sum index 93e6023..2361f0d 100644 --- a/internal/lib/go.sum +++ b/internal/lib/go.sum @@ -1,15 +1,29 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 h1:SeDV6ZUSVlTAUUPdMzPXgMyj96z+whQJRRUff8dIeic= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73/go.mod h1:pwzJMyH4Hd0AZMJkWQ+/g01dDvYWEvmJuaiRU71Xl8k= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mum4k/termdash v0.18.0 h1:wpy3FKcVV5s2TOoMTKzqQXwL5VClZIlNrRqZDpeIzBA= github.com/mum4k/termdash v0.18.0/go.mod h1:VWL18wLZDKVKF/f4TkMRiKZb9Eg8Ax99PtNuGuRAguw= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e h1:mPy47VW9tkqImnSPgcjnEHJuG3XHDBtXj2hDb1qBrRs= @@ -39,8 +53,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -50,10 +64,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/lib/results.go b/internal/lib/results.go index ffc0c20..d18731e 100644 --- a/internal/lib/results.go +++ b/internal/lib/results.go @@ -30,7 +30,7 @@ var ( currentCtx context.Context // Current context. driver DisplayDriver // Display driver, dictated by the results. pauseQueryChans map[string]chan bool // Channels for dealing with 'pause' events for results. - readerIndexes map[string]*storage.ReaderIndex // Reader indexes for queries. + readerIndexes map[string]*storage.ReaderIndex // Collection of reader index ids per query. store storage.Storage // Stored results. ctxDefaults = map[string]interface{}{ @@ -147,23 +147,29 @@ func Results( inputPauseQueryChans map[string]chan bool, ) { var ( - err error // General error holder. + err error // General error holder. + pushgateway storage.PushgatewayStorage // Pushgateway configuraiton. filters = ctx.Value("filters").([]string) // Capture filters from context. labels = ctx.Value("labels").([]string) // Capture labels from context. queries = ctx.Value("queries").([]string) // Capture queries from context. ) + // Assign global config and global control channels. + config, pauseQueryChans = inputConfig, inputPauseQueryChans + defer close(pauseDisplayChan) + for _, pauseQueryChan := range pauseQueryChans { + defer close(pauseQueryChan) + } // Initialize storage. store, err = storage.NewStorage(history) e(err) defer store.Close() - // Assign global config and global control channels. - config, pauseQueryChans = inputConfig, inputPauseQueryChans - defer close(pauseDisplayChan) - for _, pauseQueryChan := range pauseQueryChans { - defer close(pauseQueryChan) + // Initialize external storage. + if config.PushgatewayAddr != "" { + pushgateway = storage.NewPushgatewayStorage(config.PushgatewayAddr) + store.AddExternalStorage(&pushgateway) } // Initialize reader indexes. diff --git a/internal/lib/util.go b/internal/lib/util.go index f5d6ed9..b260488 100644 --- a/internal/lib/util.go +++ b/internal/lib/util.go @@ -5,14 +5,13 @@ package lib import "golang.org/x/exp/slices" -// Gets the next element in a slice, with wrap-around if selecting from the -// last element. +// Gets the next element in a slice, with wrap-around if selecting from the last element. func GetNextSliceRing[T comparable](in []T, current T) T { return in[(slices.Index(in, current)+1)%len(in)] } -// Pick items from an arbitrary slice according to provided indexes. If indexes -// is empty, it will just return the original slice. +// Pick items from an arbitrary slice according to provided indexes. If indexes is empty, it will +// just return the original slice. func FilterSlice[T interface{}](in []T, indexes []int) (out []T) { if len(indexes) == 0 { out = in @@ -25,11 +24,10 @@ func FilterSlice[T interface{}](in []T, indexes []int) (out []T) { return } -// Gives a new percentage based on globalRelativePerc after reducing totality -// by limiting Perc. +// Gives a new percentage based on globalRelativePerc after reducing totality by limiting Perc. // -// For example, given a three-way percentage split of 80/10/10, this function -// will return 50 if given the arguments 80 and 10. +// For example, given a three-way percentage split of 80/10/10, this function will return 50 if +// given the arguments 80 and 10. func RelativePerc(limitingPerc, globalRelativePerc int) int { return (100 * globalRelativePerc) / (100 - limitingPerc) } diff --git a/main.go b/main.go index 1d5a6eb..4656893 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,8 @@ type queryMode int type queriesArg []string func (q *queriesArg) String() string { - return fmt.Sprintf("%v", &q) + // XXX This is necessary to resolve the interface contract, but doesn't seem important. + return "" } func (q *queriesArg) Set(query string) error { @@ -48,17 +49,18 @@ const ( ) var ( - attempts int // Number of attempts to execute the query. - delay int // Delay between queries. - displayMode int // Result mode to display. - filters string // Result filters. - history bool // Whether or not to preserve or use historical results. - logLevel string // Log level. - mode int // Mode to execute in. - port string // Port for RPC. - queries queriesArg // Queries to execute. - silent bool // Whether or not to be quiet. - labels string // Result value labels. + count int // Number of attempts to execute the query. + delay int // Delay between queries. + displayMode int // Result mode to display. + filters string // Result filters. + history bool // Whether or not to preserve or use historical results. + logLevel string // Log level. + mode int // Mode to execute in. + port string // Port for RPC. + pushgatewayAddr string // Address for Prometheus Pushg + queries queriesArg // Queries to execute. + silent bool // Whether or not to be quiet. + labels string // Result value labels. ctx = context.Background() // Initialize context. logger = log.Default() // Logging system. @@ -87,17 +89,19 @@ func main() { ) // Define arguments. - flag.BoolVar(&history, "e", true, "Whether or not to use or preserve history.") - flag.BoolVar(&silent, "s", false, "Don't output anything to a console.") - flag.IntVar(&attempts, "t", 1, "Number of query executions. -1 for continuous.") - flag.IntVar(&delay, "d", 3, "Delay between queries (seconds).") - flag.IntVar(&displayMode, "r", int(lib.DISPLAY_MODE_RAW), "Result mode to display.") - flag.IntVar(&mode, "m", int(MODE_QUERY), "Mode to execute in.") - flag.StringVar(&filters, "f", "", "Results filters.") - flag.StringVar(&labels, "v", "", "Labels to apply to query values, separated by commas.") - flag.StringVar(&logLevel, "l", "error", "Log level.") - flag.StringVar(&port, "p", "12345", "Port for RPC.") - flag.Var(&queries, "q", "Query to execute. When in query mode, this is expected to be some command. When in profile mode it is expected to be PID.") + flag.BoolVar(&history, "history", true, "Whether or not to use or preserve history.") + flag.BoolVar(&silent, "silent", false, "Don't output anything to a console.") + flag.IntVar(&count, "count", 1, "Number of query executions. -1 for continuous.") + flag.IntVar(&delay, "delay", 3, "Delay between queries (seconds).") + flag.IntVar(&displayMode, "display", int(lib.DISPLAY_MODE_RAW), "Result mode to display.") + flag.IntVar(&mode, "mode", int(MODE_QUERY), "Mode to execute in.") + flag.StringVar(&filters, "filters", "", "Results filters.") + flag.StringVar(&labels, "labels", "", "Labels to apply to query values, separated by commas.") + flag.StringVar(&logLevel, "logLevel", "error", "Log level.") + flag.StringVar(&port, "port", "12345", "Port for RPC.") + flag.StringVar(&pushgatewayAddr, "pushgateway", "127.0.0.1:9091", "Address for Prometheus Pushgateway.") + flag.Var(&queries, "query", "Query to execute. Can be supplied multiple times. When in query"+ + "mode, this is expected to be some command. When in profile mode it is expected to be PID.") flag.Parse() // Set-up logging. @@ -119,7 +123,7 @@ func main() { doneQueriesChan, pauseQueryChans = lib.Query( lib.QUERY_MODE_PROFILE, - attempts, + count, delay, queries, port, @@ -133,7 +137,7 @@ func main() { doneQueriesChan, pauseQueryChans = lib.Query( lib.QUERY_MODE_COMMAND, - attempts, + count, delay, queries, port, @@ -164,7 +168,8 @@ func main() { ctx.Value("queries").([]string)[0], // Always start with the first query. history, lib.Config{ - LogLevel: logLevel, + LogLevel: logLevel, + PushgatewayAddr: pushgatewayAddr, }, pauseQueryChans, ) diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..28268f4 --- /dev/null +++ b/main_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +const ( + CRYPTARCH_BINARY_DIR = "dist" // Directory the executable is in. + CRYPTARCH_BINARY_NAME = "cryptarch" // Name of the Cryptarch binary. +) + +var ( + // Filepath for the Cryptarch executable. + cryptarch = filepath.Join(CRYPTARCH_BINARY_DIR, CRYPTARCH_BINARY_NAME) + // Environment to execute Cryptarch with, appended to `os.Environ()`. + testEnviron = map[string]string{ + "GOCOVERDIR": ".coverdata", + } +) + +// Converts a string-to-string mapping to a comma-delimited string. +func environToA(environ map[string]string) (a string) { + for k, v := range environ { + a += fmt.Sprintf("%s=\"%s\"%s", k, v, ",") + } + + // Take the last delimiter off. + return strings.TrimSuffix(a, ",") +} + +// Builds the Cryptarch binary. +func buildCryptarch() ([]byte, error) { + cmd := exec.Command("go", "build", "-o", cryptarch) + return cmd.CombinedOutput() +} + +// Executes the Cryptarch binary. +func runCryptarch(args []string) ([]byte, error) { + var ( + environ = append(os.Environ(), environToA(testEnviron)) + ) + + cmd := exec.Command(cryptarch, args...) + cmd.Env = environ + return cmd.CombinedOutput() +} + +// Test set-up. +func TestMain(m *testing.M) { + buildCryptarch() + os.Exit(m.Run()) +} + +// Run CLI invocation tests. +func TestCLI(t *testing.T) { + cliTests := []struct { + testName string + cliArgs []string + }{ + {"help", []string{"-h"}}, + } + + for _, cliTest := range cliTests { + t.Run(cliTest.testName, func(t *testing.T) { + _, err := runCryptarch(cliTest.cliArgs) + + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/pkg/storage/errors.go b/pkg/storage/errors.go new file mode 100644 index 0000000..d256d7b --- /dev/null +++ b/pkg/storage/errors.go @@ -0,0 +1,18 @@ +// +// Storage specific errors. + +package storage + +import ( + "fmt" +) + +// Error indicating a number is expected but was not provided. +type NaNError struct { + // Value attempted to be used + Value interface{} +} + +func (e *NaNError) Error() string { + return fmt.Sprintf("Attempted to use non-numeric value in numerical context. Value: %v", e.Value) +} diff --git a/pkg/storage/external.go b/pkg/storage/external.go new file mode 100644 index 0000000..4469179 --- /dev/null +++ b/pkg/storage/external.go @@ -0,0 +1,106 @@ +// +// External integrations. + +package storage + +import ( + "fmt" + "regexp" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/push" + "golang.org/x/exp/slog" +) + +const ( + PROMETHEUS_METRICS_HELP = "Produced by Cryptarch." // Help text for all Prometheus metrics. + PROMETHEUS_METRIC_LABEL = "cryptarch_label" // What Prometheus label to use for the Cryptarch label. + PROMETHEUS_METRIC_PREFIX = "cryptarch" // Prefix for all Prometheus metrics. +) + +var ( + // Regular expression used for constructing Prometheus metric names. Represents the negation of + // characters allowed in order to sanitize bad characters. We also replace the valid character ':' + // as they are for recording rules. + // + // See: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + prometheus_metric_name_replace_regexp = regexp.MustCompile("[^a-zA-Z0-9_]") +) + +// Interface for any external storage system. +type externalStorage interface { + // Add a result to the external storage. + Put(query string, labels []string, result Result) error +} + +// Prometheus Pushgateway specific external storage system. +type PushgatewayStorage struct { + address string // Address to connect to Pushgateway. + registry prometheus.Registerer // Prometheus registry to use. +} + +// Add a result to Prometheus Pushgtateway. +func (p *PushgatewayStorage) Put(query string, labels []string, result Result) error { + var ( + name = queryToPromName(query) + reg = prometheus.NewRegistry() + ) + + metric, err := resultToPromMetric(name, labels, result) + if err != nil { + return err + } + reg.MustRegister(metric) + + slog.Debug(fmt.Sprintf("Pushing to Pushgtateway, name: %s, result: %v ...", name, result)) + push.New((*p).address, "test").Gatherer(reg).Push() + + return nil +} + +// Converts a query string to something acceptable as a Prometheus metric name. +func queryToPromName(query string) string { + return string(prometheus_metric_name_replace_regexp.ReplaceAll([]byte(query), []byte("_"))) +} + +// Converts a result to a Prometheus metric. +func resultToPromMetric( + name string, + labels []string, + result Result, +) (metric *prometheus.GaugeVec, err error) { + metric = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: fmt.Sprintf("%s_%s", PROMETHEUS_METRIC_PREFIX, name), + Help: PROMETHEUS_METRICS_HELP, + }, + []string{PROMETHEUS_METRIC_LABEL}, + ) + + for i, value := range result.Values { + switch value.(type) { + case int64: + metric.With(prometheus.Labels{PROMETHEUS_METRIC_LABEL: labels[i]}).Set(float64(value.(int64))) + case float64: + metric.With(prometheus.Labels{PROMETHEUS_METRIC_LABEL: labels[i]}).Set(value.(float64)) + default: + // We encountered a value Prometheus can't digest. + err = &NaNError{Value: value} + + // TODO For now, we give-up if any value is non-pushable. In the future, we might consider + // still attempting to push some values, but this would also require better error handling in + // `Put`. + break + } + } + + return +} + +// Create a new storage for Pushgateway. +func NewPushgatewayStorage(address string) PushgatewayStorage { + return PushgatewayStorage{ + address: address, + registry: prometheus.NewRegistry(), + } +} diff --git a/pkg/storage/go.mod b/pkg/storage/go.mod index dcdb0e9..4e3605f 100644 --- a/pkg/storage/go.mod +++ b/pkg/storage/go.mod @@ -2,4 +2,18 @@ module storage go 1.20 -require golang.org/x/exp v0.0.0-20231226003508-02704c960a9b +require ( + github.com/prometheus/client_golang v1.18.0 + golang.org/x/exp v0.0.0-20231226003508-02704c960a9b +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/sys v0.15.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/pkg/storage/go.sum b/pkg/storage/go.sum index d20934a..d1d6cc7 100644 --- a/pkg/storage/go.sum +++ b/pkg/storage/go.sum @@ -1,2 +1,25 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/pkg/storage/reader_index.go b/pkg/storage/reader_index.go new file mode 100644 index 0000000..74e349f --- /dev/null +++ b/pkg/storage/reader_index.go @@ -0,0 +1,23 @@ +// +// Reader indexes allow for tracking the last read result from a results series. Reader indexes are +// meant to be supplied and managed by clients. + +package storage + +// Reader indexes control where a consumer has last read a result. +type ReaderIndex int + +// Decrement a reader index, to re-read the last read. +func (i *ReaderIndex) Dec() { + (*i)-- +} + +// Incremement a reader index, likely after a read. +func (i *ReaderIndex) Inc() { + (*i)++ +} + +// Sets a redaer index to a specified value. +func (i *ReaderIndex) Set(newI int) { + *i = ReaderIndex(newI) +} diff --git a/pkg/storage/results.go b/pkg/storage/results.go index 5b4b277..9866be4 100644 --- a/pkg/storage/results.go +++ b/pkg/storage/results.go @@ -16,7 +16,7 @@ import ( // Individual result. type Result struct { Time time.Time // Time the result was created. - Value interface{} // Raw value of the result. + Value string // Raw value of the result. Values []interface{} // Tokenized value of the result. } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index c9da743..fcba1fe 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -25,58 +25,39 @@ import ( _ "golang.org/x/exp/slog" ) -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Types -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -// Reader indexes control where a consumer has last read a result. -type ReaderIndex int - -// Collection of results mapped to their queries. -type Storage map[string]*Results - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Variables -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - const ( + MAX_EXTERNAL_STORAGES = 128 // Maximum external storage integrations. + MAX_RESULTS = 128 // Maximum number of result series that may be maintained. PUT_EVENT_CHANNEL_SIZE = 128 // Size of put channels, controlling the amount of waiting results. STORAGE_FILE_DIR = "cryptarch" // Directory in user cache to use for storage. STORAGE_FILE_NAME = "storage.json" // Filename to use for actual storage. ) -var ( - storageFile *os.File // File for storage writes. - storageMutex sync.Mutex // Lock for storage file writes. - - PutEvents = make(map[string](chan Result)) // Channels for broadcasting put calls. -) +// Collection of results mapped to their queries. +type Storage struct { + externalStorages []externalStorage // Integrated external storages. + putEventChans map[string](chan Result) // Map of queries to put even channels. + storageFile *os.File // File for persisting results. + storageMutex *sync.Mutex // Mutex for managing persistence writes. -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Private -// -/////////////////////////////////////////////////////////////////////////////////////////////////// + Results map[string]*Results // Map of queries to results. +} // initializes a new results series in storage. Must be called when a new results series is created. // This function is idempotent in that it will check if results for a query have already been // initialized and pass silently if so. func (s *Storage) newResults(query string, size int) { var ( - results Results // Results to add. + results Results // Results to initialize. ) - if _, ok := (*s)[query]; !ok { + if _, ok := (*s).Results[query]; !ok { // Initialize results. results = newResults(size) - (*s)[query] = &results + (*s).Results[query] = &results // Initialize the query's put event channel. - PutEvents[query] = make(chan Result, PUT_EVENT_CHANNEL_SIZE) + (*s).putEventChans[query] = make(chan Result, PUT_EVENT_CHANNEL_SIZE) } } @@ -85,39 +66,37 @@ func (s *Storage) newResults(query string, size int) { func (s *Storage) save() error { var ( err error // General error holder. - storageJson []byte // Bytes as json. + resultsJson []byte // Bytes as json. ) // Lock storage to prevent dirty writes. - storageMutex.Lock() - defer storageMutex.Unlock() + (*s).storageMutex.Lock() + defer (*s).storageMutex.Unlock() - // Translate current storage into binary json and save it. - storageJson, err = json.MarshalIndent(&s, "", "\t") - _, err = storageFile.WriteAt(storageJson, 0) + // Translate current storage results into binary json and save it. + resultsJson, err = json.MarshalIndent(&s.Results, "", "\t") + _, err = (*s).storageFile.WriteAt(resultsJson, 0) return err } -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Public -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - // Initializes a new storage, loading in any saved storage data. func NewStorage(persistence bool) (storage Storage, err error) { var ( cryptarchUserCacheDir string // Cryptarch specific user cache data. + results map[string]*Results // Pre-built storage with existing data. storageData []byte // Raw read storage data. - storageFP string // Filepath for storage. - storagePre map[string]*Results // Pre-built storage with existing data. + storageFilepath string // Filepath for storage. storageStat fs.FileInfo // Stat for the storage file. userCacheDir string // User cache directory, contextual to OS. ) // Initialize storage. - storage = Storage{} + storage = Storage{ + Results: make(map[string]*Results, MAX_RESULTS), + putEventChans: make(map[string](chan Result), MAX_RESULTS), + storageMutex: &sync.Mutex{}, + } // If we have disabled persistence, simply return the new storage instance. if !persistence { @@ -138,9 +117,9 @@ func NewStorage(persistence bool) (storage Storage, err error) { } // Instantiate the storage file. - storageFP = filepath.Join(cryptarchUserCacheDir, STORAGE_FILE_NAME) - storageFile, err = os.OpenFile( - storageFP, + storageFilepath = filepath.Join(cryptarchUserCacheDir, STORAGE_FILE_NAME) + storage.storageFile, err = os.OpenFile( + storageFilepath, os.O_CREATE|os.O_RDWR, fs.FileMode(0770), ) @@ -148,7 +127,7 @@ func NewStorage(persistence bool) (storage Storage, err error) { return } - if storageStat, err = os.Stat(storageFP); storageStat.Size() > 0 { + if storageStat, err = os.Stat(storageFilepath); storageStat.Size() > 0 { // There is pre-existing storage data. if err != nil { return @@ -156,103 +135,94 @@ func NewStorage(persistence bool) (storage Storage, err error) { // Read in storage data and supply it to storage. We must initialize any results series before // populating data. - storageData, err = io.ReadAll(storageFile) + storageData, err = io.ReadAll(storage.storageFile) if err != nil { return } - json.Unmarshal(storageData, &storagePre) - for query := range storagePre { + json.Unmarshal(storageData, &results) + + for query := range results { // TODO Results loading should also preserve and restore actual labels. - storage.newResults(query, len(storagePre[query].Results[0].Values)) + storage.newResults(query, len(results[query].Results[0].Values)) + storage.Results[query] = results[query] } - storage = storagePre } return } -// Decrement a reader index, to re-read the last read. -func (i *ReaderIndex) Dec() { - (*i)-- -} - -// Incremement a reader index, likely after a read. -func (i *ReaderIndex) Inc() { - (*i)++ -} - -// Sets a redaer index to a specified value. -func (i *ReaderIndex) Set(newI int) { - *i = ReaderIndex(newI) +// Adds an external storage. +func (s *Storage) AddExternalStorage(e externalStorage) { + (*s).externalStorages = append((*s).externalStorages, e) } // Closes a storage. Should be called after all storage operations cease. func (s *Storage) Close() { - storageFile.Close() + (*s).storageFile.Close() } // Get a result based on a timestamp. func (s *Storage) Get(query string, time time.Time) Result { - return (*s)[query].get(time) + return (*s).Results[query].get(time) } // Get all results. func (s *Storage) GetAll(query string) []Result { - return (*s)[query].Results + return (*s).Results[query].Results } // Get a result's labels. func (s *Storage) GetLabels(query string) []string { - return (*s)[query].Labels + return (*s).Results[query].Labels } // Gets results based on a start and end timestamp. func (s *Storage) GetRange(query string, startTime, endTime time.Time) []Result { - return (*s)[query].getRange(startTime, endTime) + return (*s).Results[query].getRange(startTime, endTime) } // Given results up to a reader index (a.k.a. "playback"). func (s *Storage) GetToIndex(query string, index *ReaderIndex) []Result { - return (*s)[query].Results[:(*index)+1] + return (*s).Results[query].Results[:(*index)+1] } // Given a filter, return the corresponding value index. func (s *Storage) GetValueIndex(query, filter string) int { - return (*s)[query].getValueIndex(filter) + return (*s).Results[query].getValueIndex(filter) } // Initialize a new reader index. Will attempt to set the initial value to the end of existing // results, if results already exist. func (s *Storage) NewReaderIndex(query string) *ReaderIndex { var ( - r ReaderIndex // Reader index to return the address of. + reader ReaderIndex // Reader index to initialize. ) - if _, ok := (*s)[query]; !ok { + if _, ok := (*s).Results[query]; !ok { // There is no data. - r = ReaderIndex(0) + reader = ReaderIndex(0) } else { // There is existing data to account for. - r = ReaderIndex(len((*s)[query].Results)) + reader = ReaderIndex(len((*s).Results[query].Results)) } - return &r + return &reader } // Retrieve the next result from a put event channel, blocking if none exists. -func (s *Storage) Next(query string, index *ReaderIndex) (next Result) { - next = <-PutEvents[query] - index.Inc() +func (s *Storage) Next(query string, reader *ReaderIndex) (next Result) { + next = <-(*s).putEventChans[query] + reader.Inc() return } // Retrieve the next result from a put event channel, returning an empty result if nothing exists. -func (s *Storage) NextOrEmpty(query string, index *ReaderIndex) (next Result) { +func (s *Storage) NextOrEmpty(query string, reader *ReaderIndex) (next Result) { select { - case next = <-PutEvents[query]: + case next = <-(*s).putEventChans[query]: // Only increment the read counter if something consumed the event. - index.Inc() + reader.Inc() default: } @@ -267,12 +237,12 @@ func (s *Storage) Put( ) (result Result, err error) { // Initialize the result. s.newResults(query, len(values)) - result = (*s)[query].put(value, values...) + result = (*s).Results[query].put(value, values...) // Send a non-blocking put event. Put events are lossy and clients may lose information if not // actively listening. select { - case PutEvents[query] <- result: + case (*s).putEventChans[query] <- result: default: } @@ -280,6 +250,17 @@ func (s *Storage) Put( if persistence { err = s.save() } + if err != nil { + return + } + + // Persist data to external sources. + for _, externalStore := range (*s).externalStorages { + err = externalStore.Put(query, (*s).Results[query].Labels, result) + if err != nil { + return + } + } return } @@ -287,12 +268,12 @@ func (s *Storage) Put( // Assigns explicit labels to a results series. func (s *Storage) PutLabels(query string, labels []string) { s.newResults(query, len(labels)) - (*s)[query].Labels = labels + (*s).Results[query].Labels = labels } // Show all currently stored results. func (s *Storage) Show(query string) { - (*s)[query].show() + (*s).Results[query].show() } ///////////////////////////////////////////////////////////////////////////////////////////////////