@@ -9,20 +9,33 @@ import datadog.trace.agent.test.InstrumentationSpecification
99import datadog.trace.bootstrap.instrumentation.api.Tags
1010import spock.lang.Shared
1111
12+ import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
13+ import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
14+
1215class SofaRpcTest extends InstrumentationSpecification {
1316
1417 @Shared
1518 int port = 12201
1619
20+ @Shared
21+ int errorPort = 12202
22+
1723 @Shared
1824 ProviderBootstrap providerBootstrap
1925
26+ @Shared
27+ ProviderBootstrap errorProviderBootstrap
28+
2029 @Shared
2130 GreeterService greeterService
2231
32+ @Shared
33+ FaultyService faultyService
34+
2335 def setupSpec () {
2436 ApplicationConfig appConfig = new ApplicationConfig (). setAppName(" test-server" )
2537
38+ // Happy-path server: Bolt on port 12201
2639 ServerConfig serverConfig =
2740 new ServerConfig ()
2841 .setProtocol(" bolt" )
@@ -39,61 +52,154 @@ class SofaRpcTest extends InstrumentationSpecification {
3952
4053 providerBootstrap = providerConfig. export()
4154
42- ConsumerConfig< GreeterService > consumerConfig =
55+ greeterService =
4356 new ConsumerConfig<GreeterService > ()
4457 .setApplication(new ApplicationConfig (). setAppName(" test-client" ))
4558 .setInterfaceId(GreeterService . name)
4659 .setDirectUrl(" bolt://127.0.0.1:${ port} " )
4760 .setProtocol(" bolt" )
4861 .setRegister(false )
4962 .setSubscribe(false )
63+ .refer()
64+
65+ // Error-path server: Bolt on port 12202, separate interface to avoid registry conflict
66+ ServerConfig errorServerConfig =
67+ new ServerConfig ()
68+ .setProtocol(" bolt" )
69+ .setHost(" 127.0.0.1" )
70+ .setPort(errorPort)
71+
72+ ProviderConfig<FaultyService > errorProviderConfig =
73+ new ProviderConfig<FaultyService > ()
74+ .setApplication(appConfig)
75+ .setInterfaceId(FaultyService . name)
76+ .setRef(new FaultyServiceImpl ())
77+ .setServer(errorServerConfig)
78+ .setRegister(false )
5079
51- greeterService = consumerConfig. refer()
80+ errorProviderBootstrap = errorProviderConfig. export()
81+
82+ faultyService =
83+ new ConsumerConfig<FaultyService > ()
84+ .setApplication(new ApplicationConfig (). setAppName(" test-client" ))
85+ .setInterfaceId(FaultyService . name)
86+ .setDirectUrl(" bolt://127.0.0.1:${ errorPort} " )
87+ .setProtocol(" bolt" )
88+ .setRegister(false )
89+ .setSubscribe(false )
90+ .refer()
5291 }
5392
5493 def cleanupSpec () {
5594 providerBootstrap?. unExport()
95+ errorProviderBootstrap?. unExport()
5696 }
5797
5898 def " client and server spans created for synchronous Bolt RPC call" () {
5999 setup :
60100 String serviceUniqueName = GreeterService . name + " :1.0"
61101
62102 when :
63- String reply = greeterService. sayHello(" World" )
103+ // runUnderTrace gives the client trace 2 spans (caller + sofarpc.request), making it
104+ // unambiguously distinguishable from the 1-span server trace in assertTraces below.
105+ String reply = runUnderTrace(" caller" ) { greeterService. sayHello(" World" ) }
64106
65107 then :
66108 reply == " Hello, World"
67109
68110 and :
69- // Client and server are in the same JVM but use real TCP sockets (Bolt),
70- // so each side produces its own local trace entry.
71111 assertTraces(2 ) {
72- trace(1 ) {
112+ // trace(0): client side — 2 spans [caller, sofarpc.request(client)]
113+ trace(2 ) {
114+ basicSpan(it, " caller" )
73115 span {
74116 operationName " sofarpc.request"
75117 resourceName " ${ serviceUniqueName} /sayHello"
76118 spanType " rpc"
77119 errored false
120+ childOf span(0 )
78121 tags {
79122 " $Tags . RPC_SERVICE " serviceUniqueName
123+ " rpc.system" " sofarpc"
124+ " sofarpc.protocol" " bolt"
80125 " component" " sofarpc-client"
81126 " span.kind" " client"
127+ peerServiceFrom(Tags . RPC_SERVICE )
82128 defaultTags()
83129 }
84130 }
85131 }
132+ // trace(1): server side — 1 span [sofarpc.request(server)], child of client span
86133 trace(1 ) {
87134 span {
88135 operationName " sofarpc.request"
89136 resourceName " ${ serviceUniqueName} /sayHello"
90137 spanType " rpc"
91138 errored false
139+ childOf trace(0 ). get(1 )
92140 tags {
93141 " $Tags . RPC_SERVICE " serviceUniqueName
142+ " rpc.system" " sofarpc"
143+ " sofarpc.protocol" " bolt"
94144 " component" " sofarpc-server"
95145 " span.kind" " server"
96- defaultTags(true ) // distributed root span — parent context propagated via Bolt headers
146+ defaultTags(true )
147+ }
148+ }
149+ }
150+ }
151+ }
152+
153+ def " server error is marked on server span" () {
154+ setup :
155+ String serviceUniqueName = FaultyService . name + " :1.0"
156+
157+ when :
158+ // SOFA RPC Bolt propagates server exceptions back to the client as a SofaRpcException.
159+ // The client-side AbstractCluster.invoke() returns the SofaResponse to the proxy layer,
160+ // which then throws — after our instrumentation's OnMethodExit has already closed the scope.
161+ // So the CLIENT span is not errored; only the SERVER span reflects the error.
162+ faultyService. fail ()
163+
164+ then :
165+ thrown(Exception )
166+
167+ and :
168+ assertTraces(2 ) {
169+ // Traces sorted by root-span start time. Client span starts first (initiates the call),
170+ // so the client trace is trace(0) and the server trace is trace(1).
171+ trace(1 ) {
172+ span {
173+ operationName " sofarpc.request"
174+ resourceName " ${ serviceUniqueName} /fail"
175+ spanType " rpc"
176+ errored false
177+ tags {
178+ " $Tags . RPC_SERVICE " serviceUniqueName
179+ " rpc.system" " sofarpc"
180+ " sofarpc.protocol" " bolt"
181+ " component" " sofarpc-client"
182+ " span.kind" " client"
183+ peerServiceFrom(Tags . RPC_SERVICE )
184+ defaultTags()
185+ }
186+ }
187+ }
188+ trace(1 ) {
189+ span {
190+ operationName " sofarpc.request"
191+ resourceName " ${ serviceUniqueName} /fail"
192+ spanType " rpc"
193+ errored true
194+ childOf trace(0 ). get(0 )
195+ tags {
196+ " $Tags . RPC_SERVICE " serviceUniqueName
197+ " rpc.system" " sofarpc"
198+ " sofarpc.protocol" " bolt"
199+ " component" " sofarpc-server"
200+ " span.kind" " server"
201+ " error.message" { String }
202+ defaultTags(true )
97203 }
98204 }
99205 }
@@ -110,4 +216,17 @@ class SofaRpcTest extends InstrumentationSpecification {
110216 return " Hello, ${ name} "
111217 }
112218 }
219+
220+ interface FaultyService {
221+ // Non-void return type: SOFA RPC Bolt throws SofaRpcException on client side
222+ // when the server returns an error response, which is what we verify in the test.
223+ String fail ()
224+ }
225+
226+ static class FaultyServiceImpl implements FaultyService {
227+ @Override
228+ String fail () {
229+ throw new IllegalStateException (" something went wrong" )
230+ }
231+ }
113232}
0 commit comments