Skip to content

Commit 33aa1f5

Browse files
committed
tracing: expand tracing test cover for new attributes
This commit's test modifications were developed with the assistance of a LLM tool. Signed-off-by: Craig Ringer <craig.ringer@enterprisedb.com>
1 parent bc5b9fd commit 33aa1f5

1 file changed

Lines changed: 280 additions & 52 deletions

File tree

test/e2e/tracing_test.go

Lines changed: 280 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,70 +5,220 @@ package e2e_test
55

66
import (
77
"context"
8+
"encoding/json"
89
"fmt"
910
"io"
1011
"net/http"
12+
"strconv"
1113
"strings"
1214
"testing"
1315
"time"
1416

17+
"github.com/efficientgo/core/testutil"
18+
"github.com/efficientgo/e2e"
19+
e2eobs "github.com/efficientgo/e2e/observable"
1520
"github.com/pkg/errors"
1621
"github.com/prometheus/common/model"
1722
"github.com/thanos-io/thanos/pkg/promclient"
1823
"github.com/thanos-io/thanos/pkg/runutil"
1924
"github.com/thanos-io/thanos/pkg/tracing/client"
20-
"github.com/thanos-io/thanos/pkg/tracing/jaeger"
25+
jaegercfg "github.com/thanos-io/thanos/pkg/tracing/jaeger"
2126
"github.com/thanos-io/thanos/test/e2e/e2ethanos"
22-
23-
"github.com/efficientgo/core/testutil"
24-
"github.com/efficientgo/e2e"
25-
e2eobs "github.com/efficientgo/e2e/observable"
2627
"gopkg.in/yaml.v2"
2728
)
2829

