This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
tenantID string
|
||||
username string
|
||||
password string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, tenantID, username, password string) *Client {
|
||||
return &Client{baseURL: strings.TrimRight(baseURL, "/"), tenantID: tenantID, username: username, password: password, http: &http.Client{Timeout: 20 * time.Second}}
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Line string `json:"line"`
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Count int `json:"count"`
|
||||
Matches []Match `json:"matches"`
|
||||
}
|
||||
|
||||
func (c *Client) Ready(ctx context.Context) error {
|
||||
req, err := c.request(ctx, http.MethodGet, "/ready", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return fmt.Errorf("loki ready failed: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Labels(ctx context.Context) ([]string, error) {
|
||||
var raw labelsResponse
|
||||
if err := c.getJSON(ctx, "/loki/api/v1/labels", nil, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) LabelValues(ctx context.Context, name string) ([]string, error) {
|
||||
if name == "" || strings.Contains(name, "/") {
|
||||
return nil, fmt.Errorf("invalid label name")
|
||||
}
|
||||
var raw labelsResponse
|
||||
if err := c.getJSON(ctx, "/loki/api/v1/label/"+url.PathEscape(name)+"/values", nil, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryRange(ctx context.Context, logql string, since time.Duration, limit int) (QueryResult, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("query", logql)
|
||||
values.Set("start", strconv.FormatInt(time.Now().Add(-since).UnixNano(), 10))
|
||||
values.Set("end", strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||
values.Set("limit", strconv.Itoa(limit))
|
||||
var raw response
|
||||
if err := c.getJSON(ctx, "/loki/api/v1/query_range", values, &raw); err != nil {
|
||||
return QueryResult{}, err
|
||||
}
|
||||
var result QueryResult
|
||||
for _, stream := range raw.Data.Result {
|
||||
for _, pair := range stream.Values {
|
||||
if len(pair) != 2 {
|
||||
continue
|
||||
}
|
||||
ns, _ := strconv.ParseInt(pair[0], 10, 64)
|
||||
result.Matches = append(result.Matches, Match{Timestamp: time.Unix(0, ns), Line: pair[1]})
|
||||
}
|
||||
}
|
||||
result.Count = len(result.Matches)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) getJSON(ctx context.Context, path string, values url.Values, out any) error {
|
||||
req, err := c.request(ctx, http.MethodGet, path, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return fmt.Errorf("loki request failed: status %d", resp.StatusCode)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func (c *Client) request(ctx context.Context, method, path string, values url.Values) (*http.Request, error) {
|
||||
if c.baseURL == "" {
|
||||
return nil, fmt.Errorf("LOKI_URL is not configured")
|
||||
}
|
||||
target := c.baseURL + path
|
||||
if len(values) > 0 {
|
||||
target += "?" + values.Encode()
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.tenantID != "" {
|
||||
req.Header.Set("X-Scope-OrgID", c.tenantID)
|
||||
}
|
||||
if c.username != "" || c.password != "" {
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type labelsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Result []struct {
|
||||
Stream map[string]string `json:"stream"`
|
||||
Values [][]string `json:"values"`
|
||||
} `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLabelsValuesAndQueryRange(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/loki/api/v1/labels":
|
||||
_, _ = w.Write([]byte(`{"status":"success","data":["service","container"]}`))
|
||||
case "/loki/api/v1/label/service/values":
|
||||
_, _ = w.Write([]byte(`{"status":"success","data":["api"]}`))
|
||||
case "/loki/api/v1/query_range":
|
||||
if !strings.Contains(r.URL.Query().Get("query"), "api") {
|
||||
t.Fatalf("query not passed through: %s", r.URL.RawQuery)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"status":"success","data":{"result":[{"stream":{"service":"api"},"values":[["1700000000000000000","error token=secret"]]}]}}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New(ts.URL, "", "", "")
|
||||
labels, err := client.Labels(t.Context())
|
||||
if err != nil || len(labels) != 2 {
|
||||
t.Fatalf("labels=%v err=%v", labels, err)
|
||||
}
|
||||
values, err := client.LabelValues(t.Context(), "service")
|
||||
if err != nil || values[0] != "api" {
|
||||
t.Fatalf("values=%v err=%v", values, err)
|
||||
}
|
||||
result, err := client.QueryRange(t.Context(), `{service="api"}`, time.Minute, 10)
|
||||
if err != nil || result.Count != 1 {
|
||||
t.Fatalf("result=%#v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user