3232using System . IO ;
3333using System . Net ;
3434#if ( NET8_0_OR_GREATER )
35+ using System . Diagnostics ;
3536using System . Linq ;
3637using System . Net . Http ;
3738using System . Net . Http . Headers ;
@@ -71,6 +72,8 @@ public static JsonRequest Create(JsonRpcVersion jsonRpcVersion, int id, string m
7172 }
7273 }
7374
75+ public abstract string JsonRPC { get ; }
76+
7477 /// <summary>
7578 /// Unique call id. Can be null in JSON_RPC v2.0, but xapi disallows it.
7679 /// </summary>
@@ -101,6 +104,9 @@ public JsonRequestV1(int id, string method, JToken parameters)
101104 : base ( id , method , parameters )
102105 {
103106 }
107+
108+ [ JsonIgnore ]
109+ public override string JsonRPC => "1.0" ;
104110 }
105111
106112 internal class JsonRequestV2 : JsonRequest
@@ -111,7 +117,7 @@ public JsonRequestV2(int id, string method, JToken parameters)
111117 }
112118
113119 [ JsonProperty ( "jsonrpc" , Required = Required . Always ) ]
114- public string JsonRPC => "2.0" ;
120+ public override string JsonRPC => "2.0" ;
115121 }
116122
117123
@@ -158,6 +164,42 @@ public partial class JsonRpcClient
158164 {
159165 private int _globalId ;
160166
167+ #if ( NET8_0_OR_GREATER )
168+ private static readonly Type ClassType = typeof ( JsonRpcClient ) ;
169+ private static readonly System . Reflection . AssemblyName ClassAssemblyName = ClassType ? . Assembly ? . GetName ( ) ;
170+ private static readonly ActivitySource source = new ActivitySource ( ClassAssemblyName . Name + "." + ClassType ? . FullName , ClassAssemblyName . Version ? . ToString ( ) ) ;
171+
172+ // Follow naming conventions from OpenTelemetry.SemanticConventions
173+ // Not yet on NuGet though:
174+ // dotnet add package OpenTelemetry.SemanticConventions
175+ private static class RpcAttributes
176+ {
177+ public const string AttributeRpcMethod = "rpc.method" ;
178+ public const string AttributeRpcSystem = "rpc.system" ;
179+ public const string AttributeRpcService = "rpc.service" ;
180+ public const string AttributeRpcJsonrpcErrorCode = "rpc.jsonrpc.error_code" ;
181+ public const string AttributeRpcJsonrpcErrorMessage = "rpc.jsonrpc.error_message" ;
182+ public const string AttributeRpcJsonrpcRequestId = "rpc.jsonrpc.request_id" ;
183+ public const string AttributeRpcJsonrpcVersion = "rpc.jsonrpc.version" ;
184+ public const string AttributeRpcMessageType = "rpc.message.type" ;
185+
186+ public static class RpcMessageTypeValues
187+ {
188+ public const string Sent = "SENT" ;
189+ public const string Received = "RECEIVED" ;
190+ }
191+ }
192+
193+ private static class ServerAttributes
194+ {
195+ public const string AttributeServerAddress = "server.address" ;
196+ }
197+
198+ // not part of the SemanticConventions package
199+ private const string ValueJsonRpc = "jsonrpc" ;
200+ private const string EventRpcMessage = "rpc.message" ;
201+ #endif
202+
161203 public JsonRpcClient ( string baseUrl )
162204 {
163205 Url = baseUrl ;
@@ -216,63 +258,98 @@ protected virtual T Rpc<T>(string callName, JToken parameters, JsonSerializer se
216258 // therefore the latter will be done only in DEBUG mode
217259 using ( var postStream = new MemoryStream ( ) )
218260 {
219- using ( var sw = new StreamWriter ( postStream ) )
261+ #if ( NET8_0_OR_GREATER )
262+ // the semantic convention is $package.$service/$method
263+ using ( Activity activity = source . CreateActivity ( "XenAPI/" + callName , ActivityKind . Client ) )
220264 {
265+ activity ? . Start ( ) ;
266+ // Set the fields described in the OpenTelemetry Semantic Conventions:
267+ // https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/
268+ // https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
269+ activity ? . SetTag ( RpcAttributes . AttributeRpcSystem , ValueJsonRpc ) ;
270+ activity ? . SetTag ( ServerAttributes . AttributeServerAddress , new Uri ( Url ) . Host ) ;
271+ activity ? . SetTag ( RpcAttributes . AttributeRpcMethod , callName ) ;
272+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcRequestId , id . ToString ( ) ) ;
273+ #endif
274+ using ( var sw = new StreamWriter ( postStream ) )
275+ {
221276#if DEBUG
222- var settings = CreateSettings ( serializer . Converters ) ;
223- string jsonReq = JsonConvert . SerializeObject ( request , settings ) ;
224- if ( RequestEvent != null )
225- RequestEvent ( jsonReq ) ;
226- sw . Write ( jsonReq ) ;
277+ var settings = CreateSettings ( serializer . Converters ) ;
278+ string jsonReq = JsonConvert . SerializeObject ( request , settings ) ;
279+ if ( RequestEvent != null )
280+ RequestEvent ( jsonReq ) ;
281+ sw . Write ( jsonReq ) ;
227282#else
228- if ( RequestEvent != null )
229- RequestEvent ( callName ) ;
230- serializer . Serialize ( sw , request ) ;
283+ if ( RequestEvent != null )
284+ RequestEvent ( callName ) ;
285+ serializer . Serialize ( sw , request ) ;
231286#endif
232- sw . Flush ( ) ;
233- postStream . Seek ( 0 , SeekOrigin . Begin ) ;
287+ sw . Flush ( ) ;
288+ postStream . Seek ( 0 , SeekOrigin . Begin ) ;
234289
235- using ( var responseStream = new MemoryStream ( ) )
236- {
237- PerformPostRequest ( postStream , responseStream ) ;
238- responseStream . Position = 0 ;
239-
240- using ( var responseReader = new StreamReader ( responseStream ) )
290+ using ( var responseStream = new MemoryStream ( ) )
241291 {
242- switch ( JsonRpcVersion )
292+ PerformPostRequest ( postStream , responseStream ) ;
293+ responseStream . Position = 0 ;
294+
295+ using ( var responseReader = new StreamReader ( responseStream ) )
243296 {
244- case JsonRpcVersion . v2 :
297+ #if ( NET8_0_OR_GREATER )
298+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcVersion , request . JsonRPC ) ;
299+ #endif
300+ switch ( JsonRpcVersion )
301+ {
302+ case JsonRpcVersion . v2 :
245303#if DEBUG
246- string json2 = responseReader . ReadToEnd ( ) ;
247- var res2 = JsonConvert . DeserializeObject < JsonResponseV2 < T > > ( json2 , settings ) ;
304+ string json2 = responseReader . ReadToEnd ( ) ;
305+ var res2 = JsonConvert . DeserializeObject < JsonResponseV2 < T > > ( json2 , settings ) ;
248306#else
249- var res2 = ( JsonResponseV2 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV2 < T > ) ) ;
307+ var res2 = ( JsonResponseV2 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV2 < T > ) ) ;
250308#endif
251- if ( res2 . Error != null )
252- {
253- var descr = new List < string > { res2 . Error . Message } ;
254- descr . AddRange ( res2 . Error . Data . ToObject < string [ ] > ( ) ) ;
255- throw new Failure ( descr ) ;
256- }
257- return res2 . Result ;
258- default :
309+ if ( res2 . Error != null )
310+ {
311+ var descr = new List < string > { res2 . Error . Message } ;
312+ descr . AddRange ( res2 . Error . Data . ToObject < string [ ] > ( ) ) ;
313+ #if ( NET8_0_OR_GREATER )
314+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorCode , res2 . Error . Code ) ;
315+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorMessage , descr ) ;
316+ activity ? . SetStatus ( ActivityStatusCode . Error ) ;
317+ #endif
318+ throw new Failure ( descr ) ;
319+ }
320+ #if ( NET8_0_OR_GREATER )
321+ activity ? . SetStatus ( ActivityStatusCode . Ok ) ;
322+ #endif
323+ return res2 . Result ;
324+ default :
259325#if DEBUG
260- string json1 = responseReader . ReadToEnd ( ) ;
261- var res1 = JsonConvert . DeserializeObject < JsonResponseV1 < T > > ( json1 , settings ) ;
326+ string json1 = responseReader . ReadToEnd ( ) ;
327+ var res1 = JsonConvert . DeserializeObject < JsonResponseV1 < T > > ( json1 , settings ) ;
262328#else
263- var res1 = ( JsonResponseV1 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV1 < T > ) ) ;
329+ var res1 = ( JsonResponseV1 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV1 < T > ) ) ;
264330#endif
265- if ( res1 . Error != null )
266- {
267- var errorArray = res1 . Error . ToObject < string [ ] > ( ) ;
331+ var errorArray = res1 . Error ? . ToObject < string [ ] > ( ) ;
268332 if ( errorArray != null )
333+ {
334+ #if ( NET8_0_OR_GREATER )
335+ activity ? . SetStatus ( ActivityStatusCode . Error ) ;
336+ // we can't be sure whether we'll have a Code here
337+ // the exact format of an error object is not specified in JSONRPC v1
338+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorMessage , errorArray . ToString ( ) ) ;
339+ #endif
269340 throw new Failure ( errorArray ) ;
270- }
271- return res1 . Result ;
341+ }
342+ #if ( NET8_0_OR_GREATER )
343+ activity ? . SetStatus ( ActivityStatusCode . Ok ) ;
344+ #endif
345+ return res1 . Result ;
346+ }
272347 }
273348 }
274349 }
350+ #if ( NET8_0_OR_GREATER )
275351 }
352+ #endif
276353 }
277354 }
278355
@@ -317,6 +394,26 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre
317394 requestMessage . Headers . Add ( header . Key , header . Value ) ;
318395 }
319396
397+ // propagate W3C traceparent and tracestate
398+ // HttpClient would do this automatically on .NET 5,
399+ // and .NET 6 would provide even more control over this: https://blog.ladeak.net/posts/opentelemetry-net6-httpclient
400+ // the caller must ensure that the activity is in W3C format (by inheritance or direct setting)
401+ var activity = Activity . Current ;
402+ if ( activity != null )
403+ {
404+ if ( activity . IdFormat == ActivityIdFormat . W3C )
405+ {
406+ requestMessage . Headers . Add ( "traceparent" , activity . Id ) ;
407+ var state = activity . TraceStateString ;
408+
409+ if ( state ? . Length > 0 )
410+ requestMessage . Headers . Add ( "tracestate" , state ) ;
411+ }
412+
413+ var tags = new ActivityTagsCollection { { RpcAttributes . AttributeRpcMessageType , RpcAttributes . RpcMessageTypeValues . Sent } } ;
414+ activity . AddEvent ( new ActivityEvent ( EventRpcMessage , DateTimeOffset . Now , tags ) ) ;
415+ }
416+
320417 responseMessage = httpClient . SendAsync ( requestMessage ) . Result ;
321418 responseMessage . EnsureSuccessStatusCode ( ) ;
322419
@@ -325,10 +422,16 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre
325422 responseStream . Flush ( ) ;
326423
327424 ResponseHeaders = responseMessage . Headers . ToDictionary ( header => header . Key , header => string . Join ( "," , header . Value ) ) ;
425+
426+ if ( activity != null )
427+ {
428+ var tags = new ActivityTagsCollection { { RpcAttributes . AttributeRpcMessageType , RpcAttributes . RpcMessageTypeValues . Received } } ;
429+ activity . AddEvent ( new ActivityEvent ( EventRpcMessage , DateTimeOffset . Now , tags ) ) ;
430+ }
328431 }
329432 finally
330433 {
331- RequestHeaders = null ;
434+ RequestHeaders = null ;
332435 responseMessage ? . Dispose ( ) ;
333436 requestMessage ? . Dispose ( ) ;
334437 httpClient ? . Dispose ( ) ;
0 commit comments