29-
// Test to check if the trace provider works as expected.
30-
func TestJaegerTracing(t *testing.T) {
31-
env, err := e2e.NewDockerEnvironment("e2e-tracing-test")
32-
testutil.Ok(t, err)
33-
t.Cleanup(env.Close)
34-
name := "testing"
35-
newJaegerRunnable := env.Runnable(fmt.Sprintf("jaeger-%s", name)).
36-
WithPorts(
37-
map[string]int{
38-
"http": 16686,
39-
"http.admin": 14269,
40-
"jaeger.thrift-model.proto": 14250,
41-
"jaeger.thrift": 14268,
42-
}).
30+
// jaegerResponse is a minimal representation of the Jaeger /api/traces response.
31+
type jaegerResponse struct {
32+
Data []jaegerTrace `json:"data"`
33+
}
34+
35+
type jaegerTrace struct {
36+
Spans []jaegerSpan `json:"spans"`
37+
}
38+
39+
type jaegerSpan struct {
40+
OperationName string `json:"operationName"`
41+
Tags []jaegerTag `json:"tags"`
42+
}
43+
44+
type jaegerTag struct {
45+
Key string `json:"key"`
46+
Type string `json:"type"`
47+
Value any `json:"value"`
48+
}
49+
50+
// findTag returns the tag with the given key, or nil.
51+
func (s *jaegerSpan) findTag(key string) *jaegerTag {
52+
for i := range s.Tags {
53+
if s.Tags[i].Key == key {
54+
return &s.Tags[i]
55+
}
56+
}
57+
return nil
58+
}
59+
60+
// startJaeger creates and starts a Jaeger all-in-one container.
61+
func startJaeger(t *testing.T, env e2e.Environment, name string) *e2eobs.Observable {
62+
t.Helper()
63+
r := env.Runnable(fmt.Sprintf("jaeger-%s", name)).
64+
WithPorts(map[string]int{
65+
"http": 16686,
66+
"http.admin": 14269,
67+
"jaeger.thrift-model.proto": 14250,
68+
"jaeger.thrift": 14268,
69+
}).
4370
Init(e2e.StartOptions{
4471
Image: "jaegertracing/all-in-one:1.33",
4572
Readiness: e2e.NewHTTPReadinessProbe("http.admin", "/", 200, 200),
4673
})
47-
newJaeger := e2eobs.AsObservable(newJaegerRunnable, "http.admin")
48-
testutil.Ok(t, e2e.StartAndWaitReady(newJaeger))
74+
j := e2eobs.AsObservable(r, "http.admin")
75+
testutil.Ok(t, e2e.StartAndWaitReady(j))
76+
return j
77+
}
4978

50-
jaegerConfig, err := yaml.Marshal(client.TracingConfig{
79+
// jaegerTracingConfig returns a YAML tracing config for the given Jaeger and service name.
80+
func jaegerTracingConfig(jaeger *e2eobs.Observable, serviceName string) string {
81+
cfg, _ := yaml.Marshal(client.TracingConfig{
5182
Type: client.Jaeger,
52-
Config: jaeger.Config{
53-
ServiceName: "thanos-sidecar",
83+
Config: jaegercfg.Config{
84+
ServiceName: serviceName,
5485
SamplerType: "const",
5586
SamplerParam: 1,
56-
Endpoint: "http://" + newJaeger.InternalEndpoint("jaeger.thrift") + "/api/traces",
87+
Endpoint: "http://" + jaeger.InternalEndpoint("jaeger.thrift") + "/api/traces",
5788
},
5889
})
90+
return string(cfg)
91+
}
92+
93+
// queryJaegerSpans fetches spans from Jaeger for the given service and operation,
94+
// retrying until at least one span is found or the context expires.
95+
func queryJaegerSpans(t *testing.T, ctx context.Context, jaegerEndpoint, service, operation string) []jaegerSpan {
96+
t.Helper()
97+
98+
u := fmt.Sprintf("http://%s/api/traces?service=%s&operation=%s&limit=20",
99+
strings.TrimSpace(jaegerEndpoint), service, operation)
100+
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
59101
testutil.Ok(t, err)
60102

61-
prom1, sidecar1 := e2ethanos.NewPrometheusWithJaegerTracingSidecarCustomImage(env, "alone", e2ethanos.DefaultPromConfig("prom-alone", 0, "", "", e2ethanos.LocalPrometheusTarget), "",
62-
e2ethanos.DefaultPrometheusImage(), "", e2ethanos.DefaultImage(), string(jaegerConfig), "")
103+
var spans []jaegerSpan
104+
httpClient := &http.Client{}
105+
106+
testutil.Ok(t, runutil.Retry(5*time.Second, ctx.Done(), func() error {
107+
resp, err := httpClient.Do(req)
108+
if err != nil {
109+
return err
110+
}
111+
defer resp.Body.Close()
112+
113+
if resp.StatusCode != http.StatusOK {
114+
return errors.Errorf("jaeger returned %d", resp.StatusCode)
115+
}
116+
body, err := io.ReadAll(resp.Body)
117+
if err != nil {
118+
return err
119+
}
120+
var jr jaegerResponse
121+
if err := json.Unmarshal(body, &jr); err != nil {
122+
return err
123+
}
124+
if len(jr.Data) == 0 {
125+
return errors.New("no traces returned yet")
126+
}
127+
spans = nil
128+
for _, trace := range jr.Data {
129+
for _, s := range trace.Spans {
130+
if s.OperationName == operation {
131+
spans = append(spans, s)
132+
}
133+
}
134+
}
135+
if len(spans) == 0 {
136+
return errors.Errorf("no spans with operation %q found yet", operation)
137+
}
138+
return nil
139+
}))
140+
return spans
141+
}
142+
143+
// queryJaegerSpansWithTag fetches all traces from Jaeger for the given service
144+
// and returns spans that contain a tag with the given key and a positive numeric
145+
// value. This avoids needing to know the exact operation name (which can vary or
146+
// contain special characters) and skips spans from early requests that returned
147+
// zero results.
148+
func queryJaegerSpansWithTag(t *testing.T, ctx context.Context, jaegerEndpoint, service, tagKey string) []jaegerSpan {
149+
t.Helper()
150+
151+
u := fmt.Sprintf("http://%s/api/traces?service=%s&limit=50",
152+
strings.TrimSpace(jaegerEndpoint), service)
153+
154+
var spans []jaegerSpan
155+
httpClient := &http.Client{}
156+
157+
testutil.Ok(t, runutil.Retry(5*time.Second, ctx.Done(), func() error {
158+
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
159+
if err != nil {
160+
return err
161+
}
162+
resp, err := httpClient.Do(req)
163+
if err != nil {
164+
return err
165+
}
166+
defer resp.Body.Close()
167+
168+
if resp.StatusCode != http.StatusOK {
169+
return errors.Errorf("jaeger returned %d", resp.StatusCode)
170+
}
171+
body, err := io.ReadAll(resp.Body)
172+
if err != nil {
173+
return err
174+
}
175+
var jr jaegerResponse
176+
if err := json.Unmarshal(body, &jr); err != nil {
177+
return err
178+
}
179+
if len(jr.Data) == 0 {
180+
return errors.New("no traces returned yet")
181+
}
182+
spans = nil
183+
for _, trace := range jr.Data {
184+
for _, s := range trace.Spans {
185+
if tag := s.findTag(tagKey); tag != nil && tagNumericValue(tag) > 0 {
186+
spans = append(spans, s)
187+
}
188+
}
189+
}
190+
if len(spans) == 0 {
191+
return errors.Errorf("no spans with tag %q > 0 found yet", tagKey)
192+
}
193+
return nil
194+
}))
195+
return spans
196+
}
197+
198+
// Test to check if the trace provider works as expected.
199+
func TestJaegerTracing(t *testing.T) {
200+
env, err := e2e.NewDockerEnvironment("e2e-tracing-test")
201+
testutil.Ok(t, err)
202+
t.Cleanup(env.Close)
203+
204+
j := startJaeger(t, env, "testing")
205+
206+
prom1, sidecar1 := e2ethanos.NewPrometheusWithJaegerTracingSidecarCustomImage(
207+
env, "alone",
208+
e2ethanos.DefaultPromConfig("prom-alone", 0, "", "", e2ethanos.LocalPrometheusTarget), "",
209+
e2ethanos.DefaultPrometheusImage(), "",
210+
e2ethanos.DefaultImage(), jaegerTracingConfig(j, "thanos-sidecar"), "",
211+
)
63212
testutil.Ok(t, e2e.StartAndWaitReady(prom1, sidecar1))
64213

65-
qb := e2ethanos.NewQuerierBuilder(env, "1", sidecar1.InternalEndpoint("grpc"))
66-
q := qb.WithTracingConfig(fmt.Sprintf(`type: JAEGER
214+
q := e2ethanos.NewQuerierBuilder(env, "1", sidecar1.InternalEndpoint("grpc")).
215+
WithTracingConfig(fmt.Sprintf(`type: JAEGER
67216
config:
68217
sampler_type: const
69218
sampler_param: 1
70219
service_name: thanos-query
71-
endpoint: %s`, "http://"+newJaeger.InternalEndpoint("jaeger.thrift")+"/api/traces")).Init()
220+
endpoint: %s`, "http://"+j.InternalEndpoint("jaeger.thrift")+"/api/traces")).
221+
Init()
72222
testutil.Ok(t, e2e.StartAndWaitReady(q))
73223

74224
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
@@ -84,33 +234,111 @@ config:
84234
},
85235
})
86236

87-
url := "http://" + strings.TrimSpace(newJaeger.Endpoint("http")+"/api/traces?service=thanos-query&operation=proxy.series")
88-
request, err := http.NewRequest("GET", url, nil)
237+
jaegerHTTP := j.Endpoint("http")
238+
239+
// Original assertions: verify traces exist with the expected service names.
240+
spans := queryJaegerSpans(t, ctx, jaegerHTTP, "thanos-query", "proxy.series")
241+
testutil.Assert(t, len(spans) > 0, "expected proxy.series spans")
242+
}
243+
244+
// TestTracingAttributes verifies that the span attributes added by the
245+
// tracing-part-1 branch appear on traces collected in Jaeger.
246+
//
247+
// Attributes under test:
248+
// - series.selector, result.series, result.samples (proxy.series)
249+
// - result.wire_bytes (HTTP server span)
250+
func TestTracingAttributes(t *testing.T) {
251+
env, err := e2e.NewDockerEnvironment("e2e-trace-attrs")
89252
testutil.Ok(t, err)
90-
client := &http.Client{}
253+
t.Cleanup(env.Close)
91254

92-
testutil.Ok(t, runutil.Retry(5*time.Second, ctx.Done(), func() error {
93-
response, err := client.Do(request)
94-
if err != nil {
95-
return err
96-
}
255+
j := startJaeger(t, env, "attrs")
97256

98-
if response.StatusCode != http.StatusOK {
99-
return errors.New("status code not OK")
100-
}
257+
prom, sidecar := e2ethanos.NewPrometheusWithJaegerTracingSidecarCustomImage(
258+
env, "attr",
259+
e2ethanos.DefaultPromConfig("prom-attr", 0, "", "", e2ethanos.LocalPrometheusTarget), "",
260+
e2ethanos.DefaultPrometheusImage(), "",
261+
e2ethanos.DefaultImage(), jaegerTracingConfig(j, "thanos-sidecar"), "",
262+
)
263+
testutil.Ok(t, e2e.StartAndWaitReady(prom, sidecar))
264+
265+
q := e2ethanos.NewQuerierBuilder(env, "1", sidecar.InternalEndpoint("grpc")).
266+
WithTracingConfig(fmt.Sprintf(`type: JAEGER
267+
config:
268+
sampler_type: const
269+
sampler_param: 1
270+
service_name: thanos-query
271+
endpoint: %s`, "http://"+j.InternalEndpoint("jaeger.thrift")+"/api/traces")).
272+
Init()
273+
testutil.Ok(t, e2e.StartAndWaitReady(q))
101274

102-
defer response.Body.Close()
275+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
276+
t.Cleanup(cancel)
103277

104-
body, err := io.ReadAll(response.Body)
105-
testutil.Ok(t, err)
278+
// Wait for the "up" metric to be queryable.
279+
queryAndAssertSeries(t, ctx, q.Endpoint("http"), e2ethanos.QueryUpWithoutInstance, time.Now, promclient.QueryOptions{
280+
Deduplicate: false,
281+
}, []model.Metric{
282+
{
283+
"job": "myself",
284+
"prometheus": "prom-attr",
285+
"replica": "0",
286+
},
287+
})
106288

107-
resp := string(body)
108-
if strings.Contains(resp, `"data":[]`) {
109-
return errors.New("no data returned")
110-
}
289+
jaegerHTTP := j.Endpoint("http")
111290

112-
testutil.Assert(t, strings.Contains(resp, `"serviceName":"thanos-query"`))
113-
testutil.Assert(t, strings.Contains(resp, `"serviceName":"thanos-sidecar"`))
114-
return nil
115-
}))
291+
t.Run("proxy_series_attributes", func(t *testing.T) {
292+
// Find spans with result.series > 0 (skips early requests before data was available).
293+
spans := queryJaegerSpansWithTag(t, ctx, jaegerHTTP, "thanos-query", "result.series")
294+
testutil.Assert(t, len(spans) > 0, "expected spans with result.series > 0")
295+
296+
span := spans[0]
297+
298+
tag := span.findTag("series.selector")
299+
testutil.Assert(t, tag != nil, "series.selector tag should be present")
300+
301+
tag = span.findTag("result.series")
302+
testutil.Assert(t, tag != nil, "result.series tag should be present")
303+
testutil.Assert(t, tagNumericValue(tag) > 0, "result.series should be > 0")
304+
305+
tag = span.findTag("result.samples")
306+
testutil.Assert(t, tag != nil, "result.samples tag should be present")
307+
testutil.Assert(t, tagNumericValue(tag) >= 0, "result.samples should be >= 0")
308+
})
309+
310+
t.Run("http_wire_bytes", func(t *testing.T) {
311+
spans := queryJaegerSpansWithTag(t, ctx, jaegerHTTP, "thanos-query", "result.wire_bytes")
312+
testutil.Assert(t, len(spans) > 0, "expected spans with result.wire_bytes > 0")
313+
314+
span := spans[0]
315+
316+
tag := span.findTag("result.wire_bytes")
317+
testutil.Assert(t, tag != nil, "result.wire_bytes tag should be present")
318+
testutil.Assert(t, tagNumericValue(tag) > 0, "result.wire_bytes should be > 0")
319+
})
320+
}
321+
322+
// tagNumericValue extracts a numeric value from a Jaeger tag.
323+
// Jaeger encodes int64 tags with type "int64" and a string value,
324+
// and JSON numbers may decode as float64 or json.Number.
325+
func tagNumericValue(tag *jaegerTag) float64 {
326+
if tag == nil {
327+
return 0
328+
}
329+
switch v := tag.Value.(type) {
330+
case float64:
331+
return v
332+
case json.Number:
333+
f, _ := v.Float64()
334+
return f
335+
case string:
336+
f, err := strconv.ParseFloat(v, 64)
337+
if err != nil {
338+
return 0
339+
}
340+
return f
341+
default:
342+
return 0
343+
}
116344
}

0 commit comments

Comments
 (0)