diff --git a/annotation_test.go b/annotation_test.go index ab8a62d..795bb8e 100644 --- a/annotation_test.go +++ b/annotation_test.go @@ -1,7 +1,8 @@ -package simplejson +package simplejson_test import ( "encoding/json" + "github.com/clambin/simplejson/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "os" @@ -12,12 +13,12 @@ import ( ) func TestAnnotation_MarshalJSON(t *testing.T) { - ann := Annotation{ + ann := simplejson.Annotation{ Time: time.Date(2022, time.January, 23, 0, 0, 0, 0, time.UTC), Title: "foo", Text: "bar", Tags: []string{"A", "B"}, - Request: RequestDetails{ + Request: simplejson.RequestDetails{ Name: "snafu", Datasource: "datasource", Enable: true, diff --git a/doc.go b/doc.go index 0ba34e4..a15672c 100644 --- a/doc.go +++ b/doc.go @@ -3,24 +3,21 @@ Package simplejson provides a Go implementation for Grafana's SimpleJSON datasou # Overview -A simplejson server is an HTTP server that supports one or more handlers. Each handler can support multiple query targets, -which can be either timeseries or table query. A handler may support tag key/value pairs, which can be passed to the query -to adapt its behaviour (e.g. filtering what data should be returned) through 'ad hoc filters'. Finally, a handler can support -annotations, i.e. a set of timestamps with associated text. +A simplejson server supports one or more handlers. Each handler can support multiple query targets,which can be either timeseries or table query. +A handler may support tag key/value pairs, which can be passed to the query to adapt its behaviour (e.g. filtering what data should be returned) +through 'ad hoc filters'. Finally, a handler can support annotations, i.e. a set of timestamps with associated text. # Server -To create a SimpleJSON server, create a Server and run it: +simplejson.New() creates a SimpleJSON server. The server is implemented as an http router, compatible with net/http: handlers := map[string]simplejson.Handler{ "A": &handler{}, "B": &handler{table: true}, } - s, err := simplejson.New(handlers, simplejson.WithHTTPServerOption{Option: httpserver.WithPort{Port: 8080}}) + r := simplejson.New(handlers, simplejson.WithHTTPServerOption{Option: httpserver.WithPort{Port: 8080}}) - if err == nil { - err = s.Serve() - } + _ = http.ListenAndServe(":8080", r) This starts a server, listening on port 8080, with one target "my-target", served by myHandler. @@ -124,17 +121,17 @@ When the dashboard performs a query with a tag selected, that tag & value will b # Metrics -simplejson exports two Prometheus metrics for performance analytics: +When provided with the WithQueryMetrics option, simplejson exports two Prometheus metrics for performance analytics: simplejson_query_duration_seconds: duration of query requests by target, in seconds simplejson_query_failed_count: number of failed query requests -The underlying http server is implemented by [github.com/clambin/go-common/httpserver], which exports its own set of metrics. +The underlying http router uses [PrometheusMetrics], which exports its own set of metrics. See WithHTTPMetrics for details. # Other topics For information on query arguments and tags, refer to the documentation for those data structures. -[github.com/clambin/go-common/httpserver]: https://github.com/clambin/go-common/tree/httpserver/httpserver +[PrometheusMetrics]: https://pkg.go.dev/github.com/clambin/go-common/httpserver/middleware */ package simplejson diff --git a/doc_test.go b/doc_test.go index bd9e0b7..f156d73 100644 --- a/doc_test.go +++ b/doc_test.go @@ -3,21 +3,18 @@ package simplejson_test import ( "context" "fmt" - "github.com/clambin/go-common/httpserver" - "github.com/clambin/simplejson/v5" + "github.com/clambin/simplejson/v6" + "net/http" "time" ) func Example() { - handlers := map[string]simplejson.Handler{ + r := simplejson.New(map[string]simplejson.Handler{ "A": &handler{}, "B": &handler{table: true}, - } - s, err := simplejson.New(handlers, simplejson.WithHTTPServerOption{Option: httpserver.WithPort{Port: 8080}}) + }) - if err == nil { - _ = s.Serve() - } + _ = http.ListenAndServe(":8080", r) } type handler struct{ table bool } diff --git a/endpoints.go b/endpoints.go index 4d34b2c..3cdfbc1 100644 --- a/endpoints.go +++ b/endpoints.go @@ -4,13 +4,19 @@ import ( "encoding/json" "github.com/go-http-utils/headers" "net/http" + "sort" ) func (s *Server) Search(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Set(headers.ContentType, "application/json") + var targets []string + for target := range s.Handlers { + targets = append(targets, target) + } + sort.Strings(targets) - output, _ := json.Marshal(s.Targets()) + //w.WriteHeader(http.StatusOK) + w.Header().Set(headers.ContentType, "application/json") + output, _ := json.Marshal(targets) _, _ = w.Write(output) } @@ -23,7 +29,7 @@ func (s *Server) Query(w http.ResponseWriter, req *http.Request) { func (s *Server) Annotations(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) + //w.WriteHeader(http.StatusOK) w.Header().Set(headers.AccessControlAllowHeaders, "accept, content-type") w.Header().Set(headers.AccessControlAllowMethods, "POST") w.Header().Set(headers.AccessControlAllowOrigin, "*") @@ -136,7 +142,7 @@ func handleEndpoint(w http.ResponseWriter, req *http.Request, request json.Unmar return } - w.WriteHeader(http.StatusOK) + //w.WriteHeader(http.StatusOK) w.Header().Set(headers.ContentType, "application/json") if err = json.NewEncoder(w).Encode(response); err != nil { diff --git a/endpoints_query_test.go b/endpoints_query_test.go index 7585de4..bffa5d5 100644 --- a/endpoints_query_test.go +++ b/endpoints_query_test.go @@ -1,4 +1,4 @@ -package simplejson +package simplejson_test import ( "bytes" diff --git a/endpoints_test.go b/endpoints_test.go index d5c286c..f81a769 100644 --- a/endpoints_test.go +++ b/endpoints_test.go @@ -1,4 +1,4 @@ -package simplejson +package simplejson_test import ( "bytes" diff --git a/go.mod b/go.mod index 2a0aa8c..e1af1be 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ -module github.com/clambin/simplejson/v5 +module github.com/clambin/simplejson/v6 -go 1.18 +go 1.19 require ( - github.com/clambin/go-common/httpserver v0.4.0 + github.com/clambin/go-common/httpserver v0.4.1 github.com/clambin/go-common/set v0.1.2 + github.com/go-chi/chi/v5 v5.0.8 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a github.com/grafana/grafana-plugin-sdk-go v0.147.0 github.com/mailru/easyjson v0.7.7 @@ -38,7 +39,8 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - golang.org/x/sys v0.3.0 // indirect + golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 // indirect + golang.org/x/sys v0.4.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f9390b8..40c261a 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/clambin/go-common/httpserver v0.4.0 h1:t9wH0W9P/mMHacWSdDqRdOrxhNRV58o5pLK5fF8630s= github.com/clambin/go-common/httpserver v0.4.0/go.mod h1:DQrnJehMlHai3Y/l9mF53evzCRfiXVc2O8+WqBLLhHQ= +github.com/clambin/go-common/httpserver v0.4.1 h1:TIHswNcj5hVbrAIrSPVvkTWqTU4M2770qZctvR6jQGE= +github.com/clambin/go-common/httpserver v0.4.1/go.mod h1:qCWVjj3pASnqGTmLJhLrMKCxw7AcVANug3a0kC4qW6c= github.com/clambin/go-common/set v0.1.2 h1:Pr0FXPVJsH4dc/OelhjXYo0xVmOLIUurg3saMh8KTz8= github.com/clambin/go-common/set v0.1.2/go.mod h1:GFjAZynNo4BNjlx2hzDMHIU4ays7QjpJUSHXB/3b1ZE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -38,6 +40,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -154,6 +158,8 @@ golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 h1:fJwx88sMf5RXwDwziL0/Mn9Wqs+efMSo/RYcL+37W9c= +golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -201,6 +207,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= diff --git a/option.go b/option.go index 2812612..b41bdea 100644 --- a/option.go +++ b/option.go @@ -1,7 +1,7 @@ package simplejson import ( - "github.com/clambin/go-common/httpserver" + "github.com/clambin/go-common/httpserver/middleware" ) // Option specified configuration options for Server @@ -21,11 +21,11 @@ func (o WithQueryMetrics) apply(s *Server) { s.queryMetrics = newQueryMetrics(o.Name) } -// WithHTTPServerOption will pass the provided option to the underlying HTTP Server -type WithHTTPServerOption struct { - Option httpserver.Option +// WithHTTPMetrics will configure the http router to gather statistics on SimpleJson endpoint calls and record them as Prometheus metrics +type WithHTTPMetrics struct { + Option middleware.PrometheusMetricsOptions } -func (o WithHTTPServerOption) apply(s *Server) { - s.httpServerOptions = append(s.httpServerOptions, o.Option) +func (o WithHTTPMetrics) apply(s *Server) { + s.prometheusMetrics = middleware.NewPrometheusMetrics(o.Option) } diff --git a/pkg/data/filter.go b/pkg/data/filter.go index 6a2067b..acb8fe9 100644 --- a/pkg/data/filter.go +++ b/pkg/data/filter.go @@ -1,7 +1,7 @@ package data import ( - "github.com/clambin/simplejson/v5" + "github.com/clambin/simplejson/v6" "time" ) diff --git a/pkg/data/filter_test.go b/pkg/data/filter_test.go index 0970eaf..1b7099a 100644 --- a/pkg/data/filter_test.go +++ b/pkg/data/filter_test.go @@ -1,8 +1,8 @@ package data_test import ( - "github.com/clambin/simplejson/v5" - "github.com/clambin/simplejson/v5/pkg/data" + "github.com/clambin/simplejson/v6" + "github.com/clambin/simplejson/v6/pkg/data" grafanaData "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/assert" "testing" diff --git a/pkg/data/response.go b/pkg/data/response.go index 20831ef..af11532 100644 --- a/pkg/data/response.go +++ b/pkg/data/response.go @@ -1,7 +1,7 @@ package data import ( - "github.com/clambin/simplejson/v5" + "github.com/clambin/simplejson/v6" "github.com/grafana/grafana-plugin-sdk-go/data" "time" ) diff --git a/pkg/data/table_test.go b/pkg/data/table_test.go index deb4cf8..68ff6a9 100644 --- a/pkg/data/table_test.go +++ b/pkg/data/table_test.go @@ -1,7 +1,7 @@ package data_test import ( - "github.com/clambin/simplejson/v5/pkg/data" + "github.com/clambin/simplejson/v6/pkg/data" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "strconv" diff --git a/query_test.go b/query_test.go index a368e1e..788dd17 100644 --- a/query_test.go +++ b/query_test.go @@ -1,9 +1,10 @@ -package simplejson +package simplejson_test import ( "bufio" "bytes" "encoding/json" + "github.com/clambin/simplejson/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "os" @@ -27,7 +28,7 @@ func TestRequests(t *testing.T) { ] }` - var output QueryRequest + var output simplejson.QueryRequest err := json.Unmarshal([]byte(input), &output) require.NoError(t, err) @@ -47,14 +48,14 @@ func TestResponse(t *testing.T) { tests := []struct { name string pass bool - response Response + response simplejson.Response }{ { name: "timeseries", pass: true, - response: TimeSeriesResponse{ + response: simplejson.TimeSeriesResponse{ Target: "A", - DataPoints: []DataPoint{ + DataPoints: []simplejson.DataPoint{ {Value: 100, Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {Value: 101, Timestamp: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)}, {Value: 102, Timestamp: time.Date(2020, 1, 1, 2, 0, 0, 0, time.UTC)}, @@ -64,12 +65,12 @@ func TestResponse(t *testing.T) { { name: "table", pass: true, - response: TableResponse{ - Columns: []Column{ - {Text: "Time", Data: TimeColumn{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}}, - {Text: "Label", Data: StringColumn{"foo", "bar"}}, - {Text: "Series A", Data: NumberColumn{42, 43}}, - {Text: "Series B", Data: NumberColumn{64.5, 100.0}}, + response: simplejson.TableResponse{ + Columns: []simplejson.Column{ + {Text: "Time", Data: simplejson.TimeColumn{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}}, + {Text: "Label", Data: simplejson.StringColumn{"foo", "bar"}}, + {Text: "Series A", Data: simplejson.NumberColumn{42, 43}}, + {Text: "Series B", Data: simplejson.NumberColumn{64.5, 100.0}}, }, }, }, @@ -81,12 +82,12 @@ func TestResponse(t *testing.T) { { name: "invalid", pass: false, - response: TableResponse{ - Columns: []Column{ - {Text: "Time", Data: TimeColumn{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}}, - {Text: "Label", Data: StringColumn{"foo"}}, - {Text: "Series A", Data: NumberColumn{42, 43}}, - {Text: "Series B", Data: NumberColumn{64.5, 100.0, 105.0}}, + response: simplejson.TableResponse{ + Columns: []simplejson.Column{ + {Text: "Time", Data: simplejson.TimeColumn{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}}, + {Text: "Label", Data: simplejson.StringColumn{"foo"}}, + {Text: "Series A", Data: simplejson.NumberColumn{42, 43}}, + {Text: "Series B", Data: simplejson.NumberColumn{64.5, 100.0, 105.0}}, }, }, }, @@ -136,21 +137,21 @@ func (r combinedResponse) MarshalJSON() ([]byte, error) { func makeCombinedQueryResponse() combinedResponse { testDate := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - dataseries := []TimeSeriesResponse{{ + dataseries := []simplejson.TimeSeriesResponse{{ Target: "A", - DataPoints: []DataPoint{ + DataPoints: []simplejson.DataPoint{ {Value: 100, Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {Value: 101, Timestamp: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)}, {Value: 102, Timestamp: time.Date(2020, 1, 1, 2, 0, 0, 0, time.UTC)}, }, }} - tables := []TableResponse{{ - Columns: []Column{ - {Text: "Time", Data: TimeColumn{testDate, testDate}}, - {Text: "Label", Data: StringColumn{"foo", "bar"}}, - {Text: "Series A", Data: NumberColumn{42, 43}}, - {Text: "Series B", Data: NumberColumn{64.5, 100.0}}, + tables := []simplejson.TableResponse{{ + Columns: []simplejson.Column{ + {Text: "Time", Data: simplejson.TimeColumn{testDate, testDate}}, + {Text: "Label", Data: simplejson.StringColumn{"foo", "bar"}}, + {Text: "Series A", Data: simplejson.NumberColumn{42, 43}}, + {Text: "Series B", Data: simplejson.NumberColumn{64.5, 100.0}}, }, }} @@ -176,16 +177,16 @@ func BenchmarkTimeSeriesResponse_MarshalJSON(b *testing.B) { } } -func buildTimeSeriesResponse(count int) TimeSeriesResponse { - var datapoints []DataPoint +func buildTimeSeriesResponse(count int) simplejson.TimeSeriesResponse { + var datapoints []simplejson.DataPoint timestamp := time.Date(2022, time.November, 27, 0, 0, 0, 0, time.UTC) for i := 0; i < count; i++ { - datapoints = append(datapoints, DataPoint{ + datapoints = append(datapoints, simplejson.DataPoint{ Timestamp: timestamp, Value: float64(i), }) } - return TimeSeriesResponse{Target: "foo", DataPoints: datapoints} + return simplejson.TimeSeriesResponse{Target: "foo", DataPoints: datapoints} } func BenchmarkTableResponse_MarshalJSON(b *testing.B) { @@ -198,7 +199,7 @@ func BenchmarkTableResponse_MarshalJSON(b *testing.B) { } } -func buildTableResponse(count int) TableResponse { +func buildTableResponse(count int) simplejson.TableResponse { var timestamps []time.Time var values []float64 @@ -208,8 +209,8 @@ func buildTableResponse(count int) TableResponse { values = append(values, 1.0) timestamp = timestamp.Add(time.Minute) } - return TableResponse{Columns: []Column{ - {Text: "time", Data: TimeColumn(timestamps)}, - {Text: "value", Data: NumberColumn(values)}, + return simplejson.TableResponse{Columns: []simplejson.Column{ + {Text: "time", Data: simplejson.TimeColumn(timestamps)}, + {Text: "value", Data: simplejson.NumberColumn(values)}, }} } diff --git a/server.go b/server.go index 9325af0..80620fb 100644 --- a/server.go +++ b/server.go @@ -1,74 +1,72 @@ package simplejson import ( - "github.com/clambin/go-common/httpserver" + "github.com/clambin/go-common/httpserver/middleware" + "github.com/go-chi/chi/v5" + middleware2 "github.com/go-chi/chi/v5/middleware" + "github.com/go-http-utils/headers" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/exp/slog" "net/http" - "sort" - "time" ) // Server receives SimpleJSON requests from Grafana and dispatches them to the handler that serves the specified target. type Server struct { + chi.Router Handlers map[string]Handler + prometheusMetrics *middleware.PrometheusMetrics queryMetrics *QueryMetrics - httpServerOptions []httpserver.Option - httpServer *httpserver.Server } var _ prometheus.Collector = &Server{} +var _ http.Handler = &Server{} -func New(handlers map[string]Handler, options ...Option) (*Server, error) { - s := Server{Handlers: handlers} +func New(handlers map[string]Handler, options ...Option) *Server { + s := Server{ + Handlers: handlers, + Router: chi.NewRouter(), + } for _, o := range options { o.apply(&s) } - s.httpServerOptions = append(s.httpServerOptions, httpserver.WithHandlers{ - Handlers: []httpserver.Handler{ - {Path: "/", Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })}, - {Path: "/search", Handler: http.HandlerFunc(s.Search), Methods: []string{http.MethodPost}}, - {Path: "/query", Handler: http.HandlerFunc(s.Query), Methods: []string{http.MethodPost}}, - {Path: "/annotations", Handler: http.HandlerFunc(s.Annotations), Methods: []string{http.MethodPost, http.MethodOptions}}, - {Path: "/tag-keys", Handler: http.HandlerFunc(s.TagKeys), Methods: []string{http.MethodPost}}, - {Path: "/tag-values", Handler: http.HandlerFunc(s.TagValues), Methods: []string{http.MethodPost}}, - }, + s.Router.Use(middleware2.Heartbeat("/")) + s.Router.Group(func(r chi.Router) { + r.Use(middleware.Logger(slog.Default())) + if s.prometheusMetrics != nil { + r.Use(s.prometheusMetrics.Handle) + } + r.Post("/search", s.Search) + r.Post("/query", s.Query) + r.Post("/annotations", s.Annotations) + r.Options("/annotations", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set(headers.AccessControlAllowOrigin, "*") + w.Header().Set(headers.AccessControlAllowMethods, "POST") + w.Header().Set(headers.AccessControlAllowHeaders, "accept, content-type") + }) + r.Post("/tag-keys", s.TagValues) + r.Post("/tag-values", s.TagValues) }) - var err error - s.httpServer, err = httpserver.New(s.httpServerOptions...) - - return &s, err -} - -// Serve starts the SimpleJSon Server. -func (s *Server) Serve() error { - return s.httpServer.Serve() -} - -// Shutdown stops a running Server. -func (s *Server) Shutdown(timeout time.Duration) error { - return s.httpServer.Shutdown(timeout) -} - -// Targets returns a sorted list of supported targets -func (s *Server) Targets() []string { - var targets []string - for target := range s.Handlers { - targets = append(targets, target) - } - sort.Strings(targets) - return targets + return &s } // Describe implements the prometheus.Collector interface func (s *Server) Describe(descs chan<- *prometheus.Desc) { - s.httpServer.Describe(descs) - s.queryMetrics.Describe(descs) + if s.prometheusMetrics != nil { + s.prometheusMetrics.Describe(descs) + } + if s.queryMetrics != nil { + s.queryMetrics.Describe(descs) + } } // Collect implements the prometheus.Collector interface func (s *Server) Collect(metrics chan<- prometheus.Metric) { - s.httpServer.Collect(metrics) - s.queryMetrics.Collect(metrics) + if s.prometheusMetrics != nil { + s.prometheusMetrics.Collect(metrics) + } + if s.queryMetrics != nil { + s.queryMetrics.Collect(metrics) + } } diff --git a/server_test.go b/server_test.go index 1d5c928..c507514 100644 --- a/server_test.go +++ b/server_test.go @@ -1,109 +1,100 @@ -package simplejson +package simplejson_test import ( "bytes" "context" - "errors" "flag" "fmt" + "github.com/clambin/go-common/httpserver/middleware" + "github.com/clambin/simplejson/v6" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" "net/http/httptest" - "sync" "testing" "time" ) var update = flag.Bool("update", false, "update .golden files") -func TestServer_Run_Shutdown(t *testing.T) { - srv, err := New(handlers, WithQueryMetrics{}) - require.NoError(t, err) - r := prometheus.NewRegistry() - r.MustRegister(srv) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - err2 := srv.Serve() - assert.True(t, errors.Is(err2, http.ErrServerClosed)) - wg.Done() - }() - - assert.Eventually(t, func() bool { - var resp *http.Response - resp, err = http.Get(fmt.Sprintf("http://localhost:%d/", srv.httpServer.GetPort())) - if err != nil { - return false - } - _ = resp.Body.Close() - return resp.StatusCode == http.StatusOK - }, time.Second, time.Millisecond) - - var resp *http.Response - resp, err = http.Post(fmt.Sprintf("http://localhost:%d/search", srv.httpServer.GetPort()), "", nil) - require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - _ = resp.Body.Close() +func TestNewRouter(t *testing.T) { + r := simplejson.New(nil) - err = srv.Shutdown(5 * time.Second) - require.NoError(t, err) - wg.Wait() + for _, path := range []string{"/"} { + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, path, nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + } + + for _, path := range []string{"/search", "/query", "/annotations", "/tag-keys", "/tag-values"} { + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodPost, path, nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + } + + for _, path := range []string{"/annotations"} { + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodOptions, path, nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + } } -func TestServer_Metrics(t *testing.T) { - srv, err := New(handlers, WithQueryMetrics{}) - require.NoError(t, err) - r := prometheus.NewRegistry() - r.MustRegister(srv) +func TestNewRouter_Extend(t *testing.T) { + r := simplejson.New(nil) + + r.Post("/test", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("Hello")) + }) - req, _ := http.NewRequest(http.MethodPost, "/query", bytes.NewBufferString(`{ "targets": [ { "target": "A", "type": "table" } ] }`)) w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodPost, "/test", nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "Hello", w.Body.String()) +} + +func TestNewRouter_PrometheusMetrics(t *testing.T) { + r := simplejson.New(nil, simplejson.WithHTTPMetrics{Option: middleware.PrometheusMetricsOptions{ + Namespace: "foo", + Subsystem: "bar", + Application: "snafu", + }}) + + reg := prometheus.NewPedanticRegistry() + reg.MustRegister(r) - srv.httpServer.ServeHTTP(w, req) - require.Equal(t, http.StatusOK, w.Code) + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodPost, "/search", nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) - metrics, err := r.Gather() + n, err := testutil.GatherAndCount(reg) require.NoError(t, err) - require.Len(t, metrics, 1) - for _, metric := range metrics { - require.Len(t, metric.GetMetric(), 1) - switch metric.GetName() { - case "simplejson_query_duration_seconds": - assert.Equal(t, uint64(1), metric.GetMetric()[0].Histogram.GetSampleCount()) - default: - t.Fatalf("unexpected metric: %s", metric.GetName()) - } - } + assert.Equal(t, 2, n) } -/* -func TestServer_Metrics(t *testing.T) { - r := s.GetRouter() +func TestNewRouter_QueryMetrics(t *testing.T) { + r := simplejson.New(handlers, simplejson.WithQueryMetrics{}) + + reg := prometheus.NewPedanticRegistry() + reg.MustRegister(r) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/", nil) + req, _ := http.NewRequest(http.MethodPost, "/query", bytes.NewBufferString(`{ "targets": [ { "target": "A" } ] }`)) r.ServeHTTP(w, req) + if !assert.Equal(t, http.StatusOK, w.Code) { + t.Log(w.Body) + } - require.Equal(t, http.StatusOK, w.Code) - - m, err := prometheus.DefaultGatherer.Gather() + n, err := testutil.GatherAndCount(reg) require.NoError(t, err) - var found bool - for _, entry := range m { - if *entry.Name == "simplejson_request_duration_seconds" { - require.Equal(t, pcg.MetricType_SUMMARY, *entry.Type) - require.NotZero(t, entry.Metric) - assert.NotZero(t, entry.Metric[0].Summary.GetSampleCount()) - found = true - break - } - } - assert.True(t, found) + assert.Equal(t, 1, n) } -*/ // // @@ -113,19 +104,19 @@ func TestServer_Metrics(t *testing.T) { type testHandler struct { noEndpoints bool - queryResponse Response - annotations []Annotation + queryResponse simplejson.Response + annotations []simplejson.Annotation tags []string tagValues map[string][]string } -var _ Handler = &testHandler{} +var _ simplejson.Handler = &testHandler{} var ( - queryResponses = map[string]*TimeSeriesResponse{ + queryResponses = map[string]*simplejson.TimeSeriesResponse{ "A": { Target: "A", - DataPoints: []DataPoint{ + DataPoints: []simplejson.DataPoint{ {Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Value: 100}, {Timestamp: time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC), Value: 101}, {Timestamp: time.Date(2020, 1, 1, 0, 2, 0, 0, time.UTC), Value: 103}, @@ -133,7 +124,7 @@ var ( }, "B": { Target: "B", - DataPoints: []DataPoint{ + DataPoints: []simplejson.DataPoint{ {Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Value: 100}, {Timestamp: time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC), Value: 99}, {Timestamp: time.Date(2020, 1, 1, 0, 2, 0, 0, time.UTC), Value: 98}, @@ -141,21 +132,21 @@ var ( }, } - tableQueryResponse = map[string]*TableResponse{ + tableQueryResponse = map[string]*simplejson.TableResponse{ "C": { - Columns: []Column{ - {Text: "Time", Data: TimeColumn{ + Columns: []simplejson.Column{ + {Text: "Time", Data: simplejson.TimeColumn{ time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC), }}, - {Text: "Label", Data: StringColumn{"foo", "bar"}}, - {Text: "Series A", Data: NumberColumn{42, 43}}, - {Text: "Series B", Data: NumberColumn{64.5, 100.0}}, + {Text: "Label", Data: simplejson.StringColumn{"foo", "bar"}}, + {Text: "Series A", Data: simplejson.NumberColumn{42, 43}}, + {Text: "Series B", Data: simplejson.NumberColumn{64.5, 100.0}}, }, }, } - annotations = []Annotation{{ + annotations = []simplejson.Annotation{{ Time: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), Title: "foo", Text: "bar", @@ -169,7 +160,7 @@ var ( "bar": {"1", "2"}, } - handlers = map[string]Handler{ + handlers = map[string]simplejson.Handler{ "A": &testHandler{ queryResponse: queryResponses["A"], annotations: annotations, @@ -184,10 +175,10 @@ var ( }, } - s, _ = New(handlers) + s = simplejson.New(handlers) ) -func (handler *testHandler) Endpoints() (endpoints Endpoints) { +func (handler *testHandler) Endpoints() (endpoints simplejson.Endpoints) { if handler.noEndpoints { return } @@ -206,11 +197,11 @@ func (handler *testHandler) Endpoints() (endpoints Endpoints) { return } -func (handler *testHandler) Query(_ context.Context, _ QueryRequest) (response Response, err error) { +func (handler *testHandler) Query(_ context.Context, _ simplejson.QueryRequest) (response simplejson.Response, err error) { return handler.queryResponse, nil } -func (handler *testHandler) Annotations(_ AnnotationRequest) (annotations []Annotation, err error) { +func (handler *testHandler) Annotations(_ simplejson.AnnotationRequest) (annotations []simplejson.Annotation, err error) { return handler.annotations, nil }