| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | package loki | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"bytes" | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							| 
									
										
										
										
											2022-06-17 07:47:31 +02:00
										 |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	"strconv" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							| 
									
										
										
										
											2023-01-30 09:50:27 +01:00
										 |  |  | 	jsoniter "github.com/json-iterator/go" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	"github.com/grafana/grafana/pkg/infra/log" | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	"github.com/grafana/grafana/pkg/infra/tracing" | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | 	"github.com/grafana/grafana/pkg/util/converter" | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type LokiAPI struct { | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | 	client *http.Client | 
					
						
							|  |  |  | 	url    string | 
					
						
							|  |  |  | 	log    log.Logger | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | type RawLokiResponse struct { | 
					
						
							|  |  |  | 	Body     []byte | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	Status   int | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 	Encoding string | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | func newLokiAPI(client *http.Client, url string, log log.Logger) *LokiAPI { | 
					
						
							|  |  |  | 	return &LokiAPI{client: client, url: url, log: log} | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*http.Request, error) { | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	qs := url.Values{} | 
					
						
							|  |  |  | 	qs.Set("query", query.Expr) | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-20 13:52:15 +02:00
										 |  |  | 	qs.Set("direction", string(query.Direction)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01: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 14:35:39 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	lokiUrl, err := url.Parse(lokiDsUrl) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01: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 12:30:39 +02: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 07:47:31 +02:00
										 |  |  | 			lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query_range") | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	case QueryTypeInstant: | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			qs.Set("time", strconv.FormatInt(query.End.UnixNano(), 10)) | 
					
						
							| 
									
										
										
										
											2022-06-17 07:47:31 +02:00
										 |  |  | 			lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query") | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("invalid QueryType: %v", query.QueryType) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	lokiUrl.RawQuery = qs.Encode() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-27 16:41:40 +01:00
										 |  |  | 	if query.SupportingQueryType != SupportingQueryNone { | 
					
						
							|  |  |  | 		value := getSupportingQueryHeaderValue(req, query.SupportingQueryType) | 
					
						
							|  |  |  | 		if value != "" { | 
					
						
							|  |  |  | 			req.Header.Set("X-Query-Tags", "Source="+value) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-02-25 09:14:17 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return req, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | type lokiResponseError struct { | 
					
						
							|  |  |  | 	Message string `json:"message"` | 
					
						
							|  |  |  | 	TraceID string `json:"traceID,omitempty"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | type lokiError struct { | 
					
						
							|  |  |  | 	Message string | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | func makeLokiError(bytes []byte) error { | 
					
						
							|  |  |  | 	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)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if data.Message == "" { | 
					
						
							|  |  |  | 		// we got no usable error message, we return the whole text | 
					
						
							|  |  |  | 		return fmt.Errorf("%v", string(bytes)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return fmt.Errorf("%v", data.Message) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | // we know there is an error, | 
					
						
							|  |  |  | // based on the http-response-body | 
					
						
							|  |  |  | // we have to make an informative error-object | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | func readLokiError(body io.ReadCloser) error { | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	return makeLokiError(bytes) | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames, error) { | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | 	req, err := makeDataRequest(ctx, api.url, query) | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01: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 { | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 		return nil, readLokiError(resp.Body) | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | 	iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) | 
					
						
							| 
									
										
										
										
											2022-05-24 16:17:11 -04:00
										 |  |  | 	res := converter.ReadPrometheusStyleResult(iter, converter.Options{MatrixWideSeries: false, VectorWideSeries: false}) | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if res.Error != nil { | 
					
						
							|  |  |  | 		return nil, res.Error | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-05 13:09:01 +02:00
										 |  |  | 	return res.Frames, nil | 
					
						
							| 
									
										
										
										
											2022-02-18 14:35:39 +01:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | func makeRawRequest(ctx context.Context, lokiDsUrl string, resourcePath string) (*http.Request, error) { | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	lokiUrl, err := url.Parse(lokiDsUrl) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-17 07:47:31 +02:00
										 |  |  | 	resourceUrl, err := url.Parse(resourcePath) | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-17 07:47:31 +02: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 12:42:50 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return req, nil | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) (RawLokiResponse, error) { | 
					
						
							| 
									
										
										
										
											2022-12-21 13:25:58 +01:00
										 |  |  | 	req, err := makeRawRequest(ctx, api.url, resourcePath) | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 		return RawLokiResponse{}, err | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	resp, err := api.client.Do(req) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 		return RawLokiResponse{}, err | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	defer func() { | 
					
						
							|  |  |  | 		if err := resp.Body.Close(); err != nil { | 
					
						
							|  |  |  | 			api.log.Warn("Failed to close response body", "err", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	// server errors are handled by the plugin-proxy to hide the error message | 
					
						
							|  |  |  | 	if resp.StatusCode/100 == 5 { | 
					
						
							|  |  |  | 		return RawLokiResponse{}, readLokiError(resp.Body) | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	body, err := io.ReadAll(resp.Body) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return RawLokiResponse{}, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	// client errors are passed as a json struct to the client | 
					
						
							|  |  |  | 	if resp.StatusCode/100 != 2 { | 
					
						
							|  |  |  | 		lokiResponseErr := lokiResponseError{Message: makeLokiError(body).Error()} | 
					
						
							|  |  |  | 		traceID := tracing.TraceIDFromContext(ctx, false) | 
					
						
							|  |  |  | 		if traceID != "" { | 
					
						
							|  |  |  | 			lokiResponseErr.TraceID = traceID | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		body, err = json.Marshal(lokiResponseErr) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return RawLokiResponse{}, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	rawLokiResponse := RawLokiResponse{ | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 		Body:     body, | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 		Status:   resp.StatusCode, | 
					
						
							| 
									
										
										
										
											2022-11-22 14:02:30 +01:00
										 |  |  | 		Encoding: resp.Header.Get("Content-Encoding"), | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-09 11:12:33 +01:00
										 |  |  | 	return rawLokiResponse, nil | 
					
						
							| 
									
										
										
										
											2022-04-25 13:16:14 +02:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2023-01-27 16:41:40 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | func getSupportingQueryHeaderValue(req *http.Request, supportingQueryType SupportingQueryType) string { | 
					
						
							|  |  |  | 	value := "" | 
					
						
							|  |  |  | 	switch supportingQueryType { | 
					
						
							|  |  |  | 	case SupportingQueryLogsVolume: | 
					
						
							|  |  |  | 		value = "logvolhist" | 
					
						
							|  |  |  | 	case SupportingQueryLogsSample: | 
					
						
							|  |  |  | 		value = "logsample" | 
					
						
							|  |  |  | 	case SupportingQueryDataSample: | 
					
						
							|  |  |  | 		value = "datasample" | 
					
						
							|  |  |  | 	default: //ignore | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return value | 
					
						
							|  |  |  | } |