Skip to content

Commit a45e11c

Browse files
authored
Merge pull request #183 from Nexucis/feature/refactor-rest
refactor the rest package introducing middleware concept and the usage of an http router
2 parents bf5821a + ad4466e commit a45e11c

File tree

7 files changed

+453
-353
lines changed

7 files changed

+453
-353
lines changed

cmd/promql-langserver/promql-langserver.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
kitlog "github.com/go-kit/kit/log"
2626
"github.com/prometheus-community/promql-langserver/config"
2727
promClient "github.com/prometheus-community/promql-langserver/prometheus"
28+
"github.com/prometheus/common/route"
2829

2930
"github.com/prometheus-community/promql-langserver/langserver"
3031
"github.com/prometheus-community/promql-langserver/rest"
@@ -59,13 +60,17 @@ func main() {
5960
}
6061

6162
logger = kitlog.NewSyncLogger(logger)
62-
63-
handler, err := rest.CreateInstHandler(context.Background(), prometheusClient, logger)
63+
// create the http router
64+
router := route.New()
65+
// create the api
66+
api, err := rest.NewLangServerAPI(context.Background(), prometheusClient, logger, true)
6467
if err != nil {
6568
log.Fatal(err)
6669
}
67-
68-
err = http.ListenAndServe(fmt.Sprint(":", conf.RESTAPIPort), handler)
70+
// register the different route
71+
api.Register(router, "")
72+
// start the http server
73+
err = http.ListenAndServe(fmt.Sprint(":", conf.RESTAPIPort), router)
6974
if err != nil {
7075
log.Fatal(err)
7176
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.13
55
require (
66
github.com/blang/semver v3.5.1+incompatible
77
github.com/go-kit/kit v0.10.0
8+
github.com/google/uuid v1.1.1
89
github.com/kelseyhightower/envconfig v1.4.0
910
github.com/pkg/errors v0.9.1
1011
github.com/prometheus/client_golang v1.7.1

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K
438438
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
439439
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
440440
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
441+
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
441442
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
442443
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
443444
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=

rest/api.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright 2020 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License. // You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
package rest
13+
14+
import (
15+
"context"
16+
"encoding/json"
17+
"fmt"
18+
"net/http"
19+
20+
"github.com/go-kit/kit/log"
21+
"github.com/pkg/errors"
22+
"github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol"
23+
"github.com/prometheus-community/promql-langserver/langserver"
24+
promClient "github.com/prometheus-community/promql-langserver/prometheus"
25+
"github.com/prometheus/client_golang/prometheus"
26+
"github.com/prometheus/client_golang/prometheus/promhttp"
27+
"github.com/prometheus/common/route"
28+
)
29+
30+
func respondJSON(w http.ResponseWriter, content interface{}) {
31+
encoder := json.NewEncoder(w)
32+
33+
err := encoder.Encode(content)
34+
if err != nil {
35+
http.Error(w, errors.Wrapf(err, "failed to write response").Error(), http.StatusInternalServerError)
36+
}
37+
}
38+
39+
type lspData struct {
40+
// Expr is the PromQL expression and is required for all endpoints available.
41+
Expr string `json:"expr"`
42+
// Limit is the maximum number of results returned to the client. It will be used for autocompletion and diagnostics.
43+
Limit *uint64 `json:"limit,omitempty"`
44+
// PositionLine is the number of the line for which the metadata is queried.
45+
PositionLine *float64 `json:"positionLine,omitempty"`
46+
// PositionChar for which the metadata is queried. Characters are counted as UTF-16 code points.
47+
PositionChar *float64 `json:"positionChar,omitempty"`
48+
}
49+
50+
func (d *lspData) UnmarshalJSON(data []byte) error {
51+
type plain lspData
52+
if err := json.Unmarshal(data, (*plain)(d)); err != nil {
53+
return err
54+
}
55+
if len(d.Expr) == 0 {
56+
return fmt.Errorf("PromQL expression is not specified")
57+
}
58+
return nil
59+
}
60+
61+
func (d *lspData) position() (protocol.Position, error) {
62+
if d.PositionLine == nil {
63+
return protocol.Position{}, errors.New("positionLine is not specified")
64+
}
65+
if d.PositionChar == nil {
66+
return protocol.Position{}, errors.New("positionChar is not specified")
67+
}
68+
return protocol.Position{
69+
Line: *d.PositionLine,
70+
Character: *d.PositionChar,
71+
}, nil
72+
}
73+
74+
// API is the struct that manages the different endpoints provided by the language server REST API.
75+
// It also takes care of creating all necessary HTTP middleware.
76+
type API struct {
77+
langServer langserver.HeadlessServer
78+
mdws []middlewareFunc
79+
enableMetrics bool
80+
}
81+
82+
// NewLangServerAPI creates a new instance of the Stateless API to use the LangServer through HTTP.
83+
//
84+
// If metadata is fetched from a remote Prometheus, the metadataService
85+
// implementation from the promql-langserver/prometheus package can be used,
86+
// otherwise you need to provide your own implementation of the interface.
87+
//
88+
// The provided Logger should be synchronized.
89+
//
90+
// In case "enableMetrics" is set to true, endpoint /metrics is then available and a middleware that instruments the different endpoints provided is instantiated.
91+
// Don't use it in case you already have such middleware in place.
92+
func NewLangServerAPI(ctx context.Context, metadataService promClient.MetadataService, logger log.Logger, enableMetrics bool) (*API, error) {
93+
lgs, err := langserver.CreateHeadlessServer(ctx, metadataService, logger)
94+
if err != nil {
95+
return nil, err
96+
}
97+
mdws := []middlewareFunc{manageDocumentMiddleware(lgs)}
98+
if enableMetrics {
99+
apiMetric := newAPIMetrics()
100+
prometheus.MustRegister(apiMetric)
101+
mdws = append(mdws, apiMetric.instrumentHTTPRequest)
102+
}
103+
return &API{
104+
langServer: lgs,
105+
mdws: mdws,
106+
enableMetrics: enableMetrics,
107+
}, nil
108+
}
109+
110+
// Register the API's endpoints in the given router.
111+
func (a *API) Register(r *route.Router, prefix string) {
112+
r.Post(prefix+"/diagnostics", a.handle(a.diagnostics))
113+
r.Post(prefix+"/completion", a.handle(a.completion))
114+
r.Post(prefix+"/hover", a.handle(a.hover))
115+
r.Post(prefix+"/signatureHelp", a.handle(a.signature))
116+
if a.enableMetrics {
117+
r.Get("/metrics", promhttp.Handler().ServeHTTP)
118+
}
119+
}
120+
121+
func (a *API) handle(h http.HandlerFunc) http.HandlerFunc {
122+
endpoint := h
123+
for _, mdw := range a.mdws {
124+
endpoint = mdw(endpoint)
125+
}
126+
return endpoint
127+
}
128+
129+
func (a *API) diagnostics(w http.ResponseWriter, r *http.Request) {
130+
ctx := r.Context()
131+
requestID, requestData, err := getRequestDataAndID(ctx)
132+
if err != nil {
133+
http.Error(w, err.Error(), http.StatusInternalServerError)
134+
return
135+
}
136+
137+
diagnostics, err := a.langServer.GetDiagnostics(requestID)
138+
if err != nil {
139+
http.Error(w, errors.Wrapf(err, "failed to get diagnostics").Error(), http.StatusInternalServerError)
140+
return
141+
}
142+
143+
items := diagnostics.Diagnostics
144+
limit := requestData.Limit
145+
if limit != nil && uint64(len(items)) > *limit {
146+
items = items[:*limit]
147+
}
148+
149+
respondJSON(w, items)
150+
}
151+
152+
func (a *API) hover(w http.ResponseWriter, r *http.Request) {
153+
ctx := r.Context()
154+
requestID, requestData, err := getRequestDataAndID(ctx)
155+
if err != nil {
156+
http.Error(w, err.Error(), http.StatusInternalServerError)
157+
return
158+
}
159+
position, err := requestData.position()
160+
if err != nil {
161+
http.Error(w, err.Error(), http.StatusBadRequest)
162+
return
163+
}
164+
165+
hover, err := a.langServer.Hover(r.Context(), &protocol.HoverParams{
166+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
167+
TextDocument: protocol.TextDocumentIdentifier{
168+
URI: requestID,
169+
},
170+
Position: position,
171+
},
172+
})
173+
if err != nil {
174+
http.Error(w, errors.Wrapf(err, "failed to get hover info").Error(), http.StatusInternalServerError)
175+
return
176+
}
177+
178+
respondJSON(w, hover)
179+
}
180+
181+
func (a *API) completion(w http.ResponseWriter, r *http.Request) {
182+
ctx := r.Context()
183+
requestID, requestData, err := getRequestDataAndID(ctx)
184+
if err != nil {
185+
http.Error(w, err.Error(), http.StatusInternalServerError)
186+
return
187+
}
188+
position, err := requestData.position()
189+
if err != nil {
190+
http.Error(w, err.Error(), http.StatusBadRequest)
191+
return
192+
}
193+
194+
completion, err := a.langServer.Completion(r.Context(), &protocol.CompletionParams{
195+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
196+
TextDocument: protocol.TextDocumentIdentifier{
197+
URI: requestID,
198+
},
199+
Position: position,
200+
},
201+
})
202+
if err != nil {
203+
http.Error(w, errors.Wrapf(err, "failed to get completion info").Error(), 500)
204+
return
205+
}
206+
207+
items := completion.Items
208+
limit := requestData.Limit
209+
if limit != nil && uint64(len(items)) > *limit {
210+
items = items[:*limit]
211+
}
212+
213+
respondJSON(w, items)
214+
}
215+
216+
func (a *API) signature(w http.ResponseWriter, r *http.Request) {
217+
ctx := r.Context()
218+
requestID, requestData, err := getRequestDataAndID(ctx)
219+
if err != nil {
220+
http.Error(w, err.Error(), http.StatusInternalServerError)
221+
return
222+
}
223+
position, err := requestData.position()
224+
if err != nil {
225+
http.Error(w, err.Error(), http.StatusBadRequest)
226+
return
227+
}
228+
229+
signature, err := a.langServer.SignatureHelp(r.Context(), &protocol.SignatureHelpParams{
230+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
231+
TextDocument: protocol.TextDocumentIdentifier{
232+
URI: requestID,
233+
},
234+
Position: position,
235+
},
236+
})
237+
if err != nil {
238+
http.Error(w, errors.Wrapf(err, "failed to get hover info").Error(), 500)
239+
return
240+
}
241+
242+
respondJSON(w, signature)
243+
}

