* o7 compactor Signed-off-by: Joe Elliott <number101010@gmail.com> * wip: o7 v2 Signed-off-by: Joe Elliott <number101010@gmail.com> * test cleanup Signed-off-by: Joe Elliott <number101010@gmail.com> * remove compactor from jsonnet Signed-off-by: Joe Elliott <number101010@gmail.com> * changelog Signed-off-by: Joe Elliott <number101010@gmail.com> * gen manifest Signed-off-by: Joe Elliott <number101010@gmail.com> * config cleanup Signed-off-by: Joe Elliott <number101010@gmail.com> * docs Signed-off-by: Joe Elliott <number101010@gmail.com> * jsonnet Signed-off-by: Joe Elliott <number101010@gmail.com> * remove encoding Signed-off-by: Joe Elliott <number101010@gmail.com> * remove data encoding Signed-off-by: Joe Elliott <number101010@gmail.com> * lint and cleanup Signed-off-by: Joe Elliott <number101010@gmail.com> * unflake TestWorker\? Signed-off-by: Joe Elliott <number101010@gmail.com> --------- Signed-off-by: Joe Elliott <number101010@gmail.com>
1006 lines
31 KiB
Go
1006 lines
31 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/tempo/cmd/tempo-query/tempo"
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
)
|
|
|
|
func TestMarshalingFormatFromAcceptHeader(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
acceptHeader string
|
|
expected MarshallingFormat
|
|
}{
|
|
{name: "empty accept header", acceptHeader: "", expected: MarshallingFormatJSON},
|
|
{name: "json", acceptHeader: "application/json", expected: MarshallingFormatJSON},
|
|
{name: "protobuf", acceptHeader: "application/protobuf", expected: MarshallingFormatProtobuf},
|
|
{name: "simplified json", acceptHeader: "application/vnd.grafana.llm", expected: MarshallingFormatLLM},
|
|
{name: "invalid", acceptHeader: "application/invalid", expected: MarshallingFormatJSON},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual := MarshalingFormatFromAcceptHeader(http.Header{HeaderAccept: []string{tt.acceptHeader}})
|
|
assert.Equal(t, tt.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
// For licensing reasons these strings exist in two packages. This test exists to make sure they don't
|
|
// drift.
|
|
func TestEquality(t *testing.T) {
|
|
assert.Equal(t, HeaderAccept, tempo.AcceptHeaderKey)
|
|
assert.Equal(t, HeaderAcceptProtobuf, tempo.ProtobufTypeHeaderValue)
|
|
}
|
|
|
|
func TestQuerierParseSearchRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
urlQuery string
|
|
err string
|
|
expected *tempopb.SearchRequest
|
|
}{
|
|
{
|
|
name: "empty query",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "zero ranges",
|
|
urlQuery: "start=0&end=0",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "limit set",
|
|
urlQuery: "limit=10",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
Limit: 10,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "zero limit",
|
|
urlQuery: "limit=0",
|
|
err: "invalid limit: must be a positive number",
|
|
},
|
|
{
|
|
name: "negative limit",
|
|
urlQuery: "limit=-5",
|
|
err: "invalid limit: must be a positive number",
|
|
},
|
|
{
|
|
name: "non-numeric limit",
|
|
urlQuery: "limit=five",
|
|
err: "invalid limit: strconv.Atoi: parsing \"five\": invalid syntax",
|
|
},
|
|
{
|
|
name: "minDuration and maxDuration",
|
|
urlQuery: "minDuration=10s&maxDuration=20s",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
MinDurationMs: 10000,
|
|
MaxDurationMs: 20000,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "minDuration greater than maxDuration",
|
|
urlQuery: "minDuration=20s&maxDuration=5s",
|
|
err: "invalid maxDuration: must be greater than minDuration",
|
|
},
|
|
{
|
|
name: "invalid minDuration",
|
|
urlQuery: "minDuration=10seconds",
|
|
err: "invalid minDuration: time: unknown unit \"seconds\" in duration \"10seconds\"",
|
|
},
|
|
{
|
|
name: "invalid maxDuration",
|
|
urlQuery: "maxDuration=1msec",
|
|
err: "invalid maxDuration: time: unknown unit \"msec\" in duration \"1msec\"",
|
|
},
|
|
{
|
|
name: "traceql query",
|
|
urlQuery: "q=" + url.QueryEscape(`{ .foo="bar" }`),
|
|
expected: &tempopb.SearchRequest{
|
|
Query: `{ .foo="bar" }`,
|
|
Tags: map[string]string{},
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "traceql query and tags",
|
|
urlQuery: "q=" + url.QueryEscape(`{ .foo="bar" }`) + "&tags=" + url.QueryEscape("service.name=foo"),
|
|
err: "invalid request: can't specify tags and q in the same query",
|
|
},
|
|
{
|
|
name: "tags and limit",
|
|
urlQuery: "tags=" + url.QueryEscape("limit=five") + "&limit=5",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"limit": "five",
|
|
},
|
|
Limit: 5,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "tags query parameter with duplicate tag",
|
|
urlQuery: "tags=" + url.QueryEscape("service.name=foo service.name=bar"),
|
|
err: "invalid tags: tag service.name has been set twice",
|
|
},
|
|
{
|
|
name: "top-level tags with conflicting query parameter tags",
|
|
urlQuery: "service.name=bar&tags=" + url.QueryEscape("service.name=foo"),
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"service.name": "foo",
|
|
},
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "start and end both set",
|
|
urlQuery: "tags=" + url.QueryEscape("service.name=foo") + "&start=10&end=20",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"service.name": "foo",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "end before start",
|
|
urlQuery: "tags=" + url.QueryEscape("service.name=foo") + "&start=20&end=10",
|
|
err: "http parameter start must be before end. received start=20 end=10",
|
|
},
|
|
{
|
|
name: "top-level tags",
|
|
urlQuery: "service.name=bar",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"service.name": "bar",
|
|
},
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "top-level tags with range specified are ignored",
|
|
urlQuery: "service.name=bar&start=10&end=20",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
Start: 10,
|
|
End: 20,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
},
|
|
{
|
|
name: "zero spss",
|
|
urlQuery: "spss=0",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
SpansPerSpanSet: 0,
|
|
},
|
|
},
|
|
{
|
|
name: "negative spss",
|
|
urlQuery: "spss=-2",
|
|
err: "invalid spss: must be a non-negative number",
|
|
},
|
|
{
|
|
name: "non-numeric spss",
|
|
urlQuery: "spss=four",
|
|
err: "invalid spss: strconv.Atoi: parsing \"four\": invalid syntax",
|
|
},
|
|
{
|
|
name: "only spss",
|
|
urlQuery: "spss=2",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
SpansPerSpanSet: 2,
|
|
},
|
|
},
|
|
{
|
|
name: "tags with spss",
|
|
urlQuery: "tags=" + url.QueryEscape("service.name=foo") + "&spss=7",
|
|
expected: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"service.name": "foo",
|
|
},
|
|
SpansPerSpanSet: 7,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := httptest.NewRequest("GET", "http://tempo/api/search?"+tt.urlQuery, nil)
|
|
fmt.Println("RequestURI:", r.RequestURI)
|
|
|
|
searchRequest, err := ParseSearchRequest(r)
|
|
|
|
if tt.err != "" {
|
|
assert.EqualError(t, err, tt.err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expected, searchRequest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuerierParseSearchRequestTags(t *testing.T) {
|
|
type strMap map[string]string
|
|
|
|
tests := []struct {
|
|
tags string
|
|
expected map[string]string
|
|
}{
|
|
{"service.name=foo http.url=api/search", strMap{"service.name": "foo", "http.url": "api/search"}},
|
|
{"service%n@me=foo", strMap{"service%n@me": "foo"}},
|
|
{"service.name=foo error", strMap{"service.name": "foo", "error": ""}},
|
|
{"service.name=\"foo bar\"", strMap{"service.name": "foo bar"}},
|
|
{"service.name=\"foo=bar\"", strMap{"service.name": "foo=bar"}},
|
|
{"service.name=\"foo\\bar\"", strMap{"service.name": "foo\bar"}},
|
|
{"service.name=\"foo \\\"bar\\\"\"", strMap{"service.name": "foo \"bar\""}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.tags, func(t *testing.T) {
|
|
r := httptest.NewRequest("GET", "http://tempo/api/search?tags="+url.QueryEscape(tt.tags), nil)
|
|
fmt.Println("RequestURI:", r.RequestURI)
|
|
|
|
searchRequest, err := ParseSearchRequest(r)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expected, searchRequest.Tags)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuerierParseSearchRequestTagsError(t *testing.T) {
|
|
tests := []struct {
|
|
tags string
|
|
err string
|
|
}{
|
|
{"service.name=foo =error", "invalid tags: unexpected '=' at pos 18"},
|
|
{"service.name=foo=bar", "invalid tags: unexpected '=' at pos 17"},
|
|
{"service.name=\"foo bar", "invalid tags: unterminated quoted value at pos 22"},
|
|
{"\"service name\"=foo", "invalid tags: unexpected '\"' at pos 1"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.tags, func(t *testing.T) {
|
|
r := httptest.NewRequest("GET", "http://tempo/api/search?tags="+url.QueryEscape(tt.tags), nil)
|
|
fmt.Println("RequestURI:", r.RequestURI)
|
|
|
|
_, err := ParseSearchRequest(r)
|
|
|
|
assert.EqualError(t, err, tt.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSearchBlockRequest(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
expected *tempopb.SearchBlockRequest
|
|
expectedError string
|
|
}{
|
|
{
|
|
url: "/",
|
|
expectedError: "start and end required",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20",
|
|
expectedError: "invalid startPage: strconv.ParseInt: parsing \"\": invalid syntax",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0",
|
|
expectedError: "invalid pagesToSearch : strconv.ParseInt: parsing \"\": invalid syntax",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10",
|
|
expectedError: "invalid blockID: invalid UUID length: 0",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=adsf",
|
|
expectedError: "invalid blockID: invalid UUID length: 4",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=s2",
|
|
expectedError: "invalid indexPageSize : strconv.ParseInt: parsing \"\": invalid syntax",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=s2&indexPageSize=10",
|
|
expectedError: "invalid totalRecords : strconv.ParseInt: parsing \"\": invalid syntax",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=s2&indexPageSize=10&totalRecords=-1",
|
|
expectedError: "totalRecords must be greater than 0. received -1",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=s2&indexPageSize=10&totalRecords=11&dataEncoding=v1",
|
|
expectedError: "version required",
|
|
},
|
|
{
|
|
url: "/?start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=s2&indexPageSize=10&totalRecords=11&dataEncoding=v1&version=v2&size=1000",
|
|
expectedError: "invalid footerSize : strconv.ParseUint: parsing \"\": invalid syntax",
|
|
},
|
|
{
|
|
url: "/?tags=foo%3Dbar&start=10&end=20&startPage=0&pagesToSearch=10&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&encoding=none&footerSize=2000&indexPageSize=10&totalRecords=11&dataEncoding=v1&version=v2&size=1000",
|
|
expected: &tempopb.SearchBlockRequest{
|
|
SearchReq: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
Limit: defaultLimit,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 10,
|
|
TotalRecords: 11,
|
|
Version: "v2",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
},
|
|
},
|
|
{
|
|
url: "/?tags=foo%3Dbar&start=10&end=20&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&dataEncoding=&dc=%5B%7B%22type%22%3A0%2C%22name%22%3A%22net.sock.host.addr%22%2C%22scope%22%3A0%7D%5D&encoding=none&footerSize=2000&indexPageSize=0&pagesToSearch=10&size=1000&startPage=0&totalRecords=2&version=vParquet3",
|
|
expected: &tempopb.SearchBlockRequest{
|
|
SearchReq: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
Limit: defaultLimit,
|
|
SpansPerSpanSet: defaultSpansPerSpanSet,
|
|
},
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 0,
|
|
TotalRecords: 2,
|
|
Version: "vParquet3",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
DedicatedColumns: []*tempopb.DedicatedColumn{
|
|
{Scope: tempopb.DedicatedColumn_SPAN, Name: "net.sock.host.addr", Type: tempopb.DedicatedColumn_STRING},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
r := httptest.NewRequest("GET", tc.url, nil)
|
|
actualReq, actualErr := ParseSearchBlockRequest(r)
|
|
|
|
if len(tc.expectedError) != 0 {
|
|
assert.EqualError(t, actualErr, tc.expectedError)
|
|
assert.Nil(t, actualReq)
|
|
continue
|
|
}
|
|
assert.Equal(t, tc.expected, actualReq)
|
|
}
|
|
}
|
|
|
|
func TestBuildSearchBlockRequest(t *testing.T) {
|
|
tests := []struct {
|
|
req *tempopb.SearchBlockRequest
|
|
httpReq *http.Request
|
|
query string
|
|
}{
|
|
{
|
|
req: &tempopb.SearchBlockRequest{
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 10,
|
|
TotalRecords: 11,
|
|
Version: "v2",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
},
|
|
query: "?blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&pagesToSearch=10&size=1000&startPage=0&encoding=none&indexPageSize=10&totalRecords=11&version=v2&footerSize=2000",
|
|
},
|
|
{
|
|
req: &tempopb.SearchBlockRequest{
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 10,
|
|
TotalRecords: 11,
|
|
Version: "v2",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
},
|
|
httpReq: httptest.NewRequest("GET", "/test/path", nil),
|
|
query: "/test/path?blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&pagesToSearch=10&size=1000&startPage=0&encoding=none&indexPageSize=10&totalRecords=11&version=v2&footerSize=2000",
|
|
},
|
|
{
|
|
req: &tempopb.SearchBlockRequest{
|
|
SearchReq: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
MinDurationMs: 30,
|
|
MaxDurationMs: 40,
|
|
Limit: 50,
|
|
},
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 10,
|
|
TotalRecords: 11,
|
|
Version: "v2",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
},
|
|
query: "?start=10&end=20&limit=50&maxDuration=40ms&minDuration=30ms&spss=0&tags=foo%3Dbar&blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&pagesToSearch=10&size=1000&startPage=0&encoding=none&indexPageSize=10&totalRecords=11&version=v2&footerSize=2000",
|
|
},
|
|
{
|
|
req: &tempopb.SearchBlockRequest{
|
|
StartPage: 0,
|
|
PagesToSearch: 10,
|
|
BlockID: "b92ec614-3fd7-4299-b6db-f657e7025a9b",
|
|
IndexPageSize: 0,
|
|
TotalRecords: 2,
|
|
Version: "vParquet3",
|
|
Size_: 1000,
|
|
FooterSize: 2000,
|
|
DedicatedColumns: []*tempopb.DedicatedColumn{
|
|
{Scope: tempopb.DedicatedColumn_RESOURCE, Name: "net.sock.host.addr", Type: tempopb.DedicatedColumn_STRING},
|
|
},
|
|
},
|
|
httpReq: httptest.NewRequest("GET", "/test/path", nil),
|
|
query: "/test/path?blockID=b92ec614-3fd7-4299-b6db-f657e7025a9b&pagesToSearch=10&size=1000&startPage=0&encoding=none&indexPageSize=0&totalRecords=2&version=vParquet3&footerSize=2000&dc=%5B%7B%22scope%22%3A1%2C%22name%22%3A%22net.sock.host.addr%22%7D%5D",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
jsonBytes, err := json.Marshal(tc.req.DedicatedColumns)
|
|
require.NoError(t, err)
|
|
|
|
actualURL, err := BuildSearchBlockRequest(tc.httpReq, tc.req, string(jsonBytes))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.query, actualURL.URL.String())
|
|
}
|
|
}
|
|
|
|
func TestValidateAndSanitizeRequest(t *testing.T) {
|
|
rf1After := time.Date(1970, 1, 1, 1, 16, 40, 0, time.UTC)
|
|
tests := []struct {
|
|
httpReq *http.Request
|
|
queryMode string
|
|
startTime int64
|
|
endTime int64
|
|
blockStart string
|
|
blockEnd string
|
|
rf1After time.Time
|
|
expectedError string
|
|
}{
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?blockEnd=ffffffffffffffffffffffffffffffff&blockStart=00000000000000000000000000000000&mode=blocks&start=1&end=2", nil),
|
|
queryMode: "blocks",
|
|
startTime: 1,
|
|
endTime: 2,
|
|
blockStart: "00000000000000000000000000000000",
|
|
blockEnd: "ffffffffffffffffffffffffffffffff",
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?blockEnd=ffffffffffffffffffffffffffffffff&blockStart=00000000000000000000000000000000&mode=blocks", nil),
|
|
queryMode: "blocks",
|
|
startTime: 0,
|
|
endTime: 0,
|
|
blockStart: "00000000000000000000000000000000",
|
|
blockEnd: "ffffffffffffffffffffffffffffffff",
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?mode=blocks", nil),
|
|
queryMode: "blocks",
|
|
startTime: 0,
|
|
endTime: 0,
|
|
blockStart: "00000000-0000-0000-0000-000000000000",
|
|
blockEnd: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?mode=blocks&blockStart=12345678000000001235000001240000&blockEnd=ffffffffffffffffffffffffffffffff", nil),
|
|
queryMode: "blocks",
|
|
startTime: 0,
|
|
endTime: 0,
|
|
blockStart: "12345678000000001235000001240000",
|
|
blockEnd: "ffffffffffffffffffffffffffffffff",
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?mode=blocks&blockStart=12345678000000001235000001240000&blockEnd=ffffffffffffffffffffffffffffffff&rf1After=1970-01-01T01:16:40Z", nil),
|
|
queryMode: "blocks",
|
|
startTime: 0,
|
|
endTime: 0,
|
|
blockStart: "12345678000000001235000001240000",
|
|
blockEnd: "ffffffffffffffffffffffffffffffff",
|
|
rf1After: rf1After,
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?mode=blocks&blockStart=12345678000000001235000001240000&blockEnd=ffffffffffffffffffffffffffffffff&start=1&end=1", nil),
|
|
queryMode: "blocks",
|
|
startTime: 0,
|
|
endTime: 0,
|
|
blockStart: "12345678000000001235000001240000",
|
|
blockEnd: "ffffffffffffffffffffffffffffffff",
|
|
expectedError: "http parameter start must be before end. received start=1 end=1",
|
|
},
|
|
{
|
|
httpReq: httptest.NewRequest("GET", "/api/traces/1234?mode=external&start=1&end=2", nil),
|
|
queryMode: "external",
|
|
startTime: 1,
|
|
endTime: 2,
|
|
blockStart: "00000000-0000-0000-0000-000000000000",
|
|
blockEnd: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
blockStart, blockEnd, queryMode, startTime, endTime, rf1After, err := ValidateAndSanitizeRequest(tc.httpReq)
|
|
if len(tc.expectedError) != 0 {
|
|
assert.EqualError(t, err, tc.expectedError)
|
|
continue
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.queryMode, queryMode)
|
|
assert.Equal(t, tc.blockStart, blockStart)
|
|
assert.Equal(t, tc.blockEnd, blockEnd)
|
|
assert.Equal(t, tc.startTime, startTime)
|
|
assert.Equal(t, tc.endTime, endTime)
|
|
assert.Equal(t, tc.rf1After, rf1After)
|
|
}
|
|
}
|
|
|
|
func TestBuildSearchRequest(t *testing.T) {
|
|
tests := []struct {
|
|
req *tempopb.SearchRequest
|
|
httpReq *http.Request
|
|
query string
|
|
}{
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
MinDurationMs: 30,
|
|
MaxDurationMs: 40,
|
|
Limit: 50,
|
|
SpansPerSpanSet: 60,
|
|
},
|
|
query: "?start=10&end=20&limit=50&maxDuration=40ms&minDuration=30ms&spss=60&tags=foo%3Dbar",
|
|
},
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
MaxDurationMs: 30,
|
|
Limit: 50,
|
|
},
|
|
query: "?start=10&end=20&limit=50&maxDuration=30ms&spss=0&tags=foo%3Dbar",
|
|
},
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
MinDurationMs: 30,
|
|
Limit: 50,
|
|
},
|
|
query: "?start=10&end=20&limit=50&minDuration=30ms&spss=0&tags=foo%3Dbar",
|
|
},
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Start: 10,
|
|
End: 20,
|
|
MinDurationMs: 30,
|
|
MaxDurationMs: 40,
|
|
},
|
|
query: "?start=10&end=20&maxDuration=40ms&minDuration=30ms&spss=0&tags=foo%3Dbar",
|
|
},
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Tags: map[string]string{},
|
|
Start: 10,
|
|
End: 20,
|
|
MinDurationMs: 30,
|
|
MaxDurationMs: 40,
|
|
},
|
|
query: "?start=10&end=20&maxDuration=40ms&minDuration=30ms&spss=0",
|
|
},
|
|
{
|
|
req: &tempopb.SearchRequest{
|
|
Query: "{ foo = `bar` }",
|
|
Start: 10,
|
|
End: 20,
|
|
},
|
|
query: "?start=10&end=20&spss=0&q=%7B+foo+%3D+%60bar%60+%7D",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
actualURL, err := BuildSearchRequest(tc.httpReq, tc.req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.query, actualURL.URL.String())
|
|
}
|
|
}
|
|
|
|
func Test_parseTimestamp(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
def time.Time
|
|
want time.Time
|
|
|
|
wantErr bool
|
|
}{
|
|
{"default", "", now, now, false},
|
|
{"unix timestamp", "1571332130", now, time.Unix(1571332130, 0), false},
|
|
{"unix nano timestamp", "1571334162051000000", now, time.Unix(0, 1571334162051000000), false},
|
|
{"unix timestamp with subseconds", "1571332130.934", now, time.Unix(1571332130, 934*1e6), false},
|
|
{"RFC3339 format", "2002-10-02T15:00:00Z", now, time.Date(2002, 10, 0o2, 15, 0, 0, 0, time.UTC), false},
|
|
{"RFC3339nano format", "2009-11-10T23:00:00.000000001Z", now, time.Date(2009, 11, 10, 23, 0, 0, 1, time.UTC), false},
|
|
{"invalid", "we", now, time.Time{}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseTimestamp(tt.value, tt.def)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseTimestamp() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
assert.Equal(t, got, tt.want, fmt.Sprintf("parseTimestamp() = %v, want %v", got, tt.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryRangeRoundtripEmpty(t *testing.T) {
|
|
req := &tempopb.QueryRangeRequest{
|
|
Step: uint64(time.Second), // you can't actually roundtrip an empty query b/c Build/Parse will force a default step
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(req.DedicatedColumns)
|
|
require.NoError(t, err)
|
|
|
|
httpReq := BuildQueryRangeRequest(nil, req, string(jsonBytes))
|
|
actualReq, err := ParseQueryRangeRequest(httpReq)
|
|
require.NoError(t, err)
|
|
assert.True(t, actualReq.Start != 0)
|
|
assert.True(t, actualReq.End != 0)
|
|
}
|
|
|
|
func TestQueryRangeRoundtrip(t *testing.T) {
|
|
req := &tempopb.QueryRangeRequest{
|
|
Query: "{ foo = `bar` }",
|
|
Start: uint64(24 * time.Hour),
|
|
End: uint64(25 * time.Hour),
|
|
Step: uint64(30 * time.Second),
|
|
QueryMode: "foo",
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(req.DedicatedColumns)
|
|
require.NoError(t, err)
|
|
|
|
httpReq := BuildQueryRangeRequest(nil, req, string(jsonBytes))
|
|
actualReq, err := ParseQueryRangeRequest(httpReq)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, req, actualReq)
|
|
}
|
|
|
|
func Test_determineBounds(t *testing.T) {
|
|
type args struct {
|
|
now time.Time
|
|
startString string
|
|
endString string
|
|
sinceString string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
start time.Time
|
|
end time.Time
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "no start, end, since",
|
|
args: args{
|
|
now: time.Unix(3600, 0),
|
|
startString: "",
|
|
endString: "",
|
|
sinceString: "",
|
|
},
|
|
start: time.Unix(0, 0), // Default start is one hour before 'now' if nothing is provided
|
|
end: time.Unix(3600, 0), // Default end is 'now' if nothing is provided
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "no since or no start with end in the future",
|
|
args: args{
|
|
now: time.Unix(3600, 0),
|
|
startString: "",
|
|
endString: "2022-12-18T00:00:00Z",
|
|
sinceString: "",
|
|
},
|
|
start: time.Unix(0, 0), // Default should be one hour before now
|
|
end: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "no since, valid start and end",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "2022-12-17T00:00:00Z",
|
|
endString: "2022-12-18T00:00:00Z",
|
|
sinceString: "",
|
|
},
|
|
start: time.Date(2022, 12, 17, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "invalid end",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "2022-12-17T00:00:00Z",
|
|
endString: "WHAT TIME IS IT?",
|
|
sinceString: "",
|
|
},
|
|
start: time.Time{},
|
|
end: time.Time{},
|
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
|
return assert.ErrorContains(t, err, "could not parse 'end' parameter:", i...)
|
|
},
|
|
},
|
|
{
|
|
name: "invalid start",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "LET'S GOOO",
|
|
endString: "2022-12-18T00:00:00Z",
|
|
sinceString: "",
|
|
},
|
|
start: time.Time{},
|
|
end: time.Time{},
|
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
|
return assert.ErrorContains(t, err, "could not parse 'start' parameter:", i...)
|
|
},
|
|
},
|
|
{
|
|
name: "invalid since",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "2022-12-17T00:00:00Z",
|
|
endString: "2022-12-18T00:00:00Z",
|
|
sinceString: "HI!",
|
|
},
|
|
start: time.Time{},
|
|
end: time.Time{},
|
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
|
return assert.ErrorContains(t, err, "could not parse 'since' parameter:", i...)
|
|
},
|
|
},
|
|
{
|
|
name: "since 1h with no start or end",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "",
|
|
endString: "",
|
|
sinceString: "1h",
|
|
},
|
|
start: time.Date(2022, 12, 17, 23, 0, 0, 0, time.UTC),
|
|
end: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "since 1d with no start or end",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "",
|
|
endString: "",
|
|
sinceString: "1d",
|
|
},
|
|
start: time.Date(2022, 12, 17, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "since 1h with no start and end time in the past",
|
|
args: args{
|
|
now: time.Date(2022, 12, 18, 0, 0, 0, 0, time.UTC),
|
|
startString: "",
|
|
endString: "2022-12-17T00:00:00Z",
|
|
sinceString: "1h",
|
|
},
|
|
start: time.Date(2022, 12, 16, 23, 0, 0, 0, time.UTC), // start should be calculated relative to end when end is specified
|
|
end: time.Date(2022, 12, 17, 0, 0, 0, 0, time.UTC),
|
|
wantErr: assert.NoError,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, got1, err := determineBounds(tt.args.now, tt.args.startString, tt.args.endString, tt.args.sinceString)
|
|
if !tt.wantErr(t, err, fmt.Sprintf("determineBounds(%v, %v, %v, %v)", tt.args.now, tt.args.startString, tt.args.endString, tt.args.sinceString)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.start, got, "determineBounds(%v, %v, %v, %v)", tt.args.now, tt.args.startString, tt.args.endString, tt.args.sinceString)
|
|
assert.Equalf(t, tt.end, got1, "determineBounds(%v, %v, %v, %v)", tt.args.now, tt.args.startString, tt.args.endString, tt.args.sinceString)
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeReq(params map[string]string) *http.Request {
|
|
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
|
q := req.URL.Query()
|
|
for k, v := range params {
|
|
q.Set(k, v)
|
|
}
|
|
req.URL.RawQuery = q.Encode()
|
|
return req
|
|
}
|
|
|
|
func TestClampDateRangeReq(t *testing.T) {
|
|
defStart := time.Hour
|
|
endBuffer := 5 * time.Minute
|
|
refNow := time.Date(2023, 10, 15, 12, 0, 0, 0, time.Local)
|
|
|
|
// Since parameter tests
|
|
sinceTests := []struct {
|
|
name string
|
|
params map[string]string
|
|
expectedErr string
|
|
sinceDur time.Duration
|
|
}{
|
|
{"valid 1h", map[string]string{"since": "1h"}, "", time.Hour},
|
|
{"ignored start+end", map[string]string{"since": "2h", "start": "1697360400", "end": "1697364000"}, "", 2 * time.Hour},
|
|
{"invalid format", map[string]string{"since": "invalid"}, "could not parse 'since' parameter", 0},
|
|
}
|
|
|
|
for _, tt := range sinceTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
time.Sleep(time.Until(refNow)) // Setting time.now to refNow
|
|
req := makeReq(tt.params)
|
|
start, end, err := ClampDateRangeReq(req, defStart, endBuffer)
|
|
|
|
if tt.expectedErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.expectedErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
expectedEnd := refNow.Add(-endBuffer)
|
|
expectedStart := expectedEnd.Add(-tt.sinceDur)
|
|
assert.Equal(t, expectedStart, start)
|
|
assert.Equal(t, expectedEnd, end)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Default no-params test
|
|
t.Run("default no parameters", func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
time.Sleep(time.Until(refNow)) // Setting time.now to refNow
|
|
req := makeReq(nil)
|
|
start, end, err := ClampDateRangeReq(req, defStart, endBuffer)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, refNow.Add(-defStart), start)
|
|
assert.Equal(t, refNow.Add(-endBuffer), end)
|
|
})
|
|
})
|
|
|
|
// Start/end validation tests
|
|
testsStartEndValidation := []struct {
|
|
name string
|
|
params map[string]string
|
|
err string
|
|
}{
|
|
{"only start", map[string]string{"start": "1697360400"}, "only one of start and end provided"},
|
|
{"only end", map[string]string{"end": "1697364000"}, "only one of start and end provided"},
|
|
{"start empty", map[string]string{"start": "", "end": "1697364000"}, "only one of start and end provided"},
|
|
{"end empty", map[string]string{"start": "1697360400", "end": ""}, "only one of start and end provided"},
|
|
}
|
|
|
|
for _, tt := range testsStartEndValidation {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := makeReq(tt.params)
|
|
_, _, err := ClampDateRangeReq(req, defStart, endBuffer)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.err)
|
|
})
|
|
}
|
|
|
|
validEnd := refNow.Add(-endBuffer * 2)
|
|
// End time clamping tests
|
|
testsEndClamping := []struct {
|
|
name string
|
|
params map[string]string
|
|
expectedStart time.Time
|
|
expectedEnd time.Time
|
|
}{
|
|
{"end clamped", map[string]string{"start": "1697360400", "end": "9999999999"}, time.Unix(1697360400, 0), refNow.Add(-endBuffer)},
|
|
{"end not clamped", map[string]string{"start": "1000000000", "end": strconv.FormatInt(validEnd.Unix(), 10)}, time.Unix(1000000000, 0), validEnd},
|
|
}
|
|
|
|
for _, tt := range testsEndClamping {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
time.Sleep(time.Until(refNow)) // Setting time.now to refNow
|
|
req := makeReq(tt.params)
|
|
start, end, err := ClampDateRangeReq(req, defStart, endBuffer)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.expectedStart, start)
|
|
assert.WithinDuration(t, tt.expectedEnd, end, time.Second)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Invalid timestamp parsing tests
|
|
testsInvalidParsing := []struct {
|
|
name string
|
|
params map[string]string
|
|
err string
|
|
}{
|
|
{"invalid start", map[string]string{"start": "invalid", "end": "1697364000"}, "could not parse 'start' parameter"},
|
|
{"invalid end", map[string]string{"start": "1697360400", "end": "invalid"}, "could not parse 'end' parameter"},
|
|
// {"start wrong length", map[string]string{"start": "123456", "end": "1697364000"}, "could not parse 'start' parameter"},
|
|
// {"end wrong length", map[string]string{"start": "1697360400", "end": "123456789012345"}, "could not parse 'end' parameter"},
|
|
}
|
|
|
|
for _, tt := range testsInvalidParsing {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
req := makeReq(tt.params)
|
|
_, _, err := ClampDateRangeReq(req, defStart, endBuffer)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.err)
|
|
})
|
|
})
|
|
}
|
|
}
|