Files
grafana/pkg/tsdb/jaeger/client_test.go
Gareth Dawson 977e923555 Jaeger: Migrate "search" query type to backend (#103399)
* jaeger backend migration

* move processing to JaegerClient.Search

* fix upload query error source type

* suggestions

* lint

* fix link to traceid query

* fix tests

* fix tests

* use consistent types

* add tests for TransformSearchResponse fn

* test search function

* fix filtering using tags

* suggestion

* remove unnecessary arguments

* use logfmt parser for tags

* test

* test

* use logfmt for query tags

* update

* go fmt

* run backend for all queryTypes

* run make update-workspace

* assign owner to logfmt pkg in go.mod

* apply suggestions

* update tests

* trigger workflows?
2025-05-07 09:34:04 +01:00

491 lines
14 KiB
Go

package jaeger
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/stretchr/testify/assert"
)
func TestJaegerClient_Services(t *testing.T) {
tests := []struct {
name string
mockResponse string
mockStatusCode int
mockStatus string
expectedResult []string
expectError bool
expectedError error
}{
{
name: "Successful response",
mockResponse: `{"data": ["service1", "service2"], "total": 2, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"service1", "service2"},
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedResult: []string{},
expectError: true,
expectedError: errors.New("Internal Server Error"),
},
{
name: "Invalid JSON response",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: true,
expectedError: &json.SyntaxError{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
services, err := client.Services()
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, services)
}
})
}
}
func TestJaegerClient_Operations(t *testing.T) {
tests := []struct {
name string
service string
mockResponse string
mockStatusCode int
mockStatus string
expectedResult []string
expectError bool
expectedError error
}{
{
name: "Successful response",
service: "test-service",
mockResponse: `{"data": ["operation1", "operation2"], "total": 2, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"operation1", "operation2"},
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
service: "test-service",
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedResult: []string{},
expectError: true,
expectedError: errors.New("Internal Server Error"),
},
{
name: "Invalid JSON response",
service: "test-service",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: true,
expectedError: &json.SyntaxError{},
},
{
name: "Service with special characters",
service: "test/service:1",
mockResponse: `{"data": ["operation1"], "total": 1, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"operation1"},
expectError: false,
expectedError: nil,
},
{
name: "Empty service",
service: "",
mockResponse: `{"data": [], "total": 0, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: false,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
operations, err := client.Operations(tt.service)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, operations)
}
})
}
}
func TestJaegerClient_Search(t *testing.T) {
tests := []struct {
name string
query *JaegerQuery
start int64
end int64
mockResponse string
mockStatusCode int
expectedURL string
expectError bool
expectedError error
}{
{
name: "Successful search with all parameters",
query: &JaegerQuery{
Service: "test-service",
Operation: "test-operation",
Tags: "error=true",
MinDuration: "1s",
MaxDuration: "5s",
Limit: 10,
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: `{"data":[{"traceID":"test-trace-id"}]}`,
mockStatusCode: http.StatusOK,
expectedURL: "/api/traces?end=1738368000000000&limit=10&maxDuration=5s&minDuration=1s&operation=test-operation&service=test-service&start=1735689600000000&tags=%7B%22error%22%3A%22true%22%7D",
expectError: false,
expectedError: nil,
},
{
name: "Successful search with minimal parameters",
query: &JaegerQuery{
Service: "test-service",
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: `{"data":[{"traceID":"test-trace-id"}]}`,
mockStatusCode: http.StatusOK,
expectedURL: "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
expectError: false,
expectedError: nil,
},
{
name: "Server error",
query: &JaegerQuery{
Service: "test-service",
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
expectedURL: "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
expectError: true,
expectedError: backend.DownstreamError(fmt.Errorf("request failed: %s", http.StatusText(http.StatusInternalServerError))),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actualURL string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actualURL = r.URL.String()
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
traces, err := client.Search(tt.query, tt.start, tt.end)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, traces)
assert.Equal(t, tt.expectedURL, actualURL)
}
})
}
}
func TestJaegerClient_Trace(t *testing.T) {
tests := []struct {
name string
traceId string
jsonData string
start int64
end int64
mockResponse string
mockStatusCode int
mockStatus string
expectedURL string
expectError bool
expectedError error
}{
{
name: "Successful response with time params enabled",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": true}}`,
start: 1000,
end: 2000,
mockResponse: `{"data":[{"traceID":"abc123"}]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/traces/abc123?end=2000&start=1000",
expectError: false,
expectedError: nil,
},
{
name: "Successful response with time params disabled",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": false}}`,
start: 1000,
end: 2000,
mockResponse: `{"data":[{"traceID":"abc123"}]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/traces/abc123",
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": true}}`,
start: 1000,
end: 2000,
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedURL: "/api/traces/abc123?end=2000&start=1000",
expectError: true,
expectedError: backend.PluginError(errors.New("Internal Server Error")),
},
{
name: "Invalid JSON response",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": true}}`,
start: 1000,
end: 2000,
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/traces/abc123?end=2000&start=1000",
expectError: true,
expectedError: &json.SyntaxError{},
},
{
name: "Empty trace ID",
traceId: "",
jsonData: `{"traceIdTimeParams": {"enabled": true}}`,
start: 1000,
end: 2000,
mockResponse: `{"data":[]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "",
expectError: true,
expectedError: backend.DownstreamError(errors.New("traceID is empty")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actualURL string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actualURL = r.URL.String()
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
JSONData: []byte(tt.jsonData),
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, trace)
}
assert.Equal(t, tt.expectedURL, actualURL)
})
}
}
func TestJaegerClient_Dependencies(t *testing.T) {
tests := []struct {
name string
start int64
end int64
mockResponse string
mockStatusCode int
mockStatus string
expectedURL string
expectError bool
expectedError error
}{
{
name: "Successful response with time range",
start: 1000,
end: 2000,
mockResponse: `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/dependencies?endTs=2000&lookback=1000",
expectError: false,
expectedError: nil,
},
{
name: "Successful response without time range",
start: 0,
end: 0,
mockResponse: `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/dependencies",
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
start: 1000,
end: 2000,
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedURL: "/api/dependencies?endTs=2000&lookback=1000",
expectError: true,
expectedError: backend.PluginError(errors.New("Internal Server Error")),
},
{
name: "Invalid JSON response",
start: 1000,
end: 2000,
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/dependencies?endTs=2000&lookback=1000",
expectError: true,
expectedError: &json.SyntaxError{},
},
{
name: "Empty dependencies response and no errors",
start: 1000,
end: 2000,
mockResponse: `{"data":[]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/dependencies?endTs=2000&lookback=1000",
expectError: false,
expectedError: nil,
},
{
name: "Response with errors",
start: 1000,
end: 2000,
mockResponse: `{"data":[],"errors":[{"code":500,"msg":"Internal error"}]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "/api/dependencies?endTs=2000&lookback=1000",
expectError: false,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actualURL string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actualURL = r.URL.String()
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
dependencies, err := client.Dependencies(context.Background(), tt.start, tt.end)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, dependencies)
}
assert.Equal(t, tt.expectedURL, actualURL)
})
}
}