@@ -5,70 +5,220 @@ package e2e_test
55
66import (
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
67216config:
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