rest/doc.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ Supported endpoints:
2323
/hover
2424
/signatureHelp
2525
26-
URL Query Parameters:
26+
All endpoints are only available through the HTTP method POST. For each request, you have to provide the following JSON:
2727
28-
expr : A PromQL expression.
29-
limit : (optional, only for /diagnostics and /completion endpoints) The maximum number of diagnostic messages returned.
30-
line : (only for /signatureHelp, /hover and /completion endpoints) The line (0 based) for which the metadata is queried.
31-
char : (only for /signatureHelp, /hover and /completion endpoints) The column (0 based) for which the metadata is queried. Characters are counted as UTF16 Codepoints.
28+
{
29+
"expr": "a PromQL expression" # Mandatory for all available endpoints
30+
"limit": 45 # Optional. It will be used only for the endpoints /diagnostics and /completion. It's the maximum number of results returned.
31+
"positionLine": 0 # Mandatory for the endpoints /signatureHelp, /hover and /completion. The line (0 based) for which the metadata is queried.
32+
"positionChar": 2 # Mandatory for the endpoints /signatureHelp, /hover and /completion. The column (0 based) for which the metadata is queried. Characters are counted as UTF-16 code points.
33+
}
3234
3335
3436
Examples:
3537
36-
$ curl 'localhost:8080/diagnostics?expr=some_metric()&limit=100'|jq
38+
$ curl -XPOST 'localhost:8080/diagnostics' -H "Content-Type: application/json" --data '{"expr": "some_metric()", "limit":100}'|jq
3739
[
3840
{
3941
"range": {
@@ -53,7 +55,7 @@ Examples:
5355
]
5456
5557
56-
$ curl 'localhost:8080/completion?expr=sum(go)&line=0&char=6&limit=2'|jq
58+
$ curl -XPOST 'localhost:8080/completion' -H "Content-Type: application/json" --data '{"expr": "sum(go)", "limit":2, "positionLine":0, "positionChar":6}'|jq
5759
[
5860
{
5961
"label": "go_gc_duration_seconds",

0 commit comments

Comments
 (0)