2022-02-18 07:35:39 -06:00
|
|
|
package loki
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2022-06-17 00:47:31 -05:00
|
|
|
"path"
|
2022-02-18 07:35:39 -06:00
|
|
|
"strconv"
|
|
|
|
|
2022-05-05 06:09:01 -05:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
2022-02-18 07:35:39 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2022-05-05 06:09:01 -05:00
|
|
|
"github.com/grafana/grafana/pkg/util/converter"
|
2022-02-18 07:35:39 -06:00
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
)
|
|
|
|
|
|
|
|
type LokiAPI struct {
|
2022-06-02 04:52:27 -05:00
|
|
|
client *http.Client
|
|
|
|
url string
|
|
|
|
log log.Logger
|
|
|
|
headers map[string]string
|
2022-02-18 07:35:39 -06:00
|
|
|
}
|
|
|
|
|
2022-06-02 04:52:27 -05:00
|
|
|
func newLokiAPI(client *http.Client, url string, log log.Logger, headers map[string]string) *LokiAPI {
|
|
|
|
return &LokiAPI{client: client, url: url, log: log, headers: headers}
|
2022-02-18 07:35:39 -06:00
|
|
|
}
|
|
|
|
|
2022-06-02 04:52:27 -05:00
|
|
|
func addHeaders(req *http.Request, headers map[string]string) {
|
|
|
|
for name, value := range headers {
|
|
|
|
req.Header.Set(name, value)
|
2022-05-05 05:42:50 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-02 04:52:27 -05:00
|
|
|
func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery, headers map[string]string) (*http.Request, error) {
|
2022-02-18 07:35:39 -06:00
|
|
|
qs := url.Values{}
|
|
|
|
qs.Set("query", query.Expr)
|
2022-02-25 02:14:17 -06:00
|
|
|
|
2022-04-20 06:52:15 -05:00
|
|
|
qs.Set("direction", string(query.Direction))
|
|
|
|
|
2022-02-25 02:14:17 -06:00
|
|
|
// MaxLines defaults to zero when not received,
|
|
|
|
// and Loki does not like limit=0, even when it is not needed
|
|
|
|
// (for example for metric queries), so we
|
|
|
|
// only send it when it's set
|
|
|
|
if query.MaxLines > 0 {
|
|
|
|
qs.Set("limit", fmt.Sprintf("%d", query.MaxLines))
|
|
|
|
}
|
2022-02-18 07:35:39 -06:00
|
|
|
|
|
|
|
lokiUrl, err := url.Parse(lokiDsUrl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-25 02:14:17 -06:00
|
|
|
switch query.QueryType {
|
|
|
|
case QueryTypeRange:
|
|
|
|
{
|
|
|
|
qs.Set("start", strconv.FormatInt(query.Start.UnixNano(), 10))
|
|
|
|
qs.Set("end", strconv.FormatInt(query.End.UnixNano(), 10))
|
|
|
|
// NOTE: technically for streams-producing queries `step`
|
|
|
|
// is ignored, so it would be nicer to not send it in such cases,
|
|
|
|
// but we cannot detect that situation, so we always send it.
|
|
|
|
// it should not break anything.
|
2022-04-12 05:30:39 -05:00
|
|
|
// NOTE2: we do this at millisecond precision for two reasons:
|
|
|
|
// a. Loki cannot do steps with better precision anyway,
|
|
|
|
// so the microsecond & nanosecond part can be ignored.
|
|
|
|
// b. having it always be number+'ms' makes it more robust and
|
|
|
|
// precise, as Loki does not support step with float number
|
|
|
|
// and time-specifier, like "1.5s"
|
|
|
|
qs.Set("step", fmt.Sprintf("%dms", query.Step.Milliseconds()))
|
2022-06-17 00:47:31 -05:00
|
|
|
lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query_range")
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
|
|
|
case QueryTypeInstant:
|
|
|
|
{
|
|
|
|
qs.Set("time", strconv.FormatInt(query.End.UnixNano(), 10))
|
2022-06-17 00:47:31 -05:00
|
|
|
lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query")
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("invalid QueryType: %v", query.QueryType)
|
|
|
|
}
|
|
|
|
|
2022-02-18 07:35:39 -06:00
|
|
|
lokiUrl.RawQuery = qs.Encode()
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-02 04:52:27 -05:00
|
|
|
addHeaders(req, headers)
|
2022-02-25 02:14:17 -06:00
|
|
|
|
|
|
|
if query.VolumeQuery {
|
|
|
|
req.Header.Set("X-Query-Tags", "Source=logvolhist")
|
|
|
|
}
|
2022-02-18 07:35:39 -06:00
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type lokiError struct {
|
|
|
|
Message string
|
|
|
|
}
|
|
|
|
|
|
|
|
// we know there is an error,
|
|
|
|
// based on the http-response-body
|
|
|
|
// we have to make an informative error-object
|
|
|
|
func makeLokiError(body io.ReadCloser) error {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
_, err := buf.ReadFrom(body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes := buf.Bytes()
|
|
|
|
|
|
|
|
// the error-message is probably a JSON structure,
|
|
|
|
// with a string-field named "message". we want the
|
|
|
|
// value of that field.
|
|
|
|
// but, the response might be just a simple string,
|
|
|
|
// this was used in older Loki versions.
|
|
|
|
// so our approach is this:
|
|
|
|
// - we try to convert the bytes to JSON
|
|
|
|
// - we take the value of the field "message"
|
|
|
|
// - if any of these steps fail, or if "message" is empty, we return the whole text
|
|
|
|
|
|
|
|
var data lokiError
|
|
|
|
err = json.Unmarshal(bytes, &data)
|
|
|
|
if err != nil {
|
|
|
|
// we were unable to convert the bytes to JSON, we return the whole text
|
|
|
|
return fmt.Errorf("%v", string(bytes))
|
|
|
|
}
|
|
|
|
|
|
|
|
errorMessage := data.Message
|
|
|
|
|
|
|
|
if errorMessage == "" {
|
|
|
|
// we got no usable error message, we return the whole text
|
|
|
|
return fmt.Errorf("%v", string(bytes))
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("%v", errorMessage)
|
|
|
|
}
|
|
|
|
|
2022-05-05 06:09:01 -05:00
|
|
|
func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames, error) {
|
2022-06-02 04:52:27 -05:00
|
|
|
req, err := makeDataRequest(ctx, api.url, query, api.headers)
|
2022-02-18 07:35:39 -06:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := api.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := resp.Body.Close(); err != nil {
|
|
|
|
api.log.Warn("Failed to close response body", "err", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
|
|
return nil, makeLokiError(resp.Body)
|
|
|
|
}
|
|
|
|
|
2022-05-05 06:09:01 -05:00
|
|
|
iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024)
|
2022-05-24 15:17:11 -05:00
|
|
|
res := converter.ReadPrometheusStyleResult(iter, converter.Options{MatrixWideSeries: false, VectorWideSeries: false})
|
2022-05-05 06:09:01 -05:00
|
|
|
|
2022-07-19 01:13:38 -05:00
|
|
|
if res == nil {
|
|
|
|
// it's hard to say if this is an error-case or not.
|
|
|
|
// we know the http-response was a success-response
|
|
|
|
// (otherwise we wouldn't be here in the code),
|
|
|
|
// so we will go with a success, with no data.
|
|
|
|
return data.Frames{}, nil
|
|
|
|
}
|
|
|
|
|
2022-05-05 06:09:01 -05:00
|
|
|
if res.Error != nil {
|
|
|
|
return nil, res.Error
|
2022-02-18 07:35:39 -06:00
|
|
|
}
|
|
|
|
|
2022-05-05 06:09:01 -05:00
|
|
|
return res.Frames, nil
|
2022-02-18 07:35:39 -06:00
|
|
|
}
|
2022-04-25 06:16:14 -05:00
|
|
|
|
2022-06-17 00:47:31 -05:00
|
|
|
func makeRawRequest(ctx context.Context, lokiDsUrl string, resourcePath string, headers map[string]string) (*http.Request, error) {
|
2022-04-25 06:16:14 -05:00
|
|
|
lokiUrl, err := url.Parse(lokiDsUrl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-17 00:47:31 -05:00
|
|
|
resourceUrl, err := url.Parse(resourcePath)
|
2022-04-25 06:16:14 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-17 00:47:31 -05:00
|
|
|
// we take the path and the query-string only
|
|
|
|
lokiUrl.RawQuery = resourceUrl.RawQuery
|
|
|
|
lokiUrl.Path = path.Join(lokiUrl.Path, resourceUrl.Path)
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil)
|
2022-05-05 05:42:50 -05:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-02 04:52:27 -05:00
|
|
|
addHeaders(req, headers)
|
2022-05-05 05:42:50 -05:00
|
|
|
|
|
|
|
return req, nil
|
2022-04-25 06:16:14 -05:00
|
|
|
}
|
|
|
|
|
2022-06-17 00:47:31 -05:00
|
|
|
func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) ([]byte, error) {
|
|
|
|
req, err := makeRawRequest(ctx, api.url, resourcePath, api.headers)
|
2022-04-25 06:16:14 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := api.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := resp.Body.Close(); err != nil {
|
|
|
|
api.log.Warn("Failed to close response body", "err", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
|
|
return nil, makeLokiError(resp.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
return io.ReadAll(resp.Body)
|
|
|
|
}
|