diff --git a/cmd/promql-langserver/promql-langserver.go b/cmd/promql-langserver/promql-langserver.go index 646e8e47..8c4eb155 100644 --- a/cmd/promql-langserver/promql-langserver.go +++ b/cmd/promql-langserver/promql-langserver.go @@ -20,6 +20,7 @@ import ( "log" "net/http" "os" + "time" kitlog "github.com/go-kit/kit/log" "github.com/prometheus-community/promql-langserver/config" @@ -41,7 +42,7 @@ func main() { } if conf.RESTAPIPort != 0 { fmt.Fprintln(os.Stderr, "REST API: Listening on port ", conf.RESTAPIPort) - prometheusClient, err := promClient.NewClient(conf.PrometheusURL) + prometheusClient, err := promClient.NewClient(conf.PrometheusURL, time.Duration(conf.MetadataLookbackInterval)) if err != nil { log.Fatal(err) } diff --git a/config/config.go b/config/config.go index 8433a6c3..89490671 100644 --- a/config/config.go +++ b/config/config.go @@ -18,11 +18,15 @@ import ( "net/url" "os" "strconv" + "time" "github.com/kelseyhightower/envconfig" + "github.com/prometheus/common/model" "gopkg.in/yaml.v3" ) +const defaultInterval = model.Duration(12 * 3600 * time.Second) + // ReadConfig gets the GlobalConfig from a configFile (that is a path to the file). func ReadConfig(configFile string) (*Config, error) { if len(configFile) == 0 { @@ -70,6 +74,8 @@ type Config struct { LogFormat LogFormat `yaml:"log_format"` PrometheusURL string `yaml:"prometheus_url"` RESTAPIPort uint64 `yaml:"rest_api_port"` + // MetadataLookbackInterval is the time in second used to retrieve label and metrics from Prometheus + MetadataLookbackInterval model.Duration `yaml:"metadata_lookback_interval"` } // UnmarshalYAML overrides a function used internally by the yaml.v3 lib. @@ -94,7 +100,8 @@ func (c *Config) unmarshalENV() error { PrometheusURL string // the envconfig lib is not able to convert an empty string to the value 0 // so we have to convert it manually - RESTAPIPort string + RESTAPIPort string + MetadataLookbackInterval string }{} if err := envconfig.Process(prefix, conf); err != nil { return err @@ -106,6 +113,13 @@ func (c *Config) unmarshalENV() error { return parseError } } + if len(conf.MetadataLookbackInterval) > 0 { + var parseError error + c.MetadataLookbackInterval, parseError = model.ParseDuration(conf.MetadataLookbackInterval) + if parseError != nil { + return parseError + } + } c.ActivateRPCLog = conf.ActivateRPCLog c.PrometheusURL = conf.PrometheusURL c.LogFormat = LogFormat(conf.LogFormat) @@ -129,5 +143,9 @@ func (c *Config) Validate() error { c.LogFormat = TextFormat } + if c.MetadataLookbackInterval <= 0 { + c.MetadataLookbackInterval = defaultInterval + } + return nil } diff --git a/config/config_test.go b/config/config_test.go index f8fb423e..79d541ab 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -30,23 +30,26 @@ func TestUnmarshalENV(t *testing.T) { title: "empty config", variables: map[string]string{}, expected: &Config{ - ActivateRPCLog: false, - LogFormat: TextFormat, + ActivateRPCLog: false, + LogFormat: TextFormat, + MetadataLookbackInterval: defaultInterval, }, }, { title: "full config", variables: map[string]string{ - "LANGSERVER_ACTIVATERPCLOG": "true", - "LANGSERVER_PROMETHEUSURL": "http://localhost:9090", - "LANGSERVER_RESTAPIPORT": "8080", - "LANGSERVER_LOGFORMAT": "json", + "LANGSERVER_ACTIVATERPCLOG": "true", + "LANGSERVER_PROMETHEUSURL": "http://localhost:9090", + "LANGSERVER_RESTAPIPORT": "8080", + "LANGSERVER_LOGFORMAT": "json", + "LANGSERVER_METADATALOOKBACKINTERVAL": "1w", }, expected: &Config{ - ActivateRPCLog: true, - PrometheusURL: "http://localhost:9090", - RESTAPIPort: 8080, - LogFormat: JSONFormat, + ActivateRPCLog: true, + PrometheusURL: "http://localhost:9090", + RESTAPIPort: 8080, + LogFormat: JSONFormat, + MetadataLookbackInterval: 604800000000000, }, }, } diff --git a/doc/developing_editor.md b/doc/developing_editor.md index 769a22e3..cec1b275 100644 --- a/doc/developing_editor.md +++ b/doc/developing_editor.md @@ -16,6 +16,7 @@ activate_rpc_log: false # It's a boolean in order to activate or deactivate the log_format: "text" # The format of the log printed. Possible value: json, text. Default value: "text" prometheus_url: "http://localhost:9090" # the HTTP URL of the prometheus server. rest_api_port: 8080 # When set, the server will be started as an HTTP server that provides a REST API instead of the language server protocol. Default value: 0 +metadata_lookback_interval: 2d # Interval used to retrieve data such as label and metrics from prometheus. Default value: 12h ``` In case the file is not provided, it will read the configuration from the environment variables with the following structure: @@ -23,8 +24,9 @@ In case the file is not provided, it will read the configuration from the enviro ```bash export LANGSERVER_ACTIVATERPCLOG="true" export LANGSERVER_PROMETHEUSURL="http://localhost:9090" -export LANGSERVER_RESTAPIPORT"="8080" -export LANGSERVER_LOGFORMAT"="json" +export LANGSERVER_RESTAPIPORT="8080" +export LANGSERVER_LOGFORMAT="json" +export LANGSERVER_METADATALOOKBACKINTERVAL="1w" ``` Note: documentation and default value are the same for both configuration (yaml and environment) @@ -37,7 +39,8 @@ It has the following structure: ```json { "promql": { - "url": "http://localhost:9090" # the HTTP URL of the prometheus server. + "url": "http://localhost:9090", # the HTTP URL of the prometheus server. + "metadataLookbackInterval": "2h" } } ``` diff --git a/langserver/completion.go b/langserver/completion.go index f300215c..63472818 100644 --- a/langserver/completion.go +++ b/langserver/completion.go @@ -20,7 +20,6 @@ import ( "sort" "strconv" "strings" - "time" "github.com/pkg/errors" @@ -312,7 +311,7 @@ func (s *server) completeLabel(ctx context.Context, completions *[]protocol.Comp if vs != nil { metricName = vs.Name } - allNames, err := s.metadataService.LabelNames(ctx, metricName, time.Now().Add(-100*time.Hour), time.Now()) + allNames, err := s.metadataService.LabelNames(ctx, metricName) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ @@ -355,7 +354,7 @@ 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, time.Now().Add(-100*time.Hour), time.Now()) + labelValues, err := s.metadataService.LabelValues(ctx, labelName) if err != nil { // nolint: errcheck s.client.LogMessage(s.lifetime, &protocol.LogMessageParams{ diff --git a/langserver/config.go b/langserver/config.go index 43bd85c0..c4d3392a 100644 --- a/langserver/config.go +++ b/langserver/config.go @@ -16,48 +16,78 @@ package langserver import ( "context" "fmt" + "net/url" + "time" "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol" + "github.com/prometheus/common/model" ) // DidChangeConfiguration is required by the protocol.Server interface. func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error { - langserverAddressConfigPath := []string{"promql", "url"} + if params == nil { + return nil + } + // nolint: errcheck + s.client.LogMessage( + s.lifetime, + &protocol.LogMessageParams{ + Type: protocol.Info, + Message: fmt.Sprintf("Received notification change: %v\n", params), + }) - if params != nil { + setting := params.Settings + + // the struct expected is the following + // promql: + // url: http:// + // interval: 3w + m, ok := setting.(map[string]map[string]string) + if !ok { + // nolint: errcheck + s.client.LogMessage(ctx, &protocol.LogMessageParams{ + Type: protocol.Error, + Message: fmt.Sprint("unexpected format of the configuration"), + }) + return nil + } + config, ok := m["promql"] + if !ok { // nolint: errcheck - s.client.LogMessage( - s.lifetime, - &protocol.LogMessageParams{ - Type: protocol.Info, - Message: fmt.Sprintf("Received notification change: %v\n", params), - }) + s.client.LogMessage(ctx, &protocol.LogMessageParams{ + Type: protocol.Error, + Message: fmt.Sprint("promQL key not found"), + }) + return nil + } - setting := params.Settings + if err := s.setURLFromChangeConfiguration(config); err != nil { + // nolint: errcheck + s.client.LogMessage(ctx, &protocol.LogMessageParams{ + Type: protocol.Info, + Message: err.Error(), + }) + } - for _, e := range langserverAddressConfigPath { - m, ok := setting.(map[string]interface{}) - if !ok { - break - } + if err := s.setMetadataLookbackInterval(config); err != nil { + // nolint: errcheck + s.client.LogMessage(ctx, &protocol.LogMessageParams{ + Type: protocol.Info, + Message: err.Error(), + }) + } + return nil +} - setting, ok = m[e] - if !ok { - break - } +func (s *server) setURLFromChangeConfiguration(settings map[string]string) error { + if promURL, ok := settings["url"]; ok { + if _, err := url.Parse(promURL); err != nil { + return err } - - if str, ok := setting.(string); ok { - if err := s.connectPrometheus(str); err != nil { - // nolint: errcheck - s.client.LogMessage(ctx, &protocol.LogMessageParams{ - Type: protocol.Info, - Message: err.Error(), - }) - } + if err := s.connectPrometheus(promURL); err != nil { + return err } } - return nil } @@ -72,3 +102,14 @@ func (s *server) connectPrometheus(url string) error { } return nil } + +func (s *server) setMetadataLookbackInterval(settings map[string]string) error { + if interval, ok := settings["metadataLookbackInterval"]; ok { + duration, err := model.ParseDuration(interval) + if err != nil { + return err + } + s.metadataService.SetLookbackInterval(time.Duration(duration)) + } + return nil +} diff --git a/langserver/server.go b/langserver/server.go index 759bccb9..a030d830 100644 --- a/langserver/server.go +++ b/langserver/server.go @@ -27,6 +27,7 @@ import ( "net" "os" "sync" + "time" "github.com/prometheus-community/promql-langserver/config" promClient "github.com/prometheus-community/promql-langserver/prometheus" @@ -136,7 +137,7 @@ func ServerFromStream(ctx context.Context, stream jsonrpc2.Stream, conf *config. // In order to have an error message in the IDE/editor, we are going to set the prometheusURL in the method server#Initialized. s.prometheusURL = conf.PrometheusURL - prometheusClient, err := promClient.NewClient("") + prometheusClient, err := promClient.NewClient("", time.Duration(conf.MetadataLookbackInterval)) if err != nil { // nolint: errcheck s.client.ShowMessage(s.lifetime, &protocol.ShowMessageParams{ diff --git a/prometheus/compatible.go b/prometheus/compatible.go index 591ab76a..51732f86 100644 --- a/prometheus/compatible.go +++ b/prometheus/compatible.go @@ -25,6 +25,7 @@ import ( type compatibleHTTPClient struct { MetadataService prometheusClient v1.API + lookbackInterval time.Duration } func (c *compatibleHTTPClient) MetricMetadata(ctx context.Context, metric string) (v1.Metadata, error) { @@ -46,13 +47,12 @@ func (c *compatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[strin return c.prometheusClient.Metadata(ctx, "", "") } -func (c *compatibleHTTPClient) LabelNames(ctx context.Context, name string, - startTime time.Time, endTime time.Time) ([]string, error) { +func (c *compatibleHTTPClient) LabelNames(ctx context.Context, name string) ([]string, error) { if len(name) == 0 { - names, _, err := c.prometheusClient.LabelNames(ctx, startTime, endTime) + 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}, startTime, endTime) + labelNames, _, err := c.prometheusClient.Series(ctx, []string{name}, time.Now().Add(-1*c.lookbackInterval), time.Now()) if err != nil { return nil, err } @@ -70,9 +70,8 @@ func (c *compatibleHTTPClient) LabelNames(ctx context.Context, name string, return result, nil } -func (c *compatibleHTTPClient) LabelValues(ctx context.Context, label string, - startTime time.Time, endTime time.Time) ([]model.LabelValue, error) { - values, _, err := c.prometheusClient.LabelValues(ctx, label, startTime, endTime) +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 } @@ -80,6 +79,10 @@ func (c *compatibleHTTPClient) ChangeDataSource(_ string) error { return fmt.Errorf("method not supported") } +func (c *compatibleHTTPClient) SetLookbackInterval(interval time.Duration) { + c.lookbackInterval = interval +} + func (c *compatibleHTTPClient) GetURL() string { return "" } diff --git a/prometheus/empty.go b/prometheus/empty.go index ad27d533..2419cc37 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, _ time.Time, _ time.Time) ([]string, error) { +func (c *emptyHTTPClient) LabelNames(_ context.Context, _ string) ([]string, error) { return []string{}, nil } -func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string, _ time.Time, _ time.Time) ([]model.LabelValue, error) { +func (c *emptyHTTPClient) LabelValues(_ context.Context, _ string) ([]model.LabelValue, error) { return []model.LabelValue{}, nil } @@ -46,6 +46,10 @@ func (c *emptyHTTPClient) ChangeDataSource(_ string) error { return fmt.Errorf("method not supported") } +func (c *emptyHTTPClient) SetLookbackInterval(_ time.Duration) { + +} + func (c *emptyHTTPClient) GetURL() string { return "" } diff --git a/prometheus/metadata_service.go b/prometheus/metadata_service.go index 967aeb3e..d9773dca 100644 --- a/prometheus/metadata_service.go +++ b/prometheus/metadata_service.go @@ -92,12 +92,14 @@ 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, startTime time.Time, endTime time.Time) ([]string, error) + LabelNames(ctx context.Context, metricName string) ([]string, error) // LabelValues performs a query for the values of the given label. - LabelValues(ctx context.Context, label string, startTime time.Time, endTime time.Time) ([]model.LabelValue, error) + LabelValues(ctx context.Context, label string) ([]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 + // SetLookbackInterval is a method to use to change the interval that then will be used to retrieve data such as label and metrics from prometheus. + SetLookbackInterval(interval time.Duration) // GetURL is returning the url used to contact the prometheus server // In case the instance is used directly in Prometheus, it should be the externalURL GetURL() string @@ -108,15 +110,17 @@ type MetadataService interface { // because it will manage which sub instance of the Client to use (like a factory). type httpClient struct { MetadataService - requestTimeout time.Duration - mutex sync.RWMutex - subClient MetadataService - url string + requestTimeout time.Duration + mutex sync.RWMutex + subClient MetadataService + url string + lookbackInterval time.Duration } -func NewClient(prometheusURL string) (MetadataService, error) { +func NewClient(prometheusURL string, lookbackInterval time.Duration) (MetadataService, error) { c := &httpClient{ - requestTimeout: 30, + requestTimeout: 30, + lookbackInterval: lookbackInterval, } if err := c.ChangeDataSource(prometheusURL); err != nil { return nil, err @@ -136,18 +140,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, - startTime time.Time, endTime time.Time) ([]string, error) { +func (c *httpClient) LabelNames(ctx context.Context, name string) ([]string, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelNames(ctx, name, startTime, endTime) + return c.subClient.LabelNames(ctx, name) } -func (c *httpClient) LabelValues(ctx context.Context, label string, - startTime time.Time, endTime time.Time) ([]model.LabelValue, error) { +func (c *httpClient) LabelValues(ctx context.Context, label string) ([]model.LabelValue, error) { c.mutex.RLock() defer c.mutex.RUnlock() - return c.subClient.LabelValues(ctx, label, startTime, endTime) + return c.subClient.LabelValues(ctx, label) } func (c *httpClient) GetURL() string { @@ -156,6 +158,13 @@ func (c *httpClient) GetURL() string { return c.url } +func (c *httpClient) SetLookbackInterval(interval time.Duration) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.lookbackInterval = interval + c.subClient.SetLookbackInterval(interval) +} + func (c *httpClient) ChangeDataSource(prometheusURL string) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -192,10 +201,12 @@ func (c *httpClient) ChangeDataSource(prometheusURL string) error { if isCompatible { c.subClient = &compatibleHTTPClient{ prometheusClient: v1.NewAPI(prometheusHTTPClient), + lookbackInterval: c.lookbackInterval, } } else { c.subClient = ¬CompatibleHTTPClient{ prometheusClient: v1.NewAPI(prometheusHTTPClient), + lookbackInterval: c.lookbackInterval, } } diff --git a/prometheus/not_compatible.go b/prometheus/not_compatible.go index 990799fb..f1993c3a 100644 --- a/prometheus/not_compatible.go +++ b/prometheus/not_compatible.go @@ -25,6 +25,7 @@ import ( type notCompatibleHTTPClient struct { MetadataService prometheusClient v1.API + lookbackInterval time.Duration } func (c *notCompatibleHTTPClient) MetricMetadata(ctx context.Context, metric string) (v1.Metadata, error) { @@ -54,13 +55,12 @@ func (c *notCompatibleHTTPClient) AllMetricMetadata(ctx context.Context) (map[st return allMetadata, nil } -func (c *notCompatibleHTTPClient) LabelNames(ctx context.Context, name string, - startTime time.Time, endTime time.Time) ([]string, error) { +func (c *notCompatibleHTTPClient) LabelNames(ctx context.Context, name string) ([]string, error) { if len(name) == 0 { - names, _, err := c.prometheusClient.LabelNames(ctx, startTime, endTime) + 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}, startTime, endTime) + labelNames, _, err := c.prometheusClient.Series(ctx, []string{name}, time.Now().Add(-1*c.lookbackInterval), time.Now()) if err != nil { return nil, err } @@ -78,9 +78,8 @@ func (c *notCompatibleHTTPClient) LabelNames(ctx context.Context, name string, return result, nil } -func (c *notCompatibleHTTPClient) LabelValues(ctx context.Context, label string, - startTime time.Time, endTime time.Time) ([]model.LabelValue, error) { - values, _, err := c.prometheusClient.LabelValues(ctx, label, startTime, endTime) +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 } @@ -88,6 +87,10 @@ func (c *notCompatibleHTTPClient) ChangeDataSource(_ string) error { return fmt.Errorf("method not supported") } +func (c *notCompatibleHTTPClient) SetLookbackInterval(interval time.Duration) { + c.lookbackInterval = interval +} + func (c *notCompatibleHTTPClient) GetURL() string { return "" } diff --git a/rest/handler.go b/rest/handler.go index 8458fc9c..1c2bcae5 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -26,7 +26,6 @@ import ( "github.com/prometheus-community/promql-langserver/internal/vendored/go-tools/lsp/protocol" "github.com/prometheus-community/promql-langserver/langserver" promClient "github.com/prometheus-community/promql-langserver/prometheus" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -38,6 +37,8 @@ import ( // otherwise you need to provide your own implementation of the interface. // // The provided Logger should be synchronized. +// +// interval is the period of time (in second) used to retrieve data such as label and metrics from metrics. func CreateHandler(ctx context.Context, metadataService promClient.MetadataService, logger log.Logger) (http.Handler, error) { return createHandler(ctx, metadataService, logger, false) } @@ -53,10 +54,13 @@ func CreateHandler(ctx context.Context, metadataService promClient.MetadataServi // otherwise you need to provide your own implementation of the interface. // // The provided Logger should be synchronized. +// +// interval is the period of time (in second) used to retrieve data such as label and metrics from metrics. func CreateInstHandler(ctx context.Context, metadataService promClient.MetadataService, logger log.Logger) (http.Handler, error) { return createHandler(ctx, metadataService, logger, true) } -func createHandler(ctx context.Context, metadataService promClient.MetadataService, logger log.Logger, metricsEnpoint bool) (http.Handler, error) { + +func createHandler(ctx context.Context, metadataService promClient.MetadataService, logger log.Logger, metricsEndpoint bool) (http.Handler, error) { lgs, err := langserver.CreateHeadlessServer(ctx, metadataService, logger) if err != nil { return nil, err @@ -64,7 +68,7 @@ func createHandler(ctx context.Context, metadataService promClient.MetadataServi ls := &langserverHandler{langserver: lgs} ls.m = make(map[string]http.Handler) - ls.createHandlers(metricsEnpoint) + ls.createHandlers(metricsEndpoint) return ls, nil }