From b95da1d5d027fe1e74e577bcedef2bab136ff52c Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 15 Dec 2020 15:01:22 -0500 Subject: [PATCH 1/6] Suggest label name and values based on query selection context --- go.mod | 1 + go.sum | 2 + langserver/completion.go | 67 +++- langserver/langserver_test.go | 663 ++++++++++++++++++++++++--------- prometheus/compatible.go | 55 ++- prometheus/empty.go | 4 +- prometheus/metadata_service.go | 29 +- prometheus/not_compatible.go | 102 ++++- 8 files changed, 695 insertions(+), 228 deletions(-) diff --git a/go.mod b/go.mod index 049af2c3..45faded4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/prometheus/prometheus v1.8.2-0.20200507164740-ecee9c8abfd1 github.com/rakyll/statik v0.1.7 github.com/sahilm/fuzzy v0.1.0 + github.com/stretchr/testify v1.5.1 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) diff --git a/go.sum b/go.sum index 89a3c532..d999e4d9 100644 --- a/go.sum +++ b/go.sum @@ -637,6 +637,7 @@ github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULU github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/prometheus v1.8.2-0.20200507164740-ecee9c8abfd1 h1:Oh/bmW9DXCbMeAZbxMmt2wuY6Q4cD0IIbR6vJP3kdHg= github.com/prometheus/prometheus v1.8.2-0.20200507164740-ecee9c8abfd1/go.mod h1:S5n0C6tSgdnwWshBUceRx5G1OsjLv/EeZ9t3wIfEtsY= +github.com/prometheus/prometheus v2.5.0+incompatible h1:7QPitgO2kOFG8ecuRn9O/4L9+10He72rVRJvMXrE9Hg= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -688,6 +689,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/langserver/completion.go b/langserver/completion.go index 63472818..87dd79ea 100644 --- a/langserver/completion.go +++ b/langserver/completion.go @@ -22,9 +22,9 @@ import ( "strings" "github.com/pkg/errors" - "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol" "github.com/prometheus-community/promql-langserver/langserver/cache" + "github.com/prometheus/common/model" promql "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/util/strutil" "github.com/sahilm/fuzzy" @@ -273,10 +273,7 @@ func (s *server) completeLabels(ctx context.Context, completions *[]protocol.Com } } - vs, ok := location.Node.(*promql.VectorSelector) - if !ok { - vs = nil - } + vs, _ := depthFirstVectorSelector(location.Node) item.Pos += offset @@ -294,24 +291,50 @@ func (s *server) completeLabels(ctx context.Context, completions *[]protocol.Com if isValue && lastLabel != "" { loc.Node = &item - return s.completeLabelValue(ctx, completions, &loc, lastLabel) + return s.completeLabelValue(ctx, completions, &loc, lastLabel, vs) } if item.Typ == promql.EQL || item.Typ == promql.NEQ { loc.Node = &promql.Item{Pos: item.Pos + promql.Pos(len(item.Val))} - return s.completeLabelValue(ctx, completions, &loc, lastLabel) + return s.completeLabelValue(ctx, completions, &loc, lastLabel, vs) } return nil } -func (s *server) completeLabel(ctx context.Context, completions *[]protocol.CompletionItem, location *cache.Location, vs *promql.VectorSelector) error { - metricName := "" +func depthFirstVectorSelector(node promql.Node) (*promql.VectorSelector, bool) { + if vs, ok := node.(*promql.VectorSelector); ok { + return vs, true + } + + for _, child := range promql.Children(node) { + if vs, ok := depthFirstVectorSelector(child); ok { + return vs, true + } + } + + return nil, false +} - if vs != nil { - metricName = vs.Name +func labelsSelectionFromVectorSelector(vs *promql.VectorSelector) model.LabelSet { + if vs == nil { + return nil } - allNames, err := s.metadataService.LabelNames(ctx, metricName) + selection := model.LabelSet{} + if vs.Name != "" { + selection[model.MetricNameLabel] = model.LabelValue(vs.Name) + } + for _, elem := range vs.LabelMatchers { + selection[model.LabelName(elem.Name)] = model.LabelValue(elem.Value) + } + return selection +} + +func (s *server) completeLabel(ctx context.Context, completions *[]protocol.CompletionItem, location *cache.Location, vs *promql.VectorSelector) error { + labelName := location.Node.(*promql.Item).Val + + selection := labelsSelectionFromVectorSelector(vs) + allNames, err := s.metadataService.LabelNames(ctx, selection) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ @@ -326,7 +349,6 @@ func (s *server) completeLabel(ctx context.Context, completions *[]protocol.Comp return err } - labelName := location.Node.(*promql.Item).Val OUTER: for _, match := range getMatches(labelName, allNames) { // Skip labels that already have matchers @@ -353,8 +375,23 @@ OUTER: } // nolint: funlen -func (s *server) completeLabelValue(ctx context.Context, completions *[]protocol.CompletionItem, location *cache.Location, labelName string) error { - labelValues, err := s.metadataService.LabelValues(ctx, labelName) +func (s *server) completeLabelValue( + ctx context.Context, + completions *[]protocol.CompletionItem, + location *cache.Location, + labelName string, + vs *promql.VectorSelector, +) error { + // Current selection from selector if not nil. + selection := labelsSelectionFromVectorSelector(vs) + + // Delete the current label from selection, it is incomplete and we are + // trying to complete values for it. + if _, ok := selection[model.LabelName(labelName)]; ok { + delete(selection, model.LabelName(labelName)) + } + + labelValues, err := s.metadataService.LabelValues(ctx, labelName, selection) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ diff --git a/langserver/langserver_test.go b/langserver/langserver_test.go index 939a5235..111ee27b 100644 --- a/langserver/langserver_test.go +++ b/langserver/langserver_test.go @@ -15,222 +15,231 @@ package langserver import ( "context" + "encoding/json" "fmt" + "net/http" + "net/http/httptest" + "runtime" + "sort" "strings" "testing" + "time" "github.com/prometheus-community/promql-langserver/config" "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/jsonrpc2" "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol" + "github.com/prometheus-community/promql-langserver/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestNotImplemented checks whether unimplemented functions return the approbiate Error. -func TestNotImplemented(*testing.T) { // nolint: gocognit, funlen, gocyclo +func TestNotImplemented(t *testing.T) { // nolint: gocognit, funlen, gocyclo s := &server{} err := s.DidChangeWorkspaceFolders(context.Background(), &protocol.DidChangeWorkspaceFoldersParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.DidSave(context.Background(), &protocol.DidSaveTextDocumentParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.WillSave(context.Background(), &protocol.WillSaveTextDocumentParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.DidChangeWatchedFiles(context.Background(), &protocol.DidChangeWatchedFilesParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.Progress(context.Background(), &protocol.ProgressParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.SelectionRange(context.Background(), &protocol.SelectionRangeParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.SetTraceNotification(context.Background(), &protocol.SetTraceParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.LogTraceNotification(context.Background(), &protocol.LogTraceParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Implementation(context.Background(), &protocol.ImplementationParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.TypeDefinition(context.Background(), &protocol.TypeDefinitionParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.DocumentColor(context.Background(), &protocol.DocumentColorParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.ColorPresentation(context.Background(), &protocol.ColorPresentationParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.FoldingRange(context.Background(), &protocol.FoldingRangeParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.NonstandardRequest(context.Background(), "", nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Declaration(context.Background(), &protocol.DeclarationParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.WillSaveWaitUntil(context.Background(), &protocol.WillSaveTextDocumentParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Resolve(context.Background(), &protocol.CompletionItem{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Definition(context.Background(), &protocol.DefinitionParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.References(context.Background(), &protocol.ReferenceParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.DocumentHighlight(context.Background(), &protocol.DocumentHighlightParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.DocumentSymbol(context.Background(), &protocol.DocumentSymbolParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.CodeAction(context.Background(), &protocol.CodeActionParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Symbol(context.Background(), &protocol.WorkspaceSymbolParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.CodeLens(context.Background(), &protocol.CodeLensParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.ResolveCodeLens(context.Background(), &protocol.CodeLens{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Formatting(context.Background(), &protocol.DocumentFormattingParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.RangeFormatting(context.Background(), &protocol.DocumentRangeFormattingParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.OnTypeFormatting(context.Background(), &protocol.DocumentOnTypeFormattingParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.Rename(context.Background(), &protocol.RenameParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.PrepareRename(context.Background(), &protocol.PrepareRenameParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.DocumentLink(context.Background(), &protocol.DocumentLinkParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.ResolveDocumentLink(context.Background(), &protocol.DocumentLink{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.ExecuteCommand(context.Background(), &protocol.ExecuteCommandParams{}) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.IncomingCalls(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.OutgoingCalls(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.PrepareCallHierarchy(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.SemanticTokens(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.SemanticTokensEdits(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } _, err = s.SemanticTokensRange(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.WorkDoneProgressCancel(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } err = s.WorkDoneProgressCreate(context.Background(), nil) if err != nil && err.(*jsonrpc2.Error).Code != jsonrpc2.CodeMethodNotFound { - panic("Expected a jsonrpc2 Error with CodeMethodNotFound") + require.Fail(t, "Expected a jsonrpc2 Error with CodeMethodNotFound") } } @@ -266,50 +275,45 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo var stream jsonrpc2.Stream = &dummyStream{} stream = jSONLogStream(stream, &dummyWriter{}) _, server := ServerFromStream(context.Background(), stream, &config.Config{LogFormat: config.TextFormat}) - s := server.server + s := mustServerTest(t, server.server) // Initialize Server - _, err := s.Initialize(context.Background(), &protocol.ParamInitialize{}) + _, err := s.server.Initialize(context.Background(), &protocol.ParamInitialize{}) if err != nil { - panic("Failed to initialize Server") + require.Fail(t, "Failed to initialize Server") } - _, err = s.Initialize(context.Background(), &protocol.ParamInitialize{}) + _, err = s.server.Initialize(context.Background(), &protocol.ParamInitialize{}) if err == nil { - panic("cannot initialize server twice") + require.Fail(t, "cannot initialize server twice") } // Confirm Initialisation - err = s.Initialized(context.Background(), &protocol.InitializedParams{}) + err = s.server.Initialized(context.Background(), &protocol.InitializedParams{}) if err != nil { - panic("Failed to initialize Server") + require.Fail(t, "Failed to initialize Server") } - err = s.Initialized(context.Background(), &protocol.InitializedParams{}) + err = s.server.Initialized(context.Background(), &protocol.InitializedParams{}) if err == nil { - panic("cannot confirm server initialisation twice") + require.Fail(t, "cannot confirm server initialisation twice") } // Add a document to the server - err = s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ + err = s.server.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ - URI: "test.promql", + URI: s.doc.DocumentURI(), + Version: s.doc.NextVersion(), LanguageID: "promql", - Version: 0, Text: "", }, }) if err != nil { - panic("Failed to open document") + require.Fail(t, "Failed to open document") } // Apply a Full Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 2, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: nil, @@ -319,14 +323,12 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }, }) if err != nil { - panic("Failed to apply full change to document") + require.Fail(t, "Failed to apply full change to document") } - hover, err := s.Hover(context.Background(), &protocol.HoverParams{ + hover, err := s.server.Hover(context.Background(), &protocol.HoverParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, Character: 1.0, @@ -335,22 +337,17 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }) if err != nil { - panic("Failed to get hovertext") + require.Fail(t, "Failed to get hovertext") } if hover == nil || strings.Contains("sum", hover.Contents.Value) { fmt.Println(hover) - panic("unexpected or no hovertext") + require.Fail(t, "unexpected or no hovertext") } // Apply a Full Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 3, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: nil, @@ -360,14 +357,12 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }, }) if err != nil { - panic("Failed to apply full change to document") + require.Fail(t, "Failed to apply full change to document") } - hover, err = s.Hover(context.Background(), &protocol.HoverParams{ + hover, err = s.server.Hover(context.Background(), &protocol.HoverParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, Character: 1.0, @@ -376,21 +371,16 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }) if err != nil { - panic("Failed to get hovertext") + require.Fail(t, "Failed to get hovertext") } if hover == nil || strings.Contains("metric_name", hover.Contents.Value) { fmt.Println(hover) - panic("unexpected or no hovertext") + require.Fail(t, "unexpected or no hovertext") } // Apply a partial Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 4, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: &protocol.Range{ @@ -413,23 +403,18 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo } // Wait for diagnostics - doc, err := s.cache.GetDocument("test.promql") + doc, err := s.server.cache.GetDocument("test.promql") if err != nil { - panic("Failed to get document") + require.Fail(t, "Failed to get document") } if diagnostics, err := doc.GetDiagnostics(); err != nil && len(diagnostics) != 0 { - panic("expected nonempty diagnostics") + require.Fail(t, "expected nonempty diagnostics") } // Apply a partial Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 5, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: &protocol.Range{ @@ -452,20 +437,20 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo } // Wait for diagnostics - doc, err = s.cache.GetDocument("test.promql") + doc, err = s.server.cache.GetDocument("test.promql") if err != nil { - panic("Failed to get document") + require.Fail(t, "Failed to get document") } if diagnostics, err := doc.GetDiagnostics(); err != nil && len(diagnostics) != 0 { - panic("expected empty diagnostics") + require.Fail(t, "expected empty diagnostics") } var content string content, err = doc.GetContent() if err != nil { - panic("failed to get document content") + require.Fail(t, "failed to get document content") } if content != "rate(metric)" { @@ -473,13 +458,8 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo } // Apply a Full Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 6, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: nil, @@ -489,14 +469,12 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }, }) if err != nil { - panic("Failed to apply full change to document") + require.Fail(t, "Failed to apply full change to document") } - completion, err := s.Completion(context.Background(), &protocol.CompletionParams{ + completion, err := s.server.Completion(context.Background(), &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, Character: 1.0, @@ -506,17 +484,12 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo if err != nil || completion == nil || len(completion.Items) == 0 || completion.Items[0].Label != "rate" { fmt.Println(completion) - panic("Failed to get completion") + require.Fail(t, "Failed to get completion") } // Apply a Full Change to the document - err = s.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: 7, - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, - }, + err = s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), ContentChanges: []protocol.TextDocumentContentChangeEvent{ { Range: nil, @@ -526,14 +499,12 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }, }) if err != nil { - panic("Failed to apply full change to document") + require.Fail(t, "Failed to apply full change to document") } - completion, err = s.Completion(context.Background(), &protocol.CompletionParams{ + completion, err = s.server.Completion(context.Background(), &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, Character: 1.0, @@ -543,52 +514,51 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo if err != nil || completion == nil || len(completion.Items) == 0 || completion.Items[0].Label != "rate" { fmt.Println(completion) - panic("Failed to get completion") + require.Fail(t, "Failed to get completion") } // Close a document - err = s.DidClose(context.Background(), &protocol.DidCloseTextDocumentParams{ + err = s.server.DidClose(context.Background(), &protocol.DidCloseTextDocumentParams{ TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", + URI: s.doc.DocumentURI(), }, }) if err != nil { - panic("Failed to close document") + require.Fail(t, "Failed to close document") } - _, err = s.cache.GetDocument("test.promql") + _, err = s.server.cache.GetDocument("test.promql") if err == nil { - panic("getting a closed document should have failed") + require.Fail(t, "getting a closed document should have failed") } // Close a document twice - err = s.DidClose(context.Background(), &protocol.DidCloseTextDocumentParams{ + err = s.server.DidClose(context.Background(), &protocol.DidCloseTextDocumentParams{ TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", + URI: s.doc.DocumentURI(), }, }) if err == nil { - panic("should have failed to close document") + require.Fail(t, "should have failed to close document") } // Reopen a closed document - err = s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ + s.doc.ResetVersion() + err = s.server.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ - URI: "test.promql", + URI: s.doc.DocumentURI(), + Version: s.doc.NextVersion(), LanguageID: "promql", - Version: 0, Text: "abs()", }, }) if err != nil { - panic("Failed to reopen document") + require.Fail(t, "Failed to reopen document") } - signature, err := s.SignatureHelp(context.Background(), &protocol.SignatureHelpParams{ + signature, err := s.server.SignatureHelp(context.Background(), &protocol.SignatureHelpParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 1.0, Character: 0.0, @@ -597,19 +567,17 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }) if err != nil { - panic("Failed to get signature") + require.Fail(t, "Failed to get signature") } if signature != nil && len(signature.Signatures) != 0 { fmt.Println(signature) - panic("Wrong number of signatures returned") + require.Fail(t, "Wrong number of signatures returned") } - signature, err = s.SignatureHelp(context.Background(), &protocol.SignatureHelpParams{ + signature, err = s.server.SignatureHelp(context.Background(), &protocol.SignatureHelpParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0, Character: 4, @@ -618,19 +586,17 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }) if err != nil { - panic("Failed to get signature") + require.Fail(t, "Failed to get signature") } if signature == nil || len(signature.Signatures) != 1 { fmt.Println(signature.Signatures) - panic("Wrong number of signatures returned") + require.Fail(t, "Wrong number of signatures returned") } - hover, err = s.Hover(context.Background(), &protocol.HoverParams{ + hover, err = s.server.Hover(context.Background(), &protocol.HoverParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: "test.promql", - }, + TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, Character: 1.0, @@ -639,28 +605,391 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo }) if err != nil { - panic("Failed to get hovertext") + require.Fail(t, "Failed to get hovertext") } if hover == nil || strings.Contains("abs", hover.Contents.Value) { fmt.Println(hover) - panic("unexpected or no hovertext") - } + require.Fail(t, "unexpected or no hovertext") + } + + // Run completion metadata tests. + t.Run("completion label name: sum(metric_name{})", func(t *testing.T) { + // Apply a Full Change to the document. + err := s.server.DidChange(context.Background(), + &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), + ContentChanges: []protocol.TextDocumentContentChangeEvent{ + { + Range: nil, + RangeLength: 0, + Text: "sum(metric_name{})", + }, + }, + }) + require.NoError(t, err, "Failed to apply full change to document") + + // Simulate completions for metric_name. + metaServer := s.SetupTestMetaServer() + defer metaServer.TearDown() + + metaServer.HandleFunc("/api/v1/series", + func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.ParseForm()) + assert.Equal(t, []string{"metric_name"}, r.Form["match[]"]) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "data": []interface{}{ + map[string]string{ + "foo_name": "foo_value", + "bar_name": "bar_value", + }, + }, + }) + }) + + completion, err := s.server.Completion(context.Background(), + &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: s.doc.ID(), + Position: protocol.Position{ + Line: 0.0, + Character: 16.0, + }, + }, + }) + require.NoError(t, err, "Failed to get completion") + expected := []string{ + "bar_name", + "foo_name", + } + actual := completionValuesSorted(t, completion) + require.Equal(t, expected, actual) + }) + + t.Run("completion label name: sum(metric_name{foo_name=\"foo_value\",})", func(t *testing.T) { + // Apply a Full Change to the document. + err := s.server.DidChange(context.Background(), + &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), + ContentChanges: []protocol.TextDocumentContentChangeEvent{ + { + Range: nil, + RangeLength: 0, + Text: "sum(metric_name{foo_name=\"foo_value\",})", + }, + }, + }) + require.NoError(t, err, "Failed to apply full change to document") + + // Simulate completions for metric_name. + metaServer := s.SetupTestMetaServer() + defer metaServer.TearDown() + + metaServer.HandleFunc("/api/v1/series", + func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.ParseForm()) + assert.Equal(t, []string{"metric_name{foo_name=\"foo_value\"}"}, r.Form["match[]"]) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "data": []interface{}{ + map[string]string{ + "foo_name": "foo_value", + "bar_name": "bar_value", + }, + map[string]string{ + "foo_name": "foo_value", + "baz_name": "baz_value", + }, + }, + }) + }) + + completion, err := s.server.Completion(context.Background(), + &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: s.doc.ID(), + Position: protocol.Position{ + Line: 0.0, + Character: 37.0, + }, + }, + }) + require.NoError(t, err, "Failed to get completion") + expected := []string{ + "bar_name", + "baz_name", + } + actual := completionValuesSorted(t, completion) + require.Equal(t, expected, actual) + }) + + t.Run("completion label name: sum(metric_name{foo_name=\"foo_value\",baz_name=\"\"})", func(t *testing.T) { + // Apply a Full Change to the document. + err := s.server.DidChange(context.Background(), + &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), + ContentChanges: []protocol.TextDocumentContentChangeEvent{ + { + Range: nil, + RangeLength: 0, + Text: "sum(metric_name{foo_name=\"foo_value\",baz_name=\"\"})", + }, + }, + }) + require.NoError(t, err, "Failed to apply full change to document") + + // Simulate completions for metric_name. + metaServer := s.SetupTestMetaServer() + defer metaServer.TearDown() + + metaServer.HandleFunc("/api/v1/series", + func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.ParseForm()) + assert.Equal(t, []string{"metric_name{foo_name=\"foo_value\"}"}, r.Form["match[]"]) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "data": []interface{}{ + map[string]string{ + "foo_name": "foo_value", + "bar_name": "bar_value", + }, + map[string]string{ + "foo_name": "foo_value", + "baz_name": "baz_value", + }, + map[string]string{ + "foo_name": "foo_value", + "baz_name": "baz_value2", + }, + }, + }) + }) + + completion, err := s.server.Completion(context.Background(), + &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: s.doc.ID(), + Position: protocol.Position{ + Line: 0.0, + Character: 48.0, + }, + }, + }) + require.NoError(t, err, "Failed to get completion") + expected := []string{ + "\"baz_value\"", + "\"baz_value2\"", + } + actual := completionValuesSorted(t, completion) + require.Equal(t, expected, actual) + }) + + t.Run("completion label name: sum(metric_name{foo_name=\"foo_value\"}) by ()", func(t *testing.T) { + // Apply a Full Change to the document. + err := s.server.DidChange(context.Background(), + &protocol.DidChangeTextDocumentParams{ + TextDocument: s.doc.NextVersionID(), + ContentChanges: []protocol.TextDocumentContentChangeEvent{ + { + Range: nil, + RangeLength: 0, + Text: "sum(metric_name{foo_name=\"foo_value\"}) by ()", + }, + }, + }) + require.NoError(t, err, "Failed to apply full change to document") + + // Simulate completions for metric_name. + metaServer := s.SetupTestMetaServer() + defer metaServer.TearDown() + + metaServer.HandleFunc("/api/v1/series", + func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.ParseForm()) + assert.Equal(t, []string{"metric_name{foo_name=\"foo_value\"}"}, r.Form["match[]"]) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "data": []interface{}{ + map[string]string{ + "foo_name": "foo_value", + "bar_name": "bar_value", + }, + map[string]string{ + "foo_name": "foo_value", + "baz_name": "baz_value", + }, + map[string]string{ + "foo_name": "foo_value", + "baz_name": "baz_value2", + }, + }, + }) + }) + + completion, err := s.server.Completion(context.Background(), + &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: s.doc.ID(), + Position: protocol.Position{ + Line: 0.0, + Character: 43.0, + }, + }, + }) + require.NoError(t, err, "Failed to get completion") + expected := []string{ + "bar_name", + "baz_name", + } + actual := completionValuesSorted(t, completion) + require.Equal(t, expected, actual) + }) // Shutdown Server - err = s.Shutdown(context.Background()) + err = s.server.Shutdown(context.Background()) if err != nil { - panic("Failed to initialize Server") + require.Fail(t, "Failed to initialize Server") } - err = s.Shutdown(context.Background()) + err = s.server.Shutdown(context.Background()) if err == nil { - panic("cannot shutdown server twice") + require.Fail(t, "cannot shutdown server twice") } // Left out until it does something else than calling os.Exit() // Confirm Shutdown - err = s.Exit(context.Background()) + err = s.server.Exit(context.Background()) if err != nil { - panic("Failed to initialize Server") + require.Fail(t, "Failed to initialize Server") } } + +func completionValuesSorted( + t *testing.T, + completions *protocol.CompletionList, +) []string { + require.NotNil(t, completions) + results := make([]string, 0, len(completions.Items)) + for _, item := range completions.Items { + results = append(results, item.Label) + } + sort.Strings(results) + return results +} + +type serverTest struct { + t *testing.T + server *server + doc *testDocument +} + +func mustServerTest(t *testing.T, server *server) *serverTest { + s := &serverTest{ + t: t, + server: server, + doc: &testDocument{ + name: "test.promql", + version: 0, + }, + } + s.resetMetadataService() + return s +} + +func (t *serverTest) resetMetadataService() { + // Default meta service. + svc, err := prometheus.NewClient("", 5*time.Minute) + require.NoError(t.t, err, "Failed to initialize metadata service") + + // Reset the metadata service on the server. + t.server.metadataService = svc +} + +func (t *serverTest) SetupTestMetaServer() *testMetadataServer { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/status/buildinfo", + func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(&prometheus.BuildInfoResponse{ + Status: "up", + Data: prometheus.BuildInfoData{ + Version: prometheus.RequiredVersion.String(), + Revision: "HEAD", + Branch: "master", + BuildUser: "prometheus", + BuildDate: time.Now().String(), + GoVersion: runtime.Version(), + }, + }) + }) + + server := httptest.NewServer(mux) + + // Create new metadata service. + svc, err := prometheus.NewClient(server.URL, 5*time.Minute) + require.NoError(t.t, err, "Failed to initialize fake metadata service") + + // Reset the metadata service on the server. + t.server.metadataService = svc + + return &testMetadataServer{ + serverTest: t, + mux: mux, + server: server, + } +} + +type testDocument struct { + name string + version int +} + +func (d *testDocument) ID() protocol.TextDocumentIdentifier { + return protocol.TextDocumentIdentifier{ + URI: d.DocumentURI(), + } +} + +func (d *testDocument) NextVersionID() protocol.VersionedTextDocumentIdentifier { + return protocol.VersionedTextDocumentIdentifier{ + Version: d.NextVersion(), + TextDocumentIdentifier: d.ID(), + } +} + +func (d *testDocument) DocumentURI() protocol.DocumentURI { + return protocol.DocumentURI(d.name) +} + +func (d *testDocument) NextVersion() float64 { + v := d.version + d.version++ + return float64(v) +} + +func (d *testDocument) ResetVersion() { + d.version = 0 +} + +type testMetadataServer struct { + serverTest *serverTest + mux *http.ServeMux + server *httptest.Server +} + +func (s *testMetadataServer) HandleFunc( + pattern string, + handler func(http.ResponseWriter, *http.Request), +) { + s.mux.HandleFunc(pattern, handler) +} + +func (s *testMetadataServer) TearDown() { + // Close server. + s.server.Close() + // Reset metadata service. + s.serverTest.resetMetadataService() +} diff --git a/prometheus/compatible.go b/prometheus/compatible.go index 51732f86..777c7e02 100644 --- a/prometheus/compatible.go +++ b/prometheus/compatible.go @@ -47,32 +47,57 @@ func (c *compatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[strin return c.prometheusClient.Metadata(ctx, "", "") } -func (c *compatibleHTTPClient) LabelNames(ctx context.Context, name string) ([]string, error) { - if len(name) == 0 { +func (c *compatibleHTTPClient) LabelNames( + ctx context.Context, + selection model.LabelSet, +) ([]string, error) { + if selection == nil { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } - labelNames, _, err := c.prometheusClient.Series(ctx, []string{name}, time.Now().Add(-1*c.lookbackInterval), time.Now()) + + labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, + time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) if err != nil { return nil, err } - // subResult is used as a set of label. Like that we are sure we don't have any duplication - subResult := make(map[string]bool) - for _, ln := range labelNames { - for l := range ln { - subResult[string(l)] = true - } - } - result := make([]string, 0, len(subResult)) - for l := range subResult { + + result := make([]string, 0, len(labelNameAndValues)) + for l := range labelNameAndValues { result = append(result, l) } + return result, nil } -func (c *compatibleHTTPClient) LabelValues(ctx context.Context, label string) ([]model.LabelValue, error) { - values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) - return values, err +func (c *compatibleHTTPClient) LabelValues( + ctx context.Context, + label string, + selection model.LabelSet, +) ([]model.LabelValue, error) { + if selection == nil { + values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) + return values, err + } + + labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, + time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + if err != nil { + return nil, err + } + + labelValues, ok := labelNameAndValues[label] + if !ok { + return nil, nil + } + + result := make([]model.LabelValue, 0, len(labelValues)) + for l := range labelValues { + result = append(result, model.LabelValue(l)) + } + + return result, nil + } func (c *compatibleHTTPClient) ChangeDataSource(_ string) error { diff --git a/prometheus/empty.go b/prometheus/empty.go index 2419cc37..bd77b325 100644 --- a/prometheus/empty.go +++ b/prometheus/empty.go @@ -34,11 +34,11 @@ func (c *emptyHTTPClient) AllMetricMetadata(_ context.Context) (map[string][]v1. return make(map[string][]v1.Metadata), nil } -func (c *emptyHTTPClient) LabelNames(_ context.Context, _ string) ([]string, error) { +func (c *emptyHTTPClient) LabelNames(_ context.Context, _ model.LabelSet) ([]string, error) { return []string{}, nil } -func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string) ([]model.LabelValue, error) { +func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string, _ model.LabelSet) ([]model.LabelValue, error) { return []model.LabelValue{}, nil } diff --git a/prometheus/metadata_service.go b/prometheus/metadata_service.go index d9773dca..09e93b45 100644 --- a/prometheus/metadata_service.go +++ b/prometheus/metadata_service.go @@ -30,9 +30,9 @@ import ( ) var ( - // defining this global variable will avoid to initialized it each time + // RequiredVersion is a global variable will avoid to initialized it each time // and it will crash immediately the server during the initialization in case the version is not well defined - requiredVersion = semver.MustParse("2.15.0") // nolint: gochecknoglobals + RequiredVersion = semver.MustParse("2.15.0") // nolint: gochecknoglobals ) func buildGenericRoundTripper(connectionTimeout time.Duration) *http.Transport { @@ -66,16 +66,17 @@ func buildStatusRequest(prometheusURL string) (*http.Request, error) { return httpRequest, nil } -type buildInfoResponse struct { +// BuildInfoResponse is the build info response from /api/v1/status/buildinfo. +type BuildInfoResponse struct { Status string `json:"status"` - Data buildInfoData `json:"data,omitempty"` + Data BuildInfoData `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` Warnings []string `json:"warnings,omitempty"` } -// buildInfoData contains build information about Prometheus. -type buildInfoData struct { +// BuildInfoData contains build information about Prometheus. +type BuildInfoData struct { Version string `json:"version"` Revision string `json:"revision"` Branch string `json:"branch"` @@ -92,9 +93,9 @@ type MetadataService interface { AllMetricMetadata(ctx context.Context) (map[string][]v1.Metadata, error) // LabelNames returns all the unique label names present in the block in sorted order. // If a metric is provided, then it will return all unique label names linked to the metric during a predefined period of time - LabelNames(ctx context.Context, metricName string) ([]string, error) + LabelNames(ctx context.Context, selection model.LabelSet) ([]string, error) // LabelValues performs a query for the values of the given label. - LabelValues(ctx context.Context, label string) ([]model.LabelValue, error) + LabelValues(ctx context.Context, label string, selection model.LabelSet) ([]model.LabelValue, error) // ChangeDataSource is used if the prometheusURL is changing. // The client should re init its own parameter accordingly if necessary ChangeDataSource(prometheusURL string) error @@ -140,16 +141,16 @@ func (c *httpClient) AllMetricMetadata(ctx context.Context) (map[string][]v1.Met return c.subClient.AllMetricMetadata(ctx) } -func (c *httpClient) LabelNames(ctx context.Context, name string) ([]string, error) { +func (c *httpClient) LabelNames(ctx context.Context, selection model.LabelSet) ([]string, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelNames(ctx, name) + return c.subClient.LabelNames(ctx, selection) } -func (c *httpClient) LabelValues(ctx context.Context, label string) ([]model.LabelValue, error) { +func (c *httpClient) LabelValues(ctx context.Context, label string, selection model.LabelSet) ([]model.LabelValue, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelValues(ctx, label) + return c.subClient.LabelValues(ctx, label, selection) } func (c *httpClient) GetURL() string { @@ -238,7 +239,7 @@ func (c *httpClient) isCompatible(prometheusURL string) (bool, error) { if err != nil { return false, err } - jsonResponse := buildInfoResponse{} + jsonResponse := BuildInfoResponse{} err = json.Unmarshal(data, &jsonResponse) if err != nil { return false, err @@ -247,7 +248,7 @@ func (c *httpClient) isCompatible(prometheusURL string) (bool, error) { if err != nil { return false, err } - return currentVersion.GTE(requiredVersion), nil + return currentVersion.GTE(RequiredVersion), nil } return false, nil } diff --git a/prometheus/not_compatible.go b/prometheus/not_compatible.go index f1993c3a..6edc69f1 100644 --- a/prometheus/not_compatible.go +++ b/prometheus/not_compatible.go @@ -55,32 +55,57 @@ func (c *notCompatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[st return allMetadata, nil } -func (c *notCompatibleHTTPClient) LabelNames(ctx context.Context, name string) ([]string, error) { - if len(name) == 0 { +func (c *notCompatibleHTTPClient) LabelNames( + ctx context.Context, + selection model.LabelSet, +) ([]string, error) { + if selection == nil { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } - labelNames, _, err := c.prometheusClient.Series(ctx, []string{name}, time.Now().Add(-1*c.lookbackInterval), time.Now()) + + labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, + time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) if err != nil { return nil, err } - // subResult is used as a set of label. Like that we are sure we don't have any duplication - subResult := make(map[string]bool) - for _, ln := range labelNames { - for l := range ln { - subResult[string(l)] = true - } - } - result := make([]string, 0, len(subResult)) - for l := range subResult { + + result := make([]string, 0, len(labelNameAndValues)) + for l := range labelNameAndValues { result = append(result, l) } + return result, nil } -func (c *notCompatibleHTTPClient) LabelValues(ctx context.Context, label string) ([]model.LabelValue, error) { - values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) - return values, err +func (c *notCompatibleHTTPClient) LabelValues( + ctx context.Context, + label string, + selection model.LabelSet, +) ([]model.LabelValue, error) { + if selection == nil { + values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) + return values, err + } + + labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, + time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + if err != nil { + return nil, err + } + + labelValues, ok := labelNameAndValues[label] + if !ok { + return nil, nil + } + + result := make([]model.LabelValue, 0, len(labelValues)) + for l := range labelValues { + result = append(result, model.LabelValue(l)) + } + + return result, nil + } func (c *notCompatibleHTTPClient) ChangeDataSource(_ string) error { @@ -94,3 +119,50 @@ func (c *notCompatibleHTTPClient) SetLookbackInterval(interval time.Duration) { func (c *notCompatibleHTTPClient) GetURL() string { return "" } + +func uniqueLabelNameAndValues( + ctx context.Context, + prometheusClient v1.API, + start, end time.Time, + selection model.LabelSet, +) (map[string]map[string]struct{}, error) { + metricName := "" + metricLabels := model.LabelSet{} + for k, v := range selection { + if k == model.MetricNameLabel { + metricName = string(v) + } else { + metricLabels[k] = v + } + } + + match := metricName + if len(metricLabels) > 0 { + match += metricLabels.String() + } + + results, _, err := prometheusClient.Series(ctx, []string{match}, start, end) + if err != nil { + return nil, err + } + + // deduplicated is a de-duplicated result set. + deduplicated := make(map[string]map[string]struct{}) + for _, labelSet := range results { + for name, value := range labelSet { + setKey := string(name) + curr, ok := deduplicated[setKey] + if !ok { + curr = map[string]struct{}{} + deduplicated[setKey] = curr + } + setValue := string(value) + if _, exists := curr[setValue]; exists { + continue + } + curr[setValue] = struct{}{} + } + } + + return deduplicated, nil +} From 851d8db83aecafec574db972acd8762a68485dd5 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 15 Dec 2020 15:20:59 -0500 Subject: [PATCH 2/6] Rename selection to currLabelsSelected for better readability --- prometheus/compatible.go | 12 ++++++------ prometheus/metadata_service.go | 17 ++++++++++------- prometheus/not_compatible.go | 16 ++++++++-------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/prometheus/compatible.go b/prometheus/compatible.go index 777c7e02..8c45b15d 100644 --- a/prometheus/compatible.go +++ b/prometheus/compatible.go @@ -49,15 +49,15 @@ func (c *compatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[strin func (c *compatibleHTTPClient) LabelNames( ctx context.Context, - selection model.LabelSet, + currLabelsSelected model.LabelSet, ) ([]string, error) { - if selection == nil { + if currLabelsSelected == nil { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) if err != nil { return nil, err } @@ -73,15 +73,15 @@ func (c *compatibleHTTPClient) LabelNames( func (c *compatibleHTTPClient) LabelValues( ctx context.Context, label string, - selection model.LabelSet, + currLabelsSelected model.LabelSet, ) ([]model.LabelValue, error) { - if selection == nil { + if currLabelsSelected == nil { values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) return values, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) if err != nil { return nil, err } diff --git a/prometheus/metadata_service.go b/prometheus/metadata_service.go index 09e93b45..3d26548b 100644 --- a/prometheus/metadata_service.go +++ b/prometheus/metadata_service.go @@ -92,10 +92,13 @@ type MetadataService interface { // AllMetricMetadata returns metadata about metrics currently scraped for all existing metrics. AllMetricMetadata(ctx context.Context) (map[string][]v1.Metadata, error) // LabelNames returns all the unique label names present in the block in sorted order. - // If a metric is provided, then it will return all unique label names linked to the metric during a predefined period of time - LabelNames(ctx context.Context, selection model.LabelSet) ([]string, error) + // If currLabelsSelected is not nil, then it will return all unique label + // names that are relevant to the currently specified labels for a query. + LabelNames(ctx context.Context, currLabelsSelected model.LabelSet) ([]string, error) // LabelValues performs a query for the values of the given label. - LabelValues(ctx context.Context, label string, selection model.LabelSet) ([]model.LabelValue, error) + // If currLabelsSelected is not nil, then it will return all unique label + // values that are relevant to the currently specified labels for a query. + LabelValues(ctx context.Context, label string, currLabelsSelected model.LabelSet) ([]model.LabelValue, error) // ChangeDataSource is used if the prometheusURL is changing. // The client should re init its own parameter accordingly if necessary ChangeDataSource(prometheusURL string) error @@ -141,16 +144,16 @@ func (c *httpClient) AllMetricMetadata(ctx context.Context) (map[string][]v1.Met return c.subClient.AllMetricMetadata(ctx) } -func (c *httpClient) LabelNames(ctx context.Context, selection model.LabelSet) ([]string, error) { +func (c *httpClient) LabelNames(ctx context.Context, currLabelsSelected model.LabelSet) ([]string, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelNames(ctx, selection) + return c.subClient.LabelNames(ctx, currLabelsSelected) } -func (c *httpClient) LabelValues(ctx context.Context, label string, selection model.LabelSet) ([]model.LabelValue, error) { +func (c *httpClient) LabelValues(ctx context.Context, label string, currLabelsSelected model.LabelSet) ([]model.LabelValue, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelValues(ctx, label, selection) + return c.subClient.LabelValues(ctx, label, currLabelsSelected) } func (c *httpClient) GetURL() string { diff --git a/prometheus/not_compatible.go b/prometheus/not_compatible.go index 6edc69f1..ab6065c9 100644 --- a/prometheus/not_compatible.go +++ b/prometheus/not_compatible.go @@ -57,15 +57,15 @@ func (c *notCompatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[st func (c *notCompatibleHTTPClient) LabelNames( ctx context.Context, - selection model.LabelSet, + currLabelsSelected model.LabelSet, ) ([]string, error) { - if selection == nil { + if currLabelsSelected == nil { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) if err != nil { return nil, err } @@ -81,15 +81,15 @@ func (c *notCompatibleHTTPClient) LabelNames( func (c *notCompatibleHTTPClient) LabelValues( ctx context.Context, label string, - selection model.LabelSet, + currLabelsSelected model.LabelSet, ) ([]model.LabelValue, error) { - if selection == nil { + if currLabelsSelected == nil { values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) return values, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), selection) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) if err != nil { return nil, err } @@ -124,11 +124,11 @@ func uniqueLabelNameAndValues( ctx context.Context, prometheusClient v1.API, start, end time.Time, - selection model.LabelSet, + currLabelsSelected model.LabelSet, ) (map[string]map[string]struct{}, error) { metricName := "" metricLabels := model.LabelSet{} - for k, v := range selection { + for k, v := range currLabelsSelected { if k == model.MetricNameLabel { metricName = string(v) } else { From 874ed1674f83ad76f36ebaff83520b8b3c59d017 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 15 Dec 2020 16:09:14 -0500 Subject: [PATCH 3/6] Fix lint --- prometheus/compatible.go | 1 - prometheus/not_compatible.go | 1 - 2 files changed, 2 deletions(-) diff --git a/prometheus/compatible.go b/prometheus/compatible.go index 8c45b15d..6cbd11e5 100644 --- a/prometheus/compatible.go +++ b/prometheus/compatible.go @@ -97,7 +97,6 @@ func (c *compatibleHTTPClient) LabelValues( } return result, nil - } func (c *compatibleHTTPClient) ChangeDataSource(_ string) error { diff --git a/prometheus/not_compatible.go b/prometheus/not_compatible.go index ab6065c9..d8920ffc 100644 --- a/prometheus/not_compatible.go +++ b/prometheus/not_compatible.go @@ -105,7 +105,6 @@ func (c *notCompatibleHTTPClient) LabelValues( } return result, nil - } func (c *notCompatibleHTTPClient) ChangeDataSource(_ string) error { From 61dfceb90799975ea114e97a7f61c72852a0f0e6 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 15 Dec 2020 17:00:46 -0500 Subject: [PATCH 4/6] Use label matchers insteads of label set so can use correct matcher type --- langserver/completion.go | 49 ++++++++++++++++++++---------- langserver/langserver_test.go | 14 ++++----- prometheus/compatible.go | 13 ++++---- prometheus/empty.go | 5 ++-- prometheus/metadata_service.go | 17 ++++++----- prometheus/not_compatible.go | 55 ++++++++++++++++++++++++---------- 6 files changed, 99 insertions(+), 54 deletions(-) diff --git a/langserver/completion.go b/langserver/completion.go index 87dd79ea..25bae511 100644 --- a/langserver/completion.go +++ b/langserver/completion.go @@ -25,6 +25,7 @@ import ( "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol" "github.com/prometheus-community/promql-langserver/langserver/cache" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/pkg/labels" promql "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/util/strutil" "github.com/sahilm/fuzzy" @@ -316,25 +317,33 @@ func depthFirstVectorSelector(node promql.Node) (*promql.VectorSelector, bool) { return nil, false } -func labelsSelectionFromVectorSelector(vs *promql.VectorSelector) model.LabelSet { +func matchersFromVectorSelector(vs *promql.VectorSelector) ([]*labels.Matcher, error) { if vs == nil { - return nil + return nil, nil } - selection := model.LabelSet{} + var matchers []*labels.Matcher if vs.Name != "" { - selection[model.MetricNameLabel] = model.LabelValue(vs.Name) + m, err := labels.NewMatcher(labels.MatchEqual, model.MetricNameLabel, vs.Name) + if err != nil { + return nil, err + } + matchers = append(matchers, m) } - for _, elem := range vs.LabelMatchers { - selection[model.LabelName(elem.Name)] = model.LabelValue(elem.Value) + for _, m := range vs.LabelMatchers { + matchers = append(matchers, m) } - return selection + return matchers, nil } func (s *server) completeLabel(ctx context.Context, completions *[]protocol.CompletionItem, location *cache.Location, vs *promql.VectorSelector) error { labelName := location.Node.(*promql.Item).Val - selection := labelsSelectionFromVectorSelector(vs) - allNames, err := s.metadataService.LabelNames(ctx, selection) + matchers, err := matchersFromVectorSelector(vs) + if err != nil { + return err + } + + allNames, err := s.metadataService.LabelNames(ctx, matchers) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ @@ -383,15 +392,25 @@ func (s *server) completeLabelValue( vs *promql.VectorSelector, ) error { // Current selection from selector if not nil. - selection := labelsSelectionFromVectorSelector(vs) + matchers, err := matchersFromVectorSelector(vs) + if err != nil { + return err + } - // Delete the current label from selection, it is incomplete and we are - // trying to complete values for it. - if _, ok := selection[model.LabelName(labelName)]; ok { - delete(selection, model.LabelName(labelName)) + // Delete the current label from matchers if present, it is incomplete and + // we are trying to complete values for it. + if len(matchers) != 0 { + filtering := matchers[:] + matchers = matchers[:0] + for _, m := range filtering { + if m.Name == labelName { + continue // Filter out. + } + matchers = append(matchers, m) + } } - labelValues, err := s.metadataService.LabelValues(ctx, labelName, selection) + labelValues, err := s.metadataService.LabelValues(ctx, labelName, matchers) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ diff --git a/langserver/langserver_test.go b/langserver/langserver_test.go index 111ee27b..d2771948 100644 --- a/langserver/langserver_test.go +++ b/langserver/langserver_test.go @@ -726,7 +726,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo require.Equal(t, expected, actual) }) - t.Run("completion label name: sum(metric_name{foo_name=\"foo_value\",baz_name=\"\"})", func(t *testing.T) { + t.Run("completion label name: sum(metric_name{foo_name=~\"foo_value\",baz_name=\"\"})", func(t *testing.T) { // Apply a Full Change to the document. err := s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ @@ -735,7 +735,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo { Range: nil, RangeLength: 0, - Text: "sum(metric_name{foo_name=\"foo_value\",baz_name=\"\"})", + Text: "sum(metric_name{foo_name=~\"foo_value\",baz_name=\"\"})", }, }, }) @@ -748,7 +748,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo metaServer.HandleFunc("/api/v1/series", func(w http.ResponseWriter, r *http.Request) { assert.NoError(t, r.ParseForm()) - assert.Equal(t, []string{"metric_name{foo_name=\"foo_value\"}"}, r.Form["match[]"]) + assert.Equal(t, []string{"metric_name{foo_name=~\"foo_value\"}"}, r.Form["match[]"]) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "success", @@ -788,7 +788,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo require.Equal(t, expected, actual) }) - t.Run("completion label name: sum(metric_name{foo_name=\"foo_value\"}) by ()", func(t *testing.T) { + t.Run("completion label name: sum(metric_name{foo_name=~\"foo_value\"}) by ()", func(t *testing.T) { // Apply a Full Change to the document. err := s.server.DidChange(context.Background(), &protocol.DidChangeTextDocumentParams{ @@ -797,7 +797,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo { Range: nil, RangeLength: 0, - Text: "sum(metric_name{foo_name=\"foo_value\"}) by ()", + Text: "sum(metric_name{foo_name=~\"foo_value\"}) by ()", }, }, }) @@ -810,7 +810,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo metaServer.HandleFunc("/api/v1/series", func(w http.ResponseWriter, r *http.Request) { assert.NoError(t, r.ParseForm()) - assert.Equal(t, []string{"metric_name{foo_name=\"foo_value\"}"}, r.Form["match[]"]) + assert.Equal(t, []string{"metric_name{foo_name=~\"foo_value\"}"}, r.Form["match[]"]) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "success", @@ -837,7 +837,7 @@ func TestServer(t *testing.T) { //nolint:funlen, gocognit, gocyclo TextDocument: s.doc.ID(), Position: protocol.Position{ Line: 0.0, - Character: 43.0, + Character: 44.0, }, }, }) diff --git a/prometheus/compatible.go b/prometheus/compatible.go index 6cbd11e5..5e8966bd 100644 --- a/prometheus/compatible.go +++ b/prometheus/compatible.go @@ -19,6 +19,7 @@ import ( v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/pkg/labels" ) // compatibleHTTPClient must be used to contact a distant prometheus with a version >= v2.15. @@ -49,15 +50,15 @@ func (c *compatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[strin func (c *compatibleHTTPClient) LabelNames( ctx context.Context, - currLabelsSelected model.LabelSet, + currMatchers []*labels.Matcher, ) ([]string, error) { - if currLabelsSelected == nil { + if len(currMatchers) == 0 { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currMatchers) if err != nil { return nil, err } @@ -73,15 +74,15 @@ func (c *compatibleHTTPClient) LabelNames( func (c *compatibleHTTPClient) LabelValues( ctx context.Context, label string, - currLabelsSelected model.LabelSet, + currMatchers []*labels.Matcher, ) ([]model.LabelValue, error) { - if currLabelsSelected == nil { + if len(currMatchers) == 0 { values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) return values, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currMatchers) if err != nil { return nil, err } diff --git a/prometheus/empty.go b/prometheus/empty.go index bd77b325..41a5d3cd 100644 --- a/prometheus/empty.go +++ b/prometheus/empty.go @@ -19,6 +19,7 @@ import ( v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/pkg/labels" ) // emptyHTTPClient must be used when no prometheus URL has been defined. @@ -34,11 +35,11 @@ func (c *emptyHTTPClient) AllMetricMetadata(_ context.Context) (map[string][]v1. return make(map[string][]v1.Metadata), nil } -func (c *emptyHTTPClient) LabelNames(_ context.Context, _ model.LabelSet) ([]string, error) { +func (c *emptyHTTPClient) LabelNames(_ context.Context, _ []*labels.Matcher) ([]string, error) { return []string{}, nil } -func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string, _ model.LabelSet) ([]model.LabelValue, error) { +func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string, _ []*labels.Matcher) ([]model.LabelValue, error) { return []model.LabelValue{}, nil } diff --git a/prometheus/metadata_service.go b/prometheus/metadata_service.go index 3d26548b..e45db7ed 100644 --- a/prometheus/metadata_service.go +++ b/prometheus/metadata_service.go @@ -27,6 +27,7 @@ import ( "github.com/prometheus/client_golang/api" v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/pkg/labels" ) var ( @@ -92,13 +93,13 @@ type MetadataService interface { // AllMetricMetadata returns metadata about metrics currently scraped for all existing metrics. AllMetricMetadata(ctx context.Context) (map[string][]v1.Metadata, error) // LabelNames returns all the unique label names present in the block in sorted order. - // If currLabelsSelected is not nil, then it will return all unique label + // If currMatchers is not nil, then it will return all unique label // names that are relevant to the currently specified labels for a query. - LabelNames(ctx context.Context, currLabelsSelected model.LabelSet) ([]string, error) + LabelNames(ctx context.Context, currMatchers []*labels.Matcher) ([]string, error) // LabelValues performs a query for the values of the given label. - // If currLabelsSelected is not nil, then it will return all unique label + // If currMatchers is not nil, then it will return all unique label // values that are relevant to the currently specified labels for a query. - LabelValues(ctx context.Context, label string, currLabelsSelected model.LabelSet) ([]model.LabelValue, error) + LabelValues(ctx context.Context, label string, currMatchers []*labels.Matcher) ([]model.LabelValue, error) // ChangeDataSource is used if the prometheusURL is changing. // The client should re init its own parameter accordingly if necessary ChangeDataSource(prometheusURL string) error @@ -144,16 +145,16 @@ func (c *httpClient) AllMetricMetadata(ctx context.Context) (map[string][]v1.Met return c.subClient.AllMetricMetadata(ctx) } -func (c *httpClient) LabelNames(ctx context.Context, currLabelsSelected model.LabelSet) ([]string, error) { +func (c *httpClient) LabelNames(ctx context.Context, currMatchers []*labels.Matcher) ([]string, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelNames(ctx, currLabelsSelected) + return c.subClient.LabelNames(ctx, currMatchers) } -func (c *httpClient) LabelValues(ctx context.Context, label string, currLabelsSelected model.LabelSet) ([]model.LabelValue, error) { +func (c *httpClient) LabelValues(ctx context.Context, label string, currMatchers []*labels.Matcher) ([]model.LabelValue, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelValues(ctx, label, currLabelsSelected) + return c.subClient.LabelValues(ctx, label, currMatchers) } func (c *httpClient) GetURL() string { diff --git a/prometheus/not_compatible.go b/prometheus/not_compatible.go index d8920ffc..0d104303 100644 --- a/prometheus/not_compatible.go +++ b/prometheus/not_compatible.go @@ -15,10 +15,12 @@ package prometheus import ( "context" "fmt" + "strings" "time" v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/pkg/labels" ) // notCompatibleHTTPClient must be used to contact a distant prometheus with a version < v2.15. @@ -57,15 +59,15 @@ func (c *notCompatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[st func (c *notCompatibleHTTPClient) LabelNames( ctx context.Context, - currLabelsSelected model.LabelSet, + currMatchers []*labels.Matcher, ) ([]string, error) { - if currLabelsSelected == nil { + if len(currMatchers) == 0 { names, _, err := c.prometheusClient.LabelNames(ctx, time.Now().Add(-1*c.lookbackInterval), time.Now()) return names, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currMatchers) if err != nil { return nil, err } @@ -81,15 +83,15 @@ func (c *notCompatibleHTTPClient) LabelNames( func (c *notCompatibleHTTPClient) LabelValues( ctx context.Context, label string, - currLabelsSelected model.LabelSet, + currMatchers []*labels.Matcher, ) ([]model.LabelValue, error) { - if currLabelsSelected == nil { + if len(currMatchers) == 0 { values, _, err := c.prometheusClient.LabelValues(ctx, label, time.Now().Add(-1*c.lookbackInterval), time.Now()) return values, err } labelNameAndValues, err := uniqueLabelNameAndValues(ctx, c.prometheusClient, - time.Now().Add(-1*c.lookbackInterval), time.Now(), currLabelsSelected) + time.Now().Add(-1*c.lookbackInterval), time.Now(), currMatchers) if err != nil { return nil, err } @@ -123,24 +125,45 @@ func uniqueLabelNameAndValues( ctx context.Context, prometheusClient v1.API, start, end time.Time, - currLabelsSelected model.LabelSet, + currMatchers []*labels.Matcher, ) (map[string]map[string]struct{}, error) { - metricName := "" - metricLabels := model.LabelSet{} - for k, v := range currLabelsSelected { - if k == model.MetricNameLabel { - metricName = string(v) + var ( + metricName string + metricLabels []*labels.Matcher + ) + for _, matcher := range currMatchers { + if matcher.Name == model.MetricNameLabel { + metricName = string(matcher.Value) } else { - metricLabels[k] = v + metricLabels = append(metricLabels, matcher) } } - match := metricName + var match strings.Builder + if _, err := match.WriteString(metricName); err != nil { + return nil, err + } if len(metricLabels) > 0 { - match += metricLabels.String() + if _, err := match.WriteString("{"); err != nil { + return nil, err + } + for i, matcher := range metricLabels { + if i != 0 { + if _, err := match.WriteString(","); err != nil { + return nil, err + } + } + if _, err := match.WriteString(matcher.String()); err != nil { + return nil, err + } + } + if _, err := match.WriteString("}"); err != nil { + return nil, err + } } - results, _, err := prometheusClient.Series(ctx, []string{match}, start, end) + results, _, err := prometheusClient.Series(ctx, []string{match.String()}, + start, end) if err != nil { return nil, err } From c625d6c77ca58b9cc51e7e5d35e490357ee687ac Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Wed, 16 Dec 2020 07:43:26 -0500 Subject: [PATCH 5/6] Make sure metric name label matcher only added once --- langserver/completion.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/langserver/completion.go b/langserver/completion.go index 25bae511..ec077d88 100644 --- a/langserver/completion.go +++ b/langserver/completion.go @@ -330,6 +330,11 @@ func matchersFromVectorSelector(vs *promql.VectorSelector) ([]*labels.Matcher, e matchers = append(matchers, m) } for _, m := range vs.LabelMatchers { + if m.Name == model.MetricNameLabel { + // Only add the metric name label once, sometimes this is set + // just as name, sometimes as both name and as a matcher. + continue + } matchers = append(matchers, m) } return matchers, nil From 7bd05415b714a75671c1a0cf5430856eed139c5e Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Wed, 16 Dec 2020 07:49:59 -0500 Subject: [PATCH 6/6] Avoid nil ptr for empty label matcher on a vector selector --- langserver/completion.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/langserver/completion.go b/langserver/completion.go index ec077d88..30d0001d 100644 --- a/langserver/completion.go +++ b/langserver/completion.go @@ -330,6 +330,10 @@ func matchersFromVectorSelector(vs *promql.VectorSelector) ([]*labels.Matcher, e matchers = append(matchers, m) } for _, m := range vs.LabelMatchers { + if m == nil { + // Sometimes the label matcher is nil. + continue + } if m.Name == model.MetricNameLabel { // Only add the metric name label once, sometimes this is set // just as name, sometimes as both name and as a matcher.