@@ -11,9 +11,12 @@ import (
1111 "time"
1212
1313 tastoradocker "github.com/celestiaorg/tastora/framework/docker"
14+ reth "github.com/celestiaorg/tastora/framework/docker/evstack/reth"
1415 spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor"
16+ jaeger "github.com/celestiaorg/tastora/framework/docker/jaeger"
1517 dto "github.com/prometheus/client_model/go"
1618 "github.com/stretchr/testify/require"
19+ "go.uber.org/zap/zaptest"
1720)
1821
1922// TestSpamoorSmoke spins up reth + sequencer and a Spamoor node, starts a few
@@ -22,28 +25,42 @@ func TestSpamoorSmoke(t *testing.T) {
2225 t .Parallel ()
2326
2427 sut := NewSystemUnderTest (t )
25- // Bring up reth + local DA and start sequencer with default settings.
28+ // Prepare a shared docker client and network for Jaeger and reth.
29+ ctx := t .Context ()
2630 dcli , netID := tastoradocker .Setup (t )
27- env := setupCommonEVMEnv (t , sut , dcli , netID )
31+ jcfg := jaeger.Config {Logger : zaptest .NewLogger (t ), DockerClient : dcli , DockerNetworkID : netID }
32+ jg , err := jaeger .New (ctx , jcfg , t .Name (), 0 )
33+ require .NoError (t , err , "failed to create jaeger node" )
34+ t .Cleanup (func () { _ = jg .Remove (t .Context ()) })
35+ require .NoError (t , jg .Start (ctx ), "failed to start jaeger node" )
36+
37+ // Bring up reth + local DA on the same docker network as Jaeger so reth can export traces.
38+ env := setupCommonEVMEnv (t , sut , dcli , netID ,
39+ WithRethOpts (func (b * reth.NodeBuilder ) {
40+ b .WithEnv (
41+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=" + jg .Internal .IngestHTTPEndpoint ()+ "/v1/traces" ,
42+ "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http" ,
43+ "RUST_LOG=info" ,
44+ "OTEL_SDK_DISABLED=false" ,
45+ )
46+ }),
47+ )
2848 sequencerHome := filepath .Join (t .TempDir (), "sequencer" )
2949
30- // In-process OTLP/HTTP collector to capture ev-node spans.
31- collector := newOTLPCollector (t )
32- t .Cleanup (func () {
33- _ = collector .close ()
34- })
50+ // ev-node runs on the host, so use Jaeger's external OTLP/HTTP endpoint.
51+ otlpHTTP := jg .External .IngestHTTPEndpoint ()
3552
36- // Start sequencer with tracing to our collector.
53+ // Start sequencer with tracing to Jaeger collector.
3754 setupSequencerNode (t , sut , sequencerHome , env .SequencerJWT , env .GenesisHash , env .Endpoints ,
3855 "--evnode.instrumentation.tracing=true" ,
39- "--evnode.instrumentation.tracing_endpoint" , collector . endpoint () ,
56+ "--evnode.instrumentation.tracing_endpoint" , otlpHTTP ,
4057 "--evnode.instrumentation.tracing_sample_rate" , "1.0" ,
4158 "--evnode.instrumentation.tracing_service_name" , "ev-node-smoke" ,
4259 )
4360 t .Log ("Sequencer node is up" )
4461
4562 // Start Spamoor within the same Docker network, targeting reth internal RPC.
46- ni , err := env .RethNode .GetNetworkInfo (context . Background () )
63+ ni , err := env .RethNode .GetNetworkInfo (ctx )
4764 require .NoError (t , err , "failed to get network info" )
4865
4966 internalRPC := "http://" + ni .Internal .RPCAddress ()
@@ -55,7 +72,6 @@ func TestSpamoorSmoke(t *testing.T) {
5572 WithRPCHosts (internalRPC ).
5673 WithPrivateKey (TestPrivateKey )
5774
58- ctx := t .Context ()
5975 spNode , err := spBuilder .Build (ctx )
6076 require .NoError (t , err , "failed to build sp node" )
6177
@@ -113,7 +129,8 @@ func TestSpamoorSmoke(t *testing.T) {
113129 t .Cleanup (func () { _ = api .DeleteSpammer (idToDelete ) })
114130 }
115131
116- // Allow additional time to accumulate activity.
132+ // allow spamoor enough time to generate transaction throughput
133+ // so that the expected tracing spans appear in Jaeger.
117134 time .Sleep (60 * time .Second )
118135
119136 // Fetch parsed metrics and print a concise summary.
@@ -122,8 +139,61 @@ func TestSpamoorSmoke(t *testing.T) {
122139 sent := sumCounter (metrics ["spamoor_transactions_sent_total" ])
123140 fail := sumCounter (metrics ["spamoor_transactions_failed_total" ])
124141
125- time .Sleep (2 * time .Second )
126- printCollectedTraceReport (t , collector )
142+ // Verify Jaeger received traces from ev-node.
143+ // Service name is set above via --evnode.instrumentation.tracing_service_name "ev-node-smoke".
144+ traceCtx , cancel := context .WithTimeout (ctx , 3 * time .Minute )
145+ defer cancel ()
146+ ok , err := jg .External .WaitForTraces (traceCtx , "ev-node-smoke" , 1 , 2 * time .Second )
147+ require .NoError (t , err , "error while waiting for Jaeger traces; UI: %s" , jg .External .QueryURL ())
148+ require .True (t , ok , "expected at least one trace in Jaeger; UI: %s" , jg .External .QueryURL ())
149+
150+ // Also wait for traces from ev-reth and print a small sample.
151+ ok , err = jg .External .WaitForTraces (traceCtx , "ev-reth" , 1 , 2 * time .Second )
152+ require .NoError (t , err , "error while waiting for ev-reth traces; UI: %s" , jg .External .QueryURL ())
153+ require .True (t , ok , "expected at least one trace from ev-reth; UI: %s" , jg .External .QueryURL ())
154+
155+ // fetch traces and print reports for both services.
156+ // use a large limit to fetch all traces from the test run.
157+ evNodeTraces , err := jg .External .Traces (traceCtx , "ev-node-smoke" , 10000 )
158+ require .NoError (t , err , "failed to fetch ev-node-smoke traces from Jaeger" )
159+ evNodeSpans := extractSpansFromTraces (evNodeTraces )
160+ printTraceReport (t , "ev-node-smoke" , toTraceSpans (evNodeSpans ))
161+
162+ evRethTraces , err := jg .External .Traces (traceCtx , "ev-reth" , 10000 )
163+ require .NoError (t , err , "failed to fetch ev-reth traces from Jaeger" )
164+ evRethSpans := extractSpansFromTraces (evRethTraces )
165+ printTraceReport (t , "ev-reth" , toTraceSpans (evRethSpans ))
166+
167+ // assert expected ev-node span names are present.
168+ // these spans reliably appear during block production with transactions flowing.
169+ expectedSpans := []string {
170+ "BlockExecutor.ProduceBlock" ,
171+ "BlockExecutor.ApplyBlock" ,
172+ "BlockExecutor.CreateBlock" ,
173+ "BlockExecutor.RetrieveBatch" ,
174+ "Executor.ExecuteTxs" ,
175+ "Executor.SetFinal" ,
176+ "Engine.ForkchoiceUpdated" ,
177+ "Engine.NewPayload" ,
178+ "Engine.GetPayload" ,
179+ "Eth.GetBlockByNumber" ,
180+ "Sequencer.GetNextBatch" ,
181+ "DASubmitter.SubmitHeaders" ,
182+ "DASubmitter.SubmitData" ,
183+ "DA.Submit" ,
184+ }
185+ opNames := make (map [string ]struct {}, len (evNodeSpans ))
186+ for _ , s := range evNodeSpans {
187+ opNames [s .operationName ] = struct {}{}
188+ }
189+ for _ , name := range expectedSpans {
190+ require .Contains (t , opNames , name , "expected span %q not found in ev-node-smoke traces" , name )
191+ }
192+
193+ // ev-reth span names are internal to the Rust OTLP exporter and may change
194+ // across versions, so we only assert that spans were collected at all.
195+ // TODO: check for more specific spans once implemented.
196+ require .NotEmpty (t , evRethSpans , "expected at least one span from ev-reth" )
127197
128198 require .Greater (t , sent , float64 (0 ), "at least one transaction should have been sent" )
129199 require .Zero (t , fail , "no transactions should have failed" )
@@ -165,15 +235,47 @@ func sumCounter(f *dto.MetricFamily) float64 {
165235 }
166236 return sum
167237}
168- func sumGauge (f * dto.MetricFamily ) float64 {
169- if f == nil || f .GetType () != dto .MetricType_GAUGE {
170- return 0
171- }
172- var sum float64
173- for _ , m := range f .GetMetric () {
174- if m .GetGauge () != nil && m .GetGauge ().Value != nil {
175- sum += m .GetGauge ().GetValue ()
238+
239+ // jaegerSpan holds the fields we extract from Jaeger's untyped JSON response.
240+ type jaegerSpan struct {
241+ operationName string
242+ duration float64 // microseconds
243+ }
244+
245+ func (j jaegerSpan ) SpanName () string { return j .operationName }
246+ func (j jaegerSpan ) SpanDuration () time.Duration { return time .Duration (j .duration ) * time .Microsecond }
247+
248+ // extractSpansFromTraces walks Jaeger's []any response and pulls out span operation names and durations.
249+ func extractSpansFromTraces (traces []any ) []jaegerSpan {
250+ var out []jaegerSpan
251+ for _ , t := range traces {
252+ traceMap , ok := t .(map [string ]any )
253+ if ! ok {
254+ continue
255+ }
256+ spans , ok := traceMap ["spans" ].([]any )
257+ if ! ok {
258+ continue
259+ }
260+ for _ , s := range spans {
261+ spanMap , ok := s .(map [string ]any )
262+ if ! ok {
263+ continue
264+ }
265+ name , _ := spanMap ["operationName" ].(string )
266+ dur , _ := spanMap ["duration" ].(float64 )
267+ if name != "" {
268+ out = append (out , jaegerSpan {operationName : name , duration : dur })
269+ }
176270 }
177271 }
178- return sum
272+ return out
273+ }
274+
275+ func toTraceSpans (spans []jaegerSpan ) []traceSpan {
276+ out := make ([]traceSpan , len (spans ))
277+ for i , s := range spans {
278+ out [i ] = s
279+ }
280+ return out
179281}
0 commit comments